syntaur 0.68.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-CE2CIvur.js → _basePickBy-DKk6tHtk.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-BznLQqID.js → _baseUniq-DM-f7DWz.js} +1 -1
  4. package/dashboard/dist/assets/{arc-B6JqtWve.js → arc-ZBlA3YdV.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-P8UCT3rj.js → architectureDiagram-2XIMDMQ5-BUmvtGTF.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-C1ATZKSf.js → blockDiagram-WCTKOSBZ-B3qxWK6s.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-AvN1yayQ.js → c4Diagram-IC4MRINW-BEq_UJO-.js} +1 -1
  8. package/dashboard/dist/assets/channel-fypxffzQ.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-CyYz6mlJ.js → chunk-4BX2VUAB-C-Y9ryMm.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-QyOF7ox_.js → chunk-55IACEB6-CGdtbsjw.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-DVTHM99U.js → chunk-FMBD7UC4-DllxJhUp.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-DQfxaQtT.js → chunk-JSJVCQXG-jjMM8O5F.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-4R1oobH6.js → chunk-KX2RTZJC-B_6BPltQ.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-D8H_7yNS.js → chunk-NQ4KR5QH-D0hJiXHp.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-DLxDUSuo.js → chunk-QZHKN3VN-BCWo4hLS.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-CWuFDkVp.js → chunk-WL4C6EOR-DH_jEAwg.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-1KnjQvtL.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-1KnjQvtL.js +1 -0
  19. package/dashboard/dist/assets/clone-CKMabBhS.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-D23Dy_Za.js → cose-bilkent-S5V4N54A-5ld00TOH.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CaKBk8eh.js → dagre-KLK3FWXG-Cnu6eQWy.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-BAPQPki-.js → diagram-E7M64L7V-_CBBKNP-.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-Cla6qyvn.js → diagram-IFDJBPK2-DE6WjIb1.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-DnTZq_y6.js → diagram-P4PSJMXO-DpW7UzNK.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-yc1_7ebn.js → erDiagram-INFDFZHY-BD2409fE.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-CMt2tF6O.js → flowDiagram-PKNHOUZH-1p0khhFI.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CXiK-5Gp.js → ganttDiagram-A5KZAMGK-B2zFyA4s.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-B4PbW06T.js → gitGraphDiagram-K3NZZRJ6-bH-4YH7h.js} +1 -1
  29. package/dashboard/dist/assets/{graph-CjdFy-q-.js → graph-BT24B6iQ.js} +1 -1
  30. package/dashboard/dist/assets/{index-DlUgV5eO.css → index-BZwPAi8K.css} +1 -1
  31. package/dashboard/dist/assets/index-Cxqr3rQB.js +670 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-BeOWVt7p.js → infoDiagram-LFFYTUFH-CMzP4Hcg.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-LE7swZOH.js → ishikawaDiagram-PHBUUO56-DMEmFC7M.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DG9-sksf.js → journeyDiagram-4ABVD52K-CAtzYQUm.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-7CKAw6eI.js → kanban-definition-K7BYSVSG-d1JVbtvX.js} +1 -1
  36. package/dashboard/dist/assets/{layout-CHChKPuz.js → layout-BVuI38I_.js} +1 -1
  37. package/dashboard/dist/assets/{linear-BWSj4au4.js → linear-Bc8PGMbp.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-CAwQaPqG.js → mermaid.core-UtwFYLNj.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-CtZWMkVN.js → mindmap-definition-YRQLILUH-DHc5RCDj.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CstCoaK7.js → pieDiagram-SKSYHLDU-9anIsdIA.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-DD8zRtaB.js → quadrantDiagram-337W2JSQ-FZ0D9HnU.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BWpKIK3b.js → requirementDiagram-Z7DCOOCP-BkjCvH_u.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-08gqQtFM.js → sankeyDiagram-WA2Y5GQK-DzqwYHDo.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DsfIDBUH.js → sequenceDiagram-2WXFIKYE-BW4g5Ao-.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-Dqzfk_yy.js → stateDiagram-RAJIS63D-D0tKU3Z0.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DxWQhjNO.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DgH_p9jR.js → timeline-definition-YZTLITO2-Bu269QDX.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-7XKqMhq9.js → treemap-KZPCXAKY-BGu_rrva.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-BFtNLBWM.js → vennDiagram-LZ73GAT5-Cx_n5FRZ.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-CQPevMNl.js → xychartDiagram-JWTSCODW-BOJsKV_W.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +83 -46
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +353 -326
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.js +328 -37
  57. package/dist/launch/index.js.map +1 -1
  58. package/package.json +1 -1
  59. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  60. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  61. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  62. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  63. package/dashboard/dist/assets/channel-IZujZyS6.js +0 -1
  64. package/dashboard/dist/assets/classDiagram-VBA2DB6C-ChnJofe3.js +0 -1
  65. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-ChnJofe3.js +0 -1
  66. package/dashboard/dist/assets/clone-JjIbzsqJ.js +0 -1
  67. package/dashboard/dist/assets/index-COOcebN7.js +0 -659
  68. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-D542UPiR.js +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/terminal-schema.ts","../../src/utils/paths.ts","../../src/utils/fs.ts","../../src/templates/config.ts","../../src/utils/timestamp.ts","../../src/utils/fs-migration.ts","../../src/lifecycle/types.ts","../../src/lifecycle/state-machine.ts","../../src/lifecycle/frontmatter.ts","../../src/utils/uuid.ts","../../src/db/events-db.ts","../../src/lifecycle/event-emit.ts","../../src/dashboard/parser.ts","../../src/todos/parser.ts","../../src/lifecycle/linked-todos.ts","../../src/lifecycle/transitions.ts","../../src/lifecycle/index.ts","../../src/utils/hotkeysCatalog.ts","../../src/utils/agents-schema.ts","../../src/utils/slug.ts","../../src/utils/derive-config.ts","../../src/utils/query/fields.ts","../../src/utils/query/evaluate.ts","../../src/utils/query/lexer.ts","../../src/utils/query/parser.ts","../../src/utils/query/index.ts","../../src/utils/fact-registry.ts","../../src/utils/search-schema.ts","../../src/utils/workspace-visibility-schema.ts","../../src/utils/config.ts","../../src/utils/assignment-resolver.ts","../../src/lifecycle/derive.ts","../../src/utils/playbooks.ts","../../src/launch/cwd.ts","../../src/utils/git-worktree.ts","../../src/lifecycle/facts.ts","../../src/search/types.ts","../../src/search/route.ts","../../src/utils/assignment-walk.ts","../../src/search/indexer.ts","../../src/search/fuse-provider.ts","../../src/search/semantic-provider.ts","../../src/search/index.ts","../../src/dashboard/help.ts","../../src/dashboard/session-db.ts","../../src/dashboard/agent-sessions.ts","../../src/dashboard/overviewCopy.ts","../../src/staleness/classify.ts","../../src/dashboard/api.ts","../../src/launch/url.ts","../../src/launch/plan.ts","../../src/launch/launch-prompt.ts","../../src/launch/argv.ts","../../src/tui/launch.ts","../../src/launch/execute.ts","../../src/utils/terminal-probe.ts","../../src/utils/session-id.ts","../../src/utils/process-info.ts","../../src/usage/cwd-extractor.ts","../../src/utils/transcript.ts","../../src/launch/install-detection.ts"],"sourcesContent":["export type TerminalChoice =\n | 'terminal-app'\n | 'iterm'\n | 'ghostty'\n | 'alacritty'\n | 'warp'\n | 'kitty'\n | 'cmux';\n\nexport const TERMINAL_CHOICES: readonly TerminalChoice[] = [\n 'terminal-app',\n 'iterm',\n 'ghostty',\n 'alacritty',\n 'warp',\n 'kitty',\n 'cmux',\n];\n","import { homedir } from 'node:os';\nimport { resolve } from 'node:path';\n\nexport function expandHome(p: string): string {\n if (p.startsWith('~/') || p === '~') {\n return resolve(homedir(), p.slice(2));\n }\n return p;\n}\n\nexport function syntaurRoot(): string {\n const override = process.env.SYNTAUR_HOME;\n if (override && override.length > 0) {\n return resolve(expandHome(override));\n }\n return resolve(homedir(), '.syntaur');\n}\n\nexport function defaultProjectDir(): string {\n return resolve(syntaurRoot(), 'projects');\n}\n\nexport function assignmentsDir(): string {\n return resolve(syntaurRoot(), 'assignments');\n}\n\nexport function serversDir(): string {\n return resolve(syntaurRoot(), 'servers');\n}\n\nexport function playbooksDir(): string {\n return resolve(syntaurRoot(), 'playbooks');\n}\n\nexport function todosDir(): string {\n return resolve(syntaurRoot(), 'todos');\n}\n\nexport function viewPrefsFile(): string {\n return resolve(syntaurRoot(), 'view-prefs.json');\n}\n\nexport function savedViewsFile(): string {\n return resolve(syntaurRoot(), 'saved-views.json');\n}\n\nexport function projectTodosDir(projectsDir: string, projectSlug: string): string {\n return resolve(projectsDir, projectSlug, 'todos');\n}\n\nexport function todoPlanDir(todosDir: string, workspaceOrProject: string, todoId: string): string {\n return resolve(todosDir, 'plans', workspaceOrProject, todoId);\n}\n\n// Bundle plan files live under `plans/<scopeOrProject>/bundles/<bundleId>/`,\n// keeping them disjoint from todo plans (which omit the `bundles/` segment).\nexport function bundlePlanDir(todosDir: string, scopeOrProject: string, bundleId: string): string {\n return resolve(todosDir, 'plans', scopeOrProject, 'bundles', bundleId);\n}\n\n// Bundle storage lives under a `bundles/` subdirectory so the workspace-checklist\n// discovery glob (which scans top-level *.md files in todosDir) does not pick it up.\nexport function bundlesDir(todosDir: string): string {\n return resolve(todosDir, 'bundles');\n}\n\nexport function bundlesPath(todosDir: string): string {\n return resolve(todosDir, 'bundles', 'index.md');\n}\n\nexport function proofDir(assignmentDir: string): string {\n return resolve(assignmentDir, 'proof');\n}\n","import { mkdir, writeFile, readFile, access, rename } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\nexport async function ensureDir(dir: string): Promise<void> {\n await mkdir(dir, { recursive: true });\n}\n\nexport async function fileExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function writeFileSafe(\n filePath: string,\n content: string,\n): Promise<boolean> {\n if (await fileExists(filePath)) {\n return false;\n }\n await ensureDir(dirname(filePath));\n await writeFile(filePath, content, 'utf-8');\n return true;\n}\n\nexport async function writeFileForce(\n filePath: string,\n content: string,\n): Promise<void> {\n const dir = dirname(filePath);\n const tempPath = join(\n dir,\n `.${Math.random().toString(36).slice(2)}.${Date.now()}.tmp`,\n );\n await ensureDir(dir);\n await writeFile(tempPath, content, 'utf-8');\n await rename(tempPath, filePath);\n}\n\nexport type WriteReportStatus =\n | 'written'\n | 'already-current'\n | 'differs-preserved'\n | 'overwritten';\n\n/**\n * Content-aware idempotent write. Unlike `writeFileSafe` (which skips on mere\n * existence), this compares the on-disk content:\n * - missing → write → 'written'\n * - present & equal → no-op → 'already-current'\n * - present & differs → keep (no force)→ 'differs-preserved'\n * - present & differs → overwrite → 'overwritten' (force)\n * Mirrors the skill-install status vocabulary so adapter writes report honestly.\n */\nexport async function writeFileReport(\n filePath: string,\n content: string,\n options: { force?: boolean } = {},\n): Promise<WriteReportStatus> {\n if (!(await fileExists(filePath))) {\n await ensureDir(dirname(filePath));\n await writeFile(filePath, content, 'utf-8');\n return 'written';\n }\n const current = await readFile(filePath, 'utf-8').catch(() => null);\n if (current === content) {\n return 'already-current';\n }\n if (!options.force) {\n return 'differs-preserved';\n }\n await writeFileForce(filePath, content);\n return 'overwritten';\n}\n","export interface ConfigParams {\n defaultProjectDir: string;\n}\n\nexport function renderConfig(params: ConfigParams): string {\n return `---\nversion: \"1.0\"\ndefaultProjectDir: ${params.defaultProjectDir}\nonboarding:\n completed: false\nagentDefaults:\n trustLevel: medium\n autoApprove: false\nbackup:\n repo: null\n categories: projects, playbooks, todos, servers, config\n lastBackup: null\n lastRestore: null\n---\n\n# Syntaur Configuration\n\nGlobal configuration for the Syntaur CLI.\n`;\n}\n","export function nowTimestamp(): string {\n return new Date().toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n}\n","import { readdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport type { Dirent } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { fileExists } from './fs.js';\nimport { nowTimestamp } from './timestamp.js';\n\n/**\n * Filesystem-level migrations for users upgrading from pre-v0.2.0 installs,\n * where the product used \"mission\" terminology. v0.2.0 renamed code but\n * shipped no on-disk migration, leaving user state unreadable by the new\n * scanner. These helpers close that gap.\n *\n * All helpers are idempotent, safe on missing paths, and NEVER delete user\n * files. Legacy files that are no longer read (e.g., per-project agent.md,\n * claude.md) are left untouched.\n */\n\nexport interface ProjectFilesMigrationResult {\n /** Relative paths of files that were renamed (e.g. `ai-chat-v2/mission.md`). */\n renamedProjectFiles: string[];\n /** Project slugs that still have stale agent.md / claude.md files. Reported, not deleted. */\n legacyExtras: string[];\n}\n\nexport interface ConfigMigrationResult {\n /** True if `defaultMissionDir` was renamed to `defaultProjectDir`. */\n renamedField: boolean;\n /** True if the on-disk `<root>/missions` dir was renamed to `<root>/projects`. */\n renamedDir: boolean;\n /** The resolved projects dir after migration (absolute, or null if config absent). */\n resolvedProjectsDir: string | null;\n}\n\n/**\n * Walk each project directory under `projectsDir` and rename\n * `mission.md` → `project.md` when the legacy file is present and the new\n * name isn't. Reports stale per-project `agent.md` / `claude.md` files\n * without touching them.\n *\n * Swallows per-entry errors (e.g., EPERM on a single dir) so one bad\n * project can't block the rest. Never throws.\n */\nexport async function migrateLegacyProjectFiles(\n projectsDir: string,\n): Promise<ProjectFilesMigrationResult> {\n const result: ProjectFilesMigrationResult = {\n renamedProjectFiles: [],\n legacyExtras: [],\n };\n\n if (!(await fileExists(projectsDir))) return result;\n\n let entries: Dirent[];\n try {\n entries = (await readdir(projectsDir, { withFileTypes: true })) as Dirent[];\n } catch {\n return result;\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith('.')) continue;\n\n const projectDir = resolve(projectsDir, entry.name);\n const legacy = resolve(projectDir, 'mission.md');\n const target = resolve(projectDir, 'project.md');\n\n try {\n if ((await fileExists(legacy)) && !(await fileExists(target))) {\n await rename(legacy, target);\n result.renamedProjectFiles.push(`${entry.name}/mission.md`);\n }\n } catch {\n // Swallow per-project errors (permission denied, racing editor, etc).\n continue;\n }\n\n // Surface stale legacy files without deleting them — caller decides how\n // to present (log once at startup).\n for (const stale of ['agent.md', 'claude.md']) {\n try {\n if (await fileExists(resolve(projectDir, stale))) {\n result.legacyExtras.push(`${entry.name}/${stale}`);\n }\n } catch {\n // Ignore.\n }\n }\n }\n\n return result;\n}\n\n/**\n * Set (or insert) a top-level scalar frontmatter field. Mirrors the dashboard\n * `setTopLevelField` writer: replaces the field in place when present, else\n * inserts it just before the closing `---`. Returns the content unchanged if\n * the field is absent and no closing delimiter is found.\n */\nfunction setFrontmatterField(content: string, key: string, value: boolean | string | null): string {\n const formatted = value === null ? 'null' : typeof value === 'boolean' ? String(value) : value;\n const fieldRegex = new RegExp(`^(${key}:)\\\\s*.*$`, 'm');\n if (fieldRegex.test(content)) {\n return content.replace(fieldRegex, `$1 ${formatted}`);\n }\n const closingIdx = content.indexOf('\\n---', 4);\n if (closingIdx === -1) return content;\n return `${content.slice(0, closingIdx)}\\n${key}: ${formatted}${content.slice(closingIdx)}`;\n}\n\n/** Read a top-level scalar field's raw (trimmed) value, or null if absent. */\nfunction readFrontmatterField(content: string, key: string): string | null {\n const match = content.match(new RegExp(`^${key}:\\\\s*(.*)$`, 'm'));\n if (!match) return null;\n const raw = match[1].trim().replace(/^['\"]|['\"]$/g, '');\n return raw === '' || raw === 'null' ? null : raw;\n}\n\nexport interface ArchivedProjectsMigrationResult {\n /** Project slugs reconciled from `statusOverride: archived` to the real flag. */\n reconciled: string[];\n}\n\n/**\n * Reconcile legacy \"archived-as-a-status\" projects into the orthogonal archive\n * flag. Any `project.md` whose frontmatter has `statusOverride: archived` is\n * rewritten to `archived: true` (stamping `archivedAt` if not already set) and\n * its `statusOverride` is cleared, so `archived` is the single source of truth.\n *\n * Idempotent: only rewrites a file when it still carries `statusOverride: archived`.\n * Swallows per-project errors and never throws.\n */\nexport async function migrateLegacyArchivedProjects(\n projectsDir: string,\n): Promise<ArchivedProjectsMigrationResult> {\n const result: ArchivedProjectsMigrationResult = { reconciled: [] };\n\n if (!(await fileExists(projectsDir))) return result;\n\n let entries: Dirent[];\n try {\n entries = (await readdir(projectsDir, { withFileTypes: true })) as Dirent[];\n } catch {\n return result;\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith('.')) continue;\n\n const projectMd = resolve(projectsDir, entry.name, 'project.md');\n try {\n if (!(await fileExists(projectMd))) continue;\n const content = await readFile(projectMd, 'utf-8');\n // Only act on projects whose status override is exactly \"archived\".\n if (readFrontmatterField(content, 'statusOverride') !== 'archived') continue;\n\n let next = setFrontmatterField(content, 'archived', true);\n if (readFrontmatterField(content, 'archivedAt') === null) {\n next = setFrontmatterField(next, 'archivedAt', nowTimestamp());\n }\n next = setFrontmatterField(next, 'statusOverride', null);\n next = setFrontmatterField(next, 'updated', nowTimestamp());\n\n await writeFile(projectMd, next, 'utf-8');\n result.reconciled.push(entry.name);\n } catch {\n // Swallow per-project errors (permission denied, racing editor, etc).\n continue;\n }\n }\n\n return result;\n}\n\n/**\n * Migrate ~/.syntaur/config.md frontmatter and, optionally, the on-disk\n * projects directory, from the pre-v0.2.0 \"mission\" layout.\n *\n * - Renames `defaultMissionDir` → `defaultProjectDir` in frontmatter when\n * the new key isn't already present.\n * - If the resolved projects dir ends in `/missions` AND that dir exists\n * AND its `/projects` sibling does not, renames the directory on disk\n * and updates the config to point at the new path.\n *\n * Only rewrites the config file when an actual change is made.\n */\nexport async function migrateLegacyConfig(\n configPath: string,\n): Promise<ConfigMigrationResult> {\n const result: ConfigMigrationResult = {\n renamedField: false,\n renamedDir: false,\n resolvedProjectsDir: null,\n };\n\n if (!(await fileExists(configPath))) return result;\n\n let content: string;\n try {\n content = await readFile(configPath, 'utf-8');\n } catch {\n return result;\n }\n\n const fmMatch = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?/);\n if (!fmMatch) return result;\n\n const fmBlock = fmMatch[1];\n const afterFm = content.slice(fmMatch[0].length);\n\n // --- Field rename ---\n const missionLineRe = /^(\\s*)defaultMissionDir\\s*:\\s*(.*)$/m;\n const missionLineMatch = fmBlock.match(missionLineRe);\n const hasProjectLine = /^\\s*defaultProjectDir\\s*:/m.test(fmBlock);\n\n let newFmBlock = fmBlock;\n let missionValue: string | null = null;\n if (missionLineMatch) {\n missionValue = missionLineMatch[2].trim();\n if (!hasProjectLine) {\n newFmBlock = fmBlock.replace(\n missionLineRe,\n `$1defaultProjectDir: ${missionValue}`,\n );\n result.renamedField = true;\n } else {\n // Both keys present; strip the legacy one to avoid drift.\n newFmBlock = fmBlock.replace(missionLineRe, '').replace(/\\n{2,}/g, '\\n');\n result.renamedField = true;\n }\n }\n\n // --- Resolve the current projects dir from whatever the frontmatter says. ---\n const projectLineRe = /^\\s*defaultProjectDir\\s*:\\s*(.*)$/m;\n const projectLineMatch = newFmBlock.match(projectLineRe);\n const projectsDirRaw = projectLineMatch\n ? projectLineMatch[1].trim().replace(/^['\"]|['\"]$/g, '')\n : missionValue;\n\n const expand = (p: string): string =>\n p.startsWith('~')\n ? resolve(process.env.HOME ?? '/', p.slice(p.startsWith('~/') ? 2 : 1))\n : p;\n\n let resolvedProjectsDir = projectsDirRaw ? expand(projectsDirRaw) : null;\n\n // --- Directory rename (only if the value still points at a /missions dir). ---\n if (resolvedProjectsDir && resolvedProjectsDir.endsWith('/missions')) {\n const siblingProjectsDir = resolvedProjectsDir.replace(/\\/missions$/, '/projects');\n if (\n (await fileExists(resolvedProjectsDir)) &&\n !(await fileExists(siblingProjectsDir))\n ) {\n try {\n await rename(resolvedProjectsDir, siblingProjectsDir);\n // Update the config line to point at the new dir. Preserve any ~ prefix.\n const newValue = projectsDirRaw!.endsWith('/missions')\n ? projectsDirRaw!.replace(/\\/missions$/, '/projects')\n : siblingProjectsDir;\n newFmBlock = newFmBlock.replace(\n projectLineRe,\n `defaultProjectDir: ${newValue}`,\n );\n resolvedProjectsDir = siblingProjectsDir;\n result.renamedDir = true;\n } catch {\n // If rename fails (permissions, cross-device), leave both config and\n // filesystem alone. Scanner will still hit the legacy dir.\n }\n }\n }\n\n result.resolvedProjectsDir = resolvedProjectsDir;\n\n if (result.renamedField || result.renamedDir) {\n const newContent = `---\\n${newFmBlock.replace(/\\n+$/, '')}\\n---\\n${afterFm.startsWith('\\n') ? afterFm.slice(1) : afterFm}`;\n try {\n await writeFile(configPath, newContent, 'utf-8');\n } catch {\n // If we can't persist the config, revert the flags so the caller\n // doesn't report a fake success.\n result.renamedField = false;\n result.renamedDir = false;\n }\n }\n\n return result;\n}\n\n/**\n * Format a concise summary line for startup logs. Empty string when nothing\n * material happened (caller should skip the log).\n */\nexport function summarizeMigration(\n project: ProjectFilesMigrationResult,\n config?: ConfigMigrationResult,\n): string {\n const parts: string[] = [];\n if (project.renamedProjectFiles.length > 0) {\n const firstThree = project.renamedProjectFiles\n .map((p) => p.split('/')[0])\n .slice(0, 3)\n .join(', ');\n const more =\n project.renamedProjectFiles.length > 3\n ? ` and ${project.renamedProjectFiles.length - 3} more`\n : '';\n parts.push(\n `renamed mission.md → project.md in ${project.renamedProjectFiles.length} project${project.renamedProjectFiles.length === 1 ? '' : 's'} (${firstThree}${more})`,\n );\n }\n if (config?.renamedField) parts.push('updated config defaultMissionDir → defaultProjectDir');\n if (config?.renamedDir) parts.push('renamed projects directory on disk');\n if (project.legacyExtras.length > 0) {\n parts.push(\n `${project.legacyExtras.length} legacy agent.md / claude.md file${project.legacyExtras.length === 1 ? '' : 's'} left in place (no longer read)`,\n );\n }\n return parts.length ? `[syntaur] legacy migration: ${parts.join('; ')}` : '';\n}\n","export type AssignmentStatus = string;\n\nexport type TransitionCommand = string;\n\nexport const DEFAULT_STATUSES = [\n 'draft',\n 'pending',\n 'ready_for_planning',\n 'ready_to_implement',\n 'in_progress',\n 'blocked',\n 'review',\n 'completed',\n 'failed',\n] as const;\n\nexport const DEFAULT_COMMANDS = [\n 'start',\n 'shape',\n 'plan-ready',\n 'implement',\n 'complete',\n 'block',\n 'unblock',\n 'review',\n 'fail',\n 'reopen',\n 'assign',\n] as const;\n\nexport const DEFAULT_TERMINAL_STATUSES: ReadonlySet<string> = new Set([\n 'completed',\n 'failed',\n]);\n\nexport const TERMINAL_STATUSES: ReadonlySet<string> = DEFAULT_TERMINAL_STATUSES;\n\nexport interface ExternalId {\n system: string;\n id: string;\n url: string | null;\n}\n\n/**\n * One row in an assignment's `statusHistory` frontmatter array — an append-only\n * log of status transitions. `at`/`from`/`to` are always present (`from` is null\n * only for the creation/seed entry). `command`/`by` are recorded when known;\n * `reason` is set on `block` transitions. See the Query Language design doc,\n * Piece 1, for the full data-model rationale.\n *\n * Dimension-aware extension (derived-status design v3): `from`/`to` ALWAYS hold\n * the headline status. When the underlying phase and/or disposition dimension\n * changed, the optional `phaseFrom/phaseTo` / `dispositionFrom/dispositionTo`\n * keys record it — so a phase change under an unchanged headline (e.g. progress\n * while blocked) is representable as `from: blocked, to: blocked,\n * phaseFrom: planning, phaseTo: ready_to_implement`. Entries written before the\n * dimension model simply lack the keys and parse unchanged.\n */\nexport interface StatusHistoryEntry {\n at: string;\n from: string | null;\n to: string;\n command: string;\n by: string | null;\n reason?: string;\n phaseFrom?: string | null;\n phaseTo?: string | null;\n dispositionFrom?: string | null;\n dispositionTo?: string | null;\n}\n\n/**\n * Revision-bound plan approval record (derived-status design v3, Piece 5).\n * The derived `planApproved` fact is true iff `file` is still the latest plan\n * revision AND `digest` matches its current content — so a replan or a\n * post-approval edit auto-invalidates the approval.\n */\nexport interface PlanApproval {\n file: string;\n digest: string;\n by: string | null;\n at: string;\n}\n\n/**\n * One attestation record (custom-facts-attestations): \"agent X reviewed\n * revision Y with verdict Z\". One record per (fact, actor) — re-attesting\n * replaces that actor's record. Revision-bound via the binding snapshot:\n * `file`+`digest` for binds:plan (planApproval semantics), `commit` for\n * binds:commit, neither for binds:none. A record is VALID only while its\n * snapshot still matches the live revision; stale records contribute nothing.\n */\nexport interface AttestationRecord {\n fact: string;\n actor: string;\n verdict: 'approved' | 'changes-requested';\n at: string;\n note?: string;\n /** binds:plan snapshot — plan file name + its digest at attest time. */\n file?: string;\n digest?: string;\n /** binds:commit snapshot — workspace HEAD sha at attest time. */\n commit?: string;\n}\n\n/**\n * Sticky manual status override (\"pin\"). Folded into the written headline\n * `status` at recompute time; the un-overridden derived headline travels in\n * API payloads only (divergence display). May not target a terminal status\n * and may not be applied to a terminal assignment.\n */\nexport interface StatusOverride {\n status: string;\n source: string; // 'human' | 'agent:<id>'\n reason: string | null;\n at: string;\n}\n\n/** Disposition dimension values (orthogonal to phase). */\nexport const DISPOSITIONS = ['active', 'blocked', 'parked', 'terminal'] as const;\nexport type Disposition = (typeof DISPOSITIONS)[number];\n\nexport interface Workspace {\n repository: string | null;\n worktreePath: string | null;\n branch: string | null;\n parentBranch: string | null;\n}\n\nexport interface AssignmentFrontmatter {\n id: string;\n slug: string;\n title: string;\n project: string | null;\n type: string | null;\n status: AssignmentStatus;\n priority: 'low' | 'medium' | 'high' | 'critical';\n created: string;\n updated: string;\n assignee: string | null;\n externalIds: ExternalId[];\n statusHistory: StatusHistoryEntry[];\n dependsOn: string[];\n links: string[];\n blockedReason: string | null;\n workspace: Workspace;\n tags: string[];\n archived: boolean;\n archivedAt: string | null;\n archivedReason: string | null;\n // ── derived-status v3 fields ─────────────────────────────────────────────\n /** Cached phase dimension (written by recompute; null pre-migration). */\n phase: string | null;\n /** Cached disposition dimension (written by recompute; null pre-migration). */\n disposition: string | null;\n /** Revision-bound plan approval record; null = not approved. */\n planApproval: PlanApproval | null;\n /** Intentional withhold → disposition: parked. */\n parked: boolean;\n /** Review escalation atom; feeds the review phase rung. */\n reviewRequested: boolean;\n /** Asserted \"implementation has begun\" (worktrees precede planning, so workspaceSet ≠ building). */\n implementationStarted: boolean;\n /** Sticky manual pin; null = no override. */\n override: StatusOverride | null;\n /** Custom asserted fact values (raw scalars keyed by declared name; typed\n * coercion against declarations happens in facts.ts). Absent block → {}. */\n facts: Record<string, string>;\n /** Attestation records, one per (fact, actor). Revision-bound; stale records\n * contribute nothing at compute time. Absent block → []. */\n attestations: AttestationRecord[];\n}\n\nexport interface TransitionResult {\n success: boolean;\n message: string;\n fromStatus: AssignmentStatus;\n toStatus?: AssignmentStatus;\n warnings?: string[];\n}\n","import type { AssignmentStatus, TransitionCommand } from './types.js';\nimport { TERMINAL_STATUSES } from './types.js';\n\n/**\n * Maps a command to its target status. Commands always produce the same\n * target regardless of the current status — workflow enforcement is\n * handled via agent prompting, not code guards.\n */\nexport const DEFAULT_COMMAND_TARGETS = new Map<string, string>([\n ['start', 'in_progress'],\n ['shape', 'ready_for_planning'],\n ['plan-ready', 'ready_to_implement'],\n ['implement', 'in_progress'],\n ['block', 'blocked'],\n ['unblock', 'in_progress'],\n ['review', 'review'],\n ['complete', 'completed'],\n ['fail', 'failed'],\n ['reopen', 'in_progress'],\n]);\n\n/**\n * Built-in `from:command` → `to` map for the default (no custom config) status\n * set. Used by the dashboard to guard which transitions are valid from a given\n * status (see getTargetStatus when a table is passed). The CLI transition path\n * passes no table and stays guard-free via DEFAULT_COMMAND_TARGETS.\n */\nexport const DEFAULT_TRANSITION_TABLE = new Map<string, string>([\n ['pending:start', 'in_progress'],\n ['pending:block', 'blocked'],\n ['draft:shape', 'ready_for_planning'],\n ['draft:start', 'in_progress'],\n ['ready_for_planning:plan-ready', 'ready_to_implement'],\n ['ready_for_planning:start', 'in_progress'],\n ['ready_to_implement:implement', 'in_progress'],\n ['in_progress:block', 'blocked'],\n ['in_progress:review', 'review'],\n ['in_progress:complete', 'completed'],\n ['in_progress:fail', 'failed'],\n ['blocked:unblock', 'in_progress'],\n ['review:start', 'in_progress'],\n ['review:complete', 'completed'],\n ['review:fail', 'failed'],\n ['completed:reopen', 'in_progress'],\n ['failed:reopen', 'in_progress'],\n]);\n\nexport function buildTransitionTable(\n transitions: Array<{ from: string; command: string; to: string }>,\n): Map<string, string> {\n const table = new Map<string, string>();\n for (const t of transitions) {\n table.set(`${t.from}:${t.command}`, t.to);\n }\n return table;\n}\n\nexport function buildCommandTargets(\n transitions: Array<{ from: string; command: string; to: string }>,\n): Map<string, string> {\n const targets = new Map<string, string>();\n for (const t of transitions) {\n targets.set(t.command, t.to);\n }\n return targets;\n}\n\nexport function getTargetStatus(\n _from: AssignmentStatus,\n command: TransitionCommand,\n table?: Map<string, string>,\n): AssignmentStatus | null {\n // No table provided (e.g. the CLI transition path): commands are guard-free —\n // workflow enforcement happens via agent prompting, not code — so a command\n // resolves to its canonical target regardless of the current status.\n if (!table) {\n return DEFAULT_COMMAND_TARGETS.get(command) ?? null;\n }\n // A table was provided (the dashboard passes one — custom, or the built-in\n // DEFAULT_TRANSITION_TABLE): honor `from:command` so only transitions valid\n // from the current status resolve. The kanban inline picker renders these\n // directly and must not offer e.g. `start` on an in_progress card. Both\n // DEFAULT_TRANSITION_TABLE and buildTransitionTable() key by `from:command`;\n // the bare-command lookup is a defensive fallback (no current table uses it).\n return table.get(command) ?? table.get(`${_from}:${command}`) ?? null;\n}\n\n/** @deprecated Guards removed — always returns true for known commands */\nexport function canTransition(\n _from: AssignmentStatus,\n command: TransitionCommand,\n table?: Map<string, string>,\n): boolean {\n return getTargetStatus(_from, command, table) !== null;\n}\n\nexport function isTerminalStatus(\n status: AssignmentStatus,\n terminalSet?: ReadonlySet<string>,\n): boolean {\n return (terminalSet ?? TERMINAL_STATUSES).has(status);\n}\n","import type {\n AssignmentFrontmatter,\n AttestationRecord,\n ExternalId,\n PlanApproval,\n StatusHistoryEntry,\n StatusOverride,\n Workspace,\n} from './types.js';\n\nfunction extractFrontmatter(fileContent: string): [string, string] {\n const match = fileContent.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) {\n throw new Error('No frontmatter found in file. Expected --- delimiters.');\n }\n const frontmatterBlock = match[1];\n const body = fileContent.slice(match[0].length);\n return [frontmatterBlock, body];\n}\n\nfunction parseSimpleValue(raw: string): string | null {\n const trimmed = raw.trim();\n if (trimmed === 'null' || trimmed === '~' || trimmed === '') return null;\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n // Decode the escapes formatYamlValue encodes — round-trip safety for\n // values containing quotes/backslashes (codex code-review finding 4).\n return trimmed.slice(1, -1).replace(/\\\\([\"\\\\])/g, '$1');\n }\n if (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\") && trimmed.length >= 2) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\nfunction parseDependsOn(frontmatter: string): string[] {\n const inlineMatch = frontmatter.match(/^dependsOn:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: string[] = [];\n const blockMatch = frontmatter.match(/^dependsOn:\\s*\\n((?:\\s+-\\s+.*\\n?)*)/m);\n if (blockMatch) {\n const items = blockMatch[1].matchAll(/^\\s+-\\s+(.+)$/gm);\n for (const item of items) {\n results.push(item[1].trim());\n }\n }\n return results;\n}\n\nfunction parseLinks(frontmatter: string): string[] {\n const inlineMatch = frontmatter.match(/^links:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: string[] = [];\n const blockMatch = frontmatter.match(/^links:\\s*\\n((?:\\s+-\\s+.*\\n?)*)/m);\n if (blockMatch) {\n const items = blockMatch[1].matchAll(/^\\s+-\\s+(.+)$/gm);\n for (const item of items) {\n results.push(item[1].trim());\n }\n }\n return results;\n}\n\nfunction parseExternalIds(frontmatter: string): ExternalId[] {\n const inlineMatch = frontmatter.match(/^externalIds:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: ExternalId[] = [];\n const blockMatch = frontmatter.match(\n /^externalIds:\\s*\\n((?:\\s+-\\s+[\\s\\S]*?)(?=^\\w|\\n---))/m,\n );\n if (!blockMatch) return [];\n\n const itemBlocks = blockMatch[1].split(/\\n\\s+-\\s+/).filter(Boolean);\n for (const block of itemBlocks) {\n const lines = block.split('\\n');\n const entry: Record<string, string | null> = {};\n for (const line of lines) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n if (entry['system'] && entry['id']) {\n results.push({\n system: entry['system'],\n id: entry['id'],\n url: entry['url'] || null,\n });\n }\n }\n return results;\n}\n\n/**\n * Parse the `statusHistory` list-of-mappings from a frontmatter string.\n *\n * NOTE on the boundary: `extractFrontmatter` strips the closing `\\n---`, so when\n * `statusHistory` is the LAST frontmatter key there is no trailing `---` and no\n * following top-level key. The `parseExternalIds` regex boundary `(?=^\\w|\\n---)`\n * would silently drop such a block, and `$` under `/m` matches end-of-LINE (which\n * would truncate an entry after its first line). So this uses a robust line-scan:\n * collect blank/indented lines after the header until the first column-0 non-blank\n * line OR end of input. This is end-of-input safe regardless of the `---` delimiter.\n */\nfunction parseStatusHistory(frontmatter: string): StatusHistoryEntry[] {\n if (/^statusHistory:\\s*\\[\\s*\\]/m.test(frontmatter)) return [];\n\n const headerMatch = frontmatter.match(/^statusHistory:\\s*$/m);\n if (!headerMatch) return [];\n\n // Use the regex match offset, NOT indexOf(headerMatch[0]) — an earlier scalar\n // value could contain the substring \"statusHistory:\" (e.g. a title) and shift\n // the start position, dropping the real block.\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = frontmatter.slice(bodyStart);\n\n const bodyLines: string[] = [];\n for (const line of after.split('\\n')) {\n if (line.length === 0) {\n bodyLines.push(line); // blank line — keep scanning (YAML allows blanks in a block)\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break; // column-0 non-blank → block ended\n bodyLines.push(line);\n }\n const body = bodyLines.join('\\n');\n\n const results: StatusHistoryEntry[] = [];\n const itemBlocks = body.split(/\\n\\s+-\\s+/).filter((b) => b.trim().length > 0);\n for (const block of itemBlocks) {\n const entry: Record<string, string | null> = {};\n for (const line of block.split('\\n')) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n // `to` is required; `from` is null only on the seed/create entry.\n if (!entry['to']) continue;\n const result: StatusHistoryEntry = {\n at: entry['at'] ?? '',\n from: entry['from'] ?? null,\n to: entry['to'],\n command: entry['command'] ?? '',\n by: entry['by'] ?? null,\n };\n if (entry['reason'] != null) result.reason = entry['reason'];\n // Dimension-aware optional keys (derived-status v3); absent on old entries.\n if ('phaseFrom' in entry) result.phaseFrom = entry['phaseFrom'];\n if ('phaseTo' in entry) result.phaseTo = entry['phaseTo'];\n if ('dispositionFrom' in entry) result.dispositionFrom = entry['dispositionFrom'];\n if ('dispositionTo' in entry) result.dispositionTo = entry['dispositionTo'];\n results.push(result);\n }\n return results;\n}\n\n/**\n * Parse a flat nested mapping block (`header:` + indented `key: value` lines)\n * into a string map. Returns null when the header is absent or explicitly null.\n * Shared by `planApproval` / `override` parsing; mirrors `parseWorkspace`'s\n * field scanning but generically.\n */\nfunction parseNestedBlock(frontmatter: string, header: string): Record<string, string | null> | null {\n if (new RegExp(`^${header}:\\\\s*(null|~)\\\\s*$`, 'm').test(frontmatter)) return null;\n const headerMatch = frontmatter.match(new RegExp(`^${header}:\\\\s*$`, 'm'));\n if (!headerMatch) return null;\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const after = frontmatter.slice(headerStart + headerMatch[0].length + 1);\n const out: Record<string, string | null> = {};\n for (const line of after.split('\\n')) {\n if (line.length === 0) continue;\n if (line[0] !== ' ' && line[0] !== '\\t') break; // top-level key — block ended\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim();\n if (!key) continue;\n out[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n return Object.keys(out).length > 0 ? out : null;\n}\n\nfunction parsePlanApproval(frontmatter: string): PlanApproval | null {\n const block = parseNestedBlock(frontmatter, 'planApproval');\n if (!block || !block['file'] || !block['digest']) return null;\n return {\n file: block['file'],\n digest: block['digest'],\n by: block['by'] ?? null,\n at: block['at'] ?? '',\n };\n}\n\nfunction parseOverride(frontmatter: string): StatusOverride | null {\n const block = parseNestedBlock(frontmatter, 'override');\n if (!block || !block['status']) return null;\n return {\n status: block['status'],\n source: block['source'] ?? 'human',\n reason: block['reason'] ?? null,\n at: block['at'] ?? '',\n };\n}\n\n/**\n * Parse the `facts:` map (custom asserted fact values). Reuses\n * {@link parseNestedBlock}: absent/null block → `{}`; entries whose value is\n * null (empty / `null` / `~`) are DROPPED; remaining values kept as trimmed\n * strings (parseSimpleValue already trims + strips quotes). Typed coercion\n * against declarations happens in facts.ts — hand-edited garbage degrades there.\n */\nfunction parseFactsMap(frontmatter: string): Record<string, string> {\n const block = parseNestedBlock(frontmatter, 'facts');\n if (!block) return {};\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(block)) {\n if (v === null) continue;\n out[k] = v;\n }\n return out;\n}\n\n/**\n * Parse the `attestations:` record list. Modeled on {@link parseStatusHistory}\n * (same end-of-input-safe line scan). Records missing any required key\n * (fact/actor/verdict/at) or carrying an unknown verdict are dropped.\n */\nfunction parseAttestations(frontmatter: string): AttestationRecord[] {\n if (/^attestations:\\s*\\[\\s*\\]/m.test(frontmatter)) return [];\n\n const headerMatch = frontmatter.match(/^attestations:\\s*$/m);\n if (!headerMatch) return [];\n\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = frontmatter.slice(bodyStart);\n\n const bodyLines: string[] = [];\n for (const line of after.split('\\n')) {\n if (line.length === 0) {\n bodyLines.push(line);\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n bodyLines.push(line);\n }\n const body = bodyLines.join('\\n');\n\n const results: AttestationRecord[] = [];\n const itemBlocks = body.split(/\\n\\s+-\\s+/).filter((b) => b.trim().length > 0);\n for (const block of itemBlocks) {\n const entry: Record<string, string | null> = {};\n for (const line of block.split('\\n')) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n const verdict = entry['verdict'];\n if (!entry['fact'] || !entry['actor'] || !verdict || !entry['at']) continue;\n if (verdict !== 'approved' && verdict !== 'changes-requested') continue;\n const record: AttestationRecord = {\n fact: entry['fact'],\n actor: entry['actor'],\n verdict,\n at: entry['at'],\n };\n if (entry['note'] != null) record.note = entry['note'];\n if (entry['file'] != null) record.file = entry['file'];\n if (entry['digest'] != null) record.digest = entry['digest'];\n if (entry['commit'] != null) record.commit = entry['commit'];\n results.push(record);\n }\n return results;\n}\n\nfunction parseWorkspace(frontmatter: string): Workspace {\n const defaults: Workspace = {\n repository: null,\n worktreePath: null,\n branch: null,\n parentBranch: null,\n };\n\n const fields = ['repository', 'worktreePath', 'branch', 'parentBranch'] as const;\n for (const field of fields) {\n const match = frontmatter.match(new RegExp(`^\\\\s+${field}:\\\\s*(.*)$`, 'm'));\n if (match) {\n defaults[field] = parseSimpleValue(match[1]);\n }\n }\n return defaults;\n}\n\nfunction parseTags(frontmatter: string): string[] {\n const inlineMatch = frontmatter.match(/^tags:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: string[] = [];\n const blockMatch = frontmatter.match(/^tags:\\s*\\n((?:\\s+-\\s+.*\\n?)*)/m);\n if (blockMatch) {\n const items = blockMatch[1].matchAll(/^\\s+-\\s+(.+)$/gm);\n for (const item of items) {\n results.push(item[1].trim());\n }\n }\n return results;\n}\n\nexport function parseAssignmentFrontmatter(fileContent: string): AssignmentFrontmatter {\n const [frontmatter] = extractFrontmatter(fileContent);\n\n function getField(key: string): string | null {\n const match = frontmatter.match(new RegExp(`^${key}:\\\\s*(.*)$`, 'm'));\n if (!match) return null;\n return parseSimpleValue(match[1]);\n }\n\n return {\n id: getField('id') ?? '',\n slug: getField('slug') ?? '',\n title: getField('title') ?? '',\n project: getField('project'),\n type: getField('type'),\n status: getField('status') ?? 'pending',\n priority: (getField('priority') ?? 'medium') as AssignmentFrontmatter['priority'],\n created: getField('created') ?? '',\n updated: getField('updated') ?? '',\n assignee: getField('assignee'),\n externalIds: parseExternalIds(frontmatter),\n statusHistory: parseStatusHistory(frontmatter),\n dependsOn: parseDependsOn(frontmatter),\n links: parseLinks(frontmatter),\n blockedReason: getField('blockedReason'),\n workspace: parseWorkspace(frontmatter),\n tags: parseTags(frontmatter),\n archived: getField('archived') === 'true',\n archivedAt: getField('archivedAt'),\n archivedReason: getField('archivedReason'),\n phase: getField('phase'),\n disposition: getField('disposition'),\n planApproval: parsePlanApproval(frontmatter),\n parked: getField('parked') === 'true',\n reviewRequested: getField('reviewRequested') === 'true',\n implementationStarted: getField('implementationStarted') === 'true',\n override: parseOverride(frontmatter),\n facts: parseFactsMap(frontmatter),\n attestations: parseAttestations(frontmatter),\n };\n}\n\nfunction formatYamlValue(value: string | boolean | null): string {\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (value === null) return 'null';\n // Frontmatter scalars are single-line by contract: flatten embedded\n // newlines rather than corrupting the block (codex code-review finding 4).\n if (/[\\r\\n]/.test(value)) {\n value = value.replace(/\\s*[\\r\\n]+\\s*/g, ' ').trim();\n }\n if (/^\\d{4}-\\d{2}-\\d{2}T/.test(value)) {\n return `\"${value}\"`;\n }\n // Quote YAML keyword/number look-alikes so a literal string \"null\"/\"true\"/\n // \"42\" round-trips as a string, not the YAML scalar.\n if (/^(null|~|true|false|-?\\d+(\\.\\d+)?)$/i.test(value)) {\n return `\"${value}\"`;\n }\n // Quote values containing YAML-special characters that could cause parse\n // issues, OR a value that is itself wrapped in quote chars (e.g.\n // `\"connection refused\"` / `'x'`) — otherwise parseSimpleValue strips the\n // literal surrounding quotes on read and the value does not round-trip.\n if (\n /[:#{}[\\],&*?|>!%@\\`]/.test(value) ||\n /^\\s|\\s$/.test(value) ||\n /^[\"']|[\"']$/.test(value) ||\n value === ''\n ) {\n const escaped = value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n return `\"${escaped}\"`;\n }\n return value;\n}\n\nexport function updateAssignmentFile(\n fileContent: string,\n updates: Partial<\n Pick<\n AssignmentFrontmatter,\n | 'status'\n | 'assignee'\n | 'blockedReason'\n | 'updated'\n | 'archived'\n | 'archivedAt'\n | 'archivedReason'\n | 'phase'\n | 'disposition'\n | 'parked'\n | 'reviewRequested'\n | 'implementationStarted'\n >\n >,\n): string {\n let result = fileContent;\n\n for (const [key, value] of Object.entries(updates)) {\n if (value === undefined) continue;\n const formatted = formatYamlValue(value as string | boolean | null);\n const fieldRegex = new RegExp(`^(${key}:)\\\\s*.*$`, 'm');\n if (fieldRegex.test(result)) {\n result = result.replace(fieldRegex, `$1 ${formatted}`);\n } else {\n // Insert a missing field just before the closing frontmatter delimiter.\n // `indexOf('\\n---', 4)` skips the opening `---`; mirrors setTopLevelField.\n const closeIdx = result.indexOf('\\n---', 4);\n if (closeIdx !== -1) {\n result = `${result.slice(0, closeIdx)}\\n${key}: ${formatted}${result.slice(closeIdx)}`;\n }\n }\n }\n\n return result;\n}\n\n/**\n * Locate the `workspace:` block inside a frontmatter string and return the\n * [start, end) byte offsets of the *body* of that block (lines indented under\n * `workspace:`, excluding the `workspace:` header line itself). Returns null\n * if no `workspace:` block is present.\n */\nfunction findWorkspaceBlock(\n fmBlock: string,\n): { headerStart: number; bodyStart: number; bodyEnd: number } | null {\n const headerMatch = fmBlock.match(/^workspace:\\s*$/m);\n if (!headerMatch) return null;\n // Regex match offset, not indexOf — guards against an earlier scalar value\n // (e.g. a title) containing the substring \"workspace:\". Mirrors\n // findStatusHistoryBlock / parseStatusHistory.\n const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = fmBlock.slice(bodyStart);\n const lines = after.split('\\n');\n let consumed = 0;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line.length === 0) {\n // blank line — consume but keep scanning; YAML allows blanks inside a block\n consumed += line.length + 1;\n continue;\n }\n if (line[0] !== ' ') break; // top-level key — block ended\n consumed += line.length + 1;\n }\n // Trim a trailing newline we counted past EOF\n const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);\n return { headerStart, bodyStart, bodyEnd };\n}\n\n/**\n * Update nested workspace.* fields (repository, worktreePath, branch, parentBranch)\n * in-place. Edits only inside the `workspace:` block — other indented keys\n * with the same name elsewhere in frontmatter are not touched. Preserves\n * field ordering and unknown workspace fields. If the `workspace:` block does\n * not exist, it is appended to the frontmatter.\n */\nexport function updateAssignmentWorkspace(\n fileContent: string,\n partial: Partial<Workspace>,\n): string {\n const fmMatch = fileContent.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) {\n throw new Error('No frontmatter found in assignment file. Expected --- delimiters.');\n }\n\n const fmBlock = fmMatch[2];\n const fields = ['repository', 'worktreePath', 'branch', 'parentBranch'] as const;\n const block = findWorkspaceBlock(fmBlock);\n\n let newFm = fmBlock;\n\n if (block) {\n let body = fmBlock.slice(block.bodyStart, block.bodyEnd);\n for (const field of fields) {\n if (!(field in partial)) continue;\n const value = partial[field] ?? null;\n const formatted = formatYamlValue(value);\n const lineRegex = new RegExp(`^(\\\\s+${field}:)\\\\s*.*$`, 'm');\n if (lineRegex.test(body)) {\n body = body.replace(lineRegex, `$1 ${formatted}`);\n } else {\n const trimmed = body.replace(/\\n+$/, '');\n body = `${trimmed}${trimmed.length > 0 ? '\\n' : ''} ${field}: ${formatted}\\n`;\n }\n }\n newFm =\n fmBlock.slice(0, block.bodyStart) + body + fmBlock.slice(block.bodyEnd);\n } else {\n const lines = ['workspace:'];\n for (const field of fields) {\n const value = field in partial ? (partial[field] ?? null) : null;\n lines.push(` ${field}: ${formatYamlValue(value)}`);\n }\n newFm = `${fmBlock.replace(/\\n+$/, '')}\\n${lines.join('\\n')}`;\n }\n\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;\n}\n\n/**\n * Relabel a status id within an assignment's `statusHistory` — rewrite every\n * entry whose `from`/`to` equals `oldId` to `newId`, WITHOUT appending a new\n * entry or changing any `at`. Used by `syntaur status rename`: a rename is a\n * relabel, not a transition, so it must preserve `statusAge` (no new entry) yet\n * keep historical labels consistent with the new id (so derived `completedAt`\n * stays correct after renaming a terminal status). Scoped to the frontmatter\n * block; `from:`/`to:` keys are unique to statusHistory entries there. Exact\n * value match avoids relabeling a status whose id is a substring of another.\n */\nexport function renameStatusInHistory(\n content: string,\n oldId: string,\n newId: string,\n): string {\n const fmMatch = content.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) return content;\n const esc = oldId.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n // phaseFrom/phaseTo also hold status ids (phase namespace = status definitions),\n // so a rename must relabel them too. Disposition keys hold dimension values\n // (active/blocked/...), not status ids — excluded. The OLD value may be QUOTED\n // when its id is a YAML keyword/number look-alike — match both forms with\n // (\"?)…\\2. The NEW value is (re)serialized via formatYamlValue so it is quoted\n // exactly when needed (e.g. newId `null`/`true`/`42`), instead of reusing the\n // old value's quote state (which dropped/mistyped keyword-id entries on parse).\n const re = new RegExp(`^(\\\\s+(?:from|to|phaseFrom|phaseTo):[ \\\\t]*)(\"?)${esc}\\\\2[ \\\\t]*$`, 'gm');\n const newFm = fmMatch[2].replace(re, (_m, prefix: string) => `${prefix}${formatYamlValue(newId)}`);\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${content.slice(fmMatch[0].length)}`;\n}\n\n/**\n * Locate the `statusHistory:` block (the multi-line list form, not inline `[]`)\n * inside a frontmatter string and return the [bodyStart, bodyEnd) offsets of the\n * block body (the indented `- …` item lines, excluding the header line). Returns\n * null when there is no block header. Mirrors `findWorkspaceBlock`.\n */\nfunction findStatusHistoryBlock(\n fmBlock: string,\n): { headerStart: number; bodyStart: number; bodyEnd: number } | null {\n const headerMatch = fmBlock.match(/^statusHistory:\\s*$/m);\n if (!headerMatch) return null;\n // Regex match offset, not indexOf — guards against an earlier scalar value\n // containing the substring \"statusHistory:\".\n const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = fmBlock.slice(bodyStart);\n const lines = after.split('\\n');\n let consumed = 0;\n for (const line of lines) {\n if (line.length === 0) {\n consumed += line.length + 1;\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break; // top-level key — block ended\n consumed += line.length + 1;\n }\n const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);\n return { headerStart, bodyStart, bodyEnd };\n}\n\nfunction renderStatusHistoryItem(entry: StatusHistoryEntry): string {\n const lines = [\n ` - at: ${formatYamlValue(entry.at)}`,\n ` from: ${formatYamlValue(entry.from)}`,\n ` to: ${formatYamlValue(entry.to)}`,\n ` command: ${formatYamlValue(entry.command)}`,\n ` by: ${formatYamlValue(entry.by)}`,\n ];\n if (entry.reason !== undefined && entry.reason !== null) {\n lines.push(` reason: ${formatYamlValue(entry.reason)}`);\n }\n // Dimension-aware optional keys — rendered only when present, so entries\n // written by plain status transitions stay byte-identical to the v1 format.\n for (const key of ['phaseFrom', 'phaseTo', 'dispositionFrom', 'dispositionTo'] as const) {\n if (entry[key] !== undefined) {\n lines.push(` ${key}: ${formatYamlValue(entry[key] ?? null)}`);\n }\n }\n return lines.join('\\n');\n}\n\n/**\n * Append one entry to an assignment file's `statusHistory` frontmatter list,\n * returning the new file content. Robust to three states:\n * (i) no `statusHistory:` key → create the block before the closing `---`;\n * (ii) inline `statusHistory: []` → convert it to a block with this entry;\n * (iii) existing block → append the item after the last item.\n * This is the single shared serializer used by the lifecycle transition paths and\n * the dashboard write paths. Mirrors the bespoke block handling of\n * `updateAssignmentWorkspace` (scalar `updateAssignmentFile` cannot append to a list).\n */\n/**\n * Set or clear a flat nested mapping block (`header:` + indented `key: value`\n * lines) in assignment frontmatter. `record = null` writes `header: null`\n * (preserving the key so future sets edit in place). Creates the block before\n * the closing `---` when absent. Used for `planApproval` and `override`.\n *\n * Duplicate headers: only the FIRST block is edited — consistent with\n * parseNestedBlock, which also reads the first. This writer never creates a\n * second block, so duplicates can only come from hand edits; doctor territory.\n */\nexport function updateNestedBlock(\n fileContent: string,\n header: string,\n record: Record<string, string | null> | null,\n): string {\n const fmMatch = fileContent.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) {\n throw new Error('No frontmatter found in assignment file. Expected --- delimiters.');\n }\n const fmBlock = fmMatch[2];\n\n const rendered =\n record === null\n ? `${header}: null`\n : [`${header}:`, ...Object.entries(record).map(([k, v]) => ` ${k}: ${formatYamlValue(v)}`)].join('\\n');\n\n // Replace an existing block (header + indented body) or scalar form, else append.\n const headerRe = new RegExp(`^${header}:.*$`, 'm');\n const headerMatch = fmBlock.match(headerRe);\n let newFm: string;\n if (headerMatch) {\n const start = headerMatch.index ?? 0;\n let end = start + headerMatch[0].length;\n // consume any indented body lines following the header; blanks inside a\n // block are scanned past (mirrors findWorkspaceBlock) but only indented\n // lines extend the consumed range, so trailing blanks aren't swallowed.\n const after = fmBlock.slice(end);\n let scanned = 0;\n for (const line of after.split('\\n').slice(1)) {\n if (line.length === 0) {\n scanned += 1 + line.length;\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n scanned += 1 + line.length;\n end += scanned;\n scanned = 0;\n }\n newFm = fmBlock.slice(0, start) + rendered + fmBlock.slice(end);\n } else {\n newFm = `${fmBlock.replace(/\\n+$/, '')}\\n${rendered}`;\n }\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;\n}\n\nexport function updatePlanApproval(fileContent: string, approval: PlanApproval | null): string {\n return updateNestedBlock(\n fileContent,\n 'planApproval',\n approval === null\n ? null\n : { file: approval.file, digest: approval.digest, by: approval.by, at: approval.at },\n );\n}\n\nexport function updateOverride(fileContent: string, override: StatusOverride | null): string {\n return updateNestedBlock(\n fileContent,\n 'override',\n override === null\n ? null\n : { status: override.status, source: override.source, reason: override.reason, at: override.at },\n );\n}\n\n/**\n * Set one custom fact value in the `facts:` map (read-modify-write the whole\n * map through {@link updateNestedBlock}). `value` must already be the CANONICAL\n * serialization (`'true'`/`'false'` / `String(n)`) — the CLI coerces before\n * calling. Dedicated block writer (like {@link updatePlanApproval}); no\n * `updateAssignmentFile` whitelist entry needed.\n */\nexport function updateFactsMap(fileContent: string, name: string, value: string): string {\n const [frontmatter] = extractFrontmatter(fileContent);\n const current = parseFactsMap(frontmatter);\n current[name] = value;\n return updateNestedBlock(fileContent, 'facts', current);\n}\n\nfunction renderAttestationItem(r: AttestationRecord): string {\n const lines = [\n ` - fact: ${formatYamlValue(r.fact)}`,\n ` actor: ${formatYamlValue(r.actor)}`,\n ` verdict: ${formatYamlValue(r.verdict)}`,\n ` at: ${formatYamlValue(r.at)}`,\n ];\n if (r.note !== undefined && r.note !== null) lines.push(` note: ${formatYamlValue(r.note)}`);\n if (r.file !== undefined && r.file !== null) lines.push(` file: ${formatYamlValue(r.file)}`);\n if (r.digest !== undefined && r.digest !== null) lines.push(` digest: ${formatYamlValue(r.digest)}`);\n if (r.commit !== undefined && r.commit !== null) lines.push(` commit: ${formatYamlValue(r.commit)}`);\n return lines.join('\\n');\n}\n\n/**\n * Locate the `attestations:` block (multi-line list form). Mirrors\n * {@link findStatusHistoryBlock}; returns null when no block header.\n */\nfunction findAttestationsBlock(\n fmBlock: string,\n): { headerStart: number; bodyStart: number; bodyEnd: number } | null {\n const headerMatch = fmBlock.match(/^attestations:\\s*$/m);\n if (!headerMatch) return null;\n const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = fmBlock.slice(bodyStart);\n const lines = after.split('\\n');\n let consumed = 0;\n for (const line of lines) {\n if (line.length === 0) {\n consumed += line.length + 1;\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n consumed += line.length + 1;\n }\n const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);\n return { headerStart, bodyStart, bodyEnd };\n}\n\n/**\n * Upsert one attestation record into the `attestations:` frontmatter list:\n * any existing record with the same (fact, actor) is replaced, then the whole\n * block is re-rendered. Robust to no key / inline `[]` / existing block, like\n * {@link appendStatusHistoryEntry}.\n */\nexport function upsertAttestation(fileContent: string, record: AttestationRecord): string {\n const fmMatch = fileContent.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) {\n throw new Error('No frontmatter found in assignment file. Expected --- delimiters.');\n }\n const fmBlock = fmMatch[2];\n\n const existing = parseAttestations(fmBlock);\n const next = existing.filter((r) => !(r.fact === record.fact && r.actor === record.actor));\n next.push(record);\n const rendered = `attestations:\\n${next.map(renderAttestationItem).join('\\n')}`;\n\n // Inline empty list `[]` OR a scalar `null`/`~` form — both parse as \"no\n // records\" but findAttestationsBlock (which requires an empty tail) skips the\n // scalar form, so handle both here to avoid appending a duplicate key.\n const scalarRegex = /^attestations:[ \\t]*(\\[[ \\t]*\\]|null|~)[ \\t]*$/m;\n const block = findAttestationsBlock(fmBlock);\n\n let newFm: string;\n if (scalarRegex.test(fmBlock)) {\n newFm = fmBlock.replace(scalarRegex, rendered);\n } else if (block) {\n const before = fmBlock.slice(0, block.headerStart);\n const rest = fmBlock.slice(block.bodyEnd);\n const sep = rest.length > 0 && !rest.startsWith('\\n') ? '\\n' : '';\n newFm = `${before}${rendered}${sep}${rest}`;\n } else {\n newFm = `${fmBlock.replace(/\\n+$/, '')}\\n${rendered}`;\n }\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;\n}\n\nexport function appendStatusHistoryEntry(\n fileContent: string,\n entry: StatusHistoryEntry,\n): string {\n const fmMatch = fileContent.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) {\n throw new Error('No frontmatter found in assignment file. Expected --- delimiters.');\n }\n const fmBlock = fmMatch[2];\n const item = renderStatusHistoryItem(entry);\n\n const inlineRegex = /^statusHistory:[ \\t]*\\[[ \\t]*\\][ \\t]*$/m;\n const block = findStatusHistoryBlock(fmBlock);\n\n let newFm: string;\n if (inlineRegex.test(fmBlock)) {\n // (ii) inline empty list → block.\n newFm = fmBlock.replace(inlineRegex, `statusHistory:\\n${item}`);\n } else if (block) {\n // (iii) existing block → insert after the last item line.\n const before = fmBlock.slice(0, block.bodyEnd);\n const rest = fmBlock.slice(block.bodyEnd);\n const sep1 = before.endsWith('\\n') ? '' : '\\n';\n const sep2 = rest.length > 0 && !rest.startsWith('\\n') ? '\\n' : '';\n newFm = `${before}${sep1}${item}${sep2}${rest}`;\n } else {\n // (i) no key → append a new block at the end of the frontmatter.\n newFm = `${fmBlock.replace(/\\n+$/, '')}\\nstatusHistory:\\n${item}`;\n }\n\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;\n}\n","import { randomUUID } from 'node:crypto';\n\nexport function generateId(): string {\n return randomUUID();\n}\n","import Database from 'better-sqlite3';\nimport { resolve } from 'node:path';\nimport { syntaurRoot } from '../utils/paths.js';\nimport { generateId } from '../utils/uuid.js';\n\nlet db: Database.Database | null = null;\n\nconst EVENTS_SCHEMA_VERSION = '1';\n\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS events (\n event_id TEXT PRIMARY KEY,\n assignment_id TEXT NOT NULL,\n project_slug TEXT,\n at TEXT NOT NULL,\n actor TEXT NOT NULL,\n type TEXT NOT NULL,\n details TEXT,\n source_key TEXT UNIQUE\n);\nCREATE INDEX IF NOT EXISTS idx_events_assignment_at ON events(assignment_id, at);\nCREATE INDEX IF NOT EXISTS idx_events_at ON events(at);\nCREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);\n`;\n\nexport interface EventRow {\n event_id: string;\n assignment_id: string;\n project_slug: string | null;\n at: string;\n actor: string;\n type: string;\n details: string | null;\n source_key: string | null;\n}\n\n/** Raw row shape for the module-private INSERT. */\ninterface InsertEventRow {\n event_id: string;\n assignment_id: string;\n project_slug: string | null;\n at: string;\n actor: string;\n type: string;\n details: string | null;\n source_key: string | null;\n}\n\n/** Caller-facing input for the single exported writer, `recordEvent`. */\nexport interface RecordEventInput {\n assignmentId: string;\n projectSlug?: string | null;\n type: string;\n /** Object (JSON-stringified before storage) or a pre-stringified string. NEVER pass secrets/raw bodies. */\n details?: unknown;\n actor: string;\n /** UTC ISO 8601. Defaults to now; backfill supplies a historical value. */\n at?: string;\n /** Deterministic key for backfilled events (null for live events; null always inserts). */\n sourceKey?: string | null;\n}\n\nexport interface ListEventsFilters {\n /** Inclusive lower bound on `at` (`at >= since`). */\n since?: string;\n /** Restrict to these event types (`type IN (...)`). */\n types?: string[];\n /** Max rows returned. */\n limit?: number;\n}\n\n/**\n * Initialize the events database. Shares the same `~/.syntaur/syntaur.db`\n * file as `session-db.ts` / `proof-db.ts` but owns its own\n * `events_schema_version` meta row so they can coexist. Mirrors the singleton\n * + WAL + exclusive-migration pattern from `src/db/proof-db.ts`.\n */\nexport function initEventsDb(dbPath?: string): Database.Database {\n if (db) return db;\n\n const finalPath = dbPath ?? resolve(syntaurRoot(), 'syntaur.db');\n db = new Database(finalPath);\n db.pragma('journal_mode = WAL');\n db.exec(SCHEMA_SQL);\n\n db.prepare('INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)').run(\n 'events_schema_version',\n EVENTS_SCHEMA_VERSION,\n );\n\n // No migrations yet for v1, but run an exclusive transaction to set the\n // pattern for v2+ (mirrors proof-db.ts + session-db.ts). Each future\n // versioned step re-reads `events_schema_version` inside the transaction and\n // gates on the prior version, then bumps it — e.g.:\n //\n // const vBeforeV2 = (database\n // .prepare(\"SELECT value FROM meta WHERE key = 'events_schema_version'\")\n // .get() as { value: string } | undefined)?.value;\n // if (vBeforeV2 === '1') {\n // database.exec(`... ; UPDATE meta SET value = '2' WHERE key = 'events_schema_version';`);\n // }\n //\n // EXCLUSIVE serializes concurrent initEventsDb() calls (CLI + dashboard) and\n // rolls back a half-applied upgrade on crash.\n const database = db;\n const runMigrations = database.transaction(() => {\n // future migrations go here\n });\n runMigrations.exclusive();\n\n return db;\n}\n\nexport function getEventsDb(): Database.Database {\n if (!db) {\n throw new Error('Events database not initialized. Call initEventsDb() first.');\n }\n return db;\n}\n\nexport function closeEventsDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\nexport function resetEventsDb(): void {\n db = null;\n}\n\n/**\n * Raw prepared INSERT. MODULE-PRIVATE: the only caller is `recordEvent`.\n * Uses `INSERT OR IGNORE` so a duplicate non-null `source_key` is a silent\n * no-op (SQLite exempts NULL from UNIQUE, so null keys always insert).\n */\nfunction insertEvent(row: InsertEventRow): void {\n const database = getEventsDb();\n database\n .prepare(\n `INSERT OR IGNORE INTO events (event_id, assignment_id, project_slug, at, actor, type, details, source_key)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n row.event_id,\n row.assignment_id,\n row.project_slug,\n row.at,\n row.actor,\n row.type,\n row.details,\n row.source_key,\n );\n}\n\n/**\n * The ONLY exported writer. Best-effort: wraps the whole body in try/catch,\n * logs on failure, and NEVER throws — a logging failure must not break the\n * caller's mutation. Lazily initializes the DB if the singleton isn't open.\n *\n * `event_id` is generated; `at` defaults to now; `details` is JSON-stringified\n * (objects become strings; pre-stringified strings pass through). Callers must\n * never put secrets/raw bodies in `details`.\n */\nexport function recordEvent(input: RecordEventInput): void {\n try {\n if (!db) initEventsDb();\n\n let details: string | null = null;\n if (input.details !== undefined && input.details !== null) {\n details =\n typeof input.details === 'string' ? input.details : JSON.stringify(input.details);\n }\n\n insertEvent({\n event_id: generateId(),\n assignment_id: input.assignmentId,\n project_slug: input.projectSlug ?? null,\n at: input.at ?? new Date().toISOString(),\n actor: input.actor,\n type: input.type,\n details,\n source_key: input.sourceKey ?? null,\n });\n } catch (e) {\n console.warn('[events] failed to record event:', e);\n }\n}\n\n/**\n * List events for an assignment, newest-first (`ORDER BY at DESC`). Optional\n * filters: `since` (`at >= since`), `types` (`type IN (...)`), `limit`.\n */\nexport function listEventsByAssignment(\n assignmentId: string,\n filters?: ListEventsFilters,\n): EventRow[] {\n const database = getEventsDb();\n\n const clauses: string[] = ['assignment_id = ?'];\n const params: Array<string | number> = [assignmentId];\n\n if (filters?.since) {\n clauses.push('at >= ?');\n params.push(filters.since);\n }\n\n if (filters?.types && filters.types.length > 0) {\n const placeholders = filters.types.map(() => '?').join(', ');\n clauses.push(`type IN (${placeholders})`);\n params.push(...filters.types);\n }\n\n let sql = `SELECT event_id, assignment_id, project_slug, at, actor, type, details, source_key\n FROM events\n WHERE ${clauses.join(' AND ')}\n ORDER BY at DESC`;\n\n if (filters?.limit !== undefined) {\n sql += ' LIMIT ?';\n params.push(filters.limit);\n }\n\n return database.prepare(sql).all(...params) as EventRow[];\n}\n\n/**\n * Whether any events exist for an assignment. Used ONLY for the backfill\n * dry-run preview count — NOT as an idempotency gate (idempotency is the\n * `source_key` UNIQUE constraint via `INSERT OR IGNORE`).\n */\nexport function hasEventsForAssignment(assignmentId: string): boolean {\n const database = getEventsDb();\n const row = database\n .prepare('SELECT 1 FROM events WHERE assignment_id = ? LIMIT 1')\n .get(assignmentId);\n return row !== undefined;\n}\n","/**\n * Thin emit layer over the events-db (`recordEvent`, the ONLY writer). It adds\n * two things the raw writer deliberately does not own:\n *\n * 1. A module-level `suppressEvents` switch so migrations (which replay\n * statusHistory writes) do NOT fire live events.\n * 2. A `recordStatusEvent` wrapper carrying the `from !== to` self-guard (R5),\n * so same-status writes (the recompute fact/attestation audit entry) emit\n * no `status-change` event.\n *\n * Every emit ultimately goes through `recordEvent` (R3) — nothing here touches\n * the private `insertEvent`. `recordEvent` is best-effort and never throws, so\n * these helpers are side-effect-isolated: a logging failure never breaks the\n * caller's mutation.\n */\n\nimport { recordEvent, type RecordEventInput } from '../db/events-db.js';\n\n/** When true, ALL emits from this module are no-ops (migrations set this). */\nlet suppressEvents = false;\n\nexport function setSuppressEvents(value: boolean): void {\n suppressEvents = value;\n}\n\nexport function isSuppressingEvents(): boolean {\n return suppressEvents;\n}\n\n/**\n * Run `fn` with event emission suppressed, restoring the PRIOR value in a\n * `finally` (so nested suppression and re-entrancy are safe). Works for sync\n * and async `fn` — an async return value is awaited before restoring.\n */\nexport function withSuppressedEvents<T>(fn: () => T): T {\n const prior = suppressEvents;\n suppressEvents = true;\n try {\n const result = fn();\n if (result instanceof Promise) {\n // Restore only after the async work settles.\n return result.finally(() => {\n suppressEvents = prior;\n }) as unknown as T;\n }\n suppressEvents = prior;\n return result;\n } catch (e) {\n suppressEvents = prior;\n throw e;\n }\n}\n\n/**\n * The ONLY actor mapping (R7): sites pass their own already-resolved `by`; a\n * null/undefined `by` (e.g. the recompute system path) maps to `'system'`.\n */\nexport function resolveActor(by: string | null | undefined): string {\n return by ?? 'system';\n}\n\nexport interface RecordStatusEventInput {\n assignmentId: string;\n projectSlug?: string | null;\n /** UTC ISO 8601; defaults to now inside recordEvent when omitted. */\n at?: string;\n /** Already-resolved actor string (pass through resolveActor at the site). */\n actor: string;\n from: string;\n to: string;\n /** The transition command/cause recorded on the statusHistory entry. */\n command: string;\n}\n\n/**\n * Emit a `status-change` event after a verified status write. Self-guards:\n * - suppression on → no-op (migrations);\n * - `from === to` → no event (R5; same-status audit entries are covered by\n * the underlying fact-set/attestation event).\n * Delegates to `recordEvent` (best-effort, never throws).\n */\nexport function recordStatusEvent(input: RecordStatusEventInput): void {\n if (suppressEvents) return;\n if (input.from === input.to) return;\n recordEvent({\n assignmentId: input.assignmentId,\n projectSlug: input.projectSlug ?? null,\n type: 'status-change',\n actor: input.actor,\n at: input.at,\n details: { from: input.from, to: input.to, command: input.command },\n });\n}\n\n/**\n * Suppression-aware non-status emit. A thin wrapper over `recordEvent` that\n * gates on `suppressEvents` so migrations don't emit. Use for every non-status\n * tracked event (assignee-change, priority-change, archived, restored,\n * plan-approval, fact-set, fact-clear, attestation, comment-added,\n * comment-resolved). `recordEvent` is best-effort and never throws.\n */\nexport function emitEvent(input: RecordEventInput): void {\n if (suppressEvents) return;\n recordEvent(input);\n}\n","/**\n * Generic frontmatter/markdown parser for all Syntaur file types.\n * Pattern copied from src/lifecycle/frontmatter.ts:3-23 (extractFrontmatter + parseSimpleValue).\n */\n\nimport type { AttestationRecord, StatusHistoryEntry } from '../lifecycle/types.js';\n\nexport interface ParsedFile {\n frontmatter: Record<string, string>;\n body: string;\n}\n\n/**\n * Split a markdown file into its frontmatter block and body.\n */\nexport function extractFrontmatter(fileContent: string): [string, string] {\n const match = fileContent.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) {\n return ['', fileContent];\n }\n const frontmatterBlock = match[1];\n const body = fileContent.slice(match[0].length).trim();\n return [frontmatterBlock, body];\n}\n\n/**\n * Parse a simple YAML value, handling null and quoted strings.\n */\nfunction parseSimpleValue(raw: string): string | null {\n const trimmed = raw.trim();\n if (trimmed === 'null' || trimmed === '~' || trimmed === '') return null;\n // Double-quoted: decode the escapes formatYamlValue writes (`\\\"` and `\\\\`), so\n // notes/values containing quotes or backslashes round-trip identically to the\n // lifecycle parser. Single-quoted: literal contents (parity with lifecycle).\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n return trimmed.slice(1, -1).replace(/\\\\([\"\\\\])/g, '$1');\n }\n if (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\") && trimmed.length >= 2) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * Extract a top-level scalar field from frontmatter text.\n */\nexport function getField(frontmatter: string, key: string): string | null {\n const match = frontmatter.match(new RegExp(`^${key}:\\\\s*(.*)$`, 'm'));\n if (!match) return null;\n return parseSimpleValue(match[1]);\n}\n\n/**\n * Extract an indented scalar field (one level deep) from frontmatter text.\n */\nexport function getNestedField(frontmatter: string, parent: string, key: string): string | null {\n const parentRegex = new RegExp(`^${parent}:\\\\s*\\\\n((?:\\\\s+.*\\\\n?)*)`, 'm');\n const parentMatch = frontmatter.match(parentRegex);\n if (!parentMatch) return null;\n const block = parentMatch[1];\n const fieldMatch = block.match(new RegExp(`^\\\\s+${key}:\\\\s*(.*)$`, 'm'));\n if (!fieldMatch) return null;\n return parseSimpleValue(fieldMatch[1]);\n}\n\n/**\n * Parse a YAML list field (e.g., tags, dependsOn, relatedAssignments).\n *\n * Supports the empty inline form `field: []` and the block-list form\n * `field:\\n - a\\n - b`. Does NOT support populated inline arrays\n * (`field: [a, b]`). List items are returned as raw trimmed text; callers\n * that expect quoted-string entries should pass each item through\n * {@link unquoteYamlString}.\n */\nfunction parseListField(frontmatter: string, fieldName: string): string[] {\n const inlineMatch = frontmatter.match(new RegExp(`^${fieldName}:\\\\s*\\\\[\\\\s*\\\\]`, 'm'));\n if (inlineMatch) return [];\n\n const results: string[] = [];\n const blockMatch = frontmatter.match(\n new RegExp(`^${fieldName}:\\\\s*\\\\n((?:\\\\s+-\\\\s+.*\\\\n?)*)`, 'm'),\n );\n if (blockMatch) {\n let item: RegExpExecArray | null;\n const regex = /^\\s+-\\s+(.+)$/gm;\n while ((item = regex.exec(blockMatch[1])) !== null) {\n results.push(item[1].trim());\n }\n }\n return results;\n}\n\n/**\n * Strip a paired surrounding `\"...\"` or `'...'` from a YAML scalar.\n * Mirrors `parseSimpleValue`'s quote handling for list-item entries (which\n * `parseListField` leaves raw).\n */\nfunction unquoteYamlString(value: string): string {\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n return value.slice(1, -1);\n }\n return value;\n}\n\n// --- Project Parser ---\n\nexport interface ParsedProject {\n id: string;\n slug: string;\n title: string;\n archived: boolean;\n archivedAt: string | null;\n archivedReason: string | null;\n statusOverride: string | null;\n created: string;\n updated: string;\n tags: string[];\n workspace: string | null;\n /**\n * Repositories the project spans. Empty array when the field is absent —\n * existing project.md files predate this field, so callers must treat\n * missing as `[]`. Paths with YAML-special characters (spaces, colons,\n * leading dashes) must be quoted in source; quotes are stripped here.\n */\n repositories: string[];\n externalIds: Array<{ system: string; id: string; url: string | null }>;\n body: string;\n}\n\nexport function parseProject(fileContent: string): ParsedProject {\n const [fm, body] = extractFrontmatter(fileContent);\n // Legacy alias: pre-v0.2.0 installs used `mission` as the slug key. The\n // fs-migration helper renames the file but doesn't rewrite user-owned\n // frontmatter. Accept either key.\n const slug = getField(fm, 'slug') ?? getField(fm, 'mission') ?? '';\n return {\n id: getField(fm, 'id') ?? '',\n slug,\n title: getField(fm, 'title') ?? '',\n archived: getField(fm, 'archived') === 'true',\n archivedAt: getField(fm, 'archivedAt'),\n archivedReason: getField(fm, 'archivedReason'),\n statusOverride: getField(fm, 'statusOverride'),\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n tags: parseListField(fm, 'tags'),\n workspace: getField(fm, 'workspace'),\n repositories: parseListField(fm, 'repositories').map(unquoteYamlString),\n externalIds: parseExternalIds(fm),\n body,\n };\n}\n\n// --- Status Parser (for _status.md) ---\n\nexport interface ParsedStatus {\n project: string;\n status: string;\n progress: Record<string, number> & { total: number };\n needsAttention: {\n blockedCount: number;\n failedCount: number;\n openQuestions: number;\n };\n body: string;\n}\n\nexport function parseStatus(fileContent: string): ParsedStatus {\n const [fm, body] = extractFrontmatter(fileContent);\n\n // Dynamically parse progress fields\n const progress: Record<string, number> & { total: number } = { total: 0 };\n const progressMatch = fm.match(/^progress:\\s*\\n((?:\\s+.*\\n?)*)/m);\n if (progressMatch) {\n const lines = progressMatch[1].split('\\n');\n for (const line of lines) {\n const kv = line.match(/^\\s+(\\w+):\\s*(\\d+)/);\n if (kv) {\n progress[kv[1]] = parseInt(kv[2], 10);\n }\n }\n }\n\n return {\n project: getField(fm, 'project') ?? '',\n status: getField(fm, 'status') ?? 'pending',\n progress,\n needsAttention: {\n blockedCount: parseInt(getNestedField(fm, 'needsAttention', 'blockedCount') ?? '0', 10),\n failedCount: parseInt(getNestedField(fm, 'needsAttention', 'failedCount') ?? '0', 10),\n openQuestions: parseInt(getNestedField(fm, 'needsAttention', 'openQuestions') ?? '0', 10),\n },\n body,\n };\n}\n\n// --- Assignment Summary Parser ---\n\nexport interface ParsedAssignmentSummary {\n id: string;\n slug: string;\n title: string;\n status: string;\n priority: string;\n assignee: string | null;\n dependsOn: string[];\n links: string[];\n updated: string;\n}\n\nexport function parseAssignmentSummary(fileContent: string): ParsedAssignmentSummary {\n const [fm] = extractFrontmatter(fileContent);\n return {\n id: getField(fm, 'id') ?? '',\n slug: getField(fm, 'slug') ?? '',\n title: getField(fm, 'title') ?? '',\n status: getField(fm, 'status') ?? 'pending',\n priority: getField(fm, 'priority') ?? 'medium',\n assignee: getField(fm, 'assignee'),\n dependsOn: parseListField(fm, 'dependsOn'),\n links: parseListField(fm, 'links'),\n updated: getField(fm, 'updated') ?? '',\n };\n}\n\n// --- Full Assignment Parser ---\n\nexport interface ParsedAssignmentFull {\n id: string;\n slug: string;\n title: string;\n project: string | null;\n workspaceGroup: string | null;\n type: string | null;\n status: string;\n priority: string;\n assignee: string | null;\n dependsOn: string[];\n links: string[];\n blockedReason: string | null;\n workspace: {\n repository: string | null;\n worktreePath: string | null;\n branch: string | null;\n parentBranch: string | null;\n };\n externalIds: Array<{ system: string; id: string; url: string | null }>;\n statusHistory: StatusHistoryEntry[];\n tags: string[];\n archived: boolean;\n archivedAt: string | null;\n archivedReason: string | null;\n created: string;\n updated: string;\n body: string;\n // ── derived-status v3 fields ─────────────────────────────────────────────\n phase: string | null;\n disposition: string | null;\n parked: boolean;\n reviewRequested: boolean;\n implementationStarted: boolean;\n planApproval: { file: string; digest: string; by: string | null; at: string } | null;\n override: { status: string; source: string; reason: string | null; at: string } | null;\n // ── custom facts + attestations ──────────────────────────────────────────\n /** Custom asserted fact values (raw scalars). Absent block → {}. Parity with\n * the lifecycle parser so buildDerivedDetail's cast feeds computeFacts these. */\n facts: Record<string, string>;\n /** Attestation records (one per fact+actor). Absent block → []. */\n attestations: AttestationRecord[];\n}\n\nfunction parseExternalIds(frontmatter: string): Array<{ system: string; id: string; url: string | null }> {\n const inlineMatch = frontmatter.match(/^externalIds:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: Array<{ system: string; id: string; url: string | null }> = [];\n const blockMatch = frontmatter.match(\n /^externalIds:\\s*\\n((?:\\s+-\\s+[\\s\\S]*?)(?=^\\w|\\n---))/m,\n );\n if (!blockMatch) return [];\n\n const itemBlocks = blockMatch[1].split(/\\n\\s+-\\s+/).filter(Boolean);\n for (const block of itemBlocks) {\n const lines = block.split('\\n');\n const entry: Record<string, string | null> = {};\n for (const line of lines) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n if (entry['system'] && entry['id']) {\n results.push({\n system: entry['system'],\n id: entry['id'],\n url: entry['url'] || null,\n });\n }\n }\n return results;\n}\n\n/**\n * Parse the `statusHistory` list-of-mappings. Parity copy of\n * `src/lifecycle/frontmatter.ts::parseStatusHistory` — uses the same robust\n * line-scan (NOT the `parseExternalIds` regex boundary), because this module's\n * `extractFrontmatter` also strips the closing `\\n---`, so a last-key\n * `statusHistory` block would otherwise be dropped. Keep in sync with the\n * lifecycle parser (dashboard-parser parity test guards this).\n */\nfunction parseStatusHistory(frontmatter: string): StatusHistoryEntry[] {\n if (/^statusHistory:\\s*\\[\\s*\\]/m.test(frontmatter)) return [];\n\n const headerMatch = frontmatter.match(/^statusHistory:\\s*$/m);\n if (!headerMatch) return [];\n\n // Regex match offset, not indexOf — guards against an earlier scalar value\n // containing the substring \"statusHistory:\".\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = frontmatter.slice(bodyStart);\n\n const bodyLines: string[] = [];\n for (const line of after.split('\\n')) {\n if (line.length === 0) {\n bodyLines.push(line);\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n bodyLines.push(line);\n }\n const body = bodyLines.join('\\n');\n\n const results: StatusHistoryEntry[] = [];\n const itemBlocks = body.split(/\\n\\s+-\\s+/).filter((b) => b.trim().length > 0);\n for (const block of itemBlocks) {\n const entry: Record<string, string | null> = {};\n for (const line of block.split('\\n')) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n if (!entry['to']) continue;\n const result: StatusHistoryEntry = {\n at: entry['at'] ?? '',\n from: entry['from'] ?? null,\n to: entry['to'],\n command: entry['command'] ?? '',\n by: entry['by'] ?? null,\n };\n if (entry['reason'] != null) result.reason = entry['reason'];\n // Dimension-aware optional keys (derived-status v3); keep in sync with the\n // lifecycle parser.\n if ('phaseFrom' in entry) result.phaseFrom = entry['phaseFrom'];\n if ('phaseTo' in entry) result.phaseTo = entry['phaseTo'];\n if ('dispositionFrom' in entry) result.dispositionFrom = entry['dispositionFrom'];\n if ('dispositionTo' in entry) result.dispositionTo = entry['dispositionTo'];\n results.push(result);\n }\n return results;\n}\n\n/**\n * Parse the `facts:` map (parity with lifecycle `frontmatter.ts::parseFactsMap`).\n * Absent/null block → `{}`; null-valued entries dropped; values trimmed +\n * unquoted via parseSimpleValue. Keep in sync with the lifecycle parser.\n */\nfunction parseFactsMap(frontmatter: string): Record<string, string> {\n const headerMatch = frontmatter.match(/^facts:\\s*$/m);\n if (!headerMatch) return {};\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const after = frontmatter.slice(headerStart + headerMatch[0].length + 1);\n const out: Record<string, string> = {};\n for (const line of after.split('\\n')) {\n if (line.length === 0) continue;\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim();\n if (!key) continue;\n const value = parseSimpleValue(line.slice(colonIdx + 1));\n if (value === null) continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Parse the `attestations:` record list (parity with lifecycle\n * `frontmatter.ts::parseAttestations` — same robust line-scan). Records missing\n * a required key or with an unknown verdict are dropped. Keep in sync.\n */\nfunction parseAttestations(frontmatter: string): AttestationRecord[] {\n if (/^attestations:\\s*\\[\\s*\\]/m.test(frontmatter)) return [];\n\n const headerMatch = frontmatter.match(/^attestations:\\s*$/m);\n if (!headerMatch) return [];\n\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1;\n const after = frontmatter.slice(bodyStart);\n\n const bodyLines: string[] = [];\n for (const line of after.split('\\n')) {\n if (line.length === 0) {\n bodyLines.push(line);\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n bodyLines.push(line);\n }\n const body = bodyLines.join('\\n');\n\n const results: AttestationRecord[] = [];\n const itemBlocks = body.split(/\\n\\s+-\\s+/).filter((b) => b.trim().length > 0);\n for (const block of itemBlocks) {\n const entry: Record<string, string | null> = {};\n for (const line of block.split('\\n')) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n const verdict = entry['verdict'];\n if (!entry['fact'] || !entry['actor'] || !verdict || !entry['at']) continue;\n if (verdict !== 'approved' && verdict !== 'changes-requested') continue;\n const record: AttestationRecord = {\n fact: entry['fact'],\n actor: entry['actor'],\n verdict,\n at: entry['at'],\n };\n if (entry['note'] != null) record.note = entry['note'];\n if (entry['file'] != null) record.file = entry['file'];\n if (entry['digest'] != null) record.digest = entry['digest'];\n if (entry['commit'] != null) record.commit = entry['commit'];\n results.push(record);\n }\n return results;\n}\n\nexport function parseAssignmentFull(fileContent: string): ParsedAssignmentFull {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n id: getField(fm, 'id') ?? '',\n slug: getField(fm, 'slug') ?? '',\n title: getField(fm, 'title') ?? '',\n project: getField(fm, 'project'),\n workspaceGroup: getField(fm, 'workspaceGroup'),\n type: getField(fm, 'type'),\n status: getField(fm, 'status') ?? 'pending',\n priority: getField(fm, 'priority') ?? 'medium',\n assignee: getField(fm, 'assignee'),\n dependsOn: parseListField(fm, 'dependsOn'),\n links: parseListField(fm, 'links'),\n blockedReason: getField(fm, 'blockedReason'),\n workspace: {\n repository: getNestedField(fm, 'workspace', 'repository'),\n worktreePath: getNestedField(fm, 'workspace', 'worktreePath'),\n branch: getNestedField(fm, 'workspace', 'branch'),\n parentBranch: getNestedField(fm, 'workspace', 'parentBranch'),\n },\n externalIds: parseExternalIds(fm),\n statusHistory: parseStatusHistory(fm),\n tags: parseListField(fm, 'tags'),\n archived: getField(fm, 'archived') === 'true',\n archivedAt: getField(fm, 'archivedAt'),\n archivedReason: getField(fm, 'archivedReason'),\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n phase: getField(fm, 'phase'),\n disposition: getField(fm, 'disposition'),\n parked: getField(fm, 'parked') === 'true',\n reviewRequested: getField(fm, 'reviewRequested') === 'true',\n implementationStarted: getField(fm, 'implementationStarted') === 'true',\n planApproval: (() => {\n const file = getNestedField(fm, 'planApproval', 'file');\n const digest = getNestedField(fm, 'planApproval', 'digest');\n if (!file || !digest) return null;\n return {\n file,\n digest,\n by: getNestedField(fm, 'planApproval', 'by'),\n at: getNestedField(fm, 'planApproval', 'at') ?? '',\n };\n })(),\n override: (() => {\n const status = getNestedField(fm, 'override', 'status');\n if (!status) return null;\n return {\n status,\n source: getNestedField(fm, 'override', 'source') ?? 'human',\n reason: getNestedField(fm, 'override', 'reason'),\n at: getNestedField(fm, 'override', 'at') ?? '',\n };\n })(),\n facts: parseFactsMap(fm),\n attestations: parseAttestations(fm),\n };\n}\n\n// --- Plan Parser ---\n\nexport interface ParsedPlan {\n assignment: string;\n status: string;\n created: string;\n updated: string;\n body: string;\n}\n\nexport function parsePlan(fileContent: string): ParsedPlan {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n assignment: getField(fm, 'assignment') ?? '',\n status: getField(fm, 'status') ?? '',\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Scratchpad Parser ---\n\nexport interface ParsedScratchpad {\n assignment: string;\n updated: string;\n body: string;\n}\n\nexport function parseScratchpad(fileContent: string): ParsedScratchpad {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n assignment: getField(fm, 'assignment') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Handoff Parser ---\n\nexport interface ParsedHandoff {\n assignment: string;\n handoffCount: number;\n updated: string;\n body: string;\n}\n\nexport function parseHandoff(fileContent: string): ParsedHandoff {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n assignment: getField(fm, 'assignment') ?? '',\n handoffCount: parseInt(getField(fm, 'handoffCount') ?? '0', 10),\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Decision Record Parser ---\n\nexport interface ParsedDecisionRecord {\n assignment: string;\n decisionCount: number;\n updated: string;\n body: string;\n}\n\nexport function parseDecisionRecord(fileContent: string): ParsedDecisionRecord {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n assignment: getField(fm, 'assignment') ?? '',\n decisionCount: parseInt(getField(fm, 'decisionCount') ?? '0', 10),\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Comments Parser ---\n\nexport interface ParsedComment {\n id: string;\n timestamp: string;\n author: string;\n type: 'question' | 'note' | 'feedback';\n body: string;\n replyTo?: string;\n resolved?: boolean;\n}\n\nexport interface ParsedComments {\n assignment: string;\n entryCount: number;\n updated: string;\n entries: ParsedComment[];\n body: string;\n}\n\nexport function parseComments(fileContent: string): ParsedComments {\n const [fm, body] = extractFrontmatter(fileContent);\n const entries: ParsedComment[] = [];\n // Split only at REAL comment headers — a `## <id>` line followed by the full\n // metadata prelude (Recorded → Author → Type<valid>) — so a markdown\n // `## Heading` inside a comment body (even one followed by a lone\n // `**Recorded:**` line) doesn't start a phantom section and truncate the body.\n // `\\n\\s*` mirrors the header regex's `^\\s*` tolerance (no-blank/multi-blank).\n const sections = body\n .split(\n /^## (?=[^\\n]*\\n\\s*\\*\\*Recorded:\\*\\*[^\\n]*\\n\\*\\*Author:\\*\\*[^\\n]*\\n\\*\\*Type:\\*\\*\\s*(?:question|note|feedback)\\b)/m,\n )\n .slice(1);\n for (const section of sections) {\n const newlineIdx = section.indexOf('\\n');\n if (newlineIdx === -1) continue;\n const id = section.slice(0, newlineIdx).trim();\n const rest = section.slice(newlineIdx + 1);\n const headerMatch = rest.match(\n /^\\s*\\*\\*Recorded:\\*\\*\\s*(.*)\\n\\*\\*Author:\\*\\*\\s*(.*)\\n\\*\\*Type:\\*\\*\\s*(question|note|feedback)(?:\\n\\*\\*Reply to:\\*\\*\\s*(.*))?(?:\\n\\*\\*Resolved:\\*\\*\\s*(true|false))?\\n+([\\s\\S]*)$/,\n );\n if (!headerMatch) continue;\n const [, timestamp, author, type, replyTo, resolvedStr, entryBody] = headerMatch;\n const entry: ParsedComment = {\n id,\n timestamp: timestamp.trim(),\n author: author.trim(),\n type: type as 'question' | 'note' | 'feedback',\n body: entryBody.trim(),\n };\n if (replyTo) entry.replyTo = replyTo.trim();\n if (resolvedStr) entry.resolved = resolvedStr === 'true';\n entries.push(entry);\n }\n return {\n assignment: getField(fm, 'assignment') ?? '',\n entryCount: parseInt(getField(fm, 'entryCount') ?? '0', 10),\n updated: getField(fm, 'updated') ?? '',\n entries,\n body,\n };\n}\n\n// --- Progress Parser ---\n\nexport interface ProgressEntry {\n timestamp: string;\n body: string;\n}\n\nexport interface ParsedProgress {\n assignment: string;\n entryCount: number;\n updated: string;\n entries: ProgressEntry[];\n body: string;\n}\n\nexport function parseProgress(fileContent: string): ParsedProgress {\n const [fm, body] = extractFrontmatter(fileContent);\n const entries: ProgressEntry[] = [];\n const sections = body.split(/^## /m).slice(1);\n for (const section of sections) {\n const newlineIdx = section.indexOf('\\n');\n if (newlineIdx === -1) continue;\n const timestamp = section.slice(0, newlineIdx).trim();\n const entryBody = section.slice(newlineIdx + 1).trim();\n entries.push({ timestamp, body: entryBody });\n }\n return {\n assignment: getField(fm, 'assignment') ?? '',\n entryCount: parseInt(getField(fm, 'entryCount') ?? '0', 10),\n updated: getField(fm, 'updated') ?? '',\n entries,\n body,\n };\n}\n\n// --- Resource Parser ---\n\nexport interface ParsedResource {\n name: string;\n source: string;\n category: string;\n relatedAssignments: string[];\n created: string;\n updated: string;\n body: string;\n}\n\nexport function parseResource(fileContent: string): ParsedResource {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n name: getField(fm, 'name') ?? '',\n source: getField(fm, 'source') ?? '',\n category: getField(fm, 'category') ?? '',\n relatedAssignments: parseListField(fm, 'relatedAssignments'),\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Memory Parser ---\n\nexport interface ParsedMemory {\n name: string;\n source: string;\n scope: string;\n sourceAssignment: string | null;\n relatedAssignments: string[];\n tags: string[];\n created: string;\n updated: string;\n body: string;\n}\n\nexport function parseMemory(fileContent: string): ParsedMemory {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n name: getField(fm, 'name') ?? '',\n source: getField(fm, 'source') ?? '',\n scope: getField(fm, 'scope') ?? '',\n sourceAssignment: getField(fm, 'sourceAssignment'),\n relatedAssignments: parseListField(fm, 'relatedAssignments'),\n tags: parseListField(fm, 'tags'),\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Playbook Parser ---\n\nexport interface ParsedPlaybook {\n slug: string;\n name: string;\n description: string;\n whenToUse: string;\n created: string;\n updated: string;\n tags: string[];\n body: string;\n}\n\nexport function parsePlaybook(fileContent: string): ParsedPlaybook {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n slug: getField(fm, 'slug') ?? '',\n name: getField(fm, 'name') ?? '',\n description: getField(fm, 'description') ?? '',\n whenToUse: getField(fm, 'when_to_use') ?? '',\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n tags: parseListField(fm, 'tags'),\n body,\n };\n}\n\n// --- Mermaid Graph Extractor ---\n\n/**\n * Extract the mermaid code block from _status.md body content.\n * Returns null if no mermaid block is found.\n */\nexport function extractMermaidGraph(body: string): string | null {\n const match = body.match(/```mermaid\\n([\\s\\S]*?)```/);\n return match ? match[1].trim() : null;\n}\n","import { randomBytes } from 'node:crypto';\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { extractFrontmatter, getField } from '../dashboard/parser.js';\nimport { ensureDir, fileExists, writeFileForce } from '../utils/fs.js';\nimport type {\n TodoItem,\n TodoChecklist,\n TodoStatus,\n ArchiveInterval,\n LogEntry,\n TodoLog,\n} from './types.js';\n\n// --- Short ID ---\n\nexport function generateShortId(): string {\n return randomBytes(2).toString('hex');\n}\n\nexport function generateUniqueId(existingIds: Set<string>): string {\n let id = generateShortId();\n let attempts = 0;\n while (existingIds.has(id) && attempts < 100) {\n id = generateShortId();\n attempts++;\n }\n return id;\n}\n\n// --- Checklist parsing ---\n\nconst ITEM_REGEX = /^- \\[([ x!]|>[^\\]]*)\\]\\s+(.+)$/;\nconst ID_REGEX = /\\[t:([a-f0-9]{4})\\]/;\nconst TAG_REGEX = /#([a-zA-Z0-9_-]+)/g;\n// Meta token follows `[t:<id>]` and looks like `<key=value;key=value;...>`.\n// Anchored at end of line. Recognized keys: b (branch), w (worktreePath),\n// c (createdAt), u (updatedAt), p (planDir), l (linkedAssignmentId),\n// lr (linkedAssignmentRef). Unknown keys are dropped.\nconst META_TOKEN_REGEX = /\\[t:[a-f0-9]{4}\\]\\s+<([^>]*)>\\s*$/;\nconst META_ENCODE_CHARS = ['%', '<', '>', '[', ']', '=', ';', '\\n', '\\r'];\n\n// A tag is stored inline as `#<tag>` on a single checklist line and the parser\n// only recognizes the class below (see TAG_REGEX). A tag containing whitespace,\n// a newline, or `#` would corrupt the line — splitting it, inventing tags, or\n// dropping the `[t:id]` + metadata on the next parse. Validate at every write\n// entry so such a tag is never serialized (reject, never silently sanitize).\nconst VALID_TAG_REGEX = /^[a-zA-Z0-9_-]+$/;\n\nexport function isValidTag(tag: unknown): tag is string {\n return typeof tag === 'string' && VALID_TAG_REGEX.test(tag);\n}\n\nexport function assertValidTags(tags: readonly unknown[]): void {\n for (const t of tags) {\n if (!isValidTag(t)) {\n throw new Error(\n `Invalid tag ${JSON.stringify(t)}: tags may contain only letters, digits, '-' and '_' (no spaces, newlines, or '#').`,\n );\n }\n }\n}\n\nexport function encodeMetaValue(value: string): string {\n let out = '';\n for (const ch of value) {\n if (META_ENCODE_CHARS.includes(ch)) {\n out += '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0');\n } else {\n out += ch;\n }\n }\n return out;\n}\n\nexport function decodeMetaValue(value: string): string {\n return value.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) =>\n String.fromCharCode(parseInt(hex, 16)),\n );\n}\n\ninterface MetaFields {\n branch: string | null;\n worktreePath: string | null;\n createdAt: string | null;\n updatedAt: string | null;\n planDir: string | null;\n linkedAssignmentId: string | null;\n linkedAssignmentRef: string | null;\n bundleId: string | null;\n}\n\nfunction emptyMetaFields(): MetaFields {\n return {\n branch: null,\n worktreePath: null,\n createdAt: null,\n updatedAt: null,\n planDir: null,\n linkedAssignmentId: null,\n linkedAssignmentRef: null,\n bundleId: null,\n };\n}\n\nexport function parseMetaToken(line: string): MetaFields {\n const match = line.match(META_TOKEN_REGEX);\n if (!match) return emptyMetaFields();\n const body = match[1];\n if (!body) return emptyMetaFields();\n const fields = emptyMetaFields();\n for (const pair of body.split(';')) {\n const trimmed = pair.trim();\n if (!trimmed) continue;\n const eq = trimmed.indexOf('=');\n if (eq < 0) continue;\n const key = trimmed.slice(0, eq).trim();\n const rawValue = trimmed.slice(eq + 1);\n const value = decodeMetaValue(rawValue);\n switch (key) {\n case 'b': fields.branch = value; break;\n case 'w': fields.worktreePath = value; break;\n case 'c': fields.createdAt = value; break;\n case 'u': fields.updatedAt = value; break;\n case 'p': fields.planDir = value; break;\n case 'l': fields.linkedAssignmentId = value; break;\n case 'lr': fields.linkedAssignmentRef = value; break;\n case 'bn': fields.bundleId = value; break;\n }\n }\n return fields;\n}\n\nexport function serializeMetaToken(item: TodoItem): string {\n const pairs: string[] = [];\n if (item.branch !== null) pairs.push(`b=${encodeMetaValue(item.branch)}`);\n if (item.worktreePath !== null) pairs.push(`w=${encodeMetaValue(item.worktreePath)}`);\n if (item.createdAt !== null) pairs.push(`c=${encodeMetaValue(item.createdAt)}`);\n if (item.updatedAt !== null) pairs.push(`u=${encodeMetaValue(item.updatedAt)}`);\n if (item.planDir !== null) pairs.push(`p=${encodeMetaValue(item.planDir)}`);\n if (item.linkedAssignmentId !== null) pairs.push(`l=${encodeMetaValue(item.linkedAssignmentId)}`);\n if (item.linkedAssignmentRef !== null) pairs.push(`lr=${encodeMetaValue(item.linkedAssignmentRef)}`);\n if (item.bundleId !== null) pairs.push(`bn=${encodeMetaValue(item.bundleId)}`);\n if (pairs.length === 0) return '';\n return `<${pairs.join(';')}>`;\n}\n\nfunction parseStatus(marker: string): { status: TodoStatus; session: string | null } {\n if (marker === ' ') return { status: 'open', session: null };\n if (marker === 'x') return { status: 'completed', session: null };\n if (marker === '!') return { status: 'blocked', session: null };\n if (marker.startsWith('>:')) return { status: 'in_progress', session: marker.slice(2) };\n if (marker === '>') return { status: 'in_progress', session: null };\n return { status: 'open', session: null };\n}\n\nfunction sanitizeSession(session: string): string {\n // Strip characters that would break the markdown checkbox syntax\n return session.replace(/[\\[\\]]/g, '');\n}\n\nfunction statusToMarker(item: TodoItem): string {\n switch (item.status) {\n case 'open':\n return ' ';\n case 'completed':\n return 'x';\n case 'blocked':\n return '!';\n case 'in_progress':\n return item.session ? `>:${sanitizeSession(item.session)}` : '>';\n }\n}\n\n/**\n * Escape backslash-special characters in a todo description so that prose\n * containing `#`, `[`, or `\\` is never mistaken for a structural tag/id token.\n * Order matters: backslash must be escaped first.\n */\nfunction escapeDescription(description: string): string {\n // Backslash first (so the escapes introduced below aren't double-escaped),\n // then structural chars, then newlines/CR. A todo serializes to a single\n // physical line; without encoding newlines the parser would split a\n // multi-line description across lines and silently drop the id + tail (the\n // second physical line fails ITEM_REGEX).\n return description\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/#/g, '\\\\#')\n .replace(/\\[/g, '\\\\[')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r');\n}\n\n/**\n * Reverse of escapeDescription: `\\#`→`#`, `\\[`→`[`, `\\\\`→`\\`.\n * A single pass handles all sequences correctly because escapes are\n * non-overlapping (a `\\` always pairs with the following char).\n */\nfunction unescapeDescription(escaped: string): string {\n let out = '';\n for (let i = 0; i < escaped.length; i++) {\n if (escaped[i] === '\\\\' && i + 1 < escaped.length) {\n const next = escaped[i + 1];\n if (next === '\\\\' || next === '#' || next === '[') {\n out += next;\n i++;\n continue;\n }\n if (next === 'n') {\n out += '\\n';\n i++;\n continue;\n }\n if (next === 'r') {\n out += '\\r';\n i++;\n continue;\n }\n }\n out += escaped[i];\n }\n return out;\n}\n\n/**\n * Find the index in `rest` of the first UN-escaped structural token: either a\n * `#tag` start (`#` followed by a tag char) or a `[t:` / `[` bracket. A token is\n * \"escaped\" when preceded by an odd number of backslashes. Returns the length of\n * `rest` if no structural token exists (whole line is description).\n */\nfunction findStructuralCut(rest: string): number {\n for (let i = 0; i < rest.length; i++) {\n const ch = rest[i];\n if (ch !== '#' && ch !== '[') continue;\n // Count preceding backslashes to determine escaped-ness.\n let backslashes = 0;\n let j = i - 1;\n while (j >= 0 && rest[j] === '\\\\') {\n backslashes++;\n j--;\n }\n if (backslashes % 2 === 1) continue; // escaped — part of the description\n if (ch === '#') {\n // Only a structural tag if followed by a tag char.\n if (/[a-zA-Z0-9_-]/.test(rest[i + 1] ?? '')) return i;\n } else {\n // ch === '[' — any unescaped bracket starts the structural tail.\n return i;\n }\n }\n return rest.length;\n}\n\nexport function parseChecklistItem(line: string): TodoItem | null {\n const match = line.match(ITEM_REGEX);\n if (!match) return null;\n\n const marker = match[1];\n const rest = match[2];\n\n const { status, session } = parseStatus(marker);\n\n // Split the line at the first UN-escaped structural token. Everything before\n // is the (escaped) description; everything after is the structural tail from\n // which tags / id / meta are extracted. This keeps escaped prose like `\\#42`\n // out of the tag collection.\n const cut = findStructuralCut(rest);\n const description = unescapeDescription(rest.slice(0, cut).trim());\n const tail = rest.slice(cut);\n\n const idMatch = tail.match(ID_REGEX);\n const id = idMatch ? idMatch[1] : '';\n\n const tags: string[] = [];\n let tagMatch;\n const tagRegex = new RegExp(TAG_REGEX.source, 'g');\n while ((tagMatch = tagRegex.exec(tail)) !== null) {\n tags.push(tagMatch[1]);\n }\n\n const meta = parseMetaToken(line);\n\n return {\n id,\n description,\n status,\n tags,\n session,\n branch: meta.branch,\n worktreePath: meta.worktreePath,\n createdAt: meta.createdAt,\n updatedAt: meta.updatedAt,\n planDir: meta.planDir,\n linkedAssignmentId: meta.linkedAssignmentId,\n linkedAssignmentRef: meta.linkedAssignmentRef,\n bundleId: meta.bundleId,\n };\n}\n\nexport function serializeChecklistItem(item: TodoItem): string {\n const marker = statusToMarker(item);\n // Last line of defense: never emit a tag that would corrupt the line. Entry\n // points validate first (returning a clean 400 / CLI error); this guarantees\n // file integrity regardless of caller.\n assertValidTags(item.tags);\n const tagStr = item.tags.map((t) => `#${t}`).join(' ');\n // Escape backslash-special chars in the description so prose `#`/`[`/`\\` is\n // never re-parsed as a structural tag/id token. Real tags and `[t:id]` below\n // are emitted with literal, unescaped markers.\n const parts = [`- [${marker}] ${escapeDescription(item.description)}`];\n if (tagStr) parts.push(tagStr);\n parts.push(`[t:${item.id}]`);\n const meta = serializeMetaToken(item);\n if (meta) parts.push(meta);\n return parts.join(' ');\n}\n\nexport function parseChecklist(content: string): TodoChecklist {\n const [fm, body] = extractFrontmatter(content);\n const workspace = getField(fm, 'workspace') || '_global';\n const archiveIntervalRaw = getField(fm, 'archiveInterval') || 'weekly';\n const archiveInterval = (['daily', 'weekly', 'monthly', 'never'].includes(archiveIntervalRaw)\n ? archiveIntervalRaw\n : 'weekly') as ArchiveInterval;\n\n const items: TodoItem[] = [];\n for (const line of body.split('\\n')) {\n const item = parseChecklistItem(line);\n if (item) items.push(item);\n }\n\n return { workspace, archiveInterval, items };\n}\n\nexport function serializeChecklist(checklist: TodoChecklist): string {\n const fm = [\n '---',\n `workspace: ${checklist.workspace}`,\n `archiveInterval: ${checklist.archiveInterval}`,\n '---',\n ].join('\\n');\n\n const header = '# Quick Todos';\n const items = checklist.items.map(serializeChecklistItem).join('\\n');\n\n return `${fm}\\n\\n${header}\\n\\n${items}\\n`;\n}\n\n// --- Log parsing ---\n\nexport function parseLog(content: string): TodoLog {\n const [fm, body] = extractFrontmatter(content);\n const workspace = getField(fm, 'workspace') || '_global';\n\n const entries: LogEntry[] = [];\n const sections = body.split(/^### /m).filter((s) => s.match(/^\\d{4}-/));\n\n for (const section of sections) {\n const lines = section.split('\\n');\n const heading = lines[0]?.trim() || '';\n\n // Heading format: 2026-04-07T14:30:00Z — t:a3f1, t:b7c2\n const headingMatch = heading.match(/^(\\S+)\\s*—?\\s*(.*)/);\n if (!headingMatch) continue;\n\n const timestamp = headingMatch[1];\n const idsPart = headingMatch[2] || '';\n const itemIds = [...idsPart.matchAll(/t:([a-f0-9]{4})/g)].map((m) => m[1]);\n\n const entry: LogEntry = {\n timestamp,\n itemIds,\n items: '',\n session: null,\n branch: null,\n summary: '',\n blockers: null,\n status: null,\n };\n\n for (const line of lines.slice(1)) {\n const fieldMatch = line.match(/^\\*\\*(\\w+):\\*\\*\\s*(.*)/);\n if (!fieldMatch) continue;\n const key = fieldMatch[1].toLowerCase();\n const value = fieldMatch[2].trim();\n switch (key) {\n case 'items':\n entry.items = value;\n break;\n case 'session':\n entry.session = value;\n break;\n case 'branch':\n entry.branch = value;\n break;\n case 'summary':\n entry.summary = value;\n break;\n case 'blockers':\n entry.blockers = value;\n break;\n case 'status':\n entry.status = value;\n break;\n }\n }\n\n entries.push(entry);\n }\n\n return { workspace, entries };\n}\n\nexport function serializeLogEntry(entry: LogEntry): string {\n const idStr = entry.itemIds.map((id) => `t:${id}`).join(', ');\n const lines = [`### ${entry.timestamp} — ${idStr}`];\n if (entry.items) lines.push(`**Items:** ${entry.items}`);\n if (entry.session) lines.push(`**Session:** ${entry.session}`);\n if (entry.branch) lines.push(`**Branch:** ${entry.branch}`);\n if (entry.summary) lines.push(`**Summary:** ${entry.summary}`);\n if (entry.blockers) lines.push(`**Blockers:** ${entry.blockers}`);\n if (entry.status) lines.push(`**Status:** ${entry.status}`);\n return lines.join('\\n');\n}\n\n/**\n * Serialize a full todo log file (frontmatter + header + entries) in the exact\n * format `readLog`/`appendLogEntry` produce. Uses the canonical\n * `serializeLogEntry` so no entry field (incl. `status`) is dropped. Callers\n * that rewrite a trimmed log should use this rather than hand-building lines.\n */\nexport function serializeLog(log: TodoLog): string {\n const header = `---\\nworkspace: ${log.workspace}\\n---\\n\\n# Todo Log\\n`;\n if (log.entries.length === 0) {\n return header;\n }\n return header + '\\n' + log.entries.map(serializeLogEntry).join('\\n\\n') + '\\n';\n}\n\n// --- File I/O ---\n\nexport function checklistPath(todosDir: string, workspace: string): string {\n return resolve(todosDir, `${workspace}.md`);\n}\n\nexport function logPath(todosDir: string, workspace: string): string {\n return resolve(todosDir, `${workspace}-log.md`);\n}\n\nexport function archivePath(\n todosDir: string,\n workspace: string,\n interval: ArchiveInterval,\n now: Date = new Date(),\n): string {\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n\n let suffix: string;\n switch (interval) {\n case 'daily':\n suffix = `${year}-${month}-${day}`;\n break;\n case 'weekly': {\n // ISO week number\n const jan1 = new Date(year, 0, 1);\n const days = Math.floor((now.getTime() - jan1.getTime()) / 86400000);\n const week = String(Math.ceil((days + jan1.getDay() + 1) / 7)).padStart(2, '0');\n suffix = `${year}-W${week}`;\n break;\n }\n case 'monthly':\n suffix = `${year}-${month}`;\n break;\n default:\n suffix = `${year}-${month}-${day}`;\n }\n\n return resolve(todosDir, 'archive', `${workspace}-${suffix}.md`);\n}\n\nexport async function readChecklist(todosDir: string, workspace: string): Promise<TodoChecklist> {\n const path = checklistPath(todosDir, workspace);\n if (!(await fileExists(path))) {\n return { workspace, archiveInterval: 'weekly', items: [] };\n }\n const content = await readFile(path, 'utf-8');\n return parseChecklist(content);\n}\n\nexport async function writeChecklist(todosDir: string, checklist: TodoChecklist): Promise<void> {\n await ensureDir(todosDir);\n const path = checklistPath(todosDir, checklist.workspace);\n await writeFileForce(path, serializeChecklist(checklist));\n}\n\nexport async function readLog(todosDir: string, workspace: string): Promise<TodoLog> {\n const path = logPath(todosDir, workspace);\n if (!(await fileExists(path))) {\n return { workspace, entries: [] };\n }\n const content = await readFile(path, 'utf-8');\n return parseLog(content);\n}\n\nexport async function appendLogEntry(\n todosDir: string,\n workspace: string,\n entry: LogEntry,\n): Promise<void> {\n await ensureDir(todosDir);\n const path = logPath(todosDir, workspace);\n let content: string;\n if (await fileExists(path)) {\n content = await readFile(path, 'utf-8');\n content = content.trimEnd() + '\\n\\n' + serializeLogEntry(entry) + '\\n';\n } else {\n const fm = `---\\nworkspace: ${workspace}\\n---\\n\\n# Todo Log\\n\\n`;\n content = fm + serializeLogEntry(entry) + '\\n';\n }\n await writeFileForce(path, content);\n}\n\nexport function computeCounts(items: TodoItem[]) {\n const counts = { open: 0, in_progress: 0, completed: 0, blocked: 0, total: items.length };\n for (const item of items) {\n counts[item.status]++;\n }\n return counts;\n}\n","import { readdir } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport {\n readChecklist,\n writeChecklist,\n readLog,\n appendLogEntry,\n} from '../todos/parser.js';\nimport { fileExists } from '../utils/fs.js';\nimport type { TodoItem, LogEntry } from '../todos/types.js';\n\nexport interface LinkedTodosLookup {\n /** Workspace todos dir (e.g. ~/.syntaur/todos). */\n todosDir: string;\n /** Projects root dir (e.g. ~/.syntaur/projects). Used to scan per-project todo checklists. */\n projectsDir: string;\n}\n\nexport interface LinkedTodosResult {\n completed?: number;\n reopened?: number;\n touched: Array<{ workspace: string; id: string }>;\n}\n\nconst AUTO_COMPLETE_PREFIX = 'Auto-completed: linked assignment ';\nconst AUTO_REOPEN_PREFIX = 'Auto-reopened: linked assignment ';\n\nfunction touchItem(item: TodoItem): void {\n const now = new Date().toISOString();\n if (item.createdAt === null) item.createdAt = now;\n item.updatedAt = now;\n}\n\nasync function listWorkspaceTodosFiles(todosDir: string): Promise<string[]> {\n if (!(await fileExists(todosDir))) return [];\n const files = await readdir(todosDir).catch(() => [] as string[]);\n return files\n .filter((f): f is string => typeof f === 'string')\n .filter((f) => f.endsWith('.md') && !f.endsWith('-log.md'))\n .map((f) => f.replace(/\\.md$/, ''));\n}\n\nasync function listProjectTodosWorkspaces(projectsDir: string): Promise<Array<{ projectSlug: string; todosDir: string; workspace: string }>> {\n if (!(await fileExists(projectsDir))) return [];\n const projects = await readdir(projectsDir).catch(() => [] as string[]);\n const result: Array<{ projectSlug: string; todosDir: string; workspace: string }> = [];\n for (const p of projects) {\n if (typeof p !== 'string') continue;\n const todosDir = resolve(projectsDir, p, 'todos');\n if (await fileExists(resolve(todosDir, `${p}.md`))) {\n result.push({ projectSlug: p, todosDir, workspace: p });\n }\n }\n return result;\n}\n\n/**\n * Returns true if the most recent log entry for this item has summary\n * starting with `prefix`. Used to identify items that were auto-completed\n * (so we know it is safe to auto-reopen them) and items that were already\n * auto-reopened (idempotency).\n */\nasync function lastLogEntryMatches(\n todosDir: string,\n workspace: string,\n itemId: string,\n prefix: string,\n): Promise<boolean> {\n const log = await readLog(todosDir, workspace);\n // Scan in reverse: most recent matching entry for this item.\n for (let i = log.entries.length - 1; i >= 0; i--) {\n const entry = log.entries[i];\n if (!entry.itemIds.includes(itemId)) continue;\n return entry.summary.startsWith(prefix);\n }\n return false;\n}\n\nexport async function completeLinkedTodos(\n lookup: LinkedTodosLookup,\n assignmentId: string,\n assignmentRef: string,\n): Promise<LinkedTodosResult> {\n const touched: Array<{ workspace: string; id: string }> = [];\n\n const workspaces = await listWorkspaceTodosFiles(lookup.todosDir);\n const projectWorkspaces = await listProjectTodosWorkspaces(lookup.projectsDir);\n const all: Array<{ todosDir: string; workspace: string }> = [\n ...workspaces.map((workspace) => ({ todosDir: lookup.todosDir, workspace })),\n ...projectWorkspaces.map(({ todosDir, workspace }) => ({ todosDir, workspace })),\n ];\n\n for (const { todosDir, workspace } of all) {\n const checklist = await readChecklist(todosDir, workspace);\n const idsTouched: string[] = [];\n for (const item of checklist.items) {\n if (item.linkedAssignmentId !== assignmentId) continue;\n if (item.status === 'completed') continue;\n item.status = 'completed';\n item.session = null;\n touchItem(item);\n idsTouched.push(item.id);\n }\n if (idsTouched.length === 0) continue;\n await writeChecklist(todosDir, checklist);\n for (const id of idsTouched) {\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n itemIds: [id],\n items: checklist.items.find((i) => i.id === id)?.description ?? '',\n session: null,\n branch: null,\n summary: `${AUTO_COMPLETE_PREFIX}${assignmentRef} closed`,\n blockers: null,\n status: null,\n };\n await appendLogEntry(todosDir, workspace, entry);\n touched.push({ workspace, id });\n }\n }\n\n return { completed: touched.length, touched };\n}\n\nexport async function reopenLinkedTodos(\n lookup: LinkedTodosLookup,\n assignmentId: string,\n assignmentRef: string,\n): Promise<LinkedTodosResult> {\n const touched: Array<{ workspace: string; id: string }> = [];\n\n const workspaces = await listWorkspaceTodosFiles(lookup.todosDir);\n const projectWorkspaces = await listProjectTodosWorkspaces(lookup.projectsDir);\n const all: Array<{ todosDir: string; workspace: string }> = [\n ...workspaces.map((workspace) => ({ todosDir: lookup.todosDir, workspace })),\n ...projectWorkspaces.map(({ todosDir, workspace }) => ({ todosDir, workspace })),\n ];\n\n for (const { todosDir, workspace } of all) {\n const checklist = await readChecklist(todosDir, workspace);\n const candidates = checklist.items.filter(\n (i) => i.linkedAssignmentId === assignmentId && i.status === 'completed',\n );\n if (candidates.length === 0) continue;\n const idsTouched: string[] = [];\n for (const item of candidates) {\n // Manual-completion guard: only auto-reopen items whose most recent log\n // entry is the auto-complete marker. If the user marked them complete\n // by hand afterwards, leave them alone.\n const wasAutoCompleted = await lastLogEntryMatches(\n todosDir,\n workspace,\n item.id,\n AUTO_COMPLETE_PREFIX,\n );\n if (!wasAutoCompleted) continue;\n item.status = 'in_progress';\n item.session = null;\n touchItem(item);\n idsTouched.push(item.id);\n }\n if (idsTouched.length === 0) continue;\n await writeChecklist(todosDir, checklist);\n for (const id of idsTouched) {\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n itemIds: [id],\n items: checklist.items.find((i) => i.id === id)?.description ?? '',\n session: null,\n branch: null,\n summary: `${AUTO_REOPEN_PREFIX}${assignmentRef} reopened`,\n blockers: null,\n status: null,\n };\n await appendLogEntry(todosDir, workspace, entry);\n touched.push({ workspace, id });\n }\n }\n\n return { reopened: touched.length, touched };\n}\n","import { resolve } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport { fileExists, writeFileForce } from '../utils/fs.js';\nimport { nowTimestamp } from '../utils/timestamp.js';\nimport { getTargetStatus } from './state-machine.js';\nimport { appendStatusHistoryEntry, parseAssignmentFrontmatter, updateAssignmentFile } from './frontmatter.js';\nimport { recordStatusEvent, resolveActor, emitEvent } from './event-emit.js';\nimport {\n completeLinkedTodos,\n reopenLinkedTodos,\n type LinkedTodosLookup,\n} from './linked-todos.js';\nimport type { TransitionCommand, TransitionResult, AssignmentFrontmatter } from './types.js';\n\nfunction linkedAssignmentRef(frontmatter: AssignmentFrontmatter): string {\n return frontmatter.project ? `${frontmatter.project}/${frontmatter.slug}` : frontmatter.id;\n}\n\nasync function applyLinkedTodosSideEffect(\n lookup: LinkedTodosLookup | undefined,\n command: string,\n targetStatus: string,\n frontmatter: AssignmentFrontmatter,\n): Promise<void> {\n if (!lookup) return;\n const ref = linkedAssignmentRef(frontmatter);\n if (targetStatus === 'completed') {\n await completeLinkedTodos(lookup, frontmatter.id, ref);\n } else if (command === 'reopen') {\n await reopenLinkedTodos(lookup, frontmatter.id, ref);\n }\n}\n\nfunction resolveAssignmentPath(projectDir: string, assignmentSlug: string): string {\n return resolve(projectDir, 'assignments', assignmentSlug, 'assignment.md');\n}\n\nasync function readAssignment(\n filePath: string,\n): Promise<{ content: string; frontmatter: AssignmentFrontmatter }> {\n if (!(await fileExists(filePath))) {\n throw new Error(`Assignment file not found: ${filePath}`);\n }\n const content = await readFile(filePath, 'utf-8');\n const frontmatter = parseAssignmentFrontmatter(content);\n return { content, frontmatter };\n}\n\n/**\n * Resolve which of an assignment's `dependsOn` targets are not yet terminal.\n * Exported so derive verbs (`start`/`implement`) can surface the same\n * non-blocking unmet-dependency warning the legacy transition path emits.\n */\nexport async function checkDependencies(\n projectDir: string,\n dependsOn: string[],\n terminalStatuses?: ReadonlySet<string>,\n): Promise<{ satisfied: boolean; unmet: string[] }> {\n const terminals = terminalStatuses ?? new Set(['completed']);\n const unmet: string[] = [];\n for (const depSlug of dependsOn) {\n const depPath = resolveAssignmentPath(projectDir, depSlug);\n if (!(await fileExists(depPath))) {\n unmet.push(`${depSlug} (file not found)`);\n continue;\n }\n const depContent = await readFile(depPath, 'utf-8');\n const depFrontmatter = parseAssignmentFrontmatter(depContent);\n if (!terminals.has(depFrontmatter.status)) {\n unmet.push(`${depSlug} (status: ${depFrontmatter.status})`);\n }\n }\n return { satisfied: unmet.length === 0, unmet };\n}\n\nexport interface TransitionOptions {\n reason?: string;\n agent?: string;\n /**\n * Actor to attribute the audit status-event to, INDEPENDENT of `agent` (which\n * drives assignee mutation). Dashboard transition routes pass `'human'` here\n * so a click on an already-assigned task is recorded as `human`, not the\n * assignee. When unset, falls back to `agent ?? frontmatter.assignee`.\n */\n auditActor?: string;\n transitionTable?: Map<string, string>;\n /** Guard-free custom targets: when provided (and no transitionTable), the\n * command resolves to this map's target regardless of the current status —\n * preserving a CUSTOM terminal target (e.g. complete -> done) without the\n * from:command guard, even for assignments on legacy/undefined statuses. */\n commandTargets?: Map<string, string>;\n terminalStatuses?: ReadonlySet<string>;\n /**\n * When provided, on a transition to `completed` we scan the configured todos\n * dirs and auto-complete any todo whose `linkedAssignmentId` matches this\n * assignment's UUID. On `reopen` we auto-reopen any such todo whose most\n * recent log entry is the auto-complete marker (manual completions are left\n * untouched).\n */\n linkedTodosLookup?: LinkedTodosLookup;\n}\n\nconst ASSIGNEE_SETTING_COMMANDS = new Set(['start', 'shape', 'plan-ready', 'implement']);\n\nexport async function executeTransition(\n projectDir: string,\n assignmentSlug: string,\n command: Exclude<TransitionCommand, 'assign'>,\n options: TransitionOptions = {},\n): Promise<TransitionResult> {\n const filePath = resolveAssignmentPath(projectDir, assignmentSlug);\n const { content, frontmatter } = await readAssignment(filePath);\n\n // Resolution order: a from-specific custom mapping wins; the guard-free\n // commandTargets fallback covers legacy/undefined statuses; built-ins last\n // (only when neither custom mechanism was supplied).\n const targetStatus =\n (options.transitionTable\n ? getTargetStatus(frontmatter.status, command, options.transitionTable)\n : null) ??\n options.commandTargets?.get(command) ??\n // Built-ins apply only when NEITHER custom mechanism was supplied — a\n // provided-but-miss commandTargets means \"custom config had no answer\",\n // which must refuse, not silently fall back (codex r4).\n (!options.transitionTable && !options.commandTargets\n ? getTargetStatus(frontmatter.status, command)\n : null);\n\n if (!targetStatus) {\n return {\n success: false,\n message: `Unknown command '${command}' for assignment \"${assignmentSlug}\".`,\n fromStatus: frontmatter.status,\n };\n }\n\n const warnings: string[] = [];\n\n if (command === 'start' && frontmatter.dependsOn.length > 0) {\n const depCheck = await checkDependencies(projectDir, frontmatter.dependsOn, options.terminalStatuses);\n if (!depCheck.satisfied) {\n warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(', ')}`);\n }\n }\n\n const now = nowTimestamp();\n const updates: Partial<\n Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated' | 'disposition'>\n > = {\n status: targetStatus,\n updated: now,\n };\n\n if (ASSIGNEE_SETTING_COMMANDS.has(command) && options.agent && !frontmatter.assignee) {\n updates.assignee = options.agent;\n }\n if (command === 'block') {\n // Derived-status v3: the blocked disposition keys on blockedReason\n // PRESENCE — a null reason would make block-without-reason a silent\n // no-op under derivation. Match the CLI verb's default.\n updates.blockedReason = options.reason ?? '(unspecified)';\n }\n if (command === 'unblock') {\n updates.blockedReason = null;\n }\n\n // Dimension-aware terminal cache (derived-status v3): entering a terminal\n // status sets `disposition: terminal` so payloads/queries never show a\n // terminal headline with a stale active/blocked disposition. Leaving\n // terminal (reopen) hands the cache back to derivation, which the CLI\n // reopen command runs immediately after this transition.\n const terminalSet = options.terminalStatuses ?? new Set(['completed', 'failed']);\n const enteringTerminal = terminalSet.has(targetStatus) && frontmatter.disposition !== 'terminal';\n if (enteringTerminal) {\n updates.disposition = 'terminal';\n }\n\n let updatedContent = updateAssignmentFile(content, updates);\n // Only record a history entry on an ACTUAL status change. CLI commands are\n // guard-free (getTargetStatus returns the canonical target regardless of the\n // current status), so re-running e.g. `complete` on an already-completed\n // assignment must not append a from===to entry and reset statusAge.\n if (targetStatus !== frontmatter.status) {\n updatedContent = appendStatusHistoryEntry(updatedContent, {\n at: now,\n from: frontmatter.status,\n to: targetStatus,\n command,\n by: options.agent ?? frontmatter.assignee ?? null,\n reason: command === 'block' ? options.reason : undefined,\n ...(enteringTerminal\n ? { dispositionFrom: frontmatter.disposition, dispositionTo: 'terminal' }\n : {}),\n });\n }\n await writeFileForce(filePath, updatedContent);\n\n // Audit event (best-effort): self-guards on from===to (R5). The audit actor\n // is independent of `agent` (which drives assignee mutation) — dashboard\n // routes pass `auditActor: 'human'` so a click is not recorded as the\n // assignee (FIX 1).\n recordStatusEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n at: now,\n actor: resolveActor(options.auditActor ?? options.agent ?? frontmatter.assignee ?? null),\n from: frontmatter.status,\n to: targetStatus,\n command,\n });\n\n await applyLinkedTodosSideEffect(options.linkedTodosLookup, command, targetStatus, frontmatter);\n\n return {\n success: true,\n message: `Assignment \"${assignmentSlug}\" transitioned: ${frontmatter.status} -> ${targetStatus}`,\n fromStatus: frontmatter.status,\n toStatus: targetStatus,\n warnings: warnings.length > 0 ? warnings : undefined,\n };\n}\n\nexport async function executeAssign(\n projectDir: string,\n assignmentSlug: string,\n agent: string,\n): Promise<TransitionResult> {\n const filePath = resolveAssignmentPath(projectDir, assignmentSlug);\n const { content, frontmatter } = await readAssignment(filePath);\n\n const updates: Partial<Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated'>> = {\n assignee: agent,\n updated: nowTimestamp(),\n };\n\n const updatedContent = updateAssignmentFile(content, updates);\n await writeFileForce(filePath, updatedContent);\n\n // Audit event (best-effort): assignee changed from prior to `agent`.\n if (frontmatter.assignee !== agent) {\n emitEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n type: 'assignee-change',\n actor: resolveActor(agent ?? frontmatter.assignee ?? null),\n details: { from: frontmatter.assignee, to: agent },\n });\n }\n\n return {\n success: true,\n message: `Assignment \"${assignmentSlug}\" assigned to '${agent}'.`,\n fromStatus: frontmatter.status,\n };\n}\n\nexport interface TransitionByDirOptions extends TransitionOptions {\n standalone?: boolean;\n}\n\nexport async function executeTransitionByDir(\n assignmentDir: string,\n command: Exclude<TransitionCommand, 'assign'>,\n options: TransitionByDirOptions = {},\n): Promise<TransitionResult> {\n const filePath = resolve(assignmentDir, 'assignment.md');\n const { content, frontmatter } = await readAssignment(filePath);\n\n // See executeTransition: from-specific mapping wins, commandTargets is the\n // guard-free fallback, built-ins only when no custom mechanism supplied.\n const targetStatus =\n (options.transitionTable\n ? getTargetStatus(frontmatter.status, command, options.transitionTable)\n : null) ??\n options.commandTargets?.get(command) ??\n // Built-ins apply only when NEITHER custom mechanism was supplied — a\n // provided-but-miss commandTargets means \"custom config had no answer\",\n // which must refuse, not silently fall back (codex r4).\n (!options.transitionTable && !options.commandTargets\n ? getTargetStatus(frontmatter.status, command)\n : null);\n if (!targetStatus) {\n return {\n success: false,\n message: `Unknown command '${command}' for assignment \"${frontmatter.slug || assignmentDir}\".`,\n fromStatus: frontmatter.status,\n };\n }\n\n const warnings: string[] = [];\n\n if (command === 'start' && !options.standalone && frontmatter.dependsOn.length > 0) {\n // Dependency check requires a project context — skip for standalone\n const projectDir = resolve(assignmentDir, '..', '..');\n const depCheck = await checkDependencies(\n projectDir,\n frontmatter.dependsOn,\n options.terminalStatuses,\n );\n if (!depCheck.satisfied) {\n warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(', ')}`);\n }\n }\n\n const now = nowTimestamp();\n const updates: Partial<\n Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated' | 'disposition'>\n > = {\n status: targetStatus,\n updated: now,\n };\n\n if (ASSIGNEE_SETTING_COMMANDS.has(command) && options.agent && !frontmatter.assignee) {\n updates.assignee = options.agent;\n }\n if (command === 'block') {\n // Derived-status v3: the blocked disposition keys on blockedReason\n // PRESENCE — a null reason would make block-without-reason a silent\n // no-op under derivation. Match the CLI verb's default.\n updates.blockedReason = options.reason ?? '(unspecified)';\n }\n if (command === 'unblock') {\n updates.blockedReason = null;\n }\n\n // Dimension-aware terminal cache — see executeTransition.\n const terminalSetByDir = options.terminalStatuses ?? new Set(['completed', 'failed']);\n const enteringTerminalByDir =\n terminalSetByDir.has(targetStatus) && frontmatter.disposition !== 'terminal';\n if (enteringTerminalByDir) {\n updates.disposition = 'terminal';\n }\n\n let updatedContent = updateAssignmentFile(content, updates);\n // Only record a history entry on an ACTUAL status change (see executeTransition).\n if (targetStatus !== frontmatter.status) {\n updatedContent = appendStatusHistoryEntry(updatedContent, {\n at: now,\n from: frontmatter.status,\n to: targetStatus,\n command,\n by: options.agent ?? frontmatter.assignee ?? null,\n reason: command === 'block' ? options.reason : undefined,\n ...(enteringTerminalByDir\n ? { dispositionFrom: frontmatter.disposition, dispositionTo: 'terminal' }\n : {}),\n });\n }\n await writeFileForce(filePath, updatedContent);\n\n // Audit event (best-effort): self-guards on from===to (R5). The audit actor\n // is independent of `agent` (see executeTransition / FIX 1).\n recordStatusEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n at: now,\n actor: resolveActor(options.auditActor ?? options.agent ?? frontmatter.assignee ?? null),\n from: frontmatter.status,\n to: targetStatus,\n command,\n });\n\n await applyLinkedTodosSideEffect(options.linkedTodosLookup, command, targetStatus, frontmatter);\n\n return {\n success: true,\n message: `Assignment \"${frontmatter.slug || assignmentDir}\" transitioned: ${frontmatter.status} -> ${targetStatus}`,\n fromStatus: frontmatter.status,\n toStatus: targetStatus,\n warnings: warnings.length > 0 ? warnings : undefined,\n };\n}\n\nexport async function executeAssignByDir(\n assignmentDir: string,\n agent: string,\n): Promise<TransitionResult> {\n const filePath = resolve(assignmentDir, 'assignment.md');\n const { content, frontmatter } = await readAssignment(filePath);\n\n const updates: Partial<Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated'>> = {\n assignee: agent,\n updated: nowTimestamp(),\n };\n\n const updatedContent = updateAssignmentFile(content, updates);\n await writeFileForce(filePath, updatedContent);\n\n if (frontmatter.assignee !== agent) {\n emitEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n type: 'assignee-change',\n actor: resolveActor(agent ?? frontmatter.assignee ?? null),\n details: { from: frontmatter.assignee, to: agent },\n });\n }\n\n return {\n success: true,\n message: `Assignment \"${frontmatter.slug || assignmentDir}\" assigned to '${agent}'.`,\n fromStatus: frontmatter.status,\n };\n}\n\nexport async function executeUnassign(\n projectDir: string,\n assignmentSlug: string,\n): Promise<TransitionResult> {\n const filePath = resolveAssignmentPath(projectDir, assignmentSlug);\n const { content, frontmatter } = await readAssignment(filePath);\n\n const updates: Partial<Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated'>> = {\n assignee: null,\n updated: nowTimestamp(),\n };\n\n const updatedContent = updateAssignmentFile(content, updates);\n await writeFileForce(filePath, updatedContent);\n\n if (frontmatter.assignee !== null) {\n emitEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n type: 'assignee-change',\n actor: resolveActor(frontmatter.assignee),\n details: { from: frontmatter.assignee, to: null },\n });\n }\n\n return {\n success: true,\n message: `Assignment \"${assignmentSlug}\" unassigned (assignee cleared).`,\n fromStatus: frontmatter.status,\n };\n}\n\nexport async function executeUnassignByDir(\n assignmentDir: string,\n): Promise<TransitionResult> {\n const filePath = resolve(assignmentDir, 'assignment.md');\n const { content, frontmatter } = await readAssignment(filePath);\n\n const updates: Partial<Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated'>> = {\n assignee: null,\n updated: nowTimestamp(),\n };\n\n const updatedContent = updateAssignmentFile(content, updates);\n await writeFileForce(filePath, updatedContent);\n\n if (frontmatter.assignee !== null) {\n emitEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n type: 'assignee-change',\n actor: resolveActor(frontmatter.assignee),\n details: { from: frontmatter.assignee, to: null },\n });\n }\n\n return {\n success: true,\n message: `Assignment \"${frontmatter.slug || assignmentDir}\" unassigned (assignee cleared).`,\n fromStatus: frontmatter.status,\n };\n}\n","export type {\n AssignmentStatus,\n TransitionCommand,\n AssignmentFrontmatter,\n ExternalId,\n Workspace,\n TransitionResult,\n} from './types.js';\nexport { TERMINAL_STATUSES, DEFAULT_STATUSES, DEFAULT_COMMANDS, DEFAULT_TERMINAL_STATUSES } from './types.js';\nexport { canTransition, getTargetStatus, isTerminalStatus, DEFAULT_TRANSITION_TABLE, DEFAULT_COMMAND_TARGETS, buildTransitionTable, buildCommandTargets } from './state-machine.js';\nexport { parseAssignmentFrontmatter, updateAssignmentFile, updateAssignmentWorkspace } from './frontmatter.js';\nexport { executeTransition, executeAssign, executeTransitionByDir, executeAssignByDir, executeUnassign, executeUnassignByDir } from './transitions.js';\nexport type { TransitionOptions, TransitionByDirOptions } from './transitions.js';\n","// Shared hotkey catalog: bindable action kinds, reserved combos, and the\n// canonical combo string format. Imported directly by the Express server\n// (src/dashboard/server.ts) and by the dashboard via the\n// `@shared/hotkeys-catalog` alias defined in dashboard/tsconfig.json +\n// dashboard/vite.config.ts.\n\nexport type BindableActionKind =\n | 'new-workspace'\n | 'new-project'\n | 'new-todo'\n | 'new-assignment';\n\nexport const BINDABLE_ACTION_KINDS: readonly BindableActionKind[] = [\n 'new-workspace',\n 'new-project',\n 'new-todo',\n 'new-assignment',\n];\n\nexport function isBindableActionKind(value: unknown): value is BindableActionKind {\n return (\n typeof value === 'string' &&\n (BINDABLE_ACTION_KINDS as readonly string[]).includes(value)\n );\n}\n\n// Reserved combos that user-bound hotkeys may NOT shadow. Hand-maintained;\n// scripts/check-hotkey-catalog.ts greps `useHotkey({` across the dashboard and\n// fails if it sees a `keys` value that is not represented here.\n//\n// Combos are stored in canonical form (see canonicalizeCombo). The list\n// includes:\n// - global UI combos (Mod+k, Mod+Shift+k, ?, Escape, Enter, Shift+t)\n// - g <suffix> chord prefixes (the lone \"g\" is reserved as a chord starter)\n// - list-scope letters\n// - page-scoped shortcuts that exist when those pages are mounted\nexport const BUILTIN_RESERVED_COMBOS: readonly string[] = [\n 'mod+k',\n 'mod+shift+k',\n '?',\n 'escape',\n 'enter',\n 'shift+t',\n // g-chord starter + suffixes\n 'g',\n 'g o',\n 'g m',\n 'g a',\n 'g t',\n 'g s',\n 'g !',\n 'g ,',\n // list-scope navigation\n 'j',\n 'k',\n 'o',\n // ProjectDetail page\n 'a',\n 'e',\n // AssignmentsPage board\n '/',\n 'r',\n // AssignmentDetail page\n 'p',\n 'h',\n 'd',\n 's',\n '[',\n ']',\n];\n\nconst MODIFIER_ORDER: readonly string[] = ['mod', 'ctrl', 'alt', 'shift'];\n\n/**\n * Canonicalize a combo string for storage and comparison.\n *\n * - Trims whitespace.\n * - Splits on `+` for single-key combos; preserves space-separated chord form\n * (e.g. `g a`) by canonicalizing each part independently.\n * - Lowercases everything (modifiers and the trailing key alike).\n * - Reorders modifiers into canonical order: mod, ctrl, alt, shift.\n *\n * Examples:\n * canonicalizeCombo(\"Shift+Mod+K\") -> \"mod+shift+k\"\n * canonicalizeCombo(\" cmd + Enter\") -> \"mod+enter\" (after caller maps cmd->mod)\n * canonicalizeCombo(\"g A\") -> \"g a\"\n * canonicalizeCombo(\"?\") -> \"?\"\n */\nexport function canonicalizeCombo(input: string): string {\n if (typeof input !== 'string') return '';\n const trimmed = input.trim();\n if (!trimmed) return '';\n\n // Chord form: space-separated, no `+` separators (e.g. \"g a\"). When the\n // input contains `+` it's treated as a single combo even if it has stray\n // whitespace around the separators (e.g. \"Mod + K\").\n if (/\\s/.test(trimmed) && !trimmed.includes('+')) {\n return trimmed\n .split(/\\s+/)\n .map(canonicalizeCombo)\n .filter((part) => part.length > 0)\n .join(' ');\n }\n\n const parts = trimmed.split('+').map((p) => p.trim()).filter((p) => p.length > 0);\n if (parts.length === 0) return '';\n if (parts.length === 1) {\n return parts[0].toLowerCase();\n }\n\n const key = parts[parts.length - 1].toLowerCase();\n const mods = parts.slice(0, -1).map((m) => m.toLowerCase());\n\n const seen = new Set<string>();\n const ordered: string[] = [];\n for (const m of MODIFIER_ORDER) {\n if (mods.includes(m) && !seen.has(m)) {\n ordered.push(m);\n seen.add(m);\n }\n }\n // Append any non-standard modifiers at the end (preserves user intent for\n // anything we don't recognize).\n for (const m of mods) {\n if (!seen.has(m)) {\n ordered.push(m);\n seen.add(m);\n }\n }\n\n return [...ordered, key].join('+');\n}\n\n/**\n * Returns true when `combo` (canonicalized) collides with a built-in reserved\n * combo. Server-side enforcement entry point.\n */\nexport function isReservedCombo(combo: string): boolean {\n const c = canonicalizeCombo(combo);\n if (!c) return false;\n return (BUILTIN_RESERVED_COMBOS as readonly string[]).includes(c);\n}\n\n/**\n * Default hotkey bindings shipped with the dashboard. The triple-modifier\n * `Mod+Shift+Alt+<letter>` namespace is intentionally chosen to avoid common\n * browser shortcuts (Cmd+Shift+T reopens closed tab, Cmd+Shift+P opens\n * private mode, Cmd+Shift+W closes window, etc.) while keeping the action\n * mnemonic. Users can override any of these from Settings → Hotkey Bindings.\n *\n * These are EFFECTIVE only when the user has not bound a custom combo for\n * that action — `effectiveBindings()` overlays the user's custom bindings on\n * top, so a custom binding always wins.\n */\nexport const DEFAULT_BINDABLE_HOTKEYS: Readonly<Record<BindableActionKind, string>> = {\n 'new-workspace': canonicalizeCombo('Mod+Shift+Alt+w'),\n 'new-project': canonicalizeCombo('Mod+Shift+Alt+p'),\n 'new-todo': canonicalizeCombo('Mod+Shift+Alt+t'),\n 'new-assignment': canonicalizeCombo('Mod+Shift+Alt+a'),\n};\n\n/**\n * Returns the effective binding map: defaults underneath, user customs on top.\n * A user-bound combo always wins; if the user has no entry for a kind, the\n * default is returned (if any).\n */\nexport function effectiveBindings(\n custom: Partial<Record<BindableActionKind, string>>,\n): Partial<Record<BindableActionKind, string>> {\n const out: Partial<Record<BindableActionKind, string>> = {\n ...DEFAULT_BINDABLE_HOTKEYS,\n };\n for (const kind of BINDABLE_ACTION_KINDS) {\n const override = custom[kind];\n if (typeof override === 'string' && override.length > 0) {\n out[kind] = override;\n }\n }\n return out;\n}\n\n/** True when the given kind currently uses its default combo (no user override). */\nexport function isDefaultBinding(\n custom: Partial<Record<BindableActionKind, string>>,\n kind: BindableActionKind,\n): boolean {\n const override = custom[kind];\n return typeof override !== 'string' || override.length === 0;\n}\n","export type PromptArgPosition = 'first' | 'last' | 'none';\n\n/**\n * Per-agent argv recipe for continuing a recorded session in a specific mode.\n *\n * `args` is a literal argv list with the substring `{id}` substituted for the\n * agent's session id at launch time. `command` overrides the agent's main\n * `command` field — used by subcommand-style agents (e.g. `codex resume <id>`\n * is documented as command=codex, args=['resume','{id}']; the override exists\n * for future agents whose subcommand binary differs).\n */\nexport interface SessionInvocation {\n command?: string;\n args: string[];\n}\n\nexport interface AgentConfig {\n id: string;\n label: string;\n command: string;\n args?: string[];\n promptArgPosition?: PromptArgPosition;\n default?: boolean;\n resolveFromShellAliases?: boolean;\n resume?: SessionInvocation;\n fork?: SessionInvocation;\n /**\n * Optional LLM model for this runner profile, injected into the launched CLI\n * as a generic `--model <value>` flag (see `modelFlagArgs`). Blank/undefined\n * omits the flag entirely (today's behavior). Works for agents whose CLI\n * accepts `--model` (claude, codex); leave blank for agents that don't.\n */\n model?: string;\n /**\n * Optional playbook slug for this runner profile. Back-compat shorthand: when\n * set (and no `launchPrompt`), a fresh \"Open in agent\" launch synthesizes a\n * prompt that grabs the assignment AND runs this playbook end-to-end (see\n * `resolveLaunchPrompt`). Blank/undefined keeps the plain `/grab-assignment`\n * seed. Ignored for seed assembly when `launchPrompt` is also set.\n */\n playbook?: string;\n /**\n * Editable, user-owned launch prompt: the literal first message handed to the\n * agent on a fresh \"Open in agent\" launch, with `@`-tokens resolved at launch\n * time (`@assignment` → assignment id + records-dir path + grab/read\n * instructions; `@<playbook-slug>` → a `/run-playbook` reference). When unset,\n * falls back to back-compat `playbook`, then to the bare `/grab-assignment`\n * seed. Takes precedence over `playbook`. Single-line (see `serializeAgentsConfig`).\n */\n launchPrompt?: string;\n}\n\nexport const BUILTIN_AGENTS: AgentConfig[] = [\n {\n id: 'claude',\n label: 'Claude',\n command: 'claude',\n default: true,\n resume: { args: ['--resume', '{id}'] },\n fork: { args: ['--resume', '{id}', '--fork-session'] },\n },\n {\n id: 'codex',\n label: 'Codex',\n command: 'codex',\n resume: { args: ['resume', '{id}'] },\n fork: { args: ['fork', '{id}'] },\n },\n // pi: resume/fork verified against `pi --help` (pi is installed) — `--session\n // <path|id>` continues a recorded session, `--fork <path|id>` forks one. (Not\n // `--resume`, which is an interactive picker that takes no id.)\n {\n id: 'pi',\n label: 'Pi',\n command: 'pi',\n resume: { args: ['--session', '{id}'] },\n fork: { args: ['--fork', '{id}'] },\n },\n // openclaw: resume/fork intentionally omitted. The openclaw binary is not\n // installed here, so its CLI cannot be verified; the only evidence it shares\n // pi's flags is a hedged design-memo assumption (see src/targets/registry.ts).\n // A missing recipe degrades gracefully to LaunchError('mode-not-supported'),\n // which a user can override via ~/.syntaur/config.md; a wrong recipe would\n // silently launch the wrong command. Add verified recipes once installable.\n {\n id: 'openclaw',\n label: 'OpenClaw',\n command: 'openclaw',\n },\n // hermes: resume/fork omitted — binary not installed and no resume/fork CLI is\n // documented for it. Ship launch-only; same graceful-degradation rationale.\n {\n id: 'hermes',\n label: 'Hermes',\n command: 'hermes',\n },\n];\n\nexport const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;\nexport const PROMPT_ARG_POSITIONS: readonly PromptArgPosition[] = ['first', 'last', 'none'];\n\n/**\n * Argv fragment that injects the profile's model as a generic `--model <value>`\n * flag, or `[]` when no model is set.\n */\nexport function modelFlagArgs(agent: AgentConfig): string[] {\n const m = agent.model?.trim();\n return m ? ['--model', m] : [];\n}\n\n/**\n * Remove any existing `--model`/`-m` flag (and its value) from an argv list.\n * Handles both the separate (`--model opus`) and combined (`--model=opus`,\n * `-m=opus`) forms.\n */\nfunction stripModelFlags(args: string[]): string[] {\n const out: string[] = [];\n for (let i = 0; i < args.length; i++) {\n const a = args[i];\n if (a === '--model' || a === '-m') {\n i++; // also skip the following value token\n continue;\n }\n if (a.startsWith('--model=') || a.startsWith('-m=')) continue;\n out.push(a);\n }\n return out;\n}\n\n/**\n * Apply the profile's model to a base argv list. When the profile sets a model,\n * any pre-existing `--model`/`-m` in `baseArgs` is stripped first and the\n * profile flag appended — the profile model is authoritative AND we never emit a\n * duplicate `--model` (Codex 0.135.0 rejects duplicate `--model` outright; it is\n * not last-wins). When the profile has no model, `baseArgs` is returned\n * unchanged so a hand-written `--model` in `args` still works.\n */\nexport function applyModelFlag(agent: AgentConfig, baseArgs: string[]): string[] {\n const flag = modelFlagArgs(agent);\n if (flag.length === 0) return baseArgs;\n return [...stripModelFlags(baseArgs), ...flag];\n}\n","export function slugify(title: string): string {\n return title\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\nexport function isValidSlug(slug: string): boolean {\n return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug);\n}\n","/**\n * Browser-safe derive-config primitives.\n *\n * Extracted from `config.ts` (which is Node-heavy — `node:fs/promises`,\n * `node:child_process`, etc. — and cannot be aliased into the Vite/browser\n * build). This module has ZERO imports so the dashboard client can alias it\n * (`@shared/derive-config`) and reuse the exact same validator the server and\n * `doctor` run, guaranteeing client/server/doctor parity.\n *\n * `config.ts` re-exports everything here so existing Node-side imports from\n * `config.js` keep resolving unchanged.\n */\n\n/** One rung of the phase ladder (derived-status v3). Ordered low → high; the\n * HIGHEST rung whose AQL `when` holds wins. Regressible by design (e.g. a\n * replan invalidates approval and the phase drops). `phase` must be a defined\n * status id; `next` is the human next-action label surfaced by views. */\nexport interface PhaseRung {\n phase: string;\n when: string;\n next?: string;\n}\n\n/** One disposition rule (first match wins). `when: null` is the `else` arm.\n * `is` ∈ active|blocked|parked (terminal is never a rule — terminal statuses\n * defer derivation entirely). */\nexport interface DispositionRule {\n when: string | null;\n is: string;\n}\n\n/** Headline projection: which status id the single-column board shows.\n * `terminal` is always passthrough and `active` always shows the phase; the\n * configurable parts are which status ids represent parked/blocked. */\nexport interface HeadlineProjection {\n terminal: 'passthrough';\n parked: string;\n blocked: string;\n active: 'phase';\n}\n\nexport interface DeriveConfig {\n phaseLadder: PhaseRung[];\n disposition: DispositionRule[];\n headline: HeadlineProjection;\n}\n\n/** Built-in derive rules matching DEFAULT_STATUSES (review rung = `review`). */\nexport const DEFAULT_DERIVE_CONFIG: DeriveConfig = {\n phaseLadder: [\n { phase: 'draft', when: '*', next: 'Fill in the objective and acceptance criteria' },\n {\n // planExists-but-not-approved also sits here: the default status set has\n // no `planning` id. Users who define one add a `planExists:true` rung.\n phase: 'ready_for_planning',\n when: 'hasRealObjective:true AND acRealTotal > 0',\n next: 'Write a plan and get it approved',\n },\n { phase: 'ready_to_implement', when: 'planApproved:true', next: 'Start implementing' },\n {\n phase: 'in_progress',\n when: 'planApproved:true AND implementationStarted:true',\n next: 'Finish acceptance criteria, then request review',\n },\n {\n phase: 'review',\n when: 'acAllChecked:true OR reviewRequested:true',\n next: 'Complete, or address review feedback',\n },\n ],\n disposition: [\n { when: 'parked:true', is: 'parked' },\n { when: 'blocked:true', is: 'blocked' },\n { when: null, is: 'active' },\n ],\n headline: { terminal: 'passthrough', parked: 'parked', blocked: 'blocked', active: 'phase' },\n};\n\n/**\n * Validate derive rules against a status config: rung/headline ids must be\n * defined statuses, disposition `is` values must be active|blocked|parked,\n * and every `when` must parse against the AQL field registry. Returns\n * human-readable problems (empty = valid). Used by doctor and the dashboard\n * settings API; pure so the dashboard client reuses it.\n *\n * The status param is intentionally minimal (`{ statuses: Array<{ id: string }> }`)\n * so this module stays dependency-free; it is structurally compatible with the\n * `Pick<StatusConfig, 'statuses'>` callers pass.\n */\n/**\n * Deeply shape-check an UNTRUSTED derive payload (from the dashboard POST body)\n * before {@link validateDeriveConfig} runs. validateDeriveConfig assumes every\n * rung/rule/field already has its declared type, so a malformed payload (a null\n * rung, a numeric `when`, …) would otherwise throw a 500 — and worse, slip\n * through to serialization after assignment files were already mutated. This\n * returns human-readable problems (empty = structurally sound) so the API can\n * reject with `invalid-derive` 400 before touching disk.\n */\nexport function validateDeriveShape(value: unknown): string[] {\n const problems: string[] = [];\n if (!value || typeof value !== 'object') {\n return ['derive must be an object'];\n }\n const d = value as Record<string, unknown>;\n\n if (!Array.isArray(d.phaseLadder)) {\n problems.push('derive.phaseLadder must be an array');\n } else {\n d.phaseLadder.forEach((rung, i) => {\n if (!rung || typeof rung !== 'object') {\n problems.push(`derive.phaseLadder[${i}] must be an object`);\n return;\n }\n const r = rung as Record<string, unknown>;\n if (typeof r.phase !== 'string') problems.push(`derive.phaseLadder[${i}].phase must be a string`);\n if (typeof r.when !== 'string') problems.push(`derive.phaseLadder[${i}].when must be a string`);\n if (r.next !== undefined && typeof r.next !== 'string') {\n problems.push(`derive.phaseLadder[${i}].next must be a string when present`);\n }\n });\n }\n\n if (!Array.isArray(d.disposition)) {\n problems.push('derive.disposition must be an array');\n } else {\n d.disposition.forEach((rule, i) => {\n if (!rule || typeof rule !== 'object') {\n problems.push(`derive.disposition[${i}] must be an object`);\n return;\n }\n const r = rule as Record<string, unknown>;\n if (!(r.when === null || typeof r.when === 'string')) {\n problems.push(`derive.disposition[${i}].when must be a string or null`);\n }\n if (typeof r.is !== 'string') problems.push(`derive.disposition[${i}].is must be a string`);\n });\n }\n\n const headline = d.headline as Record<string, unknown> | undefined;\n if (!headline || typeof headline !== 'object') {\n problems.push('derive.headline must be an object');\n } else {\n if (typeof headline.parked !== 'string') problems.push('derive.headline.parked must be a string');\n if (typeof headline.blocked !== 'string') problems.push('derive.headline.blocked must be a string');\n }\n\n return problems;\n}\n\nexport function validateDeriveConfig(\n derive: DeriveConfig,\n statusConfig: { statuses: Array<{ id: string }> },\n validateWhen: (when: string) => string | null = () => null,\n): string[] {\n const problems: string[] = [];\n const ids = new Set(statusConfig.statuses.map((s) => s.id));\n\n if (derive.phaseLadder.length === 0) {\n problems.push('phaseLadder must have at least one rung');\n }\n for (const rung of derive.phaseLadder) {\n if (!ids.has(rung.phase)) {\n problems.push(`phaseLadder rung \"${rung.phase}\" is not a defined status id`);\n }\n const err = rung.when === '*' ? null : validateWhen(rung.when);\n if (err) problems.push(`phaseLadder rung \"${rung.phase}\": invalid condition — ${err}`);\n }\n const VALID_DISPOSITIONS = new Set(['active', 'blocked', 'parked']);\n for (const rule of derive.disposition) {\n if (!VALID_DISPOSITIONS.has(rule.is)) {\n problems.push(\n `disposition \"${rule.is}\" is not valid (expected active, blocked, or parked — terminal is never a rule)`,\n );\n }\n if (rule.when !== null) {\n const err = validateWhen(rule.when);\n if (err) problems.push(`disposition rule \"${rule.is}\": invalid condition — ${err}`);\n }\n }\n // Disposition is first-match-wins, so the `else:` arm (when: null) must be the\n // SINGLE last rule — an else-first (or duplicate-else) config silently makes\n // every later rule unreachable.\n const elseIndices = derive.disposition\n .map((r, i) => (r.when === null ? i : -1))\n .filter((i) => i >= 0);\n if (elseIndices.length === 0) {\n problems.push('disposition rules must end with an `else:` arm (a rule with when: null)');\n } else if (elseIndices.length > 1) {\n problems.push('disposition rules must have exactly one `else:` arm (when: null)');\n } else if (elseIndices[0] !== derive.disposition.length - 1) {\n problems.push('the `else:` arm (when: null) must be the LAST disposition rule — rules after it are unreachable');\n }\n\n for (const key of ['parked', 'blocked'] as const) {\n if (!ids.has(derive.headline[key])) {\n problems.push(\n `headline.${key} → \"${derive.headline[key]}\" is not a defined status id (add the definition or run migrate-derive)`,\n );\n }\n }\n return problems;\n}\n","/**\n * AQL field registry — the curated vocabulary queries may reference.\n *\n * Seam (derived-status design v3, Piece 1): users define *conditions*\n * (queries); Syntaur defines the *fact/field set*. A new condition is a config\n * edit; a new field is a Syntaur release here.\n *\n * Browser-safe: accessors read from a plain `QueryItem` record that callers\n * (CLI loader / dashboard payload) have already materialized — never from the\n * filesystem.\n */\n\n/** The flat record a query evaluates against. */\nexport type QueryItem = Record<string, unknown>;\n\nexport type FieldKind =\n | 'enum' // case-insensitive equality (status, phase, type, …)\n | 'string' // equality, with `none` sentinel for null (assignee, project)\n | 'substring' // case-insensitive containment (title/search)\n | 'bool'\n | 'number'\n | 'ordinal' // ordered enum — supports < > (priority)\n | 'timestamp' // ISO string; comparisons vs dates and duration literals\n | 'duration' // milliseconds; comparisons vs duration-literal magnitude\n | 'list'; // membership (tags)\n\nexport interface FieldDef {\n kind: FieldKind;\n /** Read the raw value from an item. Default: direct key access by canonical name. */\n get?: (item: QueryItem) => unknown;\n /** Ordinal ordering, low → high (required for kind 'ordinal'). */\n order?: string[];\n /** Accept `field:none` as a null/empty check. */\n noneSentinel?: boolean;\n}\n\nexport type FieldRegistry = Record<string, FieldDef>;\n\nexport const PRIORITY_ORDER = ['low', 'medium', 'high', 'critical'];\n\n/**\n * Default assignment field vocabulary: core frontmatter fields (AQL design,\n * Piece 2 table) + the derived-status fact fields (derived-status design v3,\n * Piece 1). Consumers may extend or restrict (e.g. derive rules evaluate over\n * facts only).\n */\nexport const ASSIGNMENT_FIELDS: FieldRegistry = {\n // ── core fields ──────────────────────────────────────────────────────────\n status: { kind: 'enum' },\n priority: { kind: 'ordinal', order: PRIORITY_ORDER },\n type: { kind: 'enum' },\n assignee: { kind: 'string', noneSentinel: true },\n project: { kind: 'string', noneSentinel: true },\n tag: { kind: 'list', get: (i) => i['tags'] },\n tags: { kind: 'list' },\n archived: { kind: 'bool' },\n title: { kind: 'substring' },\n // `search` reads a dedicated `searchText` haystack when the item provides one\n // (so the dashboard can match title + slug + project like its filter box),\n // falling back to `title` when absent. Backward-compatible: title-only when no\n // searchText. The `title` field stays title-only.\n search: { kind: 'substring', get: (i) => i['searchText'] ?? i['title'] },\n created: { kind: 'timestamp' },\n updated: { kind: 'timestamp' },\n completedat: { kind: 'timestamp', get: (i) => i['completedAt'] },\n statusage: { kind: 'duration', get: (i) => i['statusAge'] },\n\n // ── derived-status dimensions ────────────────────────────────────────────\n phase: { kind: 'enum' },\n disposition: { kind: 'enum' },\n phaseage: { kind: 'duration', get: (i) => i['phaseAge'] },\n\n // ── objective facts ──────────────────────────────────────────────────────\n hasrealobjective: { kind: 'bool', get: (i) => i['hasRealObjective'] },\n acrealtotal: { kind: 'number', get: (i) => i['acRealTotal'] },\n acrealchecked: { kind: 'number', get: (i) => i['acRealChecked'] },\n acallchecked: { kind: 'bool', get: (i) => i['acAllChecked'] },\n planexists: { kind: 'bool', get: (i) => i['planExists'] },\n planapproved: { kind: 'bool', get: (i) => i['planApproved'] },\n workspaceset: { kind: 'bool', get: (i) => i['workspaceSet'] },\n implementationstarted: { kind: 'bool', get: (i) => i['implementationStarted'] },\n depssatisfied: { kind: 'bool', get: (i) => i['depsSatisfied'] },\n unresolvedquestions: { kind: 'number', get: (i) => i['unresolvedQuestions'] },\n progressstaledays: { kind: 'duration', get: (i) => i['progressStaleDays'] },\n\n // ── asserted facts ───────────────────────────────────────────────────────\n blocked: { kind: 'bool' },\n parked: { kind: 'bool' },\n reviewrequested: { kind: 'bool', get: (i) => i['reviewRequested'] },\n pinned: { kind: 'bool' },\n};\n\n/**\n * Field lookup is case-insensitive: registry keys are lowercase; `resolveField`\n * lowercases the query's field name. Accessors fall back to the item's\n * camelCase canonical key via `get`.\n */\nexport function resolveField(registry: FieldRegistry, name: string): FieldDef | null {\n return registry[name.toLowerCase()] ?? null;\n}\n\nexport function readField(def: FieldDef, fieldName: string, item: QueryItem): unknown {\n if (def.get) return def.get(item);\n return item[fieldName] ?? item[fieldName.toLowerCase()];\n}\n","/**\n * AQL evaluator — compiles a parsed AST into a predicate function over\n * QueryItems. Field references are validated at compile time (structured\n * errors with positions); evaluation is pure.\n *\n * Time semantics (AQL design, \"Duration literals\"):\n * - vs a TIMESTAMP field, a duration literal is a relative point in time:\n * `created > -36h` ⇒ created after (now − 36h). A bare duration (`36h`)\n * means \"ago\" (same as `-36h`).\n * - vs a DURATION field, a duration literal is a magnitude (sign ignored):\n * `statusAge > 3d` ⇒ in current status longer than 3 days.\n * - Absolute dates compare on LOCAL-day boundaries (consistent with the\n * dashboard's matchesDateRange).\n *\n * Browser-safe (no Node APIs). `now` is injected via EvalContext — never read\n * from Date.now() here — so evaluation is deterministic and testable.\n */\n\nimport type { AtomNode, QueryError, QueryNode, QueryValue } from './ast.js';\nimport { readField, resolveField, type FieldDef, type FieldRegistry, type QueryItem } from './fields.js';\n\nexport interface EvalContext {\n /** Epoch ms used to resolve relative duration literals. */\n now: number;\n}\n\nexport type Predicate = (item: QueryItem, ctx: EvalContext) => boolean;\n\nexport class CompileError extends Error {\n constructor(public errors: QueryError[]) {\n super(errors.map((e) => `${e.message} (at ${e.pos})`).join('; '));\n this.name = 'CompileError';\n }\n}\n\n/**\n * [startOfDay, startOfNextDay) for a YYYY-MM-DD in local time. Rejects\n * impossible calendar dates (e.g. 2026-02-30) instead of letting `new Date`\n * silently roll them over — otherwise `created:2026-02-30` would compile and\n * match March 2. Throws CompileError with the atom's position on invalid input.\n */\nfunction localDayBounds(value: { raw: string; pos: number }): [number, number] {\n const [y, m, d] = value.raw.split('-').map((n) => parseInt(n, 10));\n const start = new Date(y, m - 1, d);\n if (start.getFullYear() !== y || start.getMonth() !== m - 1 || start.getDate() !== d) {\n throw new CompileError([{ pos: value.pos, message: `Invalid date \"${value.raw}\"` }]);\n }\n const end = new Date(y, m - 1, d + 1).getTime();\n return [start.getTime(), end];\n}\n\nfunction toEpoch(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'string' && value.length > 0) {\n const t = Date.parse(value);\n return Number.isNaN(t) ? null : t;\n }\n return null;\n}\n\nfunction toNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\nfunction ciEquals(a: unknown, b: string): boolean {\n return typeof a === 'string' && a.toLowerCase() === b.toLowerCase();\n}\n\nfunction isNone(value: unknown): boolean {\n return value === null || value === undefined || value === '';\n}\n\nfunction compileEquality(def: FieldDef, field: string, value: QueryValue, atomPos: number): Predicate {\n switch (def.kind) {\n case 'enum':\n case 'string':\n if (def.noneSentinel && value.raw.toLowerCase() === 'none') {\n return (item) => isNone(readField(def, field, item));\n }\n return (item) => ciEquals(readField(def, field, item), value.raw);\n case 'substring':\n return (item) => {\n const v = readField(def, field, item);\n return typeof v === 'string' && v.toLowerCase().includes(value.raw.toLowerCase());\n };\n case 'bool': {\n const want = value.raw.toLowerCase();\n if (want !== 'true' && want !== 'false') {\n throw new CompileError([\n { pos: value.pos, message: `Field \"${field}\" is boolean — use ${field}:true or ${field}:false` },\n ]);\n }\n const expected = want === 'true';\n return (item) => {\n // Strict-ish coercion: real booleans pass through; the strings\n // 'true'/'false' parse (frontmatter scalars); null/undefined/'' mean\n // false (an absent fact is false). Anything else never matches —\n // Boolean('false') === true was the bug here.\n const v = readField(def, field, item);\n const b =\n typeof v === 'boolean'\n ? v\n : v === 'true'\n ? true\n : v === 'false' || v === null || v === undefined || v === ''\n ? false\n : null;\n return b !== null && b === expected;\n };\n }\n case 'number': {\n const n = value.num ?? toNumber(value.raw);\n if (n === null) {\n throw new CompileError([{ pos: value.pos, message: `Field \"${field}\" is numeric — \"${value.raw}\" is not a number` }]);\n }\n return (item) => toNumber(readField(def, field, item)) === n;\n }\n case 'ordinal':\n return (item) => ciEquals(readField(def, field, item), value.raw);\n case 'list':\n return (item) => {\n const v = readField(def, field, item);\n return Array.isArray(v) && v.some((el) => ciEquals(el, value.raw));\n };\n case 'timestamp': {\n if (value.type === 'date') {\n const [start, end] = localDayBounds(value);\n return (item) => {\n const t = toEpoch(readField(def, field, item));\n return t !== null && t >= start && t < end;\n };\n }\n throw new CompileError([\n { pos: value.pos, message: `Field \"${field}\" is a timestamp — use a comparison (e.g. ${field} > -36h) or an absolute date (${field}:2026-06-01)` },\n ]);\n }\n case 'duration':\n throw new CompileError([\n { pos: atomPos, message: `Field \"${field}\" is a duration — use a comparison (e.g. ${field} > 3d)` },\n ]);\n }\n}\n\nfunction compileComparison(def: FieldDef, field: string, op: string, value: QueryValue): Predicate {\n const cmp = (a: number, b: number): boolean => {\n switch (op) {\n case '<':\n return a < b;\n case '>':\n return a > b;\n case '<=':\n return a <= b;\n case '>=':\n return a >= b;\n case '=':\n return a === b;\n case '!=':\n return a !== b;\n default:\n return false;\n }\n };\n\n switch (def.kind) {\n case 'number': {\n const n = value.num ?? toNumber(value.raw);\n if (n === null) {\n throw new CompileError([{ pos: value.pos, message: `\"${value.raw}\" is not a number (field \"${field}\")` }]);\n }\n return (item) => {\n const v = toNumber(readField(def, field, item));\n return v !== null && cmp(v, n);\n };\n }\n case 'ordinal': {\n const order = def.order ?? [];\n const idx = order.findIndex((o) => o.toLowerCase() === value.raw.toLowerCase());\n if (idx < 0) {\n throw new CompileError([\n { pos: value.pos, message: `\"${value.raw}\" is not a valid ${field} (expected one of: ${order.join(', ')})` },\n ]);\n }\n return (item) => {\n const raw = readField(def, field, item);\n const vIdx = typeof raw === 'string' ? order.findIndex((o) => o.toLowerCase() === raw.toLowerCase()) : -1;\n return vIdx >= 0 && cmp(vIdx, idx);\n };\n }\n case 'timestamp': {\n if (value.type === 'duration') {\n // relative point in time: bare durations mean \"ago\"\n const sign = value.sign === 0 ? -1 : (value.sign ?? -1);\n const offset = sign * (value.num ?? 0);\n return (item, ctx) => {\n const t = toEpoch(readField(def, field, item));\n return t !== null && cmp(t, ctx.now + offset);\n };\n }\n if (value.type === 'date') {\n const [start, end] = localDayBounds(value);\n return (item) => {\n const t = toEpoch(readField(def, field, item));\n if (t === null) return false;\n switch (op) {\n case '<':\n return t < start;\n case '<=':\n return t < end;\n case '>':\n return t >= end;\n case '>=':\n return t >= start;\n case '=':\n return t >= start && t < end;\n case '!=':\n return t < start || t >= end;\n default:\n return false;\n }\n };\n }\n throw new CompileError([\n { pos: value.pos, message: `Compare timestamp field \"${field}\" to a duration (e.g. -36h) or a date (YYYY-MM-DD)` },\n ]);\n }\n case 'duration': {\n if (value.type !== 'duration') {\n throw new CompileError([\n { pos: value.pos, message: `Compare duration field \"${field}\" to a duration literal (e.g. 3d)` },\n ]);\n }\n const magnitude = value.num ?? 0; // sign ignored: magnitudes have no direction\n return (item) => {\n const v = toNumber(readField(def, field, item));\n return v !== null && cmp(v, magnitude);\n };\n }\n case 'enum':\n case 'string':\n case 'substring':\n case 'list': {\n if (op === '=' ) {\n return compileEquality(def, field, value, value.pos);\n }\n if (op === '!=') {\n const eq = compileEquality(def, field, value, value.pos);\n return (item, ctx) => !eq(item, ctx);\n }\n throw new CompileError([\n { pos: value.pos, message: `Field \"${field}\" does not support ordering comparisons (use \":\" or \"=\").` },\n ]);\n }\n case 'bool': {\n if (op === '=' || op === '!=') {\n const eq = compileEquality(def, field, value, value.pos);\n return op === '=' ? eq : (item, ctx) => !eq(item, ctx);\n }\n throw new CompileError([{ pos: value.pos, message: `Field \"${field}\" is boolean — use ${field}:true / ${field}:false` }]);\n }\n }\n}\n\nfunction compileAtom(atom: AtomNode, registry: FieldRegistry): Predicate {\n const def = resolveField(registry, atom.field);\n if (!def) {\n throw new CompileError([{ pos: atom.pos, message: `Unknown field \"${atom.field}\"` }]);\n }\n if (atom.op === ':') {\n // IN-list: OR of equalities\n const preds = atom.values.map((v) => compileEquality(def, atom.field, v, atom.pos));\n if (preds.length === 1) return preds[0];\n return (item, ctx) => preds.some((p) => p(item, ctx));\n }\n return compileComparison(def, atom.field, atom.op, atom.values[0]);\n}\n\nexport function compileNode(node: QueryNode, registry: FieldRegistry): Predicate {\n switch (node.kind) {\n case 'all':\n return () => true;\n case 'atom':\n return compileAtom(node, registry);\n case 'not': {\n const inner = compileNode(node.child, registry);\n return (item, ctx) => !inner(item, ctx);\n }\n case 'and': {\n const preds = node.children.map((c) => compileNode(c, registry));\n return (item, ctx) => preds.every((p) => p(item, ctx));\n }\n case 'or': {\n const preds = node.children.map((c) => compileNode(c, registry));\n return (item, ctx) => preds.some((p) => p(item, ctx));\n }\n }\n}\n","/**\n * AQL lexer. Tokenizes a query string; positions are byte offsets for\n * structured errors. Browser-safe (no Node APIs).\n */\n\nexport type TokenType =\n | 'IDENT'\n | 'STRING'\n | 'NUMBER'\n | 'DATE'\n | 'DURATION'\n | 'COLON'\n | 'LPAREN'\n | 'RPAREN'\n | 'COMMA'\n | 'OP' // < > <= >= = !=\n | 'MINUS' // negation prefix (`-field:value`)\n | 'STAR'\n | 'AND'\n | 'OR'\n | 'NOT'\n | 'EOF';\n\nexport interface Token {\n type: TokenType;\n /** Source text (for STRING: the unquoted contents). */\n text: string;\n pos: number;\n /** DURATION: magnitude in ms. NUMBER: numeric value. */\n num?: number;\n /** DURATION sign: -1, +1, or 0 (bare). */\n sign?: -1 | 0 | 1;\n}\n\nexport class LexError extends Error {\n constructor(\n public pos: number,\n message: string,\n ) {\n super(message);\n this.name = 'LexError';\n }\n}\n\n/** Duration unit → milliseconds. `m` ≈ 30d (month), `mo` explicit month, `y` ≈ 365d. */\nconst DURATION_MS: Record<string, number> = {\n h: 3_600_000,\n d: 86_400_000,\n w: 7 * 86_400_000,\n m: 30 * 86_400_000,\n mo: 30 * 86_400_000,\n y: 365 * 86_400_000,\n};\n\nconst IDENT_START = /[A-Za-z_]/;\nconst IDENT_CHAR = /[A-Za-z0-9_-]/;\n// Anchored so a trailing digit/dash can't be swallowed into a \"date\" (e.g.\n// `2026-06-1623` must NOT lex as DATE `2026-06-16` + `23`). Calendar validity\n// (month/day ranges) is enforced later in the evaluator with positional errors.\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}(?![\\d-])/;\n\nexport function lex(input: string): Token[] {\n const tokens: Token[] = [];\n let i = 0;\n\n const numberOrDuration = (start: number, sign: -1 | 0 | 1): Token => {\n let j = i;\n while (j < input.length && /\\d/.test(input[j])) j++;\n const digits = input.slice(i, j);\n // unit suffix?\n let unit = '';\n while (j < input.length && /[a-z]/i.test(input[j])) {\n unit += input[j];\n j++;\n }\n i = j;\n if (unit.length > 0) {\n const ms = DURATION_MS[unit.toLowerCase()];\n if (ms === undefined) {\n throw new LexError(start, `Unknown duration unit \"${unit}\" (expected h, d, w, m, mo, or y)`);\n }\n return {\n type: 'DURATION',\n text: input.slice(start, j),\n pos: start,\n num: parseInt(digits, 10) * ms,\n sign,\n };\n }\n if (sign !== 0) {\n // signed bare number — only meaningful as a duration; treat as number with sign applied\n return { type: 'NUMBER', text: input.slice(start, j), pos: start, num: sign * parseInt(digits, 10) };\n }\n return { type: 'NUMBER', text: digits, pos: start, num: parseInt(digits, 10) };\n };\n\n while (i < input.length) {\n const c = input[i];\n const start = i;\n\n if (c === ' ' || c === '\\t' || c === '\\n' || c === '\\r') {\n i++;\n continue;\n }\n if (c === '(') {\n tokens.push({ type: 'LPAREN', text: c, pos: start });\n i++;\n continue;\n }\n if (c === ')') {\n tokens.push({ type: 'RPAREN', text: c, pos: start });\n i++;\n continue;\n }\n if (c === ',') {\n tokens.push({ type: 'COMMA', text: c, pos: start });\n i++;\n continue;\n }\n if (c === ':') {\n tokens.push({ type: 'COLON', text: c, pos: start });\n i++;\n continue;\n }\n if (c === '*') {\n tokens.push({ type: 'STAR', text: c, pos: start });\n i++;\n continue;\n }\n if (c === '<' || c === '>') {\n if (input[i + 1] === '=') {\n tokens.push({ type: 'OP', text: c + '=', pos: start });\n i += 2;\n } else {\n tokens.push({ type: 'OP', text: c, pos: start });\n i++;\n }\n continue;\n }\n if (c === '!') {\n if (input[i + 1] === '=') {\n tokens.push({ type: 'OP', text: '!=', pos: start });\n i += 2;\n continue;\n }\n throw new LexError(start, `Unexpected \"!\" (did you mean \"!=\"?)`);\n }\n if (c === '=') {\n // accept both `=` and `==`\n i += input[i + 1] === '=' ? 2 : 1;\n tokens.push({ type: 'OP', text: '=', pos: start });\n continue;\n }\n if (c === '\"' || c === \"'\") {\n const quote = c;\n let j = i + 1;\n let out = '';\n while (j < input.length && input[j] !== quote) {\n if (input[j] === '\\\\' && j + 1 < input.length) {\n out += input[j + 1];\n j += 2;\n } else {\n out += input[j];\n j++;\n }\n }\n if (j >= input.length) throw new LexError(start, 'Unterminated string literal');\n tokens.push({ type: 'STRING', text: out, pos: start });\n i = j + 1;\n continue;\n }\n if (c === '-' || c === '+') {\n if (/\\d/.test(input[i + 1] ?? '')) {\n const sign = c === '-' ? -1 : 1;\n i++;\n tokens.push(numberOrDuration(start, sign));\n continue;\n }\n if (c === '-') {\n tokens.push({ type: 'MINUS', text: '-', pos: start });\n i++;\n continue;\n }\n throw new LexError(start, 'Unexpected \"+\"');\n }\n if (/\\d/.test(c)) {\n const dateMatch = input.slice(i).match(DATE_RE);\n if (dateMatch) {\n tokens.push({ type: 'DATE', text: dateMatch[0], pos: start });\n i += dateMatch[0].length;\n continue;\n }\n tokens.push(numberOrDuration(start, 0));\n continue;\n }\n if (IDENT_START.test(c)) {\n let j = i + 1;\n while (j < input.length && IDENT_CHAR.test(input[j])) j++;\n const word = input.slice(i, j);\n const kw = word.toLowerCase();\n if (kw === 'and') tokens.push({ type: 'AND', text: word, pos: start });\n else if (kw === 'or') tokens.push({ type: 'OR', text: word, pos: start });\n else if (kw === 'not') tokens.push({ type: 'NOT', text: word, pos: start });\n else tokens.push({ type: 'IDENT', text: word, pos: start });\n i = j;\n continue;\n }\n throw new LexError(start, `Unexpected character \"${c}\"`);\n }\n\n tokens.push({ type: 'EOF', text: '', pos: input.length });\n return tokens;\n}\n","/**\n * AQL recursive-descent parser.\n *\n * Grammar (precedence NOT > AND > OR; implicit AND between adjacent terms):\n * query := orExpr EOF | EOF (empty input → match-all)\n * orExpr := andExpr (OR andExpr)*\n * andExpr := unary ((AND)? unary)* (implicit AND)\n * unary := NOT unary | MINUS atom | primary\n * primary := LPAREN orExpr RPAREN | STAR | atom\n * atom := IDENT (COLON valueOrList | OP value)\n * valueOrList := LPAREN value (COMMA value)* RPAREN | value\n * value := IDENT | STRING | NUMBER | DATE | DURATION\n *\n * Browser-safe (no Node APIs).\n */\n\nimport type { AtomNode, ComparisonOp, QueryError, QueryNode, QueryValue } from './ast.js';\nimport { lex, LexError, type Token, type TokenType } from './lexer.js';\n\nexport class ParseError extends Error {\n constructor(\n public pos: number,\n message: string,\n ) {\n super(message);\n this.name = 'ParseError';\n }\n}\n\nconst VALUE_TOKENS: ReadonlySet<TokenType> = new Set(['IDENT', 'STRING', 'NUMBER', 'DATE', 'DURATION']);\n/** Tokens that can begin a term — used to detect implicit AND. */\nconst TERM_START: ReadonlySet<TokenType> = new Set(['IDENT', 'NOT', 'MINUS', 'LPAREN', 'STAR']);\n\nclass Parser {\n private pos = 0;\n\n constructor(private tokens: Token[]) {}\n\n private peek(): Token {\n return this.tokens[this.pos];\n }\n\n private next(): Token {\n return this.tokens[this.pos++];\n }\n\n private expect(type: TokenType, what: string): Token {\n const tok = this.peek();\n if (tok.type !== type) {\n throw new ParseError(tok.pos, `Expected ${what}, got \"${tok.text || tok.type}\"`);\n }\n return this.next();\n }\n\n parseQuery(): QueryNode {\n if (this.peek().type === 'EOF') return { kind: 'all' };\n const node = this.orExpr();\n const tok = this.peek();\n if (tok.type !== 'EOF') {\n throw new ParseError(tok.pos, `Unexpected \"${tok.text}\" — unbalanced parentheses or stray token`);\n }\n return node;\n }\n\n private orExpr(): QueryNode {\n const children = [this.andExpr()];\n while (this.peek().type === 'OR') {\n this.next();\n children.push(this.andExpr());\n }\n return children.length === 1 ? children[0] : { kind: 'or', children };\n }\n\n private andExpr(): QueryNode {\n const children = [this.unary()];\n for (;;) {\n const tok = this.peek();\n if (tok.type === 'AND') {\n this.next();\n children.push(this.unary());\n } else if (TERM_START.has(tok.type)) {\n // implicit AND: adjacent terms\n children.push(this.unary());\n } else {\n break;\n }\n }\n return children.length === 1 ? children[0] : { kind: 'and', children };\n }\n\n private unary(): QueryNode {\n const tok = this.peek();\n if (tok.type === 'NOT') {\n this.next();\n return { kind: 'not', child: this.unary() };\n }\n if (tok.type === 'MINUS') {\n this.next();\n // `-field:value` sugar — minus binds to a single atom\n const inner = this.peek();\n if (inner.type !== 'IDENT') {\n throw new ParseError(inner.pos, 'Expected a field atom after \"-\" negation');\n }\n return { kind: 'not', child: this.atom() };\n }\n return this.primary();\n }\n\n private primary(): QueryNode {\n const tok = this.peek();\n if (tok.type === 'LPAREN') {\n this.next();\n const node = this.orExpr();\n this.expect('RPAREN', '\")\"');\n return node;\n }\n if (tok.type === 'STAR') {\n this.next();\n return { kind: 'all' };\n }\n if (tok.type === 'IDENT') {\n return this.atom();\n }\n throw new ParseError(tok.pos, `Expected a field, \"(\", \"*\", or NOT — got \"${tok.text || 'end of query'}\"`);\n }\n\n private atom(): AtomNode {\n const fieldTok = this.expect('IDENT', 'a field name');\n const opTok = this.peek();\n\n if (opTok.type === 'COLON') {\n this.next();\n const values = this.valueOrList();\n return { kind: 'atom', field: fieldTok.text, op: ':', values, pos: fieldTok.pos };\n }\n if (opTok.type === 'OP') {\n this.next();\n const value = this.value();\n return {\n kind: 'atom',\n field: fieldTok.text,\n op: opTok.text as ComparisonOp,\n values: [value],\n pos: fieldTok.pos,\n };\n }\n throw new ParseError(\n opTok.pos,\n `Expected \":\" or a comparison operator after field \"${fieldTok.text}\"`,\n );\n }\n\n private valueOrList(): QueryValue[] {\n if (this.peek().type === 'LPAREN') {\n this.next();\n const values = [this.value()];\n while (this.peek().type === 'COMMA') {\n this.next();\n values.push(this.value());\n }\n this.expect('RPAREN', '\")\" to close the value list');\n return values;\n }\n return [this.value()];\n }\n\n private value(): QueryValue {\n const tok = this.peek();\n if (!VALUE_TOKENS.has(tok.type)) {\n throw new ParseError(tok.pos, `Expected a value, got \"${tok.text || tok.type}\"`);\n }\n this.next();\n switch (tok.type) {\n case 'STRING':\n return { type: 'string', raw: tok.text, pos: tok.pos };\n case 'NUMBER':\n return { type: 'number', raw: tok.text, num: tok.num, pos: tok.pos };\n case 'DATE':\n return { type: 'date', raw: tok.text, pos: tok.pos };\n case 'DURATION':\n return { type: 'duration', raw: tok.text, num: tok.num, sign: tok.sign ?? 0, pos: tok.pos };\n default:\n return { type: 'word', raw: tok.text, pos: tok.pos };\n }\n }\n}\n\n/** Parse a query string. Returns the AST or structured errors (never throws). */\nexport function parseQuery(input: string): { ast: QueryNode; errors: [] } | { ast: null; errors: QueryError[] } {\n try {\n const tokens = lex(input);\n const ast = new Parser(tokens).parseQuery();\n return { ast, errors: [] };\n } catch (err) {\n if (err instanceof LexError || err instanceof ParseError) {\n return { ast: null, errors: [{ pos: err.pos, message: err.message }] };\n }\n throw err;\n }\n}\n","/**\n * AQL — Assignment Query Language. Public surface.\n *\n * One engine, many consumers: derive rules (phase ladder / disposition),\n * `syntaur ls --query`, and dashboard filters all share this module.\n * Browser-safe: no Node-only imports anywhere under `src/utils/query/`.\n */\n\nimport type { QueryError, QueryNode } from './ast.js';\nimport { compileNode, CompileError, type EvalContext, type Predicate } from './evaluate.js';\nimport { ASSIGNMENT_FIELDS, type FieldRegistry, type QueryItem } from './fields.js';\nimport { parseQuery } from './parser.js';\n\nexport type { QueryError, QueryNode, ComparisonOp } from './ast.js';\nexport { lex, LexError } from './lexer.js';\nexport type { Token, TokenType } from './lexer.js';\nexport { parseQuery, ParseError } from './parser.js';\nexport { compileNode, CompileError } from './evaluate.js';\nexport type { EvalContext, Predicate } from './evaluate.js';\nexport {\n ASSIGNMENT_FIELDS,\n PRIORITY_ORDER,\n resolveField,\n readField,\n} from './fields.js';\nexport type { FieldDef, FieldKind, FieldRegistry, QueryItem } from './fields.js';\n\nexport interface CompiledQuery {\n predicate: Predicate;\n ast: QueryNode;\n}\n\n/**\n * Parse + compile a query against a field registry. Returns the compiled\n * predicate or structured errors (never throws on user input).\n */\nexport function compileQuery(\n input: string,\n registry: FieldRegistry = ASSIGNMENT_FIELDS,\n): { query: CompiledQuery; errors: [] } | { query: null; errors: QueryError[] } {\n const parsed = parseQuery(input);\n if (!parsed.ast) return { query: null, errors: parsed.errors };\n try {\n const predicate = compileNode(parsed.ast, registry);\n return { query: { predicate, ast: parsed.ast }, errors: [] };\n } catch (err) {\n if (err instanceof CompileError) return { query: null, errors: err.errors };\n throw err;\n }\n}\n\n/** Validate a query (parse + field check) without evaluating — for doctor/config checks. */\nexport function validateQuery(input: string, registry: FieldRegistry = ASSIGNMENT_FIELDS): QueryError[] {\n return compileQuery(input, registry).errors;\n}\n\n/** Convenience: filter a list of items with a compiled query. */\nexport function runQuery(items: QueryItem[], compiled: CompiledQuery, ctx: EvalContext): QueryItem[] {\n return items.filter((item) => compiled.predicate(item, ctx));\n}\n","/**\n * Browser-safe fact-vocabulary module.\n *\n * Extracted for the same reason as `saved-view-builder.ts`: both the dashboard\n * (Vite/browser build, `@shared/*` alias) and Node-side modules need these\n * definitions. The ONLY import here is from `./query/index.js` — no\n * Node-coupled modules (no `config.ts`, no `fs`, no `path`).\n *\n * Consumers (`src/lifecycle/derive.ts`, `src/utils/config.ts`) re-export\n * everything from here so no existing import path needs to change.\n */\n\nimport { ASSIGNMENT_FIELDS, type FieldRegistry } from './query/index.js';\n\n// ── re-exported type (lives in config.ts; declared here for browser consumers) ──\n\n/**\n * A VALIDATED custom-fact declaration (strict union). bool/number facts are\n * asserted values stored in the `facts:` frontmatter map; attestation facts\n * model \"agent reviewed revision with verdict\" and carry a revision binding.\n *\n * Re-declared here (mirroring `config.ts`) so the dashboard can import this\n * type without pulling in any Node-only module.\n */\nexport type FactDeclaration =\n | { name: string; type: 'bool' | 'number' }\n | { name: string; type: 'attestation'; binds: 'plan' | 'commit' | 'none' };\n\n// ── DERIVE_FIELDS ─────────────────────────────────────────────────────────────\n\n/**\n * Registry for derive conditions: facts only. Deliberately excludes\n * timestamps/durations (statusAge, created, …) and identity fields — a derive\n * rule referencing them fails validation, implementing \"time-based facts are\n * payload-only flags\" with teeth.\n */\nexport const DERIVE_FIELDS: FieldRegistry = {\n hasrealobjective: { kind: 'bool', get: (i) => i['hasRealObjective'] },\n acrealtotal: { kind: 'number', get: (i) => i['acRealTotal'] },\n acrealchecked: { kind: 'number', get: (i) => i['acRealChecked'] },\n acallchecked: { kind: 'bool', get: (i) => i['acAllChecked'] },\n planexists: { kind: 'bool', get: (i) => i['planExists'] },\n planapproved: { kind: 'bool', get: (i) => i['planApproved'] },\n workspaceset: { kind: 'bool', get: (i) => i['workspaceSet'] },\n implementationstarted: { kind: 'bool', get: (i) => i['implementationStarted'] },\n depssatisfied: { kind: 'bool', get: (i) => i['depsSatisfied'] },\n unresolvedquestions: { kind: 'number', get: (i) => i['unresolvedQuestions'] },\n blocked: { kind: 'bool' },\n parked: { kind: 'bool' },\n reviewrequested: { kind: 'bool', get: (i) => i['reviewRequested'] },\n pinned: { kind: 'bool' },\n};\n\n// ── FactFieldNames ─────────────────────────────────────────────────────────────\n\n/** Canonical export/registry names for one fact declaration. */\nexport interface FactFieldNames {\n /** Storage key in the `facts:` map = declared name verbatim. */\n storageKey: string;\n /** camelCase exported fact keys (attestations use all five; bool/number\n * only use `fact`). */\n exports: {\n fact: string;\n approved: string;\n changesRequested: string;\n by: string;\n approvedBy: string;\n };\n /** Lowercased registry keys this declaration contributes (1 for bool/number,\n * 5 for attestation) — the collision unit. */\n registryKeys: string[];\n}\n\n/**\n * THE one canonical naming helper (Locked Decisions): every consumer derives\n * fact field names here so no path invents its own variant. For bool/number\n * the single export is `<name>`; for attestation the five exports are `<name>`,\n * `<name>Approved`, `<name>ChangesRequested`, `<name>By`, `<name>ApprovedBy`.\n */\nexport function factFieldNames(decl: FactDeclaration): FactFieldNames {\n const name = decl.name;\n const exportNames = {\n fact: name,\n approved: `${name}Approved`,\n changesRequested: `${name}ChangesRequested`,\n by: `${name}By`,\n approvedBy: `${name}ApprovedBy`,\n };\n const registryKeys =\n decl.type === 'attestation'\n ? [\n exportNames.fact,\n exportNames.approved,\n exportNames.changesRequested,\n exportNames.by,\n exportNames.approvedBy,\n ].map((k) => k.toLowerCase())\n : [exportNames.fact.toLowerCase()];\n return { storageKey: name, exports: exportNames, registryKeys };\n}\n\n// ── RawFactDeclaration ─────────────────────────────────────────────────────────\n\n/**\n * A custom-fact declaration EXACTLY as parsed from `statuses.facts` — loose\n * parse (Locked Decisions): every field is a raw string so user input\n * round-trips through serialization even when invalid. The strict\n * {@link FactDeclaration} is derived from this via {@link normalizeFactDeclarations}\n * (defined in `config.ts`, which imports this type).\n */\nexport interface RawFactDeclaration {\n name: string;\n type: string;\n binds: string | null;\n}\n\n// ── normalizeFactDeclarations ─────────────────────────────────────────────────\n\n/**\n * Narrow raw declarations to the strict union, DROPPING malformed rows (bad\n * name format / unknown type / invalid binds) — never throws. The single\n * bridge every consumer crosses before the collision filter\n * (`acceptFactDeclarations`). `validateFactDeclarations` diagnoses the raw rows\n * so doctor can report exactly what this drops.\n *\n * Lives here (browser-safe) so both the Node side and the dashboard client can\n * run the identical normalize→accept pipeline; re-exported from `config.ts` so\n * existing Node-side imports from `config.js` keep resolving.\n */\nexport function normalizeFactDeclarations(\n raw: RawFactDeclaration[] | null | undefined,\n): FactDeclaration[] {\n const out: FactDeclaration[] = [];\n for (const row of raw ?? []) {\n if (!row || typeof row.name !== 'string') continue;\n const name = row.name.trim();\n if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) continue;\n const type = (row.type ?? '').trim();\n if (type === 'bool' || type === 'number') {\n out.push({ name, type });\n } else if (type === 'attestation') {\n const binds = (row.binds ?? 'none').toString().trim() || 'none';\n if (binds === 'plan' || binds === 'commit' || binds === 'none') {\n out.push({ name, type: 'attestation', binds });\n }\n }\n }\n return out;\n}\n\n// ── validateFactDeclarations ──────────────────────────────────────────────────\n\n/**\n * Diagnose RAW fact declarations: returns one human-readable problem\n * per malformed/colliding row — exactly what the normalize→accept pipeline will\n * drop, so doctor reports nothing silently. Checks name format, type/binds, and\n * case-insensitive collision of EVERY exported key (the declared name and, for\n * attestations, all four generated names) against `DERIVE_FIELDS`,\n * `ASSIGNMENT_FIELDS`, or an earlier declaration's exported keys. Works with\n * `derive: null` — declarations validate independently of derive rules.\n *\n * KEPT IN LOCKSTEP with {@link acceptFactDeclarations} so doctor reports\n * precisely what the runtime drops. If you change collision semantics here,\n * change them there.\n */\nexport function validateFactDeclarations(raw: RawFactDeclaration[]): string[] {\n const problems: string[] = [];\n // Owner of each lowercased key: 'built-in' or the declaring fact name. Mirrors\n // acceptFactDeclarations EXACTLY so doctor reports precisely what the runtime\n // drops — atomic per declaration (built-ins always win, first-declared wins),\n // and a declaration's keys are reserved ONLY when the whole declaration is\n // accepted (a built-in-collided declaration reserves nothing, so a later\n // declaration the runtime accepts is not falsely flagged).\n const owners = new Map<string, string>(); // lowercased key → 'built-in' | <fact name>\n for (const key of Object.keys(DERIVE_FIELDS)) owners.set(key, 'built-in');\n for (const key of Object.keys(ASSIGNMENT_FIELDS)) owners.set(key, 'built-in');\n\n for (const row of raw ?? []) {\n const name = (row?.name ?? '').trim();\n if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) {\n problems.push(\n `fact \"${row?.name ?? ''}\": invalid name — must match /^[a-z][a-zA-Z0-9]*$/`,\n );\n continue;\n }\n const type = (row.type ?? '').trim();\n if (type !== 'bool' && type !== 'number' && type !== 'attestation') {\n problems.push(\n `fact \"${name}\": invalid type \"${row.type ?? ''}\" — expected bool, number, or attestation`,\n );\n continue;\n }\n if (type === 'attestation') {\n const binds = (row.binds ?? 'none').toString().trim() || 'none';\n if (binds !== 'plan' && binds !== 'commit' && binds !== 'none') {\n problems.push(\n `fact \"${name}\": invalid binds \"${row.binds}\" — expected plan, commit, or none`,\n );\n continue;\n }\n }\n // Collision check across ALL exported keys (binds is irrelevant to names).\n const decl: FactDeclaration =\n type === 'attestation' ? { name, type, binds: 'none' } : { name, type };\n const keys = factFieldNames(decl).registryKeys;\n const collidingKey = keys.find((k) => owners.has(k));\n if (collidingKey !== undefined) {\n const owner = owners.get(collidingKey)!;\n if (owner === 'built-in') {\n problems.push(`fact \"${name}\": exported field \"${collidingKey}\" collides with a built-in field`);\n } else if (owner === name) {\n problems.push(`fact \"${name}\": duplicate declaration (a fact named \"${name}\" is already declared)`);\n } else {\n problems.push(`fact \"${name}\": exported field \"${collidingKey}\" collides with fact \"${owner}\"`);\n }\n continue; // atomic: reserve NOTHING for a rejected declaration\n }\n for (const key of keys) owners.set(key, name);\n }\n return problems;\n}\n\n// ── acceptFactDeclarations ────────────────────────────────────────────────────\n\n/**\n * THE one collision filter (Locked Decisions): drop any declaration whose\n * registry keys collide (case-insensitively) with a built-in field\n * (`DERIVE_FIELDS` ∪ `ASSIGNMENT_FIELDS`) or an earlier-accepted declaration.\n * Built-ins always win; first-declared wins among duplicates. Never throws — a\n * bad config can't brick recompute; doctor (Task 4) surfaces the same collisions\n * as errors. Returns the ACCEPTED list every consumer builds from.\n *\n * KEPT IN LOCKSTEP with {@link validateFactDeclarations}.\n */\nexport function acceptFactDeclarations(declarations: FactDeclaration[]): FactDeclaration[] {\n // DERIVE_FIELDS / ASSIGNMENT_FIELDS keys are already lowercase.\n const taken = new Set<string>([\n ...Object.keys(DERIVE_FIELDS),\n ...Object.keys(ASSIGNMENT_FIELDS),\n ]);\n const accepted: FactDeclaration[] = [];\n for (const decl of declarations) {\n const keys = factFieldNames(decl).registryKeys;\n if (keys.some((k) => taken.has(k))) continue; // collision — drop\n for (const k of keys) taken.add(k);\n accepted.push(decl);\n }\n return accepted;\n}\n\n// ── addFactFields ─────────────────────────────────────────────────────────────\n\n/** Add one accepted declaration's fields to a registry (shared by derive +\n * query registry builders so both speak the identical vocabulary). */\nexport function addFactFields(registry: FieldRegistry, decl: FactDeclaration): void {\n const names = factFieldNames(decl);\n if (decl.type === 'attestation') {\n registry[names.exports.fact.toLowerCase()] = { kind: 'bool', get: (i) => i[names.exports.fact] };\n registry[names.exports.approved.toLowerCase()] = {\n kind: 'bool',\n get: (i) => i[names.exports.approved],\n };\n registry[names.exports.changesRequested.toLowerCase()] = {\n kind: 'bool',\n get: (i) => i[names.exports.changesRequested],\n };\n // actor sets register as `list` — `:` equality + IN lists already have\n // contains semantics there (query/fields.ts kind 'list').\n registry[names.exports.by.toLowerCase()] = { kind: 'list', get: (i) => i[names.exports.by] };\n registry[names.exports.approvedBy.toLowerCase()] = {\n kind: 'list',\n get: (i) => i[names.exports.approvedBy],\n };\n } else {\n registry[names.exports.fact.toLowerCase()] = {\n kind: decl.type,\n get: (i) => i[names.exports.fact],\n };\n }\n}\n\n// ── buildDeriveRegistry / buildQueryRegistry ──────────────────────────────────\n\n/**\n * Build the DERIVE registry (facts only) from the ACCEPTED declaration list.\n * Callers run the normalize→accept pipeline first and build ONE registry per\n * config resolution (so the WeakMap compile cache stays warm across sweeps).\n */\nexport function buildDeriveRegistry(accepted: FactDeclaration[]): FieldRegistry {\n const registry: FieldRegistry = { ...DERIVE_FIELDS };\n for (const decl of accepted) addFactFields(registry, decl);\n return registry;\n}\n\n/**\n * Build the QUERY registry (full assignment vocabulary) from the ACCEPTED list —\n * custom entries merged over `ASSIGNMENT_FIELDS` for ls/dashboard query paths.\n * Same accepted input, same entries as {@link buildDeriveRegistry}.\n */\nexport function buildQueryRegistry(accepted: FactDeclaration[]): FieldRegistry {\n const registry: FieldRegistry = { ...ASSIGNMENT_FIELDS };\n for (const decl of accepted) addFactFields(registry, decl);\n return registry;\n}\n\n// ── queryFieldNames ───────────────────────────────────────────────────────────\n\n/**\n * The canonical camelCase field names available for AQL autocomplete in the\n * dashboard. Returns the static built-in list PLUS each declaration's exported\n * field names from `factFieldNames`.\n *\n * NOTE: the built-in list is a hand-maintained camelCase display mapping of the\n * `ASSIGNMENT_FIELDS` registry keys (which are lowercase, e.g. `completedat` →\n * `completedAt`). It is NOT derived at runtime, so it must be kept in sync with\n * `query/fields.ts` whenever a built-in queryable field is added or renamed.\n */\nexport function queryFieldNames(declarations: FactDeclaration[]): string[] {\n // Hand-maintained camelCase display names for the lowercase ASSIGNMENT_FIELDS\n // registry keys (fields.ts). Keep in sync with that registry.\n const builtins: string[] = [\n 'status',\n 'priority',\n 'type',\n 'assignee',\n 'project',\n 'tag',\n 'tags',\n 'archived',\n 'title',\n 'search',\n 'created',\n 'updated',\n 'completedAt',\n 'statusAge',\n 'phase',\n 'disposition',\n 'phaseAge',\n 'hasRealObjective',\n 'acRealTotal',\n 'acRealChecked',\n 'acAllChecked',\n 'planExists',\n 'planApproved',\n 'workspaceSet',\n 'implementationStarted',\n 'depsSatisfied',\n 'unresolvedQuestions',\n 'progressStaleDays',\n 'blocked',\n 'parked',\n 'reviewRequested',\n 'pinned',\n ];\n\n const custom: string[] = [];\n for (const decl of declarations) {\n const names = factFieldNames(decl);\n if (decl.type === 'attestation') {\n custom.push(\n names.exports.fact,\n names.exports.approved,\n names.exports.changesRequested,\n names.exports.by,\n names.exports.approvedBy,\n );\n } else {\n custom.push(names.exports.fact);\n }\n }\n\n return [...builtins, ...custom];\n}\n","/**\n * Search-config schema — the shape behind the `search:` block in\n * `~/.syntaur/config.md` and the command palette's customizable behavior.\n *\n * Browser-safe: no Node-only imports. Consumed by both the server\n * (`src/dashboard/api-search-config.ts`, `src/utils/config.ts`) and the SPA\n * (via the `@shared/search-schema` alias) so palette aliases, default scope, and\n * external-ID indexing validate against ONE source of truth.\n *\n * See `claude-info/plans/2026-06-15-command-palette-ui-design.md`.\n */\n\n/** The five searchable entity kinds an alias prefix can target. */\nexport type EntityKind = 'assignment' | 'project' | 'todo' | 'server' | 'playbook';\n\nexport const ENTITY_KINDS: readonly EntityKind[] = [\n 'assignment',\n 'project',\n 'todo',\n 'server',\n 'playbook',\n];\n\n/** Default search scope: `all` (everything) or one entity kind. */\nexport type DefaultScope = 'all' | EntityKind;\n\nexport interface SearchConfig {\n /** With no explicit type prefix, inject an implicit `kind:<scope>` gate. `all` = no gate. */\n defaultScope: DefaultScope;\n /** Prefix → entity kind. Replaces the hardcoded palette `TYPE_ALIASES`. */\n aliases: Record<string, EntityKind>;\n /** Fold external IDs into the index + enable `jira:`/`externalid:`/bare-ID matching. */\n externalIds: boolean;\n}\n\nexport const DEFAULT_SEARCH_CONFIG: SearchConfig = {\n defaultScope: 'all',\n aliases: { a: 'assignment', p: 'project', t: 'todo', s: 'server', pb: 'playbook' },\n externalIds: true,\n};\n\n/**\n * Palette AQL field names an alias prefix may NOT shadow (mirrors the keys of\n * `PALETTE_FIELDS` in `dashboard/src/hotkeys/paletteQuery.ts`). Declared here —\n * browser-safe and dependency-free — so the server router and the live SPA\n * validation share exactly one collision set (the server cannot import dashboard\n * code). Keep in sync with `PALETTE_FIELDS`.\n */\nexport const SEARCH_FIELD_NAMES: readonly string[] = [\n 'kind',\n 'status',\n 'tag',\n 'tags',\n 'assignee',\n 'type',\n 'project',\n 'externalid',\n 'jira',\n 'title',\n 'search',\n];\n\n/** Reserved escape prefix — `all:` searches everything; disallowed as a custom alias. */\nexport const RESERVED_ALIAS = 'all';\n\n/** An alias key must be lowercase, start with a letter, then letters/digits. */\nconst ALIAS_KEY_RE = /^[a-z][a-z0-9]*$/;\n\nfunction isEntityKind(value: unknown): value is EntityKind {\n return typeof value === 'string' && (ENTITY_KINDS as readonly string[]).includes(value);\n}\n\nfunction isDefaultScope(value: unknown): value is DefaultScope {\n return value === 'all' || isEntityKind(value);\n}\n\n/**\n * Coerce an arbitrary parsed object into a valid `SearchConfig`, filling defaults\n * for missing/invalid fields. Lenient (read/persist path): invalid alias entries\n * are dropped rather than rejected — the strict gate is `validateAliases`, used by\n * the POST router. An explicitly-present-but-empty `aliases` map stays empty; a\n * missing `aliases` falls back to the defaults.\n */\nexport function normalizeSearchConfig(raw: unknown): SearchConfig {\n if (!raw || typeof raw !== 'object') {\n return cloneDefaultSearchConfig();\n }\n const r = raw as Record<string, unknown>;\n\n const defaultScope: DefaultScope = isDefaultScope(r['defaultScope'])\n ? r['defaultScope']\n : DEFAULT_SEARCH_CONFIG.defaultScope;\n\n const externalIds =\n typeof r['externalIds'] === 'boolean'\n ? r['externalIds']\n : DEFAULT_SEARCH_CONFIG.externalIds;\n\n let aliases: Record<string, EntityKind>;\n if (r['aliases'] && typeof r['aliases'] === 'object') {\n aliases = {};\n for (const [key, value] of Object.entries(r['aliases'] as Record<string, unknown>)) {\n if (\n ALIAS_KEY_RE.test(key) &&\n key !== RESERVED_ALIAS &&\n !SEARCH_FIELD_NAMES.includes(key) &&\n isEntityKind(value)\n ) {\n aliases[key] = value;\n }\n }\n } else {\n aliases = { ...DEFAULT_SEARCH_CONFIG.aliases };\n }\n\n return { defaultScope, aliases, externalIds };\n}\n\n/**\n * Strict alias validation (POST path). Each key must be lowercase `[a-z][a-z0-9]*`,\n * must not be the reserved `all`, must not collide with a `SEARCH_FIELD_NAMES`\n * member, and must map to one of the five entity kinds. Returns every violation so\n * the router can 400 with the full list and the SPA can show inline feedback.\n */\nexport function validateAliases(\n aliases: unknown,\n): { ok: true } | { ok: false; errors: string[] } {\n if (!aliases || typeof aliases !== 'object') {\n return { ok: false, errors: ['aliases must be an object'] };\n }\n const errors: string[] = [];\n for (const [key, value] of Object.entries(aliases as Record<string, unknown>)) {\n if (!ALIAS_KEY_RE.test(key)) {\n errors.push(`alias key \"${key}\" must be lowercase and match [a-z][a-z0-9]*`);\n }\n if (key === RESERVED_ALIAS) {\n errors.push(`alias key \"${RESERVED_ALIAS}\" is reserved (the \"search everything\" escape)`);\n }\n if (SEARCH_FIELD_NAMES.includes(key)) {\n errors.push(`alias key \"${key}\" collides with the reserved field name \"${key}\"`);\n }\n if (!isEntityKind(value)) {\n errors.push(`alias \"${key}\" must map to one of: ${ENTITY_KINDS.join(', ')}`);\n }\n }\n return errors.length === 0 ? { ok: true } : { ok: false, errors };\n}\n\n/** Validate a default-scope value (POST path). */\nexport function isValidDefaultScope(value: unknown): value is DefaultScope {\n return isDefaultScope(value);\n}\n\nfunction cloneDefaultSearchConfig(): SearchConfig {\n return {\n defaultScope: DEFAULT_SEARCH_CONFIG.defaultScope,\n aliases: { ...DEFAULT_SEARCH_CONFIG.aliases },\n externalIds: DEFAULT_SEARCH_CONFIG.externalIds,\n };\n}\n","/**\n * Shared schema + pure helpers for the left-nav workspace-visibility preference.\n *\n * The preference is a BLOCKLIST of hidden workspace names: a workspace whose\n * name is absent from the list is shown. Newly created/discovered workspaces\n * therefore default to visible — hiding is strictly opt-in.\n *\n * This module is dependency-free (no imports from config.ts) so it can be\n * consumed by both the CLI/backend and the dashboard (via the `@shared` alias)\n * and unit-tested in the node-env vitest setup.\n */\n\n/**\n * The reserved name for the synthesized \"Ungrouped\" pseudo-workspace shown in\n * the sidebar when there are standalone projects/assignments with no workspace.\n * It is never returned by `listWorkspaces()`; the sidebar appends it. It is\n * treated as an ordinary blocklist member, so it can be hidden like any other.\n */\nexport const UNGROUPED_WORKSPACE = '_ungrouped' as const;\n\nexport interface WorkspaceVisibilityConfig {\n /** Names of workspaces hidden from the left nav. Absent = visible. */\n hidden: string[];\n}\n\n/**\n * Upper bound on a single workspace name. Real workspace slugs/group names are\n * short; this only guards against a pathological config.md entry.\n */\nexport const MAX_WORKSPACE_NAME_LENGTH = 256;\n\n/**\n * Normalize a raw blocklist (from disk, an API body, or a fetch response) into\n * a clean `string[]`: keep only strings, trim each, drop empties, anything\n * containing a line break, and absurdly long names, then dedupe preserving\n * first-seen order. Used on both the server (POST validation) and the client\n * (response `normalize`) so the rules are identical on both sides.\n */\nexport function normalizeHiddenList(input: unknown): string[] {\n if (!Array.isArray(input)) return [];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const raw of input) {\n if (typeof raw !== 'string') continue;\n const name = raw.trim();\n if (name.length === 0) continue;\n if (name.length > MAX_WORKSPACE_NAME_LENGTH) continue;\n if (/[\\r\\n]/.test(name)) continue;\n if (seen.has(name)) continue;\n seen.add(name);\n out.push(name);\n }\n return out;\n}\n\n/**\n * Pure filter: return `all` minus any name present in `hidden`, preserving the\n * input order. `_ungrouped` is filtered like any other name (no special case).\n */\nexport function visibleWorkspaces(all: string[], hidden: string[]): string[] {\n if (hidden.length === 0) return [...all];\n const blocked = new Set(hidden);\n return all.filter((name) => !blocked.has(name));\n}\n\n/** True when `name` is present in the blocklist. */\nexport function isWorkspaceHidden(name: string, hidden: string[]): boolean {\n return hidden.includes(name);\n}\n","import { readFile } from 'node:fs/promises';\nimport { spawnSync } from 'node:child_process';\nimport { resolve, isAbsolute } from 'node:path';\nimport { syntaurRoot, defaultProjectDir, expandHome } from './paths.js';\nimport { fileExists, writeFileForce } from './fs.js';\nimport { renderConfig } from '../templates/config.js';\nimport { migrateLegacyConfig } from './fs-migration.js';\nimport { DEFAULT_STATUSES, DEFAULT_TRANSITION_TABLE } from '../lifecycle/index.js';\nimport {\n BINDABLE_ACTION_KINDS,\n canonicalizeCombo,\n isBindableActionKind,\n isReservedCombo,\n type BindableActionKind,\n} from './hotkeysCatalog.js';\nimport {\n AGENT_ID_PATTERN,\n BUILTIN_AGENTS,\n PROMPT_ARG_POSITIONS,\n type AgentConfig,\n type PromptArgPosition,\n type SessionInvocation,\n} from './agents-schema.js';\nimport { isValidSlug } from './slug.js';\nimport {\n type FactDeclaration,\n type RawFactDeclaration,\n} from './fact-registry.js';\n\nexport {\n AGENT_ID_PATTERN,\n BUILTIN_AGENTS,\n PROMPT_ARG_POSITIONS,\n type AgentConfig,\n type PromptArgPosition,\n type SessionInvocation,\n};\n\nexport interface StatusDefinition {\n id: string;\n label: string;\n description?: string;\n color?: string;\n icon?: string;\n terminal?: boolean;\n}\n\nexport interface StatusTransition {\n from: string;\n command: string;\n to: string;\n label?: string;\n description?: string;\n requiresReason?: boolean;\n}\n\n/**\n * Derive-status primitives ({@link PhaseRung}, {@link DispositionRule},\n * {@link HeadlineProjection}, {@link DeriveConfig}, {@link DEFAULT_DERIVE_CONFIG},\n * {@link validateDeriveConfig}) live in the browser-safe `derive-config.ts` so\n * the dashboard client can alias and reuse them; imported for local use here and\n * re-exported so existing Node-side imports from `config.js` keep resolving.\n */\nimport {\n DEFAULT_DERIVE_CONFIG,\n validateDeriveConfig,\n validateDeriveShape,\n type PhaseRung,\n type DispositionRule,\n type HeadlineProjection,\n type DeriveConfig,\n} from './derive-config.js';\n\nexport { DEFAULT_DERIVE_CONFIG, validateDeriveConfig, validateDeriveShape };\nexport type { PhaseRung, DispositionRule, HeadlineProjection, DeriveConfig };\n\nimport type { StaleThresholds } from '../staleness/classify.js';\n\n/** Config keys for the `staleness:` block → `StaleThresholds` ms fields. Keyed\n * on the contradiction (phase/disposition), not raw status ids. */\nconst STALENESS_KEY_TO_FIELD: Record<string, keyof StaleThresholds> = {\n inProgressNoActivity: 'inProgressNoActivityMs',\n readyUnclaimed: 'readyUnclaimedMs',\n reviewAging: 'reviewAgingMs',\n blockedAging: 'blockedAgingMs',\n planApprovalAging: 'planApprovalAgingMs',\n};\n\nconst DURATION_RE = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)?$/;\nconst DURATION_UNIT_MS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\n/** Parse a duration like `7d`/`12h`/`30m`/`90s`/`500ms` (or a bare number = ms)\n * to milliseconds. Returns null when malformed or non-positive. */\nexport function parseDurationMs(raw: string): number | null {\n const m = raw.trim().match(DURATION_RE);\n if (!m) return null;\n const n = Number(m[1]);\n if (!Number.isFinite(n) || n <= 0) return null;\n return n * DURATION_UNIT_MS[m[2] ?? 'ms'];\n}\n\n/**\n * A custom-fact declaration EXACTLY as parsed from `statuses.facts` — loose\n * parse (Locked Decisions): every field is a raw string so user input\n * round-trips through serialization even when invalid. The strict\n * {@link FactDeclaration} is derived from this via {@link normalizeFactDeclarations}.\n *\n * Defined in `fact-registry.ts` (browser-safe); re-exported here so existing\n * Node-side imports from `config.js` keep resolving.\n */\nexport type { RawFactDeclaration } from './fact-registry.js';\n\n/**\n * A VALIDATED custom-fact declaration (strict union). bool/number facts are\n * asserted values stored in the `facts:` frontmatter map; attestation facts\n * model \"agent reviewed revision with verdict\" and carry a revision binding.\n *\n * Defined in `fact-registry.ts` (browser-safe); re-exported here so existing\n * Node-side imports from `config.js` keep resolving.\n */\nexport type { FactDeclaration } from './fact-registry.js';\nexport { validateFactDeclarations, normalizeFactDeclarations } from './fact-registry.js';\n\nexport interface StatusConfig {\n statuses: StatusDefinition[];\n order: string[];\n transitions: StatusTransition[];\n /** Derived-status rules (v3). Null/absent → DEFAULT_DERIVE_CONFIG at resolve\n * time. Persisted under `statuses:` so the Settings writer round-trips it. */\n derive?: DeriveConfig | null;\n /** Custom-fact declarations (raw — see {@link RawFactDeclaration}). Persisted\n * under `statuses.facts`; preserved verbatim so invalid rows round-trip and\n * doctor can diagnose them. Null/absent → no custom vocabulary. */\n facts?: RawFactDeclaration[] | null;\n}\n\nexport interface TypeDefinition {\n id: string;\n label?: string;\n description?: string;\n color?: string;\n icon?: string;\n}\n\nexport interface TypesConfig {\n definitions: TypeDefinition[];\n default: string;\n}\n\nexport const DEFAULT_ASSIGNMENT_TYPES: TypesConfig = {\n definitions: [\n { id: 'feature', label: 'Feature' },\n { id: 'bug', label: 'Bug' },\n { id: 'refactor', label: 'Refactor' },\n { id: 'research', label: 'Research' },\n { id: 'chore', label: 'Chore' },\n ],\n default: 'feature',\n};\n\nexport interface IntegrationConfig {\n claudePluginDir: string | null;\n codexPluginDir: string | null;\n codexMarketplacePath: string | null;\n // Per-agent cross-agent install records (pi, hermes, openclaw, ...). Optional\n // so existing `IntegrationConfig` literals (and the default config) need no\n // change. Serialized as flat `installedAgents.<id>: <scope>` keys inside the\n // `integrations:` block (the frontmatter parser only flattens two levels).\n installedAgents?: Record<string, { scope: 'project' | 'global' }>;\n}\n\nexport interface OnboardingConfig {\n completed: boolean;\n}\n\nexport interface BackupConfig {\n repo: string | null;\n categories: string;\n lastBackup: string | null;\n lastRestore: string | null;\n}\n\nexport type AutoCreateWorktree = 'skip' | 'ask' | 'always';\n\nexport interface PlaybooksConfig {\n disabled: string[];\n}\n\nexport interface ThemeConfig {\n preset: string;\n}\n\nexport interface HotkeyBindingsConfig {\n bindings: Partial<Record<BindableActionKind, string>>;\n}\n\nimport { TERMINAL_CHOICES, type TerminalChoice } from './terminal-schema.js';\nimport {\n DEFAULT_SEARCH_CONFIG,\n normalizeSearchConfig,\n type SearchConfig,\n} from './search-schema.js';\nexport { TERMINAL_CHOICES, type TerminalChoice };\n\nimport {\n normalizeHiddenList,\n type WorkspaceVisibilityConfig,\n} from './workspace-visibility-schema.js';\nexport { type WorkspaceVisibilityConfig };\n\n/**\n * Automatic session tracking scope:\n * - `all`: every discovered/hooked session is written to the sessions DB.\n * - `workspaces-only`: only sessions whose cwd has `.syntaur/context.json`.\n * - `off`: no automatic DB writes (manual `track-session` still works).\n */\nexport type SessionAutoTrack = 'all' | 'workspaces-only' | 'off';\n\nexport interface SyntaurConfig {\n version: string;\n defaultProjectDir: string;\n onboarding: OnboardingConfig;\n agentDefaults: {\n trustLevel: 'low' | 'medium' | 'high';\n autoApprove: boolean;\n autoCreateWorktree: AutoCreateWorktree;\n };\n session: {\n autoTrack: SessionAutoTrack;\n };\n integrations: IntegrationConfig;\n backup: BackupConfig | null;\n statuses: StatusConfig | null;\n types: TypesConfig | null;\n agents: AgentConfig[] | null;\n playbooks: PlaybooksConfig;\n theme: ThemeConfig | null;\n hotkeys: HotkeyBindingsConfig | null;\n terminal: TerminalChoice | null;\n searchConfig: SearchConfig | null;\n workspaceVisibility: WorkspaceVisibilityConfig;\n /** Optional per-reason staleness age-gate overrides (defaults-first; null = all defaults). */\n staleness: Partial<StaleThresholds> | null;\n /** Opt-in: run the read-only staleness watchdog on the dashboard loop (emits\n * staleness-detected/cleared audit events; never mutates status). Off by default. */\n stalenessWatchdog: boolean;\n}\n\nconst DEFAULT_CONFIG: SyntaurConfig = {\n version: '2.0',\n defaultProjectDir: defaultProjectDir(),\n onboarding: {\n completed: false,\n },\n agentDefaults: {\n trustLevel: 'medium',\n autoApprove: false,\n autoCreateWorktree: 'ask',\n },\n session: {\n autoTrack: 'all',\n },\n integrations: {\n claudePluginDir: null,\n codexPluginDir: null,\n codexMarketplacePath: null,\n },\n backup: null,\n statuses: null,\n types: null,\n agents: null,\n playbooks: {\n disabled: [],\n },\n theme: null,\n hotkeys: null,\n terminal: null,\n searchConfig: null,\n workspaceVisibility: {\n hidden: [],\n },\n staleness: null,\n stalenessWatchdog: false,\n};\n\nconst AUTO_CREATE_WORKTREE_VALUES: readonly AutoCreateWorktree[] = ['skip', 'ask', 'always'];\n\nconst SESSION_AUTO_TRACK_VALUES: readonly SessionAutoTrack[] = ['all', 'workspaces-only', 'off'];\n\nexport class AgentConfigError extends Error {}\n\n/**\n * Validate an agent command string.\n * - Absolute paths (after ~ expansion) are accepted verbatim.\n * - Bare names (no \"/\" after expansion) are accepted for PATH lookup at launch time.\n * - Relative paths (contain \"/\" but not absolute) are rejected.\n */\nexport function parseAgentCommand(value: string, agentId?: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new AgentConfigError(\n `agent${agentId ? ` \"${agentId}\"` : ''} has empty command`,\n );\n }\n const expanded = expandHome(value.trim());\n if (isAbsolute(expanded)) {\n return resolve(expanded);\n }\n if (expanded.includes('/')) {\n throw new AgentConfigError(\n `agent${agentId ? ` \"${agentId}\"` : ''} command \"${value}\" is a relative path — use an absolute path or a bare binary name`,\n );\n }\n return expanded;\n}\n\nexport function validateAgentList(agents: AgentConfig[]): void {\n const seen = new Set<string>();\n let defaults = 0;\n for (const agent of agents) {\n if (!AGENT_ID_PATTERN.test(agent.id)) {\n throw new AgentConfigError(\n `agent id \"${agent.id}\" is invalid — must match /^[a-z0-9][a-z0-9_-]*$/`,\n );\n }\n if (seen.has(agent.id)) {\n throw new AgentConfigError(`duplicate agent id \"${agent.id}\"`);\n }\n seen.add(agent.id);\n if (!agent.label || agent.label.trim() === '') {\n throw new AgentConfigError(`agent \"${agent.id}\" has empty label`);\n }\n parseAgentCommand(agent.command, agent.id);\n if (\n agent.promptArgPosition !== undefined &&\n !PROMPT_ARG_POSITIONS.includes(agent.promptArgPosition)\n ) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" has invalid promptArgPosition \"${agent.promptArgPosition}\" — expected first|last|none`,\n );\n }\n if (agent.model !== undefined && /[\\r\\n]/.test(agent.model)) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" has invalid model — must be a single line (no newlines)`,\n );\n }\n if (\n agent.playbook !== undefined &&\n agent.playbook.trim() !== '' &&\n !isValidSlug(agent.playbook)\n ) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" has invalid playbook \"${agent.playbook}\" — must be a valid playbook slug`,\n );\n }\n if (agent.launchPrompt !== undefined && /[\\r\\n]/.test(agent.launchPrompt)) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" has invalid launchPrompt — must be a single line (no newlines)`,\n );\n }\n validateSessionInvocation(agent, 'resume', agent.resume);\n validateSessionInvocation(agent, 'fork', agent.fork);\n if (agent.default) defaults++;\n }\n if (defaults > 1) {\n throw new AgentConfigError(\n `more than one agent is marked default: true (only one is allowed)`,\n );\n }\n}\n\nfunction validateSessionInvocation(\n agent: AgentConfig,\n mode: 'resume' | 'fork',\n invocation: SessionInvocation | undefined,\n): void {\n if (invocation === undefined) return;\n if (!Array.isArray(invocation.args)) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" ${mode}.args must be an array of strings`,\n );\n }\n for (const a of invocation.args) {\n if (typeof a !== 'string') {\n throw new AgentConfigError(\n `agent \"${agent.id}\" ${mode}.args must contain only strings`,\n );\n }\n }\n if (\n invocation.command !== undefined &&\n (typeof invocation.command !== 'string' || invocation.command.trim() === '')\n ) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" ${mode}.command must be a non-empty string when present`,\n );\n }\n}\n\nfunction cloneDefaultConfig(): SyntaurConfig {\n return {\n ...DEFAULT_CONFIG,\n onboarding: { ...DEFAULT_CONFIG.onboarding },\n agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },\n session: { ...DEFAULT_CONFIG.session },\n integrations: { ...DEFAULT_CONFIG.integrations },\n backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,\n statuses: DEFAULT_CONFIG.statuses\n ? {\n statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),\n order: [...DEFAULT_CONFIG.statuses.order],\n transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t })),\n }\n : null,\n types: DEFAULT_CONFIG.types\n ? {\n definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),\n default: DEFAULT_CONFIG.types.default,\n }\n : null,\n agents: DEFAULT_CONFIG.agents\n ? DEFAULT_CONFIG.agents.map((a) => ({\n ...a,\n ...(a.args ? { args: [...a.args] } : {}),\n ...(a.resume ? { resume: { ...a.resume, args: [...a.resume.args] } } : {}),\n ...(a.fork ? { fork: { ...a.fork, args: [...a.fork.args] } } : {}),\n }))\n : null,\n playbooks: {\n disabled: [...DEFAULT_CONFIG.playbooks.disabled],\n },\n theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null,\n hotkeys: DEFAULT_CONFIG.hotkeys\n ? { bindings: { ...DEFAULT_CONFIG.hotkeys.bindings } }\n : null,\n terminal: DEFAULT_CONFIG.terminal,\n workspaceVisibility: {\n hidden: [...DEFAULT_CONFIG.workspaceVisibility.hidden],\n },\n };\n}\n\nfunction parseFrontmatter(content: string): Record<string, string> {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return {};\n const result: Record<string, string> = {};\n const lines = match[1].split('\\n');\n let currentParent: string | null = null;\n for (const line of lines) {\n if (line.trim() === '') continue;\n const indent = line.length - line.trimStart().length;\n const colonIndex = line.indexOf(':');\n if (colonIndex < 0) continue;\n const key = line.slice(0, colonIndex).trim();\n const value = line.slice(colonIndex + 1).trim();\n if (indent === 0) {\n if (value === '' || value === undefined) {\n currentParent = key;\n } else {\n currentParent = null;\n result[key] = value.replace(/^[\"']|[\"']$/g, '');\n }\n } else if (indent > 0 && currentParent) {\n result[`${currentParent}.${key}`] = value.replace(/^[\"']|[\"']$/g, '');\n }\n }\n return result;\n}\n\n/**\n * Reconstruct the optional per-agent install records from the flattened\n * frontmatter. Keys look like `integrations.installedAgents.<id>` → `<scope>`.\n * Returns `{}` (no key) when none are present so the field stays absent.\n */\nfunction parseInstalledAgents(\n fm: Record<string, string>,\n): Pick<IntegrationConfig, 'installedAgents'> {\n const prefix = 'integrations.installedAgents.';\n const installedAgents: Record<string, { scope: 'project' | 'global' }> = {};\n for (const [key, value] of Object.entries(fm)) {\n if (!key.startsWith(prefix)) continue;\n const id = key.slice(prefix.length);\n if (!id) continue;\n const scope = value === 'project' ? 'project' : 'global';\n installedAgents[id] = { scope };\n }\n return Object.keys(installedAgents).length > 0 ? { installedAgents } : {};\n}\n\nexport function parseStatusConfig(content: string): StatusConfig | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n // Check if there's a top-level statuses: section\n const statusesStart = fmBlock.match(/^statuses:\\s*$/m);\n if (!statusesStart) return null;\n\n // Extract the statuses block (everything indented after \"statuses:\")\n const startIdx = fmBlock.indexOf(statusesStart[0]) + statusesStart[0].length;\n const remaining = fmBlock.slice(startIdx);\n\n const statuses: StatusDefinition[] = [];\n const order: string[] = [];\n const transitions: StatusTransition[] = [];\n const phaseLadder: PhaseRung[] = [];\n const disposition: DispositionRule[] = [];\n const headline: Record<string, string> = {};\n const facts: RawFactDeclaration[] = [];\n\n // Strip surrounding quotes from a YAML scalar (AQL conditions are quoted).\n const unquote = (v: string): string => {\n const t = v.trim();\n if ((t.startsWith('\"') && t.endsWith('\"')) || (t.startsWith(\"'\") && t.endsWith(\"'\"))) {\n return t.slice(1, -1);\n }\n return t;\n };\n\n // Like `unquote`, but also reverses the `\\`/`\"` escaping that `escapeAql`\n // applies to the THREE escaped derive-rule fields (phaseLadder when/next,\n // disposition when). Scoped to those reads only — the plain `unquote` above\n // stays the decoder for every other (unescaped) scalar (is/phase/headline/\n // facts/aliases), so no field the serializer never escapes can be\n // over-decoded. Mirrors parseSimpleValue's escape handling.\n const unquoteAql = (v: string): string => {\n const t = v.trim();\n if (t.startsWith('\"') && t.endsWith('\"') && t.length >= 2) {\n return t.slice(1, -1).replace(/\\\\([\"\\\\])/g, '$1');\n }\n if (t.startsWith(\"'\") && t.endsWith(\"'\") && t.length >= 2) {\n return t.slice(1, -1);\n }\n return t;\n };\n\n // Parse sub-sections: definitions, order, transitions + derive rules\n // (phaseLadder, disposition, headline — derived-status v3, persisted flat\n // under `statuses:`).\n let currentSection:\n | 'definitions'\n | 'order'\n | 'transitions'\n | 'phaseLadder'\n | 'disposition'\n | 'headline'\n | 'facts'\n | null = null;\n const lines = remaining.split('\\n');\n\n function parseListEntry(lineIdx: number, baseIndent: number): { entry: Record<string, string>; consumed: number } {\n const entry: Record<string, string> = {};\n const firstLine = lines[lineIdx].trimStart().slice(2).trim();\n const colonIdx = firstLine.indexOf(':');\n if (colonIdx > 0) {\n entry[firstLine.slice(0, colonIdx).trim()] = firstLine.slice(colonIdx + 1).trim();\n }\n let consumed = 1;\n for (let i = lineIdx + 1; i < lines.length; i++) {\n const next = lines[i];\n const nextTrimmed = next.trimStart();\n const nextIndent = next.length - nextTrimmed.length;\n if (nextIndent <= baseIndent || nextTrimmed.startsWith('- ')) break;\n const ci = nextTrimmed.indexOf(':');\n if (ci > 0) {\n entry[nextTrimmed.slice(0, ci).trim()] = nextTrimmed.slice(ci + 1).trim();\n }\n consumed++;\n }\n return { entry, consumed };\n }\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx];\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n\n // Top-level key under statuses (indent 2)\n if (indent === 2 && trimmed.endsWith(':')) {\n const key = trimmed.slice(0, -1).trim();\n if (key === 'definitions') currentSection = 'definitions';\n else if (key === 'order') currentSection = 'order';\n else if (key === 'transitions') currentSection = 'transitions';\n else if (key === 'phaseLadder') currentSection = 'phaseLadder';\n else if (key === 'disposition') currentSection = 'disposition';\n else if (key === 'headline') currentSection = 'headline';\n else if (key === 'facts') currentSection = 'facts';\n else currentSection = null;\n continue;\n }\n\n // Stop if we hit a new top-level key (no indent)\n if (indent === 0 && trimmed.includes(':')) break;\n\n if (currentSection === 'order' && indent >= 4 && trimmed.startsWith('- ')) {\n order.push(trimmed.slice(2).trim());\n continue;\n }\n\n if (currentSection === 'definitions' && indent >= 4 && trimmed.startsWith('- ')) {\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (entry['id']) {\n statuses.push({\n id: entry['id'],\n label: entry['label'] ?? entry['id'],\n description: entry['description'],\n color: entry['color'],\n icon: entry['icon'],\n terminal: entry['terminal'] === 'true',\n });\n }\n lineIdx += consumed - 1; // skip consumed continuation lines\n continue;\n }\n\n if (currentSection === 'transitions' && indent >= 4 && trimmed.startsWith('- ')) {\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (entry['from'] && entry['command'] && entry['to']) {\n transitions.push({\n from: entry['from'],\n command: entry['command'],\n to: entry['to'],\n label: entry['label'],\n description: entry['description'],\n requiresReason: entry['requiresReason'] === 'true',\n });\n }\n lineIdx += consumed - 1;\n continue;\n }\n\n if (currentSection === 'phaseLadder' && indent >= 4 && trimmed.startsWith('- ')) {\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (entry['phase'] && entry['when'] !== undefined) {\n phaseLadder.push({\n phase: unquote(entry['phase']),\n when: unquoteAql(entry['when']),\n next: entry['next'] !== undefined ? unquoteAql(entry['next']) : undefined,\n });\n }\n lineIdx += consumed - 1;\n continue;\n }\n\n if (currentSection === 'disposition' && indent >= 4 && trimmed.startsWith('- ')) {\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (entry['else'] !== undefined) {\n disposition.push({ when: null, is: unquote(entry['else']) });\n } else if (entry['when'] !== undefined && entry['is']) {\n disposition.push({ when: unquoteAql(entry['when']), is: unquote(entry['is']) });\n }\n lineIdx += consumed - 1;\n continue;\n }\n\n if (currentSection === 'headline' && indent >= 4 && !trimmed.startsWith('- ')) {\n const ci = trimmed.indexOf(':');\n if (ci > 0) {\n headline[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1));\n }\n continue;\n }\n\n if (currentSection === 'facts' && indent >= 4 && trimmed.startsWith('- ')) {\n // Loose parse: keep every recognizable row verbatim (RawFactDeclaration)\n // so invalid rows round-trip AND doctor can diagnose exactly what the\n // normalize/accept pipeline drops — a row missing `name` must NOT be\n // silently deleted (that is the silent-deletion bug class this feature\n // exists to prevent). A row with no recognized key at all is skipped.\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (\n entry['name'] !== undefined ||\n entry['type'] !== undefined ||\n entry['binds'] !== undefined\n ) {\n facts.push({\n name: entry['name'] !== undefined ? unquote(entry['name']) : '',\n type: entry['type'] !== undefined ? unquote(entry['type']) : '',\n binds: entry['binds'] !== undefined ? unquote(entry['binds']) : null,\n });\n }\n lineIdx += consumed - 1;\n continue;\n }\n }\n\n const derive: DeriveConfig | null =\n phaseLadder.length > 0 || disposition.length > 0 || Object.keys(headline).length > 0\n ? {\n phaseLadder: phaseLadder.length > 0 ? phaseLadder : DEFAULT_DERIVE_CONFIG.phaseLadder,\n disposition: disposition.length > 0 ? disposition : DEFAULT_DERIVE_CONFIG.disposition,\n headline: {\n terminal: 'passthrough',\n parked: headline['parked'] ?? DEFAULT_DERIVE_CONFIG.headline.parked,\n blocked: headline['blocked'] ?? DEFAULT_DERIVE_CONFIG.headline.blocked,\n active: 'phase',\n },\n }\n : null;\n\n // Return null only when the `statuses:` block carried no usable content at\n // all. A block that declares facts and/or derive rules but no status\n // `definitions` must still surface them — dropping them here is the\n // silent-deletion bug class this loose parser exists to prevent\n // (getStatusConfig falls back to default statuses/order so the board still\n // renders, while the declared facts/derive ride along).\n if (statuses.length === 0 && facts.length === 0 && derive === null) return null;\n\n return {\n statuses,\n order: order.length > 0 ? order : statuses.map((s) => s.id),\n transitions,\n derive,\n facts: facts.length > 0 ? facts : null,\n };\n}\n\n/**\n * Default per-status accent colors. Statuses without an entry fall back to\n * `'gray'` in {@link buildDefaultStatusConfig}. Shared by the dashboard's\n * `getStatusConfig()` and the `syntaur status` CLI so the two never drift.\n */\nexport const DEFAULT_STATUS_COLORS: Record<string, string> = {\n pending: 'slate',\n in_progress: 'teal',\n blocked: 'amber',\n review: 'violet',\n completed: 'emerald',\n failed: 'rose',\n};\n\n/** Turn a snake_case status id into a human label (\"in_progress\" → \"In Progress\"). */\nexport function toTitleCase(s: string): string {\n return s.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Materialize the built-in default status set as an explicit {@link StatusConfig}.\n *\n * `DEFAULT_CONFIG.statuses` is `null` (the runtime resolves defaults lazily), so\n * `syntaur status init` / `list` cannot read defaults from there. This builder\n * reproduces exactly what the dashboard's `getStatusConfig()` no-block branch\n * builds — same ids/labels/colors/terminal flags and the same transition table —\n * so the CLI and the dashboard share one source of truth.\n */\nexport function buildDefaultStatusConfig(): StatusConfig {\n return {\n statuses: DEFAULT_STATUSES.map((id) => ({\n id,\n label: toTitleCase(id),\n color: DEFAULT_STATUS_COLORS[id] ?? 'gray',\n terminal: id === 'completed' || id === 'failed',\n })),\n order: [...DEFAULT_STATUSES],\n transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {\n const [from, command] = key.split(':');\n return { from, command, to };\n }),\n };\n}\n\nexport function serializeStatusConfig(statuses: StatusConfig): string {\n const lines: string[] = [];\n // Symmetric with `unquoteAql` in parseStatusConfig: escape backslash THEN\n // quote so the derive-rule when/next/disposition-when fields round-trip even\n // when they contain a literal `\"` or `\\`. (Previously only `\"` was escaped and\n // nothing reversed it, so a quoted AQL condition accumulated a backslash on\n // every save→reload.)\n const escapeAql = (s: string): string => s.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n lines.push('statuses:');\n\n // definitions\n lines.push(' definitions:');\n for (const s of statuses.statuses) {\n lines.push(` - id: ${s.id}`);\n lines.push(` label: ${s.label}`);\n if (s.description) lines.push(` description: ${s.description}`);\n if (s.color) lines.push(` color: ${s.color}`);\n if (s.icon) lines.push(` icon: ${s.icon}`);\n if (s.terminal) lines.push(` terminal: true`);\n }\n\n // order\n lines.push(' order:');\n for (const id of statuses.order) {\n lines.push(` - ${id}`);\n }\n\n // transitions\n if (statuses.transitions.length > 0) {\n lines.push(' transitions:');\n for (const t of statuses.transitions) {\n lines.push(` - from: ${t.from}`);\n lines.push(` command: ${t.command}`);\n lines.push(` to: ${t.to}`);\n if (t.label) lines.push(` label: ${t.label}`);\n if (t.description) lines.push(` description: ${t.description}`);\n if (t.requiresReason) lines.push(` requiresReason: true`);\n }\n }\n\n // custom fact declarations — emitted verbatim (RawFactDeclaration) so the\n // round-trip preserves whatever the user wrote, even invalid rows that the\n // normalize/accept pipeline later drops. Same silent-deletion class as derive.\n if (statuses.facts && statuses.facts.length > 0) {\n lines.push(' facts:');\n for (const f of statuses.facts) {\n lines.push(` - name: ${f.name}`);\n lines.push(` type: ${f.type}`);\n if (f.binds !== null && f.binds !== undefined) {\n lines.push(` binds: ${f.binds}`);\n }\n }\n }\n\n // derive rules (derived-status v3) — serialized so every writeStatusConfig\n // round-trip preserves them (the pre-v3 writer rebuilt the block from\n // definitions/order/transitions only and silently deleted custom rules).\n if (statuses.derive) {\n const d = statuses.derive;\n lines.push(' phaseLadder:');\n for (const rung of d.phaseLadder) {\n lines.push(` - phase: ${rung.phase}`);\n lines.push(` when: \"${escapeAql(rung.when)}\"`);\n // `!== undefined`, not truthy: an accepted empty-string `next: \"\"` must\n // be preserved (otherwise it reparses as undefined — a round-trip loss).\n if (rung.next !== undefined) lines.push(` next: \"${escapeAql(rung.next)}\"`);\n }\n lines.push(' disposition:');\n for (const rule of d.disposition) {\n if (rule.when === null) {\n lines.push(` - else: ${rule.is}`);\n } else {\n lines.push(` - when: \"${escapeAql(rule.when)}\"`);\n lines.push(` is: ${rule.is}`);\n }\n }\n lines.push(' headline:');\n lines.push(` terminal: passthrough`);\n lines.push(` parked: ${d.headline.parked}`);\n lines.push(` blocked: ${d.headline.blocked}`);\n lines.push(` active: phase`);\n }\n\n return lines.join('\\n');\n}\n\nfunction serializeIntegrationConfig(integrations: IntegrationConfig): string | null {\n const lines: string[] = [];\n\n if (integrations.claudePluginDir) {\n lines.push(` claudePluginDir: ${integrations.claudePluginDir}`);\n }\n if (integrations.codexPluginDir) {\n lines.push(` codexPluginDir: ${integrations.codexPluginDir}`);\n }\n if (integrations.codexMarketplacePath) {\n lines.push(` codexMarketplacePath: ${integrations.codexMarketplacePath}`);\n }\n if (integrations.installedAgents) {\n for (const [id, rec] of Object.entries(integrations.installedAgents)) {\n lines.push(` installedAgents.${id}: ${rec.scope}`);\n }\n }\n\n if (lines.length === 0) {\n return null;\n }\n\n return ['integrations:', ...lines].join('\\n');\n}\n\nfunction serializeOnboardingConfig(onboarding: OnboardingConfig): string {\n return ['onboarding:', ` completed: ${onboarding.completed ? 'true' : 'false'}`].join('\\n');\n}\n\nfunction serializeBackupConfig(backup: BackupConfig): string {\n const lines: string[] = ['backup:'];\n lines.push(` repo: ${backup.repo ?? 'null'}`);\n lines.push(` categories: ${backup.categories}`);\n lines.push(` lastBackup: ${backup.lastBackup ?? 'null'}`);\n lines.push(` lastRestore: ${backup.lastRestore ?? 'null'}`);\n return lines.join('\\n');\n}\n\nfunction serializePlaybooksConfig(playbooks: PlaybooksConfig): string | null {\n if (!playbooks.disabled || playbooks.disabled.length === 0) {\n return null;\n }\n const lines: string[] = ['playbooks:', ' disabled:'];\n for (const slug of playbooks.disabled) {\n lines.push(` - ${slug}`);\n }\n return lines.join('\\n');\n}\n\nfunction parsePlaybooksConfig(fmBlock: string): PlaybooksConfig {\n const blockStart = fmBlock.match(/^playbooks:\\s*$/m);\n if (!blockStart) {\n return { disabled: [] };\n }\n\n const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;\n const remaining = fmBlock.slice(startIdx).split('\\n');\n\n const disabled: string[] = [];\n let currentSection: 'disabled' | null = null;\n\n for (const line of remaining) {\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n\n // End of playbooks block — next top-level key\n if (indent === 0 && trimmed.length > 0) break;\n\n if (trimmed === '') continue;\n\n if (indent === 2 && trimmed.startsWith('disabled:')) {\n currentSection = 'disabled';\n // Support inline form `disabled: []` — treat as empty list.\n const afterColon = trimmed.slice('disabled:'.length).trim();\n if (afterColon === '[]' || afterColon === '') {\n continue;\n }\n // Any other inline value is malformed; skip.\n continue;\n }\n\n if (currentSection === 'disabled' && indent >= 4 && trimmed.startsWith('- ')) {\n const raw = trimmed.slice(2).trim().replace(/^[\"']|[\"']$/g, '');\n if (raw.length === 0) continue;\n // Defer slug-format validation to callers via isValidSlug where needed;\n // here we only filter obviously invalid whitespace-containing entries.\n if (/\\s/.test(raw)) {\n console.warn(`Warning: config.md playbooks.disabled entry \"${raw}\" contains whitespace, ignoring`);\n continue;\n }\n disabled.push(raw);\n continue;\n }\n }\n\n return { disabled };\n}\n\nexport async function updatePlaybooksConfig(\n playbooks: Partial<PlaybooksConfig>,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const current = (await readConfig()).playbooks;\n const nextPlaybooks: PlaybooksConfig = {\n disabled: Array.from(new Set(playbooks.disabled ?? current.disabled)),\n };\n\n const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const bodyBlock = playbooksBlock ? `${playbooksBlock}\\n` : '';\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${bodyBlock}---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'playbooks');\n const newFm = playbooksBlock\n ? `${cleanedFm}\\n${playbooksBlock}`.replace(/^\\n+/, '')\n : cleanedFm;\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nfunction parseThemeConfig(content: string): ThemeConfig | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const blockStart = fmBlock.match(/^theme:\\s*$/m);\n if (!blockStart) return null;\n\n const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;\n const remaining = fmBlock.slice(startIdx).split('\\n');\n\n let preset: string | null = null;\n for (const line of remaining) {\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0 && trimmed.length > 0) break;\n if (trimmed === '') continue;\n if (indent === 2 && trimmed.startsWith('preset:')) {\n const value = trimmed.slice('preset:'.length).trim().replace(/^[\"']|[\"']$/g, '');\n if (value.length > 0) preset = value;\n }\n }\n\n if (!preset) return null;\n return { preset };\n}\n\nfunction serializeThemeConfig(theme: ThemeConfig): string {\n return ['theme:', ` preset: ${theme.preset}`].join('\\n');\n}\n\nexport async function writeThemeConfig(theme: ThemeConfig): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const themeBlock = serializeThemeConfig(theme);\n\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${themeBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'theme');\n const newFm = `${cleanedFm}\\n${themeBlock}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteThemeConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'theme');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n/**\n * Serialize the workspace-visibility blocklist. Mirrors the `playbooks.disabled`\n * list shape but JSON-escapes each entry so arbitrary workspace names (spaces,\n * quotes, backslashes) round-trip. Returns `null` for an empty list so the\n * writer omits the block entirely (absent = all workspaces visible).\n */\nfunction serializeWorkspaceVisibilityConfig(\n cfg: WorkspaceVisibilityConfig,\n): string | null {\n const hidden = normalizeHiddenList(cfg.hidden);\n if (hidden.length === 0) return null;\n const lines: string[] = ['workspaceVisibility:', ' hidden:'];\n for (const name of hidden) {\n lines.push(` - ${JSON.stringify(name)}`);\n }\n return lines.join('\\n');\n}\n\n/**\n * Parse the workspace-visibility blocklist from a frontmatter block. Unlike\n * `parsePlaybooksConfig` (which rejects whitespace-containing slugs), workspace\n * names are arbitrary: a JSON-quoted entry is `JSON.parse`d, an unquoted entry\n * is taken literally. Absent block → empty list (everything visible).\n */\nfunction parseWorkspaceVisibilityConfig(\n fmBlock: string,\n): WorkspaceVisibilityConfig {\n const blockStart = fmBlock.match(/^workspaceVisibility:\\s*$/m);\n if (!blockStart) {\n return { hidden: [] };\n }\n\n const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;\n const remaining = fmBlock.slice(startIdx).split('\\n');\n\n const hidden: string[] = [];\n let currentSection: 'hidden' | null = null;\n\n for (const line of remaining) {\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n\n // End of block — next top-level key.\n if (indent === 0 && trimmed.length > 0) break;\n if (trimmed === '') continue;\n\n if (indent === 2 && trimmed.startsWith('hidden:')) {\n currentSection = 'hidden';\n // Support inline form `hidden: []` — treat as empty list.\n continue;\n }\n\n if (currentSection === 'hidden' && indent >= 4 && trimmed.startsWith('- ')) {\n const rest = trimmed.slice(2).trim();\n if (rest.length === 0) continue;\n let name: string;\n if (rest.startsWith('\"')) {\n try {\n name = JSON.parse(rest) as string;\n } catch {\n // Hand-edited / malformed — strip a single surrounding quote pair.\n name = rest.replace(/^[\"']|[\"']$/g, '');\n }\n } else {\n name = rest;\n }\n hidden.push(name);\n continue;\n }\n }\n\n return { hidden: normalizeHiddenList(hidden) };\n}\n\nexport async function writeWorkspaceVisibilityConfig(\n cfg: WorkspaceVisibilityConfig,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const block = serializeWorkspaceVisibilityConfig(cfg);\n\n const existing = (await fileExists(configPath))\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const bodyBlock = block ? `${block}\\n` : '';\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${bodyBlock}---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'workspaceVisibility');\n const newFm = block\n ? `${cleanedFm}\\n${block}`.replace(/^\\n+/, '')\n : cleanedFm;\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteWorkspaceVisibilityConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'workspaceVisibility');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n/**\n * Remove any top-level `key: <value>` scalar line from a YAML frontmatter block.\n * Used for scalar keys (terminal:) that don't have child lines, so they can't\n * use the block-style `stripTopLevelBlock`. No-op when the key is absent.\n */\nfunction stripTopLevelScalar(fmBlock: string, key: string): string {\n const lines = fmBlock.split('\\n');\n const keyRegex = new RegExp(`^${key}:\\\\s*\\\\S`);\n const filtered = lines.filter((line) => !keyRegex.test(line));\n return filtered.join('\\n').replace(/\\n+$/, '');\n}\n\nexport async function writeTerminalConfig(terminal: TerminalChoice): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const terminalLine = `terminal: ${terminal}`;\n\n const existing = (await fileExists(configPath))\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${terminalLine}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelScalar(fmBlock, 'terminal');\n const newFm = `${cleanedFm}\\n${terminalLine}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteTerminalConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelScalar(fmBlock, 'terminal');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nfunction parseHotkeyBindingsConfig(content: string): HotkeyBindingsConfig | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const blockStart = fmBlock.match(/^hotkeys:\\s*$/m);\n if (!blockStart) return null;\n\n const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;\n const remaining = fmBlock.slice(startIdx).split('\\n');\n\n const bindings: Partial<Record<BindableActionKind, string>> = {};\n let inBindings = false;\n for (const line of remaining) {\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0 && trimmed.length > 0) break;\n if (trimmed === '') continue;\n if (indent === 2 && trimmed === 'bindings:') {\n inBindings = true;\n continue;\n }\n if (inBindings && indent === 4) {\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx <= 0) continue;\n const rawKind = trimmed.slice(0, colonIdx).trim();\n const rawValue = trimmed\n .slice(colonIdx + 1)\n .trim()\n .replace(/^[\"']|[\"']$/g, '');\n if (!isBindableActionKind(rawKind)) continue;\n if (rawValue.length === 0) continue;\n bindings[rawKind] = canonicalizeCombo(rawValue);\n }\n }\n\n if (Object.keys(bindings).length === 0) return null;\n return { bindings };\n}\n\nfunction serializeHotkeyBindingsConfig(cfg: HotkeyBindingsConfig): string {\n const lines: string[] = ['hotkeys:', ' bindings:'];\n // Emit in the canonical kind order so on-disk diffs are stable.\n for (const kind of BINDABLE_ACTION_KINDS) {\n const value = cfg.bindings[kind];\n if (!value) continue;\n lines.push(` ${kind}: \"${canonicalizeCombo(value)}\"`);\n }\n // If no bindings remain, return an empty block (caller will treat as delete).\n if (lines.length === 2) return '';\n return lines.join('\\n');\n}\n\nexport async function writeHotkeyBindingsConfig(\n cfg: HotkeyBindingsConfig,\n): Promise<void> {\n // Validate + canonicalize + drop reserved-combo collisions before writing.\n const cleaned: Partial<Record<BindableActionKind, string>> = {};\n for (const kind of BINDABLE_ACTION_KINDS) {\n const raw = cfg.bindings[kind];\n if (typeof raw !== 'string' || raw.trim() === '') continue;\n const canonical = canonicalizeCombo(raw);\n if (!canonical) continue;\n if (isReservedCombo(canonical)) continue;\n cleaned[kind] = canonical;\n }\n\n if (Object.keys(cleaned).length === 0) {\n await deleteHotkeyBindingsConfig();\n return;\n }\n\n const configPath = resolve(syntaurRoot(), 'config.md');\n const block = serializeHotkeyBindingsConfig({ bindings: cleaned });\n\n const existing = (await fileExists(configPath))\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${block}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'hotkeys');\n const newFm = `${cleanedFm}\\n${block}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteHotkeyBindingsConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'hotkeys');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nfunction stripTopLevelBlock(fmBlock: string, key: string): string {\n const blockStart = fmBlock.match(new RegExp(`^${key}:\\\\s*$`, 'm'));\n if (!blockStart) {\n return fmBlock.replace(/\\n+$/, '');\n }\n\n // Regex match offset, not indexOf — the `${key}:` text can appear earlier inside\n // another block's value (e.g. `search:` in an AQL string), and indexOf would cut\n // from there, corrupting unrelated frontmatter.\n const startIdx = blockStart.index ?? 0;\n const before = fmBlock.slice(0, startIdx);\n const after = fmBlock.slice(startIdx + blockStart[0].length);\n const remaining = after.split('\\n');\n let endIdx = 0;\n\n for (let i = 0; i < remaining.length; i++) {\n const line = remaining[i];\n if (line.trim() === '') {\n endIdx = i + 1;\n continue;\n }\n if (line.length > 0 && line[0] !== ' ') {\n break;\n }\n endIdx = i + 1;\n }\n\n return (before + remaining.slice(endIdx).join('\\n')).replace(/\\n+$/, '');\n}\n\nfunction parseOptionalAbsolutePath(\n value: string | undefined,\n fieldName: string,\n): string | null {\n if (!value) {\n return null;\n }\n\n const expanded = expandHome(String(value));\n if (!isAbsolute(expanded)) {\n console.warn(\n `Warning: config.md ${fieldName} is not an absolute path (\"${value}\"), ignoring it`,\n );\n return null;\n }\n\n return resolve(expanded);\n}\n\nfunction parseAgentsConfig(content: string): AgentConfig[] | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const agentsStart = fmBlock.match(/^agents:\\s*$/m);\n if (!agentsStart) return null;\n\n const startIdx = fmBlock.indexOf(agentsStart[0]) + agentsStart[0].length;\n const remaining = fmBlock.slice(startIdx);\n const lines = remaining.split('\\n');\n\n const agents: AgentConfig[] = [];\n let current: Partial<AgentConfig> & { args?: string[] } | null = null;\n let argsCapture: string[] | null = null;\n let argsBaseIndent = 0;\n // Active nested block state (e.g. `resume:` or `fork:` sub-mapping under an\n // agent). When `nestedKey` is one of `resume` / `fork`, lines at deeper\n // indent are parsed as that invocation's `command` / `args` fields. When\n // `nestedKey === '__skip__'` we swallow the indented block without\n // recording anything — this is the forward-compat path for unknown nested\n // keys added in future syntaur versions.\n let nestedKey: string | null = null;\n let nestedInvocation: SessionInvocation | null = null;\n let nestedBaseIndent = 0;\n\n function flushCurrent() {\n if (!current) return;\n if (!current.id || !current.command || !current.label) {\n current = null;\n return;\n }\n agents.push({\n id: current.id,\n label: current.label,\n command: current.command,\n ...(current.args && current.args.length > 0 ? { args: current.args } : {}),\n ...(current.promptArgPosition\n ? { promptArgPosition: current.promptArgPosition }\n : {}),\n ...(current.default ? { default: true } : {}),\n ...(current.resolveFromShellAliases ? { resolveFromShellAliases: true } : {}),\n ...(current.model ? { model: current.model } : {}),\n ...(current.playbook ? { playbook: current.playbook } : {}),\n ...(current.launchPrompt ? { launchPrompt: current.launchPrompt } : {}),\n ...(current.resume ? { resume: current.resume } : {}),\n ...(current.fork ? { fork: current.fork } : {}),\n });\n current = null;\n argsCapture = null;\n nestedKey = null;\n nestedInvocation = null;\n }\n\n function closeNestedBlock() {\n if (!nestedKey) return;\n if (current && (nestedKey === 'resume' || nestedKey === 'fork') && nestedInvocation) {\n // Only attach when args were populated — empty invocation is a no-op.\n if (Array.isArray(nestedInvocation.args)) {\n current[nestedKey] = nestedInvocation;\n }\n }\n nestedKey = null;\n nestedInvocation = null;\n argsCapture = null;\n }\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n\n if (indent === 0 && trimmed !== '' && !trimmed.startsWith('#')) {\n closeNestedBlock();\n break; // new top-level key\n }\n\n // Continue capturing list items for the active argsCapture target.\n if (argsCapture) {\n if (indent > argsBaseIndent && trimmed.startsWith('- ')) {\n argsCapture.push(decodeYamlScalar(trimmed.slice(2).trim()));\n continue;\n } else {\n argsCapture = null;\n }\n }\n\n if (indent === 2 && trimmed.startsWith('- ')) {\n closeNestedBlock();\n flushCurrent();\n current = {};\n const rest = trimmed.slice(2).trim();\n const colonIdx = rest.indexOf(':');\n if (colonIdx > 0) {\n const k = rest.slice(0, colonIdx).trim();\n const v = rest.slice(colonIdx + 1).trim();\n assignAgentField(current, k, v);\n }\n continue;\n }\n\n if (!current) continue;\n\n // Inside a nested block (resume / fork / skip-unknown).\n if (nestedKey && indent > nestedBaseIndent) {\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx <= 0) continue;\n const k = trimmed.slice(0, colonIdx).trim();\n const v = trimmed.slice(colonIdx + 1).trim();\n if (nestedKey === 'resume' || nestedKey === 'fork') {\n if (!nestedInvocation) nestedInvocation = { args: [] };\n if (k === 'args' && v === '') {\n nestedInvocation.args = [];\n argsCapture = nestedInvocation.args;\n argsBaseIndent = indent;\n continue;\n }\n if (k === 'command' && v !== '') {\n nestedInvocation.command = decodeYamlScalar(v);\n continue;\n }\n // Unknown nested-of-nested: ignore for forward compat.\n }\n // nestedKey === '__skip__' → swallow without recording.\n continue;\n }\n\n // Returning out to indent 4 (or shallower) — close any open nested block.\n if (nestedKey && indent <= nestedBaseIndent) {\n closeNestedBlock();\n }\n\n if (indent >= 4 && current) {\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx <= 0) continue;\n const k = trimmed.slice(0, colonIdx).trim();\n const v = trimmed.slice(colonIdx + 1).trim();\n if (k === 'args' && v === '') {\n argsCapture = [];\n argsBaseIndent = indent;\n current.args = argsCapture;\n continue;\n }\n // Recognized nested mapping blocks: resume / fork. Empty value + no\n // recognized scalar field → enter nested mode.\n if ((k === 'resume' || k === 'fork') && v === '') {\n nestedKey = k;\n nestedInvocation = { args: [] };\n nestedBaseIndent = indent;\n continue;\n }\n // Unknown key with empty value at agent-field indent: forward-compat\n // skip. Older parsers would crash here once a future version emits\n // a new nested block; this branch lets us pass through gracefully.\n if (v === '' && !KNOWN_AGENT_SCALAR_FIELDS.has(k)) {\n nestedKey = '__skip__';\n nestedInvocation = null;\n nestedBaseIndent = indent;\n continue;\n }\n assignAgentField(current, k, v);\n }\n }\n closeNestedBlock();\n flushCurrent();\n\n if (agents.length === 0) return [];\n return agents;\n}\n\nconst KNOWN_AGENT_SCALAR_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'label',\n 'command',\n 'promptArgPosition',\n 'default',\n 'resolveFromShellAliases',\n 'model',\n 'playbook',\n 'launchPrompt',\n]);\n\n/**\n * Normalize and validate an agents list parsed from config.md. On any\n * AgentConfigError, log a warning and fall back to built-in defaults so a\n * malformed user config does not brick `syntaur browse`. Returns the\n * normalized list (with `command` resolved through `parseAgentCommand`).\n */\nfunction normalizeAgentsFromConfig(agents: AgentConfig[] | null): AgentConfig[] | null {\n if (agents === null) return null;\n try {\n const normalized = agents.map((agent) => ({\n ...agent,\n command: parseAgentCommand(agent.command, agent.id),\n }));\n validateAgentList(normalized);\n return normalized;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(\n `Warning: ~/.syntaur/config.md agents block is invalid (${msg}) — using built-in defaults`,\n );\n return null;\n }\n}\n\n/**\n * Decode a YAML-ish scalar:\n * - Bare values returned verbatim.\n * - Single-quoted: strip outer quotes, unescape '' → '.\n * - Double-quoted: strip outer quotes, unescape \\\\ \\\" \\n \\t \\r.\n * Rejects unterminated quoted scalars (caller should surface as a parse error).\n */\nfunction decodeYamlScalar(value: string): string {\n const trimmed = value.trim();\n if (trimmed.length >= 2 && trimmed.startsWith('\"') && trimmed.endsWith('\"')) {\n const body = trimmed.slice(1, -1);\n let out = '';\n for (let i = 0; i < body.length; i++) {\n const ch = body[i];\n if (ch === '\\\\' && i + 1 < body.length) {\n const next = body[i + 1];\n switch (next) {\n case '\\\\': out += '\\\\'; break;\n case '\"': out += '\"'; break;\n case 'n': out += '\\n'; break;\n case 't': out += '\\t'; break;\n case 'r': out += '\\r'; break;\n default: out += next; break;\n }\n i++;\n continue;\n }\n out += ch;\n }\n return out;\n }\n if (trimmed.length >= 2 && trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\")) {\n return trimmed.slice(1, -1).replace(/''/g, \"'\");\n }\n return trimmed;\n}\n\nfunction assignAgentField(target: Partial<AgentConfig>, key: string, rawValue: string): void {\n const value = decodeYamlScalar(rawValue);\n switch (key) {\n case 'id':\n target.id = value;\n break;\n case 'label':\n target.label = value;\n break;\n case 'command':\n target.command = value;\n break;\n case 'promptArgPosition':\n target.promptArgPosition = value as PromptArgPosition;\n break;\n case 'default':\n target.default = value === 'true';\n break;\n case 'resolveFromShellAliases':\n target.resolveFromShellAliases = value === 'true';\n break;\n case 'model':\n target.model = value;\n break;\n case 'playbook':\n target.playbook = value;\n break;\n case 'launchPrompt':\n target.launchPrompt = value;\n break;\n }\n}\n\nfunction yamlQuoteScalar(value: string): string {\n if (/[\\r\\n]/.test(value)) {\n throw new AgentConfigError(\n `value contains newlines, which the agents config serializer does not support: ${JSON.stringify(value)}`,\n );\n }\n if (value === '' || /[:#{}[\\],&*?|>!%@`\"'\\\\\\t]/.test(value) || /^\\s|\\s$/.test(value)) {\n const escaped = value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\t/g, '\\\\t');\n return `\"${escaped}\"`;\n }\n return value;\n}\n\nfunction serializeAgentsConfig(agents: AgentConfig[]): string {\n const lines: string[] = ['agents:'];\n for (const a of agents) {\n lines.push(` - id: ${yamlQuoteScalar(a.id)}`);\n lines.push(` label: ${yamlQuoteScalar(a.label)}`);\n lines.push(` command: ${yamlQuoteScalar(a.command)}`);\n if (a.model) {\n lines.push(` model: ${yamlQuoteScalar(a.model)}`);\n }\n if (a.playbook) {\n lines.push(` playbook: ${yamlQuoteScalar(a.playbook)}`);\n }\n if (a.launchPrompt) {\n lines.push(` launchPrompt: ${yamlQuoteScalar(a.launchPrompt)}`);\n }\n if (a.args && a.args.length > 0) {\n lines.push(` args:`);\n for (const arg of a.args) {\n lines.push(` - ${yamlQuoteScalar(arg)}`);\n }\n }\n if (a.promptArgPosition && a.promptArgPosition !== 'first') {\n lines.push(` promptArgPosition: ${a.promptArgPosition}`);\n }\n if (a.default) {\n lines.push(` default: true`);\n }\n if (a.resolveFromShellAliases) {\n lines.push(` resolveFromShellAliases: true`);\n }\n if (a.resume) {\n appendSessionInvocation(lines, 'resume', a.resume);\n }\n if (a.fork) {\n appendSessionInvocation(lines, 'fork', a.fork);\n }\n }\n return lines.join('\\n');\n}\n\nfunction appendSessionInvocation(\n lines: string[],\n key: 'resume' | 'fork',\n invocation: SessionInvocation,\n): void {\n lines.push(` ${key}:`);\n if (invocation.command !== undefined) {\n lines.push(` command: ${yamlQuoteScalar(invocation.command)}`);\n }\n lines.push(` args:`);\n for (const arg of invocation.args) {\n lines.push(` - ${yamlQuoteScalar(arg)}`);\n }\n}\n\nexport async function writeAgentsConfig(agents: AgentConfig[]): Promise<void> {\n validateAgentList(agents);\n const configPath = resolve(syntaurRoot(), 'config.md');\n const agentsBlock = serializeAgentsConfig(agents);\n\n const existing = (await fileExists(configPath))\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${agentsBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content.replace(/\\n\\n---/, '\\n---'));\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'agents');\n const newFm = `${cleanedFm}\\n${agentsBlock}`.replace(/^\\n+/, '').replace(/\\n+$/, '');\n const newContent = `---\\n${newFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteAgentsConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'agents');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function writeStatusConfig(statuses: StatusConfig): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const statusBlock = serializeStatusConfig(statuses);\n\n if (!(await fileExists(configPath))) {\n // Create new config file with defaults + statuses\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ~/projects\\n${statusBlock}\\n---\\n`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n // No frontmatter — wrap in new frontmatter\n const content = `---\\nversion: \"2.0\"\\n${statusBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n\n // Remove existing statuses: block from frontmatter\n const statusesStart = fmBlock.match(/^statuses:\\s*$/m);\n let cleanedFm: string;\n if (statusesStart) {\n const startIdx = fmBlock.indexOf(statusesStart[0]);\n const before = fmBlock.slice(0, startIdx);\n const after = fmBlock.slice(startIdx + statusesStart[0].length);\n // Skip all indented lines (belonging to statuses block)\n const remaining = after.split('\\n');\n let endIdx = 0;\n for (let i = 0; i < remaining.length; i++) {\n const line = remaining[i];\n if (line.trim() === '') { endIdx = i + 1; continue; }\n if (line.length > 0 && line[0] !== ' ') break;\n endIdx = i + 1;\n }\n cleanedFm = before + remaining.slice(endIdx).join('\\n');\n } else {\n cleanedFm = fmBlock;\n }\n\n // Trim trailing whitespace/newlines from cleaned frontmatter\n cleanedFm = cleanedFm.replace(/\\n+$/, '');\n\n const newContent = `---\\n${cleanedFm}\\n${statusBlock}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteStatusConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'statuses');\n\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n/**\n * Parse the nested `search:` block from raw config.md content. Returns null when\n * absent (caller falls back to DEFAULT_SEARCH_CONFIG). Mirrors parseStatusConfig's\n * manual block walk: `defaultScope`/`externalIds` are scalars, `aliases:` is a\n * one-level prefix→kind map. Tolerant — invalid rows are dropped by\n * normalizeSearchConfig.\n */\n/**\n * Parse the optional `staleness:` block into a partial `StaleThresholds`\n * (defaults-first — only keys present here override). Values are durations\n * (`7d`, `12h`, `30m`, `90s`, `500ms`) or bare ms numbers. Malformed/non-positive\n * values are dropped (the gate falls back to its default). Returns null when the\n * block is absent or yields no valid override.\n *\n * staleness:\n * inProgressNoActivity: 14d\n * reviewAging: 2d\n */\nexport function parseStalenessConfig(content: string): Partial<StaleThresholds> | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const blockStart = fmBlock.match(/^staleness:\\s*$/m);\n if (!blockStart) return null;\n\n const startIdx = (blockStart.index ?? 0) + blockStart[0].length;\n const lines = fmBlock.slice(startIdx).split('\\n');\n\n const out: Partial<StaleThresholds> = {};\n for (const line of lines) {\n if (line.trim() === '') continue;\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0) break; // dedent out of the staleness: block\n const ci = trimmed.indexOf(':');\n if (ci <= 0) continue;\n const key = trimmed.slice(0, ci).trim();\n const field = STALENESS_KEY_TO_FIELD[key];\n if (!field) continue;\n let value = trimmed.slice(ci + 1).trim();\n if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n const ms = parseDurationMs(value);\n if (ms !== null) out[field] = ms;\n }\n\n return Object.keys(out).length > 0 ? out : null;\n}\n\n/**\n * Validate the raw `staleness:` block, returning a problem string per offending\n * entry (unknown key, or unparseable/non-positive duration). Empty array = OK\n * (including when the block is absent). The parser fails safe by dropping these\n * silently; this surfaces them in `syntaur doctor` so typos don't go unnoticed.\n */\nexport function validateStalenessConfig(content: string): string[] {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return [];\n const fmBlock = match[1];\n const blockStart = fmBlock.match(/^staleness:\\s*$/m);\n if (!blockStart) return [];\n\n const startIdx = (blockStart.index ?? 0) + blockStart[0].length;\n const lines = fmBlock.slice(startIdx).split('\\n');\n const problems: string[] = [];\n\n for (const line of lines) {\n if (line.trim() === '') continue;\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0) break;\n const ci = trimmed.indexOf(':');\n if (ci <= 0) continue;\n const key = trimmed.slice(0, ci).trim();\n let value = trimmed.slice(ci + 1).trim();\n if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n if (!(key in STALENESS_KEY_TO_FIELD)) {\n problems.push(`staleness.${key}: unknown key (expected one of ${Object.keys(STALENESS_KEY_TO_FIELD).join(', ')})`);\n continue;\n }\n if (parseDurationMs(value) === null) {\n problems.push(`staleness.${key}: \"${value}\" is not a positive duration (e.g. 7d, 12h, 30m, 90s, 500ms)`);\n }\n }\n return problems;\n}\n\nexport function parseSearchConfig(content: string): SearchConfig | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const blockStart = fmBlock.match(/^search:\\s*$/m);\n if (!blockStart) return null;\n\n // Use the regex match offset (NOT indexOf) — the literal text `search:` can\n // appear earlier inside another block's value (e.g. an AQL derive condition\n // `when: \"search:foo\"`), and indexOf would slice from there.\n const startIdx = (blockStart.index ?? 0) + blockStart[0].length;\n const lines = fmBlock.slice(startIdx).split('\\n');\n\n const unquote = (v: string): string => {\n const t = v.trim();\n if ((t.startsWith('\"') && t.endsWith('\"')) || (t.startsWith(\"'\") && t.endsWith(\"'\"))) {\n return t.slice(1, -1);\n }\n return t;\n };\n\n const raw: { defaultScope?: string; aliases?: Record<string, string>; externalIds?: boolean } = {};\n let inAliases = false;\n\n for (const line of lines) {\n if (line.trim() === '') continue;\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0) break; // dedent out of the search: block\n\n if (indent <= 2) {\n inAliases = false;\n if (trimmed === 'aliases:') {\n inAliases = true;\n raw.aliases = {};\n continue;\n }\n const ci = trimmed.indexOf(':');\n if (ci <= 0) continue;\n const key = trimmed.slice(0, ci).trim();\n const value = unquote(trimmed.slice(ci + 1).trim());\n if (key === 'defaultScope') {\n raw.defaultScope = value;\n } else if (key === 'externalIds') {\n // Only recognize real booleans; anything else stays undefined so\n // normalizeSearchConfig falls back to the default (true).\n const v = value.toLowerCase();\n if (v === 'true') raw.externalIds = true;\n else if (v === 'false') raw.externalIds = false;\n }\n } else if (inAliases) {\n const ci = trimmed.indexOf(':');\n if (ci <= 0) continue;\n raw.aliases ??= {};\n raw.aliases[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1).trim());\n }\n }\n\n return normalizeSearchConfig(raw);\n}\n\n/** Serialize a SearchConfig into the `search:` frontmatter block (no trailing newline). */\nexport function serializeSearchConfig(search: SearchConfig): string {\n const cfg = normalizeSearchConfig(search);\n const lines: string[] = ['search:'];\n lines.push(` defaultScope: ${cfg.defaultScope}`);\n lines.push(' aliases:');\n for (const [prefix, kind] of Object.entries(cfg.aliases)) {\n lines.push(` ${prefix}: ${kind}`);\n }\n lines.push(` externalIds: ${cfg.externalIds ? 'true' : 'false'}`);\n return lines.join('\\n');\n}\n\nexport async function writeSearchConfig(search: SearchConfig): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const searchBlock = serializeSearchConfig(search);\n\n if (!(await fileExists(configPath))) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ~/projects\\n${searchBlock}\\n---\\n`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\n${searchBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'search');\n\n const newContent = `---\\n${cleanedFm}\\n${searchBlock}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteSearchConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'search');\n\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n/** The configured search settings, or the built-in defaults when unset. */\nexport function getSearchConfig(config: SyntaurConfig): SearchConfig {\n return config.searchConfig ?? DEFAULT_SEARCH_CONFIG;\n}\n\nexport async function updateIntegrationConfig(\n integrations: Partial<IntegrationConfig>,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const nextIntegrations: IntegrationConfig = {\n ...(await readConfig()).integrations,\n ...integrations,\n };\n\n const integrationBlock = serializeIntegrationConfig(nextIntegrations);\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${integrationBlock ?? ''}\\n---\\n${existing}`;\n await writeFileForce(configPath, content.replace(/\\n\\n---/, '\\n---'));\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'integrations');\n const newFm = integrationBlock\n ? `${cleanedFm}\\n${integrationBlock}`.replace(/^\\n+/, '')\n : cleanedFm;\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function updateOnboardingConfig(\n onboarding: Partial<OnboardingConfig>,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const nextOnboarding: OnboardingConfig = {\n ...(await readConfig()).onboarding,\n ...onboarding,\n };\n\n const onboardingBlock = serializeOnboardingConfig(nextOnboarding);\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${onboardingBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content.replace(/\\n\\n---/, '\\n---'));\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'onboarding');\n const newFm = `${cleanedFm}\\n${onboardingBlock}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function updateBackupConfig(\n backup: Partial<BackupConfig>,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const current = (await readConfig()).backup;\n const nextBackup: BackupConfig = {\n repo: current?.repo ?? null,\n categories: current?.categories ?? 'projects, playbooks, todos, servers, config',\n lastBackup: current?.lastBackup ?? null,\n lastRestore: current?.lastRestore ?? null,\n ...backup,\n };\n\n const backupBlock = serializeBackupConfig(nextBackup);\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${backupBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content.replace(/\\n\\n---/, '\\n---'));\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'backup');\n const newFm = `${cleanedFm}\\n${backupBlock}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n// Guard so the legacy-config migration runs at most once per config path per\n// process lifetime. Keyed by absolute path so tests with multiple sandbox\n// HOMEs still get the migration applied to each.\nconst migratedConfigPaths = new Set<string>();\n\nexport async function readConfig(): Promise<SyntaurConfig> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) {\n return cloneDefaultConfig();\n }\n\n if (!migratedConfigPaths.has(configPath)) {\n migratedConfigPaths.add(configPath);\n await migrateLegacyConfig(configPath);\n }\n\n const content = await readFile(configPath, 'utf-8');\n const fm = parseFrontmatter(content);\n\n if (Object.keys(fm).length === 0) {\n console.warn('Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults');\n return cloneDefaultConfig();\n }\n\n let projectDir = fm['defaultProjectDir']\n ? expandHome(String(fm['defaultProjectDir']))\n : DEFAULT_CONFIG.defaultProjectDir;\n if (!isAbsolute(projectDir)) {\n console.warn(\n `Warning: config.md defaultProjectDir is not an absolute path (\"${fm['defaultProjectDir']}\"), using default`,\n );\n projectDir = DEFAULT_CONFIG.defaultProjectDir;\n }\n\n const fmBlock = content.match(/^---\\n([\\s\\S]*?)\\n---/)?.[1] ?? '';\n\n return {\n version: fm['version'] || DEFAULT_CONFIG.version,\n defaultProjectDir: projectDir,\n onboarding: {\n completed: fm['onboarding.completed'] === 'true',\n },\n agentDefaults: {\n trustLevel:\n (fm['agentDefaults.trustLevel'] as SyntaurConfig['agentDefaults']['trustLevel']) ||\n DEFAULT_CONFIG.agentDefaults.trustLevel,\n autoApprove:\n fm['agentDefaults.autoApprove'] === 'true' ||\n DEFAULT_CONFIG.agentDefaults.autoApprove,\n autoCreateWorktree: AUTO_CREATE_WORKTREE_VALUES.includes(\n fm['agentDefaults.autoCreateWorktree'] as AutoCreateWorktree,\n )\n ? (fm['agentDefaults.autoCreateWorktree'] as AutoCreateWorktree)\n : DEFAULT_CONFIG.agentDefaults.autoCreateWorktree,\n },\n session: {\n autoTrack: SESSION_AUTO_TRACK_VALUES.includes(\n fm['session.autoTrack'] as SessionAutoTrack,\n )\n ? (fm['session.autoTrack'] as SessionAutoTrack)\n : DEFAULT_CONFIG.session.autoTrack,\n },\n integrations: {\n claudePluginDir: parseOptionalAbsolutePath(\n fm['integrations.claudePluginDir'],\n 'integrations.claudePluginDir',\n ),\n codexPluginDir: parseOptionalAbsolutePath(\n fm['integrations.codexPluginDir'],\n 'integrations.codexPluginDir',\n ),\n codexMarketplacePath: parseOptionalAbsolutePath(\n fm['integrations.codexMarketplacePath'],\n 'integrations.codexMarketplacePath',\n ),\n ...parseInstalledAgents(fm),\n },\n backup: fm['backup.repo'] || fm['backup.categories']\n ? {\n repo: fm['backup.repo'] && fm['backup.repo'] !== 'null' ? fm['backup.repo'] : null,\n categories: fm['backup.categories'] || 'projects, playbooks, todos, servers, config',\n lastBackup: fm['backup.lastBackup'] && fm['backup.lastBackup'] !== 'null' ? fm['backup.lastBackup'] : null,\n lastRestore: fm['backup.lastRestore'] && fm['backup.lastRestore'] !== 'null' ? fm['backup.lastRestore'] : null,\n }\n : null,\n statuses: parseStatusConfig(content),\n types: null,\n agents: normalizeAgentsFromConfig(parseAgentsConfig(content)),\n playbooks: parsePlaybooksConfig(fmBlock),\n theme: parseThemeConfig(content),\n hotkeys: parseHotkeyBindingsConfig(content),\n terminal: (() => {\n try {\n return parseTerminalConfig(fm['terminal']);\n } catch (err) {\n const msg = err instanceof TerminalConfigError ? err.message : String(err);\n console.warn(`Warning: ${msg} — falling back to default`);\n return null;\n }\n })(),\n searchConfig: parseSearchConfig(content),\n workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock),\n staleness: parseStalenessConfig(content),\n stalenessWatchdog: String(fm['stalenessWatchdog']).toLowerCase() === 'true',\n };\n}\n\nexport function getAssignmentTypes(config: SyntaurConfig): TypesConfig {\n return config.types ?? DEFAULT_ASSIGNMENT_TYPES;\n}\n\nexport function getAgents(config: SyntaurConfig): AgentConfig[] {\n if (config.agents === null) return BUILTIN_AGENTS;\n // For agents whose id matches any builtin (claude/codex/pi/openclaw/hermes),\n // inherit that builtin's resume/fork for whichever the user omitted. Builtins\n // without a recipe (openclaw/hermes) have nothing to inherit, so an omitted\n // field stays omitted. Omission means \"inherit\", not\n // \"disable\": there is no syntax to express intentional disable, and the\n // dashboard agent editor (api-agents coerceAgentRow) silently drops these\n // fields, so omission is frequently accidental. User-provided values win;\n // non-builtin agents pass through untouched. Inputs are never mutated.\n const builtinById = new Map(BUILTIN_AGENTS.map((a) => [a.id, a]));\n return config.agents.map((agent) => {\n const builtin = builtinById.get(agent.id);\n if (!builtin) return agent;\n const resume = agent.resume ?? builtin.resume;\n const fork = agent.fork ?? builtin.fork;\n if (resume === agent.resume && fork === agent.fork) return agent;\n return {\n ...agent,\n ...(resume ? { resume } : {}),\n ...(fork ? { fork } : {}),\n };\n });\n}\n\nexport class TerminalConfigError extends Error {}\n\n/**\n * Parse the `terminal:` scalar from raw frontmatter values.\n * Returns null when the key is absent (caller falls back to platform default).\n * Throws TerminalConfigError when the value is not a known choice.\n */\nexport function parseTerminalConfig(value: unknown): TerminalChoice | null {\n if (value === undefined || value === null || value === '') return null;\n if (typeof value !== 'string') {\n throw new TerminalConfigError(\n `terminal must be a string — got ${typeof value}`,\n );\n }\n const trimmed = value.trim();\n if (trimmed === '') return null;\n if (!TERMINAL_CHOICES.includes(trimmed as TerminalChoice)) {\n throw new TerminalConfigError(\n `terminal \"${trimmed}\" is not a known choice — expected one of ${TERMINAL_CHOICES.join('|')}`,\n );\n }\n return trimmed as TerminalChoice;\n}\n\n/**\n * Return the configured terminal, or the platform default when unset.\n *\n * darwin → terminal-app (always available).\n * linux → first of [kitty, alacritty, warp] resolvable via `which`, in that\n * order. If none are installed, return terminal-app as a stable\n * sentinel (doctor will surface the install gap separately).\n * other → terminal-app sentinel.\n *\n * The Linux probe order is intentionally deterministic and documented so the\n * dashboard's preflight + the Settings hint show the same value.\n */\nexport function getTerminal(config: SyntaurConfig): TerminalChoice {\n if (config.terminal) return config.terminal;\n if (process.platform === 'darwin') return 'terminal-app';\n if (process.platform === 'linux') {\n const order: TerminalChoice[] = ['kitty', 'alacritty', 'warp'];\n for (const candidate of order) {\n const result = spawnSync('which', [candidate], { encoding: 'utf-8' });\n if (result.status === 0 && result.stdout.trim().length > 0) {\n return candidate;\n }\n }\n }\n return 'terminal-app';\n}\n\nexport interface AgentsMutation {\n kind: 'add' | 'remove' | 'set' | 'reorder';\n apply: (current: AgentConfig[]) => AgentConfig[];\n}\n\n/**\n * Apply a mutation to the agents list, validate, and either write or return the\n * proposed new list (for --dry-run). Always runs full validation.\n */\nexport async function updateAgentsConfig(\n mutation: AgentsMutation,\n options: { dryRun?: boolean } = {},\n): Promise<{ previous: AgentConfig[]; next: AgentConfig[]; written: boolean }> {\n const config = await readConfig();\n const previous = config.agents ?? [...BUILTIN_AGENTS];\n const next = mutation.apply(previous);\n validateAgentList(next);\n\n if (options.dryRun) {\n return { previous, next, written: false };\n }\n\n await writeAgentsConfig(next);\n return { previous, next, written: true };\n}\n","import { resolve } from 'node:path';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { fileExists } from './fs.js';\nimport { extractFrontmatter, getField } from '../dashboard/parser.js';\n\nexport interface ResolvedAssignment {\n assignmentDir: string;\n projectSlug: string | null;\n assignmentSlug: string;\n id: string;\n standalone: boolean;\n workspaceGroup: string | null;\n}\n\nexport async function resolveAssignmentById(\n projectsDir: string,\n assignmentsDir: string,\n id: string,\n): Promise<ResolvedAssignment | null> {\n let standaloneMatch: ResolvedAssignment | null = null;\n let projectMatch: ResolvedAssignment | null = null;\n\n // 1) Standalone: <assignmentsDir>/<id>/assignment.md\n const standaloneDir = resolve(assignmentsDir, id);\n const standalonePath = resolve(standaloneDir, 'assignment.md');\n if (await fileExists(standalonePath)) {\n let workspaceGroup: string | null = null;\n try {\n const content = await readFile(standalonePath, 'utf-8');\n const [fm] = extractFrontmatter(content);\n workspaceGroup = getField(fm, 'workspaceGroup');\n } catch {\n // unreadable — leave null\n }\n standaloneMatch = {\n assignmentDir: standaloneDir,\n projectSlug: null,\n assignmentSlug: id,\n id,\n standalone: true,\n workspaceGroup,\n };\n }\n\n // 2) Project-nested: scan <projectsDir>/*/assignments/*/assignment.md and match by frontmatter id\n if (await fileExists(projectsDir)) {\n try {\n const projects = await readdir(projectsDir, { withFileTypes: true });\n for (const p of projects) {\n if (!p.isDirectory()) continue;\n if (p.name.startsWith('.') || p.name.startsWith('_')) continue;\n const assignmentsPath = resolve(projectsDir, p.name, 'assignments');\n if (!(await fileExists(assignmentsPath))) continue;\n\n const entries = await readdir(assignmentsPath, { withFileTypes: true });\n for (const a of entries) {\n if (!a.isDirectory()) continue;\n const aPath = resolve(assignmentsPath, a.name, 'assignment.md');\n if (!(await fileExists(aPath))) continue;\n\n try {\n const content = await readFile(aPath, 'utf-8');\n const [fm] = extractFrontmatter(content);\n const fileId = getField(fm, 'id');\n if (fileId === id) {\n projectMatch = {\n assignmentDir: resolve(assignmentsPath, a.name),\n projectSlug: p.name,\n assignmentSlug: a.name,\n id,\n standalone: false,\n workspaceGroup: null,\n };\n break;\n }\n } catch {\n // skip unreadable\n }\n }\n if (projectMatch) break;\n }\n } catch {\n // projectsDir not readable\n }\n }\n\n if (standaloneMatch && projectMatch) {\n console.warn(\n `Duplicate assignment ID ${id} found in both standalone and project-nested locations; using standalone`,\n );\n return standaloneMatch;\n }\n\n return standaloneMatch ?? projectMatch ?? null;\n}\n","/**\n * Derived-status dimension engine (design v3, Piece 2) — PURE.\n *\n * Evaluates the configured phase ladder + disposition rules over an\n * assignment's facts and projects the headline status. No filesystem access\n * and no Node-only imports — the dashboard client can evaluate the same rules\n * over server-materialized facts. Fact *computation* lives in `facts.ts`\n * (Node-side).\n *\n * Invariants enforced here:\n * - Terminal assignments defer entirely: callers get `null` and must leave\n * every dimension as-is (terminal is reached only via the gated\n * complete/fail transitions; `reopen` re-enters derivation).\n * - Derive conditions evaluate over FACTS ONLY (`DERIVE_FIELDS`): time-based\n * fields are not in the registry, so a `statusAge > 3d` rung is a config\n * validation error — time drives payload flags, never dimensions.\n * - The override is folded into the effective status here (write-side), but\n * a terminal or unknown override target is ignored (defense in depth — the\n * pin CLI already refuses those).\n */\n\nimport type { DeriveConfig } from '../utils/derive-config.js';\nimport { compileNode, CompileError, parseQuery, type Predicate, type FieldRegistry } from '../utils/query/index.js';\nimport type { StatusOverride } from './types.js';\nimport { DERIVE_FIELDS } from '../utils/fact-registry.js';\n\n// Re-export all fact-vocabulary symbols so existing imports from this module\n// keep resolving without change.\nexport type { FactDeclaration } from '../utils/fact-registry.js';\nexport type { FactFieldNames } from '../utils/fact-registry.js';\nexport {\n DERIVE_FIELDS,\n factFieldNames,\n acceptFactDeclarations,\n addFactFields,\n buildDeriveRegistry,\n buildQueryRegistry,\n queryFieldNames,\n} from '../utils/fact-registry.js';\n\n/** The fixed built-in fact set (the 14 derived-status v3 facts). Custom facts\n * extend {@link AssignmentFacts} dynamically via the config-declared registry. */\nexport interface BuiltinFacts {\n hasRealObjective: boolean;\n acRealTotal: number;\n acRealChecked: number;\n acAllChecked: boolean;\n planExists: boolean;\n planApproved: boolean;\n workspaceSet: boolean;\n implementationStarted: boolean;\n depsSatisfied: boolean;\n unresolvedQuestions: number;\n blocked: boolean;\n parked: boolean;\n reviewRequested: boolean;\n pinned: boolean;\n}\n\n/**\n * The fact set dimensions derive from. Computed by `facts.ts` (Node) or shipped\n * in dashboard payloads (browser). The 14 built-ins are always present; custom\n * declared facts (bool/number) and attestation exports (`<name>`,\n * `<name>Approved`, … as boolean / actor `string[]`) ride in the open index.\n */\nexport type AssignmentFacts = BuiltinFacts &\n Record<string, boolean | number | string[]>;\n\n/** Validate one derive condition against a field registry (defaults to the\n * facts-only base). Returns an error message or null. Plugs into\n * validateDeriveConfig; pass a custom registry to accept declared fact names. */\nexport function validateDeriveCondition(\n when: string,\n registry: FieldRegistry = DERIVE_FIELDS,\n): string | null {\n if (when === '*') return null;\n const parsed = parseQuery(when);\n if (!parsed.ast) return parsed.errors[0]?.message ?? 'unparseable condition';\n try {\n compileNode(parsed.ast, registry);\n return null;\n } catch (err) {\n if (err instanceof CompileError) return err.errors[0]?.message ?? 'invalid condition';\n throw err;\n }\n}\n\nexport interface DerivedDimensions {\n /** Highest satisfied ladder rung (regressible — replan can drop it). */\n phase: string;\n disposition: 'active' | 'blocked' | 'parked';\n /** Headline projection BEFORE the override — payload-only, powers the\n * \"pinned to X — would otherwise be Y\" divergence display. */\n derivedStatus: string;\n /** Effective headline (override folded in) — what gets written to `status`. */\n status: string;\n /** The matched rung's `next:` label — the per-ticket call to action. */\n nextAction: string | null;\n}\n\n// Compiled-condition cache, keyed by REGISTRY object identity — config reloads\n// and config-resolution build a fresh registry → fresh cache. Keying by\n// registry (not config) lets all default-config derivations share the base\n// DERIVE_FIELDS cache, while a custom-vocabulary config gets its own. Callers\n// must build ONE registry per config resolution for sweeps to stay cached.\nconst conditionCache = new WeakMap<FieldRegistry, Map<string, Predicate>>();\n\nfunction compiledWhen(registry: FieldRegistry, when: string): Predicate {\n let cache = conditionCache.get(registry);\n if (!cache) {\n cache = new Map();\n conditionCache.set(registry, cache);\n }\n let pred = cache.get(when);\n if (!pred) {\n if (when === '*') {\n pred = () => true;\n } else {\n const parsed = parseQuery(when);\n if (!parsed.ast) {\n throw new CompileError(parsed.errors);\n }\n pred = compileNode(parsed.ast, registry);\n }\n cache.set(when, pred);\n }\n return pred;\n}\n\nexport interface DeriveInput {\n facts: AssignmentFacts;\n derive: DeriveConfig;\n /** Current headline status from frontmatter (for the terminal check). */\n currentStatus: string;\n terminalStatuses: ReadonlySet<string>;\n /** Defined status ids — headline targets outside this set fall back to phase. */\n knownStatusIds: ReadonlySet<string>;\n override: StatusOverride | null;\n /** Field registry the `when` conditions compile against. Defaults to the base\n * facts-only registry; callers with custom facts pass the resolution's\n * `buildDeriveRegistry(...)` output (ONE per config resolution — see the\n * compile-cache note). */\n registry?: FieldRegistry;\n}\n\n/**\n * Derive phase/disposition/headline for one assignment. Returns `null` when\n * the assignment is terminal — derivation defers entirely until `reopen`.\n */\nexport function deriveDimensions(input: DeriveInput): DerivedDimensions | null {\n const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;\n const registry = input.registry ?? DERIVE_FIELDS;\n\n if (terminalStatuses.has(currentStatus)) return null;\n\n const ctx = { now: 0 }; // derive conditions are time-free by construction\n const item = facts as unknown as Record<string, unknown>;\n\n // Phase: HIGHEST satisfied rung wins (iterate top-down). The bottom rung is\n // conventionally `*`; if nothing matches (misconfigured ladder), fall back\n // to the bottom rung's phase rather than inventing a status.\n let phase = derive.phaseLadder[0]?.phase ?? currentStatus;\n let nextAction: string | null = derive.phaseLadder[0]?.next ?? null;\n for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {\n const rung = derive.phaseLadder[i];\n if (compiledWhen(registry, rung.when)(item, ctx)) {\n phase = rung.phase;\n nextAction = rung.next ?? null;\n break;\n }\n }\n\n // Disposition: first match wins; `when: null` is the else arm.\n let disposition: DerivedDimensions['disposition'] = 'active';\n for (const rule of derive.disposition) {\n if (rule.when === null || compiledWhen(registry, rule.when)(item, ctx)) {\n disposition = rule.is as DerivedDimensions['disposition'];\n break;\n }\n }\n\n // Headline projection. Unknown target ids (e.g. parked without a `parked`\n // status definition) fall back to the phase so the board never shows an\n // undefined status; doctor surfaces the missing definition.\n let derivedStatus: string;\n switch (disposition) {\n case 'parked':\n derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;\n break;\n case 'blocked':\n derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;\n break;\n default:\n derivedStatus = phase;\n }\n\n // Fold the override (effective = override ?? derived). Terminal or unknown\n // targets are ignored — the pin CLI refuses them, this is defense in depth.\n let status = derivedStatus;\n if (\n override &&\n override.status &&\n !terminalStatuses.has(override.status) &&\n knownStatusIds.has(override.status)\n ) {\n status = override.status;\n }\n\n return { phase, disposition, derivedStatus, status, nextAction };\n}\n","import { resolve } from 'node:path';\nimport { readdir, readFile, unlink } from 'node:fs/promises';\nimport { fileExists, writeFileForce } from './fs.js';\nimport { parsePlaybook, type ParsedPlaybook } from '../dashboard/parser.js';\nimport { nowTimestamp } from './timestamp.js';\nimport { readConfig, updatePlaybooksConfig } from './config.js';\nimport { isValidSlug } from './slug.js';\n\nexport interface ResolvedPlaybook {\n filename: string;\n slug: string;\n parsed: ParsedPlaybook;\n}\n\nexport type PlaybookErrorCode = 'manifest' | 'not-found' | 'invalid-slug' | 'collision';\n\n/**\n * Stable error thrown by playbook helpers. Routers and CLI commands branch on\n * `code` to map to HTTP status / exit code without string matching.\n */\nexport class PlaybookError extends Error {\n readonly code: PlaybookErrorCode;\n constructor(code: PlaybookErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'PlaybookError';\n }\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Replace or insert a frontmatter scalar field. Playbook files always have\n * frontmatter (parsePlaybook depends on it). If the field is absent, insert it\n * just before the closing `---`. Values are written verbatim (caller decides\n * quoting).\n */\nfunction setFrontmatterField(content: string, key: string, value: string): string {\n const regex = new RegExp(`^(${escapeRegExp(key)}:)\\\\s*.*$`, 'm');\n if (regex.test(content)) {\n return content.replace(regex, `$1 ${value}`);\n }\n const closingIdx = content.indexOf('\\n---', 4);\n if (closingIdx === -1) {\n return content;\n }\n return `${content.slice(0, closingIdx)}\\n${key}: ${value}${content.slice(closingIdx)}`;\n}\n\nfunction isVisiblePlaybookFile(name: string, isFile: boolean): boolean {\n return isFile && name.endsWith('.md') && !name.startsWith('_') && name !== 'manifest.md';\n}\n\n/**\n * Resolve a requested slug to a concrete playbook file.\n *\n * Canonical slug is the `slug` field in the playbook's frontmatter. If that\n * field is missing we fall back to the filename stem. This means a playbook\n * with `filename: foo.md` and frontmatter `slug: bar` is reachable by `bar`\n * (and NOT by `foo`) — this keeps behavior consistent across dashboard +\n * CLI so enable/disable state is addressable by a single canonical slug.\n */\nexport async function resolvePlaybookSlug(\n playbooksDir: string,\n slug: string,\n): Promise<ResolvedPlaybook | null> {\n if (!(await fileExists(playbooksDir))) return null;\n\n const entries = await readdir(playbooksDir, { withFileTypes: true });\n\n let filenameStemFallback: ResolvedPlaybook | null = null;\n\n for (const entry of entries) {\n if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;\n\n const filePath = resolve(playbooksDir, entry.name);\n const raw = await readFile(filePath, 'utf-8');\n const parsed = parsePlaybook(raw);\n const canonical = parsed.slug || entry.name.replace(/\\.md$/, '');\n\n if (canonical === slug) {\n return { filename: entry.name, slug: canonical, parsed };\n }\n\n // Only use the filename stem as a fallback when frontmatter slug is absent.\n if (!parsed.slug && entry.name.replace(/\\.md$/, '') === slug) {\n filenameStemFallback = { filename: entry.name, slug: canonical, parsed };\n }\n }\n\n return filenameStemFallback;\n}\n\n/**\n * List the canonical slugs of all installed playbook files in `playbooksDir`\n * (enabled or not). Canonical slug is the frontmatter `slug`, falling back to\n * the filename stem — matching `resolvePlaybookSlug`. Returns an empty set if\n * the directory is absent. Used at launch to validate `@<playbook-slug>` tokens\n * in an agent's `launchPrompt`.\n */\nexport async function listPlaybookSlugs(playbooksDir: string): Promise<Set<string>> {\n const slugs = new Set<string>();\n if (!(await fileExists(playbooksDir))) return slugs;\n\n const entries = await readdir(playbooksDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;\n const filePath = resolve(playbooksDir, entry.name);\n const raw = await readFile(filePath, 'utf-8');\n const parsed = parsePlaybook(raw);\n slugs.add(parsed.slug || entry.name.replace(/\\.md$/, ''));\n }\n return slugs;\n}\n\n/**\n * Toggle a playbook's enabled/disabled state. Writes config.md, rebuilds the\n * manifest, and returns the canonical slug + resulting enabled flag.\n *\n * Throws if the slug cannot be resolved to a playbook file.\n */\nexport async function setPlaybookEnabled(\n playbooksDir: string,\n slug: string,\n enabled: boolean,\n): Promise<{ slug: string; enabled: boolean; changed: boolean }> {\n const resolved = await resolvePlaybookSlug(playbooksDir, slug);\n if (!resolved) {\n throw new Error(`Playbook \"${slug}\" not found in ${playbooksDir}`);\n }\n\n const config = await readConfig();\n const disabledSet = new Set(config.playbooks.disabled);\n const wasDisabled = disabledSet.has(resolved.slug);\n const shouldBeDisabled = !enabled;\n\n if (wasDisabled === shouldBeDisabled) {\n return { slug: resolved.slug, enabled, changed: false };\n }\n\n if (shouldBeDisabled) {\n disabledSet.add(resolved.slug);\n } else {\n disabledSet.delete(resolved.slug);\n }\n\n await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });\n await rebuildPlaybookManifest(playbooksDir);\n\n return { slug: resolved.slug, enabled, changed: true };\n}\n\n/**\n * Load a playbook ONLY if it is enabled. Returns null when the playbook does\n * not exist OR is disabled in config. Intended for agent-facing lookups that\n * must respect the disabled state.\n *\n * Dashboard admin code should NOT use this — it uses the unfiltered\n * `getPlaybookDetail` so admins can still see and re-enable disabled playbooks.\n */\nexport async function loadEnabledPlaybook(\n playbooksDir: string,\n slug: string,\n): Promise<ParsedPlaybook | null> {\n const resolved = await resolvePlaybookSlug(playbooksDir, slug);\n if (!resolved) return null;\n\n const config = await readConfig();\n if (config.playbooks.disabled.includes(resolved.slug)) {\n return null;\n }\n\n return resolved.parsed;\n}\n\n/**\n * Remove a slug from the disabled list. Called when a playbook is deleted so\n * a later reincarnation with the same slug doesn't silently start disabled.\n * No-op if the slug isn't currently disabled.\n */\nexport async function removeFromDisabledList(slug: string): Promise<void> {\n const config = await readConfig();\n if (!config.playbooks.disabled.includes(slug)) return;\n await updatePlaybooksConfig({\n disabled: config.playbooks.disabled.filter((s) => s !== slug),\n });\n}\n\nexport async function rebuildPlaybookManifest(playbooksDir: string): Promise<void> {\n if (!(await fileExists(playbooksDir))) return;\n\n const config = await readConfig();\n const disabledSet = new Set(config.playbooks.disabled);\n\n const entries = await readdir(playbooksDir, { withFileTypes: true });\n const rows: Array<{ name: string; slug: string; description: string; whenToUse: string }> = [];\n\n for (const entry of entries) {\n if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;\n\n const raw = await readFile(resolve(playbooksDir, entry.name), 'utf-8');\n const parsed = parsePlaybook(raw);\n const slug = parsed.slug || entry.name.replace(/\\.md$/, '');\n\n if (disabledSet.has(slug)) continue;\n\n rows.push({\n name: parsed.name || slug,\n slug,\n description: parsed.description,\n whenToUse: parsed.whenToUse,\n });\n }\n\n rows.sort((a, b) => a.name.localeCompare(b.name));\n\n const timestamp = nowTimestamp();\n const lines = [\n '---',\n `generated: \"${timestamp}\"`,\n `total: ${rows.length}`,\n '---',\n '',\n '# Playbooks',\n '',\n 'Behavioral rules for AI agents. Read and follow all playbooks before starting work.',\n '',\n ];\n\n for (const row of rows) {\n lines.push(`- **[${row.name}](${row.slug}.md)** — ${row.description}`);\n if (row.whenToUse) {\n lines.push(` _When to use: ${row.whenToUse}_`);\n }\n }\n\n lines.push('');\n\n await writeFileForce(resolve(playbooksDir, 'manifest.md'), lines.join('\\n'));\n}\n\n/**\n * Delete a playbook file from disk and regenerate the manifest. Refuses\n * `manifest`. Drops the slug from `config.playbooks.disabled` if present so a\n * later recreation with the same slug doesn't silently start disabled. Throws\n * `PlaybookError` on `manifest` / `not-found`.\n *\n * Shared by `DELETE /api/playbooks/:slug` and `syntaur delete-playbook`.\n */\nexport async function deletePlaybook(\n playbooksDir: string,\n slug: string,\n): Promise<{ slug: string }> {\n if (slug === 'manifest') {\n throw new PlaybookError('manifest', 'The playbook manifest cannot be deleted.');\n }\n\n const resolved = await resolvePlaybookSlug(playbooksDir, slug);\n if (!resolved) {\n throw new PlaybookError('not-found', `Playbook \"${slug}\" not found.`);\n }\n\n await unlink(resolve(playbooksDir, resolved.filename));\n await removeFromDisabledList(resolved.slug);\n await rebuildPlaybookManifest(playbooksDir);\n\n return { slug: resolved.slug };\n}\n\n/**\n * Rename a playbook to a new slug. Validates the new slug, refuses `manifest`,\n * and rejects collisions at both filename and canonical-slug levels. Updates\n * the on-disk file's frontmatter `slug:` field. Migrates the disabled-list\n * entry if needed. Regenerates the manifest.\n *\n * Special case: if `oldPath === newPath` (e.g., file is `foo.md` with\n * frontmatter `slug: bar`, caller renames `bar -> foo`), rewrite the file in\n * place without unlinking. Returns `renamedInPlace: true` in that case.\n */\nexport async function renamePlaybook(\n playbooksDir: string,\n oldSlug: string,\n newSlug: string,\n): Promise<{ from: string; to: string; renamedInPlace: boolean }> {\n if (!isValidSlug(newSlug)) {\n throw new PlaybookError(\n 'invalid-slug',\n `Invalid slug \"${newSlug}\". Slugs must be lowercase, hyphen-separated, with no special characters.`,\n );\n }\n if (newSlug === 'manifest') {\n throw new PlaybookError('manifest', 'A playbook cannot be named \"manifest\".');\n }\n\n const resolved = await resolvePlaybookSlug(playbooksDir, oldSlug);\n if (!resolved) {\n throw new PlaybookError('not-found', `Playbook \"${oldSlug}\" not found.`);\n }\n\n const oldPath = resolve(playbooksDir, resolved.filename);\n const newPath = resolve(playbooksDir, `${newSlug}.md`);\n\n // Rename-in-place: e.g., file `foo.md` with `slug: bar` renamed `bar -> foo`.\n // The on-disk filename doesn't change; only the frontmatter slug field does.\n const renamedInPlace = oldPath === newPath;\n\n if (!renamedInPlace) {\n // Filename collision: another file already occupies the new path.\n if (await fileExists(newPath)) {\n throw new PlaybookError(\n 'collision',\n `A playbook file already exists at \"${newSlug}.md\".`,\n );\n }\n // Canonical-slug collision: another file declares this slug in its frontmatter.\n const existing = await resolvePlaybookSlug(playbooksDir, newSlug);\n if (existing && resolve(playbooksDir, existing.filename) !== oldPath) {\n throw new PlaybookError(\n 'collision',\n `Another playbook already uses the canonical slug \"${newSlug}\".`,\n );\n }\n }\n\n const raw = await readFile(oldPath, 'utf-8');\n let next = setFrontmatterField(raw, 'slug', newSlug);\n next = setFrontmatterField(next, 'updated', `\"${nowTimestamp()}\"`);\n\n await writeFileForce(newPath, next);\n if (!renamedInPlace) {\n await unlink(oldPath);\n }\n\n // Migrate disabled-list entry if the old canonical slug was disabled.\n const config = await readConfig();\n if (config.playbooks.disabled.includes(resolved.slug)) {\n const nextDisabled = config.playbooks.disabled\n .filter((s) => s !== resolved.slug)\n .concat(newSlug);\n await updatePlaybooksConfig({ disabled: Array.from(new Set(nextDisabled)).sort() });\n }\n\n await rebuildPlaybookManifest(playbooksDir);\n\n return { from: resolved.slug, to: newSlug, renamedInPlace };\n}\n","import { existsSync, statSync } from 'node:fs';\nimport { isAbsolute } from 'node:path';\n\n/**\n * True only for an absolute path that exists and is a directory. Wraps the\n * `statSync` call so a race (deleted between `existsSync` and `statSync`) or a\n * permission error resolves to `false` rather than throwing.\n */\nexport function isExistingDir(p: string | null | undefined): boolean {\n if (!p || !isAbsolute(p)) return false;\n try {\n return existsSync(p) && statSync(p).isDirectory();\n } catch {\n return false;\n }\n}\n\nexport interface WorkspaceCwdInput {\n worktreePath: string | null;\n repository: string | null;\n branch: string | null;\n assignmentSlug: string;\n}\n\nexport interface WorkspaceCwdResult {\n /** Resolved, validated working directory, or `null` when none is valid. */\n cwd: string | null;\n /** Non-fatal warning when falling back from a missing/invalid worktree. */\n fallbackWarning: string | null;\n /** Human-readable reason, set only when `cwd` is `null`. */\n invalidReason: string | null;\n}\n\n/**\n * Resolve the working directory for a launch, preferring a validated\n * `worktreePath`, then a validated `repository`. NEVER returns `process.cwd()`:\n * when neither is an existing directory, returns `{ cwd: null, invalidReason }`\n * so the caller decides whether to fail (assignment launches) or fall back to\n * its own path (session launches keep `session.path`).\n */\nexport function resolveWorkspaceCwd(\n input: WorkspaceCwdInput,\n): WorkspaceCwdResult {\n const { worktreePath, repository, branch, assignmentSlug } = input;\n\n if (isExistingDir(worktreePath)) {\n return { cwd: worktreePath, fallbackWarning: null, invalidReason: null };\n }\n\n if (isExistingDir(repository)) {\n // A present-but-invalid worktreePath gets a dedicated warning; a missing\n // worktreePath reuses the standard missing-field warning so existing\n // behavior (and its tests) are preserved.\n const fallbackWarning = worktreePath\n ? `syntaur: workspace.worktreePath ${worktreePath} is not an existing directory for ${assignmentSlug} — launching in ${repository}`\n : formatFallbackCwdWarning({\n assignmentSlug,\n workspaceDir: repository as string,\n worktreePath,\n branch,\n });\n return { cwd: repository, fallbackWarning, invalidReason: null };\n }\n\n const shown = (p: string | null): string =>\n p && p.trim().length > 0 ? p : '(unset)';\n return {\n cwd: null,\n fallbackWarning: null,\n invalidReason:\n `workspace path invalid for ${assignmentSlug}: tried worktreePath ` +\n `${shown(worktreePath)} and repository ${shown(repository)} — ` +\n `neither is an existing directory`,\n };\n}\n\n/**\n * Build the one-line warning emitted when a launch falls back to a cwd because\n * the assignment is missing `workspace.worktreePath` and/or `workspace.branch`.\n * Returns null when both fields are populated (no warning needed).\n */\nexport function formatFallbackCwdWarning(opts: {\n assignmentSlug: string;\n workspaceDir: string;\n worktreePath: string | null;\n branch: string | null;\n}): string | null {\n const missing: string[] = [];\n if (!opts.worktreePath) missing.push('worktreePath');\n if (!opts.branch) missing.push('branch');\n if (missing.length === 0) return null;\n const fields = missing.map((m) => `workspace.${m}`).join(' and ');\n return `syntaur: ${fields} not set for ${opts.assignmentSlug} — launching in ${opts.workspaceDir}`;\n}\n","import { spawn } from 'node:child_process';\nimport { readFile } from 'node:fs/promises';\nimport { updateAssignmentWorkspace } from '../lifecycle/frontmatter.js';\nimport { writeFileForce } from './fs.js';\nimport { isExistingDir } from '../launch/cwd.js';\n\nexport interface CreateWorktreeOptions {\n repository: string;\n branch: string;\n worktreePath: string;\n parentBranch: string;\n}\n\nfunction run(\n command: string,\n args: string[],\n cwd?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n return new Promise((resolvePromise) => {\n const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });\n let stdout = '';\n let stderr = '';\n child.stdout.on('data', (chunk) => (stdout += chunk.toString()));\n child.stderr.on('data', (chunk) => (stderr += chunk.toString()));\n child.on('error', (err) => {\n resolvePromise({ code: -1, stdout, stderr: stderr + String(err) });\n });\n child.on('close', (code) => {\n resolvePromise({ code: code ?? -1, stdout, stderr });\n });\n });\n}\n\nexport class GitWorktreeError extends Error {\n constructor(message: string, public readonly stderr: string) {\n super(message);\n }\n}\n\n/**\n * Run `git -C <repository> worktree add -b <branch> <worktreePath> <parentBranch>`.\n * Throws GitWorktreeError on non-zero exit, preserving stderr.\n */\nexport async function createWorktree(opts: CreateWorktreeOptions): Promise<void> {\n const { repository, branch, worktreePath, parentBranch } = opts;\n const result = await run(\n 'git',\n ['-C', repository, 'worktree', 'add', '-b', branch, worktreePath, parentBranch],\n );\n if (result.code !== 0) {\n throw new GitWorktreeError(\n `git worktree add failed (exit ${result.code}): ${result.stderr.trim() || '(no stderr)'}`,\n result.stderr,\n );\n }\n}\n\nexport async function removeWorktree(\n repository: string,\n worktreePath: string,\n opts: { force?: boolean } = {},\n): Promise<{ ok: boolean; stderr: string }> {\n const args = ['-C', repository, 'worktree', 'remove'];\n // Without --force git refuses to remove a dirty/locked worktree (intentional).\n if (opts.force) args.push('--force');\n args.push(worktreePath);\n const result = await run('git', args);\n return { ok: result.code === 0, stderr: result.stderr };\n}\n\n/**\n * Best-effort `git worktree prune` — clears stale `.git/worktrees/<name>`\n * registrations for worktree directories that were deleted out-of-band, so the\n * same path/branch can be re-added without git rejecting it\n * (\"already used by worktree at ...\").\n */\nexport async function pruneWorktrees(repository: string): Promise<void> {\n await run('git', ['-C', repository, 'worktree', 'prune']);\n}\n\nexport interface WorktreeEntry {\n worktreePath: string;\n branch: string | null;\n head: string | null;\n bare: boolean;\n detached: boolean;\n}\n\n/**\n * Parse `git worktree list --porcelain` into structured entries (read-only).\n * Returns `[]` if git fails.\n */\nexport async function listWorktrees(repository: string): Promise<WorktreeEntry[]> {\n const result = await run('git', ['-C', repository, 'worktree', 'list', '--porcelain']);\n if (result.code !== 0) return [];\n const entries: WorktreeEntry[] = [];\n let current: Partial<WorktreeEntry> | null = null;\n const flush = () => {\n if (current && current.worktreePath) {\n entries.push({\n worktreePath: current.worktreePath,\n branch: current.branch ?? null,\n head: current.head ?? null,\n bare: current.bare ?? false,\n detached: current.detached ?? false,\n });\n }\n current = null;\n };\n for (const line of result.stdout.split('\\n')) {\n if (line.startsWith('worktree ')) {\n flush();\n current = { worktreePath: line.slice('worktree '.length).trim() };\n } else if (current) {\n if (line.startsWith('HEAD ')) current.head = line.slice('HEAD '.length).trim();\n else if (line.startsWith('branch ')) {\n current.branch = line.slice('branch '.length).trim().replace(/^refs\\/heads\\//, '');\n } else if (line.trim() === 'bare') current.bare = true;\n else if (line.trim() === 'detached') current.detached = true;\n }\n }\n flush();\n return entries;\n}\n\nexport async function deleteBranch(\n repository: string,\n branch: string,\n): Promise<{ ok: boolean; stderr: string }> {\n const result = await run('git', ['-C', repository, 'branch', '-D', branch]);\n return { ok: result.code === 0, stderr: result.stderr };\n}\n\n/**\n * List the local branch names of a repository (read-only).\n * Returns `[]` if git fails (e.g. a bare/empty repo with no branches yet).\n */\nexport async function listBranches(repository: string): Promise<string[]> {\n const result = await run('git', [\n '-C',\n repository,\n 'for-each-ref',\n '--format=%(refname:short)',\n 'refs/heads',\n ]);\n if (result.code !== 0) return [];\n return result.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n}\n\n/**\n * Best-effort detection of a repository's default branch (read-only):\n * 1. `origin/HEAD` symbolic ref (strip the `origin/` prefix), else\n * 2. `main` if it exists locally, else\n * 3. the current branch (ignoring detached HEAD), else\n * 4. the first local branch, else `null`.\n */\nexport async function detectDefaultBranch(repository: string): Promise<string | null> {\n const branches = await listBranches(repository);\n if (branches.length === 0) return null;\n\n // Every candidate below is checked against the local branch list so callers\n // can rely on the result being a real, checkout-able local branch (otherwise\n // `git worktree add ... <parent>` would later fail with \"does not exist\").\n const head = await run('git', [\n '-C',\n repository,\n 'symbolic-ref',\n '--quiet',\n '--short',\n 'refs/remotes/origin/HEAD',\n ]);\n if (head.code === 0) {\n const ref = head.stdout.trim().replace(/^origin\\//, '');\n if (ref && branches.includes(ref)) return ref;\n }\n\n if (branches.includes('main')) return 'main';\n\n const current = await run('git', ['-C', repository, 'rev-parse', '--abbrev-ref', 'HEAD']);\n if (current.code === 0) {\n const name = current.stdout.trim();\n if (name && name !== 'HEAD' && branches.includes(name)) return name;\n }\n\n return branches[0] ?? null;\n}\n\nexport interface RecreateWorktreeOptions {\n repository: string;\n /** The EXACT recorded path to rebuild at. */\n worktreePath: string;\n /** Branch on record, or null for a standalone session with no branch. */\n branch: string | null;\n /** Original HEAD sha captured at creation, for exact recreation. */\n originalHeadSha?: string | null;\n}\n\nexport interface RecreateWorktreeResult {\n /** Branch name or base ref actually used for the worktree add. */\n baseUsed: string;\n /** True when the original branch was restored, or the base === originalHeadSha. */\n exact: boolean;\n /** Resulting branch (null when the worktree was created detached). */\n branch: string | null;\n}\n\n/**\n * Capture the current HEAD sha of a worktree/repo directory (best-effort).\n * Returns the trimmed sha on success, or null if git fails (e.g. the dir is\n * not a git worktree, or git is unavailable). Never throws.\n */\nexport async function captureHeadSha(dir: string): Promise<string | null> {\n const result = await run('git', ['-C', dir, 'rev-parse', 'HEAD']);\n if (result.code !== 0) return null;\n const sha = result.stdout.trim();\n return sha.length > 0 ? sha : null;\n}\n\n/**\n * Resolve a branch's tip to its short SHA in `repository` (read-only). Used to\n * print a recovery hint (`git branch <name> <sha>`) before a branch is deleted.\n * Returns null if the branch can't be resolved.\n */\nexport async function resolveBranchSha(\n repository: string,\n branch: string,\n): Promise<string | null> {\n const result = await run('git', [\n '-C',\n repository,\n 'rev-parse',\n '--short',\n branch,\n ]);\n if (result.code !== 0) return null;\n const sha = result.stdout.trim();\n return sha.length > 0 ? sha : null;\n}\n\n/**\n * True when `branch` is fully merged into `base` (i.e. `base` already contains\n * every commit of `branch`). Uses `git merge-base --is-ancestor` (read-only).\n * Caller must pass a non-null branch.\n */\nexport async function isBranchMerged(\n repository: string,\n branch: string,\n base: string,\n): Promise<boolean> {\n const result = await run('git', [\n '-C',\n repository,\n 'merge-base',\n '--is-ancestor',\n branch,\n base,\n ]);\n return result.code === 0;\n}\n\n/**\n * True when the worktree at `worktreePath` has uncommitted changes (or git can't\n * be queried). Treats an unknown/failed status as dirty so callers stay safe.\n */\nexport async function isWorktreeDirty(worktreePath: string): Promise<boolean> {\n const result = await run('git', ['-C', worktreePath, 'status', '--porcelain']);\n if (result.code !== 0) return true; // unknown -> treat as dirty\n return result.stdout.trim().length > 0;\n}\n\n/**\n * Resolve the top-level directory of the git worktree containing `cwd`\n * (read-only). Returns null if `cwd` is not inside a git repo.\n */\nexport async function repoTopLevel(cwd: string): Promise<string | null> {\n const result = await run('git', ['-C', cwd, 'rev-parse', '--show-toplevel']);\n if (result.code !== 0) return null;\n const top = result.stdout.trim();\n return top.length > 0 ? top : null;\n}\n\n/**\n * Return the worktree path where `branch` is currently checked out, or null if\n * it isn't checked out by any worktree. Parses `git worktree list --porcelain`\n * (blocks of `worktree <path>` / `branch refs/heads/<name>`). Used to decide\n * whether a branch can be re-attached or must be recreated detached.\n */\nasync function branchCheckedOutAt(\n repository: string,\n branch: string,\n): Promise<string | null> {\n const result = await run('git', ['-C', repository, 'worktree', 'list', '--porcelain']);\n if (result.code !== 0) return null;\n let currentPath: string | null = null;\n for (const line of result.stdout.split('\\n')) {\n if (line.startsWith('worktree ')) {\n currentPath = line.slice('worktree '.length).trim();\n } else if (line.startsWith('branch ')) {\n const ref = line.slice('branch '.length).trim();\n if (ref === `refs/heads/${branch}` && currentPath) return currentPath;\n }\n }\n return null;\n}\n\n/**\n * Recreate a worktree at an EXACT recorded path after its directory was deleted\n * (e.g. manually `rm -rf`'d). A manual delete leaves stale\n * `.git/worktrees/<name>/` metadata with the branch still marked checked-out,\n * so `git worktree prune` runs first, then:\n * - branch still exists -> `git worktree add <path> <branch>` (exact: branch restored)\n * - branch was deleted -> `git worktree add -b <branch> <path> <base>` where <base> is, in order:\n * originalHeadSha (if it resolves), refs/remotes/origin/<branch>,\n * or detectDefaultBranch() (a LOCAL branch — never `origin/*`)\n * - no branch on record -> `git worktree add --detach <path> <base>` (a dir at the path is the\n * sufficient condition for `claude --resume <id>`)\n * Throws GitWorktreeError when the `git worktree add` fails or no base ref is available.\n */\nexport async function recreateWorktree(\n opts: RecreateWorktreeOptions,\n): Promise<RecreateWorktreeResult> {\n const { repository, worktreePath, branch } = opts;\n const originalHeadSha = opts.originalHeadSha ?? null;\n\n // Clear stale metadata for the deleted directory so a subsequent add at the\n // same path / branch is not rejected (\"already used by worktree at ...\"). Best\n // effort — any real failure surfaces from the add step below.\n await pruneWorktrees(repository);\n\n const add = async (args: string[]): Promise<void> => {\n const result = await run('git', ['-C', repository, 'worktree', 'add', ...args]);\n if (result.code !== 0) {\n throw new GitWorktreeError(\n `git worktree add failed (exit ${result.code}): ${result.stderr.trim() || '(no stderr)'}`,\n result.stderr,\n );\n }\n };\n\n const refExists = async (ref: string): Promise<boolean> => {\n const result = await run('git', [\n '-C',\n repository,\n 'rev-parse',\n '--verify',\n '--quiet',\n ref,\n ]);\n return result.code === 0;\n };\n\n // 1. Branch still exists — re-attach a worktree to it at the recorded path.\n if (branch && (await listBranches(repository)).includes(branch)) {\n const checkedOutAt = await branchCheckedOutAt(repository, branch);\n if (\n !checkedOutAt ||\n checkedOutAt === worktreePath ||\n !isExistingDir(checkedOutAt)\n ) {\n // Free to attach (post-prune the deleted worktree's claim is gone).\n await add([worktreePath, branch]);\n return { baseUsed: branch, exact: true, branch };\n }\n // The branch is checked out in another LIVE worktree, so it can't be\n // attached here. A directory at the exact path is the sufficient condition\n // for `claude --resume`, so recreate detached at the original sha (exact)\n // or the branch tip.\n const detachBase =\n originalHeadSha && (await refExists(`${originalHeadSha}^{commit}`))\n ? originalHeadSha\n : `refs/heads/${branch}`;\n await add(['--detach', worktreePath, detachBase]);\n return { baseUsed: detachBase, exact: detachBase === originalHeadSha, branch: null };\n }\n\n // 2/3. Branch missing (deleted) or never recorded — choose a base ref.\n let baseUsed: string | null = null;\n if (originalHeadSha && (await refExists(`${originalHeadSha}^{commit}`))) {\n baseUsed = originalHeadSha;\n } else if (branch && (await refExists(`refs/remotes/origin/${branch}`))) {\n baseUsed = `refs/remotes/origin/${branch}`;\n } else {\n baseUsed = await detectDefaultBranch(repository);\n }\n if (!baseUsed) {\n throw new GitWorktreeError(\n `recreateWorktree: no base ref to recreate ${worktreePath} ` +\n `(no original sha, no origin/${branch ?? '<none>'}, no default branch)`,\n '',\n );\n }\n\n const exact = baseUsed === originalHeadSha;\n if (branch) {\n // 2. Recreate the deleted branch at the base ref.\n await add(['-b', branch, worktreePath, baseUsed]);\n return { baseUsed, exact, branch };\n }\n // 3. No branch on record — a detached worktree is enough for `--resume`.\n await add(['--detach', worktreePath, baseUsed]);\n return { baseUsed, exact, branch: null };\n}\n\nexport interface CreateWorktreeAndRecordOptions extends CreateWorktreeOptions {\n assignmentPath: string;\n}\n\n/**\n * Transactional helper:\n * 1. `git worktree add` — on failure throws, nothing else touched.\n * 2. Read assignment.md, update `workspace.*` fields, write back via writeFileForce.\n * 3. If (2) fails, `git worktree remove --force` to undo step 1. If cleanup fails,\n * throw an error naming both the file-write error AND the orphan worktree path.\n */\nexport async function createWorktreeAndRecord(\n opts: CreateWorktreeAndRecordOptions,\n): Promise<void> {\n const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;\n\n await createWorktree({ repository, branch, worktreePath, parentBranch });\n\n try {\n const content = await readFile(assignmentPath, 'utf-8');\n const updated = updateAssignmentWorkspace(content, {\n repository,\n worktreePath,\n branch,\n parentBranch,\n });\n await writeFileForce(assignmentPath, updated);\n } catch (writeErr) {\n const cleanup = await removeWorktree(repository, worktreePath, { force: true });\n // Always try to delete the branch created by -b, even if worktree removal already failed.\n const branchCleanup = await deleteBranch(repository, branch);\n const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);\n throw new Error(\n formatRollbackError({\n writeMsg,\n worktreePath,\n branch,\n worktreeCleanup: cleanup,\n branchCleanup,\n }),\n );\n }\n}\n\nexport function formatRollbackError(opts: {\n writeMsg: string;\n worktreePath: string;\n branch: string;\n worktreeCleanup: { ok: boolean; stderr: string };\n branchCleanup: { ok: boolean; stderr: string };\n subject?: string;\n}): string {\n const { writeMsg, worktreePath, branch, worktreeCleanup, branchCleanup } = opts;\n const subject = opts.subject ?? 'assignment frontmatter';\n const wtMsg = worktreeCleanup.stderr.trim() || '(no stderr)';\n const brMsg = branchCleanup.stderr.trim() || '(no stderr)';\n if (!worktreeCleanup.ok && !branchCleanup.ok) {\n return (\n `Failed to update ${subject} AND failed to clean up both worktree and branch. ` +\n `Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Branch cleanup error: ${brMsg}. ` +\n `Orphan worktree at ${worktreePath} and orphan branch \"${branch}\" — remove them manually.`\n );\n }\n if (!worktreeCleanup.ok) {\n return (\n `Failed to update ${subject} AND failed to clean up worktree. ` +\n `Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. ` +\n `Orphan worktree at ${worktreePath} — remove it manually.`\n );\n }\n if (!branchCleanup.ok) {\n return (\n `Failed to update ${subject}: ${writeMsg}. Rolled back git worktree at ${worktreePath}, ` +\n `but could not delete branch \"${branch}\": ${brMsg}. ` +\n `Remove the branch manually.`\n );\n }\n return `Failed to update ${subject}: ${writeMsg}. Rolled back git worktree at ${worktreePath} and branch \"${branch}\".`;\n}\n\nexport interface CreateWorktreeForBundleOptions extends CreateWorktreeOptions {\n record: () => Promise<void>;\n}\n\n/**\n * Bundle-scoped sibling of createWorktreeAndRecord. Creates the worktree,\n * then runs the caller-supplied record() callback (which writes bundle\n * storage + checklist + .syntaur/context.json). On record() failure, rolls\n * back the worktree and branch and throws a formatted error tagged\n * `subject: 'bundle storage'` so users see a bundle-specific message.\n */\nexport async function createWorktreeForBundle(\n opts: CreateWorktreeForBundleOptions,\n): Promise<void> {\n const { repository, branch, worktreePath, parentBranch, record } = opts;\n await createWorktree({ repository, branch, worktreePath, parentBranch });\n try {\n await record();\n } catch (writeErr) {\n const cleanup = await removeWorktree(repository, worktreePath, { force: true });\n const branchCleanup = await deleteBranch(repository, branch);\n const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);\n throw new Error(\n formatRollbackError({\n writeMsg,\n worktreePath,\n branch,\n worktreeCleanup: cleanup,\n branchCleanup,\n subject: 'bundle storage',\n }),\n );\n }\n}\n","/**\n * Fact computation (derived-status design v3, Piece 1) — Node-side.\n *\n * Materializes an assignment's objective facts from the files already on disk\n * (assignment.md body, sibling plan files, comments.md) plus the asserted\n * frontmatter facts. The browser never runs this — the dashboard ships the\n * result in payloads (loader-derived, NOT stored), mirroring the\n * `deriveStatusVirtuals` pattern.\n */\n\nimport { createHash } from 'node:crypto';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { fileExists } from '../utils/fs.js';\nimport { captureHeadSha } from '../utils/git-worktree.js';\nimport { type AssignmentFacts, factFieldNames } from './derive.js';\nimport type { AssignmentFrontmatter, AttestationRecord } from './types.js';\nimport type { FactDeclaration } from '../utils/config.js';\n\n/** Matches the assignment template's placeholder list items / comments. */\nconst HTML_COMMENT_RE = /<!--[\\s\\S]*?-->/g;\n\n/** Extract the body of a `## <heading>` section (up to the next `## `). */\nfunction sectionBody(body: string, heading: string): string | null {\n const re = new RegExp(`^##\\\\s+${heading}\\\\s*$`, 'm');\n const m = body.match(re);\n if (!m || m.index === undefined) return null;\n const start = m.index + m[0].length;\n const rest = body.slice(start);\n const next = rest.search(/^##\\s+/m);\n return next >= 0 ? rest.slice(0, next) : rest;\n}\n\n/** Objective filled with real content (template placeholder comments stripped). */\nexport function hasRealObjective(body: string): boolean {\n const section = sectionBody(body, 'Objective');\n if (section === null) return false;\n return section.replace(HTML_COMMENT_RE, '').trim().length > 0;\n}\n\n/**\n * Count non-placeholder acceptance criteria. The template seeds\n * `- [ ] <!-- criterion N -->` rows — those don't count (a naive `acTotal > 0`\n * would promote every fresh draft; codex design-review finding).\n */\nexport function countRealAcceptanceCriteria(body: string): { total: number; checked: number } {\n const section = sectionBody(body, 'Acceptance Criteria');\n if (section === null) return { total: 0, checked: 0 };\n let total = 0;\n let checked = 0;\n for (const line of section.split('\\n')) {\n const m = line.match(/^\\s*-\\s*\\[([ xX])\\]\\s*(.*)$/);\n if (!m) continue;\n const content = m[2].replace(HTML_COMMENT_RE, '').trim();\n if (content.length === 0) continue; // placeholder or empty\n total++;\n if (m[1].toLowerCase() === 'x') checked++;\n }\n return { total, checked };\n}\n\nconst PLAN_FILE_RE = /^plan(?:-v(\\d+))?\\.md$/;\n\n/** Latest plan revision in an assignment dir (`plan.md` = v1 < `plan-v2.md` < …). */\nexport async function latestPlanFile(assignmentDir: string): Promise<string | null> {\n let entries: string[];\n try {\n entries = await readdir(assignmentDir);\n } catch {\n return null;\n }\n let best: { name: string; version: number } | null = null;\n for (const name of entries) {\n const m = name.match(PLAN_FILE_RE);\n if (!m) continue;\n const version = m[1] ? parseInt(m[1], 10) : 1;\n if (!best || version > best.version) best = { name, version };\n }\n return best?.name ?? null;\n}\n\nexport function planDigest(content: string): string {\n return createHash('sha256').update(content, 'utf-8').digest('hex');\n}\n\n/**\n * Revision-bound approval check: the `planApproval` record must name the\n * CURRENT latest plan file AND its digest must match that file's current\n * content. A replan (new plan-vN) or a post-approval edit auto-invalidates.\n */\nexport async function isPlanApproved(\n assignmentDir: string,\n frontmatter: Pick<AssignmentFrontmatter, 'planApproval'>,\n): Promise<boolean> {\n const approval = frontmatter.planApproval;\n if (!approval) return false;\n const latest = await latestPlanFile(assignmentDir);\n if (!latest || latest !== approval.file) return false;\n try {\n const content = await readFile(resolve(assignmentDir, latest), 'utf-8');\n return planDigest(content) === approval.digest;\n } catch {\n return false;\n }\n}\n\n/** Count open (unresolved) question comments in comments.md. Parity with the\n * dashboard's countOpenQuestions, kept dependency-light for the lifecycle layer. */\nexport async function countUnresolvedQuestions(assignmentDir: string): Promise<number> {\n const commentsPath = resolve(assignmentDir, 'comments.md');\n if (!(await fileExists(commentsPath))) return 0;\n try {\n const content = await readFile(commentsPath, 'utf-8');\n // Each entry: \"## <id>\" block with \"**Type:** question\" and \"**Resolved:** false\"\n let count = 0;\n for (const block of content.split(/^##\\s+/m).slice(1)) {\n if (/^\\*\\*Type:\\*\\*\\s*question\\s*$/m.test(block) && /^\\*\\*Resolved:\\*\\*\\s*false\\s*$/m.test(block)) {\n count++;\n }\n }\n return count;\n } catch {\n return 0;\n }\n}\n\n/** All `dependsOn` targets terminal? Standalone assignments (no project dir)\n * and empty dependency lists are trivially satisfied. */\nexport async function areDependenciesSatisfied(\n projectDir: string | null,\n dependsOn: string[],\n terminalStatuses: ReadonlySet<string>,\n): Promise<boolean> {\n if (dependsOn.length === 0 || projectDir === null) return true;\n for (const depSlug of dependsOn) {\n const depPath = resolve(projectDir, 'assignments', depSlug, 'assignment.md');\n if (!(await fileExists(depPath))) return false;\n try {\n const content = await readFile(depPath, 'utf-8');\n const m = content.match(/^status:\\s*(.+)$/m);\n const status = m ? m[1].trim() : '';\n if (!terminalStatuses.has(status)) return false;\n } catch {\n return false;\n }\n }\n return true;\n}\n\nexport interface ComputeFactsInput {\n assignmentDir: string;\n frontmatter: AssignmentFrontmatter;\n body: string;\n /** Project dir for dependency checks; null for standalone assignments. */\n projectDir: string | null;\n terminalStatuses: ReadonlySet<string>;\n /** The ACCEPTED custom-fact declarations (normalize→accept output). Absent →\n * only the 14 built-ins materialize. */\n declarations?: FactDeclaration[];\n}\n\n/**\n * Canonical fact-value coercion (Locked Decisions — used by BOTH facts.ts and\n * the CLI). bool → case-insensitive `true`/`false` only; number → trimmed,\n * `Number(value)` finite (rejects NaN/Infinity/empty). Returns the canonical\n * stored form (`'true'`/`'false'` or `String(n)`) or null if invalid. The CLI\n * rejects null with the declared type; computeFacts treats null as absent.\n */\nexport function canonicalizeFactValue(type: 'bool' | 'number', raw: string): string | null {\n const t = raw.trim();\n if (type === 'bool') {\n const low = t.toLowerCase();\n if (low === 'true') return 'true';\n if (low === 'false') return 'false';\n return null;\n }\n if (t === '') return null;\n const n = Number(t);\n return Number.isFinite(n) ? String(n) : null;\n}\n\n/** Read a stored bool fact, degrading absent/invalid to false (never throws). */\nfunction readBoolFact(raw: string | undefined): boolean {\n if (typeof raw !== 'string') return false;\n return canonicalizeFactValue('bool', raw) === 'true';\n}\n\n/** Read a stored number fact, degrading absent/invalid to 0 (never throws). */\nfunction readNumberFact(raw: string | undefined): number {\n if (typeof raw !== 'string') return 0;\n const c = canonicalizeFactValue('number', raw);\n return c === null ? 0 : Number(c);\n}\n\n/** Per-attestation-fact validity detail (one record list per declared fact). */\nexport interface AttestationDetail {\n fact: string;\n binds: 'plan' | 'commit' | 'none';\n records: Array<{ record: AttestationRecord; valid: boolean }>;\n}\n\nexport interface ComputeFactsResult {\n facts: AssignmentFacts;\n attestations: AttestationDetail[];\n}\n\n/** Resolved-once binding environment for attestation validity. */\ninterface AttestationEnv {\n latestPlanFile: string | null;\n /** Digest of the latest plan file's CURRENT content (null when no plan). */\n planDigest: string | null;\n /** Workspace HEAD sha (null when no workspace / not a git dir). */\n headSha: string | null;\n}\n\nfunction isAttestationValid(\n record: AttestationRecord,\n binds: 'plan' | 'commit' | 'none',\n env: AttestationEnv,\n): boolean {\n if (binds === 'none') return true;\n if (binds === 'plan') {\n if (!record.file || !env.latestPlanFile || record.file !== env.latestPlanFile) return false;\n if (!record.digest || !env.planDigest) return false;\n return record.digest === env.planDigest;\n }\n // binds:commit\n if (!record.commit || !env.headSha) return false;\n return record.commit === env.headSha;\n}\n\n/**\n * Materialize the full fact set PLUS per-attestation validity in ONE pass —\n * the dashboard (Task 9) calls this once per detail request so facts and\n * record-level staleness come from the same plan-file / HEAD reads. `computeFacts`\n * is a thin delegate returning just `.facts`.\n */\nexport async function computeFactsDetailed(input: ComputeFactsInput): Promise<ComputeFactsResult> {\n const { assignmentDir, frontmatter, body, projectDir, terminalStatuses } = input;\n const declarations = input.declarations ?? [];\n\n const ac = countRealAcceptanceCriteria(body);\n // Resolve the plan environment ONCE — a single read of the latest plan file's\n // content drives BOTH the built-in `planApproved` fact AND binds:plan\n // attestation validity, so a concurrent replan can't make the two disagree\n // and the plan is read at most once. Read only when something needs the digest.\n const needsPlanDigest =\n frontmatter.planApproval !== null ||\n declarations.some((d) => d.type === 'attestation' && d.binds === 'plan');\n const planFile = await latestPlanFile(assignmentDir);\n const [planFileContent, unresolvedQuestions, depsSatisfied] = await Promise.all([\n needsPlanDigest && planFile\n ? readFile(resolve(assignmentDir, planFile), 'utf-8').catch(() => null)\n : Promise.resolve(null),\n countUnresolvedQuestions(assignmentDir),\n areDependenciesSatisfied(projectDir, frontmatter.dependsOn, terminalStatuses),\n ]);\n const planFileDigest = planFileContent !== null ? planDigest(planFileContent) : null;\n const approval = frontmatter.planApproval;\n const planApproved =\n approval !== null &&\n approval.file === planFile &&\n planFileDigest !== null &&\n approval.digest === planFileDigest;\n\n const facts: AssignmentFacts = {\n hasRealObjective: hasRealObjective(body),\n acRealTotal: ac.total,\n acRealChecked: ac.checked,\n acAllChecked: ac.total > 0 && ac.checked === ac.total,\n planExists: planFile !== null,\n planApproved,\n workspaceSet: frontmatter.workspace.repository !== null && frontmatter.workspace.branch !== null,\n implementationStarted: frontmatter.implementationStarted,\n depsSatisfied,\n unresolvedQuestions,\n blocked: frontmatter.blockedReason !== null,\n parked: frontmatter.parked,\n reviewRequested: frontmatter.reviewRequested,\n pinned: frontmatter.override !== null,\n };\n\n const attestations: AttestationDetail[] = [];\n if (declarations.length > 0) {\n const storedFacts = frontmatter.facts ?? {};\n const records = frontmatter.attestations ?? [];\n\n // Custom bool/number facts (absent/invalid stored values degrade, no throw).\n for (const decl of declarations) {\n if (decl.type === 'bool') facts[decl.name] = readBoolFact(storedFacts[decl.name]);\n else if (decl.type === 'number') facts[decl.name] = readNumberFact(storedFacts[decl.name]);\n }\n\n // Attestation facts — resolve the binding env ONCE, then evaluate each.\n const attestationDecls = declarations.filter(\n (d): d is Extract<FactDeclaration, { type: 'attestation' }> => d.type === 'attestation',\n );\n if (attestationDecls.length > 0) {\n const needsCommit = attestationDecls.some((d) => d.binds === 'commit');\n let headSha: string | null = null;\n if (needsCommit) {\n const dir = frontmatter.workspace.worktreePath ?? frontmatter.workspace.repository;\n headSha = dir ? await captureHeadSha(dir) : null;\n }\n // planFile + planFileDigest were resolved once above (shared with the\n // built-in planApproved fact) — no second read, one consistent snapshot.\n const env: AttestationEnv = { latestPlanFile: planFile, planDigest: planFileDigest, headSha };\n\n for (const decl of attestationDecls) {\n const detailRecords = records\n .filter((r) => r.fact === decl.name)\n .map((record) => ({ record, valid: isAttestationValid(record, decl.binds, env) }));\n attestations.push({ fact: decl.name, binds: decl.binds, records: detailRecords });\n\n const valid = detailRecords.filter((r) => r.valid).map((r) => r.record);\n const validApproved = valid.filter((r) => r.verdict === 'approved');\n const validChanges = valid.filter((r) => r.verdict === 'changes-requested');\n const names = factFieldNames(decl);\n facts[names.exports.fact] = valid.length > 0;\n facts[names.exports.approved] = validApproved.length > 0;\n facts[names.exports.changesRequested] = validChanges.length > 0;\n facts[names.exports.by] = valid.map((r) => r.actor);\n facts[names.exports.approvedBy] = validApproved.map((r) => r.actor);\n }\n }\n }\n\n return { facts, attestations };\n}\n\n/** Materialize the full fact set for one assignment (thin delegate). */\nexport async function computeFacts(input: ComputeFactsInput): Promise<AssignmentFacts> {\n return (await computeFactsDetailed(input)).facts;\n}\n","/**\n * Shared search types — the contract between the indexer, the `SearchProvider`\n * implementations (Fuse default, Semantic stub seam), and both consumers (the\n * `syntaur search` CLI and the dashboard content-search router/palette).\n *\n * The provider returns a NEUTRAL snippet (no highlight markers) plus\n * `matches: MatchRange[]` (snippet-local char offsets) so each caller formats\n * highlighting itself: the CLI wraps with `**…**`, the API/palette wrap with\n * HTML-safe `<mark>`.\n *\n * Aligns with `EntityKind`/scope semantics in `src/utils/search-schema.ts`\n * (that module covers entity-record search; this one covers markdown bodies).\n */\n\n/** The markdown content kinds indexed for full-text body search. */\nexport type FileKind =\n | 'assignment'\n | 'plan'\n | 'progress'\n | 'comments'\n | 'handoff'\n | 'decision-record'\n | 'scratchpad'\n | 'memory'\n | 'resource';\n\nexport const FILE_KINDS: readonly FileKind[] = [\n 'assignment',\n 'plan',\n 'progress',\n 'comments',\n 'handoff',\n 'decision-record',\n 'scratchpad',\n 'memory',\n 'resource',\n];\n\n/**\n * One indexed markdown document. Carries the body to search plus the\n * filter+route identity propagated from the owning assignment / project\n * frontmatter so the provider can pre-filter (`type`/`status`/`project`/`in`)\n * and the route builder can produce a deep-link without re-reading anything.\n */\nexport interface SearchDoc {\n /** Stable id — the absolute file path doubles as the id. */\n id: string;\n /** Absolute file path on disk. */\n path: string;\n fileKind: FileKind;\n /** Human title (assignment/project/memory/resource title), used as a Fuse key. */\n title: string;\n /** The markdown body to full-text search. */\n body: string;\n /** Nearest-heading section, when the indexer can cheaply attribute one. */\n section?: string;\n\n // ── filter + route identity (carried from frontmatter) ──────────────────\n /** Owning project slug; `null` for standalone assignments. */\n projectSlug: string | null;\n /**\n * The owning project's `workspace` field (from project.md) — drives the\n * `/w/<ws>` route prefix the palette applies. `null` for standalone.\n */\n projectWorkspace: string | null;\n /** Owning assignment slug; `null` for memory/resource docs. */\n assignmentSlug: string | null;\n /** Owning assignment id (uuid); `null` for memory/resource docs. */\n assignmentId: string | null;\n /** True when the owning assignment is standalone (no containing project). */\n standalone: boolean;\n /** Memory/resource file slug (filename without `.md`); absent otherwise. */\n itemSlug?: string;\n /** Owning assignment `type` (for `--type` filtering); absent for memory/resource. */\n type?: string;\n /** Owning assignment `status` (for `--status` filtering); absent for memory/resource. */\n status?: string;\n /** Archived flag (from assignment or project frontmatter). */\n archived: boolean;\n}\n\n/** A match range in NEUTRAL char offsets into `SearchHit.snippet`. */\nexport interface MatchRange {\n start: number;\n end: number;\n}\n\n/**\n * One ranked search result. `snippet` is NEUTRAL text (no markers); callers\n * apply `matches` to highlight. `route` is the precomputed UNPREFIXED deep-link\n * (the palette prepends the per-hit `/w/<workspace>` prefix).\n */\nexport interface SearchHit {\n path: string;\n projectSlug: string | null;\n projectWorkspace: string | null;\n assignmentSlug: string | null;\n assignmentId: string | null;\n standalone: boolean;\n itemSlug?: string;\n fileKind: FileKind;\n title: string;\n score: number;\n snippet: string;\n matches: MatchRange[];\n /** 1-based line number of the match in the source body. */\n line: number;\n section?: string;\n /** Precomputed unprefixed app route (see `routeForHit`). */\n route: string;\n}\n\n/** A search request. `in` is the canonical-resolved file-kind filter. */\nexport interface SearchQuery {\n query: string;\n project?: string;\n type?: string[];\n status?: string[];\n in?: FileKind[];\n}\n\n/**\n * The provider seam. `index` ingests the docs; `query` runs a ranked search.\n * `FuseProvider` is the default; `SemanticProvider` is a stub for the future\n * embeddings slot.\n */\nexport interface SearchProvider {\n index(docs: SearchDoc[]): void | Promise<void>;\n query(q: SearchQuery, limit: number): SearchHit[] | Promise<SearchHit[]>;\n}\n\n/**\n * `--in` alias map — both singular and plural/common forms resolve to the\n * canonical `FileKind`. Resolves the `--in comments,plans` mismatch (`plans` →\n * `plan`).\n */\nexport const FILE_KIND_ALIASES: Record<string, FileKind> = {\n assignment: 'assignment',\n assignments: 'assignment',\n plan: 'plan',\n plans: 'plan',\n progress: 'progress',\n comment: 'comments',\n comments: 'comments',\n handoff: 'handoff',\n handoffs: 'handoff',\n decision: 'decision-record',\n decisions: 'decision-record',\n 'decision-record': 'decision-record',\n 'decision-records': 'decision-record',\n scratchpad: 'scratchpad',\n scratchpads: 'scratchpad',\n memory: 'memory',\n memories: 'memory',\n resource: 'resource',\n resources: 'resource',\n};\n\n/**\n * Parse a comma-separated `--in` list into canonical `FileKind[]`. Splits,\n * trims, lowercases, and resolves via {@link FILE_KIND_ALIASES}. Throws on an\n * unknown kind with a message listing the valid kinds. Empty/blank entries are\n * dropped; a fully-empty input returns `[]`.\n */\nexport function parseFileKinds(csv: string): FileKind[] {\n const out: FileKind[] = [];\n for (const raw of csv.split(',')) {\n const token = raw.trim().toLowerCase();\n if (token.length === 0) continue;\n const canonical = FILE_KIND_ALIASES[token];\n if (!canonical) {\n const valid = Array.from(new Set(Object.keys(FILE_KIND_ALIASES))).join(', ');\n throw new Error(`Unknown file kind \"${token}\". Valid kinds: ${valid}`);\n }\n if (!out.includes(canonical)) out.push(canonical);\n }\n return out;\n}\n","/**\n * Deep-link route helper for search hits. Produces UNPREFIXED app paths (the\n * dashboard palette prepends the per-hit `/w/<workspace>` prefix for nested\n * assignment-pane hits). Also exports the shared `slugifyHeading` used both here\n * (for the `#section` anchor) and by the dashboard `MarkdownRenderer` heading\n * ids, so the route hash always matches a real element id.\n */\n\nimport type { FileKind, SearchHit } from './types.js';\n\n/**\n * Content kind → the `AssignmentDetail` `?tab=` pane that renders it. Memory and\n * resource have no assignment pane; they route to their own pages.\n */\nexport const FILE_KIND_TO_TAB: Record<FileKind, string> = {\n assignment: 'summary',\n plan: 'plan',\n scratchpad: 'scratchpad',\n handoff: 'handoff',\n progress: 'progress',\n comments: 'comments',\n 'decision-record': 'decisions',\n // memory/resource never use a tab — routeForHit short-circuits them.\n memory: 'summary',\n resource: 'summary',\n};\n\n/**\n * GitHub-style heading slug — lowercase, strip non-word chars, spaces → `-`.\n * Shared with the dashboard `MarkdownRenderer` heading ids so `#<slug>` anchors\n * resolve.\n */\nexport function slugifyHeading(text: string): string {\n return text\n .trim()\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * File kinds whose dashboard pane renders its WHOLE body through\n * `MarkdownRenderer` and so gets heading `id`s a `#<slug(section)>` anchor can\n * resolve against. Excluded kinds, and why a hash there would dangle:\n * - `comments` / `progress` — render structured components (CommentsThread /\n * progress `<li>` rows), NOT markdown headings.\n * - `assignment` — the `summary` pane transforms `## Acceptance Criteria` /\n * `## Todos` into `SectionCard`s WITHOUT ids (AssignmentDetail.tsx), so its\n * headings never become element ids.\n * These all get the `?tab=` pane WITHOUT a hash.\n */\nconst ANCHORABLE_KINDS: ReadonlySet<FileKind> = new Set<FileKind>([\n 'plan',\n 'scratchpad',\n 'handoff',\n 'decision-record',\n]);\n\n/**\n * Build the UNPREFIXED deep-link for a hit:\n * - memory → `/projects/<projectSlug>/memories/<itemSlug>`\n * - resource → `/projects/<projectSlug>/resources/<itemSlug>`\n * - assignment-scoped kinds → `<base>?tab=<pane>` + optional `#<slug(section)>`,\n * where base is `/assignments/<id>` (standalone) or\n * `/projects/<projectSlug>/assignments/<assignmentSlug>` (nested).\n */\nexport function routeForHit(\n hit: Pick<\n SearchHit,\n | 'fileKind'\n | 'projectSlug'\n | 'assignmentSlug'\n | 'assignmentId'\n | 'standalone'\n | 'itemSlug'\n | 'section'\n >,\n): string {\n if (hit.fileKind === 'memory') {\n return `/projects/${hit.projectSlug}/memories/${hit.itemSlug}`;\n }\n if (hit.fileKind === 'resource') {\n return `/projects/${hit.projectSlug}/resources/${hit.itemSlug}`;\n }\n\n const base = hit.standalone\n ? `/assignments/${hit.assignmentId}`\n : `/projects/${hit.projectSlug}/assignments/${hit.assignmentSlug}`;\n\n const tab = FILE_KIND_TO_TAB[hit.fileKind];\n let route = `${base}?tab=${tab}`;\n if (hit.section && ANCHORABLE_KINDS.has(hit.fileKind)) {\n route += `#${slugifyHeading(hit.section)}`;\n }\n return route;\n}\n","import { resolve } from 'node:path';\nimport { readdir } from 'node:fs/promises';\nimport { fileExists } from './fs.js';\n\nexport interface AssignmentEntry {\n projectDir: string;\n /** `null` for standalone assignments (no containing project). */\n projectSlug: string | null;\n assignmentDir: string;\n /** For standalone, this is the UUID folder name. */\n assignmentSlug: string;\n standalone: boolean;\n}\n\nexport interface AssignmentWalkResult {\n withAssignmentMd: AssignmentEntry[];\n orphanFolders: AssignmentEntry[];\n}\n\nexport async function listAssignmentsByProject(\n projectsDir: string,\n standaloneDir: string | null,\n): Promise<AssignmentWalkResult> {\n const result: AssignmentWalkResult = {\n withAssignmentMd: [],\n orphanFolders: [],\n };\n\n if (await fileExists(projectsDir)) {\n const projects = await readdir(projectsDir, { withFileTypes: true });\n for (const m of projects) {\n if (!m.isDirectory()) continue;\n if (m.name.startsWith('.') || m.name.startsWith('_')) continue;\n const assignmentsDir = resolve(projectsDir, m.name, 'assignments');\n if (!(await fileExists(assignmentsDir))) continue;\n\n const entries = await readdir(assignmentsDir, { withFileTypes: true });\n for (const a of entries) {\n if (!a.isDirectory()) continue;\n if (a.name.startsWith('.') || a.name.startsWith('_')) continue;\n const assignmentDir = resolve(assignmentsDir, a.name);\n const assignmentMd = resolve(assignmentDir, 'assignment.md');\n const entry: AssignmentEntry = {\n projectDir: resolve(projectsDir, m.name),\n projectSlug: m.name,\n assignmentDir,\n assignmentSlug: a.name,\n standalone: false,\n };\n if (await fileExists(assignmentMd)) {\n result.withAssignmentMd.push(entry);\n } else {\n result.orphanFolders.push(entry);\n }\n }\n }\n }\n\n if (standaloneDir !== null && (await fileExists(standaloneDir))) {\n const entries = await readdir(standaloneDir, { withFileTypes: true });\n for (const a of entries) {\n if (!a.isDirectory()) continue;\n if (a.name.startsWith('.') || a.name.startsWith('_')) continue;\n const assignmentDir = resolve(standaloneDir, a.name);\n const assignmentMd = resolve(assignmentDir, 'assignment.md');\n const entry: AssignmentEntry = {\n projectDir: standaloneDir,\n projectSlug: null,\n assignmentDir,\n assignmentSlug: a.name,\n standalone: true,\n };\n if (await fileExists(assignmentMd)) {\n result.withAssignmentMd.push(entry);\n } else {\n result.orphanFolders.push(entry);\n }\n }\n }\n\n return result;\n}\n","/**\n * Content indexer — walks all Syntaur markdown content, reads bodies via the\n * canonical parsers (`src/dashboard/parser.ts`), and emits `SearchDoc[]`.\n *\n * The content dirs are PARAMETERS, never hardcoded `defaultProjectDir()` — the\n * dashboard server and the CLI may use different configured dirs, so hardcoding\n * a default would index a different tree than is displayed (audit finding #8).\n *\n * A module-level cache keyed by `projectsDir|assignmentsDir|includeArchived`\n * makes the expensive body-read happen only on first query and after a content\n * change (detected by a cheap stat-only max-mtime sweep) — never per query.\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { resolve, join } from 'node:path';\nimport { fileExists } from '../utils/fs.js';\nimport { listAssignmentsByProject } from '../utils/assignment-walk.js';\nimport { latestPlanFile } from '../lifecycle/facts.js';\nimport {\n parseAssignmentFull,\n parsePlan,\n parseProgress,\n parseComments,\n parseHandoff,\n parseDecisionRecord,\n parseScratchpad,\n parseMemory,\n parseResource,\n parseProject,\n} from '../dashboard/parser.js';\nimport type { FileKind, SearchDoc } from './types.js';\n\nexport interface IndexOptions {\n projectsDir: string;\n assignmentsDir: string;\n includeArchived?: boolean;\n}\n\n/** Identity carried from the owning assignment onto every sidecar doc. */\ninterface AssignmentIdentity {\n assignmentId: string | null;\n assignmentSlug: string;\n projectSlug: string | null;\n projectWorkspace: string | null;\n standalone: boolean;\n type?: string;\n status?: string;\n archived: boolean;\n}\n\n/** The assignment sidecars, each with its kind + parser → body extractor. */\nconst SIDECARS: Array<{ file: string; kind: FileKind; body: (content: string) => string }> = [\n { file: 'progress.md', kind: 'progress', body: (c) => parseProgress(c).body },\n { file: 'comments.md', kind: 'comments', body: (c) => parseComments(c).body },\n { file: 'handoff.md', kind: 'handoff', body: (c) => parseHandoff(c).body },\n { file: 'decision-record.md', kind: 'decision-record', body: (c) => parseDecisionRecord(c).body },\n { file: 'scratchpad.md', kind: 'scratchpad', body: (c) => parseScratchpad(c).body },\n];\n\n/**\n * Build the full content index for the given dirs. Skips archived\n * assignments/projects unless `includeArchived`.\n */\nexport async function buildIndex(opts: IndexOptions): Promise<SearchDoc[]> {\n const { projectsDir, assignmentsDir, includeArchived = false } = opts;\n const docs: SearchDoc[] = [];\n\n // ── per-project workspace lookup (read each project.md once) ────────────\n const projectWorkspace = new Map<string, string | null>();\n const projectArchived = new Map<string, boolean>();\n if (await fileExists(projectsDir)) {\n const projects = await readdir(projectsDir, { withFileTypes: true });\n for (const m of projects) {\n if (!m.isDirectory()) continue;\n if (m.name.startsWith('.') || m.name.startsWith('_')) continue;\n const projectMdPath = resolve(projectsDir, m.name, 'project.md');\n let workspace: string | null = null;\n let archived = false;\n if (await fileExists(projectMdPath)) {\n try {\n const parsed = parseProject(await readFile(projectMdPath, 'utf-8'));\n workspace = parsed.workspace;\n archived = parsed.archived;\n } catch {\n // tolerate a malformed project.md — workspace stays null\n }\n }\n projectWorkspace.set(m.name, workspace);\n projectArchived.set(m.name, archived);\n }\n }\n\n // ── assignments (project-nested + standalone) ───────────────────────────\n const { withAssignmentMd } = await listAssignmentsByProject(projectsDir, assignmentsDir);\n for (const entry of withAssignmentMd) {\n const assignmentMdPath = resolve(entry.assignmentDir, 'assignment.md');\n let assignmentContent: string;\n try {\n assignmentContent = await readFile(assignmentMdPath, 'utf-8');\n } catch {\n continue;\n }\n const assignment = parseAssignmentFull(assignmentContent);\n\n // An assignment is excluded by default when EITHER it or its owning\n // project is archived. Both flags propagate onto the docs as `archived`.\n const projectIsArchived = entry.projectSlug\n ? projectArchived.get(entry.projectSlug) === true\n : false;\n const archived = assignment.archived || projectIsArchived;\n\n if (!includeArchived && archived) continue;\n\n const workspace = entry.projectSlug ? projectWorkspace.get(entry.projectSlug) ?? null : null;\n const identity: AssignmentIdentity = {\n assignmentId: assignment.id || null,\n assignmentSlug: entry.assignmentSlug,\n projectSlug: entry.projectSlug,\n projectWorkspace: workspace,\n standalone: entry.standalone,\n type: assignment.type ?? undefined,\n status: assignment.status,\n archived,\n };\n\n // assignment.md itself\n docs.push(makeAssignmentDoc(assignmentMdPath, 'assignment', assignment.title, assignment.body, identity));\n\n // latest plan only\n const planName = await latestPlanFile(entry.assignmentDir);\n if (planName) {\n const planPath = join(entry.assignmentDir, planName);\n if (await fileExists(planPath)) {\n try {\n const plan = parsePlan(await readFile(planPath, 'utf-8'));\n docs.push(makeAssignmentDoc(planPath, 'plan', assignment.title, plan.body, identity));\n } catch {\n /* skip unreadable plan */\n }\n }\n }\n\n // sidecars\n for (const sidecar of SIDECARS) {\n const sidecarPath = resolve(entry.assignmentDir, sidecar.file);\n if (!(await fileExists(sidecarPath))) continue;\n try {\n const body = sidecar.body(await readFile(sidecarPath, 'utf-8'));\n docs.push(makeAssignmentDoc(sidecarPath, sidecar.kind, assignment.title, body, identity));\n } catch {\n /* skip unreadable sidecar */\n }\n }\n }\n\n // ── project memories + resources ────────────────────────────────────────\n if (await fileExists(projectsDir)) {\n const projects = await readdir(projectsDir, { withFileTypes: true });\n for (const m of projects) {\n if (!m.isDirectory()) continue;\n if (m.name.startsWith('.') || m.name.startsWith('_')) continue;\n const projectIsArchived = projectArchived.get(m.name) === true;\n if (projectIsArchived && !includeArchived) continue;\n const projectPath = resolve(projectsDir, m.name);\n const workspace = projectWorkspace.get(m.name) ?? null;\n\n await indexItems(\n docs,\n resolve(projectPath, 'memories'),\n 'memory',\n m.name,\n workspace,\n projectIsArchived,\n (content) => {\n const parsed = parseMemory(content);\n return { title: parsed.name, body: parsed.body };\n },\n );\n await indexItems(\n docs,\n resolve(projectPath, 'resources'),\n 'resource',\n m.name,\n workspace,\n projectIsArchived,\n (content) => {\n const parsed = parseResource(content);\n return { title: parsed.name, body: parsed.body };\n },\n );\n }\n }\n\n return docs;\n}\n\nfunction makeAssignmentDoc(\n path: string,\n fileKind: FileKind,\n title: string,\n body: string,\n identity: AssignmentIdentity,\n): SearchDoc {\n return {\n id: path,\n path,\n fileKind,\n title,\n body,\n projectSlug: identity.projectSlug,\n projectWorkspace: identity.projectWorkspace,\n assignmentSlug: identity.assignmentSlug,\n assignmentId: identity.assignmentId,\n standalone: identity.standalone,\n type: identity.type,\n status: identity.status,\n archived: identity.archived,\n };\n}\n\n/** Index every `*.md` (skipping `_index.md` / dot-prefixed) in a memories/resources dir. */\nasync function indexItems(\n docs: SearchDoc[],\n dir: string,\n fileKind: 'memory' | 'resource',\n projectSlug: string,\n projectWorkspace: string | null,\n archived: boolean,\n extract: (content: string) => { title: string; body: string },\n): Promise<void> {\n if (!(await fileExists(dir))) return;\n const entries = await readdir(dir, { withFileTypes: true });\n for (const e of entries) {\n if (!e.isFile()) continue;\n if (!e.name.endsWith('.md')) continue;\n if (e.name.startsWith('.') || e.name.startsWith('_')) continue;\n const itemSlug = e.name.slice(0, -'.md'.length);\n const filePath = resolve(dir, e.name);\n try {\n const { title, body } = extract(await readFile(filePath, 'utf-8'));\n docs.push({\n id: filePath,\n path: filePath,\n fileKind,\n title,\n body,\n projectSlug,\n projectWorkspace,\n assignmentSlug: null,\n assignmentId: null,\n standalone: false,\n itemSlug,\n archived,\n });\n } catch {\n /* skip unreadable item */\n }\n }\n}\n\n// ── cache + invalidation seam ─────────────────────────────────────────────\n\n/**\n * A stat-only fingerprint of the indexed `.md` files. `mtimeMax` alone misses\n * the deletion of a non-newest file (signature unchanged → stale cache), so we\n * also track `count` and `sizeSum` — both of which change on any add OR delete.\n */\ninterface IndexSignature {\n count: number;\n mtimeMax: number;\n sizeSum: number;\n}\n\ninterface CacheEntry {\n docs: SearchDoc[];\n builtAt: number;\n signature: IndexSignature;\n}\n\nconst cache = new Map<string, CacheEntry>();\n\nfunction cacheKey(opts: IndexOptions): string {\n return `${opts.projectsDir}|${opts.assignmentsDir}|${opts.includeArchived ?? false}`;\n}\n\nfunction signaturesEqual(a: IndexSignature, b: IndexSignature): boolean {\n return a.count === b.count && a.mtimeMax === b.mtimeMax && a.sizeSum === b.sizeSum;\n}\n\n/**\n * Cheap stat-only sweep of the content dirs → an {@link IndexSignature}. Walks\n * dirs (O(files) `stat`s, NOT reads). `count` + `sizeSum` change on add/delete;\n * `mtimeMax` changes on modification. Returns all-zeros when nothing exists.\n */\nasync function indexSignature(\n projectsDir: string,\n assignmentsDir: string,\n): Promise<IndexSignature> {\n let count = 0;\n let mtimeMax = 0;\n let sizeSum = 0;\n async function walk(dir: string): Promise<void> {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (e.name.startsWith('.')) continue;\n const full = resolve(dir, e.name);\n if (e.isDirectory()) {\n await walk(full);\n } else if (e.isFile() && e.name.endsWith('.md')) {\n try {\n const s = await stat(full);\n count += 1;\n sizeSum += s.size;\n if (s.mtimeMs > mtimeMax) mtimeMax = s.mtimeMs;\n } catch {\n /* ignore */\n }\n }\n }\n }\n await walk(projectsDir);\n if (assignmentsDir !== projectsDir) await walk(assignmentsDir);\n return { count, mtimeMax, sizeSum };\n}\n\n/**\n * Return the index for the given dirs, rebuilding only when content changed.\n *\n * Semantics: compute the current {@link IndexSignature} via a stat-only sweep;\n * if a cache entry for this key exists AND its signature is unchanged, return\n * the cached docs (no body reads); otherwise do a full `buildIndex`, replace\n * the cache entry, and return it. The signature changes on add, delete, and\n * modification of any indexed `.md` file.\n */\nexport async function getIndex(opts: IndexOptions): Promise<SearchDoc[]> {\n const key = cacheKey(opts);\n const signature = await indexSignature(opts.projectsDir, opts.assignmentsDir);\n const existing = cache.get(key);\n if (existing && signaturesEqual(existing.signature, signature)) {\n return existing.docs;\n }\n const docs = await buildIndex(opts);\n cache.set(key, { docs, builtAt: Date.now(), signature });\n return docs;\n}\n\n/** Clear the whole cache so the next `getIndex` rebuilds (file-change hook). */\nexport function invalidateIndex(): void {\n cache.clear();\n}\n","/**\n * Default `SearchProvider` — fuse.js full-text over indexed markdown bodies.\n *\n * The provider returns a NEUTRAL snippet (no highlight markers) plus\n * `matches: MatchRange[]` in snippet-local coordinates, a 1-based `line`, and\n * the nearest preceding `section` heading. Callers format highlighting\n * themselves (CLI `**…**`, API/palette HTML-safe `<mark>`).\n *\n * `extractSnippet` and `nearestSection` are pure exported helpers so they're\n * directly unit-testable. Fuse construction follows `src/tui/hooks/useSearch.ts`\n * (now with `includeMatches`).\n */\n\nimport Fuse from 'fuse.js';\nimport type { FuseResultMatch } from 'fuse.js';\nimport type { MatchRange, SearchDoc, SearchHit, SearchProvider, SearchQuery } from './types.js';\nimport { routeForHit } from './route.js';\n\n/** Half-window (chars) on each side of the match offset for the snippet. */\nconst SNIPPET_RADIUS = 60;\n\nexport class FuseProvider implements SearchProvider {\n private docs: SearchDoc[] = [];\n\n index(docs: SearchDoc[]): void {\n this.docs = docs;\n }\n\n query(q: SearchQuery, limit: number): SearchHit[] {\n // 1. Pre-filter the doc subset (cheap; keeps Fuse scores undiluted).\n const subset = this.docs.filter((d) => {\n if (q.project !== undefined && d.projectSlug !== q.project) return false;\n if (q.type && q.type.length > 0 && (!d.type || !q.type.includes(d.type))) return false;\n if (q.status && q.status.length > 0 && (!d.status || !q.status.includes(d.status))) return false;\n if (q.in && q.in.length > 0 && !q.in.includes(d.fileKind)) return false;\n return true;\n });\n\n const fuse = new Fuse(subset, {\n keys: ['title', 'body'],\n threshold: 0.4,\n includeScore: true,\n includeMatches: true,\n ignoreLocation: true,\n minMatchCharLength: 2,\n });\n\n const results = fuse.search(q.query);\n\n const hits: SearchHit[] = [];\n for (const result of results) {\n const doc = result.item;\n const score = result.score ?? 0;\n const bodyMatch = pickBodyMatch(result.matches);\n const { snippet, matches, line, section } = extractSnippet(doc.body, bodyMatch, q.query);\n\n const hit: SearchHit = {\n path: doc.path,\n projectSlug: doc.projectSlug,\n projectWorkspace: doc.projectWorkspace,\n assignmentSlug: doc.assignmentSlug,\n assignmentId: doc.assignmentId,\n standalone: doc.standalone,\n fileKind: doc.fileKind,\n title: doc.title,\n score,\n snippet,\n matches,\n line,\n route: '',\n };\n if (doc.itemSlug !== undefined) hit.itemSlug = doc.itemSlug;\n if (section !== undefined) hit.section = section;\n hit.route = routeForHit(hit);\n hits.push(hit);\n }\n\n hits.sort((a, b) => a.score - b.score);\n return hits.slice(0, limit);\n }\n}\n\n/** First Fuse match on the `body` key (the one we can locate in `doc.body`). */\nfunction pickBodyMatch(\n matches: ReadonlyArray<FuseResultMatch> | undefined,\n): FuseResultMatch | undefined {\n if (!matches) return undefined;\n return matches.find((m) => m.key === 'body');\n}\n\nexport interface SnippetResult {\n /** Neutral text window (no highlight markers). */\n snippet: string;\n /** Match ranges in snippet-local coordinates. */\n matches: MatchRange[];\n /** 1-based line of the match in the source body. */\n line: number;\n /** Nearest preceding markdown heading text, if any. */\n section?: string;\n}\n\n/**\n * Produce a neutral snippet window around the first body match (or a substring\n * fallback for `query`), with snippet-local match ranges, the 1-based line, and\n * the nearest preceding `#`-heading section.\n *\n * `bodyMatch` is the Fuse `matches[]` entry for the `body` key (its `indices`\n * are inclusive `[start, end]` tuples). When absent, we fall back to a\n * case-insensitive substring search for `query`. When no offset can be found at\n * all, the snippet is the first window chars with `matches: []` and `line: 1`.\n */\nexport function extractSnippet(\n body: string,\n bodyMatch: FuseResultMatch | undefined,\n query: string,\n): SnippetResult {\n // Resolve the source-body match ranges (inclusive end → exclusive end).\n let ranges: Array<{ start: number; end: number }> = [];\n if (bodyMatch && bodyMatch.indices.length > 0) {\n ranges = bodyMatch.indices\n .map(([s, e]) => ({ start: s, end: e + 1 }))\n .sort((a, b) => a.start - b.start);\n } else {\n const idx = body.toLowerCase().indexOf(query.trim().toLowerCase());\n if (idx >= 0 && query.trim().length > 0) {\n ranges = [{ start: idx, end: idx + query.trim().length }];\n }\n }\n\n if (ranges.length === 0) {\n const snippet = body.slice(0, SNIPPET_RADIUS * 2);\n const section = nearestSection(body, 0);\n const result: SnippetResult = { snippet, matches: [], line: 1 };\n if (section !== undefined) result.section = section;\n return result;\n }\n\n const first = ranges[0];\n const line = countLines(body, first.start);\n const section = nearestSection(body, first.start);\n\n const windowStart = Math.max(0, first.start - SNIPPET_RADIUS);\n const windowEnd = Math.min(body.length, first.start + SNIPPET_RADIUS);\n const snippet = body.slice(windowStart, windowEnd);\n\n // Translate ranges into snippet-local coords, clamped to the window.\n const matches: MatchRange[] = [];\n for (const r of ranges) {\n const start = Math.max(r.start, windowStart);\n const end = Math.min(r.end, windowEnd);\n if (end <= start) continue;\n matches.push({ start: start - windowStart, end: end - windowStart });\n }\n\n const result: SnippetResult = { snippet, matches, line };\n if (section !== undefined) result.section = section;\n return result;\n}\n\n/** 1-based line number of the char at `offset` (count `\\n` before it). */\nfunction countLines(body: string, offset: number): number {\n let line = 1;\n const limit = Math.min(offset, body.length);\n for (let i = 0; i < limit; i++) {\n if (body[i] === '\\n') line++;\n }\n return line;\n}\n\n/**\n * Nearest preceding markdown `#`-heading text at or before `offset`. Returns the\n * heading text (without the `#` markers) or `undefined` when none precedes.\n */\nexport function nearestSection(body: string, offset: number): string | undefined {\n const before = body.slice(0, offset);\n const headingRe = /^#{1,6}\\s+(.+?)\\s*$/gm;\n let match: RegExpExecArray | null;\n let last: string | undefined;\n while ((match = headingRe.exec(before)) !== null) {\n last = match[1].trim();\n }\n return last;\n}\n","/**\n * Semantic search seam — a stub provider for the future embeddings slot, plus\n * the `resolveProvider` resolver that returns the semantic provider only when\n * `--semantic` is set AND it's available, otherwise gracefully falls back to the\n * default `FuseProvider`. No embeddings are configured today, so\n * `SemanticProvider.isAvailable()` is always `false` and we always fall back.\n */\n\nimport type { SearchDoc, SearchHit, SearchProvider, SearchQuery } from './types.js';\nimport { FuseProvider } from './fuse-provider.js';\n\n/** Thrown by the stub's `index`/`query` — the seam is not yet implemented. */\nexport class NotImplementedError extends Error {\n constructor(message = 'SemanticProvider is not implemented yet') {\n super(message);\n this.name = 'NotImplementedError';\n }\n}\n\nexport class SemanticProvider implements SearchProvider {\n /** No embeddings configured → never available in v1. */\n static isAvailable(): boolean {\n return false;\n }\n\n index(_docs: SearchDoc[]): void {\n throw new NotImplementedError();\n }\n\n query(_q: SearchQuery, _limit: number): SearchHit[] {\n throw new NotImplementedError();\n }\n}\n\n/**\n * Pick the search provider. Returns a `SemanticProvider` only when the caller\n * asked for `--semantic` AND it's actually available; otherwise the default\n * `FuseProvider`.\n */\nexport function resolveProvider(opts?: { semantic?: boolean }): SearchProvider {\n if (opts?.semantic && SemanticProvider.isAvailable()) {\n return new SemanticProvider();\n }\n return new FuseProvider();\n}\n","/**\n * Barrel for the shared search core — re-exports everything the CLI\n * (`src/commands/search.ts`) and the dashboard (`src/dashboard/api-search.ts` +\n * palette) consume.\n */\n\nexport type {\n FileKind,\n SearchDoc,\n SearchHit,\n MatchRange,\n SearchQuery,\n SearchProvider,\n} from './types.js';\nexport { FILE_KINDS, FILE_KIND_ALIASES, parseFileKinds } from './types.js';\n\nexport { FILE_KIND_TO_TAB, slugifyHeading, routeForHit } from './route.js';\n\nexport { buildIndex, getIndex, invalidateIndex } from './indexer.js';\nexport type { IndexOptions } from './indexer.js';\n\nexport { FuseProvider, extractSnippet, nearestSection } from './fuse-provider.js';\nexport type { SnippetResult } from './fuse-provider.js';\n\nexport { SemanticProvider, resolveProvider, NotImplementedError } from './semantic-provider.js';\n","import type {\n HelpChecklistItem,\n HelpCommand,\n HelpResponse,\n HelpStatusGuideEntry,\n} from './types.js';\nimport { getStatusConfig } from './api.js';\n\nconst CLI_COMMANDS: HelpCommand[] = [\n // --- Core setup & scaffolding (indices 0-4) ---\n {\n command: 'syntaur setup',\n description: 'Initialize Syntaur and optionally install plugins or launch the dashboard.',\n example: 'syntaur setup',\n },\n {\n command: 'syntaur init',\n description: 'Initialize the local Syntaur home directory and config scaffolding without any prompts.',\n example: 'syntaur init',\n },\n {\n command: 'syntaur create-project',\n description: 'Create a new project folder with the required source and derived files.',\n example: 'syntaur create-project \"Ship dashboard overhaul\"',\n },\n {\n command: 'syntaur create-assignment',\n description: 'Create a new assignment inside a project.',\n example: 'syntaur create-assignment \"Implement overview API\" --project ui-overhaul',\n },\n {\n command: 'syntaur assign',\n description: 'Set the assignee for an assignment before work begins.',\n example: 'syntaur assign implement-overview --project ui-overhaul --agent codex-1',\n },\n\n // --- Lifecycle transitions ---\n {\n command: 'syntaur start',\n description: 'Transition an assignment to in_progress.',\n example: 'syntaur start implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur shape',\n description: 'Transition a draft assignment to ready_for_planning once the Objective and Acceptance Criteria are fleshed out.',\n example: 'syntaur shape implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur plan-ready',\n description: 'Transition a ready_for_planning assignment to ready_to_implement once a plan has been written and approved.',\n example: 'syntaur plan-ready implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur implement',\n description: 'Transition a ready_to_implement assignment to in_progress when coding begins.',\n example: 'syntaur implement implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur migrate-statuses',\n description: 'Suggest pending -> ready_for_planning promotions for fleshed-out assignments. Dry-run by default; pass --apply to write.',\n example: 'syntaur migrate-statuses --apply',\n },\n {\n command: 'syntaur review',\n description: 'Move active work into review once implementation is ready for inspection.',\n example: 'syntaur review implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur complete',\n description: 'Mark an assignment completed after review or direct completion.',\n example: 'syntaur complete implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur block',\n description: 'Mark an assignment blocked and record the explicit reason.',\n example: 'syntaur block implement-overview --project ui-overhaul --reason \"Waiting on API spec\"',\n },\n {\n command: 'syntaur unblock',\n description: 'Move a blocked assignment back to in_progress after the blocker is cleared.',\n example: 'syntaur unblock implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur fail',\n description: 'Mark an assignment failed when it cannot be completed as planned.',\n example: 'syntaur fail implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur reopen',\n description: 'Reopen a completed or failed assignment back to in_progress.',\n example: 'syntaur reopen implement-overview --project ui-overhaul',\n },\n\n // --- Dashboard (index 12) ---\n {\n command: 'syntaur dashboard',\n description: 'Start the local dashboard UI over the project files on disk.',\n example: 'syntaur dashboard --port 4800',\n },\n\n // --- Plugin & adapter setup (indices 13-16) ---\n {\n command: 'syntaur install-plugin',\n description: 'Install the Syntaur Claude Code plugin, detecting the local Claude marketplace when available and prompting for the target directory when interactive.',\n example: 'syntaur install-plugin --target-dir ~/.claude/plugins/marketplaces/user-plugins/plugins/syntaur',\n },\n {\n command: 'syntaur install-codex-plugin',\n description: 'Install the Syntaur Codex plugin and register its marketplace entry, prompting for both paths when interactive.',\n example: 'syntaur install-codex-plugin --target-dir ~/plugins/syntaur --marketplace-path ~/.agents/plugins/marketplace.json',\n },\n {\n command: 'syntaur uninstall',\n description: 'Remove Syntaur plugins and optionally local ~/.syntaur data.',\n example: 'syntaur uninstall --all',\n },\n {\n command: 'syntaur setup-adapter',\n description: 'Generate adapter instruction files for cursor, codex, or opencode in the current directory.',\n example: 'syntaur setup-adapter cursor --project ui-overhaul --assignment implement-overview',\n },\n\n // --- Session & server tracking (index 17) ---\n {\n command: 'syntaur track-session',\n description:\n 'Register an agent session. Requires --session-id from the agent runtime (real, not generated). Pass --transcript-path for the rollout/transcript file. --project and --assignment are optional.',\n example:\n 'syntaur track-session --agent claude --session-id <real-id> --transcript-path <path> --project ui-overhaul --assignment implement-overview',\n },\n\n // --- Browsing & playbooks (indices 18-20) ---\n {\n command: 'syntaur browse',\n description: 'Interactive TUI browser for projects and assignments.',\n example: 'syntaur browse',\n },\n {\n command: 'syntaur create-playbook',\n description: 'Create a new playbook with behavioral rules for agents.',\n example: 'syntaur create-playbook \"Code Review Standards\"',\n },\n {\n command: 'syntaur list-playbooks',\n description:\n 'List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.',\n example: 'syntaur list-playbooks --all',\n },\n {\n command: 'syntaur enable-playbook',\n description:\n 'Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.',\n example: 'syntaur enable-playbook commit-discipline',\n },\n {\n command: 'syntaur disable-playbook',\n description:\n 'Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.',\n example: 'syntaur disable-playbook commit-discipline',\n },\n {\n command: 'syntaur delete-playbook',\n description:\n 'Delete a playbook from disk and regenerate the manifest. Refuses to delete the manifest itself.',\n example: 'syntaur delete-playbook scratch-foo',\n },\n];\n\nconst WORKFLOW: HelpChecklistItem[] = [\n {\n title: 'Initialize the workspace',\n detail: 'Run setup once so Syntaur can initialize its local home directory and offer plugin installation.',\n command: CLI_COMMANDS[0],\n },\n {\n title: 'Create a project',\n detail: 'Use a project for a higher-level objective. Projects group assignments, shared resources, and memories.',\n command: CLI_COMMANDS[2],\n href: '/create/project',\n },\n {\n title: 'Create the first assignment',\n detail: 'Assignments are the execution unit. Create one for each concrete chunk of work inside the project.',\n command: CLI_COMMANDS[3],\n },\n {\n title: 'Assign the work',\n detail: 'Setting an assignee before starting is recommended for clarity, but not required.',\n command: CLI_COMMANDS[4],\n },\n {\n title: 'Start, review, complete, or block through lifecycle actions',\n detail: 'Status changes happen through lifecycle actions, kanban drag-and-drop, or the status override controls.',\n command: CLI_COMMANDS[5],\n },\n {\n title: 'Use the dashboard for triage and context',\n detail: 'Overview shows the current queue, project pages show health, assignment pages show the execution surface.',\n command: CLI_COMMANDS[12],\n href: '/',\n },\n];\n\nconst DEFAULT_STATUS_GUIDE: Record<string, { meaning: string; useWhen: string }> = {\n draft: {\n meaning: 'The assignment is a just-created stub; objective and acceptance criteria are not yet fleshed out.',\n useWhen: 'Use draft for newly-scaffolded assignments. Transition to ready_for_planning with `syntaur shape` once the Objective and AC are written.',\n },\n pending: {\n meaning: 'The assignment has not started yet.',\n useWhen: 'Use pending while waiting to start. If dependencies are unmet, pending is the normal waiting state.',\n },\n ready_for_planning: {\n meaning: 'The assignment is fully shaped; a plan needs to be written before implementation can begin.',\n useWhen: 'Use ready_for_planning after the Objective and Acceptance Criteria are filled out but before any plan.md exists. Transition to ready_to_implement with `syntaur plan-ready` after the plan is approved.',\n },\n ready_to_implement: {\n meaning: 'The plan has been written and approved; the assignment is ready to start coding.',\n useWhen: 'Use ready_to_implement once a plan.md exists and is approved. Transition to in_progress with `syntaur implement` when coding begins.',\n },\n in_progress: {\n meaning: 'An assigned agent is actively working the assignment.',\n useWhen: 'Use in_progress once the work has started and dependencies are satisfied.',\n },\n blocked: {\n meaning: 'The assignment hit a manual or runtime obstacle.',\n useWhen: 'Use blocked when work hits an obstacle. Adding a blockedReason is recommended for traceability.',\n },\n review: {\n meaning: 'Implementation is ready for inspection or validation.',\n useWhen: 'Use review after active work is ready to be checked before completion.',\n },\n completed: {\n meaning: 'The assignment is done.',\n useWhen: 'Use completed when the acceptance criteria are satisfied.',\n },\n failed: {\n meaning: 'The assignment could not be completed as planned.',\n useWhen: 'Use failed when the work cannot be recovered within the current assignment.',\n },\n};\n\nasync function buildStatusGuide(): Promise<HelpStatusGuideEntry[]> {\n const config = await getStatusConfig();\n\n return config.statuses.map((s) => {\n const defaults = DEFAULT_STATUS_GUIDE[s.id];\n return {\n status: s.id,\n meaning: s.description ?? defaults?.meaning ?? `The assignment is in the \"${s.label}\" state.`,\n useWhen: defaults?.useWhen ?? `Use ${s.id} when appropriate for the \"${s.label}\" workflow state.`,\n };\n });\n}\n\nexport async function getDashboardHelp(): Promise<HelpResponse> {\n return {\n generatedAt: new Date().toISOString(),\n whatIsSyntaur: {\n summary:\n 'Syntaur is a local-first, markdown-backed agent work system. The dashboard is a live view over project folders and files on disk.',\n bullets: [\n 'Markdown files are the source of truth.',\n 'The UI reads project folders, assignment files, and derived indexes from the local filesystem.',\n 'Derived underscore-prefixed files are projections, not the canonical edit target.',\n ],\n },\n coreConcepts: [\n {\n term: 'Project',\n description:\n 'A project is the higher-level objective. It owns assignments, shared resources, and project memories.',\n },\n {\n term: 'Assignment',\n description:\n 'An assignment is a concrete unit of execution. Assignment frontmatter is the source of truth for status, priority, assignee, and dependencies.',\n },\n {\n term: 'Resource',\n description:\n 'A project-level shared reference file that provides source material or constraints for the work.',\n },\n {\n term: 'Memory',\n description:\n 'A project-level learning or pattern captured during execution so future assignments can reuse it.',\n },\n {\n term: 'Manifest',\n description:\n 'A derived navigation file that points agents at the project overview, indexes, and agent instructions.',\n },\n {\n term: 'Derived file',\n description:\n 'An underscore-prefixed file regenerated from canonical markdown sources. Read it, but do not edit it directly.',\n },\n {\n term: 'Handoff',\n description:\n 'An append-only log that records baton-passes between agents or sessions without rewriting prior history.',\n },\n {\n term: 'Decision record',\n description:\n 'An append-only record of important decisions, rationale, and follow-up consequences.',\n },\n {\n term: 'Playbook',\n description:\n 'A behavioral rule set stored in ~/.syntaur/playbooks/. Playbooks define constraints and conventions that agents must follow during execution. Manage them via the CLI or the Playbooks page.',\n },\n {\n term: 'Workspace',\n description:\n 'The repository context for an assignment, including the repository path, worktree path, branch, and parent branch. Workspace fields connect an assignment to the code being worked on and define write boundaries.',\n },\n {\n term: 'Agent Session',\n description:\n 'A tracked AI session tied to assignment work. Sessions are registered via the track-session CLI command or the Claude Code plugin and visible on the Agent Sessions page.',\n },\n {\n term: 'Server',\n description:\n 'A tracked tmux session with automatic port discovery, branch detection, and assignment linking. The Servers page shows all tracked sessions with their windows, panes, and discovered services.',\n },\n ],\n workflow: WORKFLOW,\n statusGuide: await buildStatusGuide(),\n ownershipRules: [\n {\n label: 'Human-authored files',\n files: ['project.md', 'agent.md', 'claude.md'],\n description:\n 'These files define project intent and instructions. The dashboard treats project status as derived except for the archive fields.',\n },\n {\n label: 'Assignment working files',\n files: ['assignment.md', 'plan*.md (optional, versioned)', 'scratchpad.md'],\n description:\n 'These are agent-writable files. The dashboard lets you edit the source markdown while preserving unsupported frontmatter keys.',\n },\n {\n label: 'Append-only logs',\n files: ['handoff.md', 'decision-record.md'],\n description:\n 'These logs preserve history. The dashboard appends new entries instead of rewriting previous ones.',\n },\n {\n label: 'Derived files',\n files: ['_status.md', '_index-assignments.md', '_index-plans.md', '_index-decisions.md'],\n description:\n 'These files are read-only projections. They can lag behind source files, so the dashboard computes source-first state.',\n },\n ],\n commands: CLI_COMMANDS,\n navigation: [\n {\n label: 'Overview',\n description: 'Triage hub showing assignments that need action, recent activity, progress stats, and first-run setup guidance.',\n href: '/',\n },\n {\n label: 'Projects',\n description: 'Browse, search, filter, and sort the project directory. Create new projects and drill into project workspaces.',\n href: '/projects',\n },\n {\n label: 'Assignments',\n description: 'Cross-project kanban board of all assignments. Drag cards between columns to change status, or filter by project, assignee, or status.',\n href: '/assignments',\n },\n {\n label: 'Servers',\n description: 'Tracked tmux sessions with auto-discovered ports, URLs, git branches, and links to related assignments. Register sessions manually or let autodiscovery find them.',\n href: '/servers',\n },\n {\n label: 'Agent Sessions',\n description: 'Monitor which AI agents are currently working, what assignments they are linked to, and session duration. Sessions are registered via the Claude Code plugin or track-session CLI command.',\n href: '/agent-sessions',\n },\n {\n label: 'Playbooks',\n description: 'Create, browse, and edit behavioral rules that agents must follow. The playbook manifest at ~/.syntaur/playbooks/manifest.md is auto-generated for inclusion in agent instructions.',\n href: '/playbooks',\n },\n {\n label: 'Help',\n description: 'This page. Status guide, CLI quick reference, core concepts, and FAQ.',\n href: '/help',\n },\n {\n label: 'Settings',\n description: 'Customize status definitions, labels, colors, display order, and done states. Changes apply globally across the dashboard and CLI.',\n href: '/settings',\n },\n {\n label: 'Project page',\n description: 'The project workspace shows health stats, assignment list, dependency graph, shared resources, and memories.',\n href: '/projects',\n },\n {\n label: 'Assignment page',\n description: 'The assignment workspace shows lifecycle actions, plan editor, scratchpad, handoff log, decision records, and agent sessions.',\n href: '/projects',\n },\n ],\n faq: [\n {\n question: 'Why are some files read-only in the dashboard?',\n answer:\n 'Underscore-prefixed files are derived projections that can be rebuilt from canonical markdown sources. Editing them would create drift, so the UI treats them as read-only.',\n },\n {\n question: 'Why can an assignment be pending even when nothing looks broken?',\n answer:\n 'Pending often just means the work has not started yet or it is waiting on declared dependencies. Blocked is reserved for exceptional runtime obstacles that need intervention.',\n },\n {\n question: 'How do I change an assignment\\'s status?',\n answer:\n 'Use lifecycle CLI commands (syntaur start, syntaur complete, etc.), drag cards on the kanban board, or use the Override Status dropdown on the assignment page. Any status can be set from any other status.',\n },\n {\n question: 'How do I customize statuses?',\n answer:\n 'Open the Settings page from the sidebar. You can add, remove, rename, recolor, and reorder statuses. You can also mark statuses as done states. Changes are saved to ~/.syntaur/config.md and take effect immediately across the dashboard.',\n },\n {\n question: 'What is a done state?',\n answer:\n 'A done state (also called terminal status) means the assignment is finished. Done states fill the completed portion of progress bars and satisfy dependency requirements. By default, \"completed\" and \"failed\" are done states. You can configure which statuses are done states in Settings.',\n },\n {\n question: 'What are playbooks and how do I use them?',\n answer:\n 'Playbooks are markdown files in ~/.syntaur/playbooks/ that define behavioral rules agents must follow. Create them via the CLI (syntaur create-playbook) or the Playbooks page. The auto-generated manifest at ~/.syntaur/playbooks/manifest.md can be included in your CLAUDE.md so agents pick up the rules.',\n },\n {\n question: 'How does agent session tracking work?',\n answer:\n 'When an AI agent starts working on an assignment, it can register a session via the track-session CLI command or the Claude Code plugin\\'s /track-session command. The Agent Sessions page shows active and completed sessions with their linked assignments and duration.',\n },\n {\n question: 'How does server tracking work?',\n answer:\n 'Syntaur tracks tmux sessions to discover running dev servers, their ports, git branches, and linked assignments. Register sessions on the Servers page or let autodiscovery find them. Pane info refreshes automatically.',\n },\n ],\n firstProjectChecklist: [\n {\n title: 'Create the project',\n detail: 'Describe the overall objective in project.md, then add tags and archive metadata only when needed.',\n command: CLI_COMMANDS[1],\n href: '/create/project',\n },\n {\n title: 'Create at least one assignment',\n detail: 'Break the project into executable work units with explicit priority and dependencies.',\n command: CLI_COMMANDS[2],\n },\n {\n title: 'Assign and start the first assignment',\n detail: 'Set an assignee, then start the assignment once prerequisites are complete.',\n command: CLI_COMMANDS[3],\n },\n {\n title: 'Use the assignment workspace for execution',\n detail: 'Keep the objective and todos in assignment.md, implementation plans in optional versioned plan files (plan.md, plan-v2.md, ...), and transient notes in scratchpad.md.',\n href: '/projects',\n },\n {\n title: 'Record handoffs and decisions without rewriting history',\n detail: 'Append new handoff and decision entries instead of editing prior entries.',\n },\n {\n title: 'Return to Overview for triage',\n detail: 'Overview surfaces the queue of assignments that need action next.',\n href: '/',\n },\n ],\n links: [\n { label: 'Overview', href: '/' },\n { label: 'Project Directory', href: '/projects' },\n { label: 'Assignments Board', href: '/assignments' },\n { label: 'Servers', href: '/servers' },\n { label: 'Agent Sessions', href: '/agent-sessions' },\n { label: 'Playbooks', href: '/playbooks' },\n { label: 'Settings', href: '/settings' },\n { label: 'Create Project', href: '/create/project' },\n ],\n };\n}\n\nexport function getHelpCommandNames(): string[] {\n return CLI_COMMANDS.map((command) => command.command.replace(/^syntaur\\s+/, ''));\n}\n","import Database from 'better-sqlite3';\nimport { resolve } from 'node:path';\nimport { readdir } from 'node:fs/promises';\nimport { syntaurRoot } from '../utils/paths.js';\nimport { fileExists } from '../utils/fs.js';\nimport type { AgentSession, AgentSessionStatus } from './types.js';\n\nlet db: Database.Database | null = null;\n\nconst SCHEMA_VERSION = '5';\n\n// The base schema deliberately OMITS the project_slug indexes — they are\n// created after any legacy-schema migrations run below. Older installs may\n// have the `mission_slug` column, and creating an index that references\n// `project_slug` before the rename migration runs would fail.\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS sessions (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n transcript_path TEXT,\n pid INTEGER,\n pid_started_at TEXT,\n original_head_sha TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\nCREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\nCREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);\n`;\n\nconst POST_MIGRATION_INDEXES_SQL = `\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\nCREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n`;\n\n/**\n * Initialize the SQLite database for session tracking.\n * Creates the database file and schema if they don't exist.\n * @param dbPath Optional override for the database file path (used in tests).\n */\nexport function initSessionDb(dbPath?: string): Database.Database {\n if (db) return db;\n\n const finalPath = dbPath ?? resolve(syntaurRoot(), 'syntaur.db');\n db = new Database(finalPath);\n db.pragma('journal_mode = WAL');\n db.exec(SCHEMA_SQL);\n\n // Track schema version\n db.prepare('INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)').run(\n 'schema_version',\n SCHEMA_VERSION,\n );\n\n // Run migrations inside an EXCLUSIVE transaction. This closes two races:\n // 1. Crash between `DROP TABLE` / `RENAME` / `UPDATE meta` leaves the db\n // half-upgraded — the transaction rolls back on failure.\n // 2. Two processes (e.g. `syntaur dashboard` + `syntaur track-session`)\n // both calling initSessionDb() at once — EXCLUSIVE serializes the\n // migration and the version is re-checked inside the transaction so\n // the second process becomes a no-op once the first commits.\n // Narrow for the transaction closure — TS doesn't track the module-level\n // `db` assignment across the closure boundary.\n const database = db;\n const runMigrations = database.transaction(() => {\n // --- v1 → v2: make project/assignment nullable, add description ---\n const vBeforeV2 = (\n database\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined\n )?.value;\n\n if (vBeforeV2 === '1') {\n database.exec(`\n CREATE TABLE sessions_v2 (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;\n DROP TABLE sessions;\n ALTER TABLE sessions_v2 RENAME TO sessions;\n CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\n UPDATE meta SET value = '2' WHERE key = 'schema_version';\n `);\n }\n\n // --- v2 → v3: add transcript_path, normalize legacy mission_slug ---\n // Re-read the version AFTER v1→v2 may have run.\n const vBeforeV3 = (\n database\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined\n )?.value;\n\n if (vBeforeV3 === '2') {\n const v2Columns = database\n .prepare('PRAGMA table_info(sessions)')\n .all() as Array<{ name: string }>;\n const v2ColNames = v2Columns.map((c) => c.name);\n const hasProject = v2ColNames.includes('project_slug');\n const hasMission = v2ColNames.includes('mission_slug');\n\n // If a db somehow has both columns (e.g. a partially-renamed table),\n // prefer project_slug but fall back to mission_slug so rows that only\n // populated mission_slug aren't dropped.\n const projectSlugExpr =\n hasProject && hasMission\n ? 'COALESCE(project_slug, mission_slug)'\n : hasProject\n ? 'project_slug'\n : hasMission\n ? 'mission_slug'\n : null;\n\n if (!projectSlugExpr) {\n throw new Error(\n 'sessions table has neither project_slug nor mission_slug; cannot migrate from v2 to v3',\n );\n }\n\n database.exec(`\n CREATE TABLE sessions_v3 (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n transcript_path TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO sessions_v3\n SELECT session_id, ${projectSlugExpr}, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at\n FROM sessions;\n DROP TABLE sessions;\n ALTER TABLE sessions_v3 RENAME TO sessions;\n CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\n UPDATE meta SET value = '3' WHERE key = 'schema_version';\n `);\n }\n\n // --- v3 → v4: add pid + pid_started_at for liveness detection ---\n const vBeforeV4 = (\n database\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined\n )?.value;\n\n if (vBeforeV4 === '3') {\n database.exec(`\n CREATE TABLE sessions_v4 (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n transcript_path TEXT,\n pid INTEGER,\n pid_started_at TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO sessions_v4\n SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, description, transcript_path, NULL, NULL, created_at, updated_at\n FROM sessions;\n DROP TABLE sessions;\n ALTER TABLE sessions_v4 RENAME TO sessions;\n CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\n UPDATE meta SET value = '4' WHERE key = 'schema_version';\n `);\n }\n\n // --- v4 → v5: add original_head_sha for exact worktree recreation ---\n const vBeforeV5 = (\n database\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined\n )?.value;\n\n if (vBeforeV5 === '4') {\n database.exec(`\n CREATE TABLE sessions_v5 (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n transcript_path TEXT,\n pid INTEGER,\n pid_started_at TEXT,\n original_head_sha TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO sessions_v5\n SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, description, transcript_path, pid, pid_started_at, NULL, created_at, updated_at\n FROM sessions;\n DROP TABLE sessions;\n ALTER TABLE sessions_v5 RENAME TO sessions;\n CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\n UPDATE meta SET value = '5' WHERE key = 'schema_version';\n `);\n }\n });\n runMigrations.exclusive();\n\n // Create project-slug-dependent indexes now that we know the column exists.\n db.exec(POST_MIGRATION_INDEXES_SQL);\n\n return db;\n}\n\n/** True once initSessionDb() has run (and the handle wasn't closed/reset). */\nexport function isSessionDbInitialized(): boolean {\n return db !== null;\n}\n\n/**\n * Get the initialized database handle.\n * Throws if initSessionDb() has not been called.\n */\nexport function getSessionDb(): Database.Database {\n if (!db) {\n throw new Error(\n 'Session database not initialized. Call initSessionDb() first.',\n );\n }\n return db;\n}\n\n/**\n * Close the database connection.\n */\nexport function closeSessionDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Reset the singleton for testing purposes.\n */\nexport function resetSessionDb(): void {\n db = null;\n}\n\n/**\n * One-time migration: import sessions from markdown _index-sessions.md files into SQLite.\n * Only runs if the sessions table is empty and markdown files exist.\n */\nexport async function migrateFromMarkdown(projectsDir: string): Promise<number> {\n const database = getSessionDb();\n\n // Skip if sessions already exist in the database\n const count = database.prepare('SELECT COUNT(*) as count FROM sessions').get() as { count: number };\n if (count.count > 0) return 0;\n\n if (!(await fileExists(projectsDir))) return 0;\n\n const entries = await readdir(projectsDir, { withFileTypes: true });\n const allSessions: AgentSession[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const projectDir = resolve(projectsDir, entry.name);\n const indexPath = resolve(projectDir, '_index-sessions.md');\n if (!(await fileExists(indexPath))) continue;\n\n const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);\n allSessions.push(...sessions);\n }\n\n if (allSessions.length === 0) return 0;\n\n const insert = database.prepare(`\n INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n const insertAll = database.transaction((sessions: AgentSession[]) => {\n for (const s of sessions) {\n insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);\n }\n });\n\n insertAll(allSessions);\n console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);\n return allSessions.length;\n}\n\n/**\n * Parse an _index-sessions.md file into AgentSession objects.\n * Used only for one-time migration. This is a copy of the old parsing logic.\n */\nasync function parseMarkdownSessionsIndex(\n filePath: string,\n projectSlug: string,\n): Promise<AgentSession[]> {\n const { readFile } = await import('node:fs/promises');\n const raw = await readFile(filePath, 'utf-8');\n const sessions: AgentSession[] = [];\n\n const lines = raw.split('\\n');\n let inTable = false;\n let headerSeen = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n if (trimmed.startsWith('| Assignment') || trimmed.startsWith('|Assignment')) {\n inTable = true;\n headerSeen = false;\n continue;\n }\n\n if (inTable && !headerSeen && trimmed.match(/^\\|[-\\s|]+\\|$/)) {\n headerSeen = true;\n continue;\n }\n\n if (inTable && headerSeen && trimmed.startsWith('|')) {\n const cells = trimmed\n .split('|')\n .slice(1, -1)\n .map((c) => c.trim());\n\n if (cells.length >= 6) {\n sessions.push({\n assignmentSlug: cells[0],\n agent: cells[1],\n sessionId: cells[2],\n started: cells[3],\n status: (cells[4] as AgentSessionStatus) || 'active',\n path: cells[5],\n projectSlug,\n });\n }\n }\n }\n\n return sessions;\n}\n","import { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { fileExists } from '../utils/fs.js';\nimport { getSessionDb } from './session-db.js';\nimport type { AgentSession, AgentSessionStatus } from './types.js';\n\ninterface SessionRow {\n session_id: string;\n project_slug: string | null;\n assignment_slug: string | null;\n agent: string;\n started: string;\n ended: string | null;\n status: string;\n path: string | null;\n description: string | null;\n transcript_path: string | null;\n pid: number | null;\n pid_started_at: string | null;\n original_head_sha: string | null;\n updated_at: string | null;\n}\n\nfunction rowToSession(row: SessionRow): AgentSession {\n return {\n sessionId: row.session_id,\n projectSlug: row.project_slug ?? null,\n assignmentSlug: row.assignment_slug ?? null,\n agent: row.agent,\n started: row.started,\n ended: row.ended ?? null,\n status: row.status as AgentSessionStatus,\n path: row.path ?? '',\n description: row.description ?? null,\n transcriptPath: row.transcript_path ?? null,\n pid: row.pid ?? null,\n pidStartedAt: row.pid_started_at ?? null,\n originalHeadSha: row.original_head_sha ?? null,\n updatedAt: row.updated_at ?? null,\n };\n}\n\n/**\n * Query sessions for a specific project.\n */\nexport async function parseSessionsIndex(\n _projectDir: string,\n projectSlug: string,\n): Promise<AgentSession[]> {\n const db = getSessionDb();\n const rows = db\n .prepare('SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC')\n .all(projectSlug) as SessionRow[];\n return rows.map(rowToSession);\n}\n\n/**\n * Upsert a session keyed on `session_id`.\n *\n * On conflict, non-null fields in the new payload fill in missing values on the\n * existing row (COALESCE). `started` / `created_at` from the first insert are\n * preserved. A session already in a terminal state (`completed` / `stopped`)\n * is NOT revived by re-registration — status only moves forward — with one\n * narrow exception: `opts.reviveStopped` lets an `active` payload flip a\n * `stopped` row back to active. Callers may only pass it on live-process\n * evidence (the scanner seeing a process hold the transcript open).\n * `completed` always sticks.\n *\n * Makes registration idempotent across SessionStart hooks, `/track-session`,\n * and grab-assignment all touching the same real session ID.\n */\nexport async function appendSession(\n _projectDir: string,\n session: AgentSession,\n opts?: { reviveStopped?: boolean },\n): Promise<void> {\n const db = getSessionDb();\n db.prepare(`\n INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(session_id) DO UPDATE SET\n project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),\n assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),\n agent = excluded.agent,\n status = CASE\n WHEN status = 'completed' THEN status\n WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status\n ELSE excluded.status\n END,\n path = COALESCE(NULLIF(excluded.path, ''), path),\n description = COALESCE(NULLIF(excluded.description, ''), description),\n transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),\n pid = COALESCE(excluded.pid, pid),\n pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),\n original_head_sha = COALESCE(NULLIF(original_head_sha, ''), NULLIF(excluded.original_head_sha, '')),\n updated_at = datetime('now')\n `).run(\n session.sessionId,\n session.projectSlug ?? null,\n session.assignmentSlug ?? null,\n session.agent,\n session.started,\n session.status,\n session.path,\n session.description ?? null,\n session.transcriptPath ?? null,\n session.pid ?? null,\n session.pidStartedAt ?? null,\n session.originalHeadSha ?? null,\n opts?.reviveStopped ? 1 : 0,\n );\n}\n\n/**\n * Update a session's status by sessionId.\n * Sets `ended` timestamp for terminal statuses (completed, stopped).\n * `endedAt` (ISO 8601) overrides the default `datetime('now')` so sweeps can\n * backdate `ended` to the transcript's last mtime.\n */\nexport async function updateSessionStatus(\n _projectDir: string,\n sessionId: string,\n status: AgentSessionStatus,\n endedAt?: string,\n): Promise<boolean> {\n const db = getSessionDb();\n const isTerminal = status === 'completed' || status === 'stopped';\n\n const result = isTerminal\n ? db\n .prepare(\n 'UPDATE sessions SET status = ?, ended = COALESCE(?, datetime(\\'now\\')), updated_at = datetime(\\'now\\') WHERE session_id = ?',\n )\n .run(status, endedAt ?? null, sessionId)\n : db\n .prepare(\n 'UPDATE sessions SET status = ?, updated_at = datetime(\\'now\\') WHERE session_id = ?',\n )\n .run(status, sessionId);\n\n return result.changes > 0;\n}\n\n/**\n * List all sessions across all projects.\n */\nexport async function listAllSessions(_projectsDir: string): Promise<AgentSession[]> {\n const db = getSessionDb();\n const rows = db\n .prepare('SELECT * FROM sessions ORDER BY started DESC')\n .all() as SessionRow[];\n return rows.map(rowToSession);\n}\n\n/**\n * Fetch a single session by its agent-assigned session id.\n * Returns null when no row matches. Throws if initSessionDb() has not run.\n */\nexport function getSessionById(sessionId: string): AgentSession | null {\n const db = getSessionDb();\n const row = db\n .prepare('SELECT * FROM sessions WHERE session_id = ? LIMIT 1')\n .get(sessionId) as SessionRow | undefined;\n return row ? rowToSession(row) : null;\n}\n\n/**\n * List sessions for a specific project, optionally filtered by assignment.\n */\nexport async function listProjectSessions(\n _projectsDir: string,\n projectSlug: string,\n assignmentSlug?: string,\n): Promise<AgentSession[]> {\n const db = getSessionDb();\n\n if (assignmentSlug) {\n const rows = db\n .prepare(\n 'SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC',\n )\n .all(projectSlug, assignmentSlug) as SessionRow[];\n return rows.map(rowToSession);\n }\n\n const rows = db\n .prepare('SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC')\n .all(projectSlug) as SessionRow[];\n return rows.map(rowToSession);\n}\n\n/**\n * Delete sessions by their IDs. Returns the number of rows deleted.\n */\nexport async function deleteSessions(sessionIds: string[]): Promise<number> {\n if (sessionIds.length === 0) return 0;\n const db = getSessionDb();\n const placeholders = sessionIds.map(() => '?').join(', ');\n const result = db\n .prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`)\n .run(...sessionIds);\n return result.changes;\n}\n\n// Statuses that imply the working session is done (review means agent finished)\nconst DONE_ASSIGNMENT_STATUSES = new Set(['completed', 'failed', 'review']);\n\n/**\n * Read the status field from an assignment.md frontmatter without full parsing.\n */\nasync function readAssignmentStatusFromPath(\n assignmentMdPath: string,\n): Promise<string | null> {\n if (!(await fileExists(assignmentMdPath))) return null;\n const raw = await readFile(assignmentMdPath, 'utf-8');\n const match = raw.match(/^status:\\s*(.+)$/m);\n return match ? match[1].trim() : null;\n}\n\nasync function readAssignmentStatus(\n projectDir: string,\n assignmentSlug: string,\n): Promise<string | null> {\n return readAssignmentStatusFromPath(\n resolve(projectDir, 'assignments', assignmentSlug, 'assignment.md'),\n );\n}\n\n/**\n * Reconcile active sessions against assignment statuses.\n * Sessions whose assignments have moved to completed/failed/review are\n * marked as completed (or stopped for failed assignments).\n * Standalone sessions (project_slug NULL) are resolved via assignmentsDir.\n * Returns the number of sessions that were updated.\n */\nexport async function reconcileActiveSessions(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<number> {\n const db = getSessionDb();\n\n // Include standalone sessions (project_slug NULL) when assignmentsDir is provided.\n const activeSessions = db\n .prepare('SELECT * FROM sessions WHERE status = \\'active\\' AND assignment_slug IS NOT NULL')\n .all() as SessionRow[];\n\n if (activeSessions.length === 0) return 0;\n\n // Read assignment statuses from disk. Key is `${projectSlug ?? '__standalone__'}/${slug}`.\n const assignmentStatuses = new Map<string, string>();\n const seen = new Set<string>();\n for (const session of activeSessions) {\n const aslug = session.assignment_slug;\n if (!aslug) continue;\n\n const projectKey = session.project_slug ?? '__standalone__';\n const key = `${projectKey}/${aslug}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n if (session.project_slug) {\n const status = await readAssignmentStatus(\n resolve(projectsDir, session.project_slug),\n aslug,\n );\n if (status) assignmentStatuses.set(key, status);\n } else if (assignmentsDir) {\n const status = await readAssignmentStatusFromPath(\n resolve(assignmentsDir, aslug, 'assignment.md'),\n );\n if (status) assignmentStatuses.set(key, status);\n }\n }\n\n // Update stale sessions\n let totalUpdated = 0;\n for (const session of activeSessions) {\n const projectKey = session.project_slug ?? '__standalone__';\n const key = `${projectKey}/${session.assignment_slug}`;\n const assignmentStatus = assignmentStatuses.get(key);\n if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;\n\n const newStatus: AgentSessionStatus =\n assignmentStatus === 'failed' ? 'stopped' : 'completed';\n await updateSessionStatus('', session.session_id, newStatus);\n totalUpdated++;\n }\n\n return totalUpdated;\n}\n\n/**\n * List sessions for a resolved assignment (standalone or project-nested).\n * Standalone: filter by assignment_slug = id AND project_slug IS NULL.\n * Project-nested: filter by project_slug + assignment_slug.\n */\nexport async function listSessionsByAssignment(\n projectSlug: string | null,\n assignmentSlug: string,\n): Promise<AgentSession[]> {\n const db = getSessionDb();\n const rows = projectSlug === null\n ? (db\n .prepare(\n 'SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC',\n )\n .all(assignmentSlug) as SessionRow[])\n : (db\n .prepare(\n 'SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC',\n )\n .all(projectSlug, assignmentSlug) as SessionRow[]);\n return rows.map(rowToSession);\n}\n","/**\n * Locked Overview copy. Single source of truth for hero, segment, and dialog strings.\n *\n * Both the backend (`api.ts` hero + reason emission) and the frontend\n * (`OverviewHero`, `OverviewSegment`, etc.) import from this module so copy\n * cannot drift between the API payload and what the UI renders.\n */\n\nexport type HeroCopyKey =\n | 'review'\n | 'review.singular'\n | 'ready_to_implement'\n | 'ready_to_implement.singular'\n | 'ready_for_planning'\n | 'ready_for_planning.singular'\n | 'in_progress'\n | 'in_progress.singular'\n | 'draft'\n | 'draft.singular'\n | 'blocked'\n | 'blocked.singular'\n | 'stale'\n | 'stale.singular'\n | 'clean';\n\n/**\n * Hero strings. The `{total}` and `{title}` placeholders are substituted at\n * render time. Singular variants are used when `total === 1`.\n */\nexport const HERO_COPY: Record<HeroCopyKey, string> = {\n review: '{total} items ready for your review',\n 'review.singular': 'Review {title}',\n ready_to_implement: '{total} plans ready to implement — start with {title}',\n 'ready_to_implement.singular': 'Start implementing {title}',\n ready_for_planning: '{total} assignments ready to plan — start with {title}',\n 'ready_for_planning.singular': 'Plan {title}',\n in_progress: 'Resume {title} ({total} in progress)',\n 'in_progress.singular': 'Resume {title}',\n draft: 'Shape your {total} drafts — start with {title}',\n 'draft.singular': 'Shape {title}',\n blocked: 'Unblock {title} ({total} blocked)',\n 'blocked.singular': 'Unblock {title}',\n stale: 'Triage {total} stale items',\n 'stale.singular': 'Triage {title} — sitting stale',\n clean: 'You’re all clear. Nothing needs you right now.',\n};\n\nexport type SegmentId =\n | 'readyForReview'\n | 'readyToImplement'\n | 'readyForPlanning'\n | 'inProgress'\n | 'drafts'\n | 'blocked'\n | 'newestCreated'\n | 'stale';\n\n/** Per-segment row reason (the one-liner under the title). */\nexport const SEGMENT_REASON: Record<SegmentId, string> = {\n readyForReview: 'Ready for your review',\n readyToImplement: 'Plan finalized — ready to implement',\n readyForPlanning: 'Ready to plan',\n inProgress: 'In progress',\n drafts: 'Draft — needs shape',\n blocked: 'Blocked',\n newestCreated: 'Newly created',\n stale: 'Sitting stale',\n};\n\n/** Per-segment empty state copy. */\nexport const SEGMENT_EMPTY: Record<SegmentId, string> = {\n readyForReview: 'No assignments waiting for your review.',\n readyToImplement: 'No plans queued for implementation.',\n readyForPlanning: 'No assignments waiting to be planned.',\n inProgress: 'Nothing actively in progress.',\n drafts: 'No drafts — ideas captured here will live until they’re shaped.',\n blocked: 'Nothing is blocked. Good.',\n newestCreated: 'No assignments created recently.',\n stale: 'No stale work — everything is fresh.',\n};\n\n/** Per-segment header titles. */\nexport const SEGMENT_TITLE: Record<SegmentId, string> = {\n readyForReview: 'Ready for Review',\n readyToImplement: 'Ready to Implement',\n readyForPlanning: 'Ready for Planning',\n inProgress: 'In Progress',\n drafts: 'Drafts',\n blocked: 'Blocked',\n newestCreated: 'Newest Created',\n stale: 'Stale',\n};\n\n/** Dialog + button copy used across Overview components. */\nexport const DIALOG_COPY = {\n claimAsTitle: 'Claim assignments as',\n claimAsHint: 'Used when you claim an assignment from this dashboard. You can change it later in settings.',\n claimAsSubmit: 'Save',\n claimAsRemember: 'Remember this choice',\n quickCommentTitle: 'Add a quick note',\n quickCommentPlaceholder: 'Note…',\n quickCommentSubmit: 'Post',\n bulkArchiveLabel: 'Archive selected',\n bulkClearLabel: 'Clear',\n bulkPartialFailureBanner: 'Some items failed to archive. The list has been refreshed.',\n emptyStateCleanTitle: 'You’re all clear',\n emptyStateCleanCTA: 'Browse projects',\n draftsHeaderCTA: 'Shape →',\n staleLoadMore: 'Load more',\n staleLoadMoreRemaining: '{remaining} remaining',\n recentSessionsEmptyTitle: 'No recent sessions',\n recentSessionsEmptyHint: 'Use /grab-assignment or `syntaur track-session` to register one.',\n recentSessionsCopyPathLabel: 'Copy path',\n recentSessionsCopyPathDisabled: 'Session has no path',\n recentSessionsCopyFallbackHint: 'Press ⌘C to copy',\n} as const;\n\n/** Substitute `{key}` placeholders in a template. */\nexport function formatCopy(\n template: string,\n vars: Record<string, string | number>,\n): string {\n return template.replace(/\\{(\\w+)\\}/g, (_, key) => String(vars[key] ?? `{${key}}`));\n}\n","/**\n * Needs-attention / staleness classifier (read-only).\n *\n * The ONE place that decides whether an assignment's status has gone stale —\n * i.e. where the status CONTRADICTS reality — and why. Pure and side-effect\n * free: it never reads files, never writes status, never mutates anything. The\n * dashboard overview, the decision inbox, the CLI, and (later) a read-only\n * watchdog all feed it the same struct so the \"stale\" verdict is computed in\n * exactly one place and can't diverge between surfaces.\n *\n * Two hard rules (decision D1):\n * 1. Never timer-PROMOTE — this only flags, it never advances/regresses status.\n * 2. Contradiction + age, never raw age alone. An old timestamp is not\n * staleness; an old timestamp that disagrees with activity/claim/approval\n * is. And when an input is UNKNOWN we fail safe (do NOT flag) rather than\n * manufacture a false positive — e.g. \"no recent session\" is best-effort\n * and never fires on its own.\n */\n\nexport type StaleReasonKind =\n | 'in_progress_no_activity'\n | 'ready_unclaimed'\n | 'review_aging'\n | 'blocked_aging'\n | 'plan_awaiting_approval'\n | 'deps_unsatisfied';\n\nexport interface StaleReason {\n kind: StaleReasonKind;\n /** Human one-liner for display (dashboard/CLI). */\n label: string;\n /** Severity hint for ordering/badging. */\n severity: 'low' | 'medium' | 'high';\n}\n\n/**\n * Per-reason age gates (ms). Defaults are sensible; Task 5 lets config override\n * these keyed on disposition/phase. `deps_unsatisfied` has no age gate (a hard\n * contradiction), so it is intentionally absent here.\n */\nexport interface StaleThresholds {\n inProgressNoActivityMs: number;\n readyUnclaimedMs: number;\n reviewAgingMs: number;\n blockedAgingMs: number;\n planApprovalAgingMs: number;\n}\n\nconst DAY = 24 * 60 * 60 * 1000;\n\nexport const DEFAULT_STALE_THRESHOLDS: StaleThresholds = {\n inProgressNoActivityMs: 7 * DAY,\n readyUnclaimedMs: 3 * DAY,\n reviewAgingMs: 3 * DAY,\n blockedAgingMs: 3 * DAY,\n planApprovalAgingMs: 3 * DAY,\n};\n\n/**\n * Merge user overrides (from the `staleness:` config block) over the defaults.\n * Defaults-first: an absent or partial config keeps every unspecified gate at\n * its default. Non-positive/non-finite overrides are ignored (defensive — the\n * config parser already validates, but a stray value must never disable a gate).\n */\nexport function resolveStaleThresholds(\n overrides?: Partial<StaleThresholds> | null,\n): StaleThresholds {\n const merged = { ...DEFAULT_STALE_THRESHOLDS };\n if (overrides) {\n for (const key of Object.keys(merged) as (keyof StaleThresholds)[]) {\n const v = overrides[key];\n if (typeof v === 'number' && Number.isFinite(v) && v > 0) merged[key] = v;\n }\n }\n return merged;\n}\n\nexport interface NeedsAttentionInput {\n /** Derived phase (draft/ready_for_planning/ready_to_implement/in_progress/review). */\n phase: string | null;\n /** Derived disposition (active/blocked/parked/terminal). */\n disposition: string | null;\n /** Resolved terminal check — caller passes the config-resolved verdict, NOT a\n * hardcoded set, so renamed terminals are honored. */\n isTerminal: boolean;\n assignee: string | null;\n blockedReason: string | null;\n /** null when unknown/not-applicable (standalone, no deps). */\n depsSatisfied: boolean | null;\n planExists: boolean;\n planApproved: boolean;\n /** ms since the last HEADLINE status change. null → no aging reason fires. */\n statusAgeMs: number | null;\n /** ms since the most recent REAL activity (max-recency of progress.md mtime,\n * workspace files, session liveness). null → unknown → activity reasons never\n * fire (fail safe). NEVER assignment `updated` (recompute bumps that). */\n lastActivityMs: number | null;\n}\n\nconst PLANNING_PHASE = 'ready_for_planning';\nconst READY_PHASE = 'ready_to_implement';\nconst IN_PROGRESS_PHASE = 'in_progress';\nconst REVIEW_PHASE = 'review';\n\n/**\n * Classify why (if at all) an assignment needs attention. Returns [] when the\n * status is consistent with reality (or terminal, or inputs are unknown).\n */\nexport function classifyNeedsAttention(\n input: NeedsAttentionInput,\n thresholds: StaleThresholds = DEFAULT_STALE_THRESHOLDS,\n): StaleReason[] {\n if (input.isTerminal) return [];\n\n const reasons: StaleReason[] = [];\n const age = input.statusAgeMs; // null → aging gates below all fail closed\n const aged = (gate: number): boolean => age !== null && age >= gate;\n const blocked = input.disposition === 'blocked' || input.blockedReason !== null;\n\n // in_progress but nothing is actually happening. Requires BOTH an old status\n // AND a known-old activity signal — \"no recent session\" alone never fires.\n if (\n input.phase === IN_PROGRESS_PHASE &&\n !blocked &&\n aged(thresholds.inProgressNoActivityMs) &&\n input.lastActivityMs !== null &&\n input.lastActivityMs >= thresholds.inProgressNoActivityMs\n ) {\n reasons.push({\n kind: 'in_progress_no_activity',\n label: 'In progress, but no recent activity',\n severity: 'medium',\n });\n }\n\n // Ready to implement but nobody has claimed it.\n if (input.phase === READY_PHASE && input.assignee === null && aged(thresholds.readyUnclaimedMs)) {\n reasons.push({\n kind: 'ready_unclaimed',\n label: 'Ready to implement, unclaimed',\n severity: 'medium',\n });\n }\n\n // Waiting on a human review that no one has actioned.\n if (input.phase === REVIEW_PHASE && aged(thresholds.reviewAgingMs)) {\n reasons.push({ kind: 'review_aging', label: 'Awaiting review', severity: 'high' });\n }\n\n // Blocked and aging — the block may be stale.\n if (blocked && aged(thresholds.blockedAgingMs)) {\n reasons.push({ kind: 'blocked_aging', label: 'Blocked and aging', severity: 'high' });\n }\n\n // A plan exists but has sat unapproved.\n if (\n input.phase === PLANNING_PHASE &&\n input.planExists &&\n !input.planApproved &&\n aged(thresholds.planApprovalAgingMs)\n ) {\n reasons.push({\n kind: 'plan_awaiting_approval',\n label: 'Plan awaiting approval',\n severity: 'medium',\n });\n }\n\n // Working (or ready to work) despite unmet dependencies — a hard\n // contradiction, so no age gate. Not raised during planning (not yet\n // actionable) or when deps state is unknown (null).\n if (\n input.depsSatisfied === false &&\n (input.phase === READY_PHASE || input.phase === IN_PROGRESS_PHASE)\n ) {\n reasons.push({ kind: 'deps_unsatisfied', label: 'Unmet dependencies', severity: 'high' });\n }\n\n return reasons;\n}\n","import { readdir, readFile, writeFile, stat } from 'node:fs/promises';\nimport { resolve, dirname, basename } from 'node:path';\nimport { getTargetStatus, DEFAULT_TRANSITION_TABLE, buildTransitionTable } from '../lifecycle/index.js';\nimport { fileExists, writeFileForce } from '../utils/fs.js';\nimport { nowTimestamp } from '../utils/timestamp.js';\nimport {\n readConfig,\n buildDefaultStatusConfig,\n normalizeFactDeclarations,\n toTitleCase,\n type StatusTransition,\n type DeriveConfig,\n type FactDeclaration,\n type RawFactDeclaration,\n} from '../utils/config.js';\nimport { acceptFactDeclarations, buildDeriveRegistry, buildQueryRegistry } from '../lifecycle/derive.js';\nimport type { FieldRegistry } from '../utils/query/index.js';\nimport { resolvePlaybookSlug } from '../utils/playbooks.js';\nimport { migrateLegacyProjectFiles, migrateLegacyArchivedProjects } from '../utils/fs-migration.js';\nimport { resolveAssignmentById, type ResolvedAssignment } from '../utils/assignment-resolver.js';\nimport { latestPlanFile } from '../lifecycle/facts.js';\nimport { invalidateIndex } from '../search/index.js';\n\n/**\n * Thrown by `deleteWorkspace` when references exist and cascade is false.\n * Routers map this to a 409 response carrying the blocker payload.\n */\nexport class WorkspaceBlockedError extends Error {\n readonly blockedBy: { projects: string[]; standalones: string[] };\n constructor(blockedBy: { projects: string[]; standalones: string[] }) {\n super(\n `Workspace is referenced by ${blockedBy.projects.length} project(s) and ${blockedBy.standalones.length} standalone(s).`,\n );\n this.name = 'WorkspaceBlockedError';\n this.blockedBy = blockedBy;\n }\n}\n\n/**\n * Clear a single top-level frontmatter scalar field (regex-replace; assumes\n * the file already starts with `---` and the field exists). Used by the\n * cascade workspace delete to set `workspace:`/`workspaceGroup:` to `null`.\n */\nfunction clearFrontmatterField(content: string, key: string): string {\n const fieldRegex = new RegExp(`^(${escapeRegExp(key)}:)\\\\s*.*$`, 'm');\n return content.replace(fieldRegex, `$1 null`);\n}\n\nfunction setUpdatedField(content: string, value: string): string {\n const fieldRegex = /^(updated:)\\s*.*$/m;\n if (fieldRegex.test(content)) {\n return content.replace(fieldRegex, `$1 \"${value}\"`);\n }\n return content;\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\nimport {\n parseProject,\n parseStatus,\n parseAssignmentFull,\n parsePlan,\n parseScratchpad,\n parseHandoff,\n parseDecisionRecord,\n parseResource,\n parseMemory,\n parsePlaybook,\n parseProgress,\n parseComments,\n extractMermaidGraph,\n} from './parser.js';\nimport { getDashboardHelp } from './help.js';\nimport type {\n ArchiveResponse,\n ArchivedAssignmentItem,\n ArchivedProjectItem,\n AssignmentBoardItem,\n AssignmentDetail,\n AssignmentReference,\n AssignmentSummary,\n AssignmentsBoardResponse,\n AssignmentTransitionAction,\n AttentionItem,\n EditableDocumentResponse,\n EnrichedLink,\n HelpResponse,\n MemoryDetail,\n MemorySummary,\n MemorySummaryWithProject,\n ProjectDetail,\n ProjectSummary,\n OverviewResponse,\n OverviewSegmentId,\n OverviewSegments,\n OverviewHeroRecommendation,\n OverviewHeroKind,\n OverviewSegmentPayload,\n OverviewStaleSegmentPayload,\n ProgressCounts,\n NeedsAttention,\n RecentActivityItem,\n ResourceDetail,\n ResourceSummary,\n ResourceSummaryWithProject,\n PlaybookSummary,\n PlaybookDetail,\n} from './types.js';\nimport { listAllSessions } from './agent-sessions.js';\nimport { SEGMENT_REASON } from './overviewCopy.js';\nimport {\n classifyNeedsAttention,\n resolveStaleThresholds,\n type StaleReason,\n type StaleThresholds,\n} from '../staleness/classify.js';\nimport type { StaleCandidate } from '../staleness/watchdog.js';\n\nconst RECENT_PROJECTS_LIMIT = 6;\nconst RECENT_ACTIVITY_LIMIT = 12;\nconst RECENT_SESSIONS_LIMIT = 10;\nconst NEWEST_CREATED_LIMIT = 5;\nconst SEGMENT_DISPLAY_CAP = 5;\nconst STALE_LIMIT_DEFAULT = 50;\nconst STALE_LIMIT_MAX = 200;\n\n// --- Archive hiding helpers (cascade) ---\n// \"Hidden from normal views\" is enforced in the aggregating/consuming functions,\n// never in the parser or detail builders (those keep returning everything so the\n// Archive page + restore can read archived items).\n\n/** A project is hidden when its real `archived` flag is set. */\nfunction isProjectArchived(p: { archived?: boolean }): boolean {\n return p.archived === true;\n}\n\n/** Drop individually-archived assignments from a list (for normal/active views). */\nfunction activeAssignments<T extends { archived?: boolean }>(items: T[]): T[] {\n return items.filter((item) => item.archived !== true);\n}\n\n// ---------------------------------------------------------------------------\n// Overview perf instrumentation (opt-in via SYNTAUR_PERF_TRACE=1).\n// Used by getOverview() and helpers it calls. Inactive when traces is undefined.\n// ---------------------------------------------------------------------------\n\ninterface TraceEntry {\n label: string;\n ms: number;\n}\n\ninterface OverviewTraces {\n entries: TraceEntry[];\n subPhases: Map<string, number>;\n}\n\nfunction createTraces(): OverviewTraces {\n return { entries: [], subPhases: new Map() };\n}\n\nasync function timed<T>(\n traces: OverviewTraces | undefined,\n label: string,\n fn: () => Promise<T>,\n): Promise<T> {\n if (!traces) return fn();\n const start = performance.now();\n try {\n return await fn();\n } finally {\n traces.entries.push({ label, ms: performance.now() - start });\n }\n}\n\nfunction accumulatePhase(\n traces: OverviewTraces | undefined,\n label: string,\n ms: number,\n): void {\n if (!traces) return;\n traces.subPhases.set(label, (traces.subPhases.get(label) ?? 0) + ms);\n}\n\nfunction emitTrace(traces: OverviewTraces, meta: Record<string, unknown>): void {\n if (process.env.SYNTAUR_PERF_TRACE !== '1') return;\n const totalMs = traces.entries.reduce((sum, entry) => sum + entry.ms, 0);\n const subPhases = Object.fromEntries(traces.subPhases);\n // eslint-disable-next-line no-console\n console.log(\n JSON.stringify({ kind: 'overview-trace', totalMs, phases: traces.entries, subPhases, ...meta }),\n );\n}\n\nconst STATUS_TO_SEGMENT: Readonly<Record<string, OverviewSegmentId>> = {\n review: 'readyForReview',\n ready_to_implement: 'readyToImplement',\n ready_for_planning: 'readyForPlanning',\n in_progress: 'inProgress',\n draft: 'drafts',\n blocked: 'blocked',\n};\n\nconst HERO_PRIORITY: ReadonlyArray<[OverviewSegmentId, OverviewHeroKind]> = [\n ['readyForReview', 'review'],\n ['readyToImplement', 'ready_to_implement'],\n ['readyForPlanning', 'ready_for_planning'],\n ['inProgress', 'in_progress'],\n ['drafts', 'draft'],\n ['blocked', 'blocked'],\n ['stale', 'stale'],\n];\n\ntype AssignmentRecord = ReturnType<typeof parseAssignmentFull>;\n\ninterface ProjectRecord {\n projectPath: string;\n project: ReturnType<typeof parseProject>;\n assignments: AssignmentRecord[];\n summary: ProjectSummary;\n dependencyGraph: string | null;\n}\n\n/** A standalone assignment lives at `<assignmentsDir>/<uuid>/` and has no containing project. */\ninterface StandaloneRecord {\n assignmentDir: string;\n /** The UUID (folder name). */\n id: string;\n record: AssignmentRecord;\n}\n\n// ---------------------------------------------------------------------------\n// Shared records cache (coarse, clear-all).\n//\n// Parsed project records and standalone records are read on every hot read\n// path — /api/overview, /api/projects, /api/assignments, /api/workspaces, plus\n// the server scanner's workspace lookup. The underlying work is a file fan-out\n// (readdir + readFile + parse for every project, assignment, and comments\n// file), which dominates request latency and is badly amplified by corporate\n// EDR/AV that hooks filesystem syscalls. The dashboard server is long-lived, so\n// we cache the parsed snapshot per directory and reuse it across requests.\n//\n// Granularity is deliberately coarse: a whole-snapshot clear-all (not a\n// per-file map). Rebuild cost is one full scan, amortized across every read\n// until the next mutation. In-flight promises are stored (not just resolved\n// values) so concurrent callers de-duplicate onto a single scan, and a rejected\n// scan is dropped so the next call retries rather than caching a failure.\n//\n// Invalidation is the correctness core: there is no single fs choke-point (the\n// write routers mutate via writeFileForce, executeTransition, rm, and worktree\n// helpers), so every mutating router installs `installRecordsInvalidation`,\n// which clears the cache synchronously once each handler resolves. Non-router\n// mutators (deleteWorkspace) call invalidateRecordsCache() directly, and the\n// file watcher clears it for edits made outside the dashboard.\nconst projectRecordsCache = new Map<string, Promise<ProjectRecord[]>>();\nconst standaloneRecordsCache = new Map<string, Promise<StandaloneRecord[]>>();\n\n/** Drop all cached record snapshots. Cheap and idempotent. */\nexport function invalidateRecordsCache(): void {\n projectRecordsCache.clear();\n standaloneRecordsCache.clear();\n // Content-search index shares this invalidation seam: every record mutation\n // (write routers, file watcher, broadcast, deleteWorkspace) funnels here, so\n // clearing the search index alongside keeps `/api/search` consistent with the\n // displayed records. Cheap + idempotent; the next getIndex() rebuilds lazily.\n invalidateIndex();\n}\n\n/**\n * Install synchronous records-cache invalidation on a mutating Express router.\n * Wraps the terminal handler of every post/put/patch/delete route so the cache\n * is cleared in a `finally` once the handler resolves — before the next request\n * can read it. Centralizes invalidation at registration because the handlers\n * have no shared fs write path to hook. Typed structurally to avoid importing\n * express here.\n */\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype RouterMethod = (...args: any[]) => any;\ntype MutatingRouter = Record<'post' | 'put' | 'patch' | 'delete', RouterMethod>;\nexport function installRecordsInvalidation(router: MutatingRouter): void {\n for (const method of ['post', 'put', 'patch', 'delete'] as const) {\n const original = (router[method] as RouterMethod).bind(router);\n router[method] = (path: any, ...handlers: any[]): any => {\n if (handlers.length > 0) {\n const last = handlers[handlers.length - 1] as RouterMethod;\n handlers[handlers.length - 1] = async (req: any, res: any, next: any) => {\n try {\n return await last(req, res, next);\n } finally {\n invalidateRecordsCache();\n }\n };\n }\n return original(path, ...handlers);\n };\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nasync function listStandaloneRecords(assignmentsDir: string | undefined): Promise<StandaloneRecord[]> {\n const key = assignmentsDir ?? '';\n const cached = standaloneRecordsCache.get(key);\n if (cached) return cached;\n const promise = computeStandaloneRecords(assignmentsDir);\n standaloneRecordsCache.set(key, promise);\n promise.catch(() => standaloneRecordsCache.delete(key));\n return promise;\n}\n\nasync function computeStandaloneRecords(assignmentsDir: string | undefined): Promise<StandaloneRecord[]> {\n if (!assignmentsDir) return [];\n if (!(await fileExists(assignmentsDir))) return [];\n\n const entries = await readdir(assignmentsDir, { withFileTypes: true });\n const records: StandaloneRecord[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) continue;\n const assignmentDir = resolve(assignmentsDir, entry.name);\n const assignmentMdPath = resolve(assignmentDir, 'assignment.md');\n if (!(await fileExists(assignmentMdPath))) continue;\n try {\n const content = await readFile(assignmentMdPath, 'utf-8');\n const record = parseAssignmentFull(content);\n records.push({ assignmentDir, id: entry.name, record });\n } catch {\n // skip unreadable\n }\n }\n\n records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));\n return records;\n}\n\nconst DEFAULT_TRANSITION_DEFINITIONS: Array<{\n command: string;\n label: string;\n description: string;\n requiresReason: boolean;\n}> = [\n {\n command: 'start',\n label: 'Start',\n description: 'Move pending or review work into active execution.',\n requiresReason: false,\n },\n {\n command: 'shape',\n label: 'Shape',\n description: 'Promote a draft assignment to ready_for_planning once the Objective and Acceptance Criteria are fleshed out.',\n requiresReason: false,\n },\n {\n command: 'plan-ready',\n label: 'Plan Ready',\n description: 'Promote a ready_for_planning assignment to ready_to_implement after the plan is written and approved.',\n requiresReason: false,\n },\n {\n command: 'implement',\n label: 'Implement',\n description: 'Move a ready_to_implement assignment into in_progress when coding begins.',\n requiresReason: false,\n },\n {\n command: 'review',\n label: 'Send To Review',\n description: 'Mark the assignment ready for inspection.',\n requiresReason: false,\n },\n {\n command: 'complete',\n label: 'Complete',\n description: 'Mark the assignment done.',\n requiresReason: false,\n },\n {\n command: 'block',\n label: 'Block',\n description: 'Record an exceptional blocker and pause work.',\n requiresReason: true,\n },\n {\n command: 'unblock',\n label: 'Unblock',\n description: 'Resume active work after the blocker is cleared.',\n requiresReason: false,\n },\n {\n command: 'fail',\n label: 'Fail',\n description: 'Mark the assignment as failed when it cannot be completed as planned.',\n requiresReason: false,\n },\n {\n command: 'reopen',\n label: 'Reopen',\n description: 'Reopen a completed or failed assignment to resume work.',\n requiresReason: false,\n },\n];\n\nfunction getTransitionDefinitions(config: ResolvedStatusConfig) {\n if (!config.custom) return DEFAULT_TRANSITION_DEFINITIONS;\n // Deduplicate commands from transitions\n const seen = new Set<string>();\n return config.transitions\n .filter((t) => {\n if (seen.has(t.command)) return false;\n seen.add(t.command);\n return true;\n })\n .map((t) => ({\n command: t.command,\n label: t.label ?? toTitleCase(t.command),\n description: t.description ?? `Transition via ${t.command}.`,\n requiresReason: t.requiresReason ?? false,\n }));\n}\n\ninterface ResolvedStatusConfig {\n custom: boolean;\n statuses: Array<{ id: string; label: string; description?: string; color?: string; terminal?: boolean }>;\n order: string[];\n transitions: StatusTransition[];\n transitionTable: Map<string, string>;\n /** RAW transitions as configured (empty when the user declares none — the\n * Settings editor distinguishes \"user customized\" from \"showing defaults\"\n * via {@link transitionsCustom}, same pattern as {@link derive}). Distinct\n * from {@link transitions}, which is materialized with the default table for\n * the runtime transition guards so the board still offers commands. */\n rawTransitions: StatusTransition[];\n transitionsCustom: boolean;\n terminalStatuses: ReadonlySet<string>;\n /** Derive rules as configured (null when the user has none — resolve to\n * DEFAULT_DERIVE_CONFIG at the derivation call site, NOT here, so the\n * Settings writer can distinguish \"user customized\" from \"defaults\"). */\n derive: DeriveConfig | null;\n /** RAW custom-fact declarations (verbatim) — what the Settings writer passes\n * back to `writeStatusConfig` so a Settings save can't silently delete the\n * user's `statuses.facts` (same bug class as `derive`). */\n facts: RawFactDeclaration[] | null;\n /** ACCEPTED declarations (normalize→accept) — drives `customFacts` extraction\n * and the registry; collision-skipped/malformed rows are absent here. */\n factDeclarations: FactDeclaration[];\n /** Derive registry built ONCE per cached resolution from the accepted list —\n * reused across requests so the WeakMap compile-cache stays warm (no\n * per-request registry construction in buildDerivedDetail). */\n deriveRegistry: FieldRegistry;\n /** Query registry built ONCE per cached resolution from the accepted list —\n * sibling of deriveRegistry; stable object identity keeps the WeakMap\n * compile-cache warm across saved-view query validations. */\n queryRegistry: FieldRegistry;\n}\n\nlet _cachedConfig: ResolvedStatusConfig | null = null;\n\nexport async function getStatusConfig(): Promise<ResolvedStatusConfig> {\n if (_cachedConfig) return _cachedConfig;\n\n const config = await readConfig();\n\n if (config.statuses) {\n const sc = config.statuses;\n // A config may declare facts and/or derive rules without any custom status\n // `definitions` (parseStatusConfig now preserves those rather than dropping\n // the whole block). Fall back to the default statuses/order so the board\n // still renders, while the declared facts/derive ride along — same\n // no-silent-deletion contract as the parser.\n const defaults = sc.statuses.length === 0 ? buildDefaultStatusConfig() : null;\n const effectiveStatuses = defaults ? defaults.statuses : sc.statuses;\n const effectiveOrder = defaults ? defaults.order : sc.order;\n const terminalSet = new Set(\n effectiveStatuses.filter((s) => s.terminal).map((s) => s.id),\n );\n // If a user defines custom statuses but omits the `transitions:` block,\n // fall back to default transitions. Without this, buildTransitionTable([])\n // returns an empty Map and getTargetStatus returns null for every command,\n // so the dashboard would show zero available transitions for any assignment.\n // We materialize a FRESH table from DEFAULT_TRANSITION_TABLE entries (rather\n // than reusing the DEFAULT_TRANSITION_TABLE reference) so getTargetStatus\n // takes the custom-config code path and uses `from:command` lookups — this\n // is what enforces \"only emit transitions valid from current status\" for\n // users whose config has custom statuses but default transitions.\n const hasCustomTransitions = sc.transitions.length > 0;\n const effectiveTransitions = hasCustomTransitions\n ? sc.transitions\n : Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {\n const [from, command] = key.split(':');\n return { from, command, to };\n });\n const accepted = acceptFactDeclarations(normalizeFactDeclarations(sc.facts ?? null));\n _cachedConfig = {\n custom: true,\n statuses: effectiveStatuses,\n order: effectiveOrder,\n transitions: effectiveTransitions,\n transitionTable: buildTransitionTable(effectiveTransitions),\n rawTransitions: sc.transitions,\n transitionsCustom: hasCustomTransitions,\n terminalStatuses: terminalSet.size > 0 ? terminalSet : new Set(['completed', 'failed']),\n derive: sc.derive ?? null,\n facts: sc.facts ?? null,\n factDeclarations: accepted,\n deriveRegistry: buildDeriveRegistry(accepted),\n queryRegistry: buildQueryRegistry(accepted),\n };\n } else {\n // Shared default builder so the dashboard and the `syntaur status` CLI\n // resolve identical default statuses/order/transitions (no drift).\n const def = buildDefaultStatusConfig();\n _cachedConfig = {\n custom: false,\n statuses: def.statuses,\n order: def.order,\n transitions: def.transitions,\n transitionTable: DEFAULT_TRANSITION_TABLE,\n // No custom config at all → the Settings editor shows read-only defaults.\n rawTransitions: [],\n transitionsCustom: false,\n terminalStatuses: new Set(['completed', 'failed']),\n derive: null,\n facts: null,\n factDeclarations: [],\n deriveRegistry: buildDeriveRegistry([]),\n queryRegistry: buildQueryRegistry([]),\n };\n }\n\n return _cachedConfig;\n}\n\nexport function clearStatusConfigCache(): void {\n _cachedConfig = null;\n}\n\n/**\n * List all projects with source-first summary data.\n * GET /api/projects\n */\nexport async function listProjects(projectsDir: string): Promise<ProjectSummary[]> {\n const projectRecords = await listProjectRecords(projectsDir);\n // Archived projects are hidden from normal views; they live only on /archive.\n return projectRecords\n .filter((record) => !isProjectArchived(record.summary))\n .map((record) => record.summary);\n}\n\n/**\n * Read the workspace registry file (~/.syntaur/workspaces.json).\n * Returns an array of explicitly registered workspace names.\n */\nasync function readWorkspaceRegistry(projectsDir: string): Promise<string[]> {\n const registryPath = resolve(dirname(projectsDir), 'workspaces.json');\n try {\n const raw = await readFile(registryPath, 'utf-8');\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? parsed.filter((w): w is string => typeof w === 'string') : [];\n } catch {\n return [];\n }\n}\n\nasync function writeWorkspaceRegistry(projectsDir: string, workspaces: string[]): Promise<void> {\n const registryPath = resolve(dirname(projectsDir), 'workspaces.json');\n await writeFile(registryPath, JSON.stringify(workspaces, null, 2) + '\\n', 'utf-8');\n}\n\n/**\n * List all workspaces: merge registry (explicit) with workspaces discovered from\n * project `workspace:` fields and standalone-assignment `workspaceGroup` fields.\n * Standalones with no `workspaceGroup` contribute to `hasUngrouped`.\n * GET /api/workspaces\n */\nexport async function listWorkspaces(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<{ workspaces: string[]; hasUngrouped: boolean }> {\n const [projectRecords, registered, standaloneRecords] = await Promise.all([\n listProjectRecords(projectsDir),\n readWorkspaceRegistry(projectsDir),\n listStandaloneRecords(assignmentsDir),\n ]);\n const workspaceSet = new Set<string>(registered);\n let hasUngrouped = false;\n for (const record of projectRecords) {\n if (record.project.workspace) {\n workspaceSet.add(record.project.workspace);\n } else {\n hasUngrouped = true;\n }\n }\n for (const sr of standaloneRecords) {\n if (sr.record.workspaceGroup) {\n workspaceSet.add(sr.record.workspaceGroup);\n } else {\n hasUngrouped = true;\n }\n }\n const workspaces = Array.from(workspaceSet).sort();\n return { workspaces, hasUngrouped };\n}\n\n/**\n * Expand a workspace name to the usage rows it owns: member project slugs +\n * standalone assignment ids (folder UUIDs). `_ungrouped` selects projects with a\n * null `workspace` and standalones with no `workspaceGroup` (matching the\n * `/api/projects` and `/api/workspaces` semantics). Archived members are\n * excluded (`listProjects` already drops archived projects; standalones are\n * filtered here). The usage router turns the result into a WHERE clause that is\n * the disjoint union of project-scoped and standalone-scoped rows; unattributed\n * rows (`project_slug = '' AND assignment_slug = ''`) are never members.\n */\nexport async function resolveWorkspaceMembers(\n projectsDir: string,\n assignmentsDir: string | undefined,\n workspace: string,\n): Promise<{ projectSlugs: string[]; standaloneAssignmentIds: string[] }> {\n const [projects, standalones] = await Promise.all([\n listProjects(projectsDir), // archived projects already excluded\n listStandaloneRecords(assignmentsDir),\n ]);\n const ungrouped = workspace === '_ungrouped';\n const projectSlugs = projects\n .filter((p) => (ungrouped ? p.workspace === null : p.workspace === workspace))\n .map((p) => p.slug);\n const standaloneAssignmentIds = standalones\n .filter((sr) => sr.record.archived !== true)\n .filter((sr) => (ungrouped ? !sr.record.workspaceGroup : sr.record.workspaceGroup === workspace))\n .map((sr) => sr.id);\n return { projectSlugs, standaloneAssignmentIds };\n}\n\n/**\n * Worktree/branch records for the server scanner's tmux pane auto-linking,\n * derived from the cached records snapshot instead of a second file fan-out\n * (the scanner previously re-read every assignment.md on each cold scan). A\n * `null` projectSlug marks a standalone assignment. By convention a project\n * assignment's folder name equals its slug, and standalone folders are named by\n * UUID, so `assignmentSlug` matches the scanner's prior folder-name behavior.\n */\nexport async function listWorkspaceRecords(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<\n Array<{\n projectSlug: string | null;\n assignmentSlug: string;\n assignmentTitle: string;\n worktreePath: string | null;\n branch: string | null;\n }>\n> {\n const [projectRecords, standaloneRecords] = await Promise.all([\n listProjectRecords(projectsDir),\n listStandaloneRecords(assignmentsDir),\n ]);\n\n const records: Array<{\n projectSlug: string | null;\n assignmentSlug: string;\n assignmentTitle: string;\n worktreePath: string | null;\n branch: string | null;\n }> = [];\n\n for (const project of projectRecords) {\n for (const assignment of project.assignments) {\n records.push({\n projectSlug: project.summary.slug,\n assignmentSlug: assignment.slug,\n assignmentTitle: assignment.title || assignment.slug,\n worktreePath: assignment.workspace.worktreePath ?? null,\n branch: assignment.workspace.branch ?? null,\n });\n }\n }\n\n for (const standalone of standaloneRecords) {\n records.push({\n projectSlug: null,\n assignmentSlug: standalone.id,\n assignmentTitle: standalone.record.title || standalone.id,\n worktreePath: standalone.record.workspace.worktreePath ?? null,\n branch: standalone.record.workspace.branch ?? null,\n });\n }\n\n return records;\n}\n\n/**\n * Create an empty workspace by registering it.\n * POST /api/workspaces\n */\nexport async function createWorkspace(projectsDir: string, name: string): Promise<void> {\n const registered = await readWorkspaceRegistry(projectsDir);\n if (!registered.includes(name)) {\n registered.push(name);\n registered.sort();\n await writeWorkspaceRegistry(projectsDir, registered);\n }\n}\n\n/**\n * Delete a workspace from the registry.\n *\n * Modes:\n * - `cascade: false` (default): if any project or standalone still references\n * this workspace, throw `WorkspaceBlockedError` with the blocker lists.\n * Otherwise remove from the registry.\n * - `cascade: true`: rewrite every referencing project's `workspace:` field\n * and every referencing standalone's `workspaceGroup:` field to `null`,\n * then remove the registry entry.\n *\n * Returns `{ rewroteFiles }` so callers (server.ts) can decide whether the\n * explicit registry-level broadcast is still needed (watchers already emit\n * project-updated/assignment-updated for rewritten files).\n *\n * DELETE /api/workspaces/:name[?cascade=true]\n */\nexport async function deleteWorkspace(\n projectsDir: string,\n name: string,\n opts: { cascade?: boolean; assignmentsDir?: string } = {},\n): Promise<{ rewroteFiles: boolean }> {\n const cascade = Boolean(opts.cascade);\n const projectRecords = await listProjectRecords(projectsDir);\n const standaloneRecords = await listStandaloneRecords(opts.assignmentsDir);\n\n const projectsReferencing = projectRecords\n .filter((record) => record.project.workspace === name)\n .map((record) => record.project.slug);\n const standalonesReferencing = standaloneRecords\n .filter((record) => record.record.workspaceGroup === name)\n .map((record) => record.id);\n\n if (projectsReferencing.length + standalonesReferencing.length > 0 && !cascade) {\n throw new WorkspaceBlockedError({\n projects: projectsReferencing,\n standalones: standalonesReferencing,\n });\n }\n\n let rewroteFiles = false;\n if (cascade) {\n const timestamp = nowTimestamp();\n\n for (const slug of projectsReferencing) {\n const path = resolve(projectsDir, slug, 'project.md');\n const raw = await readFile(path, 'utf-8');\n let next = clearFrontmatterField(raw, 'workspace');\n next = setUpdatedField(next, timestamp);\n await writeFileForce(path, next);\n rewroteFiles = true;\n }\n\n for (const id of standalonesReferencing) {\n if (!opts.assignmentsDir) break;\n const path = resolve(opts.assignmentsDir, id, 'assignment.md');\n const raw = await readFile(path, 'utf-8');\n let next = clearFrontmatterField(raw, 'workspaceGroup');\n next = setUpdatedField(next, timestamp);\n await writeFileForce(path, next);\n rewroteFiles = true;\n }\n }\n\n const registered = await readWorkspaceRegistry(projectsDir);\n const filtered = registered.filter((w) => w !== name);\n await writeWorkspaceRegistry(projectsDir, filtered);\n\n // Cascade rewrote project/assignment frontmatter (workspace fields), so the\n // cached records snapshot is stale. This is a library function invoked\n // directly by a server.ts route (outside the write router), so invalidate\n // here rather than relying on a router wrapper.\n if (rewroteFiles) {\n invalidateRecordsCache();\n }\n\n return { rewroteFiles };\n}\n\n/**\n * Get overview data used by the app landing page.\n * GET /api/overview?staleLimit=&staleOffset=\n */\nexport async function getOverview(\n projectsDir: string,\n serversDir?: string,\n assignmentsDir?: string,\n options: { staleLimit?: number; staleOffset?: number } = {},\n): Promise<OverviewResponse> {\n const traceEnabled = process.env.SYNTAUR_PERF_TRACE === '1';\n const traces: OverviewTraces | undefined = traceEnabled ? createTraces() : undefined;\n const overallStart = traceEnabled ? performance.now() : 0;\n\n const projectRecords = await timed(traces, 'list-project-records', () =>\n listProjectRecords(projectsDir, traces),\n );\n const standaloneRecords = await timed(traces, 'list-standalone-records', () =>\n listStandaloneRecords(assignmentsDir),\n );\n // Archived projects + individually-archived assignments are hidden from every\n // overview aggregate (stats, recent projects, recent activity). The full record\n // sets are still used for firstRun detection and the segment-bucket builder\n // (which applies its own cascade filtering internally).\n const activeProjectRecords = projectRecords.filter((record) => !isProjectArchived(record.summary));\n const activeStandaloneRecords = standaloneRecords.filter((sr) => sr.record.archived !== true);\n const recentActivity = buildRecentActivity(activeProjectRecords, activeStandaloneRecords);\n\n const staleLimit = clamp(\n Number.isFinite(options.staleLimit) ? Number(options.staleLimit) : STALE_LIMIT_DEFAULT,\n 1,\n STALE_LIMIT_MAX,\n );\n const staleOffset = Math.max(0, Number.isFinite(options.staleOffset) ? Number(options.staleOffset) : 0);\n\n const buckets = await timed(traces, 'build-segment-buckets', () =>\n buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords, traces),\n );\n const segments = toOverviewSegments(buckets, { staleLimit, staleOffset });\n const hero = pickOverviewHero(buckets);\n\n let recentSessions: OverviewResponse['recentSessions'] = [];\n try {\n const all = await timed(traces, 'list-recent-sessions', () => listAllSessions(projectsDir));\n recentSessions = all.slice(0, RECENT_SESSIONS_LIMIT);\n } catch {\n // Sessions failure should not break overview.\n }\n\n let serverStats: OverviewResponse['serverStats'];\n if (serversDir) {\n try {\n const { scanAllSessions } = await import('./scanner.js');\n const servers = await timed(traces, 'scan-tmux-sessions', () =>\n // Overview only needs aggregate counts — never block its render on a\n // live scan; serve last-known stats and let the scan refresh in the\n // background (stale-while-revalidate).\n scanAllSessions(serversDir, projectsDir, { assignmentsDir, nonBlocking: true }),\n );\n if (servers.tmuxAvailable) {\n const alive = servers.sessions.filter(s => s.alive).length;\n const totalPorts = servers.sessions.reduce((sum, s) =>\n sum + s.windows.reduce((ws, w) =>\n ws + w.panes.reduce((ps, p) => ps + p.ports.length, 0), 0), 0);\n serverStats = {\n trackedSessions: servers.sessions.length,\n aliveSessions: alive,\n deadSessions: servers.sessions.length - alive,\n totalPorts,\n };\n }\n } catch {\n // Server scanning failure should not break overview\n }\n }\n\n if (traces) {\n const wallMs = performance.now() - overallStart;\n const totalAssignments =\n projectRecords.reduce((sum, r) => sum + r.assignments.length, 0) + standaloneRecords.length;\n emitTrace(traces, {\n wallMs,\n fixture: { projects: projectRecords.length, assignments: totalAssignments },\n });\n }\n\n return {\n generatedAt: new Date().toISOString(),\n firstRun: projectRecords.length === 0 && standaloneRecords.length === 0,\n stats: {\n activeProjects: activeProjectRecords.filter((record) => record.summary.status === 'active').length,\n inProgressAssignments: activeProjectRecords.reduce(\n (total, record) => total + (record.summary.progress['in_progress'] ?? 0),\n 0,\n ),\n blockedAssignments: activeProjectRecords.reduce(\n (total, record) => total + (record.summary.progress['blocked'] ?? 0),\n 0,\n ),\n reviewAssignments: activeProjectRecords.reduce(\n (total, record) => total + (record.summary.progress['review'] ?? 0),\n 0,\n ),\n failedAssignments: activeProjectRecords.reduce(\n (total, record) => total + (record.summary.progress['failed'] ?? 0),\n 0,\n ),\n // Derived from the SAME classifier verdict as the stale segment (via the\n // pre-cap segment total) so the badge count can never diverge from the\n // listed rows.\n staleAssignments: segments.stale.total,\n },\n hero,\n segments,\n recentSessions,\n recentProjects: activeProjectRecords\n .map((record) => record.summary)\n .sort((left, right) => compareTimestamps(right.updated, left.updated))\n .slice(0, RECENT_PROJECTS_LIMIT),\n recentActivity: recentActivity.slice(0, RECENT_ACTIVITY_LIMIT),\n serverStats,\n };\n}\n\n/**\n * Get all assignments across all projects for the global kanban board.\n * GET /api/assignments\n */\nexport async function listAssignmentsBoard(\n projectsDir: string,\n assignmentsDir?: string,\n options: { archived?: 'exclude' | 'only' } = {},\n): Promise<AssignmentsBoardResponse> {\n const mode = options.archived ?? 'exclude';\n const projectRecords = await listProjectRecords(projectsDir);\n const projectItems = await Promise.all(\n projectRecords.flatMap(async (record) => {\n if (mode === 'only') {\n // Individually-archived assignments only — ignore project-archived cascade.\n return Promise.all(\n record.assignments\n .filter((assignment) => assignment.archived === true)\n .map(async (assignment) => toAssignmentBoardItem(projectsDir, record, assignment)),\n );\n }\n // 'exclude': cascade-hide every child of an archived project, and drop\n // individually-archived children of non-archived projects.\n if (isProjectArchived(record.summary)) return [] as AssignmentBoardItem[];\n return Promise.all(\n activeAssignments(record.assignments).map(async (assignment) =>\n toAssignmentBoardItem(projectsDir, record, assignment),\n ),\n );\n }),\n );\n\n const standaloneRecords = await listStandaloneRecords(assignmentsDir);\n const filteredStandalone =\n mode === 'only'\n ? standaloneRecords.filter((sr) => sr.record.archived === true)\n : standaloneRecords.filter((sr) => sr.record.archived !== true);\n const standaloneItems = await Promise.all(\n filteredStandalone.map(async (sr) => toStandaloneBoardItem(sr)),\n );\n\n return {\n generatedAt: new Date().toISOString(),\n assignments: [...projectItems.flat(), ...standaloneItems]\n .sort((left, right) => compareTimestamps(right.updated, left.updated)),\n };\n}\n\nfunction toArchivedAssignmentItem(\n assignment: AssignmentRecord,\n projectSlug: string | null,\n projectTitle: string | null,\n): ArchivedAssignmentItem {\n return {\n id: assignment.id,\n slug: assignment.slug,\n title: assignment.title,\n status: assignment.status,\n type: assignment.type,\n priority: assignment.priority as ArchivedAssignmentItem['priority'],\n projectSlug,\n projectTitle,\n archived: assignment.archived,\n archivedAt: assignment.archivedAt,\n archivedReason: assignment.archivedReason,\n updated: assignment.updated,\n };\n}\n\n/**\n * Build the canonical archived view for the dashboard Archive page.\n * Returns archived projects (each expandable to ALL its children) plus\n * individually-archived assignments whose parent project is NOT archived\n * (so they are never double-listed) and archived standalone assignments.\n * GET /api/archived\n */\nexport async function listArchived(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<ArchiveResponse> {\n const projectRecords = await listProjectRecords(projectsDir);\n const standaloneRecords = await listStandaloneRecords(assignmentsDir);\n\n const projects: ArchivedProjectItem[] = projectRecords\n .filter((record) => isProjectArchived(record.summary))\n .map((record) => ({\n slug: record.summary.slug,\n title: record.summary.title,\n archivedAt: record.summary.archivedAt,\n archivedReason: record.summary.archivedReason,\n assignments: record.assignments\n .map((assignment) =>\n toArchivedAssignmentItem(assignment, record.summary.slug, record.summary.title),\n )\n .sort((left, right) => compareTimestamps(right.updated, left.updated)),\n }))\n .sort((left, right) => compareTimestamps(right.archivedAt ?? '', left.archivedAt ?? ''));\n\n const individuallyArchived: ArchivedAssignmentItem[] = [];\n for (const record of projectRecords) {\n if (isProjectArchived(record.summary)) continue; // its children belong under the project above\n for (const assignment of record.assignments) {\n if (assignment.archived === true) {\n individuallyArchived.push(\n toArchivedAssignmentItem(assignment, record.summary.slug, record.summary.title),\n );\n }\n }\n }\n for (const sr of standaloneRecords) {\n if (sr.record.archived === true) {\n individuallyArchived.push(toArchivedAssignmentItem(sr.record, null, null));\n }\n }\n individuallyArchived.sort((left, right) => compareTimestamps(right.updated, left.updated));\n\n return { projects, assignments: individuallyArchived };\n}\n\nasync function toStandaloneBoardItem(sr: StandaloneRecord): Promise<AssignmentBoardItem> {\n const config = await getStatusConfig();\n const { terminalStatuses } = config;\n\n let facts: AssignmentBoardItem['facts'];\n try {\n const { computeFacts } = await import('../lifecycle/facts.js');\n facts = await computeFacts({\n assignmentDir: sr.assignmentDir,\n frontmatter: sr.record as unknown as import('../lifecycle/types.js').AssignmentFrontmatter,\n body: sr.record.body,\n projectDir: null,\n terminalStatuses,\n declarations: config.factDeclarations,\n });\n } catch (err) {\n console.warn(`toStandaloneBoardItem: computeFacts failed for ${sr.assignmentDir}:`, err);\n }\n\n return {\n ...toAssignmentSummary(sr.record, terminalStatuses),\n projectSlug: null,\n projectTitle: null,\n blockedReason: sr.record.blockedReason,\n projectWorkspace: sr.record.workspaceGroup ?? null,\n availableTransitions: await getStandaloneAvailableTransitions(sr.record),\n facts,\n };\n}\n\nasync function getStandaloneAvailableTransitions(\n assignment: AssignmentRecord,\n): Promise<AssignmentTransitionAction[]> {\n // Standalone assignments have no dependencies, so skip dependency gating.\n const config = await getStatusConfig();\n const transitionDefs = getTransitionDefinitions(config);\n const actions: AssignmentTransitionAction[] = [];\n\n for (const definition of transitionDefs) {\n const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);\n // Only valid transitions reach the client; the kanban inline picker renders them directly.\n if (target === null) continue;\n\n let warning: string | null = null;\n if (definition.command === 'start' && !assignment.assignee) {\n warning = 'No assignee set — consider assigning before starting.';\n }\n actions.push({\n command: definition.command,\n label: definition.label,\n description: definition.description,\n targetStatus: target,\n disabled: false,\n disabledReason: null,\n warning,\n requiresReason: definition.requiresReason,\n });\n }\n\n return actions;\n}\n\n/**\n * Get the structured help model used by Help and onboarding surfaces.\n * GET /api/help\n */\nexport async function getHelp(): Promise<HelpResponse> {\n return getDashboardHelp();\n}\n\n/**\n * Get a raw editable document for dashboard editor pages.\n */\nexport async function getEditableDocument(\n projectsDir: string,\n documentType: EditableDocumentResponse['documentType'],\n projectSlug: string,\n assignmentSlug?: string,\n): Promise<EditableDocumentResponse | null> {\n const filePath = getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug);\n if (!filePath || !(await fileExists(filePath))) {\n return null;\n }\n\n const content = await readFile(filePath, 'utf-8');\n const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);\n\n return {\n documentType,\n title,\n content,\n projectSlug,\n assignmentSlug,\n appendOnly: documentType === 'handoff' || documentType === 'decision-record',\n };\n}\n\n/**\n * Resolve an assignment by UUID (standalone or project-nested) and return its\n * editable document payload for the given type.\n */\nexport async function getEditableDocumentById(\n projectsDir: string,\n assignmentsDir: string,\n documentType: EditableDocumentResponse['documentType'],\n id: string,\n): Promise<EditableDocumentResponse | null> {\n const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);\n if (!resolved) return null;\n\n if (!resolved.standalone && resolved.projectSlug) {\n return getEditableDocument(\n projectsDir,\n documentType,\n resolved.projectSlug,\n resolved.assignmentSlug,\n );\n }\n\n const fileName =\n documentType === 'assignment'\n ? 'assignment.md'\n : documentType === 'plan'\n ? 'plan.md'\n : documentType === 'scratchpad'\n ? 'scratchpad.md'\n : documentType === 'handoff'\n ? 'handoff.md'\n : documentType === 'decision-record'\n ? 'decision-record.md'\n : null;\n if (!fileName) return null;\n const filePath = resolve(resolved.assignmentDir, fileName);\n if (!(await fileExists(filePath))) return null;\n\n const content = await readFile(filePath, 'utf-8');\n const label = resolved.id;\n const title =\n documentType === 'assignment'\n ? `Edit Assignment: ${label}`\n : documentType === 'plan'\n ? `Edit Plan: ${label}`\n : documentType === 'scratchpad'\n ? `Edit Scratchpad: ${label}`\n : documentType === 'handoff'\n ? `Append Handoff: ${label}`\n : `Append Decision: ${label}`;\n\n return {\n documentType,\n title,\n content,\n projectSlug: null,\n assignmentSlug: undefined,\n assignmentId: resolved.id,\n appendOnly: documentType === 'handoff' || documentType === 'decision-record',\n };\n}\n\n/**\n * Get full project detail with assignments, resources, and memories.\n * GET /api/projects/:slug\n */\nexport async function getProjectDetail(\n projectsDir: string,\n slug: string,\n): Promise<ProjectDetail | null> {\n const projectPath = resolve(projectsDir, slug);\n const projectMdPath = resolve(projectPath, 'project.md');\n\n if (!(await fileExists(projectMdPath))) {\n return null;\n }\n\n const projectContent = await readFile(projectMdPath, 'utf-8');\n const project = parseProject(projectContent);\n const assignments = await listAssignmentRecords(projectPath);\n const rollup = await buildProjectRollup(projectPath, project, assignments);\n const dependencyGraph = await loadDependencyGraph(projectPath, assignments);\n const resources = await listResources(projectPath);\n const memories = await listMemories(projectPath);\n // Consistent with the project summary: the activity timestamp ignores archived\n // children so archiving an old assignment doesn't bump it.\n const updated = getProjectActivityTimestamp(project.updated, activeAssignments(assignments));\n const { terminalStatuses } = await getStatusConfig();\n\n return {\n slug: project.slug || slug,\n title: project.title,\n status: rollup.status,\n statusOverride: project.statusOverride,\n archived: project.archived,\n archivedAt: project.archivedAt,\n archivedReason: project.archivedReason,\n created: project.created,\n updated,\n tags: project.tags,\n externalIds: project.externalIds,\n body: project.body,\n progress: rollup.progress,\n needsAttention: rollup.needsAttention,\n assignments: assignments\n .map((a) => toAssignmentSummary(a, terminalStatuses))\n .sort((left, right) => compareTimestamps(right.updated, left.updated)),\n resources,\n memories,\n dependencyGraph,\n workspace: project.workspace,\n repositories: project.repositories,\n };\n}\n\n/**\n * Get full assignment detail with plan, scratchpad, handoff, and decision record.\n * GET /api/projects/:slug/assignments/:aslug\n */\nexport async function getAssignmentDetail(\n projectsDir: string,\n projectSlug: string,\n assignmentSlug: string,\n): Promise<AssignmentDetail | null> {\n const assignmentDir = resolve(projectsDir, projectSlug, 'assignments', assignmentSlug);\n const assignmentMdPath = resolve(assignmentDir, 'assignment.md');\n\n if (!(await fileExists(assignmentMdPath))) {\n return null;\n }\n\n const assignmentContent = await readFile(assignmentMdPath, 'utf-8');\n const assignment = parseAssignmentFull(assignmentContent);\n\n let projectWorkspace: string | null = null;\n const projectMdPath = resolve(projectsDir, projectSlug, 'project.md');\n if (await fileExists(projectMdPath)) {\n const projectContent = await readFile(projectMdPath, 'utf-8');\n projectWorkspace = parseProject(projectContent).workspace;\n }\n\n let plan: AssignmentDetail['plan'] = null;\n const planFile = await latestPlanFile(assignmentDir);\n if (planFile) {\n const planPath = resolve(assignmentDir, planFile);\n if (await fileExists(planPath)) {\n const planContent = await readFile(planPath, 'utf-8');\n const parsed = parsePlan(planContent);\n plan = {\n status: parsed.status,\n updated: parsed.updated,\n body: parsed.body,\n };\n }\n }\n\n let scratchpad: AssignmentDetail['scratchpad'] = null;\n const scratchpadPath = resolve(assignmentDir, 'scratchpad.md');\n if (await fileExists(scratchpadPath)) {\n const scratchpadContent = await readFile(scratchpadPath, 'utf-8');\n const parsed = parseScratchpad(scratchpadContent);\n scratchpad = {\n updated: parsed.updated,\n body: parsed.body,\n };\n }\n\n let handoff: AssignmentDetail['handoff'] = null;\n const handoffPath = resolve(assignmentDir, 'handoff.md');\n if (await fileExists(handoffPath)) {\n const handoffContent = await readFile(handoffPath, 'utf-8');\n const parsed = parseHandoff(handoffContent);\n handoff = {\n updated: parsed.updated,\n handoffCount: parsed.handoffCount,\n body: parsed.body,\n };\n }\n\n let decisionRecord: AssignmentDetail['decisionRecord'] = null;\n const decisionRecordPath = resolve(assignmentDir, 'decision-record.md');\n if (await fileExists(decisionRecordPath)) {\n const decisionRecordContent = await readFile(decisionRecordPath, 'utf-8');\n const parsed = parseDecisionRecord(decisionRecordContent);\n decisionRecord = {\n updated: parsed.updated,\n decisionCount: parsed.decisionCount,\n body: parsed.body,\n };\n }\n\n let progress: AssignmentDetail['progress'] = null;\n const progressPath = resolve(assignmentDir, 'progress.md');\n if (await fileExists(progressPath)) {\n const progressContent = await readFile(progressPath, 'utf-8');\n const parsed = parseProgress(progressContent);\n progress = {\n updated: parsed.updated,\n entryCount: parsed.entryCount,\n entries: parsed.entries,\n };\n }\n\n let comments: AssignmentDetail['comments'] = null;\n const commentsPath = resolve(assignmentDir, 'comments.md');\n if (await fileExists(commentsPath)) {\n const commentsContent = await readFile(commentsPath, 'utf-8');\n const parsed = parseComments(commentsContent);\n comments = {\n updated: parsed.updated,\n entryCount: parsed.entryCount,\n entries: parsed.entries,\n };\n }\n\n const { terminalStatuses } = await getStatusConfig();\n const detail: AssignmentDetail = {\n id: assignment.id,\n projectSlug,\n slug: assignment.slug || assignmentSlug,\n title: assignment.title,\n status: assignment.status,\n type: assignment.type,\n priority: assignment.priority as AssignmentDetail['priority'],\n assignee: assignment.assignee,\n dependsOn: assignment.dependsOn,\n links: assignment.links,\n reverseLinks: [],\n enrichedLinks: [],\n blockedReason: assignment.blockedReason,\n workspace: assignment.workspace,\n projectWorkspace,\n externalIds: assignment.externalIds,\n tags: assignment.tags,\n archived: assignment.archived,\n archivedAt: assignment.archivedAt,\n archivedReason: assignment.archivedReason,\n ...deriveStatusVirtuals(assignment, terminalStatuses),\n override: assignment.override,\n derived: await buildDerivedDetail(assignment, assignmentDir, resolve(projectsDir, projectSlug)),\n created: assignment.created,\n updated: assignment.updated,\n body: assignment.body,\n plan,\n scratchpad,\n handoff,\n decisionRecord,\n progress,\n comments,\n referencedBy: [],\n availableTransitions: await getAvailableTransitions(\n projectsDir,\n projectSlug,\n assignmentSlug,\n assignment,\n ),\n };\n\n // Compute reverse links and enrich all links\n const selfSlug = `${projectSlug}/${detail.slug}`;\n const projectRecords = await listProjectRecords(projectsDir);\n\n // Find reverse links: assignments across all projects whose links contain this assignment\n const reverseLinks: string[] = [];\n for (const mr of projectRecords) {\n for (const a of mr.assignments) {\n const qualifiedSlug = `${mr.summary.slug}/${a.slug}`;\n if (qualifiedSlug === selfSlug) continue; // skip self\n if (a.links.includes(selfSlug)) {\n reverseLinks.push(qualifiedSlug);\n }\n }\n }\n\n // Filter self-links and malformed links from forward links\n const isValidLinkFormat = (l: string) => {\n const parts = l.split('/');\n return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;\n };\n const forwardLinks = assignment.links.filter((l) => l !== selfSlug && isValidLinkFormat(l));\n\n // Deduplicate: if a slug is in both forward and reverse, keep in forward only\n const forwardSet = new Set(forwardLinks);\n const dedupedReverseLinks = reverseLinks.filter((l) => !forwardSet.has(l));\n\n detail.links = forwardLinks;\n detail.reverseLinks = dedupedReverseLinks;\n\n // Build enriched links for the frontend\n const allProjectAssignments = new Map<string, { title: string; status: string }>();\n for (const mr of projectRecords) {\n for (const a of mr.assignments) {\n allProjectAssignments.set(`${mr.summary.slug}/${a.slug}`, {\n title: a.title,\n status: a.status,\n });\n }\n }\n\n const enrichedLinks: EnrichedLink[] = [];\n for (const linkSlug of forwardLinks) {\n const [ms, as] = linkSlug.split('/');\n const info = allProjectAssignments.get(linkSlug);\n enrichedLinks.push({\n slug: linkSlug,\n projectSlug: ms,\n assignmentSlug: as,\n title: info?.title ?? linkSlug,\n status: info?.status ?? 'pending',\n isReverse: false,\n });\n }\n for (const linkSlug of dedupedReverseLinks) {\n const [ms, as] = linkSlug.split('/');\n const info = allProjectAssignments.get(linkSlug);\n enrichedLinks.push({\n slug: linkSlug,\n projectSlug: ms,\n assignmentSlug: as,\n title: info?.title ?? linkSlug,\n status: info?.status ?? 'pending',\n isReverse: true,\n });\n }\n\n detail.enrichedLinks = enrichedLinks;\n\n // Populate referencedBy — assignments that mention this one.\n detail.referencedBy = await computeReferencedBy(\n { id: assignment.id, projectSlug, slug: detail.slug },\n projectsDir,\n undefined,\n );\n\n return detail;\n}\n\nconst REFERENCED_BY_LIMIT = 50;\n\ninterface ReferenceTarget {\n id: string;\n projectSlug: string | null;\n slug: string;\n}\n\n/**\n * Scan every *other* assignment's Todos, progress, comments, and handoff bodies\n * for markdown links that resolve to `target`, and return an aggregated per-source\n * count (capped at 50).\n */\nasync function computeReferencedBy(\n target: ReferenceTarget,\n projectsDir: string,\n assignmentsDir: string | undefined,\n): Promise<AssignmentReference[]> {\n const sources: Array<{\n id: string;\n slug: string;\n title: string;\n projectSlug: string | null;\n assignmentDir: string;\n }> = [];\n\n // project-nested\n const projectRecords = await listProjectRecords(projectsDir);\n for (const rec of projectRecords) {\n for (const a of rec.assignments) {\n sources.push({\n id: a.id,\n slug: a.slug,\n title: a.title,\n projectSlug: rec.summary.slug,\n assignmentDir: resolve(rec.projectPath, 'assignments', a.slug),\n });\n }\n }\n // standalone\n const standaloneRecords = await listStandaloneRecords(assignmentsDir);\n for (const sr of standaloneRecords) {\n sources.push({\n id: sr.id,\n slug: sr.record.slug || sr.id,\n title: sr.record.title,\n projectSlug: null,\n assignmentDir: sr.assignmentDir,\n });\n }\n\n const references: AssignmentReference[] = [];\n for (const source of sources) {\n if (source.id === target.id) continue; // skip self\n const mentions = await countMentionsInAssignment(source.assignmentDir, target);\n if (mentions > 0) {\n references.push({\n sourceId: source.id,\n sourceSlug: source.slug,\n sourceTitle: source.title,\n sourceProjectSlug: source.projectSlug,\n mentions,\n });\n }\n if (references.length >= REFERENCED_BY_LIMIT) break;\n }\n\n return references.slice(0, REFERENCED_BY_LIMIT);\n}\n\nasync function countMentionsInAssignment(\n sourceDir: string,\n target: ReferenceTarget,\n): Promise<number> {\n const bodies: string[] = [];\n\n // Todos section (from assignment.md)\n const assignmentMd = resolve(sourceDir, 'assignment.md');\n if (await fileExists(assignmentMd)) {\n const content = await readFile(assignmentMd, 'utf-8');\n const todosMatch = content.match(/^## Todos\\s*$([\\s\\S]*?)(?=^## |$(?![\\r\\n]))/m);\n if (todosMatch) bodies.push(todosMatch[1]);\n }\n\n for (const filename of ['progress.md', 'comments.md', 'handoff.md']) {\n const path = resolve(sourceDir, filename);\n if (await fileExists(path)) {\n try {\n bodies.push(await readFile(path, 'utf-8'));\n } catch {\n // ignore\n }\n }\n }\n\n let total = 0;\n const patterns = buildLinkPatternsForTarget(target);\n for (const body of bodies) {\n for (const pattern of patterns) {\n const matches = body.match(pattern);\n if (matches) total += matches.length;\n }\n }\n return total;\n}\n\nfunction buildLinkPatternsForTarget(target: ReferenceTarget): RegExp[] {\n const patterns: RegExp[] = [];\n // Standalone absolute route\n patterns.push(new RegExp(`/assignments/${escapeRegExpLocal(target.id)}(?:/|\\\\b)`, 'g'));\n if (target.projectSlug) {\n // Project-nested absolute route\n patterns.push(\n new RegExp(\n `/projects/${escapeRegExpLocal(target.projectSlug)}/assignments/${escapeRegExpLocal(target.slug)}(?:/|\\\\b)`,\n 'g',\n ),\n );\n // Project-nested relative route\n patterns.push(\n new RegExp(`\\\\.\\\\./${escapeRegExpLocal(target.slug)}(?:/|\\\\b)`, 'g'),\n );\n }\n return patterns;\n}\n\nfunction escapeRegExpLocal(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Resolve an assignment by UUID (standalone or project-nested) and return its full detail payload.\n * GET /api/assignments/:id\n */\nexport async function getAssignmentDetailById(\n projectsDir: string,\n assignmentsDir: string,\n id: string,\n): Promise<AssignmentDetail | null> {\n const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);\n if (!resolved) return null;\n\n if (!resolved.standalone && resolved.projectSlug) {\n // Use the standard detail fetcher, then also scan standalone assignments\n // for backlinks.\n const detail = await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);\n if (!detail) return null;\n detail.referencedBy = await computeReferencedBy(\n { id: detail.id, projectSlug: detail.projectSlug, slug: detail.slug },\n projectsDir,\n assignmentsDir,\n );\n return detail;\n }\n\n // Standalone path — load companion docs directly from the resolved dir.\n const standaloneDetail = await buildStandaloneAssignmentDetail(resolved);\n if (!standaloneDetail) return null;\n standaloneDetail.referencedBy = await computeReferencedBy(\n { id: standaloneDetail.id, projectSlug: null, slug: standaloneDetail.slug },\n projectsDir,\n assignmentsDir,\n );\n return standaloneDetail;\n}\n\nasync function buildStandaloneAssignmentDetail(\n resolved: ResolvedAssignment,\n): Promise<AssignmentDetail | null> {\n const assignmentDir = resolved.assignmentDir;\n const assignmentMdPath = resolve(assignmentDir, 'assignment.md');\n if (!(await fileExists(assignmentMdPath))) return null;\n\n const assignmentContent = await readFile(assignmentMdPath, 'utf-8');\n const assignment = parseAssignmentFull(assignmentContent);\n\n let plan: AssignmentDetail['plan'] = null;\n const planFile = await latestPlanFile(assignmentDir);\n if (planFile) {\n const planPath = resolve(assignmentDir, planFile);\n if (await fileExists(planPath)) {\n const parsed = parsePlan(await readFile(planPath, 'utf-8'));\n plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };\n }\n }\n\n let scratchpad: AssignmentDetail['scratchpad'] = null;\n const scratchpadPath = resolve(assignmentDir, 'scratchpad.md');\n if (await fileExists(scratchpadPath)) {\n const parsed = parseScratchpad(await readFile(scratchpadPath, 'utf-8'));\n scratchpad = { updated: parsed.updated, body: parsed.body };\n }\n\n let handoff: AssignmentDetail['handoff'] = null;\n const handoffPath = resolve(assignmentDir, 'handoff.md');\n if (await fileExists(handoffPath)) {\n const parsed = parseHandoff(await readFile(handoffPath, 'utf-8'));\n handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };\n }\n\n let decisionRecord: AssignmentDetail['decisionRecord'] = null;\n const decisionRecordPath = resolve(assignmentDir, 'decision-record.md');\n if (await fileExists(decisionRecordPath)) {\n const parsed = parseDecisionRecord(await readFile(decisionRecordPath, 'utf-8'));\n decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };\n }\n\n let progress: AssignmentDetail['progress'] = null;\n const progressPath = resolve(assignmentDir, 'progress.md');\n if (await fileExists(progressPath)) {\n const parsed = parseProgress(await readFile(progressPath, 'utf-8'));\n progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };\n }\n\n let comments: AssignmentDetail['comments'] = null;\n const commentsPath = resolve(assignmentDir, 'comments.md');\n if (await fileExists(commentsPath)) {\n const parsed = parseComments(await readFile(commentsPath, 'utf-8'));\n comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };\n }\n\n const { terminalStatuses } = await getStatusConfig();\n const detail: AssignmentDetail = {\n id: assignment.id,\n projectSlug: null,\n slug: assignment.slug || resolved.id,\n title: assignment.title,\n status: assignment.status,\n type: assignment.type,\n priority: assignment.priority as AssignmentDetail['priority'],\n assignee: assignment.assignee,\n dependsOn: [], // standalone cannot declare dependencies\n links: [],\n reverseLinks: [],\n enrichedLinks: [],\n blockedReason: assignment.blockedReason,\n workspace: assignment.workspace,\n projectWorkspace: assignment.workspaceGroup,\n externalIds: assignment.externalIds,\n tags: assignment.tags,\n archived: assignment.archived,\n archivedAt: assignment.archivedAt,\n archivedReason: assignment.archivedReason,\n ...deriveStatusVirtuals(assignment, terminalStatuses),\n override: assignment.override,\n derived: await buildDerivedDetail(assignment, assignmentDir, null),\n created: assignment.created,\n updated: assignment.updated,\n body: assignment.body,\n plan,\n scratchpad,\n handoff,\n decisionRecord,\n progress,\n comments,\n referencedBy: [],\n availableTransitions: await getStandaloneAvailableTransitions(assignment),\n };\n\n return detail;\n}\n\n// Guard so legacy-file renames run at most once per `projectsDir` per process\n// lifetime. Keyed by absolute path to tolerate test suites that open multiple\n// sandboxes in the same process.\nconst migratedProjectsDirs = new Set<string>();\n\nasync function listProjectRecords(\n projectsDir: string,\n traces?: OverviewTraces,\n): Promise<ProjectRecord[]> {\n const cached = projectRecordsCache.get(projectsDir);\n if (cached) return cached;\n // `traces` only flows through on a cache miss; a hit legitimately does ~0\n // fan-out, so the absence of per-phase traces on a hit is the correct signal.\n const promise = computeProjectRecords(projectsDir, traces);\n projectRecordsCache.set(projectsDir, promise);\n promise.catch(() => projectRecordsCache.delete(projectsDir));\n return promise;\n}\n\nasync function computeProjectRecords(\n projectsDir: string,\n traces?: OverviewTraces,\n): Promise<ProjectRecord[]> {\n if (!(await fileExists(projectsDir))) {\n return [];\n }\n\n if (!migratedProjectsDirs.has(projectsDir)) {\n migratedProjectsDirs.add(projectsDir);\n await migrateLegacyProjectFiles(projectsDir);\n // Reconcile legacy \"archived-as-a-status\" projects (statusOverride: 'archived')\n // into the real `archived` flag so there is one source of truth.\n await migrateLegacyArchivedProjects(projectsDir);\n }\n\n const entries = await readdir(projectsDir, { withFileTypes: true });\n const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));\n\n const maybeRecords = await Promise.all(\n projectDirs.map(async (entry): Promise<ProjectRecord | null> => {\n const projectPath = resolve(projectsDir, entry.name);\n const projectMdPath = resolve(projectPath, 'project.md');\n\n if (!(await fileExists(projectMdPath))) {\n return null;\n }\n\n const t0 = traces ? performance.now() : 0;\n const projectContent = await readFile(projectMdPath, 'utf-8');\n const project = parseProject(projectContent);\n if (traces) accumulatePhase(traces, 'parse-project-md', performance.now() - t0);\n\n const t1 = traces ? performance.now() : 0;\n const assignments = await listAssignmentRecords(projectPath, traces);\n if (traces) accumulatePhase(traces, 'list-assignments', performance.now() - t1);\n\n const t2 = traces ? performance.now() : 0;\n const rollup = await buildProjectRollup(projectPath, project, assignments, traces);\n if (traces) accumulatePhase(traces, 'build-rollup', performance.now() - t2);\n\n // Archived children are hidden, so archiving an old one must not bump the\n // project's activity timestamp (which drives list/recent-projects ordering).\n const updated = getProjectActivityTimestamp(project.updated, activeAssignments(assignments));\n\n const t3 = traces ? performance.now() : 0;\n const dependencyGraph = await loadDependencyGraph(projectPath, assignments);\n if (traces) accumulatePhase(traces, 'load-dep-graph', performance.now() - t3);\n\n return {\n projectPath,\n project,\n assignments,\n dependencyGraph,\n summary: {\n slug: project.slug || entry.name,\n title: project.title,\n status: rollup.status,\n statusOverride: project.statusOverride,\n archived: project.archived,\n archivedAt: project.archivedAt,\n archivedReason: project.archivedReason,\n created: project.created,\n updated,\n tags: project.tags,\n externalIds: project.externalIds,\n progress: rollup.progress,\n needsAttention: rollup.needsAttention,\n workspace: project.workspace,\n },\n };\n }),\n );\n\n const records = maybeRecords.filter((r): r is ProjectRecord => r !== null);\n records.sort((left, right) => compareTimestamps(right.summary.updated, left.summary.updated));\n return records;\n}\n\nasync function listAssignmentRecords(\n projectPath: string,\n traces?: OverviewTraces,\n): Promise<AssignmentRecord[]> {\n const assignmentsDir = resolve(projectPath, 'assignments');\n if (!(await fileExists(assignmentsDir))) {\n return [];\n }\n\n const entries = await readdir(assignmentsDir, { withFileTypes: true });\n const dirEntries = entries.filter((entry) => entry.isDirectory());\n\n const maybeRecords = await Promise.all(\n dirEntries.map(async (entry): Promise<AssignmentRecord | null> => {\n const assignmentMd = resolve(assignmentsDir, entry.name, 'assignment.md');\n if (!(await fileExists(assignmentMd))) {\n return null;\n }\n const t0 = traces ? performance.now() : 0;\n const content = await readFile(assignmentMd, 'utf-8');\n const parsed = parseAssignmentFull(content);\n if (traces) accumulatePhase(traces, 'read-assignment-md', performance.now() - t0);\n return parsed;\n }),\n );\n\n const records = maybeRecords.filter((r): r is AssignmentRecord => r !== null);\n records.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return records;\n}\n\nasync function listResources(projectPath: string): Promise<ResourceSummary[]> {\n const resourcesDir = resolve(projectPath, 'resources');\n if (!(await fileExists(resourcesDir))) {\n return [];\n }\n\n const entries = await readdir(resourcesDir, { withFileTypes: true });\n const results: ResourceSummary[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('_')) {\n continue;\n }\n\n const filePath = resolve(resourcesDir, entry.name);\n const content = await readFile(filePath, 'utf-8');\n const parsed = parseResource(content);\n results.push({\n name: parsed.name,\n slug: entry.name.replace(/\\.md$/, ''),\n category: parsed.category,\n source: parsed.source,\n relatedAssignments: parsed.relatedAssignments,\n updated: parsed.updated,\n });\n }\n\n results.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return results;\n}\n\nasync function listMemories(projectPath: string): Promise<MemorySummary[]> {\n const memoriesDir = resolve(projectPath, 'memories');\n if (!(await fileExists(memoriesDir))) {\n return [];\n }\n\n const entries = await readdir(memoriesDir, { withFileTypes: true });\n const results: MemorySummary[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('_')) {\n continue;\n }\n\n const filePath = resolve(memoriesDir, entry.name);\n const content = await readFile(filePath, 'utf-8');\n const parsed = parseMemory(content);\n results.push({\n name: parsed.name,\n slug: entry.name.replace(/\\.md$/, ''),\n source: parsed.source,\n scope: parsed.scope,\n sourceAssignment: parsed.sourceAssignment,\n relatedAssignments: parsed.relatedAssignments,\n updated: parsed.updated,\n });\n }\n\n results.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return results;\n}\n\n/**\n * Walk every project and return its memories enriched with project context.\n *\n * `projectSlug` is the on-disk directory name (used for path-based routes like\n * `/api/projects/:slug/memories/:itemSlug` and the `/projects/:slug/...` UI routes).\n * In typical projects this equals the frontmatter `slug`, but fixtures/legacy projects\n * may differ — and the directory name is what every path-based route resolves against.\n */\nexport async function listAllMemories(\n projectsDir: string,\n): Promise<MemorySummaryWithProject[]> {\n const projectRecords = await listProjectRecords(projectsDir);\n const all: MemorySummaryWithProject[] = [];\n for (const record of projectRecords) {\n const memories = await listMemories(record.projectPath);\n for (const memory of memories) {\n all.push({\n ...memory,\n projectSlug: basename(record.projectPath),\n projectTitle: record.summary.title,\n });\n }\n }\n all.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return all;\n}\n\n/** Walk every project and return its resources enriched with project context. */\nexport async function listAllResources(\n projectsDir: string,\n): Promise<ResourceSummaryWithProject[]> {\n const projectRecords = await listProjectRecords(projectsDir);\n const all: ResourceSummaryWithProject[] = [];\n for (const record of projectRecords) {\n const resources = await listResources(record.projectPath);\n for (const resource of resources) {\n all.push({\n ...resource,\n projectSlug: basename(record.projectPath),\n projectTitle: record.summary.title,\n });\n }\n }\n all.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return all;\n}\n\n/**\n * Resolve a project slug to its on-disk directory path.\n * Tries the dir-name match first (the typical case); falls back to scanning every project\n * for a frontmatter-slug match. Returns `null` when no project matches.\n */\nexport async function resolveProjectPath(\n projectsDir: string,\n projectSlug: string,\n): Promise<string | null> {\n const direct = resolve(projectsDir, projectSlug);\n if (await fileExists(resolve(direct, 'project.md'))) return direct;\n const records = await listProjectRecords(projectsDir);\n const match = records.find((r) => r.summary.slug === projectSlug);\n return match ? match.projectPath : null;\n}\n\nexport async function getMemoryDetail(\n projectsDir: string,\n projectSlug: string,\n itemSlug: string,\n): Promise<MemoryDetail | null> {\n if (itemSlug.startsWith('_')) return null;\n\n const projectRecords = await listProjectRecords(projectsDir);\n // Match by directory name first (the path-based routing convention) and fall back to\n // the frontmatter slug — covers fixtures/legacy projects whose dir name differs from slug.\n const projectRecord = projectRecords.find(\n (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug,\n );\n if (!projectRecord) return null;\n\n const filePath = resolve(projectRecord.projectPath, 'memories', `${itemSlug}.md`);\n if (!(await fileExists(filePath))) return null;\n\n const content = await readFile(filePath, 'utf-8');\n const parsed = parseMemory(content);\n return {\n name: parsed.name,\n slug: itemSlug,\n source: parsed.source,\n scope: parsed.scope,\n sourceAssignment: parsed.sourceAssignment,\n relatedAssignments: parsed.relatedAssignments,\n updated: parsed.updated,\n created: parsed.created,\n body: parsed.body,\n tags: parsed.tags,\n projectSlug: basename(projectRecord.projectPath),\n projectTitle: projectRecord.summary.title,\n };\n}\n\nexport async function getResourceDetail(\n projectsDir: string,\n projectSlug: string,\n itemSlug: string,\n): Promise<ResourceDetail | null> {\n if (itemSlug.startsWith('_')) return null;\n\n const projectRecords = await listProjectRecords(projectsDir);\n const projectRecord = projectRecords.find(\n (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug,\n );\n if (!projectRecord) return null;\n\n const filePath = resolve(projectRecord.projectPath, 'resources', `${itemSlug}.md`);\n if (!(await fileExists(filePath))) return null;\n\n const content = await readFile(filePath, 'utf-8');\n const parsed = parseResource(content);\n return {\n name: parsed.name,\n slug: itemSlug,\n category: parsed.category,\n source: parsed.source,\n relatedAssignments: parsed.relatedAssignments,\n updated: parsed.updated,\n created: parsed.created,\n body: parsed.body,\n projectSlug: basename(projectRecord.projectPath),\n projectTitle: projectRecord.summary.title,\n };\n}\n\nasync function loadDependencyGraph(\n projectPath: string,\n assignments: AssignmentRecord[],\n): Promise<string | null> {\n const statusPath = resolve(projectPath, '_status.md');\n if (await fileExists(statusPath)) {\n const statusContent = await readFile(statusPath, 'utf-8');\n const parsed = parseStatus(statusContent);\n const derivedGraph = extractMermaidGraph(parsed.body);\n if (derivedGraph) {\n return derivedGraph;\n }\n }\n\n return buildDependencyGraph(assignments);\n}\n\nasync function buildProjectRollup(\n projectPath: string,\n project: ReturnType<typeof parseProject>,\n assignments: AssignmentRecord[],\n traces?: OverviewTraces,\n): Promise<{\n progress: ProgressCounts;\n needsAttention: NeedsAttention;\n status: string;\n}> {\n // Archived children are hidden from normal views, so they must not count in\n // the project's progress/totals/status rollup either (cascade consistency).\n const active = activeAssignments(assignments);\n const progress: ProgressCounts = { total: active.length };\n\n // Map: read every comments.md in parallel. Reduce: fold the per-assignment\n // results into progress counters + openQuestions sum.\n const perAssignment = await Promise.all(\n active.map(async (assignment) => {\n const t0 = traces ? performance.now() : 0;\n const openQuestions = await countOpenQuestions(projectPath, assignment.slug);\n if (traces) accumulatePhase(traces, 'count-open-questions', performance.now() - t0);\n return { status: assignment.status, openQuestions };\n }),\n );\n\n let openQuestions = 0;\n for (const entry of perAssignment) {\n progress[entry.status] = (progress[entry.status] ?? 0) + 1;\n openQuestions += entry.openQuestions;\n }\n\n const needsAttention: NeedsAttention = {\n blockedCount: progress['blocked'] ?? 0,\n failedCount: progress['failed'] ?? 0,\n openQuestions,\n };\n\n let status = 'pending';\n if (project.statusOverride) {\n status = project.statusOverride;\n } else if (project.archived) {\n status = 'archived';\n } else if (progress.total > 0 && (progress['completed'] ?? 0) === progress.total) {\n status = 'completed';\n } else if ((progress['in_progress'] ?? 0) > 0 || (progress['review'] ?? 0) > 0) {\n status = 'active';\n } else if ((progress['failed'] ?? 0) > 0) {\n status = 'failed';\n } else if ((progress['blocked'] ?? 0) > 0) {\n status = 'blocked';\n } else if (progress.total === 0 || (progress['pending'] ?? 0) === progress.total) {\n status = 'pending';\n } else {\n status = 'active';\n }\n\n return { progress, needsAttention, status };\n}\n\n/**\n * Derive the loader-only virtual fields from an assignment's `statusHistory`\n * (never stored on disk). `completedAt` is the `at` of the LAST transition into\n * the current status, but only when that status is terminal (lifecycle\n * `completed`/`failed`) — so an assignment reopened after completion reports null,\n * because its current status is no longer terminal. `statusAge` is the elapsed\n * milliseconds since the last entry (time in current status), null when there is\n * no history or the timestamp is unparseable.\n */\nfunction deriveStatusVirtuals(\n assignment: AssignmentRecord,\n terminalStatuses: ReadonlySet<string>,\n): {\n completedAt: string | null;\n statusAge: number | null;\n phaseAge: number | null;\n phase: string | null;\n disposition: string | null;\n pinned: boolean;\n} {\n const hist = assignment.statusHistory ?? [];\n\n let completedAt: string | null = null;\n if (terminalStatuses.has(assignment.status)) {\n for (const entry of hist) {\n if (entry.to === assignment.status) completedAt = entry.at;\n }\n }\n\n // statusAge counts HEADLINE changes only: dimension-only entries (from == to,\n // e.g. phase advanced while blocked) must not reset the clock. The seed\n // entry (from: null) counts as a headline change.\n let statusAge: number | null = null;\n for (let i = hist.length - 1; i >= 0; i--) {\n const entry = hist[i];\n if (entry.from !== entry.to || entry.from === null) {\n const t = Date.parse(entry.at);\n statusAge = Number.isNaN(t) ? null : Date.now() - t;\n break;\n }\n }\n\n let phaseAge: number | null = null;\n for (let i = hist.length - 1; i >= 0; i--) {\n const entry = hist[i];\n if (entry.phaseTo !== undefined && entry.phaseFrom !== entry.phaseTo) {\n const t = Date.parse(entry.at);\n phaseAge = Number.isNaN(t) ? null : Date.now() - t;\n break;\n }\n }\n\n return {\n completedAt,\n statusAge,\n phaseAge,\n phase: assignment.phase,\n disposition: assignment.disposition,\n pinned: assignment.override !== null,\n };\n}\n\n/**\n * Server-side materialization of the derivation detail for one assignment\n * (design v3: the browser never reads the filesystem — facts ship in the\n * payload). Null for terminal assignments (derivation defers entirely).\n */\nasync function buildDerivedDetail(\n assignment: AssignmentRecord,\n assignmentDir: string,\n projectDir: string | null,\n): Promise<AssignmentDetail['derived']> {\n const config = await getStatusConfig();\n if (config.terminalStatuses.has(assignment.status)) return null;\n try {\n const { computeFactsDetailed } = await import('../lifecycle/facts.js');\n const { deriveDimensions } = await import('../lifecycle/derive.js');\n const { DEFAULT_DERIVE_CONFIG } = await import('../utils/config.js');\n // ONE compute pass: facts (custom + attestation exports) and per-record\n // validity come from the same plan-file / HEAD reads. Fresh-per-request is\n // what makes binds:commit lazy convergence honest (Locked Decisions).\n const { facts, attestations } = await computeFactsDetailed({\n assignmentDir,\n frontmatter: {\n ...assignment,\n // AssignmentRecord ⊃ the fields computeFacts reads (incl. facts +\n // attestations from the parser); statusHistory + derived caches ride along.\n } as unknown as import('../lifecycle/types.js').AssignmentFrontmatter,\n body: assignment.body,\n projectDir,\n terminalStatuses: config.terminalStatuses,\n declarations: config.factDeclarations,\n });\n const dims = deriveDimensions({\n facts,\n derive: config.derive ?? DEFAULT_DERIVE_CONFIG,\n currentStatus: assignment.status,\n terminalStatuses: config.terminalStatuses,\n knownStatusIds: new Set(config.statuses.map((s) => s.id)),\n override: assignment.override,\n registry: config.deriveRegistry,\n });\n if (!dims) return null;\n\n // customFacts: declared bool/number values only — the client renders them\n // without guessing which keys are built-ins (the server separated them).\n const customFacts: Record<string, boolean | number> = {};\n for (const decl of config.factDeclarations) {\n if (decl.type === 'bool' || decl.type === 'number') {\n const v = facts[decl.name];\n if (typeof v === 'boolean' || typeof v === 'number') customFacts[decl.name] = v;\n }\n }\n\n return {\n derivedStatus: dims.derivedStatus,\n nextAction: dims.nextAction,\n facts: facts as unknown as Record<string, boolean | number | string[]>,\n customFacts,\n attestations: attestations.map((a) => ({\n fact: a.fact,\n binds: a.binds,\n records: a.records.map(({ record, valid }) => ({\n actor: record.actor,\n verdict: record.verdict,\n at: record.at,\n note: record.note ?? null,\n stale: !valid,\n })),\n })),\n };\n } catch (err) {\n // Best-effort enrichment, never a 500 — but not silent (codex finding 12).\n console.warn(`buildDerivedDetail failed for ${assignmentDir}:`, err);\n return null;\n }\n}\n\nfunction toAssignmentSummary(\n assignment: AssignmentRecord,\n terminalStatuses: ReadonlySet<string>,\n): AssignmentSummary {\n return {\n id: assignment.id,\n slug: assignment.slug,\n title: assignment.title,\n status: assignment.status,\n type: assignment.type,\n priority: assignment.priority as AssignmentSummary['priority'],\n assignee: assignment.assignee,\n dependsOn: assignment.dependsOn,\n links: assignment.links,\n tags: assignment.tags,\n externalIds: assignment.externalIds,\n created: assignment.created,\n updated: assignment.updated,\n archived: assignment.archived,\n archivedAt: assignment.archivedAt,\n archivedReason: assignment.archivedReason,\n ...deriveStatusVirtuals(assignment, terminalStatuses),\n };\n}\n\nasync function toAssignmentBoardItem(\n projectsDir: string,\n projectRecord: ProjectRecord,\n assignment: AssignmentRecord,\n): Promise<AssignmentBoardItem> {\n const config = await getStatusConfig();\n const { terminalStatuses } = config;\n\n const assignmentDir = resolve(projectRecord.projectPath, 'assignments', assignment.slug);\n const projectDir = projectRecord.projectPath;\n\n let facts: AssignmentBoardItem['facts'];\n try {\n const { computeFacts } = await import('../lifecycle/facts.js');\n facts = await computeFacts({\n assignmentDir,\n frontmatter: assignment as unknown as import('../lifecycle/types.js').AssignmentFrontmatter,\n body: assignment.body,\n projectDir,\n terminalStatuses,\n declarations: config.factDeclarations,\n });\n } catch (err) {\n console.warn(`toAssignmentBoardItem: computeFacts failed for ${assignmentDir}:`, err);\n }\n\n return {\n ...toAssignmentSummary(assignment, terminalStatuses),\n projectSlug: projectRecord.summary.slug,\n projectTitle: projectRecord.summary.title,\n blockedReason: assignment.blockedReason,\n projectWorkspace: projectRecord.project.workspace,\n availableTransitions: await getAvailableTransitions(\n projectsDir,\n projectRecord.summary.slug,\n assignment.slug,\n assignment,\n ),\n facts,\n };\n}\n\nconst DEFAULT_GRAPH_COLORS: Record<string, string> = {\n completed: 'fill:#4ea84f,stroke:#1f6b29,color:#ffffff',\n in_progress: 'fill:#1e6fd9,stroke:#0f3f8f,color:#ffffff',\n pending: 'fill:#c0ccd9,stroke:#738399,color:#163047',\n blocked: 'fill:#db5a3f,stroke:#8d2815,color:#ffffff',\n failed: 'fill:#9f2d2d,stroke:#651616,color:#ffffff',\n review: 'fill:#c6911e,stroke:#7a5a10,color:#ffffff',\n};\n\nfunction buildDependencyGraph(assignments: AssignmentRecord[]): string | null {\n const edges: string[] = [];\n const usedStatuses = new Set<string>();\n\n for (const assignment of assignments) {\n for (const dependency of assignment.dependsOn) {\n const depStatus = findAssignmentStatus(assignments, dependency);\n usedStatuses.add(depStatus);\n usedStatuses.add(assignment.status);\n edges.push(\n ` ${dependency}:::${depStatus} --> ${assignment.slug}:::${assignment.status}`,\n );\n }\n }\n\n if (edges.length === 0) {\n return null;\n }\n\n const classDefs: string[] = [];\n for (const status of usedStatuses) {\n const colors = DEFAULT_GRAPH_COLORS[status] ?? 'fill:#94a3b8,stroke:#64748b,color:#ffffff';\n classDefs.push(` classDef ${status} ${colors}`);\n }\n\n return ['graph TD', ...edges, ...classDefs].join('\\n');\n}\n\nfunction findAssignmentStatus(assignments: AssignmentRecord[], slug: string): string {\n return assignments.find((assignment) => assignment.slug === slug)?.status ?? 'pending';\n}\n\nasync function getAvailableTransitions(\n projectsDir: string,\n projectSlug: string,\n assignmentSlug: string,\n assignment: AssignmentRecord,\n options?: {\n dependencyStatusMap?: ReadonlyMap<string, string>;\n traces?: OverviewTraces;\n },\n): Promise<AssignmentTransitionAction[]> {\n const config = await getStatusConfig();\n const transitionDefs = getTransitionDefinitions(config);\n const actions: AssignmentTransitionAction[] = [];\n const projectPath = resolve(projectsDir, projectSlug);\n const traces = options?.traces;\n\n for (const definition of transitionDefs) {\n const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);\n // Only valid transitions reach the client; the kanban inline picker renders them directly.\n if (target === null) continue;\n\n let warning: string | null = null;\n\n if (definition.command === 'start' && !assignment.assignee) {\n warning = 'No assignee set — consider assigning before starting.';\n }\n\n if (definition.command === 'start' && assignment.dependsOn.length > 0) {\n const t0 = traces ? performance.now() : 0;\n const unmetDependencies = await getUnmetDependencies(\n projectPath,\n assignment.dependsOn,\n config.terminalStatuses,\n options?.dependencyStatusMap,\n );\n if (traces) accumulatePhase(traces, 'get-unmet-dependencies', performance.now() - t0);\n if (unmetDependencies.length > 0) {\n warning = `Unmet dependencies: ${unmetDependencies.join(', ')}.`;\n }\n }\n\n actions.push({\n command: definition.command,\n label: definition.label,\n description: definition.description,\n targetStatus: target,\n disabled: false,\n disabledReason: null,\n warning,\n requiresReason: definition.requiresReason,\n });\n }\n\n return actions;\n}\n\nasync function getUnmetDependencies(\n projectPath: string,\n dependsOn: string[],\n terminalStatuses?: ReadonlySet<string>,\n dependencyStatusMap?: ReadonlyMap<string, string>,\n): Promise<string[]> {\n const terminals = terminalStatuses ?? new Set(['completed']);\n const unmet: string[] = [];\n\n for (const dependency of dependsOn) {\n // Fast path: in-memory map (built once by the overview pass over already-parsed records).\n if (dependencyStatusMap) {\n const mappedStatus = dependencyStatusMap.get(dependency);\n if (mappedStatus !== undefined) {\n if (!terminals.has(mappedStatus)) {\n unmet.push(`${dependency} (${mappedStatus})`);\n }\n continue;\n }\n // Fall through to disk read only if the map didn't know about this dependency.\n }\n\n const dependencyPath = resolve(projectPath, 'assignments', dependency, 'assignment.md');\n if (!(await fileExists(dependencyPath))) {\n unmet.push(`${dependency} (missing)`);\n continue;\n }\n\n const content = await readFile(dependencyPath, 'utf-8');\n const parsed = parseAssignmentFull(content);\n if (!terminals.has(parsed.status)) {\n unmet.push(`${dependency} (${parsed.status})`);\n }\n }\n\n return unmet;\n}\n\ninterface OverviewSegmentBuckets {\n readyForReview: AttentionItem[];\n readyToImplement: AttentionItem[];\n readyForPlanning: AttentionItem[];\n inProgress: AttentionItem[];\n drafts: AttentionItem[];\n blocked: AttentionItem[];\n newestCreated: AttentionItem[];\n stale: AttentionItem[];\n}\n\nfunction emptyBuckets(): OverviewSegmentBuckets {\n return {\n readyForReview: [],\n readyToImplement: [],\n readyForPlanning: [],\n inProgress: [],\n drafts: [],\n blocked: [],\n newestCreated: [],\n stale: [],\n };\n}\n\nfunction segmentSeverity(segment: OverviewSegmentId): AttentionItem['severity'] {\n switch (segment) {\n case 'blocked':\n return 'high';\n case 'readyForReview':\n return 'medium';\n case 'stale':\n return 'low';\n default:\n return 'medium';\n }\n}\n\nconst STALE_SEVERITY_RANK: Record<StaleReason['severity'], number> = { high: 3, medium: 2, low: 1 };\n\n/** Highest-severity reason (drives the displayed stale reason line). */\nfunction topStaleReason(reasons: StaleReason[]): StaleReason | null {\n if (reasons.length === 0) return null;\n return reasons\n .slice()\n .sort((a, b) => STALE_SEVERITY_RANK[b.severity] - STALE_SEVERITY_RANK[a.severity])[0];\n}\n\n/** Activity age from `progress.md` mtime (the honest signal — NOT assignment\n * `updated`, which recompute bumps). `null` when there is no progress.md, so the\n * classifier's activity-based reason fails safe (never fires on unknown). */\nasync function readProgressActivityMs(progressPath: string, now: number): Promise<number | null> {\n try {\n const s = await stat(progressPath);\n return Math.max(0, now - s.mtimeMs);\n } catch {\n return null;\n }\n}\n\n/** Run the shared staleness classifier for one assignment record. */\nfunction classifyAssignmentRecord(\n assignment: AssignmentRecord,\n terminalStatuses: ReadonlySet<string>,\n depsSatisfied: boolean | null,\n lastActivityMs: number | null,\n thresholds: StaleThresholds,\n): StaleReason[] {\n const virtuals = deriveStatusVirtuals(assignment, terminalStatuses);\n return classifyNeedsAttention(\n {\n phase: virtuals.phase,\n disposition: virtuals.disposition,\n isTerminal: terminalStatuses.has(assignment.status),\n assignee: assignment.assignee ?? null,\n blockedReason: assignment.blockedReason,\n depsSatisfied,\n // plan_awaiting_approval is deferred to the decision inbox's plan-approval\n // category for now; pass values that keep that reason dormant.\n planExists: false,\n planApproved: true,\n statusAgeMs: virtuals.statusAge,\n lastActivityMs,\n },\n thresholds,\n );\n}\n\n/**\n * Read-only scan of EVERY active assignment (project + standalone, unpaged) for\n * the staleness watchdog. Reuses the same classifier + resolved terminals +\n * config thresholds as the overview, keyed by assignment id (stable UUID). Never\n * writes anything.\n */\nexport async function collectStaleCandidates(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<StaleCandidate[]> {\n const [projectRecords, standaloneRecords] = await Promise.all([\n listProjectRecords(projectsDir),\n listStandaloneRecords(assignmentsDir),\n ]);\n const { terminalStatuses } = await getStatusConfig();\n const thresholds = resolveStaleThresholds((await readConfig()).staleness);\n const now = Date.now();\n const out: StaleCandidate[] = [];\n\n for (const record of projectRecords) {\n if (isProjectArchived(record.summary)) continue;\n const projectPath = resolve(projectsDir, record.summary.slug);\n const depMap = new Map<string, string>();\n for (const a of record.assignments) depMap.set(a.slug, a.status);\n for (const assignment of activeAssignments(record.assignments)) {\n const depsSatisfied =\n assignment.dependsOn.length === 0\n ? true\n : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;\n const lastActivityMs = await readProgressActivityMs(\n resolve(projectPath, 'assignments', assignment.slug, 'progress.md'),\n now,\n );\n const reasons = classifyAssignmentRecord(assignment, terminalStatuses, depsSatisfied, lastActivityMs, thresholds);\n if (reasons.length > 0) {\n out.push({ assignmentId: assignment.id, projectSlug: record.summary.slug, reasons });\n }\n }\n }\n\n for (const sr of standaloneRecords) {\n if (sr.record.archived === true) continue;\n const lastActivityMs = await readProgressActivityMs(resolve(sr.assignmentDir, 'progress.md'), now);\n const reasons = classifyAssignmentRecord(sr.record, terminalStatuses, true, lastActivityMs, thresholds);\n if (reasons.length > 0) out.push({ assignmentId: sr.record.id, projectSlug: null, reasons });\n }\n\n return out;\n}\n\nasync function buildOverviewSegmentBuckets(\n projectsDir: string,\n projectRecords: ProjectRecord[],\n standaloneRecords: StandaloneRecord[],\n traces?: OverviewTraces,\n): Promise<OverviewSegmentBuckets> {\n const now = Date.now();\n const buckets = emptyBuckets();\n // Resolved terminal statuses (honors renamed/custom terminals — not the\n // module-local hardcoded TERMINAL_STATUSES) for the staleness classifier.\n const { terminalStatuses } = await getStatusConfig();\n // Staleness age-gates: config overrides merged over defaults (defaults-first).\n const staleThresholds = resolveStaleThresholds((await readConfig()).staleness);\n // Pool of all non-terminal rows (across primary segments) used to seed\n // `newestCreated`. Each entry remembers its `created` timestamp + the row\n // we'd clone into the segment.\n const newestPool: Array<{ created: string; clone: AttentionItem }> = [];\n\n for (const record of projectRecords) {\n // Cascade-hide: an archived project contributes none of its assignments to\n // the overview segments.\n if (isProjectArchived(record.summary)) continue;\n\n // Build a dep-status map once per project so getUnmetDependencies can resolve\n // dependency status from memory instead of re-reading each dep's assignment.md.\n // (Built over ALL assignments so dependency resolution is unaffected by hiding.)\n const depMap = new Map<string, string>();\n for (const a of record.assignments) {\n depMap.set(a.slug, a.status);\n }\n\n // Individually-archived assignments are hidden from the overview segments.\n const visibleAssignments = activeAssignments(record.assignments);\n\n // Resolve every per-assignment getAvailableTransitions call for this project\n // in parallel, then run the synchronous classification logic below over the results.\n const projectPath = resolve(projectsDir, record.summary.slug);\n const resolvedTransitions = await Promise.all(\n visibleAssignments.map(async (assignment) => {\n const t0 = traces ? performance.now() : 0;\n const availableTransitions = await getAvailableTransitions(\n projectsDir,\n record.summary.slug,\n assignment.slug,\n assignment,\n { traces, dependencyStatusMap: depMap },\n );\n if (traces) accumulatePhase(traces, 'get-available-transitions', performance.now() - t0);\n // Inputs for the staleness classifier (resolved off already-parsed data\n // + one progress.md stat). depsSatisfied via the in-memory depMap; no\n // extra disk read when there are no deps.\n const depsSatisfied =\n assignment.dependsOn.length === 0\n ? true\n : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap))\n .length === 0;\n const lastActivityMs = await readProgressActivityMs(\n resolve(projectPath, 'assignments', assignment.slug, 'progress.md'),\n now,\n );\n return { assignment, availableTransitions, depsSatisfied, lastActivityMs };\n }),\n );\n\n for (const { assignment, availableTransitions, depsSatisfied, lastActivityMs } of resolvedTransitions) {\n const segmentId = STATUS_TO_SEGMENT[assignment.status];\n const isTerminal = terminalStatuses.has(assignment.status);\n const staleReasons = classifyAssignmentRecord(\n assignment,\n terminalStatuses,\n depsSatisfied,\n lastActivityMs,\n staleThresholds,\n );\n const stale = staleReasons.length > 0;\n const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));\n const baseId = `${record.summary.slug}:${assignment.slug}`;\n\n const shared = {\n projectSlug: record.summary.slug,\n projectTitle: record.summary.title,\n assignmentSlug: assignment.slug,\n assignmentTitle: assignment.title,\n status: assignment.status,\n updated: assignment.updated,\n href: `/projects/${record.summary.slug}/assignments/${assignment.slug}`,\n blockedReason: assignment.blockedReason,\n stale,\n agingMs,\n assignee: assignment.assignee ?? null,\n availableTransitions,\n };\n\n if (segmentId) {\n const reason =\n segmentId === 'blocked' && assignment.blockedReason\n ? assignment.blockedReason\n : SEGMENT_REASON[segmentId];\n const primary: AttentionItem = {\n ...shared,\n id: `${baseId}:${segmentId}`,\n severity: segmentSeverity(segmentId),\n reason,\n segment: segmentId,\n };\n buckets[segmentId].push(primary);\n }\n\n if (stale && !isTerminal) {\n const top = topStaleReason(staleReasons);\n const staleItem: AttentionItem = {\n ...shared,\n id: `${baseId}:stale`,\n severity: 'low',\n reason: top?.label ?? SEGMENT_REASON.stale,\n segment: 'stale',\n };\n buckets.stale.push(staleItem);\n }\n\n if (!isTerminal) {\n newestPool.push({\n created: assignment.created,\n clone: {\n ...shared,\n id: `${baseId}:newest`,\n severity: 'low',\n reason: SEGMENT_REASON.newestCreated,\n segment: 'newestCreated',\n },\n });\n }\n }\n }\n\n const resolvedStandaloneTransitions = await Promise.all(\n standaloneRecords\n .filter((sr) => sr.record.archived !== true)\n .map(async (sr) => {\n const t0 = traces ? performance.now() : 0;\n const availableTransitions = await getStandaloneAvailableTransitions(sr.record);\n if (traces) accumulatePhase(traces, 'get-available-transitions', performance.now() - t0);\n const lastActivityMs = await readProgressActivityMs(resolve(sr.assignmentDir, 'progress.md'), now);\n return { sr, availableTransitions, lastActivityMs };\n }),\n );\n\n for (const { sr, availableTransitions, lastActivityMs } of resolvedStandaloneTransitions) {\n const assignment = sr.record;\n const segmentId = STATUS_TO_SEGMENT[assignment.status];\n const isTerminal = terminalStatuses.has(assignment.status);\n // Standalone assignments cannot declare dependencies → depsSatisfied is true.\n const staleReasons = classifyAssignmentRecord(\n assignment,\n terminalStatuses,\n true,\n lastActivityMs,\n staleThresholds,\n );\n const stale = staleReasons.length > 0;\n const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));\n const baseId = `standalone:${sr.id}`;\n\n const shared = {\n projectSlug: null,\n projectTitle: null,\n assignmentSlug: assignment.slug || sr.id,\n assignmentTitle: assignment.title,\n status: assignment.status,\n updated: assignment.updated,\n href: `/assignments/${sr.id}`,\n blockedReason: assignment.blockedReason,\n stale,\n agingMs,\n assignee: assignment.assignee ?? null,\n availableTransitions,\n };\n\n if (segmentId) {\n const reason =\n segmentId === 'blocked' && assignment.blockedReason\n ? assignment.blockedReason\n : SEGMENT_REASON[segmentId];\n buckets[segmentId].push({\n ...shared,\n id: `${baseId}:${segmentId}`,\n severity: segmentSeverity(segmentId),\n reason,\n segment: segmentId,\n });\n }\n\n if (stale && !isTerminal) {\n const top = topStaleReason(staleReasons);\n buckets.stale.push({\n ...shared,\n id: `${baseId}:stale`,\n severity: 'low',\n reason: top?.label ?? SEGMENT_REASON.stale,\n segment: 'stale',\n });\n }\n\n if (!isTerminal) {\n newestPool.push({\n created: assignment.created,\n clone: {\n ...shared,\n id: `${baseId}:newest`,\n severity: 'low',\n reason: SEGMENT_REASON.newestCreated,\n segment: 'newestCreated',\n },\n });\n }\n }\n\n newestPool.sort((a, b) => compareTimestamps(b.created, a.created));\n buckets.newestCreated = newestPool.slice(0, NEWEST_CREATED_LIMIT).map((entry) => entry.clone);\n\n for (const key of Object.keys(buckets) as OverviewSegmentId[]) {\n if (key === 'newestCreated') continue; // already sorted by `created`\n if (key === 'stale') {\n buckets[key].sort((a, b) => b.agingMs - a.agingMs);\n continue;\n }\n buckets[key].sort((a, b) => compareTimestamps(b.updated, a.updated));\n }\n\n return buckets;\n}\n\nfunction toOverviewSegments(\n buckets: OverviewSegmentBuckets,\n staleOpts: { staleLimit: number; staleOffset: number },\n): OverviewSegments {\n const sliceCap = (items: AttentionItem[]): OverviewSegmentPayload => ({\n items: items.slice(0, SEGMENT_DISPLAY_CAP),\n total: items.length,\n });\n\n const stale = buckets.stale;\n const staleSlice = stale.slice(staleOpts.staleOffset, staleOpts.staleOffset + staleOpts.staleLimit);\n const staleSegment: OverviewStaleSegmentPayload = {\n items: staleSlice,\n total: stale.length,\n limit: staleOpts.staleLimit,\n offset: staleOpts.staleOffset,\n hasMore: staleOpts.staleOffset + staleSlice.length < stale.length,\n };\n\n return {\n readyForReview: sliceCap(buckets.readyForReview),\n readyToImplement: sliceCap(buckets.readyToImplement),\n readyForPlanning: sliceCap(buckets.readyForPlanning),\n inProgress: sliceCap(buckets.inProgress),\n drafts: sliceCap(buckets.drafts),\n blocked: sliceCap(buckets.blocked),\n newestCreated: { items: buckets.newestCreated, total: buckets.newestCreated.length },\n stale: staleSegment,\n };\n}\n\nfunction pickOverviewHero(buckets: OverviewSegmentBuckets): OverviewHeroRecommendation {\n for (const [segmentId, kind] of HERO_PRIORITY) {\n const bucket = buckets[segmentId];\n if (bucket.length === 0) continue;\n const top = bucket[0];\n const total = bucket.length;\n const copyKey = total === 1 ? `${kind}.singular` : kind;\n return { kind, copyKey, itemId: top.id, total };\n }\n return { kind: 'clean', copyKey: 'clean', itemId: null, total: 0 };\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) return min;\n return Math.min(Math.max(value, min), max);\n}\n\nfunction buildRecentActivity(\n projectRecords: ProjectRecord[],\n standaloneRecords: StandaloneRecord[] = [],\n): RecentActivityItem[] {\n const activity: RecentActivityItem[] = [];\n\n for (const record of projectRecords) {\n activity.push({\n id: `project:${record.summary.slug}`,\n type: 'project',\n title: record.summary.title,\n updated: record.summary.updated,\n href: `/projects/${record.summary.slug}`,\n projectSlug: record.summary.slug,\n projectTitle: record.summary.title,\n assignmentSlug: null,\n summary: `Project status is ${record.summary.status}.`,\n });\n\n for (const assignment of activeAssignments(record.assignments)) {\n activity.push({\n id: `assignment:${record.summary.slug}:${assignment.slug}`,\n type: 'assignment',\n title: assignment.title,\n updated: assignment.updated,\n href: `/projects/${record.summary.slug}/assignments/${assignment.slug}`,\n projectSlug: record.summary.slug,\n projectTitle: record.summary.title,\n assignmentSlug: assignment.slug,\n summary: `Assignment is ${assignment.status} with ${assignment.priority} priority.`,\n });\n }\n }\n\n for (const sr of standaloneRecords) {\n const assignment = sr.record;\n activity.push({\n id: `standalone-assignment:${sr.id}`,\n type: 'assignment',\n title: assignment.title,\n updated: assignment.updated,\n href: `/assignments/${sr.id}`,\n projectSlug: null,\n projectTitle: null,\n assignmentSlug: assignment.slug || sr.id,\n summary: `Standalone assignment is ${assignment.status} with ${assignment.priority} priority.`,\n });\n }\n\n activity.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return activity;\n}\n\nfunction compareTimestamps(left: string, right: string): number {\n return parseTimestamp(left) - parseTimestamp(right);\n}\n\nfunction parseTimestamp(timestamp: string): number {\n const parsed = Date.parse(timestamp);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction countPendingAnswers(body: string): number {\n const matches = body.match(/^\\*\\*A:\\*\\*\\s+pending\\s*$/gim);\n return matches ? matches.length : 0;\n}\n\nasync function countOpenQuestions(\n projectPath: string,\n assignmentSlug: string,\n): Promise<number> {\n const commentsPath = resolve(\n projectPath,\n 'assignments',\n assignmentSlug,\n 'comments.md',\n );\n if (!(await fileExists(commentsPath))) {\n return 0;\n }\n try {\n const content = await readFile(commentsPath, 'utf-8');\n const parsed = parseComments(content);\n return parsed.entries.filter(\n (e) => e.type === 'question' && e.resolved !== true,\n ).length;\n } catch {\n return 0;\n }\n}\n\nfunction getProjectActivityTimestamp(projectUpdated: string, assignments: AssignmentRecord[]): string {\n let latest = projectUpdated;\n for (const assignment of assignments) {\n if (compareTimestamps(assignment.updated, latest) > 0) {\n latest = assignment.updated;\n }\n }\n return latest;\n}\n\nfunction getDocumentPath(\n projectsDir: string,\n documentType: EditableDocumentResponse['documentType'],\n projectSlug: string,\n assignmentSlug?: string,\n): string | null {\n switch (documentType) {\n case 'project':\n return resolve(projectsDir, projectSlug, 'project.md');\n case 'assignment':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'assignment.md')\n : null;\n case 'plan':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'plan.md')\n : null;\n case 'scratchpad':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'scratchpad.md')\n : null;\n case 'handoff':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'handoff.md')\n : null;\n case 'decision-record':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'decision-record.md')\n : null;\n case 'memory':\n // For memory/resource, the second positional is the item slug.\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'memories', `${assignmentSlug}.md`)\n : null;\n case 'resource':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'resources', `${assignmentSlug}.md`)\n : null;\n default:\n return null;\n }\n}\n\nfunction getEditableDocumentTitle(\n documentType: EditableDocumentResponse['documentType'],\n projectSlug: string,\n assignmentSlug?: string,\n): string {\n switch (documentType) {\n case 'project':\n return `Edit Project: ${projectSlug}`;\n case 'assignment':\n return `Edit Assignment: ${assignmentSlug || 'assignment'}`;\n case 'plan':\n return `Edit Plan: ${assignmentSlug || 'assignment'}`;\n case 'scratchpad':\n return `Edit Scratchpad: ${assignmentSlug || 'assignment'}`;\n case 'handoff':\n return `Append Handoff: ${assignmentSlug || 'assignment'}`;\n case 'decision-record':\n return `Append Decision: ${assignmentSlug || 'assignment'}`;\n case 'playbook':\n return `Edit Playbook: ${projectSlug}`;\n case 'memory':\n return `Edit Memory: ${assignmentSlug || 'memory'}`;\n case 'resource':\n return `Edit Resource: ${assignmentSlug || 'resource'}`;\n default:\n return projectSlug;\n }\n}\n\n// --- Playbook API ---\n\nexport async function listPlaybooks(playbooksDir: string): Promise<PlaybookSummary[]> {\n if (!(await fileExists(playbooksDir))) return [];\n\n const config = await readConfig();\n const disabledSet = new Set(config.playbooks.disabled);\n\n const entries = await readdir(playbooksDir, { withFileTypes: true });\n const playbooks: PlaybookSummary[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('_') || entry.name === 'manifest.md') continue;\n\n const filePath = resolve(playbooksDir, entry.name);\n const raw = await readFile(filePath, 'utf-8');\n const parsed = parsePlaybook(raw);\n\n const slug = parsed.slug || entry.name.replace(/\\.md$/, '');\n playbooks.push({\n slug,\n name: parsed.name || slug,\n description: parsed.description,\n whenToUse: parsed.whenToUse,\n tags: parsed.tags,\n created: parsed.created,\n updated: parsed.updated,\n enabled: !disabledSet.has(slug),\n });\n }\n\n return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));\n}\n\nexport async function getPlaybookDetail(\n playbooksDir: string,\n slug: string,\n): Promise<PlaybookDetail | null> {\n const resolved = await resolvePlaybookSlug(playbooksDir, slug);\n if (!resolved) return null;\n\n const config = await readConfig();\n const enabled = !config.playbooks.disabled.includes(resolved.slug);\n\n const parsed = resolved.parsed;\n return {\n slug: resolved.slug,\n name: parsed.name || resolved.slug,\n description: parsed.description,\n whenToUse: parsed.whenToUse,\n tags: parsed.tags,\n created: parsed.created,\n updated: parsed.updated,\n body: parsed.body,\n enabled,\n };\n}\n","import {\n TERMINAL_CHOICES,\n type TerminalChoice,\n} from '../utils/terminal-schema.js';\n\nexport type OpenUrlErrorCode =\n | 'bad-scheme'\n | 'bad-host'\n | 'missing-id'\n | 'both-ids'\n | 'malformed'\n | 'duplicate-param'\n | 'bad-terminal'\n | 'bad-mode'\n | 'invalid-prompt';\n\n/**\n * Maximum length of a `prompt=` launch-prompt override. Bounds the\n * `syntaur://` URL and is enforced server-side in `parseOpenUrl` so a\n * hand-crafted direct URL can't bypass the dashboard dialog's cap.\n */\nexport const MAX_OPEN_PROMPT_LENGTH = 2000;\n\nexport class OpenUrlError extends Error {\n readonly code: OpenUrlErrorCode;\n constructor(code: OpenUrlErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'OpenUrlError';\n }\n}\n\nexport type SessionMode = 'resume' | 'fork';\n\nconst SESSION_MODES: readonly SessionMode[] = ['resume', 'fork'];\n\nexport interface ParsedOpenUrl {\n kind: 'assignment' | 'session';\n id: string;\n /**\n * Optional one-shot terminal override. When present, the launch plan uses\n * this instead of the configured `terminal:`. The dashboard's\n * missing-terminal fallback dialog sets this so a confirm-to-fallback flow\n * doesn't require mutating user config.\n */\n terminal?: TerminalChoice;\n /**\n * Only set when `kind === 'session'`. Defaults to `'resume'` when the URL\n * has no `mode` query param. Distinguishes \"continue this session under the\n * same id\" (resume) from \"branch a new session id at this point in\n * history\" (fork) so the dashboard can disable Resume while the original\n * process may still be writing the transcript.\n */\n mode?: SessionMode;\n /**\n * Optional agent id to launch with (the `agent=` query param). Lets the\n * dashboard's \"Open in agent\" picker launch a specific runner profile instead\n * of the configured default. Only honored for `kind === 'assignment'`; for\n * sessions the agent is pinned by the session record, so the value is\n * parsed-but-ignored rather than rejected (keeps the parser simple).\n */\n agent?: string;\n /**\n * Optional one-shot launch-prompt override (the `prompt=` query param) — the\n * dashboard's editable prompt box sends the (possibly edited) template here.\n * Only set for `kind === 'assignment'` (sessions take their first message\n * from history). Length-bounded (`MAX_OPEN_PROMPT_LENGTH`).\n * Presence-significant: an empty string is a deliberate override (re-resolves\n * to the fallback seed), distinct from `undefined` (no override).\n */\n prompt?: string;\n}\n\n/**\n * Parse a `syntaur://open?assignment=<id>` or `syntaur://open?session=<id>` URL.\n *\n * Validation:\n * - scheme must be `syntaur:`\n * - host must be `open`\n * - exactly one of `assignment` or `session` query params must be present\n * - neither param may be duplicated\n * - when `session` is present, optional `mode=resume|fork` (default `resume`)\n * - optional `terminal=<choice>` one-shot override (validated against\n * `TERMINAL_CHOICES`)\n *\n * Throws OpenUrlError with a structured code on any failure.\n */\nexport function parseOpenUrl(input: string): ParsedOpenUrl {\n let url: URL;\n try {\n url = new URL(input);\n } catch {\n throw new OpenUrlError(\n 'malformed',\n `Could not parse URL: ${JSON.stringify(input)}`,\n );\n }\n\n if (url.protocol !== 'syntaur:') {\n throw new OpenUrlError(\n 'bad-scheme',\n `Expected scheme 'syntaur:' but got '${url.protocol}'`,\n );\n }\n\n if (url.hostname !== 'open') {\n throw new OpenUrlError(\n 'bad-host',\n `Expected host 'open' but got '${url.hostname}'`,\n );\n }\n\n const assignmentVals = url.searchParams.getAll('assignment');\n const sessionVals = url.searchParams.getAll('session');\n\n if (assignmentVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `assignment` query param',\n );\n }\n if (sessionVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `session` query param',\n );\n }\n\n // Both-ids is decided by PARAM PRESENCE (`...length === 1`), not by value\n // truthiness. `?assignment=&session=x` has BOTH params present even though\n // assignment's value is empty — that must error as both-ids, not silently\n // fall through to the session branch.\n if (assignmentVals.length === 1 && sessionVals.length === 1) {\n throw new OpenUrlError(\n 'both-ids',\n 'URL has both `assignment` and `session` query params — only one is allowed',\n );\n }\n\n const terminalVals = url.searchParams.getAll('terminal');\n if (terminalVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `terminal` query param',\n );\n }\n let terminal: TerminalChoice | undefined;\n if (terminalVals.length === 1 && terminalVals[0].trim() !== '') {\n const candidate = terminalVals[0];\n if (!(TERMINAL_CHOICES as readonly string[]).includes(candidate)) {\n throw new OpenUrlError(\n 'bad-terminal',\n `\\`terminal\\` query param must be one of: ${TERMINAL_CHOICES.join(', ')}`,\n );\n }\n terminal = candidate as TerminalChoice;\n }\n\n const agentVals = url.searchParams.getAll('agent');\n if (agentVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `agent` query param',\n );\n }\n let agent: string | undefined;\n if (agentVals.length === 1 && agentVals[0].trim() !== '') {\n agent = agentVals[0];\n }\n\n // `prompt=` is presence-significant: keep an empty value (a deliberate clear)\n // distinct from absent. Bounded so it can't bloat the URL.\n const promptVals = url.searchParams.getAll('prompt');\n if (promptVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `prompt` query param',\n );\n }\n let prompt: string | undefined;\n if (promptVals.length === 1) {\n const value = promptVals[0];\n if (value.length > MAX_OPEN_PROMPT_LENGTH) {\n throw new OpenUrlError(\n 'invalid-prompt',\n `\\`prompt\\` query param exceeds ${MAX_OPEN_PROMPT_LENGTH} characters`,\n );\n }\n prompt = value;\n }\n\n if (assignmentVals.length === 1) {\n const id = assignmentVals[0];\n if (id.trim() === '') {\n throw new OpenUrlError(\n 'missing-id',\n '`assignment` query param is empty',\n );\n }\n return {\n kind: 'assignment',\n id,\n ...(terminal ? { terminal } : {}),\n ...(agent ? { agent } : {}),\n // assignment-only; keep '' (presence-significant) — hence !== undefined.\n ...(prompt !== undefined ? { prompt } : {}),\n };\n }\n\n if (sessionVals.length === 1) {\n const id = sessionVals[0];\n if (id.trim() === '') {\n throw new OpenUrlError('missing-id', '`session` query param is empty');\n }\n\n const modeVals = url.searchParams.getAll('mode');\n if (modeVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `mode` query param',\n );\n }\n let mode: SessionMode = 'resume';\n if (modeVals.length === 1) {\n const raw = modeVals[0];\n if (!SESSION_MODES.includes(raw as SessionMode)) {\n throw new OpenUrlError(\n 'bad-mode',\n `\\`mode\\` must be one of ${SESSION_MODES.join('|')} (got \"${raw}\")`,\n );\n }\n mode = raw as SessionMode;\n }\n return {\n kind: 'session',\n id,\n mode,\n ...(terminal ? { terminal } : {}),\n ...(agent ? { agent } : {}),\n };\n }\n\n throw new OpenUrlError(\n 'missing-id',\n 'URL must include either `assignment=<id>` or `session=<id>`',\n );\n}\n","import {\n type AgentConfig,\n type SyntaurConfig,\n type TerminalChoice,\n getAgents,\n getTerminal,\n} from '../utils/config.js';\nimport { resolveAssignmentById } from '../utils/assignment-resolver.js';\nimport {\n getAssignmentDetail,\n getAssignmentDetailById,\n} from '../dashboard/api.js';\n// Import the resolver directly (not via ./index.js) to avoid the\n// argv→tui/launch import cycle the barrel would introduce.\nimport { resolveLaunchPrompt } from './launch-prompt.js';\nimport { playbooksDir } from '../utils/paths.js';\nimport { listPlaybookSlugs } from '../utils/playbooks.js';\nimport { formatFallbackCwdWarning, resolveWorkspaceCwd } from './cwd.js';\nimport { getSessionById } from '../dashboard/agent-sessions.js';\nimport { buildFreshArgv, buildSessionArgv } from './argv.js';\nimport type { ResolvedArgv } from './types.js';\nimport type { SessionMode } from './url.js';\n\nexport type LaunchErrorCode =\n | 'no-agents-configured'\n | 'assignment-not-found'\n | 'session-not-found'\n | 'agent-not-configured'\n | 'mode-not-supported'\n | 'workspace-path-invalid';\n\nexport class LaunchError extends Error {\n readonly code: LaunchErrorCode;\n constructor(code: LaunchErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'LaunchError';\n }\n}\n\nexport interface LaunchPlan {\n terminal: TerminalChoice;\n cwd: string;\n argv: ResolvedArgv;\n env: NodeJS.ProcessEnv;\n agentId: string;\n /** Non-fatal warning about a fallback cwd (worktree path missing). */\n fallbackWarning: string | null;\n /** Non-fatal warning from shell-alias resolution falling back to /bin/sh. */\n shellFallbackWarning: string | null;\n /** Non-fatal launch-prompt token warnings (unknown/malformed `@`-tokens). */\n promptWarnings?: string[];\n /**\n * Session identity at launch time, for register-at-birth. `sessionId` is only\n * known for resume-mode session launches; fresh/fork launches mint a NEW id\n * inside the agent, so they carry `null` and rely on the pending runtime\n * marker + scanner to close the gap. Absent on assignment launches.\n */\n session?: { sessionId: string | null };\n}\n\nexport interface ResolveLaunchPlanInput {\n kind: 'assignment' | 'session';\n id: string;\n /**\n * Only consulted when `kind === 'session'`. Defaults to `'resume'` so\n * callers that haven't been updated to thread the URL mode through still\n * get the prior behavior (continue the same session id).\n */\n mode?: SessionMode;\n config: SyntaurConfig;\n projectsDir: string;\n assignmentsDir: string;\n /**\n * One-shot terminal override. When set, used in place of\n * `getTerminal(config)`. Wired through from `?terminal=<choice>` on the\n * incoming `syntaur://` URL so the dashboard's missing-terminal fallback\n * dialog can confirm a different terminal without mutating user config.\n */\n terminalOverride?: TerminalChoice;\n /**\n * Only consulted when `kind === 'assignment'`. The agent id to launch with,\n * wired from `?agent=<id>` on the incoming `syntaur://` URL so the dashboard's\n * \"Open in agent\" picker can launch a specific runner profile. When unset,\n * falls back to `pickAgent(config)` (the default agent). An unknown id throws\n * `LaunchError('agent-not-configured')`.\n */\n agentId?: string;\n /**\n * Only consulted when `kind === 'assignment'`. A one-shot launch-prompt\n * override, wired from `?prompt=<text>` on the incoming `syntaur://` URL (the\n * dashboard's editable prompt box). **Presence-significant:** when defined\n * (including `''`) it is used as the `template` for `resolveLaunchPrompt`\n * instead of `agent.launchPrompt`, so its `@`-tokens re-resolve and an empty\n * value falls back through the normal empty-template path (never silently\n * reusing `agent.launchPrompt`). Per-launch only — never written to config.\n */\n promptOverride?: string;\n}\n\n/**\n * Pick the agent the \"Open in agent\" flow should use. Order of preference:\n * - the first agent with `default: true`\n * - else the first entry in the list\n * Throws LaunchError('no-agents-configured') if the list is empty (only\n * possible when the user explicitly wrote `agents: []` — absence falls back to\n * BUILTIN_AGENTS via `getAgents()`).\n */\nexport function pickAgent(config: SyntaurConfig): AgentConfig {\n const agents = getAgents(config);\n if (agents.length === 0) {\n throw new LaunchError(\n 'no-agents-configured',\n 'No agents in ~/.syntaur/config.md. Run `syntaur agents add` to configure one.',\n );\n }\n return agents.find((a) => a.default) ?? agents[0];\n}\n\n/**\n * Resolve the launch plan for a \"Open in agent\" click. Reads the assignment or\n * session record, picks the cwd + agent, builds the argv, and returns a\n * structured plan that `executeLaunchPlan` (or an Electron caller) can run.\n */\nexport async function resolveLaunchPlan(\n input: ResolveLaunchPlanInput,\n): Promise<LaunchPlan> {\n const terminal = input.terminalOverride ?? getTerminal(input.config);\n\n if (input.kind === 'assignment') {\n return resolveAssignmentPlan(input, terminal);\n }\n return resolveSessionPlan(input, terminal);\n}\n\nasync function resolveAssignmentPlan(\n input: ResolveLaunchPlanInput,\n terminal: TerminalChoice,\n): Promise<LaunchPlan> {\n const resolved = await resolveAssignmentById(\n input.projectsDir,\n input.assignmentsDir,\n input.id,\n );\n if (!resolved) {\n throw new LaunchError(\n 'assignment-not-found',\n `Assignment with id ${JSON.stringify(input.id)} not found`,\n );\n }\n\n const detail = await getAssignmentDetailById(\n input.projectsDir,\n input.assignmentsDir,\n input.id,\n );\n if (!detail) {\n throw new LaunchError(\n 'assignment-not-found',\n `Assignment ${input.id} resolver returned a directory but detail could not be loaded`,\n );\n }\n\n const picked = resolveWorkspaceCwd({\n worktreePath: detail.workspace.worktreePath,\n repository: detail.workspace.repository,\n branch: detail.workspace.branch,\n assignmentSlug: resolved.assignmentSlug,\n });\n if (picked.cwd === null) {\n // No valid worktree or repository directory — refuse rather than silently\n // launching in the dashboard process cwd.\n throw new LaunchError('workspace-path-invalid', picked.invalidReason as string);\n }\n const cwd = picked.cwd;\n const fallbackWarning = picked.fallbackWarning;\n\n let agent: AgentConfig;\n if (input.agentId) {\n const found = getAgents(input.config).find((a) => a.id === input.agentId);\n if (!found) {\n throw new LaunchError(\n 'agent-not-configured',\n `Agent \"${input.agentId}\" requested in the open URL is not in your agents list.`,\n );\n }\n agent = found;\n } else {\n agent = pickAgent(input.config);\n }\n const knownPlaybookSlugs = await listPlaybookSlugs(playbooksDir());\n // A defined promptOverride (incl. '') wins over the stored template by\n // presence — clearing the box must not silently reuse agent.launchPrompt.\n const template =\n input.promptOverride !== undefined ? input.promptOverride : agent.launchPrompt;\n const { prompt, warnings: promptWarnings } = resolveLaunchPrompt({\n template,\n playbook: agent.playbook,\n id: resolved.id,\n assignmentDir: resolved.assignmentDir,\n projectSlug: resolved.projectSlug,\n assignmentSlug: resolved.assignmentSlug,\n knownPlaybookSlugs,\n });\n const { argv, shellFallbackWarning } = buildFreshArgv(agent, prompt);\n\n return {\n terminal,\n cwd,\n argv,\n env: process.env,\n agentId: agent.id,\n fallbackWarning,\n shellFallbackWarning,\n promptWarnings,\n };\n}\n\nasync function resolveSessionPlan(\n input: ResolveLaunchPlanInput,\n terminal: TerminalChoice,\n): Promise<LaunchPlan> {\n const session = getSessionById(input.id);\n if (!session) {\n throw new LaunchError(\n 'session-not-found',\n `Session with id ${JSON.stringify(input.id)} not found`,\n );\n }\n\n let cwd = session.path;\n let fallbackWarning: string | null = null;\n\n if (session.projectSlug && session.assignmentSlug) {\n const detail = await getAssignmentDetail(\n input.projectsDir,\n session.projectSlug,\n session.assignmentSlug,\n );\n if (detail) {\n const picked = resolveWorkspaceCwd({\n worktreePath: detail.workspace.worktreePath,\n repository: detail.workspace.repository,\n branch: detail.workspace.branch,\n assignmentSlug: session.assignmentSlug,\n });\n if (picked.cwd !== null) {\n cwd = picked.cwd;\n fallbackWarning = picked.fallbackWarning;\n } else {\n // Neither worktree nor repository is a valid directory. Sessions keep\n // their recorded `session.path` (may be '') rather than failing the\n // launch — only assignment launches hard-error on an invalid workspace.\n fallbackWarning = formatFallbackCwdWarning({\n assignmentSlug: session.assignmentSlug,\n workspaceDir: session.path,\n worktreePath: detail.workspace.worktreePath,\n branch: detail.workspace.branch,\n });\n }\n }\n }\n\n const agent = getAgents(input.config).find((a) => a.id === session.agent);\n if (!agent) {\n throw new LaunchError(\n 'agent-not-configured',\n `Session ${input.id} was started with agent \"${session.agent}\" which is not in your agents list. Run \\`syntaur agents add ${session.agent}\\` or pick a different session.`,\n );\n }\n\n const { argv, shellFallbackWarning } = buildSessionArgv(\n agent,\n session.sessionId,\n input.mode ?? 'resume',\n );\n\n return {\n terminal,\n cwd,\n argv,\n env: process.env,\n agentId: agent.id,\n fallbackWarning,\n shellFallbackWarning,\n // Resume continues the SAME session id; fork mints a new one in-agent.\n session: { sessionId: (input.mode ?? 'resume') === 'resume' ? session.sessionId : null },\n };\n}\n\n","import { isValidSlug } from '../utils/slug.js';\n\n/**\n * Editable launch-prompt resolution.\n *\n * An agent profile may carry an editable `launchPrompt` template whose\n * `@`-tokens are expanded at launch time. This module is the single, pure home\n * for that expansion plus the low-level seed builders shared with the\n * (deprecated) `INITIAL_PROMPT`. It deliberately imports nothing from\n * `../tui/launch.js` so there is no import cycle (`argv.ts` imports `tui/launch`,\n * and `launch/index.ts` re-exports `argv.ts`) — callers import this module\n * directly, not via the `launch/index.js` barrel.\n */\n\n/**\n * The bare `/grab-assignment` seed — today's zero-config launch behavior. This\n * is the single source of these strings, shared by `resolveLaunchPrompt`'s\n * no-template fallback and `INITIAL_PROMPT`'s no-playbook branch (kept\n * byte-identical for back-compat).\n */\nexport function bareGrabSeed(params: {\n projectSlug: string | null;\n assignmentSlug: string;\n id?: string;\n}): string {\n if (params.projectSlug) {\n return `/grab-assignment ${params.projectSlug} ${params.assignmentSlug}`;\n }\n if (params.id) {\n return `/grab-assignment --id ${params.id}`;\n }\n // No project and no id — fall back to the slug. Should be rare; only happens\n // if a caller forgot to pass the id for a standalone assignment.\n return `/grab-assignment ${params.assignmentSlug}`;\n}\n\n/**\n * The noun phrase a `@<playbook-slug>` token resolves to. The template author\n * writes the surrounding verbs. NOTE: this is the new \"via the /run-playbook\n * skill\" wording used ONLY by the resolver — `INITIAL_PROMPT`'s legacy playbook\n * branch keeps its own \"using the /run-playbook skill\" sentence.\n */\nexport function runPlaybookClause(slug: string): string {\n return `the \\`${slug}\\` playbook via the /run-playbook skill`;\n}\n\n/** The `@assignment` expansion: a pointer to the records dir, not a snapshot. */\nfunction assignmentPointer(id: string | undefined, assignmentDir: string): string {\n const subject = id\n ? `This session is Syntaur assignment ${id}, with records at ${assignmentDir}.`\n : `This session's Syntaur assignment records are at ${assignmentDir}.`;\n return (\n `${subject} Claim and bind it with the /grab-assignment skill if available; ` +\n `otherwise read assignment.md, plan*.md, and progress.md in that directory for full context.`\n );\n}\n\nexport interface ResolveLaunchPromptInput {\n /** The agent's editable launch prompt template (may contain `@`-tokens). */\n template?: string | null;\n /** Back-compat playbook slug; used only when `template` is empty. */\n playbook?: string | null;\n /** Assignment id (optional only to represent the rare slug-fallback seed). */\n id?: string;\n /** Records directory (where assignment.md lives), for `@assignment`. */\n assignmentDir: string;\n /** Null for a standalone assignment. */\n projectSlug: string | null;\n assignmentSlug: string;\n /**\n * Installed playbook slugs, injected by the call site. When provided, a\n * well-formed `@<slug>` not in this set warns and is left literal. When\n * undefined, every well-formed slug resolves without validation.\n */\n knownPlaybookSlugs?: ReadonlySet<string>;\n}\n\nexport interface ResolveLaunchPromptResult {\n prompt: string;\n /** Non-fatal warnings (unknown/malformed `@`-tokens). Never throws. */\n warnings: string[];\n}\n\n/** `@` at start-of-string or after whitespace, then a maximal token run. */\nconst TOKEN_RE = /(^|\\s)@([A-Za-z0-9_-]+)/g;\n\nfunction resolveTemplate(\n template: string,\n ctx: { id?: string; assignmentDir: string; knownPlaybookSlugs?: ReadonlySet<string> },\n): ResolveLaunchPromptResult {\n const warnings: string[] = [];\n const prompt = template.replace(TOKEN_RE, (_match, boundary: string, token: string) => {\n if (token === 'assignment') {\n return boundary + assignmentPointer(ctx.id, ctx.assignmentDir);\n }\n if (!isValidSlug(token)) {\n warnings.push(`launchPrompt: \"@${token}\" is not a valid playbook token — left as literal text.`);\n return boundary + '@' + token;\n }\n if (ctx.knownPlaybookSlugs !== undefined && !ctx.knownPlaybookSlugs.has(token)) {\n warnings.push(`launchPrompt: playbook \"${token}\" (from \"@${token}\") is not installed — left as literal text.`);\n return boundary + '@' + token;\n }\n return boundary + runPlaybookClause(token);\n });\n return { prompt, warnings };\n}\n\n/**\n * Resolve the launch seed for a fresh \"Open in agent\" launch. Pure: never reads\n * the filesystem, never prints. The caller owns warning output.\n *\n * Fallback chain:\n * 1. `template` (trimmed non-empty) → resolve its `@`-tokens.\n * 2. else `playbook` set → synthesize `<@assignment pointer> Run <clause> end-to-end.`\n * (built directly — no `@`-token re-resolution, so a playbook literally named\n * `assignment` cannot collide with the reserved token).\n * 3. else → today's bare `/grab-assignment` seed.\n * `template` wins over `playbook`.\n */\nexport function resolveLaunchPrompt(input: ResolveLaunchPromptInput): ResolveLaunchPromptResult {\n const { template, playbook, id, assignmentDir, projectSlug, assignmentSlug, knownPlaybookSlugs } =\n input;\n\n if (template && template.trim()) {\n return resolveTemplate(template, { id, assignmentDir, knownPlaybookSlugs });\n }\n\n const pb = playbook?.trim();\n if (pb) {\n const pointer = assignmentPointer(id, assignmentDir);\n return { prompt: `${pointer} Run ${runPlaybookClause(pb)} end-to-end.`, warnings: [] };\n }\n\n return { prompt: bareGrabSeed({ projectSlug, assignmentSlug, id }), warnings: [] };\n}\n\n/**\n * The editable **template** to prefill the dashboard's \"Open in agent\" prompt\n * box — NOT the resolved text. Returns:\n * - `launchPrompt` verbatim when set (non-empty after trim); else\n * - a synth template `@assignment Run <runPlaybookClause(playbook)> end-to-end.`\n * when `playbook` is set — the playbook clause is LITERAL (only `@assignment`\n * is a token), so re-resolving this through `resolveLaunchPrompt` reproduces\n * this module's playbook synth (above) byte-for-byte for ANY playbook\n * (installed / disabled / uninstalled / the reserved `assignment`); else\n * - the bare `/grab-assignment` seed (no `@`-tokens).\n *\n * Prefilling the template (not resolved text) and resolving exactly once at\n * launch avoids re-tokenizing an `@<slug>` that may appear inside an expanded\n * records-dir path.\n */\nexport function effectiveLaunchTemplate(input: {\n launchPrompt?: string | null;\n playbook?: string | null;\n projectSlug: string | null;\n assignmentSlug: string;\n id?: string;\n}): string {\n if (input.launchPrompt && input.launchPrompt.trim()) {\n return input.launchPrompt;\n }\n const pb = input.playbook?.trim();\n if (pb) {\n return `@assignment Run ${runPlaybookClause(pb)} end-to-end.`;\n }\n return bareGrabSeed({\n projectSlug: input.projectSlug,\n assignmentSlug: input.assignmentSlug,\n id: input.id,\n });\n}\n","import { isAbsolute } from 'node:path';\nimport type { AgentConfig } from '../utils/config.js';\nimport { applyModelFlag } from '../utils/agents-schema.js';\nimport { buildAgentArgv, shellQuote } from '../tui/launch.js';\nimport type { BuiltArgv } from './types.js';\nimport { LaunchError } from './plan.js';\nimport type { SessionMode } from './url.js';\n\n/**\n * Re-export the fresh-launch argv builder under a parallel name so the launch\n * core has a single import surface: `buildFreshArgv` (new run) and\n * `buildSessionArgv` (resume/fork an existing session).\n */\nexport const buildFreshArgv = buildAgentArgv;\n\n/**\n * Build argv for continuing an existing agent session under a specific mode.\n *\n * The argv shape per agent is declared in `AgentConfig.resume` / `.fork`\n * (`SessionInvocation`):\n * - `args` is a literal argv list. The substring `{id}` is replaced with\n * `sessionId`.\n * - `command` optionally overrides `agent.command` for subcommand-style\n * agents whose binary differs (none in builtins).\n *\n * Existing `agent.args` (the base flags applied to a fresh launch — e.g.\n * `--dangerously-skip-permissions`) are preserved and prefixed before the\n * invocation args, matching the prior `buildResumeArgv` behavior.\n *\n * The `resolveFromShellAliases` rewriting is preserved identically: the\n * command is rewritten to `$SHELL`/`/bin/sh` and args become\n * `['-i', '-c', '<quoted>']`. The quoted command line uses the (possibly\n * overridden) executable.\n *\n * Throws `LaunchError('mode-not-supported', ...)` when the agent has no\n * entry for the requested mode.\n */\nexport function buildSessionArgv(\n agent: AgentConfig,\n sessionId: string,\n mode: SessionMode,\n env: NodeJS.ProcessEnv = process.env,\n): BuiltArgv {\n const invocation = agent[mode];\n if (!invocation) {\n throw new LaunchError(\n 'mode-not-supported',\n `Agent \"${agent.id}\" does not support ${mode} (no agent.${mode} configured)`,\n );\n }\n\n const substituted = invocation.args.map((a) =>\n a === '{id}' ? sessionId : a,\n );\n const command = invocation.command ?? agent.command;\n // Profile model is applied to the agent's own args (stripping any pre-existing\n // `--model` so we never emit a duplicate) and sits BEFORE the resume/fork\n // `substituted` args — subcommand-style agents need `--model <v>` ahead of the\n // `resume`/`fork` token (e.g. `codex --model <v> resume <id>`).\n const agentArgs = [...applyModelFlag(agent, [...(agent.args ?? [])]), ...substituted];\n\n if (agent.resolveFromShellAliases) {\n const requested = env.SHELL;\n let shell = requested;\n let warning: string | null = null;\n if (!shell || !isAbsolute(shell)) {\n warning = `syntaur: $SHELL ${\n requested ? `(\"${requested}\") is not absolute` : 'is unset'\n } — falling back to /bin/sh for shell-alias resolution`;\n shell = '/bin/sh';\n }\n const quoted = [command, ...agentArgs].map(shellQuote).join(' ');\n return {\n argv: { command: shell, args: ['-i', '-c', quoted] },\n shellFallbackWarning: warning,\n };\n }\n\n return {\n argv: { command, args: agentArgs },\n shellFallbackWarning: null,\n };\n}\n","import { spawn } from 'node:child_process';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { isAbsolute, resolve } from 'node:path';\nimport { getAssignmentDetail } from '../dashboard/api.js';\nimport type { AgentConfig } from '../utils/config.js';\nimport { applyModelFlag } from '../utils/agents-schema.js';\nimport type { BuiltArgv } from '../launch/types.js';\nimport {\n formatFallbackCwdWarning,\n isExistingDir,\n resolveWorkspaceCwd,\n} from '../launch/cwd.js';\nimport type { SpawnFn } from '../launch/execute.js';\nimport { bareGrabSeed, resolveLaunchPrompt } from '../launch/launch-prompt.js';\nimport { playbooksDir } from '../utils/paths.js';\nimport { listPlaybookSlugs } from '../utils/playbooks.js';\n\nexport type { ResolvedArgv, BuiltArgv } from '../launch/types.js';\n// `formatFallbackCwdWarning` now lives in ../launch/cwd.ts (a neutral module so\n// plan.ts can import the cwd helpers without a cycle). Re-exported here so the\n// existing `import { formatFallbackCwdWarning } from '../tui/launch.js'` sites\n// (e.g. launch-argv.test.ts) keep working.\nexport { formatFallbackCwdWarning } from '../launch/cwd.js';\n\nexport interface LaunchOptions {\n projectsDir: string;\n projectSlug: string;\n assignmentSlug: string;\n agent: AgentConfig;\n cwdOverride?: string;\n /**\n * Test hook: called with the exit code of the spawned child instead of\n * `process.exit(code)`. Default behavior is `process.exit`. Production\n * callers should leave this unset.\n */\n onExit?: (code: number) => void;\n /**\n * Test hook: replaces `child_process.spawn` so unit tests can assert exactly\n * what (and with which cwd) the launcher invoked without spawning a real\n * process. Default is the real `spawn`. Production callers leave this unset.\n */\n spawnFn?: SpawnFn;\n}\n\n/**\n * Initial message sent to the agent the first time it starts up at an\n * assignment. This is the protocol entry point: `/grab-assignment` is the\n * Claude Code skill that loads project/playbook/memory context for the\n * assignment and (per its pre-flight check) prompts the user if a different\n * assignment is already active in this workspace.\n *\n * Argument shapes match the skill's documented input:\n * - project-nested: `/grab-assignment <project-slug> <assignment-slug>`\n * - standalone: `/grab-assignment --id <uuid>`\n *\n * When `playbook` is set (an agent runner profile), the seed switches to an\n * instruction-style message that chains BOTH `/grab-assignment` and\n * `/run-playbook`. This is deliberate: a Claude Code message fires only ONE\n * leading slash-command — everything after it is swallowed as that command's\n * arguments — so two slash-commands cannot be issued from a single seed. A\n * plain-language instruction lets the agent invoke both skills itself\n * (grab-assignment loads playbook *context*; run-playbook *executes* a specific\n * enabled playbook end-to-end — complementary, not redundant). The no-playbook\n * path keeps the exact, well-tested `/grab-assignment` invocation unchanged.\n */\n/**\n * @deprecated Both launch call sites now route through `resolveLaunchPrompt`\n * (`../launch/launch-prompt.js`), which supports the editable `launchPrompt`\n * field. `INITIAL_PROMPT` is retained only for its existing tests / transitional\n * reference; its no-playbook branch shares `bareGrabSeed` with the resolver so\n * those bare-seed strings stay byte-identical.\n */\nexport const INITIAL_PROMPT = (params: {\n projectSlug: string | null;\n assignmentSlug: string;\n id?: string;\n playbook?: string | null;\n}): string => {\n const playbook = params.playbook?.trim();\n\n if (!playbook) {\n return bareGrabSeed({\n projectSlug: params.projectSlug,\n assignmentSlug: params.assignmentSlug,\n id: params.id,\n });\n }\n\n // Playbook profile: chain grab + run-playbook via a plain-language seed.\n const grabClause = params.projectSlug\n ? `the assignment \\`${params.projectSlug}/${params.assignmentSlug}\\` using the /grab-assignment skill`\n : params.id\n ? `the assignment id \\`${params.id}\\` using /grab-assignment --id ${params.id}`\n : `the assignment \\`${params.assignmentSlug}\\` using the /grab-assignment skill`;\n return (\n `Grab ${grabClause}, then load and run the \\`${playbook}\\` playbook ` +\n `using the /run-playbook skill and carry it out end-to-end.`\n );\n};\n\n/**\n * POSIX single-quote shell escaping. Safe to embed in `sh -c '<result>'`.\n * Replaces ' with '\\'' and wraps the whole value in single quotes.\n */\nexport function shellQuote(arg: string): string {\n if (arg === '') return \"''\";\n return `'${arg.replace(/'/g, `'\\\\''`)}'`;\n}\n\n/**\n * Build argv for an agent launch. Handles:\n * - `resolveFromShellAliases: true` → `$SHELL -i -c '<quoted...>'`\n * - `promptArgPosition: 'first' | 'last' | 'none'`\n * - plain absolute or bare-name command.\n */\nexport function buildAgentArgv(\n agent: AgentConfig,\n prompt: string,\n env: NodeJS.ProcessEnv = process.env,\n): BuiltArgv {\n const position = agent.promptArgPosition ?? 'first';\n // Profile model is appended after the agent's own args (and any pre-existing\n // `--model` in those args is stripped first) so exactly one authoritative\n // `--model` is emitted — never a duplicate, which some CLIs reject.\n const baseArgs = applyModelFlag(agent, [...(agent.args ?? [])]);\n const agentArgs =\n position === 'first'\n ? [prompt, ...baseArgs]\n : position === 'last'\n ? [...baseArgs, prompt]\n : baseArgs;\n\n if (agent.resolveFromShellAliases) {\n const requested = env.SHELL;\n let shell = requested;\n let warning: string | null = null;\n if (!shell || !isAbsolute(shell)) {\n warning = `syntaur: $SHELL ${\n requested ? `(\"${requested}\") is not absolute` : 'is unset'\n } — falling back to /bin/sh for shell-alias resolution`;\n shell = '/bin/sh';\n }\n const quoted = [agent.command, ...agentArgs].map(shellQuote).join(' ');\n return {\n argv: { command: shell, args: ['-i', '-c', quoted] },\n shellFallbackWarning: warning,\n };\n }\n\n return {\n argv: { command: agent.command, args: agentArgs },\n shellFallbackWarning: null,\n };\n}\n\nexport async function launchAgent(options: LaunchOptions): Promise<void> {\n const { projectsDir, projectSlug, assignmentSlug, agent, cwdOverride } = options;\n const exitWith = options.onExit ?? ((code: number) => process.exit(code));\n\n const detail = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);\n if (!detail) {\n console.error(`Assignment not found: ${projectSlug}/${assignmentSlug}`);\n process.exit(1);\n }\n\n const projectDir = resolve(projectsDir, projectSlug);\n const assignmentDir = resolve(projectDir, 'assignments', assignmentSlug);\n\n // Resolve + VALIDATE the working directory before writing context.json or\n // spawning. Never silently fall back to process.cwd() — refuse the launch so\n // we don't open the agent (or write context) in the wrong directory.\n let workspaceDir: string;\n if (cwdOverride) {\n // An explicit, present-but-invalid override is a caller bug — hard error\n // rather than silently falling through to the workspace fields.\n if (!isExistingDir(cwdOverride)) {\n console.error(\n `syntaur: --cwd ${cwdOverride} is not an existing directory — refusing to launch.`,\n );\n exitWith(1);\n return;\n }\n workspaceDir = cwdOverride;\n } else {\n const picked = resolveWorkspaceCwd({\n worktreePath: detail.workspace.worktreePath,\n repository: detail.workspace.repository,\n branch: detail.workspace.branch,\n assignmentSlug,\n });\n if (picked.cwd === null) {\n console.error(`syntaur: ${picked.invalidReason} — refusing to launch.`);\n exitWith(1);\n return;\n }\n workspaceDir = picked.cwd;\n // Preserve the existing missing-field warning behavior: when worktree is\n // valid but `branch` (or worktreePath) is unset we still nudge the user.\n // `picked.fallbackWarning` covers the worktree→repository fallback cases.\n const warning =\n picked.fallbackWarning ??\n formatFallbackCwdWarning({\n assignmentSlug,\n workspaceDir,\n worktreePath: detail.workspace.worktreePath,\n branch: detail.workspace.branch,\n });\n if (warning) console.warn(warning);\n }\n\n const contextDir = resolve(workspaceDir, '.syntaur');\n await mkdir(contextDir, { recursive: true });\n\n const context = {\n projectSlug,\n assignmentSlug,\n projectDir,\n assignmentDir,\n workspaceRoot: workspaceDir,\n title: detail.title,\n branch: detail.workspace.branch ?? null,\n grabbedAt: new Date().toISOString(),\n };\n\n await writeFile(\n resolve(contextDir, 'context.json'),\n JSON.stringify(context, null, 2) + '\\n',\n );\n\n const knownPlaybookSlugs = await listPlaybookSlugs(playbooksDir());\n const { prompt, warnings } = resolveLaunchPrompt({\n template: agent.launchPrompt,\n playbook: agent.playbook,\n id: detail.id,\n assignmentDir,\n projectSlug,\n assignmentSlug,\n knownPlaybookSlugs,\n });\n for (const warning of warnings) console.warn(warning);\n\n const { argv, shellFallbackWarning } = buildAgentArgv(agent, prompt);\n if (shellFallbackWarning) {\n console.warn(shellFallbackWarning);\n }\n\n const spawnImpl = options.spawnFn ?? spawn;\n return new Promise<void>((resolvePromise) => {\n const child = spawnImpl(argv.command, argv.args, {\n cwd: workspaceDir,\n stdio: 'inherit',\n });\n\n child.on('error', (err) => {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') {\n console.error(\n `syntaur: agent \"${agent.id}\" command \"${agent.command}\" not found. ` +\n `If \"${agent.command}\" is a shell alias, set resolveFromShellAliases: true on this agent in ~/.syntaur/config.md.`,\n );\n } else if (code === 'EACCES') {\n console.error(\n `syntaur: agent \"${agent.id}\" command \"${agent.command}\" is not executable (EACCES). ` +\n `Check file permissions.`,\n );\n } else {\n console.error(\n `syntaur: failed to launch agent \"${agent.id}\" (${code ?? 'unknown'}): ${err.message}`,\n );\n }\n resolvePromise();\n exitWith(1);\n });\n\n child.on('exit', (code) => {\n resolvePromise();\n exitWith(code ?? 0);\n });\n });\n}\n","import { spawn, type ChildProcess, type SpawnOptions } from 'node:child_process';\nimport { homedir } from 'node:os';\nimport { basename, join, resolve } from 'node:path';\nimport { shellQuote } from '../tui/launch.js';\nimport { resolveCmuxCli } from '../utils/terminal-probe.js';\nimport { fileExists } from '../utils/fs.js';\nimport { readConfig } from '../utils/config.js';\nimport { writeRuntimeMarker } from '../utils/session-id.js';\nimport { captureProcessStartedAt } from '../utils/process-info.js';\nimport type { LaunchPlan } from './plan.js';\nimport type { TerminalChoice } from '../utils/config.js';\n\nexport class TerminalNotFoundError extends Error {\n readonly terminal: TerminalChoice;\n readonly remediation: string;\n constructor(terminal: TerminalChoice, remediation: string) {\n super(\n `Terminal \"${terminal}\" is not installed or not invokable. ${remediation}`,\n );\n this.terminal = terminal;\n this.remediation = remediation;\n this.name = 'TerminalNotFoundError';\n }\n}\n\n/**\n * Test hook: a function that replaces `child_process.spawn` so unit tests can\n * assert exactly what the launcher invoked without spawning real processes.\n * Must return a `ChildProcess`-shaped object — `executeLaunchPlan` listens for\n * `'error'`, `'spawn'`, and `'exit'` events to detect missing terminals.\n */\nexport type SpawnFn = (\n command: string,\n args: readonly string[],\n options: SpawnOptions,\n) => ChildProcess;\n\nconst realSpawn: SpawnFn = (command, args, options) =>\n spawn(command, args as string[], options);\n\n/**\n * Result of a launch — enough for the scheduler to acknowledge the launch\n * (poll for a runtime marker / session row attributable to it). For wrapper\n * terminals (osascript/open/sh) `pid` is the wrapper's pid, not the agent's;\n * the launch-ack scanner matches by cwd + write-time rather than pid alone.\n * Interactive callers ignore this value (the return was previously `void`).\n */\nexport interface LaunchHandle {\n pid: number | undefined;\n plan: LaunchPlan;\n /** ISO timestamp captured once the spawn settled. */\n startedAt: string;\n}\n\n/**\n * Commands we treat as \"wrappers\" that synchronously delegate to the actual\n * terminal app. These fail fast (non-zero exit + stderr) when the target app\n * or URL scheme isn't installed, so we monitor their exit code briefly.\n *\n * Membership is tested by BASENAME (see `isWrapperCommand`): `osascript`/`open`\n * are spawned by bare name, but cmux launches via an absolute `/bin/sh -c`\n * cold-start wrapper, so a plain set-membership check on the full command\n * (`/bin/sh`) would miss `'sh'` and wrongly classify it as a long-running\n * launcher (skipping exit-code monitoring).\n */\nconst WRAPPER_COMMANDS = new Set(['osascript', 'open', 'sh']);\n\n/**\n * A command is a wrapper if its basename is in `WRAPPER_COMMANDS`. Using the\n * basename lets cmux's absolute `/bin/sh` interpreter match `'sh'` while leaving\n * the bare `osascript`/`open` names (basename === name) unaffected.\n */\nfunction isWrapperCommand(command: string): boolean {\n return WRAPPER_COMMANDS.has(basename(command));\n}\n\n/**\n * How long we wait for a wrapper (osascript/open) to exit before assuming it\n * spawned the target app successfully and detaching. Wrappers that succeed\n * usually exit in tens of milliseconds; wrappers that fail exit even faster.\n * A small window keeps the CLI responsive without missing legitimate failures.\n *\n * Per-invocation override via `TerminalInvocation.wrapperTimeoutMs` — cmux needs\n * a larger window because its cold-start script can poll for socket readiness\n * for several seconds before it exits with the real success/failure code.\n */\nconst WRAPPER_EXIT_TIMEOUT_MS = 1500;\n\n/**\n * Run the launch plan: spawn the configured terminal in a new window with the\n * resolved cwd + agent argv. Returns once the spawn has been initiated and\n * confirmed; for wrapper commands (osascript/open) it briefly waits for the\n * wrapper to exit so that missing apps surface as a non-zero CLI exit.\n *\n * Throws `TerminalNotFoundError` when the spawn errors (ENOENT on direct CLI\n * launchers) or when a wrapper exits non-zero (target app missing).\n */\nexport async function executeLaunchPlan(\n plan: LaunchPlan,\n spawnFn: SpawnFn = realSpawn,\n): Promise<LaunchHandle> {\n if (plan.terminal === 'warp') {\n // Warp's URI scheme opens a window at the cwd but does not auto-start a\n // command — there is no documented `command=` parameter. Surface this so\n // the user knows to start the agent themselves once the window opens.\n console.error(\n `syntaur: Warp will open a window at ${plan.cwd} but cannot auto-start ${plan.argv.command} — run it yourself once the window appears`,\n );\n }\n const invocation = buildTerminalInvocation(plan);\n const isWrapper = isWrapperCommand(invocation.command);\n\n // Capture the launch timestamp at SPAWN time, not after the wrapper-exit wait:\n // a fast agent can write its real session marker before the wrapper safety-net\n // resolves, and launch-ack matches markers written at/after this instant.\n const startedAt = new Date().toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n\n let child: ChildProcess;\n try {\n child = spawnFn(invocation.command, invocation.args, {\n detached: true,\n // Wrappers: capture stderr so we can surface error text. Direct CLI\n // launchers: ignore all streams so they keep running after we detach.\n stdio: isWrapper ? ['ignore', 'ignore', 'pipe'] : 'ignore',\n env: plan.env,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new TerminalNotFoundError(\n plan.terminal,\n `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`,\n );\n }\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n let stderr = '';\n\n const finishOk = () => {\n if (settled) return;\n settled = true;\n try { child.unref(); } catch { /* unref can throw if already exited */ }\n resolve();\n };\n\n const finishErr = (remediation: string) => {\n if (settled) return;\n settled = true;\n reject(new TerminalNotFoundError(plan.terminal, remediation));\n };\n\n if (child.stderr) {\n child.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n }\n\n child.once('error', (err: Error) => {\n finishErr(\n `Spawn failed: ${err.message}. Verify the terminal is installed and on PATH.`,\n );\n });\n\n if (isWrapper) {\n child.once('exit', (code, signal) => {\n if (code === 0 || code === null) {\n finishOk();\n } else {\n const detail = stderr.trim() || (\n signal\n ? `terminated by signal ${signal}`\n : 'check that the terminal app is installed and the URL scheme handler is registered'\n );\n finishErr(`${invocation.command} exited with code ${code}: ${detail}`);\n }\n });\n // Safety net: if the wrapper hasn't exited within the window, assume\n // success and detach. This is the normal \"Terminal.app spawned, wrapper\n // is keeping the connection open\" case. cmux overrides the window\n // (wrapperTimeoutMs) so it exceeds the cold-start readiness poll —\n // otherwise a slow cold-start FAILURE would be masked as success here.\n setTimeout(\n finishOk,\n invocation.wrapperTimeoutMs ?? WRAPPER_EXIT_TIMEOUT_MS,\n ).unref();\n } else {\n child.once('spawn', () => {\n finishOk();\n });\n }\n });\n\n await registerLaunchAtBirth(plan, child);\n\n return { pid: child.pid, plan, startedAt };\n}\n\n/**\n * Register-at-birth: anything Syntaur spawns leaves a generic runtime marker\n * at `~/.syntaur/runtime/sessions/<pid>.json` regardless of the agent's hook\n * support, and — when the session id is already known (resume-mode launches) —\n * an active row in the sessions DB. Fresh/fork launches write a PENDING marker\n * (no sessionId; ids are never synthesized) and the scanner closes the gap on\n * its next tick. For wrapper terminals (osascript/open/sh) the pid is the\n * wrapper's, not the agent's — acceptable: the ancestor-walk in\n * `resolveOwnSessionId` tolerates intermediate pids and the scanner is the\n * guaranteed floor. Best-effort: never throws, never fails the launch.\n */\nasync function registerLaunchAtBirth(plan: LaunchPlan, child: ChildProcess): Promise<void> {\n try {\n const pid = child.pid;\n if (!pid) return;\n\n const autoTrack = (await readConfig()).session.autoTrack;\n if (autoTrack === 'off') return;\n\n const sessionId = plan.session?.sessionId ?? null;\n const procStart = captureProcessStartedAt(pid);\n\n const envDir = process.env.SYNTAUR_RUNTIME_SESSIONS_DIR;\n const markerDir = envDir && envDir.length > 0\n ? envDir\n : join(homedir(), '.syntaur', 'runtime', 'sessions');\n writeRuntimeMarker(\n pid,\n {\n ...(sessionId ? { sessionId } : {}),\n agent: plan.agentId,\n cwd: plan.cwd,\n ...(procStart ? { procStart } : {}),\n writtenAt: Date.now(),\n },\n markerDir,\n );\n\n if (!sessionId) return;\n if (\n autoTrack === 'workspaces-only' &&\n !(await fileExists(resolve(plan.cwd, '.syntaur', 'context.json')))\n ) {\n return;\n }\n\n const { initSessionDb } = await import('../dashboard/session-db.js');\n const { appendSession } = await import('../dashboard/agent-sessions.js');\n initSessionDb();\n await appendSession(\n '',\n {\n sessionId,\n projectSlug: null,\n assignmentSlug: null,\n agent: plan.agentId,\n started: new Date().toISOString(),\n status: 'active',\n path: plan.cwd,\n description: null,\n transcriptPath: null,\n pid,\n pidStartedAt: procStart,\n originalHeadSha: null,\n },\n // Resuming a stopped session IS live-process evidence — we just spawned it.\n { reviveStopped: true },\n );\n } catch {\n // Best-effort only — a tracking failure must never fail the launch.\n }\n}\n\ninterface TerminalInvocation {\n command: string;\n args: string[];\n /**\n * Override for the wrapper-exit safety-net window (ms). Set only by terminals\n * whose wrapper legitimately runs longer than `WRAPPER_EXIT_TIMEOUT_MS`\n * before exiting (cmux's cold-start readiness poll). Omitted = default.\n */\n wrapperTimeoutMs?: number;\n}\n\n/** cmux app bundle id, used to launch it on a cold start via `open -b`. */\nconst CMUX_BUNDLE_ID = 'com.cmuxterm.app';\n\n/**\n * Upper bound (ms) on cmux's cold-start readiness poll: CMUX_LAUNCH_SCRIPT tries\n * 20 times at 0.25s = 5s. Keep these two in sync if the script's loop changes.\n */\nconst CMUX_READINESS_MAX_MS = 20 * 250;\n\n/**\n * Wrapper safety-net window for cmux. Must exceed CMUX_READINESS_MAX_MS (plus\n * app-launch + workspace-create overhead) so that a cold-start failure exits\n * with its real non-zero code and surfaces as a TerminalNotFoundError, rather\n * than the safety net falsely resolving success mid-poll.\n */\nconst CMUX_LAUNCH_TIMEOUT_MS = CMUX_READINESS_MAX_MS + 3000;\n\n/**\n * POSIX-sh cold-start orchestration for cmux, run as a single monitored\n * `/bin/sh -c` spawn. `workspace create` is a socket-control command that needs\n * the cmux app running, so on a cold start (app closed) it would fail. This\n * script: (1) launches cmux if needed via `open -b` (a no-op when already\n * running; PATH-independent — `open` is at /usr/bin even under the applet's\n * stripped PATH); (2) polls `cmux ping` for socket readiness, bounded so it\n * never hangs; (3) `exec`s `workspace create` so its exit code is the script's\n * exit code (a failure surfaces as TerminalNotFoundError via the wrapper path).\n *\n * Values are passed as positional args ($1=cli, $2=cwd, $3=command) rather than\n * interpolated, so no second layer of shell-quoting is needed and a hostile cwd\n * or command cannot break out of the script.\n */\nconst CMUX_LAUNCH_SCRIPT = [\n `open -b ${CMUX_BUNDLE_ID} >/dev/null 2>&1 || true`,\n 'i=0',\n 'while [ \"$i\" -lt 20 ]; do',\n ' \"$1\" ping >/dev/null 2>&1 && break',\n ' i=$((i + 1))',\n ' sleep 0.25',\n 'done',\n 'exec \"$1\" workspace create --cwd \"$2\" --command \"$3\" --focus true',\n].join('\\n');\n\n/**\n * The agent command line with every token shell-quoted, WITHOUT a `cd` prefix:\n * `'<command>' '<arg>' …`. cmux uses this directly (it sets the workspace cwd\n * via `--cwd`, so it must not prepend `cd`); `buildShellCommandLine` adds the\n * `cd` for the terminals that drop the user into a shell.\n */\nexport function buildAgentCommandLine(plan: LaunchPlan): string {\n return [plan.argv.command, ...plan.argv.args].map(shellQuote).join(' ');\n}\n\n/**\n * Build the plain POSIX shell command line that actually runs inside the\n * terminal: `cd '<cwd>' && '<command>' '<arg>' …` with every token\n * shell-quoted. This is the single source of truth for \"the command the launch\n * button runs\" — consumed by `buildTerminalInvocation` (which wraps it per\n * terminal app) and by the dashboard's copy-launch-command endpoint. Exported\n * for reuse + unit testing.\n */\nexport function buildShellCommandLine(plan: LaunchPlan): string {\n return `cd ${shellQuote(plan.cwd)} && ${buildAgentCommandLine(plan)}`;\n}\n\n/**\n * Build the argv that will be handed to `spawn` to open `plan.argv` in a new\n * window of `plan.terminal` at `plan.cwd`. Exported for unit testing.\n */\nexport function buildTerminalInvocation(plan: LaunchPlan): TerminalInvocation {\n const cdAndRun = buildShellCommandLine(plan);\n\n switch (plan.terminal) {\n case 'terminal-app':\n // Terminal.app cold-start quirk: launching it auto-opens a blank window,\n // and `do script` opens ANOTHER — two windows, one blank. Capture the\n // running state BEFORE the `tell` block (addressing Terminal would launch\n // it), then on a cold start run the command in the blank launch window\n // instead of opening a second one. Warm starts still get a fresh window.\n return {\n command: 'osascript',\n args: [\n '-e',\n 'set wasRunning to application \"Terminal\" is running',\n '-e',\n 'tell application \"Terminal\"',\n '-e',\n 'activate',\n '-e',\n 'if wasRunning then',\n '-e',\n `do script ${appleScriptString(cdAndRun)}`,\n '-e',\n 'else',\n '-e',\n 'repeat until (count of windows) > 0',\n '-e',\n 'delay 0.1',\n '-e',\n 'end repeat',\n '-e',\n `do script ${appleScriptString(cdAndRun)} in window 1`,\n '-e',\n 'end if',\n '-e',\n 'end tell',\n ],\n };\n\n case 'iterm':\n // iTerm2's AppleScript dictionary uses the application name `iTerm` in\n // tell blocks (per https://iterm2.com/documentation-scripting.html),\n // even though the bundle id is `com.googlecode.iterm2`. If a future\n // iTerm release switches to \"iTerm2\", the doctor check's bundle-id\n // lookup will still succeed; only this script would need updating.\n return {\n command: 'osascript',\n args: [\n '-e',\n 'tell application \"iTerm\"',\n '-e',\n 'activate',\n '-e',\n 'set newWindow to (create window with default profile)',\n '-e',\n `tell current session of newWindow to write text ${appleScriptString(cdAndRun)}`,\n '-e',\n 'end tell',\n ],\n };\n\n case 'ghostty':\n // Ghostty's AppleScript dictionary doesn't actually expose\n // `new window` / `terminal` / `input text` / `send key` as usable\n // verbs at runtime — calls fail with \"Can't make new window into\n // integer\" / \"can't get terminal 1\". Drive Ghostty via synthesized\n // key events instead: activate the app, press Cmd-N for a new\n // window, type the command, then press Return.\n //\n // Requires Accessibility permission for the process that emits the\n // Apple Events (here: `osascript` itself). macOS will prompt the\n // first time this code path fires.\n return {\n command: 'osascript',\n args: [\n '-e',\n 'tell application \"Ghostty\" to activate',\n '-e',\n 'delay 0.3',\n '-e',\n 'tell application \"System Events\"',\n '-e',\n 'keystroke \"n\" using command down',\n '-e',\n 'delay 0.4',\n '-e',\n `keystroke ${appleScriptString(cdAndRun)}`,\n '-e',\n 'key code 36',\n '-e',\n 'end tell',\n ],\n };\n\n case 'alacritty':\n return {\n command: 'alacritty',\n args: [\n '--working-directory',\n plan.cwd,\n '-e',\n plan.argv.command,\n ...plan.argv.args,\n ],\n };\n\n case 'warp': {\n // Warp's URI scheme (https://docs.warp.dev/terminal/more-features/uri-scheme)\n // supports `warp://action/new_window?path=...` but does NOT accept a\n // `command=` param — the agent is not auto-started. `executeLaunchPlan`\n // emits a console.error warning above so the user knows to start the\n // agent manually once the Warp window appears. If a future Warp version\n // adds `command=` (or a documented alternative), update this branch\n // and drop the warning.\n const params = new URLSearchParams({ path: plan.cwd });\n return {\n command: 'open',\n args: [`warp://action/new_window?${params.toString()}`],\n };\n }\n\n case 'kitty':\n // Two-path strategy from the plan: prefer `kitty @ launch` when remote\n // control is enabled (gated by the doctor `terminal.kitty-remote-control`\n // check; if disabled the agent still gets launched, just via the\n // simpler path here). The `--` separator is required so `-`-prefixed\n // args like `--resume` reach the agent rather than kitty itself.\n return {\n command: 'kitty',\n args: [\n '--directory',\n plan.cwd,\n '--',\n plan.argv.command,\n ...plan.argv.args,\n ],\n };\n\n case 'cmux':\n // cmux is a socket-controlled workspace multiplexer driven by its\n // first-party CLI, which lives INSIDE the app bundle and is not on a\n // standard PATH dir. The macOS URL-handler applet launches with a\n // stripped LaunchServices PATH, so we resolve the CLI to an absolute path\n // (resolveCmuxCli: bundle → canonical dir → running-app via lsappinfo →\n // `which` off-darwin) rather than relying on a bare `cmux` (which would\n // ENOENT there). Canonical hits keep priority over the running-app lookup:\n // when a canonical copy exists but a different copy is running (e.g. off a\n // DMG), the canonical CLI still drives the running app over the shared\n // socket — fine while versions match, could skew after an update. Because\n // `workspace create` is\n // a socket command that needs the app running, we wrap it in a cold-start\n // `/bin/sh -c` script (CMUX_LAUNCH_SCRIPT): launch-if-needed via `open\n // -b`, await socket readiness, then `workspace create --cwd <cwd>\n // --command <cmd> --focus true` (which makes a workspace at --cwd and\n // sends the agent command text+Enter to it). The command is the bare\n // shell-quoted agent command (NO `cd` prefix — cmux sets the cwd via\n // --cwd) because cmux types it into the new workspace's shell. The /bin/sh\n // interpreter is registered in WRAPPER_COMMANDS (matched by basename\n // 'sh'), so a missing binary or dead socket surfaces as a\n // TerminalNotFoundError.\n return {\n command: '/bin/sh',\n args: [\n '-c',\n CMUX_LAUNCH_SCRIPT,\n 'syntaur-cmux-launch', // $0 (label in ps / error messages)\n resolveCmuxCli() ?? 'cmux', // $1\n plan.cwd, // $2\n buildAgentCommandLine(plan), // $3\n ],\n // Exceed the cold-start readiness poll so a failed cold launch surfaces\n // as an error instead of being masked by the wrapper safety net.\n wrapperTimeoutMs: CMUX_LAUNCH_TIMEOUT_MS,\n };\n }\n}\n\n/**\n * Quote a string for embedding inside an AppleScript double-quoted literal.\n * AppleScript interprets a literal backslash and a literal double-quote inside\n * \"...\" strings; everything else passes through.\n */\nfunction appleScriptString(value: string): string {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n","import { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { TerminalChoice } from './terminal-schema.js';\n\n/**\n * macOS bundle identifiers for Apple-Event-driven terminals. Used with\n * `mdfind kMDItemCFBundleIdentifier == '<id>'` to confirm install.\n */\nexport const APP_BUNDLE_IDS: Partial<Record<TerminalChoice, string>> = {\n 'terminal-app': 'com.apple.Terminal',\n iterm: 'com.googlecode.iterm2',\n ghostty: 'com.mitchellh.ghostty',\n warp: 'dev.warp.Warp-Stable',\n};\n\n/**\n * Standard `.app` bundle locations, used as a fallback when `mdfind` returns\n * nothing — `mdfind` exits 0 with empty stdout in non-indexed / launchd /\n * background contexts, falsely reporting an installed app as missing. For\n * non-system terminals these are bundle *names* resolved against the\n * applications directories; Terminal.app ships at a fixed system path.\n *\n * Keep in sync with `detectInstalledTerminals()` in\n * scripts/install-macos-url-handler.mjs — EXCEPT `cmux`, which is intentionally\n * present here but absent from `detectInstalledTerminals()`. That function lists\n * the *AppleScript-driven* terminals (the applet runs `tell application` blocks\n * for them); cmux is CLI-driven and falls through to `executeLaunchPlan`, so it\n * must not be added there. `cmux.app` is listed here only so `findAppBundle`\n * (and `resolveCmuxCli`) can locate the bundle that contains the cmux CLI.\n */\nexport const APP_BUNDLE_NAMES: Partial<Record<TerminalChoice, string>> = {\n iterm: 'iTerm.app',\n ghostty: 'Ghostty.app',\n warp: 'Warp.app',\n cmux: 'cmux.app',\n};\n\n/** Fixed absolute paths for apps not found under the applications dirs. */\nconst APP_FIXED_PATHS: Partial<Record<TerminalChoice, string>> = {\n 'terminal-app': '/System/Applications/Utilities/Terminal.app',\n};\n\n/** Default macOS application directories searched for `.app` bundles. */\nfunction defaultApplicationsDirs(): string[] {\n return ['/Applications', join(homedir(), 'Applications')];\n}\n\n/**\n * Find an installed `.app` bundle for a terminal by checking standard\n * locations on disk. Returns the absolute path to the bundle, or null. The\n * `dirs` parameter is injectable so tests can point at a temp directory\n * instead of the host's real /Applications.\n */\nexport function findAppBundle(\n terminal: TerminalChoice,\n dirs: string[] = defaultApplicationsDirs(),\n): string | null {\n const fixed = APP_FIXED_PATHS[terminal];\n if (fixed && existsSync(fixed)) return fixed;\n\n const bundleName = APP_BUNDLE_NAMES[terminal];\n if (bundleName) {\n for (const dir of dirs) {\n const candidate = join(dir, bundleName);\n if (existsSync(candidate)) return candidate;\n }\n }\n return null;\n}\n\n/**\n * CLI names for shell-out-driven terminals. Used with `which <name>` to\n * confirm install on PATH.\n */\nexport const CLI_NAMES: Partial<Record<TerminalChoice, string>> = {\n alacritty: 'alacritty',\n kitty: 'kitty',\n // cmux is CLI-driven, but its CLI lives inside the app bundle (not on a\n // standard PATH dir), so detection uses `resolveCmuxCli` rather than a bare\n // `which cmux`. The entry here is for doctor messaging (e.g. \"resolved cmux\n // → <path>\") and to document cmux as CLI-driven.\n cmux: 'cmux',\n};\n\n/**\n * Canonical absolute directories a cmux CLI symlink/binary may live in, checked\n * by `existsSync` (PATH-independent) so resolution agrees between the dashboard\n * server (full PATH) and the macOS applet (stripped PATH). Exported for tests.\n */\nexport const CMUX_CLI_DIRS: readonly string[] = [\n '/usr/local/bin',\n '/opt/homebrew/bin',\n];\n\n/**\n * Queries LaunchServices for the bundle path of the RUNNING cmux app. Returns\n * the raw `lsappinfo` stdout, or null when the query fails or cmux is not\n * running. Injectable (see `resolveCmuxCli`) so tests never shell out to the\n * host's real `/usr/bin/lsappinfo`.\n */\nexport type CmuxLsappinfoRunner = () => string | null;\n\n/**\n * Default running-app lookup: ask LaunchServices where the running cmux lives.\n * Invokes `/usr/bin/lsappinfo` by ABSOLUTE path so it resolves under the\n * `syntaur://` URL-handler applet's stripped LaunchServices PATH\n * (`/usr/bin:/bin:/usr/sbin:/sbin`), where a bare `lsappinfo` is unresolvable.\n * Returns stdout on a clean exit, else null (non-zero exit / spawn failure).\n */\nconst defaultCmuxLsappinfoRunner: CmuxLsappinfoRunner = () => {\n const result = spawnSync(\n '/usr/bin/lsappinfo',\n ['info', '-only', 'bundlepath', 'com.cmuxterm.app'],\n { encoding: 'utf-8' },\n );\n return result.status === 0 ? result.stdout : null;\n};\n\n/**\n * Absolute path to the cmux CLI (inside the app bundle, a canonical install\n * dir, the running app's bundle, or on PATH), or null when cmux is not\n * installed.\n *\n * cmux is controlled by a first-party CLI that ships *inside* the app bundle at\n * `Contents/Resources/bin/cmux` and is NOT on any standard PATH dir. The macOS\n * `syntaur://` URL-handler applet, which drives the production \"Open in agent\"\n * flow, launches with a stripped LaunchServices PATH\n * (`/usr/bin:/bin:/usr/sbin:/sbin`), where a bare `cmux` is unresolvable. So\n * BOTH detection (this probe) and launch (`buildTerminalInvocation`) must\n * resolve an absolute path; sharing one resolver keeps server-side preflight\n * and the actual applet launch consistent.\n *\n * Resolution order, chosen so resolution is PATH-independent on macOS (where the\n * applet launch is the only stripped-PATH context) — server preflight and the\n * applet then agree by construction:\n * 1. the bundle CLI (`findAppBundle` → `Contents/Resources/bin/cmux`)\n * 2. a canonical install dir (`CMUX_CLI_DIRS`, via `existsSync` — covers a\n * Homebrew/`/usr/local` symlink even under the applet's stripped PATH)\n * 3. the RUNNING app via LaunchServices (`/usr/bin/lsappinfo info -only\n * bundlepath com.cmuxterm.app` → `<bundle>/Contents/Resources/bin/cmux`),\n * darwin-only. This catches a cmux launched from a non-canonical location\n * (e.g. straight off a mounted DMG at `/Volumes/cmux/cmux.app`) that misses\n * every fixed location above. Steps 1–2 keep priority — when a canonical\n * copy exists but a DIFFERENT copy is running, the canonical CLI drives the\n * running app over the shared control socket: fine while versions match\n * (both 0.64.13 today), could skew after an update. That caveat is\n * documented, not guarded at runtime. Not-running + non-canonical\n * legitimately stays unresolved (a cold start from nowhere is unlaunchable)\n * and surfaces as not-installed.\n * 4. `which cmux` — PATH-DEPENDENT, so it is consulted ONLY off macOS. On\n * darwin we deliberately skip it: a cmux reachable only via a non-canonical\n * PATH dir would pass the full-PATH server preflight but be invisible to\n * the stripped-PATH applet, so accepting it would be a false positive.\n * Skipping it makes macOS detection consistent with the applet (real macOS\n * installs are the .app bundle, or now the running-app lookup above). Off\n * macOS there is no stripped-PATH applet, so launch and preflight share the\n * same PATH.\n *\n * `applicationsDirsOverride` / `cliDirsOverride` / `lsappinfoRunnerOverride`\n * REPLACE the defaults so tests stay hermetic from the host's real\n * `/Applications`, `/usr/local/bin`, and `/usr/bin/lsappinfo`.\n */\nexport function resolveCmuxCli(\n applicationsDirsOverride?: string[],\n cliDirsOverride?: string[],\n lsappinfoRunnerOverride?: CmuxLsappinfoRunner,\n): string | null {\n const bundle = findAppBundle('cmux', applicationsDirsOverride);\n if (bundle) {\n const cli = join(bundle, 'Contents/Resources/bin/cmux');\n if (existsSync(cli)) return cli;\n }\n for (const dir of cliDirsOverride ?? CMUX_CLI_DIRS) {\n const cli = join(dir, 'cmux');\n if (existsSync(cli)) return cli;\n }\n if (process.platform === 'darwin') {\n // Running-app fallback: a cmux launched from a non-canonical location is\n // invisible to the fixed checks above. Ask LaunchServices where it lives.\n const runner = lsappinfoRunnerOverride ?? defaultCmuxLsappinfoRunner;\n const stdout = runner();\n if (stdout) {\n // lsappinfo emits e.g. ` \"LSBundlePath\"=\"/Volumes/cmux/cmux.app\"\\n` with\n // leading whitespace and a trailing newline — do NOT anchor the match.\n const match = stdout.match(/\"LSBundlePath\"=\"([^\"]+)\"/);\n if (match) {\n const cli = join(match[1], 'Contents/Resources/bin/cmux');\n if (existsSync(cli)) return cli;\n }\n }\n // darwin deliberately skips `which` (see step 4) — fall through to null.\n return null;\n }\n const which = spawnSync('which', ['cmux'], { encoding: 'utf-8' });\n if (which.status === 0 && which.stdout.trim().length > 0) {\n return which.stdout.trim();\n }\n return null;\n}\n\nexport interface ProbeResult {\n ok: boolean;\n /** Absolute path to the .app bundle or CLI binary, when found. */\n foundPath?: string;\n /** Why the probe returned ok:false. */\n reason?: 'not-installed' | 'no-probe-available';\n}\n\n/**\n * Probe whether a terminal is installed on this machine, using the same\n * primitives as the doctor `terminal.installed` check:\n * - `mdfind` for Apple-Event terminals registered with LaunchServices\n * - `which` for CLI terminals on PATH\n *\n * Returns `{ ok: false, reason: 'no-probe-available' }` when the terminal id\n * has no entry in either map — this should be impossible for known\n * `TerminalChoice` values but lets callers handle a future terminal addition\n * gracefully.\n */\nexport function probeTerminalInstalled(\n terminal: TerminalChoice,\n /**\n * `applicationsDirsOverride` REPLACES (does not extend) the default\n * applications directories for the `.app` fallback. Production never sets it;\n * it exists so tests can point the fallback at a temp dir and stay isolated\n * from the host's real /Applications (merging with the defaults would make a\n * host that actually has the app produce a false positive).\n */\n opts: {\n applicationsDirsOverride?: string[];\n /** REPLACES `CMUX_CLI_DIRS` for the cmux resolver; tests only. */\n cmuxCliDirsOverride?: string[];\n /** REPLACES the default `/usr/bin/lsappinfo` runner; tests only. */\n cmuxLsappinfoRunnerOverride?: CmuxLsappinfoRunner;\n } = {},\n): ProbeResult {\n // cmux is special-cased before the generic bundle-id / CLI-name paths: its\n // control CLI lives inside the app bundle and is not on a standard PATH dir,\n // so neither the `mdfind` bundle-id path (cmux is deliberately absent from\n // APP_BUNDLE_IDS — it would resolve the `.app`, not the CLI) nor a bare\n // `which cmux` is correct. `resolveCmuxCli` finds the bundle CLI (or canonical\n // dir / PATH fallback) and is the same resolver `buildTerminalInvocation`\n // uses, so detection and launch agree under a stripped PATH.\n if (terminal === 'cmux') {\n const cli = resolveCmuxCli(\n opts.applicationsDirsOverride,\n opts.cmuxCliDirsOverride,\n opts.cmuxLsappinfoRunnerOverride,\n );\n return cli\n ? { ok: true, foundPath: cli }\n : { ok: false, reason: 'not-installed' };\n }\n\n const bundleId = APP_BUNDLE_IDS[terminal];\n if (bundleId) {\n const result = spawnSync(\n 'mdfind',\n [`kMDItemCFBundleIdentifier == '${bundleId}'`],\n { encoding: 'utf-8' },\n );\n if (result.status === 0 && result.stdout.trim().length > 0) {\n return { ok: true, foundPath: result.stdout.trim().split('\\n')[0] };\n }\n // `mdfind` yielded no path. This covers BOTH a non-zero exit AND an exit 0\n // with empty stdout (Spotlight not indexing, e.g. background/launchd\n // contexts). Fall back to the standard `.app` locations before declaring\n // the terminal not installed.\n const bundlePath = findAppBundle(terminal, opts.applicationsDirsOverride);\n if (bundlePath) {\n return { ok: true, foundPath: bundlePath };\n }\n return { ok: false, reason: 'not-installed' };\n }\n\n const cliName = CLI_NAMES[terminal];\n if (cliName) {\n const result = spawnSync('which', [cliName], { encoding: 'utf-8' });\n if (result.status === 0 && result.stdout.trim().length > 0) {\n return { ok: true, foundPath: result.stdout.trim() };\n }\n return { ok: false, reason: 'not-installed' };\n }\n\n return { ok: false, reason: 'no-probe-available' };\n}\n","/**\n * Resolve the *real* agent session id for the currently-running process.\n *\n * Session identity is an ambient property of the running process — it must\n * never be looked up from shared mutable state (`.syntaur/context.json`'s\n * `sessionId` scalar), because a co-tenant sharing the same workspace clobbers\n * that scalar and a long-lived session would then read the *wrong* id.\n *\n * `resolveOwnSessionId` returns the first non-empty hit across six layers,\n * ordered by trustworthiness:\n * 1. explicit `--session-id` override (`opts.sessionId`)\n * 2. injected env var: CLAUDE_CODE_SESSION_ID / OPENCODE_SESSION_ID / PI_SESSION_ID\n * 3. agent side channel (Cursor nonce → conversation_id; seam, see Phase E)\n * 4. ancestor-pid → runtime marker (`~/.claude/sessions/<pid>.json`,\n * then `~/.syntaur/runtime/sessions/<pid>.json`), pid-reuse-guarded\n * 5. cwd/mtime transcript scan (last automatic resort; ambiguous under\n * co-tenancy — same caveat as platforms/codex/scripts/resolve-session.sh)\n * 6. legacy hint (`opts.legacyHint`, i.e. the context.json scalar)\n *\n * Callers that must stay *exact* (the Codex/Claude cleanup paths and the\n * `session resolve-id` subcommand) simply omit `opts.legacyHint`, so they never\n * re-introduce the clobbered scalar. Identity-with-fallback callers\n * (`session save`) pass `legacyHint: ctx?.sessionId`.\n *\n * The function is `async` because layer 5 delegates to `cwd-extractor` file I/O;\n * layers 1, 2, and 4 are effectively synchronous.\n *\n * All process/env/fs touch points are injectable via `ResolverDeps` so unit\n * tests can drive every layer deterministically (mirrors the `LivenessDeps`\n * pattern in `src/dashboard/session-liveness.ts`).\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { captureProcessStartedAt } from './process-info.js';\nimport { walkClaudeProjects, walkCodexSessions } from '../usage/cwd-extractor.js';\n\n/** Env vars (in precedence order) that agent runtimes inject with the real id. */\nexport const SESSION_ID_ENV_VARS = [\n 'CLAUDE_CODE_SESSION_ID',\n 'OPENCODE_SESSION_ID',\n 'PI_SESSION_ID',\n] as const;\n\n// Resolved ids become filesystem path segments (sessions/<id>/summary.md) and\n// URL path segments in the cleanup hooks. Now that ids come from widened sources\n// (env vars, ancestor markers, transcript scans), validate them so a malformed\n// value can't traverse out of the sessions dir or inject into a URL. Real agent\n// ids are UUIDs/ULIDs — alphanumerics, hyphens, underscores — so this is strict\n// but never rejects a legitimate id. Invalid candidates are treated as a miss\n// and the resolver falls through to the next layer.\nconst SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;\nexport function isSafeSessionId(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= 256 && SAFE_SESSION_ID.test(value);\n}\n\n/**\n * Shape of a per-process runtime marker file at\n * `<claudeSessionsDir | runtimeSessionsDir>/<pid>.json`. Claude Code writes its\n * native `~/.claude/sessions/<pid>.json` in (a superset of) this shape; the\n * generic `~/.syntaur/runtime/sessions/<pid>.json` is written by a\n * capture-at-birth hook for agents that learn the real id but cannot inject env.\n * Extra fields are tolerated. `sessionId` may be absent on a PENDING marker —\n * written at launch time before the agent has minted its real id (fresh/fork\n * launches); `readRuntimeMarker` rejects those, so pending markers never\n * resolve ids until something backfills them.\n */\nexport interface RuntimeSessionMarker {\n sessionId?: string;\n agent?: string;\n cwd?: string;\n /** `ps -o lstart=`-style start time, used to guard against pid reuse. */\n procStart?: string;\n writtenAt?: number;\n}\n\n/** Injectable dependencies; production callers pass nothing. */\nexport interface ResolverDeps {\n /** Environment to read layer-2 vars from. Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /** Where the ancestor walk starts. Defaults to `process.ppid` (the agent). */\n startPid?: number;\n /** Home directory for the default marker dirs. Defaults to `os.homedir()`. */\n homeDir?: string;\n /** Returns the parent pid of `pid`, or null. Defaults to `ps -o ppid=`. */\n readPpid?: (pid: number) => number | null;\n /** Returns the start-time of `pid`, or null. Defaults to `captureProcessStartedAt`. */\n pidStartedAt?: (pid: number) => string | null;\n /** Returns a file's mtime in ms, or null. Defaults to `statSync(path).mtimeMs`. */\n statMtimeMs?: (path: string) => number | null;\n /** Claude's native marker dir. Defaults to `<home>/.claude/sessions`. */\n claudeSessionsDir?: string;\n /** Generic agent-neutral marker dir. Defaults to `<home>/.syntaur/runtime/sessions`. */\n runtimeSessionsDir?: string;\n /** Max ancestor-chain depth to walk. Defaults to 12. */\n maxDepth?: number;\n}\n\nexport interface ResolveSessionOpts {\n /** Explicit override (layer 1). */\n sessionId?: string;\n /** Working directory for the layer-5 transcript scan. */\n cwd?: string;\n /** Legacy `context.json.sessionId` hint (layer 6). Omit to stay exact-only. */\n legacyHint?: string | null;\n}\n\n/** Parent pid of `pid` via `ps -o ppid=`, or null. Exported for callers that\n * need the hook-equivalent \"shell that owns the agent\" fallback pid. */\nexport function readPpid(pid: number): number | null {\n if (!Number.isFinite(pid) || pid <= 1) return null;\n try {\n const out = execFileSync('ps', ['-o', 'ppid=', '-p', String(pid)], {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n const parent = Number.parseInt(out.trim(), 10);\n return Number.isInteger(parent) && parent > 0 ? parent : null;\n } catch {\n return null;\n }\n}\n\nfunction defaultStatMtimeMs(path: string): number | null {\n try {\n return statSync(path).mtimeMs;\n } catch {\n return null;\n }\n}\n\n/** Read + validate a runtime marker for `pid` under `dir`. Returns null on any miss. */\nexport function readRuntimeMarker(pid: number, dir: string): RuntimeSessionMarker | null {\n if (!Number.isInteger(pid) || pid <= 0) return null;\n const path = join(dir, `${pid}.json`);\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));\n if (\n parsed &&\n typeof parsed === 'object' &&\n typeof (parsed as Record<string, unknown>).sessionId === 'string' &&\n ((parsed as Record<string, unknown>).sessionId as string).length > 0\n ) {\n return parsed as RuntimeSessionMarker;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** Write a generic runtime marker for `pid` (used by tests and capture-at-birth). */\nexport function writeRuntimeMarker(pid: number, marker: RuntimeSessionMarker, dir: string): void {\n const path = join(dir, `${pid}.json`);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, JSON.stringify(marker));\n}\n\n/**\n * Layer 3 seam — agent side channels that key on a per-invocation nonce rather\n * than cwd (so they stay co-tenant-safe). Cursor's nonce→conversation_id\n * handshake plugs in here (Phase E). Returns undefined until implemented.\n */\nasync function resolveSideChannelSessionId(\n _opts: ResolveSessionOpts,\n _deps: ResolverDeps,\n): Promise<string | undefined> {\n return undefined;\n}\n\n/** Layer 4 — walk the ancestor-pid chain, returning the nearest valid marker's id. */\nfunction resolveFromAncestorMarkers(\n startPid: number,\n claudeSessionsDir: string,\n runtimeSessionsDir: string,\n readPpid: (pid: number) => number | null,\n pidStartedAt: (pid: number) => string | null,\n maxDepth: number,\n): string | undefined {\n let pid = startPid;\n for (let depth = 0; depth < maxDepth; depth += 1) {\n if (!Number.isInteger(pid) || pid <= 1) break;\n for (const dir of [claudeSessionsDir, runtimeSessionsDir]) {\n // Read the marker FIRST; only probe `ps` for the pid-reuse guard when a\n // marker actually exists (avoids a `ps` call per level on empty levels).\n const marker = readRuntimeMarker(pid, dir);\n if (!marker) continue;\n if (marker.procStart) {\n // Fail CLOSED: a recorded procStart must be PROVEN to still match. If we\n // can't read the live start time, we can't prove the pid wasn't recycled\n // (a stale marker for a reused pid), so skip rather than trust it.\n const actual = pidStartedAt(pid);\n if (!actual || actual !== marker.procStart) continue;\n }\n if (isSafeSessionId(marker.sessionId)) return marker.sessionId;\n }\n const parent = readPpid(pid);\n if (parent === null) break;\n pid = parent;\n }\n return undefined;\n}\n\n/** Layer 5 — scan transcripts for `cwd`, pick the most-recently-written. */\nasync function resolveFromCwdScan(\n cwd: string,\n statMtimeMs: (path: string) => number | null,\n): Promise<string | undefined> {\n const candidates: Array<{ sessionId: string; mtime: number }> = [];\n for await (const meta of walkClaudeProjects()) {\n if (meta.cwd === cwd && isSafeSessionId(meta.sessionId)) {\n candidates.push({ sessionId: meta.sessionId, mtime: statMtimeMs(meta.path) ?? 0 });\n }\n }\n for await (const meta of walkCodexSessions()) {\n if (meta.cwd === cwd && isSafeSessionId(meta.sessionId)) {\n candidates.push({ sessionId: meta.sessionId, mtime: statMtimeMs(meta.path) ?? 0 });\n }\n }\n if (candidates.length === 0) return undefined;\n // Deterministic: newest mtime wins; ties broken by sessionId descending.\n candidates.sort((a, b) => b.mtime - a.mtime || (a.sessionId < b.sessionId ? 1 : a.sessionId > b.sessionId ? -1 : 0));\n return candidates[0].sessionId;\n}\n\nexport async function resolveOwnSessionId(\n opts: ResolveSessionOpts = {},\n deps: ResolverDeps = {},\n): Promise<string | undefined> {\n // Layer 1 — explicit override.\n if (isSafeSessionId(opts.sessionId)) return opts.sessionId;\n\n // Layer 2 — injected env var (clobber-proof, per-process).\n const env = deps.env ?? process.env;\n for (const key of SESSION_ID_ENV_VARS) {\n const value = env[key];\n if (isSafeSessionId(value)) return value;\n }\n\n // Layer 3 — agent side channel (seam).\n const sideChannel = await resolveSideChannelSessionId(opts, deps);\n if (sideChannel) return sideChannel;\n\n // Layer 4 — ancestor-pid runtime marker.\n const home = deps.homeDir ?? homedir();\n const claudeSessionsDir = deps.claudeSessionsDir ?? join(home, '.claude', 'sessions');\n const runtimeSessionsDir = deps.runtimeSessionsDir ?? join(home, '.syntaur', 'runtime', 'sessions');\n const startPid = deps.startPid ?? process.ppid;\n const fromMarker = resolveFromAncestorMarkers(\n startPid,\n claudeSessionsDir,\n runtimeSessionsDir,\n deps.readPpid ?? readPpid,\n deps.pidStartedAt ?? captureProcessStartedAt,\n deps.maxDepth ?? 12,\n );\n if (fromMarker) return fromMarker;\n\n // Layer 5 — cwd/mtime transcript scan (last automatic resort).\n if (opts.cwd) {\n const fromScan = await resolveFromCwdScan(opts.cwd, deps.statMtimeMs ?? defaultStatMtimeMs);\n if (fromScan) return fromScan;\n }\n\n // Layer 6 — legacy context.json hint (only when the caller opts in).\n if (isSafeSessionId(opts.legacyHint)) return opts.legacyHint;\n\n return undefined;\n}\n","import { execFileSync } from 'node:child_process';\n\n/**\n * Capture the start-time of a process via `ps -o lstart=`. Used as the\n * recycling-defense baseline for session liveness detection: the server later\n * compares this stored start-time to the current `ps -o lstart=` output, so a\n * recycled PID with the same number but different start-time correctly\n * reports as not-live.\n *\n * Returns null when `ps` fails (process already gone, or `ps` not on PATH).\n * Null is the expected sentinel for \"no recycling baseline available\" — the\n * liveness check trusts `kill -0` alone in that case (small false-positive\n * risk on PID reuse, acceptable).\n */\nexport function captureProcessStartedAt(pid: number): string | null {\n if (!Number.isFinite(pid) || pid <= 0) return null;\n try {\n const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n const trimmed = out.trim();\n return trimmed === '' ? null : trimmed;\n } catch {\n return null;\n }\n}\n","/**\n * Session metadata extractor.\n *\n * Reads Claude Code, Codex, and Pi JSONL session files to produce\n * `(sessionId, cwd, startTs, endTs)` tuples for use in the attribution\n * join. Mutates nothing; touches no DB.\n *\n * - Claude Code: `~/.claude/projects/<cwd-slug>/<session-id>.jsonl`. The\n * directory name is treated as opaque (slug decoding is unsafe because\n * legitimate directory names can contain `-`). `cwd` is read from inside\n * the transcript via the existing `derivePathFromTranscript` utility.\n * `sessionId` is the basename without `.jsonl`.\n *\n * - Codex: `<sessions-root>/YYYY/MM/DD/rollout-*.jsonl` (or flat at the\n * sessions-root for older Codex versions). Line 1 is a `session_meta`\n * envelope with `{type, timestamp, payload:{id, cwd, ...}}` — `timestamp`\n * is at the TOP LEVEL (verified against\n * `src/__tests__/codex-resolve-session.test.ts:30-34`), NOT inside\n * `payload`. Sessions root resolves via:\n * CODEX_SESSIONS_DIR\n * ?? path.join(CODEX_HOME, 'sessions')\n * ?? ~/.codex/sessions\n *\n * - Pi: `<sessions-root>/<encoded-cwd>/<ts>_<uuid>.jsonl`. Line 1 is a\n * session-start envelope `{type, version, id, timestamp, cwd}`. The\n * sessionId is the UUID suffix of the filename (after the last `_`,\n * `.jsonl` stripped). Sessions root resolves via:\n * PI_AGENT_DIR (treated as Pi home) → <PI_AGENT_DIR>/sessions\n * ?? ~/.pi/agent/sessions\n */\n\nimport { open, readdir, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { expandHome } from '../utils/paths.js';\nimport { derivePathFromTranscript } from '../utils/transcript.js';\n\nconst SCAN_LINE_CAP = 50;\nconst TAIL_READ_BYTES = 8 * 1024;\nconst TAIL_READ_BYTES_MAX = 64 * 1024;\n\nexport interface ClaudeSessionMeta {\n tool: 'claude';\n sessionId: string;\n cwd: string;\n startTs: string | null;\n endTs: string | null;\n /** Absolute path to the transcript file (for mtime-based ordering). */\n path: string;\n}\n\nexport interface CodexSessionMeta {\n tool: 'codex';\n sessionId: string;\n cwd: string;\n startTs: string;\n endTs: string;\n /** Absolute path to the rollout file (for mtime-based ordering). */\n path: string;\n}\n\nexport interface PiSessionMeta {\n tool: 'pi';\n sessionId: string;\n cwd: string;\n startTs: string | null;\n endTs: string | null;\n /** Absolute path to the transcript file (for mtime-based ordering). */\n path: string;\n}\n\nexport type SessionMeta = ClaudeSessionMeta | CodexSessionMeta | PiSessionMeta;\n\n// --- Claude Code ----------------------------------------------------------\n\n/**\n * Extract session metadata from a Claude Code transcript file. Returns\n * `null` when the file is unreadable, has no `cwd`, or fails to parse.\n */\nexport async function extractClaudeSessionMeta(\n jsonlPath: string,\n): Promise<ClaudeSessionMeta | null> {\n const cwd = await derivePathFromTranscript(jsonlPath);\n if (!cwd) return null;\n\n const basename = jsonlPath.split('/').pop() ?? '';\n const sessionId = basename.replace(/\\.jsonl$/, '');\n if (!sessionId) return null;\n\n const startTs = await readFirstTimestamp(jsonlPath);\n const endTs = await readLastTimestamp(jsonlPath);\n\n return {\n tool: 'claude',\n sessionId,\n cwd,\n startTs,\n endTs,\n path: jsonlPath,\n };\n}\n\n// --- Codex ----------------------------------------------------------------\n\n/**\n * Extract session metadata from a Codex rollout file. Returns `null` if line\n * 1 isn't a valid `session_meta` envelope.\n */\nexport async function extractCodexSessionMeta(\n jsonlPath: string,\n): Promise<CodexSessionMeta | null> {\n let handle;\n try {\n handle = await open(jsonlPath, 'r');\n } catch {\n return null;\n }\n try {\n const stream = handle.createReadStream({ encoding: 'utf-8' });\n let buffer = '';\n let firstLine: string | null = null;\n for await (const chunk of stream) {\n buffer += chunk;\n const nl = buffer.indexOf('\\n');\n if (nl !== -1) {\n firstLine = buffer.slice(0, nl);\n stream.destroy();\n break;\n }\n }\n if (!firstLine && buffer.length > 0) firstLine = buffer;\n if (!firstLine) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(firstLine);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object') return null;\n\n const obj = parsed as Record<string, unknown>;\n if (obj.type !== 'session_meta') return null;\n\n const timestamp = typeof obj.timestamp === 'string' ? obj.timestamp : null;\n const payload = obj.payload as Record<string, unknown> | undefined;\n const id = payload && typeof payload.id === 'string' ? payload.id : null;\n const cwd = payload && typeof payload.cwd === 'string' ? payload.cwd : null;\n\n if (!timestamp || !id || !cwd) return null;\n\n const endTs = (await readLastTimestamp(jsonlPath)) ?? timestamp;\n\n return {\n tool: 'codex',\n sessionId: id,\n cwd,\n startTs: timestamp,\n endTs,\n path: jsonlPath,\n };\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\n// --- Walkers --------------------------------------------------------------\n\n/**\n * Yield session metadata for every Claude Code transcript under `root`\n * (default `~/.claude/projects`). One `cwd` is cached per directory after\n * the first session in it produces a hit — every Claude session under a\n * `<cwd-slug>` directory launched from the same cwd.\n *\n * Optional `sinceMtimeMs` bounds the walk to files modified at or after\n * the given epoch ms (matching the CLI's first-run 30-day window).\n */\nexport async function* walkClaudeProjects(opts: {\n root?: string;\n sinceMtimeMs?: number;\n} = {}): AsyncGenerator<ClaudeSessionMeta> {\n const root = expandHome(opts.root ?? '~/.claude/projects');\n const dirs = await listDirSafe(root);\n for (const dirent of dirs) {\n if (!dirent.isDirectory) continue;\n const dirPath = join(root, dirent.name);\n const files = await listDirSafe(dirPath);\n let cachedCwd: string | null = null;\n for (const f of files) {\n if (!f.isFile || !f.name.endsWith('.jsonl')) continue;\n const filePath = join(dirPath, f.name);\n if (opts.sinceMtimeMs !== undefined) {\n const mtime = await mtimeMs(filePath);\n if (mtime !== null && mtime < opts.sinceMtimeMs) continue;\n }\n let meta: ClaudeSessionMeta | null;\n if (cachedCwd) {\n // Still need timestamps + sessionId from this file.\n const sessionId = f.name.replace(/\\.jsonl$/, '');\n const startTs = await readFirstTimestamp(filePath);\n const endTs = await readLastTimestamp(filePath);\n meta = { tool: 'claude', sessionId, cwd: cachedCwd, startTs, endTs, path: filePath };\n } else {\n meta = await extractClaudeSessionMeta(filePath);\n if (meta) cachedCwd = meta.cwd;\n }\n if (meta) yield meta;\n }\n }\n}\n\n/**\n * Yield session metadata for every Codex rollout file under the resolved\n * sessions root.\n */\nexport async function* walkCodexSessions(opts: {\n root?: string;\n sinceMtimeMs?: number;\n} = {}): AsyncGenerator<CodexSessionMeta> {\n const root = resolveCodexSessionsRoot(opts.root);\n for await (const filePath of walkJsonlRecursive(root)) {\n const basename = filePath.split('/').pop() ?? '';\n // Codex names files like `rollout-*.jsonl`; tolerate but prefer that prefix.\n if (!basename.endsWith('.jsonl')) continue;\n if (opts.sinceMtimeMs !== undefined) {\n const mtime = await mtimeMs(filePath);\n if (mtime !== null && mtime < opts.sinceMtimeMs) continue;\n }\n const meta = await extractCodexSessionMeta(filePath);\n if (meta) yield meta;\n }\n}\n\nexport function resolveCodexSessionsRoot(override?: string): string {\n if (override) return expandHome(override);\n const fromSessionsEnv = process.env.CODEX_SESSIONS_DIR;\n if (fromSessionsEnv && fromSessionsEnv.length > 0) return expandHome(fromSessionsEnv);\n const fromHomeEnv = process.env.CODEX_HOME;\n if (fromHomeEnv && fromHomeEnv.length > 0) return join(expandHome(fromHomeEnv), 'sessions');\n return join(homedir(), '.codex', 'sessions');\n}\n\n// --- Pi -------------------------------------------------------------------\n\n/**\n * Extract session metadata from a Pi agent transcript file. Returns `null`\n * when the file is unreadable, has no `cwd` on line 1, or fails to parse.\n *\n * Filename format: `<ts>_<uuid>.jsonl` — `sessionId` is the UUID suffix\n * (after the last `_`, with `.jsonl` stripped).\n * Line 1 format: `{type, version, id, timestamp, cwd}`.\n */\nexport async function extractPiSessionMeta(\n jsonlPath: string,\n): Promise<PiSessionMeta | null> {\n // Derive sessionId from filename: substring after last '_', strip '.jsonl'.\n const basename = jsonlPath.split('/').pop() ?? '';\n const underscoreIdx = basename.lastIndexOf('_');\n if (underscoreIdx === -1) return null;\n const sessionId = basename.slice(underscoreIdx + 1).replace(/\\.jsonl$/, '');\n if (!sessionId) return null;\n\n // Read cwd from first line.\n let handle;\n try {\n handle = await open(jsonlPath, 'r');\n } catch {\n return null;\n }\n try {\n const stream = handle.createReadStream({ encoding: 'utf-8' });\n let buffer = '';\n let firstLine: string | null = null;\n for await (const chunk of stream) {\n buffer += chunk;\n const nl = buffer.indexOf('\\n');\n if (nl !== -1) {\n firstLine = buffer.slice(0, nl);\n stream.destroy();\n break;\n }\n }\n if (!firstLine && buffer.length > 0) firstLine = buffer;\n if (!firstLine) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(firstLine);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object') return null;\n const obj = parsed as Record<string, unknown>;\n if (typeof obj.cwd !== 'string' || obj.cwd.length === 0) return null;\n const cwd = obj.cwd;\n\n const startTs = await readFirstTimestamp(jsonlPath);\n const endTs = await readLastTimestamp(jsonlPath);\n\n return {\n tool: 'pi',\n sessionId,\n cwd,\n startTs,\n endTs,\n path: jsonlPath,\n };\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\nexport function resolvePiSessionsRoot(override?: string): string {\n if (override) return expandHome(override);\n const fromHomeEnv = process.env.PI_AGENT_DIR;\n if (fromHomeEnv && fromHomeEnv.length > 0) return join(expandHome(fromHomeEnv), 'sessions');\n return join(homedir(), '.pi', 'agent', 'sessions');\n}\n\n/**\n * Yield session metadata for every Pi agent transcript under `root`\n * (default `~/.pi/agent/sessions`). Pi organises files as\n * `<root>/<encoded-cwd>/<ts>_<uuid>.jsonl`.\n *\n * One `cwd` is cached per directory after the first file in it is parsed\n * (every file under a given dir shares the same cwd).\n *\n * Optional `sinceMtimeMs` bounds the walk to files modified at or after\n * the given epoch ms.\n */\nexport async function* walkPiSessions(opts: {\n root?: string;\n sinceMtimeMs?: number;\n} = {}): AsyncGenerator<PiSessionMeta> {\n const root = resolvePiSessionsRoot(opts.root);\n const dirs = await listDirSafe(root);\n for (const dirent of dirs) {\n if (!dirent.isDirectory) continue;\n const dirPath = join(root, dirent.name);\n const files = await listDirSafe(dirPath);\n let cachedCwd: string | null = null;\n for (const f of files) {\n if (!f.isFile || !f.name.endsWith('.jsonl')) continue;\n const filePath = join(dirPath, f.name);\n if (opts.sinceMtimeMs !== undefined) {\n const mtime = await mtimeMs(filePath);\n if (mtime !== null && mtime < opts.sinceMtimeMs) continue;\n }\n let meta: PiSessionMeta | null;\n if (cachedCwd) {\n // Derive sessionId from filename; reuse cached cwd.\n const underscoreIdx = f.name.lastIndexOf('_');\n if (underscoreIdx === -1) continue;\n const sessionId = f.name.slice(underscoreIdx + 1).replace(/\\.jsonl$/, '');\n if (!sessionId) continue;\n const startTs = await readFirstTimestamp(filePath);\n const endTs = await readLastTimestamp(filePath);\n meta = { tool: 'pi', sessionId, cwd: cachedCwd, startTs, endTs, path: filePath };\n } else {\n meta = await extractPiSessionMeta(filePath);\n if (meta) cachedCwd = meta.cwd;\n }\n if (meta) yield meta;\n }\n }\n}\n\n// --- Internals ------------------------------------------------------------\n\nasync function listDirSafe(\n path: string,\n): Promise<Array<{ name: string; isFile: boolean; isDirectory: boolean }>> {\n try {\n const entries = await readdir(path, { withFileTypes: true });\n return entries.map((e) => ({\n name: e.name,\n isFile: e.isFile(),\n isDirectory: e.isDirectory(),\n }));\n } catch {\n return [];\n }\n}\n\nasync function* walkJsonlRecursive(root: string): AsyncGenerator<string> {\n const stack: string[] = [root];\n while (stack.length > 0) {\n const current = stack.pop()!;\n const entries = await listDirSafe(current);\n for (const e of entries) {\n const full = join(current, e.name);\n if (e.isDirectory) {\n stack.push(full);\n } else if (e.isFile && e.name.endsWith('.jsonl')) {\n yield full;\n }\n }\n }\n}\n\nasync function mtimeMs(path: string): Promise<number | null> {\n try {\n const s = await stat(path);\n return s.mtimeMs;\n } catch {\n return null;\n }\n}\n\n/**\n * Bounded forward scan for the first JSON line carrying a `timestamp` field.\n * Returns `null` if none found within `SCAN_LINE_CAP` lines.\n */\nasync function readFirstTimestamp(path: string): Promise<string | null> {\n let handle;\n try {\n handle = await open(path, 'r');\n } catch {\n return null;\n }\n try {\n const stream = handle.createReadStream({ encoding: 'utf-8' });\n let buffer = '';\n let scanned = 0;\n for await (const chunk of stream) {\n buffer += chunk;\n let nl = buffer.indexOf('\\n');\n while (nl !== -1) {\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n const ts = extractTimestamp(line);\n if (ts) {\n stream.destroy();\n return ts;\n }\n scanned++;\n if (scanned >= SCAN_LINE_CAP) {\n stream.destroy();\n return null;\n }\n nl = buffer.indexOf('\\n');\n }\n }\n if (buffer.length > 0) return extractTimestamp(buffer);\n return null;\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\n/**\n * Bounded reverse scan: read the last `TAIL_READ_BYTES` of the file, walk\n * lines from end to start, return the first parsed `timestamp` found. Falls\n * back to expanding the window once to `TAIL_READ_BYTES_MAX`.\n */\nasync function readLastTimestamp(path: string): Promise<string | null> {\n let handle;\n try {\n handle = await open(path, 'r');\n } catch {\n return null;\n }\n try {\n const stats = await handle.stat();\n const size = stats.size;\n if (size === 0) return null;\n\n for (const windowBytes of [TAIL_READ_BYTES, TAIL_READ_BYTES_MAX]) {\n const start = Math.max(0, size - windowBytes);\n const length = size - start;\n const buf = Buffer.alloc(length);\n await handle.read(buf, 0, length, start);\n const text = buf.toString('utf-8');\n const lines = text.split('\\n');\n // If we didn't read from byte 0, the first line may be partial — drop it.\n if (start > 0) lines.shift();\n for (let i = lines.length - 1; i >= 0; i--) {\n const ts = extractTimestamp(lines[i]);\n if (ts) return ts;\n }\n if (start === 0) break; // already read the whole file\n }\n return null;\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\nfunction extractTimestamp(line: string): string | null {\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed[0] !== '{') return null;\n try {\n const parsed = JSON.parse(trimmed) as { timestamp?: unknown };\n if (typeof parsed.timestamp === 'string' && parsed.timestamp.length > 0) {\n return parsed.timestamp;\n }\n } catch {\n // Truncated or non-JSON; ignore.\n }\n return null;\n}\n","import { open } from 'node:fs/promises';\n\n// Cap on lines we'll scan looking for `cwd`. The launch cwd is recorded in the\n// first few entries of every Claude Code transcript; 50 lines is generous\n// enough to absorb leading non-JSON noise (blank lines, permission-mode rows\n// without a cwd) without slurping multi-MB transcripts into memory.\nconst MAX_LINES_SCANNED = 50;\n\n/**\n * Read the first `cwd` field from a Claude Code transcript JSONL file.\n *\n * Claude Code derives `~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`\n * from the *launch* cwd of the session, and `claude --resume <id>` only finds\n * the transcript when invoked from a matching cwd. The transcript itself is\n * therefore the authoritative source of truth for \"which directory does this\n * session belong to\" — read it once, prefer it over whatever a registering\n * caller might have happened to be sitting in.\n *\n * Returns `null` when the path is empty, the file doesn't exist or can't be\n * read, no JSON line within the scan window contains a `cwd` field, or the\n * value is not a non-empty string. Never throws.\n */\nexport async function derivePathFromTranscript(\n transcriptPath: string | null | undefined,\n): Promise<string | null> {\n if (!transcriptPath) return null;\n\n let handle;\n try {\n handle = await open(transcriptPath, 'r');\n } catch {\n return null;\n }\n\n try {\n const stream = handle.createReadStream({ encoding: 'utf-8' });\n let buffer = '';\n let scanned = 0;\n\n for await (const chunk of stream) {\n buffer += chunk;\n let nl = buffer.indexOf('\\n');\n while (nl !== -1) {\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n\n const cwd = extractCwd(line);\n if (cwd) {\n stream.destroy();\n return cwd;\n }\n\n scanned++;\n if (scanned >= MAX_LINES_SCANNED) {\n stream.destroy();\n return null;\n }\n nl = buffer.indexOf('\\n');\n }\n }\n\n // Trailing line without a newline (rare, but handle it).\n if (buffer.length > 0) {\n const cwd = extractCwd(buffer);\n if (cwd) return cwd;\n }\n return null;\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\nfunction extractCwd(line: string): string | null {\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed[0] !== '{') return null;\n try {\n const parsed = JSON.parse(trimmed) as { cwd?: unknown };\n if (typeof parsed.cwd === 'string' && parsed.cwd.length > 0) {\n return parsed.cwd;\n }\n } catch {\n // Non-JSON or truncated line — keep scanning.\n }\n return null;\n}\n","import { fileURLToPath } from 'node:url';\nimport { dirname, resolve, join } from 'node:path';\nimport { realpathSync, readFileSync, mkdirSync } from 'node:fs';\nimport { syntaurRoot } from '../utils/paths.js';\nimport { fileExists, writeFileForce } from '../utils/fs.js';\n\nexport type InstallKind = 'npx' | 'global' | 'local' | 'unknown';\n\ninterface DetectOptions {\n realpath?: (p: string) => string;\n readFile?: (p: string) => string;\n envUserAgent?: string;\n}\n\n/**\n * Anchored cache-layout regexes. All require a `/node_modules/` suffix after\n * the cache hash segment to avoid false positives on user dirs that happen\n * to contain `_npx` / `dlx` / `bunx-` literals. Order matches the legacy\n * `isRunningViaNpx` in `src/utils/npx-prompt.ts` so behavior is consistent.\n */\nconst NPX_PATTERNS: { kind: 'npm' | 'pnpm' | 'bun'; re: RegExp }[] = [\n { kind: 'npm', re: /\\/_npx\\/([^/]+)\\/node_modules(?:\\/|$)/ },\n { kind: 'pnpm', re: /\\/pnpm\\/dlx\\/([^/]+)\\/node_modules(?:\\/|$)/ },\n { kind: 'bun', re: /\\/bunx-([^/]+)\\/node_modules(?:\\/|$)/ },\n];\n\n/**\n * Canonical npm global layout: `<prefix>/lib/node_modules/syntaur/...`\n * Matches /usr/local, nvm's `<v>/lib/node_modules/syntaur/`, Homebrew, etc.\n */\nconst GLOBAL_PATTERN = /\\/lib\\/node_modules\\/syntaur(?:\\/|$)/;\n\n/**\n * Resolve a file:// URL to an absolute filesystem path, applying realpath\n * so symlinks (e.g. an npm `bin/` symlink pointing into a cached node_modules)\n * resolve to the actual install location. Returns null on parse errors.\n */\nfunction resolveScriptPath(\n scriptUrl: string,\n realpath: (p: string) => string,\n): string | null {\n let p: string;\n try {\n p = fileURLToPath(scriptUrl);\n } catch {\n return null;\n }\n try {\n return realpath(p);\n } catch {\n // Path doesn't exist (test fixtures, deleted file). Fall back to the\n // unresolved path so classifier can still match by pattern.\n return p;\n }\n}\n\nfunction normalizeSlashes(p: string): string {\n return p.replace(/\\\\/g, '/');\n}\n\n/**\n * Classify the install origin of the running CLI.\n *\n * Decision order:\n * 1. npx-style cache patterns (anchored to `/node_modules/` suffix).\n * 2. `npm_config_user_agent` containing `npx/` (some pnpm-shim invocations\n * don't put dlx in the path).\n * 3. Canonical npm global layout `/lib/node_modules/syntaur/`.\n * 4. Local checkout (walks up to find a `package.json` named `syntaur`\n * whose dir is not under any `node_modules/`).\n * 5. `unknown` — the subcommand refuses these alongside `npx` to avoid\n * registering a bundle path that may not survive.\n */\nexport function detectInstallKind(\n scriptUrl: string,\n opts: DetectOptions = {},\n): InstallKind {\n const realpath = opts.realpath ?? realpathSync.native;\n const readFile = opts.readFile ?? ((p) => readFileSync(p, 'utf-8'));\n const ua =\n opts.envUserAgent !== undefined\n ? opts.envUserAgent\n : (process.env.npm_config_user_agent ?? '');\n\n const resolved = resolveScriptPath(scriptUrl, realpath);\n if (resolved === null) {\n return 'unknown';\n }\n const norm = normalizeSlashes(resolved);\n\n for (const pat of NPX_PATTERNS) {\n if (pat.re.test(norm)) return 'npx';\n }\n if (ua.includes('npx/')) {\n return 'npx';\n }\n\n if (GLOBAL_PATTERN.test(norm)) {\n return 'global';\n }\n\n // Walk up looking for a syntaur package.json that is NOT inside a\n // node_modules/ — that pattern indicates a local source checkout.\n let dir = dirname(resolved);\n for (let depth = 0; depth < 8; depth++) {\n const pkgJsonPath = join(dir, 'package.json');\n let raw: string;\n try {\n raw = readFile(pkgJsonPath);\n } catch {\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n continue;\n }\n try {\n const pkg = JSON.parse(raw) as { name?: unknown };\n if (\n typeof pkg.name === 'string' &&\n pkg.name === 'syntaur' &&\n !normalizeSlashes(dir).includes('/node_modules/')\n ) {\n return 'local';\n }\n } catch {\n // Malformed package.json on the way up — ignore and keep walking.\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n return 'unknown';\n}\n\n/**\n * Extract the cache-hash segment from an npx-style script URL.\n * Returns null for global/local/unknown installs.\n */\nexport function extractNpxHash(\n scriptUrl: string,\n opts: DetectOptions = {},\n): string | null {\n const realpath = opts.realpath ?? realpathSync.native;\n const resolved = resolveScriptPath(scriptUrl, realpath);\n if (resolved === null) return null;\n const norm = normalizeSlashes(resolved);\n for (const pat of NPX_PATTERNS) {\n const m = norm.match(pat.re);\n if (m) return m[1] ?? null;\n }\n return null;\n}\n\nexport function nudgeStampDir(): string {\n return resolve(syntaurRoot(), 'npx-handler-nudge');\n}\n\n/**\n * Sanitize the hash to a safe filename: anything outside [A-Za-z0-9_-] is\n * replaced with `_`. The npx-cache regex captures already exclude `/`, but\n * this is defense-in-depth against future upstream cache layouts.\n */\nfunction sanitizeHash(hash: string): string {\n return hash.replace(/[^A-Za-z0-9_-]/g, '_') || '_';\n}\n\nexport function nudgeStampPath(hash: string): string {\n return join(nudgeStampDir(), sanitizeHash(hash));\n}\n\nexport async function hasNudgedHash(hash: string): Promise<boolean> {\n return fileExists(nudgeStampPath(hash));\n}\n\nexport async function recordNudge(hash: string): Promise<void> {\n try {\n mkdirSync(nudgeStampDir(), { recursive: true });\n } catch {\n // Best-effort; if mkdir fails (e.g. a regular file at the path), the\n // writeFileForce below will surface its own error which we also swallow.\n }\n try {\n await writeFileForce(nudgeStampPath(hash), '');\n } catch {\n // Best-effort. Worst case the nudge fires again on next invocation,\n // which is annoying but not destructive.\n }\n}\n\n/**\n * Truthiness rules for `SYNTAUR_SKIP_HANDLER_NUDGE`:\n * - `'1'`, `'true'`, `'yes'` (case-insensitive, trimmed) → disabled\n * - empty, unset, `'0'`, `'false'`, whitespace → enabled\n *\n * Deliberately narrow so users who set the var to `'0'` to mean \"off the\n * skip\" don't accidentally disable the nudge.\n */\nexport function isHandlerNudgeDisabled(): boolean {\n const raw = process.env.SYNTAUR_SKIP_HANDLER_NUDGE;\n if (raw === undefined) return false;\n const trimmed = raw.trim();\n return /^(1|true|yes)$/i.test(trimmed);\n}\n\nexport function nudgeMessage(): string {\n return 'syntaur: running from npx — the syntaur:// deep-link handler is not registered. Install durably with `npm i -g syntaur` to enable \"Open in agent\" buttons.';\n}\n\nexport async function shouldNudgeForNpx(hash: string | null): Promise<boolean> {\n if (isHandlerNudgeDisabled()) return false;\n if (hash === null) return false;\n if (await hasNudgedHash(hash)) return false;\n return true;\n}\n\n/**\n * Args that short-circuit the nudge: when the user invoked `--help` or\n * `--version`, they're not running the CLI for real, so don't bother them\n * with the install-durably banner. Mirrors `META_ARGS` in\n * `src/utils/npx-prompt.ts`.\n */\nconst META_ARGS = new Set(['-h', '--help', '-V', '--version', 'help']);\n\n/**\n * Pre-Commander startup hook. Mirrors `maybePromptInstall` from\n * `src/utils/npx-prompt.ts` in shape so `src/index.ts` can call them\n * back-to-back.\n */\nexport async function maybeNudgeForNpxInstall(scriptUrl: string): Promise<void> {\n if (detectInstallKind(scriptUrl) !== 'npx') return;\n const args = process.argv.slice(2);\n if (args.some((a) => META_ARGS.has(a))) return;\n const hash = extractNpxHash(scriptUrl);\n if (!(await shouldNudgeForNpx(hash))) return;\n // hash is non-null here — shouldNudgeForNpx returned false for null above.\n console.error(nudgeMessage());\n if (hash !== null) {\n await recordNudge(hash);\n }\n}\n"],"mappings":"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","names":["readFile","rename","writeFile","resolve","resolve","readFile","resolve","init_parser","readdir","resolve","init_parser","resolve","readFile","init_parser","init_parser","readFile","resolve","init_config","resolve","readdir","readFile","resolve","readdir","readFile","playbooksDir","init_config","isAbsolute","readFile","readdir","readFile","resolve","init_types","resolve","readdir","readdir","readFile","resolve","join","init_types","Database","resolve","readdir","readFile","readFile","resolve","db","rows","readdir","readFile","writeFile","stat","resolve","dirname","openQuestions","computeFactsDetailed","deriveDimensions","DEFAULT_DERIVE_CONFIG","init_config","init_config","isAbsolute","spawn","mkdir","writeFile","isAbsolute","resolve","isAbsolute","isAbsolute","spawn","homedir","basename","join","resolve","spawnSync","existsSync","homedir","join","join","homedir","existsSync","spawnSync","join","existsSync","init_config","execFileSync","statSync","homedir","dirname","join","open","readdir","stat","join","homedir","join","dirname","spawn","basename","resolve","join","homedir","initSessionDb","appendSession","dirname","resolve","join","readFileSync","mkdirSync","readFile","parent"]}
1
+ {"version":3,"sources":["../../src/utils/terminal-schema.ts","../../src/utils/paths.ts","../../src/utils/fs.ts","../../src/templates/config.ts","../../src/utils/timestamp.ts","../../src/utils/fs-migration.ts","../../src/lifecycle/types.ts","../../src/lifecycle/state-machine.ts","../../src/lifecycle/frontmatter.ts","../../src/utils/uuid.ts","../../src/db/events-db.ts","../../src/lifecycle/event-emit.ts","../../src/dashboard/parser.ts","../../src/todos/parser.ts","../../src/lifecycle/linked-todos.ts","../../src/lifecycle/transitions.ts","../../src/lifecycle/index.ts","../../src/utils/hotkeysCatalog.ts","../../src/utils/agents-schema.ts","../../src/utils/slug.ts","../../src/utils/derive-config.ts","../../src/utils/query/fields.ts","../../src/utils/query/evaluate.ts","../../src/utils/query/lexer.ts","../../src/utils/query/parser.ts","../../src/utils/query/index.ts","../../src/utils/fact-registry.ts","../../src/utils/search-schema.ts","../../src/utils/workspace-visibility-schema.ts","../../src/utils/config.ts","../../src/utils/assignment-resolver.ts","../../src/lifecycle/derive.ts","../../src/utils/playbooks.ts","../../src/launch/cwd.ts","../../src/utils/git-worktree.ts","../../src/lifecycle/facts.ts","../../src/search/types.ts","../../src/search/route.ts","../../src/utils/assignment-walk.ts","../../src/search/indexer.ts","../../src/search/fuse-provider.ts","../../src/search/semantic-provider.ts","../../src/search/index.ts","../../src/dashboard/help.ts","../../src/dashboard/session-db.ts","../../src/dashboard/agent-sessions.ts","../../src/dashboard/overviewCopy.ts","../../src/staleness/classify.ts","../../src/dashboard/api.ts","../../src/launch/url.ts","../../src/launch/plan.ts","../../src/launch/launch-prompt.ts","../../src/launch/argv.ts","../../src/tui/launch.ts","../../src/launch/execute.ts","../../src/utils/terminal-probe.ts","../../src/utils/session-id.ts","../../src/utils/process-info.ts","../../src/usage/cwd-extractor.ts","../../src/utils/transcript.ts","../../src/launch/install-detection.ts"],"sourcesContent":["export type TerminalChoice =\n | 'terminal-app'\n | 'iterm'\n | 'ghostty'\n | 'alacritty'\n | 'warp'\n | 'kitty'\n | 'cmux';\n\nexport const TERMINAL_CHOICES: readonly TerminalChoice[] = [\n 'terminal-app',\n 'iterm',\n 'ghostty',\n 'alacritty',\n 'warp',\n 'kitty',\n 'cmux',\n];\n","import { homedir } from 'node:os';\nimport { resolve } from 'node:path';\n\nexport function expandHome(p: string): string {\n if (p.startsWith('~/') || p === '~') {\n return resolve(homedir(), p.slice(2));\n }\n return p;\n}\n\nexport function syntaurRoot(): string {\n const override = process.env.SYNTAUR_HOME;\n if (override && override.length > 0) {\n return resolve(expandHome(override));\n }\n return resolve(homedir(), '.syntaur');\n}\n\nexport function defaultProjectDir(): string {\n return resolve(syntaurRoot(), 'projects');\n}\n\nexport function assignmentsDir(): string {\n return resolve(syntaurRoot(), 'assignments');\n}\n\nexport function serversDir(): string {\n return resolve(syntaurRoot(), 'servers');\n}\n\nexport function playbooksDir(): string {\n return resolve(syntaurRoot(), 'playbooks');\n}\n\nexport function todosDir(): string {\n return resolve(syntaurRoot(), 'todos');\n}\n\nexport function viewPrefsFile(): string {\n return resolve(syntaurRoot(), 'view-prefs.json');\n}\n\nexport function savedViewsFile(): string {\n return resolve(syntaurRoot(), 'saved-views.json');\n}\n\nexport function projectTodosDir(projectsDir: string, projectSlug: string): string {\n return resolve(projectsDir, projectSlug, 'todos');\n}\n\nexport function todoPlanDir(todosDir: string, workspaceOrProject: string, todoId: string): string {\n return resolve(todosDir, 'plans', workspaceOrProject, todoId);\n}\n\n// Bundle plan files live under `plans/<scopeOrProject>/bundles/<bundleId>/`,\n// keeping them disjoint from todo plans (which omit the `bundles/` segment).\nexport function bundlePlanDir(todosDir: string, scopeOrProject: string, bundleId: string): string {\n return resolve(todosDir, 'plans', scopeOrProject, 'bundles', bundleId);\n}\n\n// Bundle storage lives under a `bundles/` subdirectory so the workspace-checklist\n// discovery glob (which scans top-level *.md files in todosDir) does not pick it up.\nexport function bundlesDir(todosDir: string): string {\n return resolve(todosDir, 'bundles');\n}\n\nexport function bundlesPath(todosDir: string): string {\n return resolve(todosDir, 'bundles', 'index.md');\n}\n\nexport function proofDir(assignmentDir: string): string {\n return resolve(assignmentDir, 'proof');\n}\n","import { mkdir, writeFile, readFile, access, rename } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\nexport async function ensureDir(dir: string): Promise<void> {\n await mkdir(dir, { recursive: true });\n}\n\nexport async function fileExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function writeFileSafe(\n filePath: string,\n content: string,\n): Promise<boolean> {\n if (await fileExists(filePath)) {\n return false;\n }\n await ensureDir(dirname(filePath));\n await writeFile(filePath, content, 'utf-8');\n return true;\n}\n\nexport async function writeFileForce(\n filePath: string,\n content: string,\n): Promise<void> {\n const dir = dirname(filePath);\n const tempPath = join(\n dir,\n `.${Math.random().toString(36).slice(2)}.${Date.now()}.tmp`,\n );\n await ensureDir(dir);\n await writeFile(tempPath, content, 'utf-8');\n await rename(tempPath, filePath);\n}\n\nexport type WriteReportStatus =\n | 'written'\n | 'already-current'\n | 'differs-preserved'\n | 'overwritten';\n\n/**\n * Content-aware idempotent write. Unlike `writeFileSafe` (which skips on mere\n * existence), this compares the on-disk content:\n * - missing → write → 'written'\n * - present & equal → no-op → 'already-current'\n * - present & differs → keep (no force)→ 'differs-preserved'\n * - present & differs → overwrite → 'overwritten' (force)\n * Mirrors the skill-install status vocabulary so adapter writes report honestly.\n */\nexport async function writeFileReport(\n filePath: string,\n content: string,\n options: { force?: boolean } = {},\n): Promise<WriteReportStatus> {\n if (!(await fileExists(filePath))) {\n await ensureDir(dirname(filePath));\n await writeFile(filePath, content, 'utf-8');\n return 'written';\n }\n const current = await readFile(filePath, 'utf-8').catch(() => null);\n if (current === content) {\n return 'already-current';\n }\n if (!options.force) {\n return 'differs-preserved';\n }\n await writeFileForce(filePath, content);\n return 'overwritten';\n}\n","export interface ConfigParams {\n defaultProjectDir: string;\n}\n\nexport function renderConfig(params: ConfigParams): string {\n return `---\nversion: \"1.0\"\ndefaultProjectDir: ${params.defaultProjectDir}\nonboarding:\n completed: false\nagentDefaults:\n trustLevel: medium\n autoApprove: false\nbackup:\n repo: null\n categories: projects, playbooks, todos, servers, config\n lastBackup: null\n lastRestore: null\n---\n\n# Syntaur Configuration\n\nGlobal configuration for the Syntaur CLI.\n`;\n}\n","export function nowTimestamp(): string {\n return new Date().toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n}\n","import { readdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport type { Dirent } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { fileExists } from './fs.js';\nimport { nowTimestamp } from './timestamp.js';\n\n/**\n * Filesystem-level migrations for users upgrading from pre-v0.2.0 installs,\n * where the product used \"mission\" terminology. v0.2.0 renamed code but\n * shipped no on-disk migration, leaving user state unreadable by the new\n * scanner. These helpers close that gap.\n *\n * All helpers are idempotent, safe on missing paths, and NEVER delete user\n * files. Legacy files that are no longer read (e.g., per-project agent.md,\n * claude.md) are left untouched.\n */\n\nexport interface ProjectFilesMigrationResult {\n /** Relative paths of files that were renamed (e.g. `ai-chat-v2/mission.md`). */\n renamedProjectFiles: string[];\n /** Project slugs that still have stale agent.md / claude.md files. Reported, not deleted. */\n legacyExtras: string[];\n}\n\nexport interface ConfigMigrationResult {\n /** True if `defaultMissionDir` was renamed to `defaultProjectDir`. */\n renamedField: boolean;\n /** True if the on-disk `<root>/missions` dir was renamed to `<root>/projects`. */\n renamedDir: boolean;\n /** The resolved projects dir after migration (absolute, or null if config absent). */\n resolvedProjectsDir: string | null;\n}\n\n/**\n * Walk each project directory under `projectsDir` and rename\n * `mission.md` → `project.md` when the legacy file is present and the new\n * name isn't. Reports stale per-project `agent.md` / `claude.md` files\n * without touching them.\n *\n * Swallows per-entry errors (e.g., EPERM on a single dir) so one bad\n * project can't block the rest. Never throws.\n */\nexport async function migrateLegacyProjectFiles(\n projectsDir: string,\n): Promise<ProjectFilesMigrationResult> {\n const result: ProjectFilesMigrationResult = {\n renamedProjectFiles: [],\n legacyExtras: [],\n };\n\n if (!(await fileExists(projectsDir))) return result;\n\n let entries: Dirent[];\n try {\n entries = (await readdir(projectsDir, { withFileTypes: true })) as Dirent[];\n } catch {\n return result;\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith('.')) continue;\n\n const projectDir = resolve(projectsDir, entry.name);\n const legacy = resolve(projectDir, 'mission.md');\n const target = resolve(projectDir, 'project.md');\n\n try {\n if ((await fileExists(legacy)) && !(await fileExists(target))) {\n await rename(legacy, target);\n result.renamedProjectFiles.push(`${entry.name}/mission.md`);\n }\n } catch {\n // Swallow per-project errors (permission denied, racing editor, etc).\n continue;\n }\n\n // Surface stale legacy files without deleting them — caller decides how\n // to present (log once at startup).\n for (const stale of ['agent.md', 'claude.md']) {\n try {\n if (await fileExists(resolve(projectDir, stale))) {\n result.legacyExtras.push(`${entry.name}/${stale}`);\n }\n } catch {\n // Ignore.\n }\n }\n }\n\n return result;\n}\n\n/**\n * Set (or insert) a top-level scalar frontmatter field. Mirrors the dashboard\n * `setTopLevelField` writer: replaces the field in place when present, else\n * inserts it just before the closing `---`. Returns the content unchanged if\n * the field is absent and no closing delimiter is found.\n */\nfunction setFrontmatterField(content: string, key: string, value: boolean | string | null): string {\n const formatted = value === null ? 'null' : typeof value === 'boolean' ? String(value) : value;\n const fieldRegex = new RegExp(`^(${key}:)\\\\s*.*$`, 'm');\n if (fieldRegex.test(content)) {\n return content.replace(fieldRegex, `$1 ${formatted}`);\n }\n const closingIdx = content.indexOf('\\n---', 4);\n if (closingIdx === -1) return content;\n return `${content.slice(0, closingIdx)}\\n${key}: ${formatted}${content.slice(closingIdx)}`;\n}\n\n/** Read a top-level scalar field's raw (trimmed) value, or null if absent. */\nfunction readFrontmatterField(content: string, key: string): string | null {\n const match = content.match(new RegExp(`^${key}:\\\\s*(.*)$`, 'm'));\n if (!match) return null;\n const raw = match[1].trim().replace(/^['\"]|['\"]$/g, '');\n return raw === '' || raw === 'null' ? null : raw;\n}\n\nexport interface ArchivedProjectsMigrationResult {\n /** Project slugs reconciled from `statusOverride: archived` to the real flag. */\n reconciled: string[];\n}\n\n/**\n * Reconcile legacy \"archived-as-a-status\" projects into the orthogonal archive\n * flag. Any `project.md` whose frontmatter has `statusOverride: archived` is\n * rewritten to `archived: true` (stamping `archivedAt` if not already set) and\n * its `statusOverride` is cleared, so `archived` is the single source of truth.\n *\n * Idempotent: only rewrites a file when it still carries `statusOverride: archived`.\n * Swallows per-project errors and never throws.\n */\nexport async function migrateLegacyArchivedProjects(\n projectsDir: string,\n): Promise<ArchivedProjectsMigrationResult> {\n const result: ArchivedProjectsMigrationResult = { reconciled: [] };\n\n if (!(await fileExists(projectsDir))) return result;\n\n let entries: Dirent[];\n try {\n entries = (await readdir(projectsDir, { withFileTypes: true })) as Dirent[];\n } catch {\n return result;\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith('.')) continue;\n\n const projectMd = resolve(projectsDir, entry.name, 'project.md');\n try {\n if (!(await fileExists(projectMd))) continue;\n const content = await readFile(projectMd, 'utf-8');\n // Only act on projects whose status override is exactly \"archived\".\n if (readFrontmatterField(content, 'statusOverride') !== 'archived') continue;\n\n let next = setFrontmatterField(content, 'archived', true);\n if (readFrontmatterField(content, 'archivedAt') === null) {\n next = setFrontmatterField(next, 'archivedAt', nowTimestamp());\n }\n next = setFrontmatterField(next, 'statusOverride', null);\n next = setFrontmatterField(next, 'updated', nowTimestamp());\n\n await writeFile(projectMd, next, 'utf-8');\n result.reconciled.push(entry.name);\n } catch {\n // Swallow per-project errors (permission denied, racing editor, etc).\n continue;\n }\n }\n\n return result;\n}\n\n/**\n * Migrate ~/.syntaur/config.md frontmatter and, optionally, the on-disk\n * projects directory, from the pre-v0.2.0 \"mission\" layout.\n *\n * - Renames `defaultMissionDir` → `defaultProjectDir` in frontmatter when\n * the new key isn't already present.\n * - If the resolved projects dir ends in `/missions` AND that dir exists\n * AND its `/projects` sibling does not, renames the directory on disk\n * and updates the config to point at the new path.\n *\n * Only rewrites the config file when an actual change is made.\n */\nexport async function migrateLegacyConfig(\n configPath: string,\n): Promise<ConfigMigrationResult> {\n const result: ConfigMigrationResult = {\n renamedField: false,\n renamedDir: false,\n resolvedProjectsDir: null,\n };\n\n if (!(await fileExists(configPath))) return result;\n\n let content: string;\n try {\n content = await readFile(configPath, 'utf-8');\n } catch {\n return result;\n }\n\n const fmMatch = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?/);\n if (!fmMatch) return result;\n\n const fmBlock = fmMatch[1];\n const afterFm = content.slice(fmMatch[0].length);\n\n // --- Field rename ---\n const missionLineRe = /^(\\s*)defaultMissionDir\\s*:\\s*(.*)$/m;\n const missionLineMatch = fmBlock.match(missionLineRe);\n const hasProjectLine = /^\\s*defaultProjectDir\\s*:/m.test(fmBlock);\n\n let newFmBlock = fmBlock;\n let missionValue: string | null = null;\n if (missionLineMatch) {\n missionValue = missionLineMatch[2].trim();\n if (!hasProjectLine) {\n newFmBlock = fmBlock.replace(\n missionLineRe,\n `$1defaultProjectDir: ${missionValue}`,\n );\n result.renamedField = true;\n } else {\n // Both keys present; strip the legacy one to avoid drift.\n newFmBlock = fmBlock.replace(missionLineRe, '').replace(/\\n{2,}/g, '\\n');\n result.renamedField = true;\n }\n }\n\n // --- Resolve the current projects dir from whatever the frontmatter says. ---\n const projectLineRe = /^\\s*defaultProjectDir\\s*:\\s*(.*)$/m;\n const projectLineMatch = newFmBlock.match(projectLineRe);\n const projectsDirRaw = projectLineMatch\n ? projectLineMatch[1].trim().replace(/^['\"]|['\"]$/g, '')\n : missionValue;\n\n const expand = (p: string): string =>\n p.startsWith('~')\n ? resolve(process.env.HOME ?? '/', p.slice(p.startsWith('~/') ? 2 : 1))\n : p;\n\n let resolvedProjectsDir = projectsDirRaw ? expand(projectsDirRaw) : null;\n\n // --- Directory rename (only if the value still points at a /missions dir). ---\n if (resolvedProjectsDir && resolvedProjectsDir.endsWith('/missions')) {\n const siblingProjectsDir = resolvedProjectsDir.replace(/\\/missions$/, '/projects');\n if (\n (await fileExists(resolvedProjectsDir)) &&\n !(await fileExists(siblingProjectsDir))\n ) {\n try {\n await rename(resolvedProjectsDir, siblingProjectsDir);\n // Update the config line to point at the new dir. Preserve any ~ prefix.\n const newValue = projectsDirRaw!.endsWith('/missions')\n ? projectsDirRaw!.replace(/\\/missions$/, '/projects')\n : siblingProjectsDir;\n newFmBlock = newFmBlock.replace(\n projectLineRe,\n `defaultProjectDir: ${newValue}`,\n );\n resolvedProjectsDir = siblingProjectsDir;\n result.renamedDir = true;\n } catch {\n // If rename fails (permissions, cross-device), leave both config and\n // filesystem alone. Scanner will still hit the legacy dir.\n }\n }\n }\n\n result.resolvedProjectsDir = resolvedProjectsDir;\n\n if (result.renamedField || result.renamedDir) {\n const newContent = `---\\n${newFmBlock.replace(/\\n+$/, '')}\\n---\\n${afterFm.startsWith('\\n') ? afterFm.slice(1) : afterFm}`;\n try {\n await writeFile(configPath, newContent, 'utf-8');\n } catch {\n // If we can't persist the config, revert the flags so the caller\n // doesn't report a fake success.\n result.renamedField = false;\n result.renamedDir = false;\n }\n }\n\n return result;\n}\n\n/**\n * Format a concise summary line for startup logs. Empty string when nothing\n * material happened (caller should skip the log).\n */\nexport function summarizeMigration(\n project: ProjectFilesMigrationResult,\n config?: ConfigMigrationResult,\n): string {\n const parts: string[] = [];\n if (project.renamedProjectFiles.length > 0) {\n const firstThree = project.renamedProjectFiles\n .map((p) => p.split('/')[0])\n .slice(0, 3)\n .join(', ');\n const more =\n project.renamedProjectFiles.length > 3\n ? ` and ${project.renamedProjectFiles.length - 3} more`\n : '';\n parts.push(\n `renamed mission.md → project.md in ${project.renamedProjectFiles.length} project${project.renamedProjectFiles.length === 1 ? '' : 's'} (${firstThree}${more})`,\n );\n }\n if (config?.renamedField) parts.push('updated config defaultMissionDir → defaultProjectDir');\n if (config?.renamedDir) parts.push('renamed projects directory on disk');\n if (project.legacyExtras.length > 0) {\n parts.push(\n `${project.legacyExtras.length} legacy agent.md / claude.md file${project.legacyExtras.length === 1 ? '' : 's'} left in place (no longer read)`,\n );\n }\n return parts.length ? `[syntaur] legacy migration: ${parts.join('; ')}` : '';\n}\n","export type AssignmentStatus = string;\n\nexport type TransitionCommand = string;\n\nexport const DEFAULT_STATUSES = [\n 'draft',\n 'pending',\n 'ready_for_planning',\n 'ready_to_implement',\n 'in_progress',\n 'blocked',\n 'review',\n 'completed',\n 'failed',\n] as const;\n\nexport const DEFAULT_COMMANDS = [\n 'start',\n 'shape',\n 'plan-ready',\n 'implement',\n 'complete',\n 'block',\n 'unblock',\n 'review',\n 'fail',\n 'reopen',\n 'assign',\n] as const;\n\nexport const DEFAULT_TERMINAL_STATUSES: ReadonlySet<string> = new Set([\n 'completed',\n 'failed',\n]);\n\nexport const TERMINAL_STATUSES: ReadonlySet<string> = DEFAULT_TERMINAL_STATUSES;\n\nexport interface ExternalId {\n system: string;\n id: string;\n url: string | null;\n}\n\n/**\n * One row in an assignment's `statusHistory` frontmatter array — an append-only\n * log of status transitions. `at`/`from`/`to` are always present (`from` is null\n * only for the creation/seed entry). `command`/`by` are recorded when known;\n * `reason` is set on `block` transitions. See the Query Language design doc,\n * Piece 1, for the full data-model rationale.\n *\n * Dimension-aware extension (derived-status design v3): `from`/`to` ALWAYS hold\n * the headline status. When the underlying phase and/or disposition dimension\n * changed, the optional `phaseFrom/phaseTo` / `dispositionFrom/dispositionTo`\n * keys record it — so a phase change under an unchanged headline (e.g. progress\n * while blocked) is representable as `from: blocked, to: blocked,\n * phaseFrom: planning, phaseTo: ready_to_implement`. Entries written before the\n * dimension model simply lack the keys and parse unchanged.\n */\nexport interface StatusHistoryEntry {\n at: string;\n from: string | null;\n to: string;\n command: string;\n by: string | null;\n reason?: string;\n phaseFrom?: string | null;\n phaseTo?: string | null;\n dispositionFrom?: string | null;\n dispositionTo?: string | null;\n}\n\n/**\n * Revision-bound plan approval record (derived-status design v3, Piece 5).\n * The derived `planApproved` fact is true iff `file` is still the latest plan\n * revision AND `digest` matches its current content — so a replan or a\n * post-approval edit auto-invalidates the approval.\n */\nexport interface PlanApproval {\n file: string;\n digest: string;\n by: string | null;\n at: string;\n}\n\n/**\n * One attestation record (custom-facts-attestations): \"agent X reviewed\n * revision Y with verdict Z\". One record per (fact, actor) — re-attesting\n * replaces that actor's record. Revision-bound via the binding snapshot:\n * `file`+`digest` for binds:plan (planApproval semantics), `commit` for\n * binds:commit, neither for binds:none. A record is VALID only while its\n * snapshot still matches the live revision; stale records contribute nothing.\n */\nexport interface AttestationRecord {\n fact: string;\n actor: string;\n verdict: 'approved' | 'changes-requested';\n at: string;\n note?: string;\n /** binds:plan snapshot — plan file name + its digest at attest time. */\n file?: string;\n digest?: string;\n /** binds:commit snapshot — workspace HEAD sha at attest time. */\n commit?: string;\n}\n\n/**\n * Sticky manual status override (\"pin\"). Folded into the written headline\n * `status` at recompute time; the un-overridden derived headline travels in\n * API payloads only (divergence display). May not target a terminal status\n * and may not be applied to a terminal assignment.\n */\nexport interface StatusOverride {\n status: string;\n source: string; // 'human' | 'agent:<id>'\n reason: string | null;\n at: string;\n}\n\n/** Disposition dimension values (orthogonal to phase). */\nexport const DISPOSITIONS = ['active', 'blocked', 'parked', 'terminal'] as const;\nexport type Disposition = (typeof DISPOSITIONS)[number];\n\nexport interface Workspace {\n repository: string | null;\n worktreePath: string | null;\n branch: string | null;\n parentBranch: string | null;\n}\n\nexport interface AssignmentFrontmatter {\n id: string;\n slug: string;\n title: string;\n project: string | null;\n type: string | null;\n status: AssignmentStatus;\n priority: 'low' | 'medium' | 'high' | 'critical';\n created: string;\n updated: string;\n assignee: string | null;\n externalIds: ExternalId[];\n statusHistory: StatusHistoryEntry[];\n dependsOn: string[];\n links: string[];\n blockedReason: string | null;\n workspace: Workspace;\n tags: string[];\n archived: boolean;\n archivedAt: string | null;\n archivedReason: string | null;\n // ── derived-status v3 fields ─────────────────────────────────────────────\n /** Cached phase dimension (written by recompute; null pre-migration). */\n phase: string | null;\n /** Cached disposition dimension (written by recompute; null pre-migration). */\n disposition: string | null;\n /** Revision-bound plan approval record; null = not approved. */\n planApproval: PlanApproval | null;\n /** Intentional withhold → disposition: parked. */\n parked: boolean;\n /** Review escalation atom; feeds the review phase rung. */\n reviewRequested: boolean;\n /** Asserted \"implementation has begun\" (worktrees precede planning, so workspaceSet ≠ building). */\n implementationStarted: boolean;\n /** Sticky manual pin; null = no override. */\n override: StatusOverride | null;\n /** Custom asserted fact values (raw scalars keyed by declared name; typed\n * coercion against declarations happens in facts.ts). Absent block → {}. */\n facts: Record<string, string>;\n /** Attestation records, one per (fact, actor). Revision-bound; stale records\n * contribute nothing at compute time. Absent block → []. */\n attestations: AttestationRecord[];\n}\n\nexport interface TransitionResult {\n success: boolean;\n message: string;\n fromStatus: AssignmentStatus;\n toStatus?: AssignmentStatus;\n warnings?: string[];\n}\n","import type { AssignmentStatus, TransitionCommand } from './types.js';\nimport { TERMINAL_STATUSES } from './types.js';\n\n/**\n * Maps a command to its target status. Commands always produce the same\n * target regardless of the current status — workflow enforcement is\n * handled via agent prompting, not code guards.\n */\nexport const DEFAULT_COMMAND_TARGETS = new Map<string, string>([\n ['start', 'in_progress'],\n ['shape', 'ready_for_planning'],\n ['plan-ready', 'ready_to_implement'],\n ['implement', 'in_progress'],\n ['block', 'blocked'],\n ['unblock', 'in_progress'],\n ['review', 'review'],\n ['complete', 'completed'],\n ['fail', 'failed'],\n ['reopen', 'in_progress'],\n]);\n\n/**\n * Built-in `from:command` → `to` map for the default (no custom config) status\n * set. Used by the dashboard to guard which transitions are valid from a given\n * status (see getTargetStatus when a table is passed). The CLI transition path\n * passes no table and stays guard-free via DEFAULT_COMMAND_TARGETS.\n */\nexport const DEFAULT_TRANSITION_TABLE = new Map<string, string>([\n ['pending:start', 'in_progress'],\n ['pending:block', 'blocked'],\n ['draft:shape', 'ready_for_planning'],\n ['draft:start', 'in_progress'],\n ['ready_for_planning:plan-ready', 'ready_to_implement'],\n ['ready_for_planning:start', 'in_progress'],\n ['ready_to_implement:implement', 'in_progress'],\n ['in_progress:block', 'blocked'],\n ['in_progress:review', 'review'],\n ['in_progress:complete', 'completed'],\n ['in_progress:fail', 'failed'],\n ['blocked:unblock', 'in_progress'],\n ['review:start', 'in_progress'],\n ['review:complete', 'completed'],\n ['review:fail', 'failed'],\n ['completed:reopen', 'in_progress'],\n ['failed:reopen', 'in_progress'],\n]);\n\nexport function buildTransitionTable(\n transitions: Array<{ from: string; command: string; to: string }>,\n): Map<string, string> {\n const table = new Map<string, string>();\n for (const t of transitions) {\n table.set(`${t.from}:${t.command}`, t.to);\n }\n return table;\n}\n\nexport function buildCommandTargets(\n transitions: Array<{ from: string; command: string; to: string }>,\n): Map<string, string> {\n const targets = new Map<string, string>();\n for (const t of transitions) {\n targets.set(t.command, t.to);\n }\n return targets;\n}\n\nexport function getTargetStatus(\n _from: AssignmentStatus,\n command: TransitionCommand,\n table?: Map<string, string>,\n): AssignmentStatus | null {\n // No table provided (e.g. the CLI transition path): commands are guard-free —\n // workflow enforcement happens via agent prompting, not code — so a command\n // resolves to its canonical target regardless of the current status.\n if (!table) {\n return DEFAULT_COMMAND_TARGETS.get(command) ?? null;\n }\n // A table was provided (the dashboard passes one — custom, or the built-in\n // DEFAULT_TRANSITION_TABLE): honor `from:command` so only transitions valid\n // from the current status resolve. The kanban inline picker renders these\n // directly and must not offer e.g. `start` on an in_progress card. Look up the\n // status-specific `from:command` key FIRST so a per-status guard always wins;\n // the bare-command key is only a defensive fallback (no current table emits\n // one — DEFAULT_TRANSITION_TABLE and buildTransitionTable() key by\n // `from:command`). The old bare-first order would have let a future bare entry\n // silently override the status-specific guard.\n return table.get(`${_from}:${command}`) ?? table.get(command) ?? null;\n}\n\n/** @deprecated Guards removed — always returns true for known commands */\nexport function canTransition(\n _from: AssignmentStatus,\n command: TransitionCommand,\n table?: Map<string, string>,\n): boolean {\n return getTargetStatus(_from, command, table) !== null;\n}\n\nexport function isTerminalStatus(\n status: AssignmentStatus,\n terminalSet?: ReadonlySet<string>,\n): boolean {\n return (terminalSet ?? TERMINAL_STATUSES).has(status);\n}\n","import type {\n AssignmentFrontmatter,\n AttestationRecord,\n ExternalId,\n PlanApproval,\n StatusHistoryEntry,\n StatusOverride,\n Workspace,\n} from './types.js';\n\nfunction extractFrontmatter(fileContent: string): [string, string] {\n const match = fileContent.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) {\n throw new Error('No frontmatter found in file. Expected --- delimiters.');\n }\n const frontmatterBlock = match[1];\n const body = fileContent.slice(match[0].length);\n return [frontmatterBlock, body];\n}\n\nfunction parseSimpleValue(raw: string): string | null {\n const trimmed = raw.trim();\n if (trimmed === 'null' || trimmed === '~' || trimmed === '') return null;\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n // Decode the escapes formatYamlValue encodes — round-trip safety for\n // values containing quotes/backslashes (codex code-review finding 4).\n return trimmed.slice(1, -1).replace(/\\\\([\"\\\\])/g, '$1');\n }\n if (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\") && trimmed.length >= 2) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\nfunction parseDependsOn(frontmatter: string): string[] {\n const inlineMatch = frontmatter.match(/^dependsOn:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: string[] = [];\n const blockMatch = frontmatter.match(/^dependsOn:\\s*\\n((?:\\s+-\\s+.*\\n?)*)/m);\n if (blockMatch) {\n const items = blockMatch[1].matchAll(/^\\s+-\\s+(.+)$/gm);\n for (const item of items) {\n results.push(item[1].trim());\n }\n }\n return results;\n}\n\nfunction parseLinks(frontmatter: string): string[] {\n const inlineMatch = frontmatter.match(/^links:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: string[] = [];\n const blockMatch = frontmatter.match(/^links:\\s*\\n((?:\\s+-\\s+.*\\n?)*)/m);\n if (blockMatch) {\n const items = blockMatch[1].matchAll(/^\\s+-\\s+(.+)$/gm);\n for (const item of items) {\n results.push(item[1].trim());\n }\n }\n return results;\n}\n\nfunction parseExternalIds(frontmatter: string): ExternalId[] {\n const inlineMatch = frontmatter.match(/^externalIds:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: ExternalId[] = [];\n const blockMatch = frontmatter.match(\n /^externalIds:\\s*\\n((?:\\s+-\\s+[\\s\\S]*?)(?=^\\w|\\n---))/m,\n );\n if (!blockMatch) return [];\n\n const itemBlocks = blockMatch[1].split(/\\n\\s+-\\s+/).filter(Boolean);\n for (const block of itemBlocks) {\n const lines = block.split('\\n');\n const entry: Record<string, string | null> = {};\n for (const line of lines) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n if (entry['system'] && entry['id']) {\n results.push({\n system: entry['system'],\n id: entry['id'],\n url: entry['url'] || null,\n });\n }\n }\n return results;\n}\n\n/**\n * Parse the `statusHistory` list-of-mappings from a frontmatter string.\n *\n * NOTE on the boundary: `extractFrontmatter` strips the closing `\\n---`, so when\n * `statusHistory` is the LAST frontmatter key there is no trailing `---` and no\n * following top-level key. The `parseExternalIds` regex boundary `(?=^\\w|\\n---)`\n * would silently drop such a block, and `$` under `/m` matches end-of-LINE (which\n * would truncate an entry after its first line). So this uses a robust line-scan:\n * collect blank/indented lines after the header until the first column-0 non-blank\n * line OR end of input. This is end-of-input safe regardless of the `---` delimiter.\n */\nfunction parseStatusHistory(frontmatter: string): StatusHistoryEntry[] {\n if (/^statusHistory:\\s*\\[\\s*\\]/m.test(frontmatter)) return [];\n\n const headerMatch = frontmatter.match(/^statusHistory:\\s*$/m);\n if (!headerMatch) return [];\n\n // Use the regex match offset, NOT indexOf(headerMatch[0]) — an earlier scalar\n // value could contain the substring \"statusHistory:\" (e.g. a title) and shift\n // the start position, dropping the real block.\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = frontmatter.slice(bodyStart);\n\n const bodyLines: string[] = [];\n for (const line of after.split('\\n')) {\n if (line.length === 0) {\n bodyLines.push(line); // blank line — keep scanning (YAML allows blanks in a block)\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break; // column-0 non-blank → block ended\n bodyLines.push(line);\n }\n const body = bodyLines.join('\\n');\n\n const results: StatusHistoryEntry[] = [];\n const itemBlocks = body.split(/\\n\\s+-\\s+/).filter((b) => b.trim().length > 0);\n for (const block of itemBlocks) {\n const entry: Record<string, string | null> = {};\n for (const line of block.split('\\n')) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n // `to` is required; `from` is null only on the seed/create entry.\n if (!entry['to']) continue;\n const result: StatusHistoryEntry = {\n at: entry['at'] ?? '',\n from: entry['from'] ?? null,\n to: entry['to'],\n command: entry['command'] ?? '',\n by: entry['by'] ?? null,\n };\n if (entry['reason'] != null) result.reason = entry['reason'];\n // Dimension-aware optional keys (derived-status v3); absent on old entries.\n if ('phaseFrom' in entry) result.phaseFrom = entry['phaseFrom'];\n if ('phaseTo' in entry) result.phaseTo = entry['phaseTo'];\n if ('dispositionFrom' in entry) result.dispositionFrom = entry['dispositionFrom'];\n if ('dispositionTo' in entry) result.dispositionTo = entry['dispositionTo'];\n results.push(result);\n }\n return results;\n}\n\n/**\n * Parse a flat nested mapping block (`header:` + indented `key: value` lines)\n * into a string map. Returns null when the header is absent or explicitly null.\n * Shared by `planApproval` / `override` parsing; mirrors `parseWorkspace`'s\n * field scanning but generically.\n */\nfunction parseNestedBlock(frontmatter: string, header: string): Record<string, string | null> | null {\n if (new RegExp(`^${header}:\\\\s*(null|~)\\\\s*$`, 'm').test(frontmatter)) return null;\n const headerMatch = frontmatter.match(new RegExp(`^${header}:\\\\s*$`, 'm'));\n if (!headerMatch) return null;\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const after = frontmatter.slice(headerStart + headerMatch[0].length + 1);\n const out: Record<string, string | null> = {};\n for (const line of after.split('\\n')) {\n if (line.length === 0) continue;\n if (line[0] !== ' ' && line[0] !== '\\t') break; // top-level key — block ended\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim();\n if (!key) continue;\n out[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n return Object.keys(out).length > 0 ? out : null;\n}\n\nfunction parsePlanApproval(frontmatter: string): PlanApproval | null {\n const block = parseNestedBlock(frontmatter, 'planApproval');\n if (!block || !block['file'] || !block['digest']) return null;\n return {\n file: block['file'],\n digest: block['digest'],\n by: block['by'] ?? null,\n at: block['at'] ?? '',\n };\n}\n\nfunction parseOverride(frontmatter: string): StatusOverride | null {\n const block = parseNestedBlock(frontmatter, 'override');\n if (!block || !block['status']) return null;\n return {\n status: block['status'],\n source: block['source'] ?? 'human',\n reason: block['reason'] ?? null,\n at: block['at'] ?? '',\n };\n}\n\n/**\n * Parse the `facts:` map (custom asserted fact values). Reuses\n * {@link parseNestedBlock}: absent/null block → `{}`; entries whose value is\n * null (empty / `null` / `~`) are DROPPED; remaining values kept as trimmed\n * strings (parseSimpleValue already trims + strips quotes). Typed coercion\n * against declarations happens in facts.ts — hand-edited garbage degrades there.\n */\nfunction parseFactsMap(frontmatter: string): Record<string, string> {\n const block = parseNestedBlock(frontmatter, 'facts');\n if (!block) return {};\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(block)) {\n if (v === null) continue;\n out[k] = v;\n }\n return out;\n}\n\n/**\n * Parse the `attestations:` record list. Modeled on {@link parseStatusHistory}\n * (same end-of-input-safe line scan). Records missing any required key\n * (fact/actor/verdict/at) or carrying an unknown verdict are dropped.\n */\nfunction parseAttestations(frontmatter: string): AttestationRecord[] {\n if (/^attestations:\\s*\\[\\s*\\]/m.test(frontmatter)) return [];\n\n const headerMatch = frontmatter.match(/^attestations:\\s*$/m);\n if (!headerMatch) return [];\n\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = frontmatter.slice(bodyStart);\n\n const bodyLines: string[] = [];\n for (const line of after.split('\\n')) {\n if (line.length === 0) {\n bodyLines.push(line);\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n bodyLines.push(line);\n }\n const body = bodyLines.join('\\n');\n\n const results: AttestationRecord[] = [];\n const itemBlocks = body.split(/\\n\\s+-\\s+/).filter((b) => b.trim().length > 0);\n for (const block of itemBlocks) {\n const entry: Record<string, string | null> = {};\n for (const line of block.split('\\n')) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n const verdict = entry['verdict'];\n if (!entry['fact'] || !entry['actor'] || !verdict || !entry['at']) continue;\n if (verdict !== 'approved' && verdict !== 'changes-requested') continue;\n const record: AttestationRecord = {\n fact: entry['fact'],\n actor: entry['actor'],\n verdict,\n at: entry['at'],\n };\n if (entry['note'] != null) record.note = entry['note'];\n if (entry['file'] != null) record.file = entry['file'];\n if (entry['digest'] != null) record.digest = entry['digest'];\n if (entry['commit'] != null) record.commit = entry['commit'];\n results.push(record);\n }\n return results;\n}\n\nfunction parseWorkspace(frontmatter: string): Workspace {\n const defaults: Workspace = {\n repository: null,\n worktreePath: null,\n branch: null,\n parentBranch: null,\n };\n\n const fields = ['repository', 'worktreePath', 'branch', 'parentBranch'] as const;\n for (const field of fields) {\n const match = frontmatter.match(new RegExp(`^\\\\s+${field}:\\\\s*(.*)$`, 'm'));\n if (match) {\n defaults[field] = parseSimpleValue(match[1]);\n }\n }\n return defaults;\n}\n\nfunction parseTags(frontmatter: string): string[] {\n const inlineMatch = frontmatter.match(/^tags:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: string[] = [];\n const blockMatch = frontmatter.match(/^tags:\\s*\\n((?:\\s+-\\s+.*\\n?)*)/m);\n if (blockMatch) {\n const items = blockMatch[1].matchAll(/^\\s+-\\s+(.+)$/gm);\n for (const item of items) {\n results.push(item[1].trim());\n }\n }\n return results;\n}\n\nexport function parseAssignmentFrontmatter(fileContent: string): AssignmentFrontmatter {\n const [frontmatter] = extractFrontmatter(fileContent);\n\n function getField(key: string): string | null {\n const match = frontmatter.match(new RegExp(`^${key}:\\\\s*(.*)$`, 'm'));\n if (!match) return null;\n return parseSimpleValue(match[1]);\n }\n\n return {\n id: getField('id') ?? '',\n slug: getField('slug') ?? '',\n title: getField('title') ?? '',\n project: getField('project'),\n type: getField('type'),\n status: getField('status') ?? 'pending',\n priority: (getField('priority') ?? 'medium') as AssignmentFrontmatter['priority'],\n created: getField('created') ?? '',\n updated: getField('updated') ?? '',\n assignee: getField('assignee'),\n externalIds: parseExternalIds(frontmatter),\n statusHistory: parseStatusHistory(frontmatter),\n dependsOn: parseDependsOn(frontmatter),\n links: parseLinks(frontmatter),\n blockedReason: getField('blockedReason'),\n workspace: parseWorkspace(frontmatter),\n tags: parseTags(frontmatter),\n archived: getField('archived') === 'true',\n archivedAt: getField('archivedAt'),\n archivedReason: getField('archivedReason'),\n phase: getField('phase'),\n disposition: getField('disposition'),\n planApproval: parsePlanApproval(frontmatter),\n parked: getField('parked') === 'true',\n reviewRequested: getField('reviewRequested') === 'true',\n implementationStarted: getField('implementationStarted') === 'true',\n override: parseOverride(frontmatter),\n facts: parseFactsMap(frontmatter),\n attestations: parseAttestations(frontmatter),\n };\n}\n\nfunction formatYamlValue(value: string | boolean | null): string {\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (value === null) return 'null';\n // Frontmatter scalars are single-line by contract: flatten embedded\n // newlines rather than corrupting the block (codex code-review finding 4).\n if (/[\\r\\n]/.test(value)) {\n value = value.replace(/\\s*[\\r\\n]+\\s*/g, ' ').trim();\n }\n if (/^\\d{4}-\\d{2}-\\d{2}T/.test(value)) {\n return `\"${value}\"`;\n }\n // Quote YAML keyword/number look-alikes so a literal string \"null\"/\"true\"/\n // \"42\" round-trips as a string, not the YAML scalar.\n if (/^(null|~|true|false|-?\\d+(\\.\\d+)?)$/i.test(value)) {\n return `\"${value}\"`;\n }\n // Quote values containing YAML-special characters that could cause parse\n // issues, OR a value that is itself wrapped in quote chars (e.g.\n // `\"connection refused\"` / `'x'`) — otherwise parseSimpleValue strips the\n // literal surrounding quotes on read and the value does not round-trip.\n if (\n /[:#{}[\\],&*?|>!%@\\`]/.test(value) ||\n /^\\s|\\s$/.test(value) ||\n /^[\"']|[\"']$/.test(value) ||\n value === ''\n ) {\n const escaped = value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n return `\"${escaped}\"`;\n }\n return value;\n}\n\nexport function updateAssignmentFile(\n fileContent: string,\n updates: Partial<\n Pick<\n AssignmentFrontmatter,\n | 'status'\n | 'assignee'\n | 'blockedReason'\n | 'updated'\n | 'archived'\n | 'archivedAt'\n | 'archivedReason'\n | 'phase'\n | 'disposition'\n | 'parked'\n | 'reviewRequested'\n | 'implementationStarted'\n >\n >,\n): string {\n let result = fileContent;\n\n for (const [key, value] of Object.entries(updates)) {\n if (value === undefined) continue;\n const formatted = formatYamlValue(value as string | boolean | null);\n const fieldRegex = new RegExp(`^(${key}:)\\\\s*.*$`, 'm');\n if (fieldRegex.test(result)) {\n result = result.replace(fieldRegex, `$1 ${formatted}`);\n } else {\n // Insert a missing field just before the closing frontmatter delimiter.\n // `indexOf('\\n---', 4)` skips the opening `---`; mirrors setTopLevelField.\n const closeIdx = result.indexOf('\\n---', 4);\n if (closeIdx !== -1) {\n result = `${result.slice(0, closeIdx)}\\n${key}: ${formatted}${result.slice(closeIdx)}`;\n }\n }\n }\n\n return result;\n}\n\n/**\n * Locate the `workspace:` block inside a frontmatter string and return the\n * [start, end) byte offsets of the *body* of that block (lines indented under\n * `workspace:`, excluding the `workspace:` header line itself). Returns null\n * if no `workspace:` block is present.\n */\nfunction findWorkspaceBlock(\n fmBlock: string,\n): { headerStart: number; bodyStart: number; bodyEnd: number } | null {\n const headerMatch = fmBlock.match(/^workspace:\\s*$/m);\n if (!headerMatch) return null;\n // Regex match offset, not indexOf — guards against an earlier scalar value\n // (e.g. a title) containing the substring \"workspace:\". Mirrors\n // findStatusHistoryBlock / parseStatusHistory.\n const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = fmBlock.slice(bodyStart);\n const lines = after.split('\\n');\n let consumed = 0;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line.length === 0) {\n // blank line — consume but keep scanning; YAML allows blanks inside a block\n consumed += line.length + 1;\n continue;\n }\n if (line[0] !== ' ') break; // top-level key — block ended\n consumed += line.length + 1;\n }\n // Trim a trailing newline we counted past EOF\n const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);\n return { headerStart, bodyStart, bodyEnd };\n}\n\n/**\n * Update nested workspace.* fields (repository, worktreePath, branch, parentBranch)\n * in-place. Edits only inside the `workspace:` block — other indented keys\n * with the same name elsewhere in frontmatter are not touched. Preserves\n * field ordering and unknown workspace fields. If the `workspace:` block does\n * not exist, it is appended to the frontmatter.\n */\nexport function updateAssignmentWorkspace(\n fileContent: string,\n partial: Partial<Workspace>,\n): string {\n const fmMatch = fileContent.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) {\n throw new Error('No frontmatter found in assignment file. Expected --- delimiters.');\n }\n\n const fmBlock = fmMatch[2];\n const fields = ['repository', 'worktreePath', 'branch', 'parentBranch'] as const;\n const block = findWorkspaceBlock(fmBlock);\n\n let newFm = fmBlock;\n\n if (block) {\n let body = fmBlock.slice(block.bodyStart, block.bodyEnd);\n for (const field of fields) {\n if (!(field in partial)) continue;\n const value = partial[field] ?? null;\n const formatted = formatYamlValue(value);\n const lineRegex = new RegExp(`^(\\\\s+${field}:)\\\\s*.*$`, 'm');\n if (lineRegex.test(body)) {\n body = body.replace(lineRegex, `$1 ${formatted}`);\n } else {\n const trimmed = body.replace(/\\n+$/, '');\n body = `${trimmed}${trimmed.length > 0 ? '\\n' : ''} ${field}: ${formatted}\\n`;\n }\n }\n newFm =\n fmBlock.slice(0, block.bodyStart) + body + fmBlock.slice(block.bodyEnd);\n } else {\n const lines = ['workspace:'];\n for (const field of fields) {\n const value = field in partial ? (partial[field] ?? null) : null;\n lines.push(` ${field}: ${formatYamlValue(value)}`);\n }\n newFm = `${fmBlock.replace(/\\n+$/, '')}\\n${lines.join('\\n')}`;\n }\n\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;\n}\n\n/**\n * Relabel a status id within an assignment's `statusHistory` — rewrite every\n * entry whose `from`/`to` equals `oldId` to `newId`, WITHOUT appending a new\n * entry or changing any `at`. Used by `syntaur status rename`: a rename is a\n * relabel, not a transition, so it must preserve `statusAge` (no new entry) yet\n * keep historical labels consistent with the new id (so derived `completedAt`\n * stays correct after renaming a terminal status). Scoped to the frontmatter\n * block; `from:`/`to:` keys are unique to statusHistory entries there. Exact\n * value match avoids relabeling a status whose id is a substring of another.\n */\nexport function renameStatusInHistory(\n content: string,\n oldId: string,\n newId: string,\n): string {\n const fmMatch = content.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) return content;\n const esc = oldId.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n // phaseFrom/phaseTo also hold status ids (phase namespace = status definitions),\n // so a rename must relabel them too. Disposition keys hold dimension values\n // (active/blocked/...), not status ids — excluded. The OLD value may be QUOTED\n // when its id is a YAML keyword/number look-alike — match both forms with\n // (\"?)…\\2. The NEW value is (re)serialized via formatYamlValue so it is quoted\n // exactly when needed (e.g. newId `null`/`true`/`42`), instead of reusing the\n // old value's quote state (which dropped/mistyped keyword-id entries on parse).\n const re = new RegExp(`^(\\\\s+(?:from|to|phaseFrom|phaseTo):[ \\\\t]*)(\"?)${esc}\\\\2[ \\\\t]*$`, 'gm');\n const newFm = fmMatch[2].replace(re, (_m, prefix: string) => `${prefix}${formatYamlValue(newId)}`);\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${content.slice(fmMatch[0].length)}`;\n}\n\n/**\n * Locate the `statusHistory:` block (the multi-line list form, not inline `[]`)\n * inside a frontmatter string and return the [bodyStart, bodyEnd) offsets of the\n * block body (the indented `- …` item lines, excluding the header line). Returns\n * null when there is no block header. Mirrors `findWorkspaceBlock`.\n */\nfunction findStatusHistoryBlock(\n fmBlock: string,\n): { headerStart: number; bodyStart: number; bodyEnd: number } | null {\n const headerMatch = fmBlock.match(/^statusHistory:\\s*$/m);\n if (!headerMatch) return null;\n // Regex match offset, not indexOf — guards against an earlier scalar value\n // containing the substring \"statusHistory:\".\n const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = fmBlock.slice(bodyStart);\n const lines = after.split('\\n');\n let consumed = 0;\n for (const line of lines) {\n if (line.length === 0) {\n consumed += line.length + 1;\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break; // top-level key — block ended\n consumed += line.length + 1;\n }\n const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);\n return { headerStart, bodyStart, bodyEnd };\n}\n\nfunction renderStatusHistoryItem(entry: StatusHistoryEntry): string {\n const lines = [\n ` - at: ${formatYamlValue(entry.at)}`,\n ` from: ${formatYamlValue(entry.from)}`,\n ` to: ${formatYamlValue(entry.to)}`,\n ` command: ${formatYamlValue(entry.command)}`,\n ` by: ${formatYamlValue(entry.by)}`,\n ];\n if (entry.reason !== undefined && entry.reason !== null) {\n lines.push(` reason: ${formatYamlValue(entry.reason)}`);\n }\n // Dimension-aware optional keys — rendered only when present, so entries\n // written by plain status transitions stay byte-identical to the v1 format.\n for (const key of ['phaseFrom', 'phaseTo', 'dispositionFrom', 'dispositionTo'] as const) {\n if (entry[key] !== undefined) {\n lines.push(` ${key}: ${formatYamlValue(entry[key] ?? null)}`);\n }\n }\n return lines.join('\\n');\n}\n\n/**\n * Append one entry to an assignment file's `statusHistory` frontmatter list,\n * returning the new file content. Robust to three states:\n * (i) no `statusHistory:` key → create the block before the closing `---`;\n * (ii) inline `statusHistory: []` → convert it to a block with this entry;\n * (iii) existing block → append the item after the last item.\n * This is the single shared serializer used by the lifecycle transition paths and\n * the dashboard write paths. Mirrors the bespoke block handling of\n * `updateAssignmentWorkspace` (scalar `updateAssignmentFile` cannot append to a list).\n */\n/**\n * Set or clear a flat nested mapping block (`header:` + indented `key: value`\n * lines) in assignment frontmatter. `record = null` writes `header: null`\n * (preserving the key so future sets edit in place). Creates the block before\n * the closing `---` when absent. Used for `planApproval` and `override`.\n *\n * Duplicate headers: only the FIRST block is edited — consistent with\n * parseNestedBlock, which also reads the first. This writer never creates a\n * second block, so duplicates can only come from hand edits; doctor territory.\n */\nexport function updateNestedBlock(\n fileContent: string,\n header: string,\n record: Record<string, string | null> | null,\n): string {\n const fmMatch = fileContent.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) {\n throw new Error('No frontmatter found in assignment file. Expected --- delimiters.');\n }\n const fmBlock = fmMatch[2];\n\n const rendered =\n record === null\n ? `${header}: null`\n : [`${header}:`, ...Object.entries(record).map(([k, v]) => ` ${k}: ${formatYamlValue(v)}`)].join('\\n');\n\n // Replace an existing block (header + indented body) or scalar form, else append.\n const headerRe = new RegExp(`^${header}:.*$`, 'm');\n const headerMatch = fmBlock.match(headerRe);\n let newFm: string;\n if (headerMatch) {\n const start = headerMatch.index ?? 0;\n let end = start + headerMatch[0].length;\n // consume any indented body lines following the header; blanks inside a\n // block are scanned past (mirrors findWorkspaceBlock) but only indented\n // lines extend the consumed range, so trailing blanks aren't swallowed.\n const after = fmBlock.slice(end);\n let scanned = 0;\n for (const line of after.split('\\n').slice(1)) {\n if (line.length === 0) {\n scanned += 1 + line.length;\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n scanned += 1 + line.length;\n end += scanned;\n scanned = 0;\n }\n newFm = fmBlock.slice(0, start) + rendered + fmBlock.slice(end);\n } else {\n newFm = `${fmBlock.replace(/\\n+$/, '')}\\n${rendered}`;\n }\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;\n}\n\nexport function updatePlanApproval(fileContent: string, approval: PlanApproval | null): string {\n return updateNestedBlock(\n fileContent,\n 'planApproval',\n approval === null\n ? null\n : { file: approval.file, digest: approval.digest, by: approval.by, at: approval.at },\n );\n}\n\nexport function updateOverride(fileContent: string, override: StatusOverride | null): string {\n return updateNestedBlock(\n fileContent,\n 'override',\n override === null\n ? null\n : { status: override.status, source: override.source, reason: override.reason, at: override.at },\n );\n}\n\n/**\n * Set one custom fact value in the `facts:` map (read-modify-write the whole\n * map through {@link updateNestedBlock}). `value` must already be the CANONICAL\n * serialization (`'true'`/`'false'` / `String(n)`) — the CLI coerces before\n * calling. Dedicated block writer (like {@link updatePlanApproval}); no\n * `updateAssignmentFile` whitelist entry needed.\n */\nexport function updateFactsMap(fileContent: string, name: string, value: string): string {\n const [frontmatter] = extractFrontmatter(fileContent);\n const current = parseFactsMap(frontmatter);\n current[name] = value;\n return updateNestedBlock(fileContent, 'facts', current);\n}\n\nfunction renderAttestationItem(r: AttestationRecord): string {\n const lines = [\n ` - fact: ${formatYamlValue(r.fact)}`,\n ` actor: ${formatYamlValue(r.actor)}`,\n ` verdict: ${formatYamlValue(r.verdict)}`,\n ` at: ${formatYamlValue(r.at)}`,\n ];\n if (r.note !== undefined && r.note !== null) lines.push(` note: ${formatYamlValue(r.note)}`);\n if (r.file !== undefined && r.file !== null) lines.push(` file: ${formatYamlValue(r.file)}`);\n if (r.digest !== undefined && r.digest !== null) lines.push(` digest: ${formatYamlValue(r.digest)}`);\n if (r.commit !== undefined && r.commit !== null) lines.push(` commit: ${formatYamlValue(r.commit)}`);\n return lines.join('\\n');\n}\n\n/**\n * Locate the `attestations:` block (multi-line list form). Mirrors\n * {@link findStatusHistoryBlock}; returns null when no block header.\n */\nfunction findAttestationsBlock(\n fmBlock: string,\n): { headerStart: number; bodyStart: number; bodyEnd: number } | null {\n const headerMatch = fmBlock.match(/^attestations:\\s*$/m);\n if (!headerMatch) return null;\n const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = fmBlock.slice(bodyStart);\n const lines = after.split('\\n');\n let consumed = 0;\n for (const line of lines) {\n if (line.length === 0) {\n consumed += line.length + 1;\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n consumed += line.length + 1;\n }\n const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);\n return { headerStart, bodyStart, bodyEnd };\n}\n\n/**\n * Upsert one attestation record into the `attestations:` frontmatter list:\n * any existing record with the same (fact, actor) is replaced, then the whole\n * block is re-rendered. Robust to no key / inline `[]` / existing block, like\n * {@link appendStatusHistoryEntry}.\n */\nexport function upsertAttestation(fileContent: string, record: AttestationRecord): string {\n const fmMatch = fileContent.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) {\n throw new Error('No frontmatter found in assignment file. Expected --- delimiters.');\n }\n const fmBlock = fmMatch[2];\n\n const existing = parseAttestations(fmBlock);\n const next = existing.filter((r) => !(r.fact === record.fact && r.actor === record.actor));\n next.push(record);\n const rendered = `attestations:\\n${next.map(renderAttestationItem).join('\\n')}`;\n\n // Inline empty list `[]` OR a scalar `null`/`~` form — both parse as \"no\n // records\" but findAttestationsBlock (which requires an empty tail) skips the\n // scalar form, so handle both here to avoid appending a duplicate key.\n const scalarRegex = /^attestations:[ \\t]*(\\[[ \\t]*\\]|null|~)[ \\t]*$/m;\n const block = findAttestationsBlock(fmBlock);\n\n let newFm: string;\n if (scalarRegex.test(fmBlock)) {\n newFm = fmBlock.replace(scalarRegex, rendered);\n } else if (block) {\n const before = fmBlock.slice(0, block.headerStart);\n const rest = fmBlock.slice(block.bodyEnd);\n const sep = rest.length > 0 && !rest.startsWith('\\n') ? '\\n' : '';\n newFm = `${before}${rendered}${sep}${rest}`;\n } else {\n newFm = `${fmBlock.replace(/\\n+$/, '')}\\n${rendered}`;\n }\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;\n}\n\nexport function appendStatusHistoryEntry(\n fileContent: string,\n entry: StatusHistoryEntry,\n): string {\n const fmMatch = fileContent.match(/^(---\\n)([\\s\\S]*?)(\\n---)/);\n if (!fmMatch) {\n throw new Error('No frontmatter found in assignment file. Expected --- delimiters.');\n }\n const fmBlock = fmMatch[2];\n const item = renderStatusHistoryItem(entry);\n\n const inlineRegex = /^statusHistory:[ \\t]*\\[[ \\t]*\\][ \\t]*$/m;\n const block = findStatusHistoryBlock(fmBlock);\n\n let newFm: string;\n if (inlineRegex.test(fmBlock)) {\n // (ii) inline empty list → block.\n newFm = fmBlock.replace(inlineRegex, `statusHistory:\\n${item}`);\n } else if (block) {\n // (iii) existing block → insert after the last item line.\n const before = fmBlock.slice(0, block.bodyEnd);\n const rest = fmBlock.slice(block.bodyEnd);\n const sep1 = before.endsWith('\\n') ? '' : '\\n';\n const sep2 = rest.length > 0 && !rest.startsWith('\\n') ? '\\n' : '';\n newFm = `${before}${sep1}${item}${sep2}${rest}`;\n } else {\n // (i) no key → append a new block at the end of the frontmatter.\n newFm = `${fmBlock.replace(/\\n+$/, '')}\\nstatusHistory:\\n${item}`;\n }\n\n return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;\n}\n","import { randomUUID } from 'node:crypto';\n\nexport function generateId(): string {\n return randomUUID();\n}\n","import Database from 'better-sqlite3';\nimport { resolve } from 'node:path';\nimport { syntaurRoot } from '../utils/paths.js';\nimport { generateId } from '../utils/uuid.js';\n\nlet db: Database.Database | null = null;\n\nconst EVENTS_SCHEMA_VERSION = '1';\n\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS events (\n event_id TEXT PRIMARY KEY,\n assignment_id TEXT NOT NULL,\n project_slug TEXT,\n at TEXT NOT NULL,\n actor TEXT NOT NULL,\n type TEXT NOT NULL,\n details TEXT,\n source_key TEXT UNIQUE\n);\nCREATE INDEX IF NOT EXISTS idx_events_assignment_at ON events(assignment_id, at);\nCREATE INDEX IF NOT EXISTS idx_events_at ON events(at);\nCREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);\n`;\n\nexport interface EventRow {\n event_id: string;\n assignment_id: string;\n project_slug: string | null;\n at: string;\n actor: string;\n type: string;\n details: string | null;\n source_key: string | null;\n}\n\n/** Raw row shape for the module-private INSERT. */\ninterface InsertEventRow {\n event_id: string;\n assignment_id: string;\n project_slug: string | null;\n at: string;\n actor: string;\n type: string;\n details: string | null;\n source_key: string | null;\n}\n\n/** Caller-facing input for the single exported writer, `recordEvent`. */\nexport interface RecordEventInput {\n assignmentId: string;\n projectSlug?: string | null;\n type: string;\n /** Object (JSON-stringified before storage) or a pre-stringified string. NEVER pass secrets/raw bodies. */\n details?: unknown;\n actor: string;\n /** UTC ISO 8601. Defaults to now; backfill supplies a historical value. */\n at?: string;\n /** Deterministic key for backfilled events (null for live events; null always inserts). */\n sourceKey?: string | null;\n}\n\nexport interface ListEventsFilters {\n /** Inclusive lower bound on `at` (`at >= since`). */\n since?: string;\n /** Restrict to these event types (`type IN (...)`). */\n types?: string[];\n /** Max rows returned. */\n limit?: number;\n}\n\n/**\n * Initialize the events database. Shares the same `~/.syntaur/syntaur.db`\n * file as `session-db.ts` / `proof-db.ts` but owns its own\n * `events_schema_version` meta row so they can coexist. Mirrors the singleton\n * + WAL + exclusive-migration pattern from `src/db/proof-db.ts`.\n */\nexport function initEventsDb(dbPath?: string): Database.Database {\n if (db) return db;\n\n const finalPath = dbPath ?? resolve(syntaurRoot(), 'syntaur.db');\n db = new Database(finalPath);\n db.pragma('journal_mode = WAL');\n db.exec(SCHEMA_SQL);\n\n db.prepare('INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)').run(\n 'events_schema_version',\n EVENTS_SCHEMA_VERSION,\n );\n\n // No migrations yet for v1, but run an exclusive transaction to set the\n // pattern for v2+ (mirrors proof-db.ts + session-db.ts). Each future\n // versioned step re-reads `events_schema_version` inside the transaction and\n // gates on the prior version, then bumps it — e.g.:\n //\n // const vBeforeV2 = (database\n // .prepare(\"SELECT value FROM meta WHERE key = 'events_schema_version'\")\n // .get() as { value: string } | undefined)?.value;\n // if (vBeforeV2 === '1') {\n // database.exec(`... ; UPDATE meta SET value = '2' WHERE key = 'events_schema_version';`);\n // }\n //\n // EXCLUSIVE serializes concurrent initEventsDb() calls (CLI + dashboard) and\n // rolls back a half-applied upgrade on crash.\n const database = db;\n const runMigrations = database.transaction(() => {\n // future migrations go here\n });\n runMigrations.exclusive();\n\n return db;\n}\n\nexport function getEventsDb(): Database.Database {\n if (!db) {\n throw new Error('Events database not initialized. Call initEventsDb() first.');\n }\n return db;\n}\n\nexport function closeEventsDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\nexport function resetEventsDb(): void {\n db = null;\n}\n\n/**\n * Raw prepared INSERT. MODULE-PRIVATE: the only caller is `recordEvent`.\n * Uses `INSERT OR IGNORE` so a duplicate non-null `source_key` is a silent\n * no-op (SQLite exempts NULL from UNIQUE, so null keys always insert).\n */\nfunction insertEvent(row: InsertEventRow): void {\n const database = getEventsDb();\n database\n .prepare(\n `INSERT OR IGNORE INTO events (event_id, assignment_id, project_slug, at, actor, type, details, source_key)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n row.event_id,\n row.assignment_id,\n row.project_slug,\n row.at,\n row.actor,\n row.type,\n row.details,\n row.source_key,\n );\n}\n\n/**\n * The ONLY exported writer. Best-effort: wraps the whole body in try/catch,\n * logs on failure, and NEVER throws — a logging failure must not break the\n * caller's mutation. Lazily initializes the DB if the singleton isn't open.\n *\n * `event_id` is generated; `at` defaults to now; `details` is JSON-stringified\n * (objects become strings; pre-stringified strings pass through). Callers must\n * never put secrets/raw bodies in `details`.\n */\nexport function recordEvent(input: RecordEventInput): void {\n try {\n if (!db) initEventsDb();\n\n let details: string | null = null;\n if (input.details !== undefined && input.details !== null) {\n details =\n typeof input.details === 'string' ? input.details : JSON.stringify(input.details);\n }\n\n insertEvent({\n event_id: generateId(),\n assignment_id: input.assignmentId,\n project_slug: input.projectSlug ?? null,\n at: input.at ?? new Date().toISOString(),\n actor: input.actor,\n type: input.type,\n details,\n source_key: input.sourceKey ?? null,\n });\n } catch (e) {\n console.warn('[events] failed to record event:', e);\n }\n}\n\n/**\n * List events for an assignment, newest-first (`ORDER BY at DESC`). Optional\n * filters: `since` (`at >= since`), `types` (`type IN (...)`), `limit`.\n */\nexport function listEventsByAssignment(\n assignmentId: string,\n filters?: ListEventsFilters,\n): EventRow[] {\n const database = getEventsDb();\n\n const clauses: string[] = ['assignment_id = ?'];\n const params: Array<string | number> = [assignmentId];\n\n if (filters?.since) {\n clauses.push('at >= ?');\n params.push(filters.since);\n }\n\n if (filters?.types && filters.types.length > 0) {\n const placeholders = filters.types.map(() => '?').join(', ');\n clauses.push(`type IN (${placeholders})`);\n params.push(...filters.types);\n }\n\n let sql = `SELECT event_id, assignment_id, project_slug, at, actor, type, details, source_key\n FROM events\n WHERE ${clauses.join(' AND ')}\n ORDER BY at DESC`;\n\n if (filters?.limit !== undefined) {\n sql += ' LIMIT ?';\n params.push(filters.limit);\n }\n\n return database.prepare(sql).all(...params) as EventRow[];\n}\n\n/**\n * Whether any events exist for an assignment. Used ONLY for the backfill\n * dry-run preview count — NOT as an idempotency gate (idempotency is the\n * `source_key` UNIQUE constraint via `INSERT OR IGNORE`).\n */\nexport function hasEventsForAssignment(assignmentId: string): boolean {\n const database = getEventsDb();\n const row = database\n .prepare('SELECT 1 FROM events WHERE assignment_id = ? LIMIT 1')\n .get(assignmentId);\n return row !== undefined;\n}\n","/**\n * Thin emit layer over the events-db (`recordEvent`, the ONLY writer). It adds\n * two things the raw writer deliberately does not own:\n *\n * 1. A module-level `suppressEvents` switch so migrations (which replay\n * statusHistory writes) do NOT fire live events.\n * 2. A `recordStatusEvent` wrapper carrying the `from !== to` self-guard (R5),\n * so same-status writes (the recompute fact/attestation audit entry) emit\n * no `status-change` event.\n *\n * Every emit ultimately goes through `recordEvent` (R3) — nothing here touches\n * the private `insertEvent`. `recordEvent` is best-effort and never throws, so\n * these helpers are side-effect-isolated: a logging failure never breaks the\n * caller's mutation.\n */\n\nimport { recordEvent, type RecordEventInput } from '../db/events-db.js';\n\n/** When true, ALL emits from this module are no-ops (migrations set this). */\nlet suppressEvents = false;\n\nexport function setSuppressEvents(value: boolean): void {\n suppressEvents = value;\n}\n\nexport function isSuppressingEvents(): boolean {\n return suppressEvents;\n}\n\n/**\n * Run `fn` with event emission suppressed, restoring the PRIOR value in a\n * `finally` (so nested suppression and re-entrancy are safe). Works for sync\n * and async `fn` — an async return value is awaited before restoring.\n */\nexport function withSuppressedEvents<T>(fn: () => T): T {\n const prior = suppressEvents;\n suppressEvents = true;\n try {\n const result = fn();\n if (result instanceof Promise) {\n // Restore only after the async work settles.\n return result.finally(() => {\n suppressEvents = prior;\n }) as unknown as T;\n }\n suppressEvents = prior;\n return result;\n } catch (e) {\n suppressEvents = prior;\n throw e;\n }\n}\n\n/**\n * The ONLY actor mapping (R7): sites pass their own already-resolved `by`; a\n * null/undefined `by` (e.g. the recompute system path) maps to `'system'`.\n */\nexport function resolveActor(by: string | null | undefined): string {\n return by ?? 'system';\n}\n\nexport interface RecordStatusEventInput {\n assignmentId: string;\n projectSlug?: string | null;\n /** UTC ISO 8601; defaults to now inside recordEvent when omitted. */\n at?: string;\n /** Already-resolved actor string (pass through resolveActor at the site). */\n actor: string;\n from: string;\n to: string;\n /** The transition command/cause recorded on the statusHistory entry. */\n command: string;\n}\n\n/**\n * Emit a `status-change` event after a verified status write. Self-guards:\n * - suppression on → no-op (migrations);\n * - `from === to` → no event (R5; same-status audit entries are covered by\n * the underlying fact-set/attestation event).\n * Delegates to `recordEvent` (best-effort, never throws).\n */\nexport function recordStatusEvent(input: RecordStatusEventInput): void {\n if (suppressEvents) return;\n if (input.from === input.to) return;\n recordEvent({\n assignmentId: input.assignmentId,\n projectSlug: input.projectSlug ?? null,\n type: 'status-change',\n actor: input.actor,\n at: input.at,\n details: { from: input.from, to: input.to, command: input.command },\n });\n}\n\n/**\n * Suppression-aware non-status emit. A thin wrapper over `recordEvent` that\n * gates on `suppressEvents` so migrations don't emit. Use for every non-status\n * tracked event (assignee-change, priority-change, archived, restored,\n * plan-approval, fact-set, fact-clear, attestation, comment-added,\n * comment-resolved). `recordEvent` is best-effort and never throws.\n */\nexport function emitEvent(input: RecordEventInput): void {\n if (suppressEvents) return;\n recordEvent(input);\n}\n","/**\n * Generic frontmatter/markdown parser for all Syntaur file types.\n * Pattern copied from src/lifecycle/frontmatter.ts:3-23 (extractFrontmatter + parseSimpleValue).\n */\n\nimport type { AttestationRecord, StatusHistoryEntry } from '../lifecycle/types.js';\n\nexport interface ParsedFile {\n frontmatter: Record<string, string>;\n body: string;\n}\n\n/**\n * Split a markdown file into its frontmatter block and body.\n */\nexport function extractFrontmatter(fileContent: string): [string, string] {\n const match = fileContent.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) {\n return ['', fileContent];\n }\n const frontmatterBlock = match[1];\n const body = fileContent.slice(match[0].length).trim();\n return [frontmatterBlock, body];\n}\n\n/**\n * Parse a simple YAML value, handling null and quoted strings.\n */\nfunction parseSimpleValue(raw: string): string | null {\n const trimmed = raw.trim();\n if (trimmed === 'null' || trimmed === '~' || trimmed === '') return null;\n // Double-quoted: decode the escapes formatYamlValue writes (`\\\"` and `\\\\`), so\n // notes/values containing quotes or backslashes round-trip identically to the\n // lifecycle parser. Single-quoted: literal contents (parity with lifecycle).\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n return trimmed.slice(1, -1).replace(/\\\\([\"\\\\])/g, '$1');\n }\n if (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\") && trimmed.length >= 2) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * Extract a top-level scalar field from frontmatter text.\n */\nexport function getField(frontmatter: string, key: string): string | null {\n const match = frontmatter.match(new RegExp(`^${key}:\\\\s*(.*)$`, 'm'));\n if (!match) return null;\n return parseSimpleValue(match[1]);\n}\n\n/**\n * Extract an indented scalar field (one level deep) from frontmatter text.\n */\nexport function getNestedField(frontmatter: string, parent: string, key: string): string | null {\n const parentRegex = new RegExp(`^${parent}:\\\\s*\\\\n((?:\\\\s+.*\\\\n?)*)`, 'm');\n const parentMatch = frontmatter.match(parentRegex);\n if (!parentMatch) return null;\n const block = parentMatch[1];\n const fieldMatch = block.match(new RegExp(`^\\\\s+${key}:\\\\s*(.*)$`, 'm'));\n if (!fieldMatch) return null;\n return parseSimpleValue(fieldMatch[1]);\n}\n\n/**\n * Parse a YAML list field (e.g., tags, dependsOn, relatedAssignments).\n *\n * Supports the empty inline form `field: []` and the block-list form\n * `field:\\n - a\\n - b`. Does NOT support populated inline arrays\n * (`field: [a, b]`). List items are returned as raw trimmed text; callers\n * that expect quoted-string entries should pass each item through\n * {@link unquoteYamlString}.\n */\nfunction parseListField(frontmatter: string, fieldName: string): string[] {\n const inlineMatch = frontmatter.match(new RegExp(`^${fieldName}:\\\\s*\\\\[\\\\s*\\\\]`, 'm'));\n if (inlineMatch) return [];\n\n const results: string[] = [];\n const blockMatch = frontmatter.match(\n new RegExp(`^${fieldName}:\\\\s*\\\\n((?:\\\\s+-\\\\s+.*\\\\n?)*)`, 'm'),\n );\n if (blockMatch) {\n let item: RegExpExecArray | null;\n const regex = /^\\s+-\\s+(.+)$/gm;\n while ((item = regex.exec(blockMatch[1])) !== null) {\n results.push(item[1].trim());\n }\n }\n return results;\n}\n\n/**\n * Strip a paired surrounding `\"...\"` or `'...'` from a YAML scalar.\n * Mirrors `parseSimpleValue`'s quote handling for list-item entries (which\n * `parseListField` leaves raw).\n */\nfunction unquoteYamlString(value: string): string {\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n return value.slice(1, -1);\n }\n return value;\n}\n\n// --- Project Parser ---\n\nexport interface ParsedProject {\n id: string;\n slug: string;\n title: string;\n archived: boolean;\n archivedAt: string | null;\n archivedReason: string | null;\n statusOverride: string | null;\n created: string;\n updated: string;\n tags: string[];\n workspace: string | null;\n /**\n * Repositories the project spans. Empty array when the field is absent —\n * existing project.md files predate this field, so callers must treat\n * missing as `[]`. Paths with YAML-special characters (spaces, colons,\n * leading dashes) must be quoted in source; quotes are stripped here.\n */\n repositories: string[];\n externalIds: Array<{ system: string; id: string; url: string | null }>;\n body: string;\n}\n\nexport function parseProject(fileContent: string): ParsedProject {\n const [fm, body] = extractFrontmatter(fileContent);\n // Legacy alias: pre-v0.2.0 installs used `mission` as the slug key. The\n // fs-migration helper renames the file but doesn't rewrite user-owned\n // frontmatter. Accept either key.\n const slug = getField(fm, 'slug') ?? getField(fm, 'mission') ?? '';\n return {\n id: getField(fm, 'id') ?? '',\n slug,\n title: getField(fm, 'title') ?? '',\n archived: getField(fm, 'archived') === 'true',\n archivedAt: getField(fm, 'archivedAt'),\n archivedReason: getField(fm, 'archivedReason'),\n statusOverride: getField(fm, 'statusOverride'),\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n tags: parseListField(fm, 'tags'),\n workspace: getField(fm, 'workspace'),\n repositories: parseListField(fm, 'repositories').map(unquoteYamlString),\n externalIds: parseExternalIds(fm),\n body,\n };\n}\n\n// --- Status Parser (for _status.md) ---\n\nexport interface ParsedStatus {\n project: string;\n status: string;\n progress: Record<string, number> & { total: number };\n needsAttention: {\n blockedCount: number;\n failedCount: number;\n openQuestions: number;\n };\n body: string;\n}\n\nexport function parseStatus(fileContent: string): ParsedStatus {\n const [fm, body] = extractFrontmatter(fileContent);\n\n // Dynamically parse progress fields\n const progress: Record<string, number> & { total: number } = { total: 0 };\n const progressMatch = fm.match(/^progress:\\s*\\n((?:\\s+.*\\n?)*)/m);\n if (progressMatch) {\n const lines = progressMatch[1].split('\\n');\n for (const line of lines) {\n const kv = line.match(/^\\s+(\\w+):\\s*(\\d+)/);\n if (kv) {\n progress[kv[1]] = parseInt(kv[2], 10);\n }\n }\n }\n\n return {\n project: getField(fm, 'project') ?? '',\n status: getField(fm, 'status') ?? 'pending',\n progress,\n needsAttention: {\n blockedCount: parseInt(getNestedField(fm, 'needsAttention', 'blockedCount') ?? '0', 10),\n failedCount: parseInt(getNestedField(fm, 'needsAttention', 'failedCount') ?? '0', 10),\n openQuestions: parseInt(getNestedField(fm, 'needsAttention', 'openQuestions') ?? '0', 10),\n },\n body,\n };\n}\n\n// --- Assignment Summary Parser ---\n\nexport interface ParsedAssignmentSummary {\n id: string;\n slug: string;\n title: string;\n status: string;\n priority: string;\n assignee: string | null;\n dependsOn: string[];\n links: string[];\n updated: string;\n}\n\nexport function parseAssignmentSummary(fileContent: string): ParsedAssignmentSummary {\n const [fm] = extractFrontmatter(fileContent);\n return {\n id: getField(fm, 'id') ?? '',\n slug: getField(fm, 'slug') ?? '',\n title: getField(fm, 'title') ?? '',\n status: getField(fm, 'status') ?? 'pending',\n priority: getField(fm, 'priority') ?? 'medium',\n assignee: getField(fm, 'assignee'),\n dependsOn: parseListField(fm, 'dependsOn'),\n links: parseListField(fm, 'links'),\n updated: getField(fm, 'updated') ?? '',\n };\n}\n\n// --- Full Assignment Parser ---\n\nexport interface ParsedAssignmentFull {\n id: string;\n slug: string;\n title: string;\n project: string | null;\n workspaceGroup: string | null;\n type: string | null;\n status: string;\n priority: string;\n assignee: string | null;\n dependsOn: string[];\n links: string[];\n blockedReason: string | null;\n workspace: {\n repository: string | null;\n worktreePath: string | null;\n branch: string | null;\n parentBranch: string | null;\n };\n externalIds: Array<{ system: string; id: string; url: string | null }>;\n statusHistory: StatusHistoryEntry[];\n tags: string[];\n archived: boolean;\n archivedAt: string | null;\n archivedReason: string | null;\n created: string;\n updated: string;\n body: string;\n // ── derived-status v3 fields ─────────────────────────────────────────────\n phase: string | null;\n disposition: string | null;\n parked: boolean;\n reviewRequested: boolean;\n implementationStarted: boolean;\n planApproval: { file: string; digest: string; by: string | null; at: string } | null;\n override: { status: string; source: string; reason: string | null; at: string } | null;\n // ── custom facts + attestations ──────────────────────────────────────────\n /** Custom asserted fact values (raw scalars). Absent block → {}. Parity with\n * the lifecycle parser so buildDerivedDetail's cast feeds computeFacts these. */\n facts: Record<string, string>;\n /** Attestation records (one per fact+actor). Absent block → []. */\n attestations: AttestationRecord[];\n}\n\nfunction parseExternalIds(frontmatter: string): Array<{ system: string; id: string; url: string | null }> {\n const inlineMatch = frontmatter.match(/^externalIds:\\s*\\[\\s*\\]/m);\n if (inlineMatch) return [];\n\n const results: Array<{ system: string; id: string; url: string | null }> = [];\n const blockMatch = frontmatter.match(\n /^externalIds:\\s*\\n((?:\\s+-\\s+[\\s\\S]*?)(?=^\\w|\\n---))/m,\n );\n if (!blockMatch) return [];\n\n const itemBlocks = blockMatch[1].split(/\\n\\s+-\\s+/).filter(Boolean);\n for (const block of itemBlocks) {\n const lines = block.split('\\n');\n const entry: Record<string, string | null> = {};\n for (const line of lines) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n if (entry['system'] && entry['id']) {\n results.push({\n system: entry['system'],\n id: entry['id'],\n url: entry['url'] || null,\n });\n }\n }\n return results;\n}\n\n/**\n * Parse the `statusHistory` list-of-mappings. Parity copy of\n * `src/lifecycle/frontmatter.ts::parseStatusHistory` — uses the same robust\n * line-scan (NOT the `parseExternalIds` regex boundary), because this module's\n * `extractFrontmatter` also strips the closing `\\n---`, so a last-key\n * `statusHistory` block would otherwise be dropped. Keep in sync with the\n * lifecycle parser (dashboard-parser parity test guards this).\n */\nfunction parseStatusHistory(frontmatter: string): StatusHistoryEntry[] {\n if (/^statusHistory:\\s*\\[\\s*\\]/m.test(frontmatter)) return [];\n\n const headerMatch = frontmatter.match(/^statusHistory:\\s*$/m);\n if (!headerMatch) return [];\n\n // Regex match offset, not indexOf — guards against an earlier scalar value\n // containing the substring \"statusHistory:\".\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1; // skip the trailing \\n\n const after = frontmatter.slice(bodyStart);\n\n const bodyLines: string[] = [];\n for (const line of after.split('\\n')) {\n if (line.length === 0) {\n bodyLines.push(line);\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n bodyLines.push(line);\n }\n const body = bodyLines.join('\\n');\n\n const results: StatusHistoryEntry[] = [];\n const itemBlocks = body.split(/\\n\\s+-\\s+/).filter((b) => b.trim().length > 0);\n for (const block of itemBlocks) {\n const entry: Record<string, string | null> = {};\n for (const line of block.split('\\n')) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n if (!entry['to']) continue;\n const result: StatusHistoryEntry = {\n at: entry['at'] ?? '',\n from: entry['from'] ?? null,\n to: entry['to'],\n command: entry['command'] ?? '',\n by: entry['by'] ?? null,\n };\n if (entry['reason'] != null) result.reason = entry['reason'];\n // Dimension-aware optional keys (derived-status v3); keep in sync with the\n // lifecycle parser.\n if ('phaseFrom' in entry) result.phaseFrom = entry['phaseFrom'];\n if ('phaseTo' in entry) result.phaseTo = entry['phaseTo'];\n if ('dispositionFrom' in entry) result.dispositionFrom = entry['dispositionFrom'];\n if ('dispositionTo' in entry) result.dispositionTo = entry['dispositionTo'];\n results.push(result);\n }\n return results;\n}\n\n/**\n * Parse the `facts:` map (parity with lifecycle `frontmatter.ts::parseFactsMap`).\n * Absent/null block → `{}`; null-valued entries dropped; values trimmed +\n * unquoted via parseSimpleValue. Keep in sync with the lifecycle parser.\n */\nfunction parseFactsMap(frontmatter: string): Record<string, string> {\n const headerMatch = frontmatter.match(/^facts:\\s*$/m);\n if (!headerMatch) return {};\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const after = frontmatter.slice(headerStart + headerMatch[0].length + 1);\n const out: Record<string, string> = {};\n for (const line of after.split('\\n')) {\n if (line.length === 0) continue;\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim();\n if (!key) continue;\n const value = parseSimpleValue(line.slice(colonIdx + 1));\n if (value === null) continue;\n out[key] = value;\n }\n return out;\n}\n\n/**\n * Parse the `attestations:` record list (parity with lifecycle\n * `frontmatter.ts::parseAttestations` — same robust line-scan). Records missing\n * a required key or with an unknown verdict are dropped. Keep in sync.\n */\nfunction parseAttestations(frontmatter: string): AttestationRecord[] {\n if (/^attestations:\\s*\\[\\s*\\]/m.test(frontmatter)) return [];\n\n const headerMatch = frontmatter.match(/^attestations:\\s*$/m);\n if (!headerMatch) return [];\n\n const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);\n const bodyStart = headerStart + headerMatch[0].length + 1;\n const after = frontmatter.slice(bodyStart);\n\n const bodyLines: string[] = [];\n for (const line of after.split('\\n')) {\n if (line.length === 0) {\n bodyLines.push(line);\n continue;\n }\n if (line[0] !== ' ' && line[0] !== '\\t') break;\n bodyLines.push(line);\n }\n const body = bodyLines.join('\\n');\n\n const results: AttestationRecord[] = [];\n const itemBlocks = body.split(/\\n\\s+-\\s+/).filter((b) => b.trim().length > 0);\n for (const block of itemBlocks) {\n const entry: Record<string, string | null> = {};\n for (const line of block.split('\\n')) {\n const colonIdx = line.indexOf(':');\n if (colonIdx < 0) continue;\n const key = line.slice(0, colonIdx).trim().replace(/^-\\s+/, '');\n if (!key) continue;\n entry[key] = parseSimpleValue(line.slice(colonIdx + 1));\n }\n const verdict = entry['verdict'];\n if (!entry['fact'] || !entry['actor'] || !verdict || !entry['at']) continue;\n if (verdict !== 'approved' && verdict !== 'changes-requested') continue;\n const record: AttestationRecord = {\n fact: entry['fact'],\n actor: entry['actor'],\n verdict,\n at: entry['at'],\n };\n if (entry['note'] != null) record.note = entry['note'];\n if (entry['file'] != null) record.file = entry['file'];\n if (entry['digest'] != null) record.digest = entry['digest'];\n if (entry['commit'] != null) record.commit = entry['commit'];\n results.push(record);\n }\n return results;\n}\n\nexport function parseAssignmentFull(fileContent: string): ParsedAssignmentFull {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n id: getField(fm, 'id') ?? '',\n slug: getField(fm, 'slug') ?? '',\n title: getField(fm, 'title') ?? '',\n project: getField(fm, 'project'),\n workspaceGroup: getField(fm, 'workspaceGroup'),\n type: getField(fm, 'type'),\n status: getField(fm, 'status') ?? 'pending',\n priority: getField(fm, 'priority') ?? 'medium',\n assignee: getField(fm, 'assignee'),\n dependsOn: parseListField(fm, 'dependsOn'),\n links: parseListField(fm, 'links'),\n blockedReason: getField(fm, 'blockedReason'),\n workspace: {\n repository: getNestedField(fm, 'workspace', 'repository'),\n worktreePath: getNestedField(fm, 'workspace', 'worktreePath'),\n branch: getNestedField(fm, 'workspace', 'branch'),\n parentBranch: getNestedField(fm, 'workspace', 'parentBranch'),\n },\n externalIds: parseExternalIds(fm),\n statusHistory: parseStatusHistory(fm),\n tags: parseListField(fm, 'tags'),\n archived: getField(fm, 'archived') === 'true',\n archivedAt: getField(fm, 'archivedAt'),\n archivedReason: getField(fm, 'archivedReason'),\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n phase: getField(fm, 'phase'),\n disposition: getField(fm, 'disposition'),\n parked: getField(fm, 'parked') === 'true',\n reviewRequested: getField(fm, 'reviewRequested') === 'true',\n implementationStarted: getField(fm, 'implementationStarted') === 'true',\n planApproval: (() => {\n const file = getNestedField(fm, 'planApproval', 'file');\n const digest = getNestedField(fm, 'planApproval', 'digest');\n if (!file || !digest) return null;\n return {\n file,\n digest,\n by: getNestedField(fm, 'planApproval', 'by'),\n at: getNestedField(fm, 'planApproval', 'at') ?? '',\n };\n })(),\n override: (() => {\n const status = getNestedField(fm, 'override', 'status');\n if (!status) return null;\n return {\n status,\n source: getNestedField(fm, 'override', 'source') ?? 'human',\n reason: getNestedField(fm, 'override', 'reason'),\n at: getNestedField(fm, 'override', 'at') ?? '',\n };\n })(),\n facts: parseFactsMap(fm),\n attestations: parseAttestations(fm),\n };\n}\n\n// --- Plan Parser ---\n\nexport interface ParsedPlan {\n assignment: string;\n status: string;\n created: string;\n updated: string;\n body: string;\n}\n\nexport function parsePlan(fileContent: string): ParsedPlan {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n assignment: getField(fm, 'assignment') ?? '',\n status: getField(fm, 'status') ?? '',\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Scratchpad Parser ---\n\nexport interface ParsedScratchpad {\n assignment: string;\n updated: string;\n body: string;\n}\n\nexport function parseScratchpad(fileContent: string): ParsedScratchpad {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n assignment: getField(fm, 'assignment') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Handoff Parser ---\n\nexport interface ParsedHandoff {\n assignment: string;\n handoffCount: number;\n updated: string;\n body: string;\n}\n\nexport function parseHandoff(fileContent: string): ParsedHandoff {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n assignment: getField(fm, 'assignment') ?? '',\n handoffCount: parseInt(getField(fm, 'handoffCount') ?? '0', 10),\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Decision Record Parser ---\n\nexport interface ParsedDecisionRecord {\n assignment: string;\n decisionCount: number;\n updated: string;\n body: string;\n}\n\nexport function parseDecisionRecord(fileContent: string): ParsedDecisionRecord {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n assignment: getField(fm, 'assignment') ?? '',\n decisionCount: parseInt(getField(fm, 'decisionCount') ?? '0', 10),\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Comments Parser ---\n\nexport interface ParsedComment {\n id: string;\n timestamp: string;\n author: string;\n type: 'question' | 'note' | 'feedback';\n body: string;\n replyTo?: string;\n resolved?: boolean;\n}\n\nexport interface ParsedComments {\n assignment: string;\n entryCount: number;\n updated: string;\n entries: ParsedComment[];\n body: string;\n}\n\nexport function parseComments(fileContent: string): ParsedComments {\n const [fm, body] = extractFrontmatter(fileContent);\n const entries: ParsedComment[] = [];\n // Split only at REAL comment headers — a `## <id>` line followed by the full\n // metadata prelude (Recorded → Author → Type<valid>) — so a markdown\n // `## Heading` inside a comment body (even one followed by a lone\n // `**Recorded:**` line) doesn't start a phantom section and truncate the body.\n // `\\n\\s*` mirrors the header regex's `^\\s*` tolerance (no-blank/multi-blank).\n const sections = body\n .split(\n /^## (?=[^\\n]*\\n\\s*\\*\\*Recorded:\\*\\*[^\\n]*\\n\\*\\*Author:\\*\\*[^\\n]*\\n\\*\\*Type:\\*\\*\\s*(?:question|note|feedback)\\b)/m,\n )\n .slice(1);\n for (const section of sections) {\n const newlineIdx = section.indexOf('\\n');\n if (newlineIdx === -1) continue;\n const id = section.slice(0, newlineIdx).trim();\n const rest = section.slice(newlineIdx + 1);\n const headerMatch = rest.match(\n /^\\s*\\*\\*Recorded:\\*\\*\\s*(.*)\\n\\*\\*Author:\\*\\*\\s*(.*)\\n\\*\\*Type:\\*\\*\\s*(question|note|feedback)(?:\\n\\*\\*Reply to:\\*\\*\\s*(.*))?(?:\\n\\*\\*Resolved:\\*\\*\\s*(true|false))?\\n+([\\s\\S]*)$/,\n );\n if (!headerMatch) continue;\n const [, timestamp, author, type, replyTo, resolvedStr, entryBody] = headerMatch;\n const entry: ParsedComment = {\n id,\n timestamp: timestamp.trim(),\n author: author.trim(),\n type: type as 'question' | 'note' | 'feedback',\n body: entryBody.trim(),\n };\n if (replyTo) entry.replyTo = replyTo.trim();\n if (resolvedStr) entry.resolved = resolvedStr === 'true';\n entries.push(entry);\n }\n return {\n assignment: getField(fm, 'assignment') ?? '',\n entryCount: parseInt(getField(fm, 'entryCount') ?? '0', 10),\n updated: getField(fm, 'updated') ?? '',\n entries,\n body,\n };\n}\n\n// --- Progress Parser ---\n\nexport interface ProgressEntry {\n timestamp: string;\n body: string;\n}\n\nexport interface ParsedProgress {\n assignment: string;\n entryCount: number;\n updated: string;\n entries: ProgressEntry[];\n body: string;\n}\n\nexport function parseProgress(fileContent: string): ParsedProgress {\n const [fm, body] = extractFrontmatter(fileContent);\n const entries: ProgressEntry[] = [];\n const sections = body.split(/^## /m).slice(1);\n for (const section of sections) {\n const newlineIdx = section.indexOf('\\n');\n if (newlineIdx === -1) continue;\n const timestamp = section.slice(0, newlineIdx).trim();\n const entryBody = section.slice(newlineIdx + 1).trim();\n entries.push({ timestamp, body: entryBody });\n }\n return {\n assignment: getField(fm, 'assignment') ?? '',\n entryCount: parseInt(getField(fm, 'entryCount') ?? '0', 10),\n updated: getField(fm, 'updated') ?? '',\n entries,\n body,\n };\n}\n\n// --- Resource Parser ---\n\nexport interface ParsedResource {\n name: string;\n source: string;\n category: string;\n relatedAssignments: string[];\n created: string;\n updated: string;\n body: string;\n}\n\nexport function parseResource(fileContent: string): ParsedResource {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n name: getField(fm, 'name') ?? '',\n source: getField(fm, 'source') ?? '',\n category: getField(fm, 'category') ?? '',\n relatedAssignments: parseListField(fm, 'relatedAssignments'),\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Memory Parser ---\n\nexport interface ParsedMemory {\n name: string;\n source: string;\n scope: string;\n sourceAssignment: string | null;\n relatedAssignments: string[];\n tags: string[];\n created: string;\n updated: string;\n body: string;\n}\n\nexport function parseMemory(fileContent: string): ParsedMemory {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n name: getField(fm, 'name') ?? '',\n source: getField(fm, 'source') ?? '',\n scope: getField(fm, 'scope') ?? '',\n sourceAssignment: getField(fm, 'sourceAssignment'),\n relatedAssignments: parseListField(fm, 'relatedAssignments'),\n tags: parseListField(fm, 'tags'),\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n body,\n };\n}\n\n// --- Playbook Parser ---\n\nexport interface ParsedPlaybook {\n slug: string;\n name: string;\n description: string;\n whenToUse: string;\n created: string;\n updated: string;\n tags: string[];\n body: string;\n}\n\nexport function parsePlaybook(fileContent: string): ParsedPlaybook {\n const [fm, body] = extractFrontmatter(fileContent);\n return {\n slug: getField(fm, 'slug') ?? '',\n name: getField(fm, 'name') ?? '',\n description: getField(fm, 'description') ?? '',\n whenToUse: getField(fm, 'when_to_use') ?? '',\n created: getField(fm, 'created') ?? '',\n updated: getField(fm, 'updated') ?? '',\n tags: parseListField(fm, 'tags'),\n body,\n };\n}\n\n// --- Mermaid Graph Extractor ---\n\n/**\n * Extract the mermaid code block from _status.md body content.\n * Returns null if no mermaid block is found.\n */\nexport function extractMermaidGraph(body: string): string | null {\n const match = body.match(/```mermaid\\n([\\s\\S]*?)```/);\n return match ? match[1].trim() : null;\n}\n","import { randomBytes } from 'node:crypto';\nimport { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { extractFrontmatter, getField } from '../dashboard/parser.js';\nimport { ensureDir, fileExists, writeFileForce } from '../utils/fs.js';\nimport type {\n TodoItem,\n TodoChecklist,\n TodoStatus,\n ArchiveInterval,\n LogEntry,\n TodoLog,\n} from './types.js';\n\n// --- Short ID ---\n\nexport function generateShortId(): string {\n return randomBytes(2).toString('hex');\n}\n\nexport function generateUniqueId(existingIds: Set<string>): string {\n let id = generateShortId();\n let attempts = 0;\n while (existingIds.has(id) && attempts < 100) {\n id = generateShortId();\n attempts++;\n }\n return id;\n}\n\n// --- Checklist parsing ---\n\nconst ITEM_REGEX = /^- \\[([ x!]|>[^\\]]*)\\]\\s+(.+)$/;\nconst ID_REGEX = /\\[t:([a-f0-9]{4})\\]/;\nconst TAG_REGEX = /#([a-zA-Z0-9_-]+)/g;\n// Meta token follows `[t:<id>]` and looks like `<key=value;key=value;...>`.\n// Anchored at end of line. Recognized keys: b (branch), w (worktreePath),\n// c (createdAt), u (updatedAt), p (planDir), l (linkedAssignmentId),\n// lr (linkedAssignmentRef). Unknown keys are dropped.\nconst META_TOKEN_REGEX = /\\[t:[a-f0-9]{4}\\]\\s+<([^>]*)>\\s*$/;\nconst META_ENCODE_CHARS = ['%', '<', '>', '[', ']', '=', ';', '\\n', '\\r'];\n\n// A tag is stored inline as `#<tag>` on a single checklist line and the parser\n// only recognizes the class below (see TAG_REGEX). A tag containing whitespace,\n// a newline, or `#` would corrupt the line — splitting it, inventing tags, or\n// dropping the `[t:id]` + metadata on the next parse. Validate at every write\n// entry so such a tag is never serialized (reject, never silently sanitize).\nconst VALID_TAG_REGEX = /^[a-zA-Z0-9_-]+$/;\n\nexport function isValidTag(tag: unknown): tag is string {\n return typeof tag === 'string' && VALID_TAG_REGEX.test(tag);\n}\n\nexport function assertValidTags(tags: readonly unknown[]): void {\n for (const t of tags) {\n if (!isValidTag(t)) {\n throw new Error(\n `Invalid tag ${JSON.stringify(t)}: tags may contain only letters, digits, '-' and '_' (no spaces, newlines, or '#').`,\n );\n }\n }\n}\n\nexport function encodeMetaValue(value: string): string {\n let out = '';\n for (const ch of value) {\n if (META_ENCODE_CHARS.includes(ch)) {\n out += '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0');\n } else {\n out += ch;\n }\n }\n return out;\n}\n\nexport function decodeMetaValue(value: string): string {\n return value.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) =>\n String.fromCharCode(parseInt(hex, 16)),\n );\n}\n\ninterface MetaFields {\n branch: string | null;\n worktreePath: string | null;\n createdAt: string | null;\n updatedAt: string | null;\n planDir: string | null;\n linkedAssignmentId: string | null;\n linkedAssignmentRef: string | null;\n bundleId: string | null;\n}\n\nfunction emptyMetaFields(): MetaFields {\n return {\n branch: null,\n worktreePath: null,\n createdAt: null,\n updatedAt: null,\n planDir: null,\n linkedAssignmentId: null,\n linkedAssignmentRef: null,\n bundleId: null,\n };\n}\n\nexport function parseMetaToken(line: string): MetaFields {\n const match = line.match(META_TOKEN_REGEX);\n if (!match) return emptyMetaFields();\n const body = match[1];\n if (!body) return emptyMetaFields();\n const fields = emptyMetaFields();\n for (const pair of body.split(';')) {\n const trimmed = pair.trim();\n if (!trimmed) continue;\n const eq = trimmed.indexOf('=');\n if (eq < 0) continue;\n const key = trimmed.slice(0, eq).trim();\n const rawValue = trimmed.slice(eq + 1);\n const value = decodeMetaValue(rawValue);\n switch (key) {\n case 'b': fields.branch = value; break;\n case 'w': fields.worktreePath = value; break;\n case 'c': fields.createdAt = value; break;\n case 'u': fields.updatedAt = value; break;\n case 'p': fields.planDir = value; break;\n case 'l': fields.linkedAssignmentId = value; break;\n case 'lr': fields.linkedAssignmentRef = value; break;\n case 'bn': fields.bundleId = value; break;\n }\n }\n return fields;\n}\n\nexport function serializeMetaToken(item: TodoItem): string {\n const pairs: string[] = [];\n if (item.branch !== null) pairs.push(`b=${encodeMetaValue(item.branch)}`);\n if (item.worktreePath !== null) pairs.push(`w=${encodeMetaValue(item.worktreePath)}`);\n if (item.createdAt !== null) pairs.push(`c=${encodeMetaValue(item.createdAt)}`);\n if (item.updatedAt !== null) pairs.push(`u=${encodeMetaValue(item.updatedAt)}`);\n if (item.planDir !== null) pairs.push(`p=${encodeMetaValue(item.planDir)}`);\n if (item.linkedAssignmentId !== null) pairs.push(`l=${encodeMetaValue(item.linkedAssignmentId)}`);\n if (item.linkedAssignmentRef !== null) pairs.push(`lr=${encodeMetaValue(item.linkedAssignmentRef)}`);\n if (item.bundleId !== null) pairs.push(`bn=${encodeMetaValue(item.bundleId)}`);\n if (pairs.length === 0) return '';\n return `<${pairs.join(';')}>`;\n}\n\nfunction parseStatus(marker: string): { status: TodoStatus; session: string | null } {\n if (marker === ' ') return { status: 'open', session: null };\n if (marker === 'x') return { status: 'completed', session: null };\n if (marker === '!') return { status: 'blocked', session: null };\n if (marker.startsWith('>:')) return { status: 'in_progress', session: marker.slice(2) };\n if (marker === '>') return { status: 'in_progress', session: null };\n return { status: 'open', session: null };\n}\n\nfunction sanitizeSession(session: string): string {\n // Strip characters that would break the markdown checkbox syntax\n return session.replace(/[\\[\\]]/g, '');\n}\n\nfunction statusToMarker(item: TodoItem): string {\n switch (item.status) {\n case 'open':\n return ' ';\n case 'completed':\n return 'x';\n case 'blocked':\n return '!';\n case 'in_progress':\n return item.session ? `>:${sanitizeSession(item.session)}` : '>';\n }\n}\n\n/**\n * Escape backslash-special characters in a todo description so that prose\n * containing `#`, `[`, or `\\` is never mistaken for a structural tag/id token.\n * Order matters: backslash must be escaped first.\n */\nfunction escapeDescription(description: string): string {\n // Backslash first (so the escapes introduced below aren't double-escaped),\n // then structural chars, then newlines/CR. A todo serializes to a single\n // physical line; without encoding newlines the parser would split a\n // multi-line description across lines and silently drop the id + tail (the\n // second physical line fails ITEM_REGEX).\n return description\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/#/g, '\\\\#')\n .replace(/\\[/g, '\\\\[')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r');\n}\n\n/**\n * Reverse of escapeDescription: `\\#`→`#`, `\\[`→`[`, `\\\\`→`\\`.\n * A single pass handles all sequences correctly because escapes are\n * non-overlapping (a `\\` always pairs with the following char).\n */\nfunction unescapeDescription(escaped: string): string {\n let out = '';\n for (let i = 0; i < escaped.length; i++) {\n if (escaped[i] === '\\\\' && i + 1 < escaped.length) {\n const next = escaped[i + 1];\n if (next === '\\\\' || next === '#' || next === '[') {\n out += next;\n i++;\n continue;\n }\n if (next === 'n') {\n out += '\\n';\n i++;\n continue;\n }\n if (next === 'r') {\n out += '\\r';\n i++;\n continue;\n }\n }\n out += escaped[i];\n }\n return out;\n}\n\n/**\n * Find the index in `rest` of the first UN-escaped structural token: either a\n * `#tag` start (`#` followed by a tag char) or a `[t:` / `[` bracket. A token is\n * \"escaped\" when preceded by an odd number of backslashes. Returns the length of\n * `rest` if no structural token exists (whole line is description).\n */\nfunction findStructuralCut(rest: string): number {\n for (let i = 0; i < rest.length; i++) {\n const ch = rest[i];\n if (ch !== '#' && ch !== '[') continue;\n // Count preceding backslashes to determine escaped-ness.\n let backslashes = 0;\n let j = i - 1;\n while (j >= 0 && rest[j] === '\\\\') {\n backslashes++;\n j--;\n }\n if (backslashes % 2 === 1) continue; // escaped — part of the description\n if (ch === '#') {\n // Only a structural tag if followed by a tag char.\n if (/[a-zA-Z0-9_-]/.test(rest[i + 1] ?? '')) return i;\n } else {\n // ch === '[' — any unescaped bracket starts the structural tail.\n return i;\n }\n }\n return rest.length;\n}\n\nexport function parseChecklistItem(line: string): TodoItem | null {\n const match = line.match(ITEM_REGEX);\n if (!match) return null;\n\n const marker = match[1];\n const rest = match[2];\n\n const { status, session } = parseStatus(marker);\n\n // Split the line at the first UN-escaped structural token. Everything before\n // is the (escaped) description; everything after is the structural tail from\n // which tags / id / meta are extracted. This keeps escaped prose like `\\#42`\n // out of the tag collection.\n const cut = findStructuralCut(rest);\n const description = unescapeDescription(rest.slice(0, cut).trim());\n const tail = rest.slice(cut);\n\n const idMatch = tail.match(ID_REGEX);\n const id = idMatch ? idMatch[1] : '';\n\n const tags: string[] = [];\n let tagMatch;\n const tagRegex = new RegExp(TAG_REGEX.source, 'g');\n while ((tagMatch = tagRegex.exec(tail)) !== null) {\n tags.push(tagMatch[1]);\n }\n\n const meta = parseMetaToken(line);\n\n return {\n id,\n description,\n status,\n tags,\n session,\n branch: meta.branch,\n worktreePath: meta.worktreePath,\n createdAt: meta.createdAt,\n updatedAt: meta.updatedAt,\n planDir: meta.planDir,\n linkedAssignmentId: meta.linkedAssignmentId,\n linkedAssignmentRef: meta.linkedAssignmentRef,\n bundleId: meta.bundleId,\n };\n}\n\nexport function serializeChecklistItem(item: TodoItem): string {\n const marker = statusToMarker(item);\n // Last line of defense: never emit a tag that would corrupt the line. Entry\n // points validate first (returning a clean 400 / CLI error); this guarantees\n // file integrity regardless of caller.\n assertValidTags(item.tags);\n const tagStr = item.tags.map((t) => `#${t}`).join(' ');\n // Escape backslash-special chars in the description so prose `#`/`[`/`\\` is\n // never re-parsed as a structural tag/id token. Real tags and `[t:id]` below\n // are emitted with literal, unescaped markers.\n const parts = [`- [${marker}] ${escapeDescription(item.description)}`];\n if (tagStr) parts.push(tagStr);\n parts.push(`[t:${item.id}]`);\n const meta = serializeMetaToken(item);\n if (meta) parts.push(meta);\n return parts.join(' ');\n}\n\nexport function parseChecklist(content: string): TodoChecklist {\n const [fm, body] = extractFrontmatter(content);\n const workspace = getField(fm, 'workspace') || '_global';\n const archiveIntervalRaw = getField(fm, 'archiveInterval') || 'weekly';\n const archiveInterval = (['daily', 'weekly', 'monthly', 'never'].includes(archiveIntervalRaw)\n ? archiveIntervalRaw\n : 'weekly') as ArchiveInterval;\n\n const items: TodoItem[] = [];\n for (const line of body.split('\\n')) {\n const item = parseChecklistItem(line);\n if (item) items.push(item);\n }\n\n return { workspace, archiveInterval, items };\n}\n\nexport function serializeChecklist(checklist: TodoChecklist): string {\n const fm = [\n '---',\n `workspace: ${checklist.workspace}`,\n `archiveInterval: ${checklist.archiveInterval}`,\n '---',\n ].join('\\n');\n\n const header = '# Quick Todos';\n const items = checklist.items.map(serializeChecklistItem).join('\\n');\n\n return `${fm}\\n\\n${header}\\n\\n${items}\\n`;\n}\n\n// --- Log parsing ---\n\nexport function parseLog(content: string): TodoLog {\n const [fm, body] = extractFrontmatter(content);\n const workspace = getField(fm, 'workspace') || '_global';\n\n const entries: LogEntry[] = [];\n const sections = body.split(/^### /m).filter((s) => s.match(/^\\d{4}-/));\n\n for (const section of sections) {\n const lines = section.split('\\n');\n const heading = lines[0]?.trim() || '';\n\n // Heading format: 2026-04-07T14:30:00Z — t:a3f1, t:b7c2\n const headingMatch = heading.match(/^(\\S+)\\s*—?\\s*(.*)/);\n if (!headingMatch) continue;\n\n const timestamp = headingMatch[1];\n const idsPart = headingMatch[2] || '';\n const itemIds = [...idsPart.matchAll(/t:([a-f0-9]{4})/g)].map((m) => m[1]);\n\n const entry: LogEntry = {\n timestamp,\n itemIds,\n items: '',\n session: null,\n branch: null,\n summary: '',\n blockers: null,\n status: null,\n };\n\n for (const line of lines.slice(1)) {\n const fieldMatch = line.match(/^\\*\\*(\\w+):\\*\\*\\s*(.*)/);\n if (!fieldMatch) continue;\n const key = fieldMatch[1].toLowerCase();\n const value = fieldMatch[2].trim();\n switch (key) {\n case 'items':\n entry.items = value;\n break;\n case 'session':\n entry.session = value;\n break;\n case 'branch':\n entry.branch = value;\n break;\n case 'summary':\n entry.summary = value;\n break;\n case 'blockers':\n entry.blockers = value;\n break;\n case 'status':\n entry.status = value;\n break;\n }\n }\n\n entries.push(entry);\n }\n\n return { workspace, entries };\n}\n\nexport function serializeLogEntry(entry: LogEntry): string {\n const idStr = entry.itemIds.map((id) => `t:${id}`).join(', ');\n const lines = [`### ${entry.timestamp} — ${idStr}`];\n if (entry.items) lines.push(`**Items:** ${entry.items}`);\n if (entry.session) lines.push(`**Session:** ${entry.session}`);\n if (entry.branch) lines.push(`**Branch:** ${entry.branch}`);\n if (entry.summary) lines.push(`**Summary:** ${entry.summary}`);\n if (entry.blockers) lines.push(`**Blockers:** ${entry.blockers}`);\n if (entry.status) lines.push(`**Status:** ${entry.status}`);\n return lines.join('\\n');\n}\n\n/**\n * Serialize a full todo log file (frontmatter + header + entries) in the exact\n * format `readLog`/`appendLogEntry` produce. Uses the canonical\n * `serializeLogEntry` so no entry field (incl. `status`) is dropped. Callers\n * that rewrite a trimmed log should use this rather than hand-building lines.\n */\nexport function serializeLog(log: TodoLog): string {\n const header = `---\\nworkspace: ${log.workspace}\\n---\\n\\n# Todo Log\\n`;\n if (log.entries.length === 0) {\n return header;\n }\n return header + '\\n' + log.entries.map(serializeLogEntry).join('\\n\\n') + '\\n';\n}\n\n// --- File I/O ---\n\nexport function checklistPath(todosDir: string, workspace: string): string {\n return resolve(todosDir, `${workspace}.md`);\n}\n\nexport function logPath(todosDir: string, workspace: string): string {\n return resolve(todosDir, `${workspace}-log.md`);\n}\n\nexport function archivePath(\n todosDir: string,\n workspace: string,\n interval: ArchiveInterval,\n now: Date = new Date(),\n): string {\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n\n let suffix: string;\n switch (interval) {\n case 'daily':\n suffix = `${year}-${month}-${day}`;\n break;\n case 'weekly': {\n // ISO week number\n const jan1 = new Date(year, 0, 1);\n const days = Math.floor((now.getTime() - jan1.getTime()) / 86400000);\n const week = String(Math.ceil((days + jan1.getDay() + 1) / 7)).padStart(2, '0');\n suffix = `${year}-W${week}`;\n break;\n }\n case 'monthly':\n suffix = `${year}-${month}`;\n break;\n default:\n suffix = `${year}-${month}-${day}`;\n }\n\n return resolve(todosDir, 'archive', `${workspace}-${suffix}.md`);\n}\n\nexport async function readChecklist(todosDir: string, workspace: string): Promise<TodoChecklist> {\n const path = checklistPath(todosDir, workspace);\n if (!(await fileExists(path))) {\n return { workspace, archiveInterval: 'weekly', items: [] };\n }\n const content = await readFile(path, 'utf-8');\n return parseChecklist(content);\n}\n\nexport async function writeChecklist(todosDir: string, checklist: TodoChecklist): Promise<void> {\n await ensureDir(todosDir);\n const path = checklistPath(todosDir, checklist.workspace);\n await writeFileForce(path, serializeChecklist(checklist));\n}\n\nexport async function readLog(todosDir: string, workspace: string): Promise<TodoLog> {\n const path = logPath(todosDir, workspace);\n if (!(await fileExists(path))) {\n return { workspace, entries: [] };\n }\n const content = await readFile(path, 'utf-8');\n return parseLog(content);\n}\n\nexport async function appendLogEntry(\n todosDir: string,\n workspace: string,\n entry: LogEntry,\n): Promise<void> {\n await ensureDir(todosDir);\n const path = logPath(todosDir, workspace);\n let content: string;\n if (await fileExists(path)) {\n content = await readFile(path, 'utf-8');\n content = content.trimEnd() + '\\n\\n' + serializeLogEntry(entry) + '\\n';\n } else {\n const fm = `---\\nworkspace: ${workspace}\\n---\\n\\n# Todo Log\\n\\n`;\n content = fm + serializeLogEntry(entry) + '\\n';\n }\n await writeFileForce(path, content);\n}\n\nexport function computeCounts(items: TodoItem[]) {\n const counts = { open: 0, in_progress: 0, completed: 0, blocked: 0, total: items.length };\n for (const item of items) {\n counts[item.status]++;\n }\n return counts;\n}\n","import { readdir } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport {\n readChecklist,\n writeChecklist,\n readLog,\n appendLogEntry,\n} from '../todos/parser.js';\nimport { fileExists } from '../utils/fs.js';\nimport type { TodoItem, LogEntry } from '../todos/types.js';\n\nexport interface LinkedTodosLookup {\n /** Workspace todos dir (e.g. ~/.syntaur/todos). */\n todosDir: string;\n /** Projects root dir (e.g. ~/.syntaur/projects). Used to scan per-project todo checklists. */\n projectsDir: string;\n}\n\nexport interface LinkedTodosResult {\n completed?: number;\n reopened?: number;\n touched: Array<{ workspace: string; id: string }>;\n}\n\nconst AUTO_COMPLETE_PREFIX = 'Auto-completed: linked assignment ';\nconst AUTO_REOPEN_PREFIX = 'Auto-reopened: linked assignment ';\n\nfunction touchItem(item: TodoItem): void {\n const now = new Date().toISOString();\n if (item.createdAt === null) item.createdAt = now;\n item.updatedAt = now;\n}\n\nasync function listWorkspaceTodosFiles(todosDir: string): Promise<string[]> {\n if (!(await fileExists(todosDir))) return [];\n const files = await readdir(todosDir).catch(() => [] as string[]);\n return files\n .filter((f): f is string => typeof f === 'string')\n .filter((f) => f.endsWith('.md') && !f.endsWith('-log.md'))\n .map((f) => f.replace(/\\.md$/, ''));\n}\n\nasync function listProjectTodosWorkspaces(projectsDir: string): Promise<Array<{ projectSlug: string; todosDir: string; workspace: string }>> {\n if (!(await fileExists(projectsDir))) return [];\n const projects = await readdir(projectsDir).catch(() => [] as string[]);\n const result: Array<{ projectSlug: string; todosDir: string; workspace: string }> = [];\n for (const p of projects) {\n if (typeof p !== 'string') continue;\n const todosDir = resolve(projectsDir, p, 'todos');\n if (await fileExists(resolve(todosDir, `${p}.md`))) {\n result.push({ projectSlug: p, todosDir, workspace: p });\n }\n }\n return result;\n}\n\n/**\n * Returns true if the most recent log entry for this item has summary\n * starting with `prefix`. Used to identify items that were auto-completed\n * (so we know it is safe to auto-reopen them) and items that were already\n * auto-reopened (idempotency).\n */\nasync function lastLogEntryMatches(\n todosDir: string,\n workspace: string,\n itemId: string,\n prefix: string,\n): Promise<boolean> {\n const log = await readLog(todosDir, workspace);\n // Scan in reverse: most recent matching entry for this item.\n for (let i = log.entries.length - 1; i >= 0; i--) {\n const entry = log.entries[i];\n if (!entry.itemIds.includes(itemId)) continue;\n return entry.summary.startsWith(prefix);\n }\n return false;\n}\n\nexport async function completeLinkedTodos(\n lookup: LinkedTodosLookup,\n assignmentId: string,\n assignmentRef: string,\n): Promise<LinkedTodosResult> {\n const touched: Array<{ workspace: string; id: string }> = [];\n\n const workspaces = await listWorkspaceTodosFiles(lookup.todosDir);\n const projectWorkspaces = await listProjectTodosWorkspaces(lookup.projectsDir);\n const all: Array<{ todosDir: string; workspace: string }> = [\n ...workspaces.map((workspace) => ({ todosDir: lookup.todosDir, workspace })),\n ...projectWorkspaces.map(({ todosDir, workspace }) => ({ todosDir, workspace })),\n ];\n\n for (const { todosDir, workspace } of all) {\n const checklist = await readChecklist(todosDir, workspace);\n const idsTouched: string[] = [];\n for (const item of checklist.items) {\n if (item.linkedAssignmentId !== assignmentId) continue;\n if (item.status === 'completed') continue;\n item.status = 'completed';\n item.session = null;\n touchItem(item);\n idsTouched.push(item.id);\n }\n if (idsTouched.length === 0) continue;\n await writeChecklist(todosDir, checklist);\n for (const id of idsTouched) {\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n itemIds: [id],\n items: checklist.items.find((i) => i.id === id)?.description ?? '',\n session: null,\n branch: null,\n summary: `${AUTO_COMPLETE_PREFIX}${assignmentRef} closed`,\n blockers: null,\n status: null,\n };\n await appendLogEntry(todosDir, workspace, entry);\n touched.push({ workspace, id });\n }\n }\n\n return { completed: touched.length, touched };\n}\n\nexport async function reopenLinkedTodos(\n lookup: LinkedTodosLookup,\n assignmentId: string,\n assignmentRef: string,\n): Promise<LinkedTodosResult> {\n const touched: Array<{ workspace: string; id: string }> = [];\n\n const workspaces = await listWorkspaceTodosFiles(lookup.todosDir);\n const projectWorkspaces = await listProjectTodosWorkspaces(lookup.projectsDir);\n const all: Array<{ todosDir: string; workspace: string }> = [\n ...workspaces.map((workspace) => ({ todosDir: lookup.todosDir, workspace })),\n ...projectWorkspaces.map(({ todosDir, workspace }) => ({ todosDir, workspace })),\n ];\n\n for (const { todosDir, workspace } of all) {\n const checklist = await readChecklist(todosDir, workspace);\n const candidates = checklist.items.filter(\n (i) => i.linkedAssignmentId === assignmentId && i.status === 'completed',\n );\n if (candidates.length === 0) continue;\n const idsTouched: string[] = [];\n for (const item of candidates) {\n // Manual-completion guard: only auto-reopen items whose most recent log\n // entry is the auto-complete marker. If the user marked them complete\n // by hand afterwards, leave them alone.\n const wasAutoCompleted = await lastLogEntryMatches(\n todosDir,\n workspace,\n item.id,\n AUTO_COMPLETE_PREFIX,\n );\n if (!wasAutoCompleted) continue;\n item.status = 'in_progress';\n item.session = null;\n touchItem(item);\n idsTouched.push(item.id);\n }\n if (idsTouched.length === 0) continue;\n await writeChecklist(todosDir, checklist);\n for (const id of idsTouched) {\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n itemIds: [id],\n items: checklist.items.find((i) => i.id === id)?.description ?? '',\n session: null,\n branch: null,\n summary: `${AUTO_REOPEN_PREFIX}${assignmentRef} reopened`,\n blockers: null,\n status: null,\n };\n await appendLogEntry(todosDir, workspace, entry);\n touched.push({ workspace, id });\n }\n }\n\n return { reopened: touched.length, touched };\n}\n","import { resolve } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport { fileExists, writeFileForce } from '../utils/fs.js';\nimport { nowTimestamp } from '../utils/timestamp.js';\nimport { getTargetStatus } from './state-machine.js';\nimport { appendStatusHistoryEntry, parseAssignmentFrontmatter, updateAssignmentFile } from './frontmatter.js';\nimport { recordStatusEvent, resolveActor, emitEvent } from './event-emit.js';\nimport {\n completeLinkedTodos,\n reopenLinkedTodos,\n type LinkedTodosLookup,\n} from './linked-todos.js';\nimport type { TransitionCommand, TransitionResult, AssignmentFrontmatter } from './types.js';\n\nfunction linkedAssignmentRef(frontmatter: AssignmentFrontmatter): string {\n return frontmatter.project ? `${frontmatter.project}/${frontmatter.slug}` : frontmatter.id;\n}\n\nasync function applyLinkedTodosSideEffect(\n lookup: LinkedTodosLookup | undefined,\n command: string,\n targetStatus: string,\n frontmatter: AssignmentFrontmatter,\n): Promise<void> {\n if (!lookup) return;\n const ref = linkedAssignmentRef(frontmatter);\n if (targetStatus === 'completed') {\n await completeLinkedTodos(lookup, frontmatter.id, ref);\n } else if (command === 'reopen') {\n await reopenLinkedTodos(lookup, frontmatter.id, ref);\n }\n}\n\nfunction resolveAssignmentPath(projectDir: string, assignmentSlug: string): string {\n return resolve(projectDir, 'assignments', assignmentSlug, 'assignment.md');\n}\n\nasync function readAssignment(\n filePath: string,\n): Promise<{ content: string; frontmatter: AssignmentFrontmatter }> {\n if (!(await fileExists(filePath))) {\n throw new Error(`Assignment file not found: ${filePath}`);\n }\n const content = await readFile(filePath, 'utf-8');\n const frontmatter = parseAssignmentFrontmatter(content);\n return { content, frontmatter };\n}\n\n/**\n * Resolve which of an assignment's `dependsOn` targets are not yet terminal.\n * Exported so derive verbs (`start`/`implement`) can surface the same\n * non-blocking unmet-dependency warning the legacy transition path emits.\n */\nexport async function checkDependencies(\n projectDir: string,\n dependsOn: string[],\n terminalStatuses?: ReadonlySet<string>,\n): Promise<{ satisfied: boolean; unmet: string[] }> {\n const terminals = terminalStatuses ?? new Set(['completed']);\n const unmet: string[] = [];\n for (const depSlug of dependsOn) {\n const depPath = resolveAssignmentPath(projectDir, depSlug);\n if (!(await fileExists(depPath))) {\n unmet.push(`${depSlug} (file not found)`);\n continue;\n }\n const depContent = await readFile(depPath, 'utf-8');\n const depFrontmatter = parseAssignmentFrontmatter(depContent);\n if (!terminals.has(depFrontmatter.status)) {\n unmet.push(`${depSlug} (status: ${depFrontmatter.status})`);\n }\n }\n return { satisfied: unmet.length === 0, unmet };\n}\n\nexport interface TransitionOptions {\n reason?: string;\n agent?: string;\n /**\n * Actor to attribute the audit status-event to, INDEPENDENT of `agent` (which\n * drives assignee mutation). Dashboard transition routes pass `'human'` here\n * so a click on an already-assigned task is recorded as `human`, not the\n * assignee. When unset, falls back to `agent ?? frontmatter.assignee`.\n */\n auditActor?: string;\n transitionTable?: Map<string, string>;\n /** Guard-free custom targets: when provided (and no transitionTable), the\n * command resolves to this map's target regardless of the current status —\n * preserving a CUSTOM terminal target (e.g. complete -> done) without the\n * from:command guard, even for assignments on legacy/undefined statuses. */\n commandTargets?: Map<string, string>;\n terminalStatuses?: ReadonlySet<string>;\n /**\n * When provided, on a transition to `completed` we scan the configured todos\n * dirs and auto-complete any todo whose `linkedAssignmentId` matches this\n * assignment's UUID. On `reopen` we auto-reopen any such todo whose most\n * recent log entry is the auto-complete marker (manual completions are left\n * untouched).\n */\n linkedTodosLookup?: LinkedTodosLookup;\n}\n\nconst ASSIGNEE_SETTING_COMMANDS = new Set(['start', 'shape', 'plan-ready', 'implement']);\n\nexport async function executeTransition(\n projectDir: string,\n assignmentSlug: string,\n command: Exclude<TransitionCommand, 'assign'>,\n options: TransitionOptions = {},\n): Promise<TransitionResult> {\n const filePath = resolveAssignmentPath(projectDir, assignmentSlug);\n const { content, frontmatter } = await readAssignment(filePath);\n\n // Resolution order: a from-specific custom mapping wins; the guard-free\n // commandTargets fallback covers legacy/undefined statuses; built-ins last\n // (only when neither custom mechanism was supplied).\n const targetStatus =\n (options.transitionTable\n ? getTargetStatus(frontmatter.status, command, options.transitionTable)\n : null) ??\n options.commandTargets?.get(command) ??\n // Built-ins apply only when NEITHER custom mechanism was supplied — a\n // provided-but-miss commandTargets means \"custom config had no answer\",\n // which must refuse, not silently fall back (codex r4).\n (!options.transitionTable && !options.commandTargets\n ? getTargetStatus(frontmatter.status, command)\n : null);\n\n if (!targetStatus) {\n return {\n success: false,\n message: `Unknown command '${command}' for assignment \"${assignmentSlug}\".`,\n fromStatus: frontmatter.status,\n };\n }\n\n const warnings: string[] = [];\n\n if (command === 'start' && frontmatter.dependsOn.length > 0) {\n const depCheck = await checkDependencies(projectDir, frontmatter.dependsOn, options.terminalStatuses);\n if (!depCheck.satisfied) {\n warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(', ')}`);\n }\n }\n\n const now = nowTimestamp();\n const updates: Partial<\n Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated' | 'disposition'>\n > = {\n status: targetStatus,\n updated: now,\n };\n\n if (ASSIGNEE_SETTING_COMMANDS.has(command) && options.agent && !frontmatter.assignee) {\n updates.assignee = options.agent;\n }\n if (command === 'block') {\n // Derived-status v3: the blocked disposition keys on blockedReason\n // PRESENCE — a null reason would make block-without-reason a silent\n // no-op under derivation. Match the CLI verb's default.\n updates.blockedReason = options.reason ?? '(unspecified)';\n }\n if (command === 'unblock') {\n updates.blockedReason = null;\n }\n\n // Dimension-aware terminal cache (derived-status v3): entering a terminal\n // status sets `disposition: terminal` so payloads/queries never show a\n // terminal headline with a stale active/blocked disposition. Leaving\n // terminal (reopen) hands the cache back to derivation, which the CLI\n // reopen command runs immediately after this transition.\n const terminalSet = options.terminalStatuses ?? new Set(['completed', 'failed']);\n const enteringTerminal = terminalSet.has(targetStatus) && frontmatter.disposition !== 'terminal';\n if (enteringTerminal) {\n updates.disposition = 'terminal';\n }\n\n let updatedContent = updateAssignmentFile(content, updates);\n // Only record a history entry on an ACTUAL status change. CLI commands are\n // guard-free (getTargetStatus returns the canonical target regardless of the\n // current status), so re-running e.g. `complete` on an already-completed\n // assignment must not append a from===to entry and reset statusAge.\n if (targetStatus !== frontmatter.status) {\n updatedContent = appendStatusHistoryEntry(updatedContent, {\n at: now,\n from: frontmatter.status,\n to: targetStatus,\n command,\n by: options.agent ?? frontmatter.assignee ?? null,\n reason: command === 'block' ? options.reason : undefined,\n ...(enteringTerminal\n ? { dispositionFrom: frontmatter.disposition, dispositionTo: 'terminal' }\n : {}),\n });\n }\n await writeFileForce(filePath, updatedContent);\n\n // Audit event (best-effort): self-guards on from===to (R5). The audit actor\n // is independent of `agent` (which drives assignee mutation) — dashboard\n // routes pass `auditActor: 'human'` so a click is not recorded as the\n // assignee (FIX 1).\n recordStatusEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n at: now,\n actor: resolveActor(options.auditActor ?? options.agent ?? frontmatter.assignee ?? null),\n from: frontmatter.status,\n to: targetStatus,\n command,\n });\n\n await applyLinkedTodosSideEffect(options.linkedTodosLookup, command, targetStatus, frontmatter);\n\n return {\n success: true,\n message: `Assignment \"${assignmentSlug}\" transitioned: ${frontmatter.status} -> ${targetStatus}`,\n fromStatus: frontmatter.status,\n toStatus: targetStatus,\n warnings: warnings.length > 0 ? warnings : undefined,\n };\n}\n\nexport async function executeAssign(\n projectDir: string,\n assignmentSlug: string,\n agent: string,\n): Promise<TransitionResult> {\n const filePath = resolveAssignmentPath(projectDir, assignmentSlug);\n const { content, frontmatter } = await readAssignment(filePath);\n\n const updates: Partial<Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated'>> = {\n assignee: agent,\n updated: nowTimestamp(),\n };\n\n const updatedContent = updateAssignmentFile(content, updates);\n await writeFileForce(filePath, updatedContent);\n\n // Audit event (best-effort): assignee changed from prior to `agent`.\n if (frontmatter.assignee !== agent) {\n emitEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n type: 'assignee-change',\n actor: resolveActor(agent ?? frontmatter.assignee ?? null),\n details: { from: frontmatter.assignee, to: agent },\n });\n }\n\n return {\n success: true,\n message: `Assignment \"${assignmentSlug}\" assigned to '${agent}'.`,\n fromStatus: frontmatter.status,\n };\n}\n\nexport interface TransitionByDirOptions extends TransitionOptions {\n standalone?: boolean;\n}\n\nexport async function executeTransitionByDir(\n assignmentDir: string,\n command: Exclude<TransitionCommand, 'assign'>,\n options: TransitionByDirOptions = {},\n): Promise<TransitionResult> {\n const filePath = resolve(assignmentDir, 'assignment.md');\n const { content, frontmatter } = await readAssignment(filePath);\n\n // See executeTransition: from-specific mapping wins, commandTargets is the\n // guard-free fallback, built-ins only when no custom mechanism supplied.\n const targetStatus =\n (options.transitionTable\n ? getTargetStatus(frontmatter.status, command, options.transitionTable)\n : null) ??\n options.commandTargets?.get(command) ??\n // Built-ins apply only when NEITHER custom mechanism was supplied — a\n // provided-but-miss commandTargets means \"custom config had no answer\",\n // which must refuse, not silently fall back (codex r4).\n (!options.transitionTable && !options.commandTargets\n ? getTargetStatus(frontmatter.status, command)\n : null);\n if (!targetStatus) {\n return {\n success: false,\n message: `Unknown command '${command}' for assignment \"${frontmatter.slug || assignmentDir}\".`,\n fromStatus: frontmatter.status,\n };\n }\n\n const warnings: string[] = [];\n\n if (command === 'start' && !options.standalone && frontmatter.dependsOn.length > 0) {\n // Dependency check requires a project context — skip for standalone\n const projectDir = resolve(assignmentDir, '..', '..');\n const depCheck = await checkDependencies(\n projectDir,\n frontmatter.dependsOn,\n options.terminalStatuses,\n );\n if (!depCheck.satisfied) {\n warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(', ')}`);\n }\n }\n\n const now = nowTimestamp();\n const updates: Partial<\n Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated' | 'disposition'>\n > = {\n status: targetStatus,\n updated: now,\n };\n\n if (ASSIGNEE_SETTING_COMMANDS.has(command) && options.agent && !frontmatter.assignee) {\n updates.assignee = options.agent;\n }\n if (command === 'block') {\n // Derived-status v3: the blocked disposition keys on blockedReason\n // PRESENCE — a null reason would make block-without-reason a silent\n // no-op under derivation. Match the CLI verb's default.\n updates.blockedReason = options.reason ?? '(unspecified)';\n }\n if (command === 'unblock') {\n updates.blockedReason = null;\n }\n\n // Dimension-aware terminal cache — see executeTransition.\n const terminalSetByDir = options.terminalStatuses ?? new Set(['completed', 'failed']);\n const enteringTerminalByDir =\n terminalSetByDir.has(targetStatus) && frontmatter.disposition !== 'terminal';\n if (enteringTerminalByDir) {\n updates.disposition = 'terminal';\n }\n\n let updatedContent = updateAssignmentFile(content, updates);\n // Only record a history entry on an ACTUAL status change (see executeTransition).\n if (targetStatus !== frontmatter.status) {\n updatedContent = appendStatusHistoryEntry(updatedContent, {\n at: now,\n from: frontmatter.status,\n to: targetStatus,\n command,\n by: options.agent ?? frontmatter.assignee ?? null,\n reason: command === 'block' ? options.reason : undefined,\n ...(enteringTerminalByDir\n ? { dispositionFrom: frontmatter.disposition, dispositionTo: 'terminal' }\n : {}),\n });\n }\n await writeFileForce(filePath, updatedContent);\n\n // Audit event (best-effort): self-guards on from===to (R5). The audit actor\n // is independent of `agent` (see executeTransition / FIX 1).\n recordStatusEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n at: now,\n actor: resolveActor(options.auditActor ?? options.agent ?? frontmatter.assignee ?? null),\n from: frontmatter.status,\n to: targetStatus,\n command,\n });\n\n await applyLinkedTodosSideEffect(options.linkedTodosLookup, command, targetStatus, frontmatter);\n\n return {\n success: true,\n message: `Assignment \"${frontmatter.slug || assignmentDir}\" transitioned: ${frontmatter.status} -> ${targetStatus}`,\n fromStatus: frontmatter.status,\n toStatus: targetStatus,\n warnings: warnings.length > 0 ? warnings : undefined,\n };\n}\n\nexport async function executeAssignByDir(\n assignmentDir: string,\n agent: string,\n): Promise<TransitionResult> {\n const filePath = resolve(assignmentDir, 'assignment.md');\n const { content, frontmatter } = await readAssignment(filePath);\n\n const updates: Partial<Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated'>> = {\n assignee: agent,\n updated: nowTimestamp(),\n };\n\n const updatedContent = updateAssignmentFile(content, updates);\n await writeFileForce(filePath, updatedContent);\n\n if (frontmatter.assignee !== agent) {\n emitEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n type: 'assignee-change',\n actor: resolveActor(agent ?? frontmatter.assignee ?? null),\n details: { from: frontmatter.assignee, to: agent },\n });\n }\n\n return {\n success: true,\n message: `Assignment \"${frontmatter.slug || assignmentDir}\" assigned to '${agent}'.`,\n fromStatus: frontmatter.status,\n };\n}\n\nexport async function executeUnassign(\n projectDir: string,\n assignmentSlug: string,\n): Promise<TransitionResult> {\n const filePath = resolveAssignmentPath(projectDir, assignmentSlug);\n const { content, frontmatter } = await readAssignment(filePath);\n\n const updates: Partial<Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated'>> = {\n assignee: null,\n updated: nowTimestamp(),\n };\n\n const updatedContent = updateAssignmentFile(content, updates);\n await writeFileForce(filePath, updatedContent);\n\n if (frontmatter.assignee !== null) {\n emitEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n type: 'assignee-change',\n actor: resolveActor(frontmatter.assignee),\n details: { from: frontmatter.assignee, to: null },\n });\n }\n\n return {\n success: true,\n message: `Assignment \"${assignmentSlug}\" unassigned (assignee cleared).`,\n fromStatus: frontmatter.status,\n };\n}\n\nexport async function executeUnassignByDir(\n assignmentDir: string,\n): Promise<TransitionResult> {\n const filePath = resolve(assignmentDir, 'assignment.md');\n const { content, frontmatter } = await readAssignment(filePath);\n\n const updates: Partial<Pick<AssignmentFrontmatter, 'status' | 'assignee' | 'blockedReason' | 'updated'>> = {\n assignee: null,\n updated: nowTimestamp(),\n };\n\n const updatedContent = updateAssignmentFile(content, updates);\n await writeFileForce(filePath, updatedContent);\n\n if (frontmatter.assignee !== null) {\n emitEvent({\n assignmentId: frontmatter.id,\n projectSlug: frontmatter.project,\n type: 'assignee-change',\n actor: resolveActor(frontmatter.assignee),\n details: { from: frontmatter.assignee, to: null },\n });\n }\n\n return {\n success: true,\n message: `Assignment \"${frontmatter.slug || assignmentDir}\" unassigned (assignee cleared).`,\n fromStatus: frontmatter.status,\n };\n}\n","export type {\n AssignmentStatus,\n TransitionCommand,\n AssignmentFrontmatter,\n ExternalId,\n Workspace,\n TransitionResult,\n} from './types.js';\nexport { TERMINAL_STATUSES, DEFAULT_STATUSES, DEFAULT_COMMANDS, DEFAULT_TERMINAL_STATUSES } from './types.js';\nexport { canTransition, getTargetStatus, isTerminalStatus, DEFAULT_TRANSITION_TABLE, DEFAULT_COMMAND_TARGETS, buildTransitionTable, buildCommandTargets } from './state-machine.js';\nexport { parseAssignmentFrontmatter, updateAssignmentFile, updateAssignmentWorkspace } from './frontmatter.js';\nexport { executeTransition, executeAssign, executeTransitionByDir, executeAssignByDir, executeUnassign, executeUnassignByDir } from './transitions.js';\nexport type { TransitionOptions, TransitionByDirOptions } from './transitions.js';\n","// Shared hotkey catalog: bindable action kinds, reserved combos, and the\n// canonical combo string format. Imported directly by the Express server\n// (src/dashboard/server.ts) and by the dashboard via the\n// `@shared/hotkeys-catalog` alias defined in dashboard/tsconfig.json +\n// dashboard/vite.config.ts.\n\nexport type BindableActionKind =\n | 'new-workspace'\n | 'new-project'\n | 'new-todo'\n | 'new-assignment';\n\nexport const BINDABLE_ACTION_KINDS: readonly BindableActionKind[] = [\n 'new-workspace',\n 'new-project',\n 'new-todo',\n 'new-assignment',\n];\n\nexport function isBindableActionKind(value: unknown): value is BindableActionKind {\n return (\n typeof value === 'string' &&\n (BINDABLE_ACTION_KINDS as readonly string[]).includes(value)\n );\n}\n\n// Reserved combos that user-bound hotkeys may NOT shadow. Hand-maintained;\n// scripts/check-hotkey-catalog.ts greps `useHotkey({` across the dashboard and\n// fails if it sees a `keys` value that is not represented here.\n//\n// Combos are stored in canonical form (see canonicalizeCombo). The list\n// includes:\n// - global UI combos (Mod+k, Mod+Shift+k, ?, Escape, Enter, Shift+t)\n// - g <suffix> chord prefixes (the lone \"g\" is reserved as a chord starter)\n// - list-scope letters\n// - page-scoped shortcuts that exist when those pages are mounted\nexport const BUILTIN_RESERVED_COMBOS: readonly string[] = [\n 'mod+k',\n 'mod+shift+k',\n '?',\n 'escape',\n 'enter',\n 'shift+t',\n // g-chord starter + suffixes\n 'g',\n 'g o',\n 'g m',\n 'g a',\n 'g t',\n 'g s',\n 'g !',\n 'g ,',\n // list-scope navigation\n 'j',\n 'k',\n 'o',\n // ProjectDetail page\n 'a',\n 'e',\n // AssignmentsPage board\n '/',\n 'r',\n // AssignmentDetail page\n 'p',\n 'h',\n 'd',\n 's',\n '[',\n ']',\n];\n\nconst MODIFIER_ORDER: readonly string[] = ['mod', 'ctrl', 'alt', 'shift'];\n\n/**\n * Canonicalize a combo string for storage and comparison.\n *\n * - Trims whitespace.\n * - Splits on `+` for single-key combos; preserves space-separated chord form\n * (e.g. `g a`) by canonicalizing each part independently.\n * - Lowercases everything (modifiers and the trailing key alike).\n * - Reorders modifiers into canonical order: mod, ctrl, alt, shift.\n *\n * Examples:\n * canonicalizeCombo(\"Shift+Mod+K\") -> \"mod+shift+k\"\n * canonicalizeCombo(\" cmd + Enter\") -> \"mod+enter\" (after caller maps cmd->mod)\n * canonicalizeCombo(\"g A\") -> \"g a\"\n * canonicalizeCombo(\"?\") -> \"?\"\n */\nexport function canonicalizeCombo(input: string): string {\n if (typeof input !== 'string') return '';\n const trimmed = input.trim();\n if (!trimmed) return '';\n\n // Chord form: space-separated, no `+` separators (e.g. \"g a\"). When the\n // input contains `+` it's treated as a single combo even if it has stray\n // whitespace around the separators (e.g. \"Mod + K\").\n if (/\\s/.test(trimmed) && !trimmed.includes('+')) {\n return trimmed\n .split(/\\s+/)\n .map(canonicalizeCombo)\n .filter((part) => part.length > 0)\n .join(' ');\n }\n\n const parts = trimmed.split('+').map((p) => p.trim()).filter((p) => p.length > 0);\n if (parts.length === 0) return '';\n if (parts.length === 1) {\n return parts[0].toLowerCase();\n }\n\n const key = parts[parts.length - 1].toLowerCase();\n const mods = parts.slice(0, -1).map((m) => m.toLowerCase());\n\n const seen = new Set<string>();\n const ordered: string[] = [];\n for (const m of MODIFIER_ORDER) {\n if (mods.includes(m) && !seen.has(m)) {\n ordered.push(m);\n seen.add(m);\n }\n }\n // Append any non-standard modifiers at the end (preserves user intent for\n // anything we don't recognize).\n for (const m of mods) {\n if (!seen.has(m)) {\n ordered.push(m);\n seen.add(m);\n }\n }\n\n return [...ordered, key].join('+');\n}\n\n/**\n * Returns true when `combo` (canonicalized) collides with a built-in reserved\n * combo. Server-side enforcement entry point.\n */\nexport function isReservedCombo(combo: string): boolean {\n const c = canonicalizeCombo(combo);\n if (!c) return false;\n return (BUILTIN_RESERVED_COMBOS as readonly string[]).includes(c);\n}\n\n/**\n * Default hotkey bindings shipped with the dashboard. The triple-modifier\n * `Mod+Shift+Alt+<letter>` namespace is intentionally chosen to avoid common\n * browser shortcuts (Cmd+Shift+T reopens closed tab, Cmd+Shift+P opens\n * private mode, Cmd+Shift+W closes window, etc.) while keeping the action\n * mnemonic. Users can override any of these from Settings → Hotkey Bindings.\n *\n * These are EFFECTIVE only when the user has not bound a custom combo for\n * that action — `effectiveBindings()` overlays the user's custom bindings on\n * top, so a custom binding always wins.\n */\nexport const DEFAULT_BINDABLE_HOTKEYS: Readonly<Record<BindableActionKind, string>> = {\n 'new-workspace': canonicalizeCombo('Mod+Shift+Alt+w'),\n 'new-project': canonicalizeCombo('Mod+Shift+Alt+p'),\n 'new-todo': canonicalizeCombo('Mod+Shift+Alt+t'),\n 'new-assignment': canonicalizeCombo('Mod+Shift+Alt+a'),\n};\n\n/**\n * Returns the effective binding map: defaults underneath, user customs on top.\n * A user-bound combo always wins; if the user has no entry for a kind, the\n * default is returned (if any).\n */\nexport function effectiveBindings(\n custom: Partial<Record<BindableActionKind, string>>,\n): Partial<Record<BindableActionKind, string>> {\n const out: Partial<Record<BindableActionKind, string>> = {\n ...DEFAULT_BINDABLE_HOTKEYS,\n };\n for (const kind of BINDABLE_ACTION_KINDS) {\n const override = custom[kind];\n if (typeof override === 'string' && override.length > 0) {\n out[kind] = override;\n }\n }\n return out;\n}\n\n/** True when the given kind currently uses its default combo (no user override). */\nexport function isDefaultBinding(\n custom: Partial<Record<BindableActionKind, string>>,\n kind: BindableActionKind,\n): boolean {\n const override = custom[kind];\n return typeof override !== 'string' || override.length === 0;\n}\n","export type PromptArgPosition = 'first' | 'last' | 'none';\n\n/**\n * Per-agent argv recipe for continuing a recorded session in a specific mode.\n *\n * `args` is a literal argv list with the substring `{id}` substituted for the\n * agent's session id at launch time. `command` overrides the agent's main\n * `command` field — used by subcommand-style agents (e.g. `codex resume <id>`\n * is documented as command=codex, args=['resume','{id}']; the override exists\n * for future agents whose subcommand binary differs).\n */\nexport interface SessionInvocation {\n command?: string;\n args: string[];\n}\n\nexport interface AgentConfig {\n id: string;\n label: string;\n command: string;\n args?: string[];\n promptArgPosition?: PromptArgPosition;\n default?: boolean;\n resolveFromShellAliases?: boolean;\n resume?: SessionInvocation;\n fork?: SessionInvocation;\n /**\n * Optional LLM model for this runner profile, injected into the launched CLI\n * as a generic `--model <value>` flag (see `modelFlagArgs`). Blank/undefined\n * omits the flag entirely (today's behavior). Works for agents whose CLI\n * accepts `--model` (claude, codex); leave blank for agents that don't.\n */\n model?: string;\n /**\n * Optional playbook slug for this runner profile. Back-compat shorthand: when\n * set (and no `launchPrompt`), a fresh \"Open in agent\" launch synthesizes a\n * prompt that grabs the assignment AND runs this playbook end-to-end (see\n * `resolveLaunchPrompt`). Blank/undefined keeps the plain `/grab-assignment`\n * seed. Ignored for seed assembly when `launchPrompt` is also set.\n */\n playbook?: string;\n /**\n * Editable, user-owned launch prompt: the literal first message handed to the\n * agent on a fresh \"Open in agent\" launch, with `@`-tokens resolved at launch\n * time (`@assignment` → assignment id + records-dir path + grab/read\n * instructions; `@<playbook-slug>` → a `/run-playbook` reference). When unset,\n * falls back to back-compat `playbook`, then to the bare `/grab-assignment`\n * seed. Takes precedence over `playbook`. Single-line (see `serializeAgentsConfig`).\n */\n launchPrompt?: string;\n}\n\nexport const BUILTIN_AGENTS: AgentConfig[] = [\n {\n id: 'claude',\n label: 'Claude',\n command: 'claude',\n default: true,\n resume: { args: ['--resume', '{id}'] },\n fork: { args: ['--resume', '{id}', '--fork-session'] },\n },\n {\n id: 'codex',\n label: 'Codex',\n command: 'codex',\n resume: { args: ['resume', '{id}'] },\n fork: { args: ['fork', '{id}'] },\n },\n // pi: resume/fork verified against `pi --help` (pi is installed) — `--session\n // <path|id>` continues a recorded session, `--fork <path|id>` forks one. (Not\n // `--resume`, which is an interactive picker that takes no id.)\n {\n id: 'pi',\n label: 'Pi',\n command: 'pi',\n resume: { args: ['--session', '{id}'] },\n fork: { args: ['--fork', '{id}'] },\n },\n // openclaw: resume/fork intentionally omitted. The openclaw binary is not\n // installed here, so its CLI cannot be verified; the only evidence it shares\n // pi's flags is a hedged design-memo assumption (see src/targets/registry.ts).\n // A missing recipe degrades gracefully to LaunchError('mode-not-supported'),\n // which a user can override via ~/.syntaur/config.md; a wrong recipe would\n // silently launch the wrong command. Add verified recipes once installable.\n {\n id: 'openclaw',\n label: 'OpenClaw',\n command: 'openclaw',\n },\n // hermes: resume/fork omitted — binary not installed and no resume/fork CLI is\n // documented for it. Ship launch-only; same graceful-degradation rationale.\n {\n id: 'hermes',\n label: 'Hermes',\n command: 'hermes',\n },\n];\n\nexport const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;\nexport const PROMPT_ARG_POSITIONS: readonly PromptArgPosition[] = ['first', 'last', 'none'];\n\n/**\n * Argv fragment that injects the profile's model as a generic `--model <value>`\n * flag, or `[]` when no model is set.\n */\nexport function modelFlagArgs(agent: AgentConfig): string[] {\n const m = agent.model?.trim();\n return m ? ['--model', m] : [];\n}\n\n/**\n * Remove any existing `--model`/`-m` flag (and its value) from an argv list.\n * Handles both the separate (`--model opus`) and combined (`--model=opus`,\n * `-m=opus`) forms.\n */\nfunction stripModelFlags(args: string[]): string[] {\n const out: string[] = [];\n for (let i = 0; i < args.length; i++) {\n const a = args[i];\n if (a === '--model' || a === '-m') {\n i++; // also skip the following value token\n continue;\n }\n if (a.startsWith('--model=') || a.startsWith('-m=')) continue;\n out.push(a);\n }\n return out;\n}\n\n/**\n * Apply the profile's model to a base argv list. When the profile sets a model,\n * any pre-existing `--model`/`-m` in `baseArgs` is stripped first and the\n * profile flag appended — the profile model is authoritative AND we never emit a\n * duplicate `--model` (Codex 0.135.0 rejects duplicate `--model` outright; it is\n * not last-wins). When the profile has no model, `baseArgs` is returned\n * unchanged so a hand-written `--model` in `args` still works.\n */\nexport function applyModelFlag(agent: AgentConfig, baseArgs: string[]): string[] {\n const flag = modelFlagArgs(agent);\n if (flag.length === 0) return baseArgs;\n return [...stripModelFlags(baseArgs), ...flag];\n}\n","export function slugify(title: string): string {\n return title\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\nexport function isValidSlug(slug: string): boolean {\n return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug);\n}\n","/**\n * Browser-safe derive-config primitives.\n *\n * Extracted from `config.ts` (which is Node-heavy — `node:fs/promises`,\n * `node:child_process`, etc. — and cannot be aliased into the Vite/browser\n * build). This module has ZERO imports so the dashboard client can alias it\n * (`@shared/derive-config`) and reuse the exact same validator the server and\n * `doctor` run, guaranteeing client/server/doctor parity.\n *\n * `config.ts` re-exports everything here so existing Node-side imports from\n * `config.js` keep resolving unchanged.\n */\n\n/** One rung of the phase ladder (derived-status v3). Ordered low → high; the\n * HIGHEST rung whose AQL `when` holds wins. Regressible by design (e.g. a\n * replan invalidates approval and the phase drops). `phase` must be a defined\n * status id; `next` is the human next-action label surfaced by views. */\nexport interface PhaseRung {\n phase: string;\n when: string;\n next?: string;\n}\n\n/** One disposition rule (first match wins). `when: null` is the `else` arm.\n * `is` ∈ active|blocked|parked (terminal is never a rule — terminal statuses\n * defer derivation entirely). */\nexport interface DispositionRule {\n when: string | null;\n is: string;\n}\n\n/** Headline projection: which status id the single-column board shows.\n * `terminal` is always passthrough and `active` always shows the phase; the\n * configurable parts are which status ids represent parked/blocked. */\nexport interface HeadlineProjection {\n terminal: 'passthrough';\n parked: string;\n blocked: string;\n active: 'phase';\n}\n\nexport interface DeriveConfig {\n phaseLadder: PhaseRung[];\n disposition: DispositionRule[];\n headline: HeadlineProjection;\n}\n\n/** Built-in derive rules matching DEFAULT_STATUSES (review rung = `review`). */\nexport const DEFAULT_DERIVE_CONFIG: DeriveConfig = {\n phaseLadder: [\n { phase: 'draft', when: '*', next: 'Fill in the objective and acceptance criteria' },\n {\n // planExists-but-not-approved also sits here: the default status set has\n // no `planning` id. Users who define one add a `planExists:true` rung.\n phase: 'ready_for_planning',\n when: 'hasRealObjective:true AND acRealTotal > 0',\n next: 'Write a plan and get it approved',\n },\n { phase: 'ready_to_implement', when: 'planApproved:true', next: 'Start implementing' },\n {\n phase: 'in_progress',\n when: 'planApproved:true AND implementationStarted:true',\n next: 'Finish acceptance criteria, then request review',\n },\n {\n phase: 'review',\n when: 'acAllChecked:true OR reviewRequested:true',\n next: 'Complete, or address review feedback',\n },\n ],\n disposition: [\n { when: 'parked:true', is: 'parked' },\n { when: 'blocked:true', is: 'blocked' },\n { when: null, is: 'active' },\n ],\n headline: { terminal: 'passthrough', parked: 'parked', blocked: 'blocked', active: 'phase' },\n};\n\n/**\n * Validate derive rules against a status config: rung/headline ids must be\n * defined statuses, disposition `is` values must be active|blocked|parked,\n * and every `when` must parse against the AQL field registry. Returns\n * human-readable problems (empty = valid). Used by doctor and the dashboard\n * settings API; pure so the dashboard client reuses it.\n *\n * The status param is intentionally minimal (`{ statuses: Array<{ id: string }> }`)\n * so this module stays dependency-free; it is structurally compatible with the\n * `Pick<StatusConfig, 'statuses'>` callers pass.\n */\n/**\n * Deeply shape-check an UNTRUSTED derive payload (from the dashboard POST body)\n * before {@link validateDeriveConfig} runs. validateDeriveConfig assumes every\n * rung/rule/field already has its declared type, so a malformed payload (a null\n * rung, a numeric `when`, …) would otherwise throw a 500 — and worse, slip\n * through to serialization after assignment files were already mutated. This\n * returns human-readable problems (empty = structurally sound) so the API can\n * reject with `invalid-derive` 400 before touching disk.\n */\nexport function validateDeriveShape(value: unknown): string[] {\n const problems: string[] = [];\n if (!value || typeof value !== 'object') {\n return ['derive must be an object'];\n }\n const d = value as Record<string, unknown>;\n\n if (!Array.isArray(d.phaseLadder)) {\n problems.push('derive.phaseLadder must be an array');\n } else {\n d.phaseLadder.forEach((rung, i) => {\n if (!rung || typeof rung !== 'object') {\n problems.push(`derive.phaseLadder[${i}] must be an object`);\n return;\n }\n const r = rung as Record<string, unknown>;\n if (typeof r.phase !== 'string') problems.push(`derive.phaseLadder[${i}].phase must be a string`);\n if (typeof r.when !== 'string') problems.push(`derive.phaseLadder[${i}].when must be a string`);\n if (r.next !== undefined && typeof r.next !== 'string') {\n problems.push(`derive.phaseLadder[${i}].next must be a string when present`);\n }\n });\n }\n\n if (!Array.isArray(d.disposition)) {\n problems.push('derive.disposition must be an array');\n } else {\n d.disposition.forEach((rule, i) => {\n if (!rule || typeof rule !== 'object') {\n problems.push(`derive.disposition[${i}] must be an object`);\n return;\n }\n const r = rule as Record<string, unknown>;\n if (!(r.when === null || typeof r.when === 'string')) {\n problems.push(`derive.disposition[${i}].when must be a string or null`);\n }\n if (typeof r.is !== 'string') problems.push(`derive.disposition[${i}].is must be a string`);\n });\n }\n\n const headline = d.headline as Record<string, unknown> | undefined;\n if (!headline || typeof headline !== 'object') {\n problems.push('derive.headline must be an object');\n } else {\n if (typeof headline.parked !== 'string') problems.push('derive.headline.parked must be a string');\n if (typeof headline.blocked !== 'string') problems.push('derive.headline.blocked must be a string');\n }\n\n return problems;\n}\n\nexport function validateDeriveConfig(\n derive: DeriveConfig,\n statusConfig: { statuses: Array<{ id: string }> },\n validateWhen: (when: string) => string | null = () => null,\n): string[] {\n const problems: string[] = [];\n const ids = new Set(statusConfig.statuses.map((s) => s.id));\n\n if (derive.phaseLadder.length === 0) {\n problems.push('phaseLadder must have at least one rung');\n }\n for (const rung of derive.phaseLadder) {\n if (!ids.has(rung.phase)) {\n problems.push(`phaseLadder rung \"${rung.phase}\" is not a defined status id`);\n }\n const err = rung.when === '*' ? null : validateWhen(rung.when);\n if (err) problems.push(`phaseLadder rung \"${rung.phase}\": invalid condition — ${err}`);\n }\n const VALID_DISPOSITIONS = new Set(['active', 'blocked', 'parked']);\n for (const rule of derive.disposition) {\n if (!VALID_DISPOSITIONS.has(rule.is)) {\n problems.push(\n `disposition \"${rule.is}\" is not valid (expected active, blocked, or parked — terminal is never a rule)`,\n );\n }\n if (rule.when !== null) {\n const err = validateWhen(rule.when);\n if (err) problems.push(`disposition rule \"${rule.is}\": invalid condition — ${err}`);\n }\n }\n // Disposition is first-match-wins, so the `else:` arm (when: null) must be the\n // SINGLE last rule — an else-first (or duplicate-else) config silently makes\n // every later rule unreachable.\n const elseIndices = derive.disposition\n .map((r, i) => (r.when === null ? i : -1))\n .filter((i) => i >= 0);\n if (elseIndices.length === 0) {\n problems.push('disposition rules must end with an `else:` arm (a rule with when: null)');\n } else if (elseIndices.length > 1) {\n problems.push('disposition rules must have exactly one `else:` arm (when: null)');\n } else if (elseIndices[0] !== derive.disposition.length - 1) {\n problems.push('the `else:` arm (when: null) must be the LAST disposition rule — rules after it are unreachable');\n }\n\n for (const key of ['parked', 'blocked'] as const) {\n if (!ids.has(derive.headline[key])) {\n problems.push(\n `headline.${key} → \"${derive.headline[key]}\" is not a defined status id (add the definition or run migrate-derive)`,\n );\n }\n }\n return problems;\n}\n","/**\n * AQL field registry — the curated vocabulary queries may reference.\n *\n * Seam (derived-status design v3, Piece 1): users define *conditions*\n * (queries); Syntaur defines the *fact/field set*. A new condition is a config\n * edit; a new field is a Syntaur release here.\n *\n * Browser-safe: accessors read from a plain `QueryItem` record that callers\n * (CLI loader / dashboard payload) have already materialized — never from the\n * filesystem.\n */\n\n/** The flat record a query evaluates against. */\nexport type QueryItem = Record<string, unknown>;\n\nexport type FieldKind =\n | 'enum' // case-insensitive equality (status, phase, type, …)\n | 'string' // equality, with `none` sentinel for null (assignee, project)\n | 'substring' // case-insensitive containment (title/search)\n | 'bool'\n | 'number'\n | 'ordinal' // ordered enum — supports < > (priority)\n | 'timestamp' // ISO string; comparisons vs dates and duration literals\n | 'duration' // milliseconds; comparisons vs duration-literal magnitude\n | 'list'; // membership (tags)\n\nexport interface FieldDef {\n kind: FieldKind;\n /** Read the raw value from an item. Default: direct key access by canonical name. */\n get?: (item: QueryItem) => unknown;\n /** Ordinal ordering, low → high (required for kind 'ordinal'). */\n order?: string[];\n /** Accept `field:none` as a null/empty check. */\n noneSentinel?: boolean;\n}\n\nexport type FieldRegistry = Record<string, FieldDef>;\n\nexport const PRIORITY_ORDER = ['low', 'medium', 'high', 'critical'];\n\n/**\n * Default assignment field vocabulary: core frontmatter fields (AQL design,\n * Piece 2 table) + the derived-status fact fields (derived-status design v3,\n * Piece 1). Consumers may extend or restrict (e.g. derive rules evaluate over\n * facts only).\n */\nexport const ASSIGNMENT_FIELDS: FieldRegistry = {\n // ── core fields ──────────────────────────────────────────────────────────\n status: { kind: 'enum' },\n priority: { kind: 'ordinal', order: PRIORITY_ORDER },\n type: { kind: 'enum' },\n assignee: { kind: 'string', noneSentinel: true },\n project: { kind: 'string', noneSentinel: true },\n tag: { kind: 'list', get: (i) => i['tags'] },\n tags: { kind: 'list' },\n archived: { kind: 'bool' },\n title: { kind: 'substring' },\n // `search` reads a dedicated `searchText` haystack when the item provides one\n // (so the dashboard can match title + slug + project like its filter box),\n // falling back to `title` when absent. Backward-compatible: title-only when no\n // searchText. The `title` field stays title-only.\n search: { kind: 'substring', get: (i) => i['searchText'] ?? i['title'] },\n created: { kind: 'timestamp' },\n updated: { kind: 'timestamp' },\n completedat: { kind: 'timestamp', get: (i) => i['completedAt'] },\n statusage: { kind: 'duration', get: (i) => i['statusAge'] },\n\n // ── derived-status dimensions ────────────────────────────────────────────\n phase: { kind: 'enum' },\n disposition: { kind: 'enum' },\n phaseage: { kind: 'duration', get: (i) => i['phaseAge'] },\n\n // ── objective facts ──────────────────────────────────────────────────────\n hasrealobjective: { kind: 'bool', get: (i) => i['hasRealObjective'] },\n acrealtotal: { kind: 'number', get: (i) => i['acRealTotal'] },\n acrealchecked: { kind: 'number', get: (i) => i['acRealChecked'] },\n acallchecked: { kind: 'bool', get: (i) => i['acAllChecked'] },\n planexists: { kind: 'bool', get: (i) => i['planExists'] },\n planapproved: { kind: 'bool', get: (i) => i['planApproved'] },\n workspaceset: { kind: 'bool', get: (i) => i['workspaceSet'] },\n implementationstarted: { kind: 'bool', get: (i) => i['implementationStarted'] },\n depssatisfied: { kind: 'bool', get: (i) => i['depsSatisfied'] },\n unresolvedquestions: { kind: 'number', get: (i) => i['unresolvedQuestions'] },\n progressstaledays: { kind: 'duration', get: (i) => i['progressStaleDays'] },\n\n // ── asserted facts ───────────────────────────────────────────────────────\n blocked: { kind: 'bool' },\n parked: { kind: 'bool' },\n reviewrequested: { kind: 'bool', get: (i) => i['reviewRequested'] },\n pinned: { kind: 'bool' },\n};\n\n/**\n * Field lookup is case-insensitive: registry keys are lowercase; `resolveField`\n * lowercases the query's field name. Accessors fall back to the item's\n * camelCase canonical key via `get`.\n */\nexport function resolveField(registry: FieldRegistry, name: string): FieldDef | null {\n return registry[name.toLowerCase()] ?? null;\n}\n\nexport function readField(def: FieldDef, fieldName: string, item: QueryItem): unknown {\n if (def.get) return def.get(item);\n return item[fieldName] ?? item[fieldName.toLowerCase()];\n}\n","/**\n * AQL evaluator — compiles a parsed AST into a predicate function over\n * QueryItems. Field references are validated at compile time (structured\n * errors with positions); evaluation is pure.\n *\n * Time semantics (AQL design, \"Duration literals\"):\n * - vs a TIMESTAMP field, a duration literal is a relative point in time:\n * `created > -36h` ⇒ created after (now − 36h). A bare duration (`36h`)\n * means \"ago\" (same as `-36h`).\n * - vs a DURATION field, a duration literal is a magnitude (sign ignored):\n * `statusAge > 3d` ⇒ in current status longer than 3 days.\n * - Absolute dates compare on LOCAL-day boundaries (consistent with the\n * dashboard's matchesDateRange).\n *\n * Browser-safe (no Node APIs). `now` is injected via EvalContext — never read\n * from Date.now() here — so evaluation is deterministic and testable.\n */\n\nimport type { AtomNode, QueryError, QueryNode, QueryValue } from './ast.js';\nimport { readField, resolveField, type FieldDef, type FieldRegistry, type QueryItem } from './fields.js';\n\nexport interface EvalContext {\n /** Epoch ms used to resolve relative duration literals. */\n now: number;\n}\n\nexport type Predicate = (item: QueryItem, ctx: EvalContext) => boolean;\n\nexport class CompileError extends Error {\n constructor(public errors: QueryError[]) {\n super(errors.map((e) => `${e.message} (at ${e.pos})`).join('; '));\n this.name = 'CompileError';\n }\n}\n\n/**\n * [startOfDay, startOfNextDay) for a YYYY-MM-DD in local time. Rejects\n * impossible calendar dates (e.g. 2026-02-30) instead of letting `new Date`\n * silently roll them over — otherwise `created:2026-02-30` would compile and\n * match March 2. Throws CompileError with the atom's position on invalid input.\n */\nfunction localDayBounds(value: { raw: string; pos: number }): [number, number] {\n const [y, m, d] = value.raw.split('-').map((n) => parseInt(n, 10));\n const start = new Date(y, m - 1, d);\n if (start.getFullYear() !== y || start.getMonth() !== m - 1 || start.getDate() !== d) {\n throw new CompileError([{ pos: value.pos, message: `Invalid date \"${value.raw}\"` }]);\n }\n const end = new Date(y, m - 1, d + 1).getTime();\n return [start.getTime(), end];\n}\n\nfunction toEpoch(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'string' && value.length > 0) {\n const t = Date.parse(value);\n return Number.isNaN(t) ? null : t;\n }\n return null;\n}\n\nfunction toNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\nfunction ciEquals(a: unknown, b: string): boolean {\n return typeof a === 'string' && a.toLowerCase() === b.toLowerCase();\n}\n\nfunction isNone(value: unknown): boolean {\n return value === null || value === undefined || value === '';\n}\n\nfunction compileEquality(def: FieldDef, field: string, value: QueryValue, atomPos: number): Predicate {\n switch (def.kind) {\n case 'enum':\n case 'string':\n if (def.noneSentinel && value.raw.toLowerCase() === 'none') {\n return (item) => isNone(readField(def, field, item));\n }\n return (item) => ciEquals(readField(def, field, item), value.raw);\n case 'substring':\n return (item) => {\n const v = readField(def, field, item);\n return typeof v === 'string' && v.toLowerCase().includes(value.raw.toLowerCase());\n };\n case 'bool': {\n const want = value.raw.toLowerCase();\n if (want !== 'true' && want !== 'false') {\n throw new CompileError([\n { pos: value.pos, message: `Field \"${field}\" is boolean — use ${field}:true or ${field}:false` },\n ]);\n }\n const expected = want === 'true';\n return (item) => {\n // Strict-ish coercion: real booleans pass through; the strings\n // 'true'/'false' parse (frontmatter scalars); null/undefined/'' mean\n // false (an absent fact is false). Anything else never matches —\n // Boolean('false') === true was the bug here.\n const v = readField(def, field, item);\n const b =\n typeof v === 'boolean'\n ? v\n : v === 'true'\n ? true\n : v === 'false' || v === null || v === undefined || v === ''\n ? false\n : null;\n return b !== null && b === expected;\n };\n }\n case 'number': {\n const n = value.num ?? toNumber(value.raw);\n if (n === null) {\n throw new CompileError([{ pos: value.pos, message: `Field \"${field}\" is numeric — \"${value.raw}\" is not a number` }]);\n }\n return (item) => toNumber(readField(def, field, item)) === n;\n }\n case 'ordinal':\n return (item) => ciEquals(readField(def, field, item), value.raw);\n case 'list':\n return (item) => {\n const v = readField(def, field, item);\n return Array.isArray(v) && v.some((el) => ciEquals(el, value.raw));\n };\n case 'timestamp': {\n if (value.type === 'date') {\n const [start, end] = localDayBounds(value);\n return (item) => {\n const t = toEpoch(readField(def, field, item));\n return t !== null && t >= start && t < end;\n };\n }\n throw new CompileError([\n { pos: value.pos, message: `Field \"${field}\" is a timestamp — use a comparison (e.g. ${field} > -36h) or an absolute date (${field}:2026-06-01)` },\n ]);\n }\n case 'duration':\n throw new CompileError([\n { pos: atomPos, message: `Field \"${field}\" is a duration — use a comparison (e.g. ${field} > 3d)` },\n ]);\n }\n}\n\nfunction compileComparison(def: FieldDef, field: string, op: string, value: QueryValue): Predicate {\n const cmp = (a: number, b: number): boolean => {\n switch (op) {\n case '<':\n return a < b;\n case '>':\n return a > b;\n case '<=':\n return a <= b;\n case '>=':\n return a >= b;\n case '=':\n return a === b;\n case '!=':\n return a !== b;\n default:\n return false;\n }\n };\n\n switch (def.kind) {\n case 'number': {\n const n = value.num ?? toNumber(value.raw);\n if (n === null) {\n throw new CompileError([{ pos: value.pos, message: `\"${value.raw}\" is not a number (field \"${field}\")` }]);\n }\n return (item) => {\n const v = toNumber(readField(def, field, item));\n return v !== null && cmp(v, n);\n };\n }\n case 'ordinal': {\n const order = def.order ?? [];\n const idx = order.findIndex((o) => o.toLowerCase() === value.raw.toLowerCase());\n if (idx < 0) {\n throw new CompileError([\n { pos: value.pos, message: `\"${value.raw}\" is not a valid ${field} (expected one of: ${order.join(', ')})` },\n ]);\n }\n return (item) => {\n const raw = readField(def, field, item);\n const vIdx = typeof raw === 'string' ? order.findIndex((o) => o.toLowerCase() === raw.toLowerCase()) : -1;\n return vIdx >= 0 && cmp(vIdx, idx);\n };\n }\n case 'timestamp': {\n if (value.type === 'duration') {\n // relative point in time: bare durations mean \"ago\"\n const sign = value.sign === 0 ? -1 : (value.sign ?? -1);\n const offset = sign * (value.num ?? 0);\n return (item, ctx) => {\n const t = toEpoch(readField(def, field, item));\n return t !== null && cmp(t, ctx.now + offset);\n };\n }\n if (value.type === 'date') {\n const [start, end] = localDayBounds(value);\n return (item) => {\n const t = toEpoch(readField(def, field, item));\n if (t === null) return false;\n switch (op) {\n case '<':\n return t < start;\n case '<=':\n return t < end;\n case '>':\n return t >= end;\n case '>=':\n return t >= start;\n case '=':\n return t >= start && t < end;\n case '!=':\n return t < start || t >= end;\n default:\n return false;\n }\n };\n }\n throw new CompileError([\n { pos: value.pos, message: `Compare timestamp field \"${field}\" to a duration (e.g. -36h) or a date (YYYY-MM-DD)` },\n ]);\n }\n case 'duration': {\n if (value.type !== 'duration') {\n throw new CompileError([\n { pos: value.pos, message: `Compare duration field \"${field}\" to a duration literal (e.g. 3d)` },\n ]);\n }\n const magnitude = value.num ?? 0; // sign ignored: magnitudes have no direction\n return (item) => {\n const v = toNumber(readField(def, field, item));\n return v !== null && cmp(v, magnitude);\n };\n }\n case 'enum':\n case 'string':\n case 'substring':\n case 'list': {\n if (op === '=' ) {\n return compileEquality(def, field, value, value.pos);\n }\n if (op === '!=') {\n const eq = compileEquality(def, field, value, value.pos);\n return (item, ctx) => !eq(item, ctx);\n }\n throw new CompileError([\n { pos: value.pos, message: `Field \"${field}\" does not support ordering comparisons (use \":\" or \"=\").` },\n ]);\n }\n case 'bool': {\n if (op === '=' || op === '!=') {\n const eq = compileEquality(def, field, value, value.pos);\n return op === '=' ? eq : (item, ctx) => !eq(item, ctx);\n }\n throw new CompileError([{ pos: value.pos, message: `Field \"${field}\" is boolean — use ${field}:true / ${field}:false` }]);\n }\n }\n}\n\nfunction compileAtom(atom: AtomNode, registry: FieldRegistry): Predicate {\n const def = resolveField(registry, atom.field);\n if (!def) {\n throw new CompileError([{ pos: atom.pos, message: `Unknown field \"${atom.field}\"` }]);\n }\n if (atom.op === ':') {\n // IN-list: OR of equalities\n const preds = atom.values.map((v) => compileEquality(def, atom.field, v, atom.pos));\n if (preds.length === 1) return preds[0];\n return (item, ctx) => preds.some((p) => p(item, ctx));\n }\n return compileComparison(def, atom.field, atom.op, atom.values[0]);\n}\n\nexport function compileNode(node: QueryNode, registry: FieldRegistry): Predicate {\n switch (node.kind) {\n case 'all':\n return () => true;\n case 'atom':\n return compileAtom(node, registry);\n case 'not': {\n const inner = compileNode(node.child, registry);\n return (item, ctx) => !inner(item, ctx);\n }\n case 'and': {\n const preds = node.children.map((c) => compileNode(c, registry));\n return (item, ctx) => preds.every((p) => p(item, ctx));\n }\n case 'or': {\n const preds = node.children.map((c) => compileNode(c, registry));\n return (item, ctx) => preds.some((p) => p(item, ctx));\n }\n }\n}\n","/**\n * AQL lexer. Tokenizes a query string; positions are byte offsets for\n * structured errors. Browser-safe (no Node APIs).\n */\n\nexport type TokenType =\n | 'IDENT'\n | 'STRING'\n | 'NUMBER'\n | 'DATE'\n | 'DURATION'\n | 'COLON'\n | 'LPAREN'\n | 'RPAREN'\n | 'COMMA'\n | 'OP' // < > <= >= = !=\n | 'MINUS' // negation prefix (`-field:value`)\n | 'STAR'\n | 'AND'\n | 'OR'\n | 'NOT'\n | 'EOF';\n\nexport interface Token {\n type: TokenType;\n /** Source text (for STRING: the unquoted contents). */\n text: string;\n pos: number;\n /** DURATION: magnitude in ms. NUMBER: numeric value. */\n num?: number;\n /** DURATION sign: -1, +1, or 0 (bare). */\n sign?: -1 | 0 | 1;\n}\n\nexport class LexError extends Error {\n constructor(\n public pos: number,\n message: string,\n ) {\n super(message);\n this.name = 'LexError';\n }\n}\n\n/** Duration unit → milliseconds. `m` ≈ 30d (month), `mo` explicit month, `y` ≈ 365d. */\nconst DURATION_MS: Record<string, number> = {\n h: 3_600_000,\n d: 86_400_000,\n w: 7 * 86_400_000,\n m: 30 * 86_400_000,\n mo: 30 * 86_400_000,\n y: 365 * 86_400_000,\n};\n\nconst IDENT_START = /[A-Za-z_]/;\nconst IDENT_CHAR = /[A-Za-z0-9_-]/;\n// Anchored so a trailing digit/dash can't be swallowed into a \"date\" (e.g.\n// `2026-06-1623` must NOT lex as DATE `2026-06-16` + `23`). Calendar validity\n// (month/day ranges) is enforced later in the evaluator with positional errors.\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}(?![\\d-])/;\n\nexport function lex(input: string): Token[] {\n const tokens: Token[] = [];\n let i = 0;\n\n const numberOrDuration = (start: number, sign: -1 | 0 | 1): Token => {\n let j = i;\n while (j < input.length && /\\d/.test(input[j])) j++;\n const digits = input.slice(i, j);\n // unit suffix?\n let unit = '';\n while (j < input.length && /[a-z]/i.test(input[j])) {\n unit += input[j];\n j++;\n }\n i = j;\n if (unit.length > 0) {\n const ms = DURATION_MS[unit.toLowerCase()];\n if (ms === undefined) {\n throw new LexError(start, `Unknown duration unit \"${unit}\" (expected h, d, w, m, mo, or y)`);\n }\n return {\n type: 'DURATION',\n text: input.slice(start, j),\n pos: start,\n num: parseInt(digits, 10) * ms,\n sign,\n };\n }\n if (sign !== 0) {\n // signed bare number — only meaningful as a duration; treat as number with sign applied\n return { type: 'NUMBER', text: input.slice(start, j), pos: start, num: sign * parseInt(digits, 10) };\n }\n return { type: 'NUMBER', text: digits, pos: start, num: parseInt(digits, 10) };\n };\n\n while (i < input.length) {\n const c = input[i];\n const start = i;\n\n if (c === ' ' || c === '\\t' || c === '\\n' || c === '\\r') {\n i++;\n continue;\n }\n if (c === '(') {\n tokens.push({ type: 'LPAREN', text: c, pos: start });\n i++;\n continue;\n }\n if (c === ')') {\n tokens.push({ type: 'RPAREN', text: c, pos: start });\n i++;\n continue;\n }\n if (c === ',') {\n tokens.push({ type: 'COMMA', text: c, pos: start });\n i++;\n continue;\n }\n if (c === ':') {\n tokens.push({ type: 'COLON', text: c, pos: start });\n i++;\n continue;\n }\n if (c === '*') {\n tokens.push({ type: 'STAR', text: c, pos: start });\n i++;\n continue;\n }\n if (c === '<' || c === '>') {\n if (input[i + 1] === '=') {\n tokens.push({ type: 'OP', text: c + '=', pos: start });\n i += 2;\n } else {\n tokens.push({ type: 'OP', text: c, pos: start });\n i++;\n }\n continue;\n }\n if (c === '!') {\n if (input[i + 1] === '=') {\n tokens.push({ type: 'OP', text: '!=', pos: start });\n i += 2;\n continue;\n }\n throw new LexError(start, `Unexpected \"!\" (did you mean \"!=\"?)`);\n }\n if (c === '=') {\n // accept both `=` and `==`\n i += input[i + 1] === '=' ? 2 : 1;\n tokens.push({ type: 'OP', text: '=', pos: start });\n continue;\n }\n if (c === '\"' || c === \"'\") {\n const quote = c;\n let j = i + 1;\n let out = '';\n while (j < input.length && input[j] !== quote) {\n if (input[j] === '\\\\' && j + 1 < input.length) {\n out += input[j + 1];\n j += 2;\n } else {\n out += input[j];\n j++;\n }\n }\n if (j >= input.length) throw new LexError(start, 'Unterminated string literal');\n tokens.push({ type: 'STRING', text: out, pos: start });\n i = j + 1;\n continue;\n }\n if (c === '-' || c === '+') {\n if (/\\d/.test(input[i + 1] ?? '')) {\n const sign = c === '-' ? -1 : 1;\n i++;\n tokens.push(numberOrDuration(start, sign));\n continue;\n }\n if (c === '-') {\n tokens.push({ type: 'MINUS', text: '-', pos: start });\n i++;\n continue;\n }\n throw new LexError(start, 'Unexpected \"+\"');\n }\n if (/\\d/.test(c)) {\n const dateMatch = input.slice(i).match(DATE_RE);\n if (dateMatch) {\n tokens.push({ type: 'DATE', text: dateMatch[0], pos: start });\n i += dateMatch[0].length;\n continue;\n }\n tokens.push(numberOrDuration(start, 0));\n continue;\n }\n if (IDENT_START.test(c)) {\n let j = i + 1;\n while (j < input.length && IDENT_CHAR.test(input[j])) j++;\n const word = input.slice(i, j);\n const kw = word.toLowerCase();\n if (kw === 'and') tokens.push({ type: 'AND', text: word, pos: start });\n else if (kw === 'or') tokens.push({ type: 'OR', text: word, pos: start });\n else if (kw === 'not') tokens.push({ type: 'NOT', text: word, pos: start });\n else tokens.push({ type: 'IDENT', text: word, pos: start });\n i = j;\n continue;\n }\n throw new LexError(start, `Unexpected character \"${c}\"`);\n }\n\n tokens.push({ type: 'EOF', text: '', pos: input.length });\n return tokens;\n}\n","/**\n * AQL recursive-descent parser.\n *\n * Grammar (precedence NOT > AND > OR; implicit AND between adjacent terms):\n * query := orExpr EOF | EOF (empty input → match-all)\n * orExpr := andExpr (OR andExpr)*\n * andExpr := unary ((AND)? unary)* (implicit AND)\n * unary := NOT unary | MINUS atom | primary\n * primary := LPAREN orExpr RPAREN | STAR | atom\n * atom := IDENT (COLON valueOrList | OP value)\n * valueOrList := LPAREN value (COMMA value)* RPAREN | value\n * value := IDENT | STRING | NUMBER | DATE | DURATION\n *\n * Browser-safe (no Node APIs).\n */\n\nimport type { AtomNode, ComparisonOp, QueryError, QueryNode, QueryValue } from './ast.js';\nimport { lex, LexError, type Token, type TokenType } from './lexer.js';\n\nexport class ParseError extends Error {\n constructor(\n public pos: number,\n message: string,\n ) {\n super(message);\n this.name = 'ParseError';\n }\n}\n\nconst VALUE_TOKENS: ReadonlySet<TokenType> = new Set(['IDENT', 'STRING', 'NUMBER', 'DATE', 'DURATION']);\n/** Tokens that can begin a term — used to detect implicit AND. */\nconst TERM_START: ReadonlySet<TokenType> = new Set(['IDENT', 'NOT', 'MINUS', 'LPAREN', 'STAR']);\n\nclass Parser {\n private pos = 0;\n\n constructor(private tokens: Token[]) {}\n\n private peek(): Token {\n return this.tokens[this.pos];\n }\n\n private next(): Token {\n return this.tokens[this.pos++];\n }\n\n private expect(type: TokenType, what: string): Token {\n const tok = this.peek();\n if (tok.type !== type) {\n throw new ParseError(tok.pos, `Expected ${what}, got \"${tok.text || tok.type}\"`);\n }\n return this.next();\n }\n\n parseQuery(): QueryNode {\n if (this.peek().type === 'EOF') return { kind: 'all' };\n const node = this.orExpr();\n const tok = this.peek();\n if (tok.type !== 'EOF') {\n throw new ParseError(tok.pos, `Unexpected \"${tok.text}\" — unbalanced parentheses or stray token`);\n }\n return node;\n }\n\n private orExpr(): QueryNode {\n const children = [this.andExpr()];\n while (this.peek().type === 'OR') {\n this.next();\n children.push(this.andExpr());\n }\n return children.length === 1 ? children[0] : { kind: 'or', children };\n }\n\n private andExpr(): QueryNode {\n const children = [this.unary()];\n for (;;) {\n const tok = this.peek();\n if (tok.type === 'AND') {\n this.next();\n children.push(this.unary());\n } else if (TERM_START.has(tok.type)) {\n // implicit AND: adjacent terms\n children.push(this.unary());\n } else {\n break;\n }\n }\n return children.length === 1 ? children[0] : { kind: 'and', children };\n }\n\n private unary(): QueryNode {\n const tok = this.peek();\n if (tok.type === 'NOT') {\n this.next();\n return { kind: 'not', child: this.unary() };\n }\n if (tok.type === 'MINUS') {\n this.next();\n // `-field:value` sugar — minus binds to a single atom\n const inner = this.peek();\n if (inner.type !== 'IDENT') {\n throw new ParseError(inner.pos, 'Expected a field atom after \"-\" negation');\n }\n return { kind: 'not', child: this.atom() };\n }\n return this.primary();\n }\n\n private primary(): QueryNode {\n const tok = this.peek();\n if (tok.type === 'LPAREN') {\n this.next();\n const node = this.orExpr();\n this.expect('RPAREN', '\")\"');\n return node;\n }\n if (tok.type === 'STAR') {\n this.next();\n return { kind: 'all' };\n }\n if (tok.type === 'IDENT') {\n return this.atom();\n }\n throw new ParseError(tok.pos, `Expected a field, \"(\", \"*\", or NOT — got \"${tok.text || 'end of query'}\"`);\n }\n\n private atom(): AtomNode {\n const fieldTok = this.expect('IDENT', 'a field name');\n const opTok = this.peek();\n\n if (opTok.type === 'COLON') {\n this.next();\n const values = this.valueOrList();\n return { kind: 'atom', field: fieldTok.text, op: ':', values, pos: fieldTok.pos };\n }\n if (opTok.type === 'OP') {\n this.next();\n const value = this.value();\n return {\n kind: 'atom',\n field: fieldTok.text,\n op: opTok.text as ComparisonOp,\n values: [value],\n pos: fieldTok.pos,\n };\n }\n throw new ParseError(\n opTok.pos,\n `Expected \":\" or a comparison operator after field \"${fieldTok.text}\"`,\n );\n }\n\n private valueOrList(): QueryValue[] {\n if (this.peek().type === 'LPAREN') {\n this.next();\n const values = [this.value()];\n while (this.peek().type === 'COMMA') {\n this.next();\n values.push(this.value());\n }\n this.expect('RPAREN', '\")\" to close the value list');\n return values;\n }\n return [this.value()];\n }\n\n private value(): QueryValue {\n const tok = this.peek();\n if (!VALUE_TOKENS.has(tok.type)) {\n throw new ParseError(tok.pos, `Expected a value, got \"${tok.text || tok.type}\"`);\n }\n this.next();\n switch (tok.type) {\n case 'STRING':\n return { type: 'string', raw: tok.text, pos: tok.pos };\n case 'NUMBER':\n return { type: 'number', raw: tok.text, num: tok.num, pos: tok.pos };\n case 'DATE':\n return { type: 'date', raw: tok.text, pos: tok.pos };\n case 'DURATION':\n return { type: 'duration', raw: tok.text, num: tok.num, sign: tok.sign ?? 0, pos: tok.pos };\n default:\n return { type: 'word', raw: tok.text, pos: tok.pos };\n }\n }\n}\n\n/** Parse a query string. Returns the AST or structured errors (never throws). */\nexport function parseQuery(input: string): { ast: QueryNode; errors: [] } | { ast: null; errors: QueryError[] } {\n try {\n const tokens = lex(input);\n const ast = new Parser(tokens).parseQuery();\n return { ast, errors: [] };\n } catch (err) {\n if (err instanceof LexError || err instanceof ParseError) {\n return { ast: null, errors: [{ pos: err.pos, message: err.message }] };\n }\n throw err;\n }\n}\n","/**\n * AQL — Assignment Query Language. Public surface.\n *\n * One engine, many consumers: derive rules (phase ladder / disposition),\n * `syntaur ls --query`, and dashboard filters all share this module.\n * Browser-safe: no Node-only imports anywhere under `src/utils/query/`.\n */\n\nimport type { QueryError, QueryNode } from './ast.js';\nimport { compileNode, CompileError, type EvalContext, type Predicate } from './evaluate.js';\nimport { ASSIGNMENT_FIELDS, type FieldRegistry, type QueryItem } from './fields.js';\nimport { parseQuery } from './parser.js';\n\nexport type { QueryError, QueryNode, ComparisonOp } from './ast.js';\nexport { lex, LexError } from './lexer.js';\nexport type { Token, TokenType } from './lexer.js';\nexport { parseQuery, ParseError } from './parser.js';\nexport { compileNode, CompileError } from './evaluate.js';\nexport type { EvalContext, Predicate } from './evaluate.js';\nexport {\n ASSIGNMENT_FIELDS,\n PRIORITY_ORDER,\n resolveField,\n readField,\n} from './fields.js';\nexport type { FieldDef, FieldKind, FieldRegistry, QueryItem } from './fields.js';\n\nexport interface CompiledQuery {\n predicate: Predicate;\n ast: QueryNode;\n}\n\n/**\n * Parse + compile a query against a field registry. Returns the compiled\n * predicate or structured errors (never throws on user input).\n */\nexport function compileQuery(\n input: string,\n registry: FieldRegistry = ASSIGNMENT_FIELDS,\n): { query: CompiledQuery; errors: [] } | { query: null; errors: QueryError[] } {\n const parsed = parseQuery(input);\n if (!parsed.ast) return { query: null, errors: parsed.errors };\n try {\n const predicate = compileNode(parsed.ast, registry);\n return { query: { predicate, ast: parsed.ast }, errors: [] };\n } catch (err) {\n if (err instanceof CompileError) return { query: null, errors: err.errors };\n throw err;\n }\n}\n\n/** Validate a query (parse + field check) without evaluating — for doctor/config checks. */\nexport function validateQuery(input: string, registry: FieldRegistry = ASSIGNMENT_FIELDS): QueryError[] {\n return compileQuery(input, registry).errors;\n}\n\n/** Convenience: filter a list of items with a compiled query. */\nexport function runQuery(items: QueryItem[], compiled: CompiledQuery, ctx: EvalContext): QueryItem[] {\n return items.filter((item) => compiled.predicate(item, ctx));\n}\n","/**\n * Browser-safe fact-vocabulary module.\n *\n * Extracted for the same reason as `saved-view-builder.ts`: both the dashboard\n * (Vite/browser build, `@shared/*` alias) and Node-side modules need these\n * definitions. The ONLY import here is from `./query/index.js` — no\n * Node-coupled modules (no `config.ts`, no `fs`, no `path`).\n *\n * Consumers (`src/lifecycle/derive.ts`, `src/utils/config.ts`) re-export\n * everything from here so no existing import path needs to change.\n */\n\nimport { ASSIGNMENT_FIELDS, type FieldRegistry } from './query/index.js';\n\n// ── re-exported type (lives in config.ts; declared here for browser consumers) ──\n\n/**\n * A VALIDATED custom-fact declaration (strict union). bool/number facts are\n * asserted values stored in the `facts:` frontmatter map; attestation facts\n * model \"agent reviewed revision with verdict\" and carry a revision binding.\n *\n * Re-declared here (mirroring `config.ts`) so the dashboard can import this\n * type without pulling in any Node-only module.\n */\nexport type FactDeclaration =\n | { name: string; type: 'bool' | 'number' }\n | { name: string; type: 'attestation'; binds: 'plan' | 'commit' | 'none' };\n\n// ── DERIVE_FIELDS ─────────────────────────────────────────────────────────────\n\n/**\n * Registry for derive conditions: facts only. Deliberately excludes\n * timestamps/durations (statusAge, created, …) and identity fields — a derive\n * rule referencing them fails validation, implementing \"time-based facts are\n * payload-only flags\" with teeth.\n */\nexport const DERIVE_FIELDS: FieldRegistry = {\n hasrealobjective: { kind: 'bool', get: (i) => i['hasRealObjective'] },\n acrealtotal: { kind: 'number', get: (i) => i['acRealTotal'] },\n acrealchecked: { kind: 'number', get: (i) => i['acRealChecked'] },\n acallchecked: { kind: 'bool', get: (i) => i['acAllChecked'] },\n planexists: { kind: 'bool', get: (i) => i['planExists'] },\n planapproved: { kind: 'bool', get: (i) => i['planApproved'] },\n workspaceset: { kind: 'bool', get: (i) => i['workspaceSet'] },\n implementationstarted: { kind: 'bool', get: (i) => i['implementationStarted'] },\n depssatisfied: { kind: 'bool', get: (i) => i['depsSatisfied'] },\n unresolvedquestions: { kind: 'number', get: (i) => i['unresolvedQuestions'] },\n blocked: { kind: 'bool' },\n parked: { kind: 'bool' },\n reviewrequested: { kind: 'bool', get: (i) => i['reviewRequested'] },\n pinned: { kind: 'bool' },\n};\n\n// ── FactFieldNames ─────────────────────────────────────────────────────────────\n\n/** Canonical export/registry names for one fact declaration. */\nexport interface FactFieldNames {\n /** Storage key in the `facts:` map = declared name verbatim. */\n storageKey: string;\n /** camelCase exported fact keys (attestations use all five; bool/number\n * only use `fact`). */\n exports: {\n fact: string;\n approved: string;\n changesRequested: string;\n by: string;\n approvedBy: string;\n };\n /** Lowercased registry keys this declaration contributes (1 for bool/number,\n * 5 for attestation) — the collision unit. */\n registryKeys: string[];\n}\n\n/**\n * THE one canonical naming helper (Locked Decisions): every consumer derives\n * fact field names here so no path invents its own variant. For bool/number\n * the single export is `<name>`; for attestation the five exports are `<name>`,\n * `<name>Approved`, `<name>ChangesRequested`, `<name>By`, `<name>ApprovedBy`.\n */\nexport function factFieldNames(decl: FactDeclaration): FactFieldNames {\n const name = decl.name;\n const exportNames = {\n fact: name,\n approved: `${name}Approved`,\n changesRequested: `${name}ChangesRequested`,\n by: `${name}By`,\n approvedBy: `${name}ApprovedBy`,\n };\n const registryKeys =\n decl.type === 'attestation'\n ? [\n exportNames.fact,\n exportNames.approved,\n exportNames.changesRequested,\n exportNames.by,\n exportNames.approvedBy,\n ].map((k) => k.toLowerCase())\n : [exportNames.fact.toLowerCase()];\n return { storageKey: name, exports: exportNames, registryKeys };\n}\n\n// ── RawFactDeclaration ─────────────────────────────────────────────────────────\n\n/**\n * A custom-fact declaration EXACTLY as parsed from `statuses.facts` — loose\n * parse (Locked Decisions): every field is a raw string so user input\n * round-trips through serialization even when invalid. The strict\n * {@link FactDeclaration} is derived from this via {@link normalizeFactDeclarations}\n * (defined in `config.ts`, which imports this type).\n */\nexport interface RawFactDeclaration {\n name: string;\n type: string;\n binds: string | null;\n}\n\n// ── normalizeFactDeclarations ─────────────────────────────────────────────────\n\n/**\n * Narrow raw declarations to the strict union, DROPPING malformed rows (bad\n * name format / unknown type / invalid binds) — never throws. The single\n * bridge every consumer crosses before the collision filter\n * (`acceptFactDeclarations`). `validateFactDeclarations` diagnoses the raw rows\n * so doctor can report exactly what this drops.\n *\n * Lives here (browser-safe) so both the Node side and the dashboard client can\n * run the identical normalize→accept pipeline; re-exported from `config.ts` so\n * existing Node-side imports from `config.js` keep resolving.\n */\nexport function normalizeFactDeclarations(\n raw: RawFactDeclaration[] | null | undefined,\n): FactDeclaration[] {\n const out: FactDeclaration[] = [];\n for (const row of raw ?? []) {\n if (!row || typeof row.name !== 'string') continue;\n const name = row.name.trim();\n if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) continue;\n const type = (row.type ?? '').trim();\n if (type === 'bool' || type === 'number') {\n out.push({ name, type });\n } else if (type === 'attestation') {\n const binds = (row.binds ?? 'none').toString().trim() || 'none';\n if (binds === 'plan' || binds === 'commit' || binds === 'none') {\n out.push({ name, type: 'attestation', binds });\n }\n }\n }\n return out;\n}\n\n// ── validateFactDeclarations ──────────────────────────────────────────────────\n\n/**\n * Diagnose RAW fact declarations: returns one human-readable problem\n * per malformed/colliding row — exactly what the normalize→accept pipeline will\n * drop, so doctor reports nothing silently. Checks name format, type/binds, and\n * case-insensitive collision of EVERY exported key (the declared name and, for\n * attestations, all four generated names) against `DERIVE_FIELDS`,\n * `ASSIGNMENT_FIELDS`, or an earlier declaration's exported keys. Works with\n * `derive: null` — declarations validate independently of derive rules.\n *\n * KEPT IN LOCKSTEP with {@link acceptFactDeclarations} so doctor reports\n * precisely what the runtime drops. If you change collision semantics here,\n * change them there.\n */\nexport function validateFactDeclarations(raw: RawFactDeclaration[]): string[] {\n const problems: string[] = [];\n // Owner of each lowercased key: 'built-in' or the declaring fact name. Mirrors\n // acceptFactDeclarations EXACTLY so doctor reports precisely what the runtime\n // drops — atomic per declaration (built-ins always win, first-declared wins),\n // and a declaration's keys are reserved ONLY when the whole declaration is\n // accepted (a built-in-collided declaration reserves nothing, so a later\n // declaration the runtime accepts is not falsely flagged).\n const owners = new Map<string, string>(); // lowercased key → 'built-in' | <fact name>\n for (const key of Object.keys(DERIVE_FIELDS)) owners.set(key, 'built-in');\n for (const key of Object.keys(ASSIGNMENT_FIELDS)) owners.set(key, 'built-in');\n\n for (const row of raw ?? []) {\n const name = (row?.name ?? '').trim();\n if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) {\n problems.push(\n `fact \"${row?.name ?? ''}\": invalid name — must match /^[a-z][a-zA-Z0-9]*$/`,\n );\n continue;\n }\n const type = (row.type ?? '').trim();\n if (type !== 'bool' && type !== 'number' && type !== 'attestation') {\n problems.push(\n `fact \"${name}\": invalid type \"${row.type ?? ''}\" — expected bool, number, or attestation`,\n );\n continue;\n }\n if (type === 'attestation') {\n const binds = (row.binds ?? 'none').toString().trim() || 'none';\n if (binds !== 'plan' && binds !== 'commit' && binds !== 'none') {\n problems.push(\n `fact \"${name}\": invalid binds \"${row.binds}\" — expected plan, commit, or none`,\n );\n continue;\n }\n }\n // Collision check across ALL exported keys (binds is irrelevant to names).\n const decl: FactDeclaration =\n type === 'attestation' ? { name, type, binds: 'none' } : { name, type };\n const keys = factFieldNames(decl).registryKeys;\n const collidingKey = keys.find((k) => owners.has(k));\n if (collidingKey !== undefined) {\n const owner = owners.get(collidingKey)!;\n if (owner === 'built-in') {\n problems.push(`fact \"${name}\": exported field \"${collidingKey}\" collides with a built-in field`);\n } else if (owner === name) {\n problems.push(`fact \"${name}\": duplicate declaration (a fact named \"${name}\" is already declared)`);\n } else {\n problems.push(`fact \"${name}\": exported field \"${collidingKey}\" collides with fact \"${owner}\"`);\n }\n continue; // atomic: reserve NOTHING for a rejected declaration\n }\n for (const key of keys) owners.set(key, name);\n }\n return problems;\n}\n\n// ── acceptFactDeclarations ────────────────────────────────────────────────────\n\n/**\n * THE one collision filter (Locked Decisions): drop any declaration whose\n * registry keys collide (case-insensitively) with a built-in field\n * (`DERIVE_FIELDS` ∪ `ASSIGNMENT_FIELDS`) or an earlier-accepted declaration.\n * Built-ins always win; first-declared wins among duplicates. Never throws — a\n * bad config can't brick recompute; doctor (Task 4) surfaces the same collisions\n * as errors. Returns the ACCEPTED list every consumer builds from.\n *\n * KEPT IN LOCKSTEP with {@link validateFactDeclarations}.\n */\nexport function acceptFactDeclarations(declarations: FactDeclaration[]): FactDeclaration[] {\n // DERIVE_FIELDS / ASSIGNMENT_FIELDS keys are already lowercase.\n const taken = new Set<string>([\n ...Object.keys(DERIVE_FIELDS),\n ...Object.keys(ASSIGNMENT_FIELDS),\n ]);\n const accepted: FactDeclaration[] = [];\n for (const decl of declarations) {\n const keys = factFieldNames(decl).registryKeys;\n if (keys.some((k) => taken.has(k))) continue; // collision — drop\n for (const k of keys) taken.add(k);\n accepted.push(decl);\n }\n return accepted;\n}\n\n// ── addFactFields ─────────────────────────────────────────────────────────────\n\n/** Add one accepted declaration's fields to a registry (shared by derive +\n * query registry builders so both speak the identical vocabulary). */\nexport function addFactFields(registry: FieldRegistry, decl: FactDeclaration): void {\n const names = factFieldNames(decl);\n if (decl.type === 'attestation') {\n registry[names.exports.fact.toLowerCase()] = { kind: 'bool', get: (i) => i[names.exports.fact] };\n registry[names.exports.approved.toLowerCase()] = {\n kind: 'bool',\n get: (i) => i[names.exports.approved],\n };\n registry[names.exports.changesRequested.toLowerCase()] = {\n kind: 'bool',\n get: (i) => i[names.exports.changesRequested],\n };\n // actor sets register as `list` — `:` equality + IN lists already have\n // contains semantics there (query/fields.ts kind 'list').\n registry[names.exports.by.toLowerCase()] = { kind: 'list', get: (i) => i[names.exports.by] };\n registry[names.exports.approvedBy.toLowerCase()] = {\n kind: 'list',\n get: (i) => i[names.exports.approvedBy],\n };\n } else {\n registry[names.exports.fact.toLowerCase()] = {\n kind: decl.type,\n get: (i) => i[names.exports.fact],\n };\n }\n}\n\n// ── buildDeriveRegistry / buildQueryRegistry ──────────────────────────────────\n\n/**\n * Build the DERIVE registry (facts only) from the ACCEPTED declaration list.\n * Callers run the normalize→accept pipeline first and build ONE registry per\n * config resolution (so the WeakMap compile cache stays warm across sweeps).\n */\nexport function buildDeriveRegistry(accepted: FactDeclaration[]): FieldRegistry {\n const registry: FieldRegistry = { ...DERIVE_FIELDS };\n for (const decl of accepted) addFactFields(registry, decl);\n return registry;\n}\n\n/**\n * Build the QUERY registry (full assignment vocabulary) from the ACCEPTED list —\n * custom entries merged over `ASSIGNMENT_FIELDS` for ls/dashboard query paths.\n * Same accepted input, same entries as {@link buildDeriveRegistry}.\n */\nexport function buildQueryRegistry(accepted: FactDeclaration[]): FieldRegistry {\n const registry: FieldRegistry = { ...ASSIGNMENT_FIELDS };\n for (const decl of accepted) addFactFields(registry, decl);\n return registry;\n}\n\n// ── queryFieldNames ───────────────────────────────────────────────────────────\n\n/**\n * The canonical camelCase field names available for AQL autocomplete in the\n * dashboard. Returns the static built-in list PLUS each declaration's exported\n * field names from `factFieldNames`.\n *\n * NOTE: the built-in list is a hand-maintained camelCase display mapping of the\n * `ASSIGNMENT_FIELDS` registry keys (which are lowercase, e.g. `completedat` →\n * `completedAt`). It is NOT derived at runtime, so it must be kept in sync with\n * `query/fields.ts` whenever a built-in queryable field is added or renamed.\n */\nexport function queryFieldNames(declarations: FactDeclaration[]): string[] {\n // Hand-maintained camelCase display names for the lowercase ASSIGNMENT_FIELDS\n // registry keys (fields.ts). Keep in sync with that registry.\n const builtins: string[] = [\n 'status',\n 'priority',\n 'type',\n 'assignee',\n 'project',\n 'tag',\n 'tags',\n 'archived',\n 'title',\n 'search',\n 'created',\n 'updated',\n 'completedAt',\n 'statusAge',\n 'phase',\n 'disposition',\n 'phaseAge',\n 'hasRealObjective',\n 'acRealTotal',\n 'acRealChecked',\n 'acAllChecked',\n 'planExists',\n 'planApproved',\n 'workspaceSet',\n 'implementationStarted',\n 'depsSatisfied',\n 'unresolvedQuestions',\n 'progressStaleDays',\n 'blocked',\n 'parked',\n 'reviewRequested',\n 'pinned',\n ];\n\n const custom: string[] = [];\n for (const decl of declarations) {\n const names = factFieldNames(decl);\n if (decl.type === 'attestation') {\n custom.push(\n names.exports.fact,\n names.exports.approved,\n names.exports.changesRequested,\n names.exports.by,\n names.exports.approvedBy,\n );\n } else {\n custom.push(names.exports.fact);\n }\n }\n\n return [...builtins, ...custom];\n}\n","/**\n * Search-config schema — the shape behind the `search:` block in\n * `~/.syntaur/config.md` and the command palette's customizable behavior.\n *\n * Browser-safe: no Node-only imports. Consumed by both the server\n * (`src/dashboard/api-search-config.ts`, `src/utils/config.ts`) and the SPA\n * (via the `@shared/search-schema` alias) so palette aliases, default scope, and\n * external-ID indexing validate against ONE source of truth.\n *\n * See `claude-info/plans/2026-06-15-command-palette-ui-design.md`.\n */\n\n/** The five searchable entity kinds an alias prefix can target. */\nexport type EntityKind = 'assignment' | 'project' | 'todo' | 'server' | 'playbook';\n\nexport const ENTITY_KINDS: readonly EntityKind[] = [\n 'assignment',\n 'project',\n 'todo',\n 'server',\n 'playbook',\n];\n\n/** Default search scope: `all` (everything) or one entity kind. */\nexport type DefaultScope = 'all' | EntityKind;\n\nexport interface SearchConfig {\n /** With no explicit type prefix, inject an implicit `kind:<scope>` gate. `all` = no gate. */\n defaultScope: DefaultScope;\n /** Prefix → entity kind. Replaces the hardcoded palette `TYPE_ALIASES`. */\n aliases: Record<string, EntityKind>;\n /** Fold external IDs into the index + enable `jira:`/`externalid:`/bare-ID matching. */\n externalIds: boolean;\n}\n\nexport const DEFAULT_SEARCH_CONFIG: SearchConfig = {\n defaultScope: 'all',\n aliases: { a: 'assignment', p: 'project', t: 'todo', s: 'server', pb: 'playbook' },\n externalIds: true,\n};\n\n/**\n * Palette AQL field names an alias prefix may NOT shadow (mirrors the keys of\n * `PALETTE_FIELDS` in `dashboard/src/hotkeys/paletteQuery.ts`). Declared here —\n * browser-safe and dependency-free — so the server router and the live SPA\n * validation share exactly one collision set (the server cannot import dashboard\n * code). Keep in sync with `PALETTE_FIELDS`.\n */\nexport const SEARCH_FIELD_NAMES: readonly string[] = [\n 'kind',\n 'status',\n 'tag',\n 'tags',\n 'assignee',\n 'type',\n 'project',\n 'externalid',\n 'jira',\n 'title',\n 'search',\n];\n\n/** Reserved escape prefix — `all:` searches everything; disallowed as a custom alias. */\nexport const RESERVED_ALIAS = 'all';\n\n/** An alias key must be lowercase, start with a letter, then letters/digits. */\nconst ALIAS_KEY_RE = /^[a-z][a-z0-9]*$/;\n\nfunction isEntityKind(value: unknown): value is EntityKind {\n return typeof value === 'string' && (ENTITY_KINDS as readonly string[]).includes(value);\n}\n\nfunction isDefaultScope(value: unknown): value is DefaultScope {\n return value === 'all' || isEntityKind(value);\n}\n\n/**\n * Coerce an arbitrary parsed object into a valid `SearchConfig`, filling defaults\n * for missing/invalid fields. Lenient (read/persist path): invalid alias entries\n * are dropped rather than rejected — the strict gate is `validateAliases`, used by\n * the POST router. An explicitly-present-but-empty `aliases` map stays empty; a\n * missing `aliases` falls back to the defaults.\n */\nexport function normalizeSearchConfig(raw: unknown): SearchConfig {\n if (!raw || typeof raw !== 'object') {\n return cloneDefaultSearchConfig();\n }\n const r = raw as Record<string, unknown>;\n\n const defaultScope: DefaultScope = isDefaultScope(r['defaultScope'])\n ? r['defaultScope']\n : DEFAULT_SEARCH_CONFIG.defaultScope;\n\n const externalIds =\n typeof r['externalIds'] === 'boolean'\n ? r['externalIds']\n : DEFAULT_SEARCH_CONFIG.externalIds;\n\n let aliases: Record<string, EntityKind>;\n if (r['aliases'] && typeof r['aliases'] === 'object') {\n aliases = {};\n for (const [key, value] of Object.entries(r['aliases'] as Record<string, unknown>)) {\n if (\n ALIAS_KEY_RE.test(key) &&\n key !== RESERVED_ALIAS &&\n !SEARCH_FIELD_NAMES.includes(key) &&\n isEntityKind(value)\n ) {\n aliases[key] = value;\n }\n }\n } else {\n aliases = { ...DEFAULT_SEARCH_CONFIG.aliases };\n }\n\n return { defaultScope, aliases, externalIds };\n}\n\n/**\n * Strict alias validation (POST path). Each key must be lowercase `[a-z][a-z0-9]*`,\n * must not be the reserved `all`, must not collide with a `SEARCH_FIELD_NAMES`\n * member, and must map to one of the five entity kinds. Returns every violation so\n * the router can 400 with the full list and the SPA can show inline feedback.\n */\nexport function validateAliases(\n aliases: unknown,\n): { ok: true } | { ok: false; errors: string[] } {\n if (!aliases || typeof aliases !== 'object') {\n return { ok: false, errors: ['aliases must be an object'] };\n }\n const errors: string[] = [];\n for (const [key, value] of Object.entries(aliases as Record<string, unknown>)) {\n if (!ALIAS_KEY_RE.test(key)) {\n errors.push(`alias key \"${key}\" must be lowercase and match [a-z][a-z0-9]*`);\n }\n if (key === RESERVED_ALIAS) {\n errors.push(`alias key \"${RESERVED_ALIAS}\" is reserved (the \"search everything\" escape)`);\n }\n if (SEARCH_FIELD_NAMES.includes(key)) {\n errors.push(`alias key \"${key}\" collides with the reserved field name \"${key}\"`);\n }\n if (!isEntityKind(value)) {\n errors.push(`alias \"${key}\" must map to one of: ${ENTITY_KINDS.join(', ')}`);\n }\n }\n return errors.length === 0 ? { ok: true } : { ok: false, errors };\n}\n\n/** Validate a default-scope value (POST path). */\nexport function isValidDefaultScope(value: unknown): value is DefaultScope {\n return isDefaultScope(value);\n}\n\nfunction cloneDefaultSearchConfig(): SearchConfig {\n return {\n defaultScope: DEFAULT_SEARCH_CONFIG.defaultScope,\n aliases: { ...DEFAULT_SEARCH_CONFIG.aliases },\n externalIds: DEFAULT_SEARCH_CONFIG.externalIds,\n };\n}\n","/**\n * Shared schema + pure helpers for the left-nav workspace-visibility preference.\n *\n * The preference is a BLOCKLIST of hidden workspace names: a workspace whose\n * name is absent from the list is shown. Newly created/discovered workspaces\n * therefore default to visible — hiding is strictly opt-in.\n *\n * This module is dependency-free (no imports from config.ts) so it can be\n * consumed by both the CLI/backend and the dashboard (via the `@shared` alias)\n * and unit-tested in the node-env vitest setup.\n */\n\n/**\n * The reserved name for the synthesized \"Ungrouped\" pseudo-workspace shown in\n * the sidebar when there are standalone projects/assignments with no workspace.\n * It is never returned by `listWorkspaces()`; the sidebar appends it. It is\n * treated as an ordinary blocklist member, so it can be hidden like any other.\n */\nexport const UNGROUPED_WORKSPACE = '_ungrouped' as const;\n\nexport interface WorkspaceVisibilityConfig {\n /** Names of workspaces hidden from the left nav. Absent = visible. */\n hidden: string[];\n}\n\n/**\n * Upper bound on a single workspace name. Real workspace slugs/group names are\n * short; this only guards against a pathological config.md entry.\n */\nexport const MAX_WORKSPACE_NAME_LENGTH = 256;\n\n/**\n * Normalize a raw blocklist (from disk, an API body, or a fetch response) into\n * a clean `string[]`: keep only strings, trim each, drop empties, anything\n * containing a line break, and absurdly long names, then dedupe preserving\n * first-seen order. Used on both the server (POST validation) and the client\n * (response `normalize`) so the rules are identical on both sides.\n */\nexport function normalizeHiddenList(input: unknown): string[] {\n if (!Array.isArray(input)) return [];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const raw of input) {\n if (typeof raw !== 'string') continue;\n const name = raw.trim();\n if (name.length === 0) continue;\n if (name.length > MAX_WORKSPACE_NAME_LENGTH) continue;\n if (/[\\r\\n]/.test(name)) continue;\n if (seen.has(name)) continue;\n seen.add(name);\n out.push(name);\n }\n return out;\n}\n\n/**\n * Pure filter: return `all` minus any name present in `hidden`, preserving the\n * input order. `_ungrouped` is filtered like any other name (no special case).\n */\nexport function visibleWorkspaces(all: string[], hidden: string[]): string[] {\n if (hidden.length === 0) return [...all];\n const blocked = new Set(hidden);\n return all.filter((name) => !blocked.has(name));\n}\n\n/** True when `name` is present in the blocklist. */\nexport function isWorkspaceHidden(name: string, hidden: string[]): boolean {\n return hidden.includes(name);\n}\n","import { readFile } from 'node:fs/promises';\nimport { spawnSync } from 'node:child_process';\nimport { resolve, isAbsolute } from 'node:path';\nimport { syntaurRoot, defaultProjectDir, expandHome } from './paths.js';\nimport { fileExists, writeFileForce } from './fs.js';\nimport { renderConfig } from '../templates/config.js';\nimport { migrateLegacyConfig } from './fs-migration.js';\nimport { DEFAULT_STATUSES, DEFAULT_TRANSITION_TABLE } from '../lifecycle/index.js';\nimport {\n BINDABLE_ACTION_KINDS,\n canonicalizeCombo,\n isBindableActionKind,\n isReservedCombo,\n type BindableActionKind,\n} from './hotkeysCatalog.js';\nimport {\n AGENT_ID_PATTERN,\n BUILTIN_AGENTS,\n PROMPT_ARG_POSITIONS,\n type AgentConfig,\n type PromptArgPosition,\n type SessionInvocation,\n} from './agents-schema.js';\nimport { isValidSlug } from './slug.js';\nimport {\n type FactDeclaration,\n type RawFactDeclaration,\n} from './fact-registry.js';\n\nexport {\n AGENT_ID_PATTERN,\n BUILTIN_AGENTS,\n PROMPT_ARG_POSITIONS,\n type AgentConfig,\n type PromptArgPosition,\n type SessionInvocation,\n};\n\nexport interface StatusDefinition {\n id: string;\n label: string;\n description?: string;\n color?: string;\n icon?: string;\n terminal?: boolean;\n}\n\nexport interface StatusTransition {\n from: string;\n command: string;\n to: string;\n label?: string;\n description?: string;\n requiresReason?: boolean;\n}\n\n/**\n * Derive-status primitives ({@link PhaseRung}, {@link DispositionRule},\n * {@link HeadlineProjection}, {@link DeriveConfig}, {@link DEFAULT_DERIVE_CONFIG},\n * {@link validateDeriveConfig}) live in the browser-safe `derive-config.ts` so\n * the dashboard client can alias and reuse them; imported for local use here and\n * re-exported so existing Node-side imports from `config.js` keep resolving.\n */\nimport {\n DEFAULT_DERIVE_CONFIG,\n validateDeriveConfig,\n validateDeriveShape,\n type PhaseRung,\n type DispositionRule,\n type HeadlineProjection,\n type DeriveConfig,\n} from './derive-config.js';\n\nexport { DEFAULT_DERIVE_CONFIG, validateDeriveConfig, validateDeriveShape };\nexport type { PhaseRung, DispositionRule, HeadlineProjection, DeriveConfig };\n\nimport type { StaleThresholds } from '../staleness/classify.js';\n\n/** Config keys for the `staleness:` block → `StaleThresholds` ms fields. Keyed\n * on the contradiction (phase/disposition), not raw status ids. */\nconst STALENESS_KEY_TO_FIELD: Record<string, keyof StaleThresholds> = {\n inProgressNoActivity: 'inProgressNoActivityMs',\n readyUnclaimed: 'readyUnclaimedMs',\n reviewAging: 'reviewAgingMs',\n blockedAging: 'blockedAgingMs',\n planApprovalAging: 'planApprovalAgingMs',\n};\n\nconst DURATION_RE = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)?$/;\nconst DURATION_UNIT_MS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\n/** Parse a duration like `7d`/`12h`/`30m`/`90s`/`500ms` (or a bare number = ms)\n * to milliseconds. Returns null when malformed or non-positive. */\nexport function parseDurationMs(raw: string): number | null {\n const m = raw.trim().match(DURATION_RE);\n if (!m) return null;\n const n = Number(m[1]);\n if (!Number.isFinite(n) || n <= 0) return null;\n return n * DURATION_UNIT_MS[m[2] ?? 'ms'];\n}\n\n/**\n * A custom-fact declaration EXACTLY as parsed from `statuses.facts` — loose\n * parse (Locked Decisions): every field is a raw string so user input\n * round-trips through serialization even when invalid. The strict\n * {@link FactDeclaration} is derived from this via {@link normalizeFactDeclarations}.\n *\n * Defined in `fact-registry.ts` (browser-safe); re-exported here so existing\n * Node-side imports from `config.js` keep resolving.\n */\nexport type { RawFactDeclaration } from './fact-registry.js';\n\n/**\n * A VALIDATED custom-fact declaration (strict union). bool/number facts are\n * asserted values stored in the `facts:` frontmatter map; attestation facts\n * model \"agent reviewed revision with verdict\" and carry a revision binding.\n *\n * Defined in `fact-registry.ts` (browser-safe); re-exported here so existing\n * Node-side imports from `config.js` keep resolving.\n */\nexport type { FactDeclaration } from './fact-registry.js';\nexport { validateFactDeclarations, normalizeFactDeclarations } from './fact-registry.js';\n\nexport interface StatusConfig {\n statuses: StatusDefinition[];\n order: string[];\n transitions: StatusTransition[];\n /** Derived-status rules (v3). Null/absent → DEFAULT_DERIVE_CONFIG at resolve\n * time. Persisted under `statuses:` so the Settings writer round-trips it. */\n derive?: DeriveConfig | null;\n /** Custom-fact declarations (raw — see {@link RawFactDeclaration}). Persisted\n * under `statuses.facts`; preserved verbatim so invalid rows round-trip and\n * doctor can diagnose them. Null/absent → no custom vocabulary. */\n facts?: RawFactDeclaration[] | null;\n}\n\nexport interface TypeDefinition {\n id: string;\n label?: string;\n description?: string;\n color?: string;\n icon?: string;\n}\n\nexport interface TypesConfig {\n definitions: TypeDefinition[];\n default: string;\n}\n\nexport const DEFAULT_ASSIGNMENT_TYPES: TypesConfig = {\n definitions: [\n { id: 'feature', label: 'Feature' },\n { id: 'bug', label: 'Bug' },\n { id: 'refactor', label: 'Refactor' },\n { id: 'research', label: 'Research' },\n { id: 'chore', label: 'Chore' },\n ],\n default: 'feature',\n};\n\nexport interface IntegrationConfig {\n claudePluginDir: string | null;\n codexPluginDir: string | null;\n codexMarketplacePath: string | null;\n // Per-agent cross-agent install records (pi, hermes, openclaw, ...). Optional\n // so existing `IntegrationConfig` literals (and the default config) need no\n // change. Serialized as flat `installedAgents.<id>: <scope>` keys inside the\n // `integrations:` block (the frontmatter parser only flattens two levels).\n installedAgents?: Record<string, { scope: 'project' | 'global' }>;\n}\n\nexport interface OnboardingConfig {\n completed: boolean;\n}\n\nexport interface BackupConfig {\n repo: string | null;\n categories: string;\n lastBackup: string | null;\n lastRestore: string | null;\n}\n\nexport type AutoCreateWorktree = 'skip' | 'ask' | 'always';\n\nexport interface PlaybooksConfig {\n disabled: string[];\n}\n\nexport interface ThemeConfig {\n preset: string;\n}\n\nexport interface HotkeyBindingsConfig {\n bindings: Partial<Record<BindableActionKind, string>>;\n}\n\nimport { TERMINAL_CHOICES, type TerminalChoice } from './terminal-schema.js';\nimport {\n DEFAULT_SEARCH_CONFIG,\n normalizeSearchConfig,\n type SearchConfig,\n} from './search-schema.js';\nexport { TERMINAL_CHOICES, type TerminalChoice };\n\nimport {\n normalizeHiddenList,\n type WorkspaceVisibilityConfig,\n} from './workspace-visibility-schema.js';\nexport { type WorkspaceVisibilityConfig };\n\n/**\n * Automatic session tracking scope:\n * - `all`: every discovered/hooked session is written to the sessions DB.\n * - `workspaces-only`: only sessions whose cwd has `.syntaur/context.json`.\n * - `off`: no automatic DB writes (manual `track-session` still works).\n */\nexport type SessionAutoTrack = 'all' | 'workspaces-only' | 'off';\n\nexport interface SyntaurConfig {\n version: string;\n defaultProjectDir: string;\n onboarding: OnboardingConfig;\n agentDefaults: {\n trustLevel: 'low' | 'medium' | 'high';\n autoApprove: boolean;\n autoCreateWorktree: AutoCreateWorktree;\n };\n session: {\n autoTrack: SessionAutoTrack;\n };\n integrations: IntegrationConfig;\n backup: BackupConfig | null;\n statuses: StatusConfig | null;\n types: TypesConfig | null;\n agents: AgentConfig[] | null;\n playbooks: PlaybooksConfig;\n theme: ThemeConfig | null;\n hotkeys: HotkeyBindingsConfig | null;\n terminal: TerminalChoice | null;\n searchConfig: SearchConfig | null;\n workspaceVisibility: WorkspaceVisibilityConfig;\n /** Optional per-reason staleness age-gate overrides (defaults-first; null = all defaults). */\n staleness: Partial<StaleThresholds> | null;\n /** Opt-in: run the read-only staleness watchdog on the dashboard loop (emits\n * staleness-detected/cleared audit events; never mutates status). Off by default. */\n stalenessWatchdog: boolean;\n}\n\nconst DEFAULT_CONFIG: SyntaurConfig = {\n version: '2.0',\n defaultProjectDir: defaultProjectDir(),\n onboarding: {\n completed: false,\n },\n agentDefaults: {\n trustLevel: 'medium',\n autoApprove: false,\n autoCreateWorktree: 'ask',\n },\n session: {\n autoTrack: 'all',\n },\n integrations: {\n claudePluginDir: null,\n codexPluginDir: null,\n codexMarketplacePath: null,\n },\n backup: null,\n statuses: null,\n types: null,\n agents: null,\n playbooks: {\n disabled: [],\n },\n theme: null,\n hotkeys: null,\n terminal: null,\n searchConfig: null,\n workspaceVisibility: {\n hidden: [],\n },\n staleness: null,\n stalenessWatchdog: false,\n};\n\nconst AUTO_CREATE_WORKTREE_VALUES: readonly AutoCreateWorktree[] = ['skip', 'ask', 'always'];\n\nconst SESSION_AUTO_TRACK_VALUES: readonly SessionAutoTrack[] = ['all', 'workspaces-only', 'off'];\n\nexport class AgentConfigError extends Error {}\n\n/**\n * Validate an agent command string.\n * - Absolute paths (after ~ expansion) are accepted verbatim.\n * - Bare names (no \"/\" after expansion) are accepted for PATH lookup at launch time.\n * - Relative paths (contain \"/\" but not absolute) are rejected.\n */\nexport function parseAgentCommand(value: string, agentId?: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new AgentConfigError(\n `agent${agentId ? ` \"${agentId}\"` : ''} has empty command`,\n );\n }\n const expanded = expandHome(value.trim());\n if (isAbsolute(expanded)) {\n return resolve(expanded);\n }\n if (expanded.includes('/')) {\n throw new AgentConfigError(\n `agent${agentId ? ` \"${agentId}\"` : ''} command \"${value}\" is a relative path — use an absolute path or a bare binary name`,\n );\n }\n return expanded;\n}\n\nexport function validateAgentList(agents: AgentConfig[]): void {\n const seen = new Set<string>();\n let defaults = 0;\n for (const agent of agents) {\n if (!AGENT_ID_PATTERN.test(agent.id)) {\n throw new AgentConfigError(\n `agent id \"${agent.id}\" is invalid — must match /^[a-z0-9][a-z0-9_-]*$/`,\n );\n }\n if (seen.has(agent.id)) {\n throw new AgentConfigError(`duplicate agent id \"${agent.id}\"`);\n }\n seen.add(agent.id);\n if (!agent.label || agent.label.trim() === '') {\n throw new AgentConfigError(`agent \"${agent.id}\" has empty label`);\n }\n parseAgentCommand(agent.command, agent.id);\n if (\n agent.promptArgPosition !== undefined &&\n !PROMPT_ARG_POSITIONS.includes(agent.promptArgPosition)\n ) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" has invalid promptArgPosition \"${agent.promptArgPosition}\" — expected first|last|none`,\n );\n }\n if (agent.model !== undefined && /[\\r\\n]/.test(agent.model)) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" has invalid model — must be a single line (no newlines)`,\n );\n }\n if (\n agent.playbook !== undefined &&\n agent.playbook.trim() !== '' &&\n !isValidSlug(agent.playbook)\n ) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" has invalid playbook \"${agent.playbook}\" — must be a valid playbook slug`,\n );\n }\n if (agent.launchPrompt !== undefined && /[\\r\\n]/.test(agent.launchPrompt)) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" has invalid launchPrompt — must be a single line (no newlines)`,\n );\n }\n validateSessionInvocation(agent, 'resume', agent.resume);\n validateSessionInvocation(agent, 'fork', agent.fork);\n if (agent.default) defaults++;\n }\n if (defaults > 1) {\n throw new AgentConfigError(\n `more than one agent is marked default: true (only one is allowed)`,\n );\n }\n}\n\nfunction validateSessionInvocation(\n agent: AgentConfig,\n mode: 'resume' | 'fork',\n invocation: SessionInvocation | undefined,\n): void {\n if (invocation === undefined) return;\n if (!Array.isArray(invocation.args)) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" ${mode}.args must be an array of strings`,\n );\n }\n for (const a of invocation.args) {\n if (typeof a !== 'string') {\n throw new AgentConfigError(\n `agent \"${agent.id}\" ${mode}.args must contain only strings`,\n );\n }\n }\n if (\n invocation.command !== undefined &&\n (typeof invocation.command !== 'string' || invocation.command.trim() === '')\n ) {\n throw new AgentConfigError(\n `agent \"${agent.id}\" ${mode}.command must be a non-empty string when present`,\n );\n }\n}\n\nfunction cloneDefaultConfig(): SyntaurConfig {\n return {\n ...DEFAULT_CONFIG,\n onboarding: { ...DEFAULT_CONFIG.onboarding },\n agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },\n session: { ...DEFAULT_CONFIG.session },\n integrations: { ...DEFAULT_CONFIG.integrations },\n backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,\n statuses: DEFAULT_CONFIG.statuses\n ? {\n statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),\n order: [...DEFAULT_CONFIG.statuses.order],\n transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t })),\n }\n : null,\n types: DEFAULT_CONFIG.types\n ? {\n definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),\n default: DEFAULT_CONFIG.types.default,\n }\n : null,\n agents: DEFAULT_CONFIG.agents\n ? DEFAULT_CONFIG.agents.map((a) => ({\n ...a,\n ...(a.args ? { args: [...a.args] } : {}),\n ...(a.resume ? { resume: { ...a.resume, args: [...a.resume.args] } } : {}),\n ...(a.fork ? { fork: { ...a.fork, args: [...a.fork.args] } } : {}),\n }))\n : null,\n playbooks: {\n disabled: [...DEFAULT_CONFIG.playbooks.disabled],\n },\n theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null,\n hotkeys: DEFAULT_CONFIG.hotkeys\n ? { bindings: { ...DEFAULT_CONFIG.hotkeys.bindings } }\n : null,\n terminal: DEFAULT_CONFIG.terminal,\n workspaceVisibility: {\n hidden: [...DEFAULT_CONFIG.workspaceVisibility.hidden],\n },\n };\n}\n\nfunction parseFrontmatter(content: string): Record<string, string> {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return {};\n const result: Record<string, string> = {};\n const lines = match[1].split('\\n');\n let currentParent: string | null = null;\n for (const line of lines) {\n if (line.trim() === '') continue;\n const indent = line.length - line.trimStart().length;\n const colonIndex = line.indexOf(':');\n if (colonIndex < 0) continue;\n const key = line.slice(0, colonIndex).trim();\n const value = line.slice(colonIndex + 1).trim();\n if (indent === 0) {\n if (value === '' || value === undefined) {\n currentParent = key;\n } else {\n currentParent = null;\n result[key] = value.replace(/^[\"']|[\"']$/g, '');\n }\n } else if (indent > 0 && currentParent) {\n result[`${currentParent}.${key}`] = value.replace(/^[\"']|[\"']$/g, '');\n }\n }\n return result;\n}\n\n/**\n * Reconstruct the optional per-agent install records from the flattened\n * frontmatter. Keys look like `integrations.installedAgents.<id>` → `<scope>`.\n * Returns `{}` (no key) when none are present so the field stays absent.\n */\nfunction parseInstalledAgents(\n fm: Record<string, string>,\n): Pick<IntegrationConfig, 'installedAgents'> {\n const prefix = 'integrations.installedAgents.';\n const installedAgents: Record<string, { scope: 'project' | 'global' }> = {};\n for (const [key, value] of Object.entries(fm)) {\n if (!key.startsWith(prefix)) continue;\n const id = key.slice(prefix.length);\n if (!id) continue;\n const scope = value === 'project' ? 'project' : 'global';\n installedAgents[id] = { scope };\n }\n return Object.keys(installedAgents).length > 0 ? { installedAgents } : {};\n}\n\nexport function parseStatusConfig(content: string): StatusConfig | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n // Check if there's a top-level statuses: section\n const statusesStart = fmBlock.match(/^statuses:\\s*$/m);\n if (!statusesStart) return null;\n\n // Extract the statuses block (everything indented after \"statuses:\")\n const startIdx = fmBlock.indexOf(statusesStart[0]) + statusesStart[0].length;\n const remaining = fmBlock.slice(startIdx);\n\n const statuses: StatusDefinition[] = [];\n const order: string[] = [];\n const transitions: StatusTransition[] = [];\n const phaseLadder: PhaseRung[] = [];\n const disposition: DispositionRule[] = [];\n const headline: Record<string, string> = {};\n const facts: RawFactDeclaration[] = [];\n\n // Strip surrounding quotes from a YAML scalar (AQL conditions are quoted).\n const unquote = (v: string): string => {\n const t = v.trim();\n if ((t.startsWith('\"') && t.endsWith('\"')) || (t.startsWith(\"'\") && t.endsWith(\"'\"))) {\n return t.slice(1, -1);\n }\n return t;\n };\n\n // Like `unquote`, but also reverses the `\\`/`\"` escaping that `escapeAql`\n // applies to the THREE escaped derive-rule fields (phaseLadder when/next,\n // disposition when). Scoped to those reads only — the plain `unquote` above\n // stays the decoder for every other (unescaped) scalar (is/phase/headline/\n // facts/aliases), so no field the serializer never escapes can be\n // over-decoded. Mirrors parseSimpleValue's escape handling.\n const unquoteAql = (v: string): string => {\n const t = v.trim();\n if (t.startsWith('\"') && t.endsWith('\"') && t.length >= 2) {\n return t.slice(1, -1).replace(/\\\\([\"\\\\])/g, '$1');\n }\n if (t.startsWith(\"'\") && t.endsWith(\"'\") && t.length >= 2) {\n return t.slice(1, -1);\n }\n return t;\n };\n\n // Parse sub-sections: definitions, order, transitions + derive rules\n // (phaseLadder, disposition, headline — derived-status v3, persisted flat\n // under `statuses:`).\n let currentSection:\n | 'definitions'\n | 'order'\n | 'transitions'\n | 'phaseLadder'\n | 'disposition'\n | 'headline'\n | 'facts'\n | null = null;\n const lines = remaining.split('\\n');\n\n function parseListEntry(lineIdx: number, baseIndent: number): { entry: Record<string, string>; consumed: number } {\n const entry: Record<string, string> = {};\n const firstLine = lines[lineIdx].trimStart().slice(2).trim();\n const colonIdx = firstLine.indexOf(':');\n if (colonIdx > 0) {\n entry[firstLine.slice(0, colonIdx).trim()] = firstLine.slice(colonIdx + 1).trim();\n }\n let consumed = 1;\n for (let i = lineIdx + 1; i < lines.length; i++) {\n const next = lines[i];\n const nextTrimmed = next.trimStart();\n const nextIndent = next.length - nextTrimmed.length;\n if (nextIndent <= baseIndent || nextTrimmed.startsWith('- ')) break;\n const ci = nextTrimmed.indexOf(':');\n if (ci > 0) {\n entry[nextTrimmed.slice(0, ci).trim()] = nextTrimmed.slice(ci + 1).trim();\n }\n consumed++;\n }\n return { entry, consumed };\n }\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx];\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n\n // Top-level key under statuses (indent 2)\n if (indent === 2 && trimmed.endsWith(':')) {\n const key = trimmed.slice(0, -1).trim();\n if (key === 'definitions') currentSection = 'definitions';\n else if (key === 'order') currentSection = 'order';\n else if (key === 'transitions') currentSection = 'transitions';\n else if (key === 'phaseLadder') currentSection = 'phaseLadder';\n else if (key === 'disposition') currentSection = 'disposition';\n else if (key === 'headline') currentSection = 'headline';\n else if (key === 'facts') currentSection = 'facts';\n else currentSection = null;\n continue;\n }\n\n // Stop if we hit a new top-level key (no indent)\n if (indent === 0 && trimmed.includes(':')) break;\n\n if (currentSection === 'order' && indent >= 4 && trimmed.startsWith('- ')) {\n order.push(trimmed.slice(2).trim());\n continue;\n }\n\n if (currentSection === 'definitions' && indent >= 4 && trimmed.startsWith('- ')) {\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (entry['id']) {\n statuses.push({\n id: entry['id'],\n label: entry['label'] ?? entry['id'],\n description: entry['description'],\n color: entry['color'],\n icon: entry['icon'],\n terminal: entry['terminal'] === 'true',\n });\n }\n lineIdx += consumed - 1; // skip consumed continuation lines\n continue;\n }\n\n if (currentSection === 'transitions' && indent >= 4 && trimmed.startsWith('- ')) {\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (entry['from'] && entry['command'] && entry['to']) {\n transitions.push({\n from: entry['from'],\n command: entry['command'],\n to: entry['to'],\n label: entry['label'],\n description: entry['description'],\n requiresReason: entry['requiresReason'] === 'true',\n });\n }\n lineIdx += consumed - 1;\n continue;\n }\n\n if (currentSection === 'phaseLadder' && indent >= 4 && trimmed.startsWith('- ')) {\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (entry['phase'] && entry['when'] !== undefined) {\n phaseLadder.push({\n phase: unquote(entry['phase']),\n when: unquoteAql(entry['when']),\n next: entry['next'] !== undefined ? unquoteAql(entry['next']) : undefined,\n });\n }\n lineIdx += consumed - 1;\n continue;\n }\n\n if (currentSection === 'disposition' && indent >= 4 && trimmed.startsWith('- ')) {\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (entry['else'] !== undefined) {\n disposition.push({ when: null, is: unquote(entry['else']) });\n } else if (entry['when'] !== undefined && entry['is']) {\n disposition.push({ when: unquoteAql(entry['when']), is: unquote(entry['is']) });\n }\n lineIdx += consumed - 1;\n continue;\n }\n\n if (currentSection === 'headline' && indent >= 4 && !trimmed.startsWith('- ')) {\n const ci = trimmed.indexOf(':');\n if (ci > 0) {\n headline[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1));\n }\n continue;\n }\n\n if (currentSection === 'facts' && indent >= 4 && trimmed.startsWith('- ')) {\n // Loose parse: keep every recognizable row verbatim (RawFactDeclaration)\n // so invalid rows round-trip AND doctor can diagnose exactly what the\n // normalize/accept pipeline drops — a row missing `name` must NOT be\n // silently deleted (that is the silent-deletion bug class this feature\n // exists to prevent). A row with no recognized key at all is skipped.\n const { entry, consumed } = parseListEntry(lineIdx, indent);\n if (\n entry['name'] !== undefined ||\n entry['type'] !== undefined ||\n entry['binds'] !== undefined\n ) {\n facts.push({\n name: entry['name'] !== undefined ? unquote(entry['name']) : '',\n type: entry['type'] !== undefined ? unquote(entry['type']) : '',\n binds: entry['binds'] !== undefined ? unquote(entry['binds']) : null,\n });\n }\n lineIdx += consumed - 1;\n continue;\n }\n }\n\n const derive: DeriveConfig | null =\n phaseLadder.length > 0 || disposition.length > 0 || Object.keys(headline).length > 0\n ? {\n phaseLadder: phaseLadder.length > 0 ? phaseLadder : DEFAULT_DERIVE_CONFIG.phaseLadder,\n disposition: disposition.length > 0 ? disposition : DEFAULT_DERIVE_CONFIG.disposition,\n headline: {\n terminal: 'passthrough',\n parked: headline['parked'] ?? DEFAULT_DERIVE_CONFIG.headline.parked,\n blocked: headline['blocked'] ?? DEFAULT_DERIVE_CONFIG.headline.blocked,\n active: 'phase',\n },\n }\n : null;\n\n // Return null only when the `statuses:` block carried no usable content at\n // all. A block that declares facts and/or derive rules but no status\n // `definitions` must still surface them — dropping them here is the\n // silent-deletion bug class this loose parser exists to prevent\n // (getStatusConfig falls back to default statuses/order so the board still\n // renders, while the declared facts/derive ride along).\n if (statuses.length === 0 && facts.length === 0 && derive === null) return null;\n\n return {\n statuses,\n order: order.length > 0 ? order : statuses.map((s) => s.id),\n transitions,\n derive,\n facts: facts.length > 0 ? facts : null,\n };\n}\n\n/**\n * Default per-status accent colors. Statuses without an entry fall back to\n * `'gray'` in {@link buildDefaultStatusConfig}. Shared by the dashboard's\n * `getStatusConfig()` and the `syntaur status` CLI so the two never drift.\n */\nexport const DEFAULT_STATUS_COLORS: Record<string, string> = {\n pending: 'slate',\n in_progress: 'teal',\n blocked: 'amber',\n review: 'violet',\n completed: 'emerald',\n failed: 'rose',\n};\n\n/** Turn a snake_case status id into a human label (\"in_progress\" → \"In Progress\"). */\nexport function toTitleCase(s: string): string {\n return s.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Materialize the built-in default status set as an explicit {@link StatusConfig}.\n *\n * `DEFAULT_CONFIG.statuses` is `null` (the runtime resolves defaults lazily), so\n * `syntaur status init` / `list` cannot read defaults from there. This builder\n * reproduces exactly what the dashboard's `getStatusConfig()` no-block branch\n * builds — same ids/labels/colors/terminal flags and the same transition table —\n * so the CLI and the dashboard share one source of truth.\n */\nexport function buildDefaultStatusConfig(): StatusConfig {\n return {\n statuses: DEFAULT_STATUSES.map((id) => ({\n id,\n label: toTitleCase(id),\n color: DEFAULT_STATUS_COLORS[id] ?? 'gray',\n terminal: id === 'completed' || id === 'failed',\n })),\n order: [...DEFAULT_STATUSES],\n transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {\n const [from, command] = key.split(':');\n return { from, command, to };\n }),\n };\n}\n\nexport function serializeStatusConfig(statuses: StatusConfig): string {\n const lines: string[] = [];\n // Symmetric with `unquoteAql` in parseStatusConfig: escape backslash THEN\n // quote so the derive-rule when/next/disposition-when fields round-trip even\n // when they contain a literal `\"` or `\\`. (Previously only `\"` was escaped and\n // nothing reversed it, so a quoted AQL condition accumulated a backslash on\n // every save→reload.)\n const escapeAql = (s: string): string => s.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n lines.push('statuses:');\n\n // definitions\n lines.push(' definitions:');\n for (const s of statuses.statuses) {\n lines.push(` - id: ${s.id}`);\n lines.push(` label: ${s.label}`);\n if (s.description) lines.push(` description: ${s.description}`);\n if (s.color) lines.push(` color: ${s.color}`);\n if (s.icon) lines.push(` icon: ${s.icon}`);\n if (s.terminal) lines.push(` terminal: true`);\n }\n\n // order\n lines.push(' order:');\n for (const id of statuses.order) {\n lines.push(` - ${id}`);\n }\n\n // transitions\n if (statuses.transitions.length > 0) {\n lines.push(' transitions:');\n for (const t of statuses.transitions) {\n lines.push(` - from: ${t.from}`);\n lines.push(` command: ${t.command}`);\n lines.push(` to: ${t.to}`);\n if (t.label) lines.push(` label: ${t.label}`);\n if (t.description) lines.push(` description: ${t.description}`);\n if (t.requiresReason) lines.push(` requiresReason: true`);\n }\n }\n\n // custom fact declarations — emitted verbatim (RawFactDeclaration) so the\n // round-trip preserves whatever the user wrote, even invalid rows that the\n // normalize/accept pipeline later drops. Same silent-deletion class as derive.\n if (statuses.facts && statuses.facts.length > 0) {\n lines.push(' facts:');\n for (const f of statuses.facts) {\n lines.push(` - name: ${f.name}`);\n lines.push(` type: ${f.type}`);\n if (f.binds !== null && f.binds !== undefined) {\n lines.push(` binds: ${f.binds}`);\n }\n }\n }\n\n // derive rules (derived-status v3) — serialized so every writeStatusConfig\n // round-trip preserves them (the pre-v3 writer rebuilt the block from\n // definitions/order/transitions only and silently deleted custom rules).\n if (statuses.derive) {\n const d = statuses.derive;\n lines.push(' phaseLadder:');\n for (const rung of d.phaseLadder) {\n lines.push(` - phase: ${rung.phase}`);\n lines.push(` when: \"${escapeAql(rung.when)}\"`);\n // `!== undefined`, not truthy: an accepted empty-string `next: \"\"` must\n // be preserved (otherwise it reparses as undefined — a round-trip loss).\n if (rung.next !== undefined) lines.push(` next: \"${escapeAql(rung.next)}\"`);\n }\n lines.push(' disposition:');\n for (const rule of d.disposition) {\n if (rule.when === null) {\n lines.push(` - else: ${rule.is}`);\n } else {\n lines.push(` - when: \"${escapeAql(rule.when)}\"`);\n lines.push(` is: ${rule.is}`);\n }\n }\n lines.push(' headline:');\n lines.push(` terminal: passthrough`);\n lines.push(` parked: ${d.headline.parked}`);\n lines.push(` blocked: ${d.headline.blocked}`);\n lines.push(` active: phase`);\n }\n\n return lines.join('\\n');\n}\n\nfunction serializeIntegrationConfig(integrations: IntegrationConfig): string | null {\n const lines: string[] = [];\n\n if (integrations.claudePluginDir) {\n lines.push(` claudePluginDir: ${integrations.claudePluginDir}`);\n }\n if (integrations.codexPluginDir) {\n lines.push(` codexPluginDir: ${integrations.codexPluginDir}`);\n }\n if (integrations.codexMarketplacePath) {\n lines.push(` codexMarketplacePath: ${integrations.codexMarketplacePath}`);\n }\n if (integrations.installedAgents) {\n for (const [id, rec] of Object.entries(integrations.installedAgents)) {\n lines.push(` installedAgents.${id}: ${rec.scope}`);\n }\n }\n\n if (lines.length === 0) {\n return null;\n }\n\n return ['integrations:', ...lines].join('\\n');\n}\n\nfunction serializeOnboardingConfig(onboarding: OnboardingConfig): string {\n return ['onboarding:', ` completed: ${onboarding.completed ? 'true' : 'false'}`].join('\\n');\n}\n\nfunction serializeBackupConfig(backup: BackupConfig): string {\n const lines: string[] = ['backup:'];\n lines.push(` repo: ${backup.repo ?? 'null'}`);\n lines.push(` categories: ${backup.categories}`);\n lines.push(` lastBackup: ${backup.lastBackup ?? 'null'}`);\n lines.push(` lastRestore: ${backup.lastRestore ?? 'null'}`);\n return lines.join('\\n');\n}\n\nfunction serializePlaybooksConfig(playbooks: PlaybooksConfig): string | null {\n if (!playbooks.disabled || playbooks.disabled.length === 0) {\n return null;\n }\n const lines: string[] = ['playbooks:', ' disabled:'];\n for (const slug of playbooks.disabled) {\n lines.push(` - ${slug}`);\n }\n return lines.join('\\n');\n}\n\nfunction parsePlaybooksConfig(fmBlock: string): PlaybooksConfig {\n const blockStart = fmBlock.match(/^playbooks:\\s*$/m);\n if (!blockStart) {\n return { disabled: [] };\n }\n\n const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;\n const remaining = fmBlock.slice(startIdx).split('\\n');\n\n const disabled: string[] = [];\n let currentSection: 'disabled' | null = null;\n\n for (const line of remaining) {\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n\n // End of playbooks block — next top-level key\n if (indent === 0 && trimmed.length > 0) break;\n\n if (trimmed === '') continue;\n\n if (indent === 2 && trimmed.startsWith('disabled:')) {\n currentSection = 'disabled';\n // Support inline form `disabled: []` — treat as empty list.\n const afterColon = trimmed.slice('disabled:'.length).trim();\n if (afterColon === '[]' || afterColon === '') {\n continue;\n }\n // Any other inline value is malformed; skip.\n continue;\n }\n\n if (currentSection === 'disabled' && indent >= 4 && trimmed.startsWith('- ')) {\n const raw = trimmed.slice(2).trim().replace(/^[\"']|[\"']$/g, '');\n if (raw.length === 0) continue;\n // Defer slug-format validation to callers via isValidSlug where needed;\n // here we only filter obviously invalid whitespace-containing entries.\n if (/\\s/.test(raw)) {\n console.warn(`Warning: config.md playbooks.disabled entry \"${raw}\" contains whitespace, ignoring`);\n continue;\n }\n disabled.push(raw);\n continue;\n }\n }\n\n return { disabled };\n}\n\nexport async function updatePlaybooksConfig(\n playbooks: Partial<PlaybooksConfig>,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const current = (await readConfig()).playbooks;\n const nextPlaybooks: PlaybooksConfig = {\n disabled: Array.from(new Set(playbooks.disabled ?? current.disabled)),\n };\n\n const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const bodyBlock = playbooksBlock ? `${playbooksBlock}\\n` : '';\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${bodyBlock}---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'playbooks');\n const newFm = playbooksBlock\n ? `${cleanedFm}\\n${playbooksBlock}`.replace(/^\\n+/, '')\n : cleanedFm;\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nfunction parseThemeConfig(content: string): ThemeConfig | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const blockStart = fmBlock.match(/^theme:\\s*$/m);\n if (!blockStart) return null;\n\n const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;\n const remaining = fmBlock.slice(startIdx).split('\\n');\n\n let preset: string | null = null;\n for (const line of remaining) {\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0 && trimmed.length > 0) break;\n if (trimmed === '') continue;\n if (indent === 2 && trimmed.startsWith('preset:')) {\n const value = trimmed.slice('preset:'.length).trim().replace(/^[\"']|[\"']$/g, '');\n if (value.length > 0) preset = value;\n }\n }\n\n if (!preset) return null;\n return { preset };\n}\n\nfunction serializeThemeConfig(theme: ThemeConfig): string {\n return ['theme:', ` preset: ${theme.preset}`].join('\\n');\n}\n\nexport async function writeThemeConfig(theme: ThemeConfig): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const themeBlock = serializeThemeConfig(theme);\n\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${themeBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'theme');\n const newFm = `${cleanedFm}\\n${themeBlock}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteThemeConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'theme');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n/**\n * Serialize the workspace-visibility blocklist. Mirrors the `playbooks.disabled`\n * list shape but JSON-escapes each entry so arbitrary workspace names (spaces,\n * quotes, backslashes) round-trip. Returns `null` for an empty list so the\n * writer omits the block entirely (absent = all workspaces visible).\n */\nfunction serializeWorkspaceVisibilityConfig(\n cfg: WorkspaceVisibilityConfig,\n): string | null {\n const hidden = normalizeHiddenList(cfg.hidden);\n if (hidden.length === 0) return null;\n const lines: string[] = ['workspaceVisibility:', ' hidden:'];\n for (const name of hidden) {\n lines.push(` - ${JSON.stringify(name)}`);\n }\n return lines.join('\\n');\n}\n\n/**\n * Parse the workspace-visibility blocklist from a frontmatter block. Unlike\n * `parsePlaybooksConfig` (which rejects whitespace-containing slugs), workspace\n * names are arbitrary: a JSON-quoted entry is `JSON.parse`d, an unquoted entry\n * is taken literally. Absent block → empty list (everything visible).\n */\nfunction parseWorkspaceVisibilityConfig(\n fmBlock: string,\n): WorkspaceVisibilityConfig {\n const blockStart = fmBlock.match(/^workspaceVisibility:\\s*$/m);\n if (!blockStart) {\n return { hidden: [] };\n }\n\n const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;\n const remaining = fmBlock.slice(startIdx).split('\\n');\n\n const hidden: string[] = [];\n let currentSection: 'hidden' | null = null;\n\n for (const line of remaining) {\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n\n // End of block — next top-level key.\n if (indent === 0 && trimmed.length > 0) break;\n if (trimmed === '') continue;\n\n if (indent === 2 && trimmed.startsWith('hidden:')) {\n currentSection = 'hidden';\n // Support inline form `hidden: []` — treat as empty list.\n continue;\n }\n\n if (currentSection === 'hidden' && indent >= 4 && trimmed.startsWith('- ')) {\n const rest = trimmed.slice(2).trim();\n if (rest.length === 0) continue;\n let name: string;\n if (rest.startsWith('\"')) {\n try {\n name = JSON.parse(rest) as string;\n } catch {\n // Hand-edited / malformed — strip a single surrounding quote pair.\n name = rest.replace(/^[\"']|[\"']$/g, '');\n }\n } else {\n name = rest;\n }\n hidden.push(name);\n continue;\n }\n }\n\n return { hidden: normalizeHiddenList(hidden) };\n}\n\nexport async function writeWorkspaceVisibilityConfig(\n cfg: WorkspaceVisibilityConfig,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const block = serializeWorkspaceVisibilityConfig(cfg);\n\n const existing = (await fileExists(configPath))\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const bodyBlock = block ? `${block}\\n` : '';\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${bodyBlock}---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'workspaceVisibility');\n const newFm = block\n ? `${cleanedFm}\\n${block}`.replace(/^\\n+/, '')\n : cleanedFm;\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteWorkspaceVisibilityConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'workspaceVisibility');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n/**\n * Remove any top-level `key: <value>` scalar line from a YAML frontmatter block.\n * Used for scalar keys (terminal:) that don't have child lines, so they can't\n * use the block-style `stripTopLevelBlock`. No-op when the key is absent.\n */\nfunction stripTopLevelScalar(fmBlock: string, key: string): string {\n const lines = fmBlock.split('\\n');\n const keyRegex = new RegExp(`^${key}:\\\\s*\\\\S`);\n const filtered = lines.filter((line) => !keyRegex.test(line));\n return filtered.join('\\n').replace(/\\n+$/, '');\n}\n\nexport async function writeTerminalConfig(terminal: TerminalChoice): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const terminalLine = `terminal: ${terminal}`;\n\n const existing = (await fileExists(configPath))\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${terminalLine}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelScalar(fmBlock, 'terminal');\n const newFm = `${cleanedFm}\\n${terminalLine}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteTerminalConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelScalar(fmBlock, 'terminal');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nfunction parseHotkeyBindingsConfig(content: string): HotkeyBindingsConfig | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const blockStart = fmBlock.match(/^hotkeys:\\s*$/m);\n if (!blockStart) return null;\n\n const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;\n const remaining = fmBlock.slice(startIdx).split('\\n');\n\n const bindings: Partial<Record<BindableActionKind, string>> = {};\n let inBindings = false;\n for (const line of remaining) {\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0 && trimmed.length > 0) break;\n if (trimmed === '') continue;\n if (indent === 2 && trimmed === 'bindings:') {\n inBindings = true;\n continue;\n }\n if (inBindings && indent === 4) {\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx <= 0) continue;\n const rawKind = trimmed.slice(0, colonIdx).trim();\n const rawValue = trimmed\n .slice(colonIdx + 1)\n .trim()\n .replace(/^[\"']|[\"']$/g, '');\n if (!isBindableActionKind(rawKind)) continue;\n if (rawValue.length === 0) continue;\n bindings[rawKind] = canonicalizeCombo(rawValue);\n }\n }\n\n if (Object.keys(bindings).length === 0) return null;\n return { bindings };\n}\n\nfunction serializeHotkeyBindingsConfig(cfg: HotkeyBindingsConfig): string {\n const lines: string[] = ['hotkeys:', ' bindings:'];\n // Emit in the canonical kind order so on-disk diffs are stable.\n for (const kind of BINDABLE_ACTION_KINDS) {\n const value = cfg.bindings[kind];\n if (!value) continue;\n lines.push(` ${kind}: \"${canonicalizeCombo(value)}\"`);\n }\n // If no bindings remain, return an empty block (caller will treat as delete).\n if (lines.length === 2) return '';\n return lines.join('\\n');\n}\n\nexport async function writeHotkeyBindingsConfig(\n cfg: HotkeyBindingsConfig,\n): Promise<void> {\n // Validate + canonicalize + drop reserved-combo collisions before writing.\n const cleaned: Partial<Record<BindableActionKind, string>> = {};\n for (const kind of BINDABLE_ACTION_KINDS) {\n const raw = cfg.bindings[kind];\n if (typeof raw !== 'string' || raw.trim() === '') continue;\n const canonical = canonicalizeCombo(raw);\n if (!canonical) continue;\n if (isReservedCombo(canonical)) continue;\n cleaned[kind] = canonical;\n }\n\n if (Object.keys(cleaned).length === 0) {\n await deleteHotkeyBindingsConfig();\n return;\n }\n\n const configPath = resolve(syntaurRoot(), 'config.md');\n const block = serializeHotkeyBindingsConfig({ bindings: cleaned });\n\n const existing = (await fileExists(configPath))\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${block}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'hotkeys');\n const newFm = `${cleanedFm}\\n${block}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteHotkeyBindingsConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'hotkeys');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nfunction stripTopLevelBlock(fmBlock: string, key: string): string {\n const blockStart = fmBlock.match(new RegExp(`^${key}:\\\\s*$`, 'm'));\n if (!blockStart) {\n return fmBlock.replace(/\\n+$/, '');\n }\n\n // Regex match offset, not indexOf — the `${key}:` text can appear earlier inside\n // another block's value (e.g. `search:` in an AQL string), and indexOf would cut\n // from there, corrupting unrelated frontmatter.\n const startIdx = blockStart.index ?? 0;\n const before = fmBlock.slice(0, startIdx);\n const after = fmBlock.slice(startIdx + blockStart[0].length);\n const remaining = after.split('\\n');\n let endIdx = 0;\n\n for (let i = 0; i < remaining.length; i++) {\n const line = remaining[i];\n if (line.trim() === '') {\n endIdx = i + 1;\n continue;\n }\n if (line.length > 0 && line[0] !== ' ') {\n break;\n }\n endIdx = i + 1;\n }\n\n return (before + remaining.slice(endIdx).join('\\n')).replace(/\\n+$/, '');\n}\n\nfunction parseOptionalAbsolutePath(\n value: string | undefined,\n fieldName: string,\n): string | null {\n if (!value) {\n return null;\n }\n\n const expanded = expandHome(String(value));\n if (!isAbsolute(expanded)) {\n console.warn(\n `Warning: config.md ${fieldName} is not an absolute path (\"${value}\"), ignoring it`,\n );\n return null;\n }\n\n return resolve(expanded);\n}\n\nfunction parseAgentsConfig(content: string): AgentConfig[] | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const agentsStart = fmBlock.match(/^agents:\\s*$/m);\n if (!agentsStart) return null;\n\n const startIdx = fmBlock.indexOf(agentsStart[0]) + agentsStart[0].length;\n const remaining = fmBlock.slice(startIdx);\n const lines = remaining.split('\\n');\n\n const agents: AgentConfig[] = [];\n let current: Partial<AgentConfig> & { args?: string[] } | null = null;\n let argsCapture: string[] | null = null;\n let argsBaseIndent = 0;\n // Active nested block state (e.g. `resume:` or `fork:` sub-mapping under an\n // agent). When `nestedKey` is one of `resume` / `fork`, lines at deeper\n // indent are parsed as that invocation's `command` / `args` fields. When\n // `nestedKey === '__skip__'` we swallow the indented block without\n // recording anything — this is the forward-compat path for unknown nested\n // keys added in future syntaur versions.\n let nestedKey: string | null = null;\n let nestedInvocation: SessionInvocation | null = null;\n let nestedBaseIndent = 0;\n\n function flushCurrent() {\n if (!current) return;\n if (!current.id || !current.command || !current.label) {\n current = null;\n return;\n }\n agents.push({\n id: current.id,\n label: current.label,\n command: current.command,\n ...(current.args && current.args.length > 0 ? { args: current.args } : {}),\n ...(current.promptArgPosition\n ? { promptArgPosition: current.promptArgPosition }\n : {}),\n ...(current.default ? { default: true } : {}),\n ...(current.resolveFromShellAliases ? { resolveFromShellAliases: true } : {}),\n ...(current.model ? { model: current.model } : {}),\n ...(current.playbook ? { playbook: current.playbook } : {}),\n ...(current.launchPrompt ? { launchPrompt: current.launchPrompt } : {}),\n ...(current.resume ? { resume: current.resume } : {}),\n ...(current.fork ? { fork: current.fork } : {}),\n });\n current = null;\n argsCapture = null;\n nestedKey = null;\n nestedInvocation = null;\n }\n\n function closeNestedBlock() {\n if (!nestedKey) return;\n if (current && (nestedKey === 'resume' || nestedKey === 'fork') && nestedInvocation) {\n // Only attach when args were populated — empty invocation is a no-op.\n if (Array.isArray(nestedInvocation.args)) {\n current[nestedKey] = nestedInvocation;\n }\n }\n nestedKey = null;\n nestedInvocation = null;\n argsCapture = null;\n }\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n\n if (indent === 0 && trimmed !== '' && !trimmed.startsWith('#')) {\n closeNestedBlock();\n break; // new top-level key\n }\n\n // Continue capturing list items for the active argsCapture target.\n if (argsCapture) {\n if (indent > argsBaseIndent && trimmed.startsWith('- ')) {\n argsCapture.push(decodeYamlScalar(trimmed.slice(2).trim()));\n continue;\n } else {\n argsCapture = null;\n }\n }\n\n if (indent === 2 && trimmed.startsWith('- ')) {\n closeNestedBlock();\n flushCurrent();\n current = {};\n const rest = trimmed.slice(2).trim();\n const colonIdx = rest.indexOf(':');\n if (colonIdx > 0) {\n const k = rest.slice(0, colonIdx).trim();\n const v = rest.slice(colonIdx + 1).trim();\n assignAgentField(current, k, v);\n }\n continue;\n }\n\n if (!current) continue;\n\n // Inside a nested block (resume / fork / skip-unknown).\n if (nestedKey && indent > nestedBaseIndent) {\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx <= 0) continue;\n const k = trimmed.slice(0, colonIdx).trim();\n const v = trimmed.slice(colonIdx + 1).trim();\n if (nestedKey === 'resume' || nestedKey === 'fork') {\n if (!nestedInvocation) nestedInvocation = { args: [] };\n if (k === 'args' && v === '') {\n nestedInvocation.args = [];\n argsCapture = nestedInvocation.args;\n argsBaseIndent = indent;\n continue;\n }\n if (k === 'command' && v !== '') {\n nestedInvocation.command = decodeYamlScalar(v);\n continue;\n }\n // Unknown nested-of-nested: ignore for forward compat.\n }\n // nestedKey === '__skip__' → swallow without recording.\n continue;\n }\n\n // Returning out to indent 4 (or shallower) — close any open nested block.\n if (nestedKey && indent <= nestedBaseIndent) {\n closeNestedBlock();\n }\n\n if (indent >= 4 && current) {\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx <= 0) continue;\n const k = trimmed.slice(0, colonIdx).trim();\n const v = trimmed.slice(colonIdx + 1).trim();\n if (k === 'args' && v === '') {\n argsCapture = [];\n argsBaseIndent = indent;\n current.args = argsCapture;\n continue;\n }\n // Recognized nested mapping blocks: resume / fork. Empty value + no\n // recognized scalar field → enter nested mode.\n if ((k === 'resume' || k === 'fork') && v === '') {\n nestedKey = k;\n nestedInvocation = { args: [] };\n nestedBaseIndent = indent;\n continue;\n }\n // Unknown key with empty value at agent-field indent: forward-compat\n // skip. Older parsers would crash here once a future version emits\n // a new nested block; this branch lets us pass through gracefully.\n if (v === '' && !KNOWN_AGENT_SCALAR_FIELDS.has(k)) {\n nestedKey = '__skip__';\n nestedInvocation = null;\n nestedBaseIndent = indent;\n continue;\n }\n assignAgentField(current, k, v);\n }\n }\n closeNestedBlock();\n flushCurrent();\n\n if (agents.length === 0) return [];\n return agents;\n}\n\nconst KNOWN_AGENT_SCALAR_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'label',\n 'command',\n 'promptArgPosition',\n 'default',\n 'resolveFromShellAliases',\n 'model',\n 'playbook',\n 'launchPrompt',\n]);\n\n/**\n * Normalize and validate an agents list parsed from config.md. On any\n * AgentConfigError, log a warning and fall back to built-in defaults so a\n * malformed user config does not brick `syntaur browse`. Returns the\n * normalized list (with `command` resolved through `parseAgentCommand`).\n */\nfunction normalizeAgentsFromConfig(agents: AgentConfig[] | null): AgentConfig[] | null {\n if (agents === null) return null;\n try {\n const normalized = agents.map((agent) => ({\n ...agent,\n command: parseAgentCommand(agent.command, agent.id),\n }));\n validateAgentList(normalized);\n return normalized;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(\n `Warning: ~/.syntaur/config.md agents block is invalid (${msg}) — using built-in defaults`,\n );\n return null;\n }\n}\n\n/**\n * Decode a YAML-ish scalar:\n * - Bare values returned verbatim.\n * - Single-quoted: strip outer quotes, unescape '' → '.\n * - Double-quoted: strip outer quotes, unescape \\\\ \\\" \\n \\t \\r.\n * Rejects unterminated quoted scalars (caller should surface as a parse error).\n */\nfunction decodeYamlScalar(value: string): string {\n const trimmed = value.trim();\n if (trimmed.length >= 2 && trimmed.startsWith('\"') && trimmed.endsWith('\"')) {\n const body = trimmed.slice(1, -1);\n let out = '';\n for (let i = 0; i < body.length; i++) {\n const ch = body[i];\n if (ch === '\\\\' && i + 1 < body.length) {\n const next = body[i + 1];\n switch (next) {\n case '\\\\': out += '\\\\'; break;\n case '\"': out += '\"'; break;\n case 'n': out += '\\n'; break;\n case 't': out += '\\t'; break;\n case 'r': out += '\\r'; break;\n default: out += next; break;\n }\n i++;\n continue;\n }\n out += ch;\n }\n return out;\n }\n if (trimmed.length >= 2 && trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\")) {\n return trimmed.slice(1, -1).replace(/''/g, \"'\");\n }\n return trimmed;\n}\n\nfunction assignAgentField(target: Partial<AgentConfig>, key: string, rawValue: string): void {\n const value = decodeYamlScalar(rawValue);\n switch (key) {\n case 'id':\n target.id = value;\n break;\n case 'label':\n target.label = value;\n break;\n case 'command':\n target.command = value;\n break;\n case 'promptArgPosition':\n target.promptArgPosition = value as PromptArgPosition;\n break;\n case 'default':\n target.default = value === 'true';\n break;\n case 'resolveFromShellAliases':\n target.resolveFromShellAliases = value === 'true';\n break;\n case 'model':\n target.model = value;\n break;\n case 'playbook':\n target.playbook = value;\n break;\n case 'launchPrompt':\n target.launchPrompt = value;\n break;\n }\n}\n\nfunction yamlQuoteScalar(value: string): string {\n if (/[\\r\\n]/.test(value)) {\n throw new AgentConfigError(\n `value contains newlines, which the agents config serializer does not support: ${JSON.stringify(value)}`,\n );\n }\n if (value === '' || /[:#{}[\\],&*?|>!%@`\"'\\\\\\t]/.test(value) || /^\\s|\\s$/.test(value)) {\n const escaped = value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\t/g, '\\\\t');\n return `\"${escaped}\"`;\n }\n return value;\n}\n\nfunction serializeAgentsConfig(agents: AgentConfig[]): string {\n const lines: string[] = ['agents:'];\n for (const a of agents) {\n lines.push(` - id: ${yamlQuoteScalar(a.id)}`);\n lines.push(` label: ${yamlQuoteScalar(a.label)}`);\n lines.push(` command: ${yamlQuoteScalar(a.command)}`);\n if (a.model) {\n lines.push(` model: ${yamlQuoteScalar(a.model)}`);\n }\n if (a.playbook) {\n lines.push(` playbook: ${yamlQuoteScalar(a.playbook)}`);\n }\n if (a.launchPrompt) {\n lines.push(` launchPrompt: ${yamlQuoteScalar(a.launchPrompt)}`);\n }\n if (a.args && a.args.length > 0) {\n lines.push(` args:`);\n for (const arg of a.args) {\n lines.push(` - ${yamlQuoteScalar(arg)}`);\n }\n }\n if (a.promptArgPosition && a.promptArgPosition !== 'first') {\n lines.push(` promptArgPosition: ${a.promptArgPosition}`);\n }\n if (a.default) {\n lines.push(` default: true`);\n }\n if (a.resolveFromShellAliases) {\n lines.push(` resolveFromShellAliases: true`);\n }\n if (a.resume) {\n appendSessionInvocation(lines, 'resume', a.resume);\n }\n if (a.fork) {\n appendSessionInvocation(lines, 'fork', a.fork);\n }\n }\n return lines.join('\\n');\n}\n\nfunction appendSessionInvocation(\n lines: string[],\n key: 'resume' | 'fork',\n invocation: SessionInvocation,\n): void {\n lines.push(` ${key}:`);\n if (invocation.command !== undefined) {\n lines.push(` command: ${yamlQuoteScalar(invocation.command)}`);\n }\n lines.push(` args:`);\n for (const arg of invocation.args) {\n lines.push(` - ${yamlQuoteScalar(arg)}`);\n }\n}\n\nexport async function writeAgentsConfig(agents: AgentConfig[]): Promise<void> {\n validateAgentList(agents);\n const configPath = resolve(syntaurRoot(), 'config.md');\n const agentsBlock = serializeAgentsConfig(agents);\n\n const existing = (await fileExists(configPath))\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${agentsBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content.replace(/\\n\\n---/, '\\n---'));\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'agents');\n const newFm = `${cleanedFm}\\n${agentsBlock}`.replace(/^\\n+/, '').replace(/\\n+$/, '');\n const newContent = `---\\n${newFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteAgentsConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'agents');\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function writeStatusConfig(statuses: StatusConfig): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const statusBlock = serializeStatusConfig(statuses);\n\n if (!(await fileExists(configPath))) {\n // Create new config file with defaults + statuses\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ~/projects\\n${statusBlock}\\n---\\n`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n // No frontmatter — wrap in new frontmatter\n const content = `---\\nversion: \"2.0\"\\n${statusBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n\n // Remove existing statuses: block from frontmatter\n const statusesStart = fmBlock.match(/^statuses:\\s*$/m);\n let cleanedFm: string;\n if (statusesStart) {\n const startIdx = fmBlock.indexOf(statusesStart[0]);\n const before = fmBlock.slice(0, startIdx);\n const after = fmBlock.slice(startIdx + statusesStart[0].length);\n // Skip all indented lines (belonging to statuses block)\n const remaining = after.split('\\n');\n let endIdx = 0;\n for (let i = 0; i < remaining.length; i++) {\n const line = remaining[i];\n if (line.trim() === '') { endIdx = i + 1; continue; }\n if (line.length > 0 && line[0] !== ' ') break;\n endIdx = i + 1;\n }\n cleanedFm = before + remaining.slice(endIdx).join('\\n');\n } else {\n cleanedFm = fmBlock;\n }\n\n // Trim trailing whitespace/newlines from cleaned frontmatter\n cleanedFm = cleanedFm.replace(/\\n+$/, '');\n\n const newContent = `---\\n${cleanedFm}\\n${statusBlock}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteStatusConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'statuses');\n\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n/**\n * Parse the nested `search:` block from raw config.md content. Returns null when\n * absent (caller falls back to DEFAULT_SEARCH_CONFIG). Mirrors parseStatusConfig's\n * manual block walk: `defaultScope`/`externalIds` are scalars, `aliases:` is a\n * one-level prefix→kind map. Tolerant — invalid rows are dropped by\n * normalizeSearchConfig.\n */\n/**\n * Parse the optional `staleness:` block into a partial `StaleThresholds`\n * (defaults-first — only keys present here override). Values are durations\n * (`7d`, `12h`, `30m`, `90s`, `500ms`) or bare ms numbers. Malformed/non-positive\n * values are dropped (the gate falls back to its default). Returns null when the\n * block is absent or yields no valid override.\n *\n * staleness:\n * inProgressNoActivity: 14d\n * reviewAging: 2d\n */\nexport function parseStalenessConfig(content: string): Partial<StaleThresholds> | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const blockStart = fmBlock.match(/^staleness:\\s*$/m);\n if (!blockStart) return null;\n\n const startIdx = (blockStart.index ?? 0) + blockStart[0].length;\n const lines = fmBlock.slice(startIdx).split('\\n');\n\n const out: Partial<StaleThresholds> = {};\n for (const line of lines) {\n if (line.trim() === '') continue;\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0) break; // dedent out of the staleness: block\n const ci = trimmed.indexOf(':');\n if (ci <= 0) continue;\n const key = trimmed.slice(0, ci).trim();\n const field = STALENESS_KEY_TO_FIELD[key];\n if (!field) continue;\n let value = trimmed.slice(ci + 1).trim();\n if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n const ms = parseDurationMs(value);\n if (ms !== null) out[field] = ms;\n }\n\n return Object.keys(out).length > 0 ? out : null;\n}\n\n/**\n * Validate the raw `staleness:` block, returning a problem string per offending\n * entry (unknown key, or unparseable/non-positive duration). Empty array = OK\n * (including when the block is absent). The parser fails safe by dropping these\n * silently; this surfaces them in `syntaur doctor` so typos don't go unnoticed.\n */\nexport function validateStalenessConfig(content: string): string[] {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return [];\n const fmBlock = match[1];\n const blockStart = fmBlock.match(/^staleness:\\s*$/m);\n if (!blockStart) return [];\n\n const startIdx = (blockStart.index ?? 0) + blockStart[0].length;\n const lines = fmBlock.slice(startIdx).split('\\n');\n const problems: string[] = [];\n\n for (const line of lines) {\n if (line.trim() === '') continue;\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0) break;\n const ci = trimmed.indexOf(':');\n if (ci <= 0) continue;\n const key = trimmed.slice(0, ci).trim();\n let value = trimmed.slice(ci + 1).trim();\n if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n if (!(key in STALENESS_KEY_TO_FIELD)) {\n problems.push(`staleness.${key}: unknown key (expected one of ${Object.keys(STALENESS_KEY_TO_FIELD).join(', ')})`);\n continue;\n }\n if (parseDurationMs(value) === null) {\n problems.push(`staleness.${key}: \"${value}\" is not a positive duration (e.g. 7d, 12h, 30m, 90s, 500ms)`);\n }\n }\n return problems;\n}\n\nexport function parseSearchConfig(content: string): SearchConfig | null {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return null;\n const fmBlock = match[1];\n\n const blockStart = fmBlock.match(/^search:\\s*$/m);\n if (!blockStart) return null;\n\n // Use the regex match offset (NOT indexOf) — the literal text `search:` can\n // appear earlier inside another block's value (e.g. an AQL derive condition\n // `when: \"search:foo\"`), and indexOf would slice from there.\n const startIdx = (blockStart.index ?? 0) + blockStart[0].length;\n const lines = fmBlock.slice(startIdx).split('\\n');\n\n const unquote = (v: string): string => {\n const t = v.trim();\n if ((t.startsWith('\"') && t.endsWith('\"')) || (t.startsWith(\"'\") && t.endsWith(\"'\"))) {\n return t.slice(1, -1);\n }\n return t;\n };\n\n const raw: { defaultScope?: string; aliases?: Record<string, string>; externalIds?: boolean } = {};\n let inAliases = false;\n\n for (const line of lines) {\n if (line.trim() === '') continue;\n const trimmed = line.trimStart();\n const indent = line.length - trimmed.length;\n if (indent === 0) break; // dedent out of the search: block\n\n if (indent <= 2) {\n inAliases = false;\n if (trimmed === 'aliases:') {\n inAliases = true;\n raw.aliases = {};\n continue;\n }\n const ci = trimmed.indexOf(':');\n if (ci <= 0) continue;\n const key = trimmed.slice(0, ci).trim();\n const value = unquote(trimmed.slice(ci + 1).trim());\n if (key === 'defaultScope') {\n raw.defaultScope = value;\n } else if (key === 'externalIds') {\n // Only recognize real booleans; anything else stays undefined so\n // normalizeSearchConfig falls back to the default (true).\n const v = value.toLowerCase();\n if (v === 'true') raw.externalIds = true;\n else if (v === 'false') raw.externalIds = false;\n }\n } else if (inAliases) {\n const ci = trimmed.indexOf(':');\n if (ci <= 0) continue;\n raw.aliases ??= {};\n raw.aliases[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1).trim());\n }\n }\n\n return normalizeSearchConfig(raw);\n}\n\n/** Serialize a SearchConfig into the `search:` frontmatter block (no trailing newline). */\nexport function serializeSearchConfig(search: SearchConfig): string {\n const cfg = normalizeSearchConfig(search);\n const lines: string[] = ['search:'];\n lines.push(` defaultScope: ${cfg.defaultScope}`);\n lines.push(' aliases:');\n for (const [prefix, kind] of Object.entries(cfg.aliases)) {\n lines.push(` ${prefix}: ${kind}`);\n }\n lines.push(` externalIds: ${cfg.externalIds ? 'true' : 'false'}`);\n return lines.join('\\n');\n}\n\nexport async function writeSearchConfig(search: SearchConfig): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const searchBlock = serializeSearchConfig(search);\n\n if (!(await fileExists(configPath))) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ~/projects\\n${searchBlock}\\n---\\n`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\n${searchBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content);\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'search');\n\n const newContent = `---\\n${cleanedFm}\\n${searchBlock}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function deleteSearchConfig(): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) return;\n\n const existing = await readFile(configPath, 'utf-8');\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) return;\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'search');\n\n const newContent = `---\\n${cleanedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n/** The configured search settings, or the built-in defaults when unset. */\nexport function getSearchConfig(config: SyntaurConfig): SearchConfig {\n return config.searchConfig ?? DEFAULT_SEARCH_CONFIG;\n}\n\nexport async function updateIntegrationConfig(\n integrations: Partial<IntegrationConfig>,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const nextIntegrations: IntegrationConfig = {\n ...(await readConfig()).integrations,\n ...integrations,\n };\n\n const integrationBlock = serializeIntegrationConfig(nextIntegrations);\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${integrationBlock ?? ''}\\n---\\n${existing}`;\n await writeFileForce(configPath, content.replace(/\\n\\n---/, '\\n---'));\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'integrations');\n const newFm = integrationBlock\n ? `${cleanedFm}\\n${integrationBlock}`.replace(/^\\n+/, '')\n : cleanedFm;\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function updateOnboardingConfig(\n onboarding: Partial<OnboardingConfig>,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const nextOnboarding: OnboardingConfig = {\n ...(await readConfig()).onboarding,\n ...onboarding,\n };\n\n const onboardingBlock = serializeOnboardingConfig(nextOnboarding);\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${onboardingBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content.replace(/\\n\\n---/, '\\n---'));\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'onboarding');\n const newFm = `${cleanedFm}\\n${onboardingBlock}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\nexport async function updateBackupConfig(\n backup: Partial<BackupConfig>,\n): Promise<void> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n const current = (await readConfig()).backup;\n const nextBackup: BackupConfig = {\n repo: current?.repo ?? null,\n categories: current?.categories ?? 'projects, playbooks, todos, servers, config',\n lastBackup: current?.lastBackup ?? null,\n lastRestore: current?.lastRestore ?? null,\n ...backup,\n };\n\n const backupBlock = serializeBackupConfig(nextBackup);\n const existing = await fileExists(configPath)\n ? await readFile(configPath, 'utf-8')\n : renderConfig({ defaultProjectDir: defaultProjectDir() });\n\n const fmMatch = existing.match(/^(---\\n)([\\s\\S]*?)\\n(---)/);\n if (!fmMatch) {\n const content = `---\\nversion: \"2.0\"\\ndefaultProjectDir: ${defaultProjectDir()}\\n${backupBlock}\\n---\\n${existing}`;\n await writeFileForce(configPath, content.replace(/\\n\\n---/, '\\n---'));\n return;\n }\n\n const fmBlock = fmMatch[2];\n const afterFrontmatter = existing.slice(fmMatch[0].length);\n const cleanedFm = stripTopLevelBlock(fmBlock, 'backup');\n const newFm = `${cleanedFm}\\n${backupBlock}`.replace(/^\\n+/, '');\n const normalizedFm = newFm.replace(/\\n+$/, '');\n const newContent = `---\\n${normalizedFm}\\n---${afterFrontmatter}`;\n await writeFileForce(configPath, newContent);\n}\n\n// Guard so the legacy-config migration runs at most once per config path per\n// process lifetime. Keyed by absolute path so tests with multiple sandbox\n// HOMEs still get the migration applied to each.\nconst migratedConfigPaths = new Set<string>();\n\nexport async function readConfig(): Promise<SyntaurConfig> {\n const configPath = resolve(syntaurRoot(), 'config.md');\n if (!(await fileExists(configPath))) {\n return cloneDefaultConfig();\n }\n\n if (!migratedConfigPaths.has(configPath)) {\n migratedConfigPaths.add(configPath);\n await migrateLegacyConfig(configPath);\n }\n\n const content = await readFile(configPath, 'utf-8');\n const fm = parseFrontmatter(content);\n\n if (Object.keys(fm).length === 0) {\n console.warn('Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults');\n return cloneDefaultConfig();\n }\n\n let projectDir = fm['defaultProjectDir']\n ? expandHome(String(fm['defaultProjectDir']))\n : DEFAULT_CONFIG.defaultProjectDir;\n if (!isAbsolute(projectDir)) {\n console.warn(\n `Warning: config.md defaultProjectDir is not an absolute path (\"${fm['defaultProjectDir']}\"), using default`,\n );\n projectDir = DEFAULT_CONFIG.defaultProjectDir;\n }\n\n const fmBlock = content.match(/^---\\n([\\s\\S]*?)\\n---/)?.[1] ?? '';\n\n return {\n version: fm['version'] || DEFAULT_CONFIG.version,\n defaultProjectDir: projectDir,\n onboarding: {\n completed: fm['onboarding.completed'] === 'true',\n },\n agentDefaults: {\n trustLevel:\n (fm['agentDefaults.trustLevel'] as SyntaurConfig['agentDefaults']['trustLevel']) ||\n DEFAULT_CONFIG.agentDefaults.trustLevel,\n autoApprove:\n fm['agentDefaults.autoApprove'] === 'true' ||\n DEFAULT_CONFIG.agentDefaults.autoApprove,\n autoCreateWorktree: AUTO_CREATE_WORKTREE_VALUES.includes(\n fm['agentDefaults.autoCreateWorktree'] as AutoCreateWorktree,\n )\n ? (fm['agentDefaults.autoCreateWorktree'] as AutoCreateWorktree)\n : DEFAULT_CONFIG.agentDefaults.autoCreateWorktree,\n },\n session: {\n autoTrack: SESSION_AUTO_TRACK_VALUES.includes(\n fm['session.autoTrack'] as SessionAutoTrack,\n )\n ? (fm['session.autoTrack'] as SessionAutoTrack)\n : DEFAULT_CONFIG.session.autoTrack,\n },\n integrations: {\n claudePluginDir: parseOptionalAbsolutePath(\n fm['integrations.claudePluginDir'],\n 'integrations.claudePluginDir',\n ),\n codexPluginDir: parseOptionalAbsolutePath(\n fm['integrations.codexPluginDir'],\n 'integrations.codexPluginDir',\n ),\n codexMarketplacePath: parseOptionalAbsolutePath(\n fm['integrations.codexMarketplacePath'],\n 'integrations.codexMarketplacePath',\n ),\n ...parseInstalledAgents(fm),\n },\n backup: fm['backup.repo'] || fm['backup.categories']\n ? {\n repo: fm['backup.repo'] && fm['backup.repo'] !== 'null' ? fm['backup.repo'] : null,\n categories: fm['backup.categories'] || 'projects, playbooks, todos, servers, config',\n lastBackup: fm['backup.lastBackup'] && fm['backup.lastBackup'] !== 'null' ? fm['backup.lastBackup'] : null,\n lastRestore: fm['backup.lastRestore'] && fm['backup.lastRestore'] !== 'null' ? fm['backup.lastRestore'] : null,\n }\n : null,\n statuses: parseStatusConfig(content),\n types: null,\n agents: normalizeAgentsFromConfig(parseAgentsConfig(content)),\n playbooks: parsePlaybooksConfig(fmBlock),\n theme: parseThemeConfig(content),\n hotkeys: parseHotkeyBindingsConfig(content),\n terminal: (() => {\n try {\n return parseTerminalConfig(fm['terminal']);\n } catch (err) {\n const msg = err instanceof TerminalConfigError ? err.message : String(err);\n console.warn(`Warning: ${msg} — falling back to default`);\n return null;\n }\n })(),\n searchConfig: parseSearchConfig(content),\n workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock),\n staleness: parseStalenessConfig(content),\n stalenessWatchdog: String(fm['stalenessWatchdog']).toLowerCase() === 'true',\n };\n}\n\nexport function getAssignmentTypes(config: SyntaurConfig): TypesConfig {\n return config.types ?? DEFAULT_ASSIGNMENT_TYPES;\n}\n\nexport function getAgents(config: SyntaurConfig): AgentConfig[] {\n if (config.agents === null) return BUILTIN_AGENTS;\n // For agents whose id matches any builtin (claude/codex/pi/openclaw/hermes),\n // inherit that builtin's resume/fork for whichever the user omitted. Builtins\n // without a recipe (openclaw/hermes) have nothing to inherit, so an omitted\n // field stays omitted. Omission means \"inherit\", not\n // \"disable\": there is no syntax to express intentional disable, and the\n // dashboard agent editor (api-agents coerceAgentRow) silently drops these\n // fields, so omission is frequently accidental. User-provided values win;\n // non-builtin agents pass through untouched. Inputs are never mutated.\n const builtinById = new Map(BUILTIN_AGENTS.map((a) => [a.id, a]));\n return config.agents.map((agent) => {\n const builtin = builtinById.get(agent.id);\n if (!builtin) return agent;\n const resume = agent.resume ?? builtin.resume;\n const fork = agent.fork ?? builtin.fork;\n if (resume === agent.resume && fork === agent.fork) return agent;\n return {\n ...agent,\n ...(resume ? { resume } : {}),\n ...(fork ? { fork } : {}),\n };\n });\n}\n\nexport class TerminalConfigError extends Error {}\n\n/**\n * Parse the `terminal:` scalar from raw frontmatter values.\n * Returns null when the key is absent (caller falls back to platform default).\n * Throws TerminalConfigError when the value is not a known choice.\n */\nexport function parseTerminalConfig(value: unknown): TerminalChoice | null {\n if (value === undefined || value === null || value === '') return null;\n if (typeof value !== 'string') {\n throw new TerminalConfigError(\n `terminal must be a string — got ${typeof value}`,\n );\n }\n const trimmed = value.trim();\n if (trimmed === '') return null;\n if (!TERMINAL_CHOICES.includes(trimmed as TerminalChoice)) {\n throw new TerminalConfigError(\n `terminal \"${trimmed}\" is not a known choice — expected one of ${TERMINAL_CHOICES.join('|')}`,\n );\n }\n return trimmed as TerminalChoice;\n}\n\n/**\n * Return the configured terminal, or the platform default when unset.\n *\n * darwin → terminal-app (always available).\n * linux → first of [kitty, alacritty, warp] resolvable via `which`, in that\n * order. If none are installed, return terminal-app as a stable\n * sentinel (doctor will surface the install gap separately).\n * other → terminal-app sentinel.\n *\n * The Linux probe order is intentionally deterministic and documented so the\n * dashboard's preflight + the Settings hint show the same value.\n */\nexport function getTerminal(config: SyntaurConfig): TerminalChoice {\n if (config.terminal) return config.terminal;\n if (process.platform === 'darwin') return 'terminal-app';\n if (process.platform === 'linux') {\n const order: TerminalChoice[] = ['kitty', 'alacritty', 'warp'];\n for (const candidate of order) {\n const result = spawnSync('which', [candidate], { encoding: 'utf-8' });\n if (result.status === 0 && result.stdout.trim().length > 0) {\n return candidate;\n }\n }\n }\n return 'terminal-app';\n}\n\nexport interface AgentsMutation {\n kind: 'add' | 'remove' | 'set' | 'reorder';\n apply: (current: AgentConfig[]) => AgentConfig[];\n}\n\n/**\n * Apply a mutation to the agents list, validate, and either write or return the\n * proposed new list (for --dry-run). Always runs full validation.\n */\nexport async function updateAgentsConfig(\n mutation: AgentsMutation,\n options: { dryRun?: boolean } = {},\n): Promise<{ previous: AgentConfig[]; next: AgentConfig[]; written: boolean }> {\n const config = await readConfig();\n const previous = config.agents ?? [...BUILTIN_AGENTS];\n const next = mutation.apply(previous);\n validateAgentList(next);\n\n if (options.dryRun) {\n return { previous, next, written: false };\n }\n\n await writeAgentsConfig(next);\n return { previous, next, written: true };\n}\n","import { resolve } from 'node:path';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { fileExists } from './fs.js';\nimport { extractFrontmatter, getField } from '../dashboard/parser.js';\n\nexport interface ResolvedAssignment {\n assignmentDir: string;\n projectSlug: string | null;\n assignmentSlug: string;\n id: string;\n standalone: boolean;\n workspaceGroup: string | null;\n}\n\nexport async function resolveAssignmentById(\n projectsDir: string,\n assignmentsDir: string,\n id: string,\n): Promise<ResolvedAssignment | null> {\n let standaloneMatch: ResolvedAssignment | null = null;\n let projectMatch: ResolvedAssignment | null = null;\n\n // 1) Standalone: <assignmentsDir>/<id>/assignment.md\n const standaloneDir = resolve(assignmentsDir, id);\n const standalonePath = resolve(standaloneDir, 'assignment.md');\n if (await fileExists(standalonePath)) {\n let workspaceGroup: string | null = null;\n try {\n const content = await readFile(standalonePath, 'utf-8');\n const [fm] = extractFrontmatter(content);\n workspaceGroup = getField(fm, 'workspaceGroup');\n } catch {\n // unreadable — leave null\n }\n standaloneMatch = {\n assignmentDir: standaloneDir,\n projectSlug: null,\n assignmentSlug: id,\n id,\n standalone: true,\n workspaceGroup,\n };\n }\n\n // 2) Project-nested: scan <projectsDir>/*/assignments/*/assignment.md and match by frontmatter id\n if (await fileExists(projectsDir)) {\n try {\n const projects = await readdir(projectsDir, { withFileTypes: true });\n for (const p of projects) {\n if (!p.isDirectory()) continue;\n if (p.name.startsWith('.') || p.name.startsWith('_')) continue;\n const assignmentsPath = resolve(projectsDir, p.name, 'assignments');\n if (!(await fileExists(assignmentsPath))) continue;\n\n const entries = await readdir(assignmentsPath, { withFileTypes: true });\n for (const a of entries) {\n if (!a.isDirectory()) continue;\n const aPath = resolve(assignmentsPath, a.name, 'assignment.md');\n if (!(await fileExists(aPath))) continue;\n\n try {\n const content = await readFile(aPath, 'utf-8');\n const [fm] = extractFrontmatter(content);\n const fileId = getField(fm, 'id');\n if (fileId === id) {\n projectMatch = {\n assignmentDir: resolve(assignmentsPath, a.name),\n projectSlug: p.name,\n assignmentSlug: a.name,\n id,\n standalone: false,\n workspaceGroup: null,\n };\n break;\n }\n } catch {\n // skip unreadable\n }\n }\n if (projectMatch) break;\n }\n } catch {\n // projectsDir not readable\n }\n }\n\n if (standaloneMatch && projectMatch) {\n console.warn(\n `Duplicate assignment ID ${id} found in both standalone and project-nested locations; using standalone`,\n );\n return standaloneMatch;\n }\n\n return standaloneMatch ?? projectMatch ?? null;\n}\n","/**\n * Derived-status dimension engine (design v3, Piece 2) — PURE.\n *\n * Evaluates the configured phase ladder + disposition rules over an\n * assignment's facts and projects the headline status. No filesystem access\n * and no Node-only imports — the dashboard client can evaluate the same rules\n * over server-materialized facts. Fact *computation* lives in `facts.ts`\n * (Node-side).\n *\n * Invariants enforced here:\n * - Terminal assignments defer entirely: callers get `null` and must leave\n * every dimension as-is (terminal is reached only via the gated\n * complete/fail transitions; `reopen` re-enters derivation).\n * - Derive conditions evaluate over FACTS ONLY (`DERIVE_FIELDS`): time-based\n * fields are not in the registry, so a `statusAge > 3d` rung is a config\n * validation error — time drives payload flags, never dimensions.\n * - The override is folded into the effective status here (write-side), but\n * a terminal or unknown override target is ignored (defense in depth — the\n * pin CLI already refuses those).\n */\n\nimport type { DeriveConfig } from '../utils/derive-config.js';\nimport { compileNode, CompileError, parseQuery, type Predicate, type FieldRegistry } from '../utils/query/index.js';\nimport type { StatusOverride } from './types.js';\nimport { DERIVE_FIELDS } from '../utils/fact-registry.js';\n\n// Re-export all fact-vocabulary symbols so existing imports from this module\n// keep resolving without change.\nexport type { FactDeclaration } from '../utils/fact-registry.js';\nexport type { FactFieldNames } from '../utils/fact-registry.js';\nexport {\n DERIVE_FIELDS,\n factFieldNames,\n acceptFactDeclarations,\n addFactFields,\n buildDeriveRegistry,\n buildQueryRegistry,\n queryFieldNames,\n} from '../utils/fact-registry.js';\n\n/** The fixed built-in fact set (the 14 derived-status v3 facts). Custom facts\n * extend {@link AssignmentFacts} dynamically via the config-declared registry. */\nexport interface BuiltinFacts {\n hasRealObjective: boolean;\n acRealTotal: number;\n acRealChecked: number;\n acAllChecked: boolean;\n planExists: boolean;\n planApproved: boolean;\n workspaceSet: boolean;\n implementationStarted: boolean;\n depsSatisfied: boolean;\n unresolvedQuestions: number;\n blocked: boolean;\n parked: boolean;\n reviewRequested: boolean;\n pinned: boolean;\n}\n\n/**\n * The fact set dimensions derive from. Computed by `facts.ts` (Node) or shipped\n * in dashboard payloads (browser). The 14 built-ins are always present; custom\n * declared facts (bool/number) and attestation exports (`<name>`,\n * `<name>Approved`, … as boolean / actor `string[]`) ride in the open index.\n */\nexport type AssignmentFacts = BuiltinFacts &\n Record<string, boolean | number | string[]>;\n\n/** Validate one derive condition against a field registry (defaults to the\n * facts-only base). Returns an error message or null. Plugs into\n * validateDeriveConfig; pass a custom registry to accept declared fact names. */\nexport function validateDeriveCondition(\n when: string,\n registry: FieldRegistry = DERIVE_FIELDS,\n): string | null {\n if (when === '*') return null;\n const parsed = parseQuery(when);\n if (!parsed.ast) return parsed.errors[0]?.message ?? 'unparseable condition';\n try {\n compileNode(parsed.ast, registry);\n return null;\n } catch (err) {\n if (err instanceof CompileError) return err.errors[0]?.message ?? 'invalid condition';\n throw err;\n }\n}\n\nexport interface DerivedDimensions {\n /** Highest satisfied ladder rung (regressible — replan can drop it). */\n phase: string;\n disposition: 'active' | 'blocked' | 'parked';\n /** Headline projection BEFORE the override — payload-only, powers the\n * \"pinned to X — would otherwise be Y\" divergence display. */\n derivedStatus: string;\n /** Effective headline (override folded in) — what gets written to `status`. */\n status: string;\n /** The matched rung's `next:` label — the per-ticket call to action. */\n nextAction: string | null;\n}\n\n// Compiled-condition cache, keyed by REGISTRY object identity — config reloads\n// and config-resolution build a fresh registry → fresh cache. Keying by\n// registry (not config) lets all default-config derivations share the base\n// DERIVE_FIELDS cache, while a custom-vocabulary config gets its own. Callers\n// must build ONE registry per config resolution for sweeps to stay cached.\nconst conditionCache = new WeakMap<FieldRegistry, Map<string, Predicate>>();\n\nfunction compiledWhen(registry: FieldRegistry, when: string): Predicate {\n let cache = conditionCache.get(registry);\n if (!cache) {\n cache = new Map();\n conditionCache.set(registry, cache);\n }\n let pred = cache.get(when);\n if (!pred) {\n if (when === '*') {\n pred = () => true;\n } else {\n const parsed = parseQuery(when);\n if (!parsed.ast) {\n throw new CompileError(parsed.errors);\n }\n pred = compileNode(parsed.ast, registry);\n }\n cache.set(when, pred);\n }\n return pred;\n}\n\nexport interface DeriveInput {\n facts: AssignmentFacts;\n derive: DeriveConfig;\n /** Current headline status from frontmatter (for the terminal check). */\n currentStatus: string;\n terminalStatuses: ReadonlySet<string>;\n /** Defined status ids — headline targets outside this set fall back to phase. */\n knownStatusIds: ReadonlySet<string>;\n override: StatusOverride | null;\n /** Field registry the `when` conditions compile against. Defaults to the base\n * facts-only registry; callers with custom facts pass the resolution's\n * `buildDeriveRegistry(...)` output (ONE per config resolution — see the\n * compile-cache note). */\n registry?: FieldRegistry;\n}\n\n/**\n * Derive phase/disposition/headline for one assignment. Returns `null` when\n * the assignment is terminal — derivation defers entirely until `reopen`.\n */\nexport function deriveDimensions(input: DeriveInput): DerivedDimensions | null {\n const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;\n const registry = input.registry ?? DERIVE_FIELDS;\n\n if (terminalStatuses.has(currentStatus)) return null;\n\n const ctx = { now: 0 }; // derive conditions are time-free by construction\n const item = facts as unknown as Record<string, unknown>;\n\n // Phase: HIGHEST satisfied rung wins (iterate top-down). The bottom rung is\n // conventionally `*`; if nothing matches (misconfigured ladder), fall back\n // to the bottom rung's phase rather than inventing a status.\n let phase = derive.phaseLadder[0]?.phase ?? currentStatus;\n let nextAction: string | null = derive.phaseLadder[0]?.next ?? null;\n for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {\n const rung = derive.phaseLadder[i];\n if (compiledWhen(registry, rung.when)(item, ctx)) {\n phase = rung.phase;\n nextAction = rung.next ?? null;\n break;\n }\n }\n\n // Disposition: first match wins; `when: null` is the else arm.\n let disposition: DerivedDimensions['disposition'] = 'active';\n for (const rule of derive.disposition) {\n if (rule.when === null || compiledWhen(registry, rule.when)(item, ctx)) {\n disposition = rule.is as DerivedDimensions['disposition'];\n break;\n }\n }\n\n // Headline projection. Unknown target ids (e.g. parked without a `parked`\n // status definition) fall back to the phase so the board never shows an\n // undefined status; doctor surfaces the missing definition.\n let derivedStatus: string;\n switch (disposition) {\n case 'parked':\n derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;\n break;\n case 'blocked':\n derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;\n break;\n default:\n derivedStatus = phase;\n }\n\n // Fold the override (effective = override ?? derived). Terminal or unknown\n // targets are ignored — the pin CLI refuses them, this is defense in depth.\n let status = derivedStatus;\n if (\n override &&\n override.status &&\n !terminalStatuses.has(override.status) &&\n knownStatusIds.has(override.status)\n ) {\n status = override.status;\n }\n\n return { phase, disposition, derivedStatus, status, nextAction };\n}\n","import { resolve } from 'node:path';\nimport { readdir, readFile, unlink } from 'node:fs/promises';\nimport { fileExists, writeFileForce } from './fs.js';\nimport { parsePlaybook, type ParsedPlaybook } from '../dashboard/parser.js';\nimport { nowTimestamp } from './timestamp.js';\nimport { readConfig, updatePlaybooksConfig } from './config.js';\nimport { isValidSlug } from './slug.js';\n\nexport interface ResolvedPlaybook {\n filename: string;\n slug: string;\n parsed: ParsedPlaybook;\n}\n\nexport type PlaybookErrorCode = 'manifest' | 'not-found' | 'invalid-slug' | 'collision';\n\n/**\n * Stable error thrown by playbook helpers. Routers and CLI commands branch on\n * `code` to map to HTTP status / exit code without string matching.\n */\nexport class PlaybookError extends Error {\n readonly code: PlaybookErrorCode;\n constructor(code: PlaybookErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'PlaybookError';\n }\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Replace or insert a frontmatter scalar field. Playbook files always have\n * frontmatter (parsePlaybook depends on it). If the field is absent, insert it\n * just before the closing `---`. Values are written verbatim (caller decides\n * quoting).\n */\nfunction setFrontmatterField(content: string, key: string, value: string): string {\n const regex = new RegExp(`^(${escapeRegExp(key)}:)\\\\s*.*$`, 'm');\n if (regex.test(content)) {\n return content.replace(regex, `$1 ${value}`);\n }\n const closingIdx = content.indexOf('\\n---', 4);\n if (closingIdx === -1) {\n return content;\n }\n return `${content.slice(0, closingIdx)}\\n${key}: ${value}${content.slice(closingIdx)}`;\n}\n\nfunction isVisiblePlaybookFile(name: string, isFile: boolean): boolean {\n return isFile && name.endsWith('.md') && !name.startsWith('_') && name !== 'manifest.md';\n}\n\n/**\n * Resolve a requested slug to a concrete playbook file.\n *\n * Canonical slug is the `slug` field in the playbook's frontmatter. If that\n * field is missing we fall back to the filename stem. This means a playbook\n * with `filename: foo.md` and frontmatter `slug: bar` is reachable by `bar`\n * (and NOT by `foo`) — this keeps behavior consistent across dashboard +\n * CLI so enable/disable state is addressable by a single canonical slug.\n */\nexport async function resolvePlaybookSlug(\n playbooksDir: string,\n slug: string,\n): Promise<ResolvedPlaybook | null> {\n if (!(await fileExists(playbooksDir))) return null;\n\n const entries = await readdir(playbooksDir, { withFileTypes: true });\n\n let filenameStemFallback: ResolvedPlaybook | null = null;\n\n for (const entry of entries) {\n if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;\n\n const filePath = resolve(playbooksDir, entry.name);\n const raw = await readFile(filePath, 'utf-8');\n const parsed = parsePlaybook(raw);\n const canonical = parsed.slug || entry.name.replace(/\\.md$/, '');\n\n if (canonical === slug) {\n return { filename: entry.name, slug: canonical, parsed };\n }\n\n // Only use the filename stem as a fallback when frontmatter slug is absent.\n if (!parsed.slug && entry.name.replace(/\\.md$/, '') === slug) {\n filenameStemFallback = { filename: entry.name, slug: canonical, parsed };\n }\n }\n\n return filenameStemFallback;\n}\n\n/**\n * List the canonical slugs of all installed playbook files in `playbooksDir`\n * (enabled or not). Canonical slug is the frontmatter `slug`, falling back to\n * the filename stem — matching `resolvePlaybookSlug`. Returns an empty set if\n * the directory is absent. Used at launch to validate `@<playbook-slug>` tokens\n * in an agent's `launchPrompt`.\n */\nexport async function listPlaybookSlugs(playbooksDir: string): Promise<Set<string>> {\n const slugs = new Set<string>();\n if (!(await fileExists(playbooksDir))) return slugs;\n\n const entries = await readdir(playbooksDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;\n const filePath = resolve(playbooksDir, entry.name);\n const raw = await readFile(filePath, 'utf-8');\n const parsed = parsePlaybook(raw);\n slugs.add(parsed.slug || entry.name.replace(/\\.md$/, ''));\n }\n return slugs;\n}\n\n/**\n * Toggle a playbook's enabled/disabled state. Writes config.md, rebuilds the\n * manifest, and returns the canonical slug + resulting enabled flag.\n *\n * Throws if the slug cannot be resolved to a playbook file.\n */\nexport async function setPlaybookEnabled(\n playbooksDir: string,\n slug: string,\n enabled: boolean,\n): Promise<{ slug: string; enabled: boolean; changed: boolean }> {\n const resolved = await resolvePlaybookSlug(playbooksDir, slug);\n if (!resolved) {\n throw new Error(`Playbook \"${slug}\" not found in ${playbooksDir}`);\n }\n\n const config = await readConfig();\n const disabledSet = new Set(config.playbooks.disabled);\n const wasDisabled = disabledSet.has(resolved.slug);\n const shouldBeDisabled = !enabled;\n\n if (wasDisabled === shouldBeDisabled) {\n return { slug: resolved.slug, enabled, changed: false };\n }\n\n if (shouldBeDisabled) {\n disabledSet.add(resolved.slug);\n } else {\n disabledSet.delete(resolved.slug);\n }\n\n await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });\n await rebuildPlaybookManifest(playbooksDir);\n\n return { slug: resolved.slug, enabled, changed: true };\n}\n\n/**\n * Load a playbook ONLY if it is enabled. Returns null when the playbook does\n * not exist OR is disabled in config. Intended for agent-facing lookups that\n * must respect the disabled state.\n *\n * Dashboard admin code should NOT use this — it uses the unfiltered\n * `getPlaybookDetail` so admins can still see and re-enable disabled playbooks.\n */\nexport async function loadEnabledPlaybook(\n playbooksDir: string,\n slug: string,\n): Promise<ParsedPlaybook | null> {\n const resolved = await resolvePlaybookSlug(playbooksDir, slug);\n if (!resolved) return null;\n\n const config = await readConfig();\n if (config.playbooks.disabled.includes(resolved.slug)) {\n return null;\n }\n\n return resolved.parsed;\n}\n\n/**\n * Remove a slug from the disabled list. Called when a playbook is deleted so\n * a later reincarnation with the same slug doesn't silently start disabled.\n * No-op if the slug isn't currently disabled.\n */\nexport async function removeFromDisabledList(slug: string): Promise<void> {\n const config = await readConfig();\n if (!config.playbooks.disabled.includes(slug)) return;\n await updatePlaybooksConfig({\n disabled: config.playbooks.disabled.filter((s) => s !== slug),\n });\n}\n\nexport async function rebuildPlaybookManifest(playbooksDir: string): Promise<void> {\n if (!(await fileExists(playbooksDir))) return;\n\n const config = await readConfig();\n const disabledSet = new Set(config.playbooks.disabled);\n\n const entries = await readdir(playbooksDir, { withFileTypes: true });\n const rows: Array<{ name: string; slug: string; description: string; whenToUse: string }> = [];\n\n for (const entry of entries) {\n if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;\n\n const raw = await readFile(resolve(playbooksDir, entry.name), 'utf-8');\n const parsed = parsePlaybook(raw);\n const slug = parsed.slug || entry.name.replace(/\\.md$/, '');\n\n if (disabledSet.has(slug)) continue;\n\n rows.push({\n name: parsed.name || slug,\n slug,\n description: parsed.description,\n whenToUse: parsed.whenToUse,\n });\n }\n\n rows.sort((a, b) => a.name.localeCompare(b.name));\n\n const timestamp = nowTimestamp();\n const lines = [\n '---',\n `generated: \"${timestamp}\"`,\n `total: ${rows.length}`,\n '---',\n '',\n '# Playbooks',\n '',\n 'Behavioral rules for AI agents. Read and follow all playbooks before starting work.',\n '',\n ];\n\n for (const row of rows) {\n lines.push(`- **[${row.name}](${row.slug}.md)** — ${row.description}`);\n if (row.whenToUse) {\n lines.push(` _When to use: ${row.whenToUse}_`);\n }\n }\n\n lines.push('');\n\n await writeFileForce(resolve(playbooksDir, 'manifest.md'), lines.join('\\n'));\n}\n\n/**\n * Delete a playbook file from disk and regenerate the manifest. Refuses\n * `manifest`. Drops the slug from `config.playbooks.disabled` if present so a\n * later recreation with the same slug doesn't silently start disabled. Throws\n * `PlaybookError` on `manifest` / `not-found`.\n *\n * Shared by `DELETE /api/playbooks/:slug` and `syntaur delete-playbook`.\n */\nexport async function deletePlaybook(\n playbooksDir: string,\n slug: string,\n): Promise<{ slug: string }> {\n if (slug === 'manifest') {\n throw new PlaybookError('manifest', 'The playbook manifest cannot be deleted.');\n }\n\n const resolved = await resolvePlaybookSlug(playbooksDir, slug);\n if (!resolved) {\n throw new PlaybookError('not-found', `Playbook \"${slug}\" not found.`);\n }\n\n await unlink(resolve(playbooksDir, resolved.filename));\n await removeFromDisabledList(resolved.slug);\n await rebuildPlaybookManifest(playbooksDir);\n\n return { slug: resolved.slug };\n}\n\n/**\n * Rename a playbook to a new slug. Validates the new slug, refuses `manifest`,\n * and rejects collisions at both filename and canonical-slug levels. Updates\n * the on-disk file's frontmatter `slug:` field. Migrates the disabled-list\n * entry if needed. Regenerates the manifest.\n *\n * Special case: if `oldPath === newPath` (e.g., file is `foo.md` with\n * frontmatter `slug: bar`, caller renames `bar -> foo`), rewrite the file in\n * place without unlinking. Returns `renamedInPlace: true` in that case.\n */\nexport async function renamePlaybook(\n playbooksDir: string,\n oldSlug: string,\n newSlug: string,\n): Promise<{ from: string; to: string; renamedInPlace: boolean }> {\n if (!isValidSlug(newSlug)) {\n throw new PlaybookError(\n 'invalid-slug',\n `Invalid slug \"${newSlug}\". Slugs must be lowercase, hyphen-separated, with no special characters.`,\n );\n }\n if (newSlug === 'manifest') {\n throw new PlaybookError('manifest', 'A playbook cannot be named \"manifest\".');\n }\n\n const resolved = await resolvePlaybookSlug(playbooksDir, oldSlug);\n if (!resolved) {\n throw new PlaybookError('not-found', `Playbook \"${oldSlug}\" not found.`);\n }\n\n const oldPath = resolve(playbooksDir, resolved.filename);\n const newPath = resolve(playbooksDir, `${newSlug}.md`);\n\n // Rename-in-place: e.g., file `foo.md` with `slug: bar` renamed `bar -> foo`.\n // The on-disk filename doesn't change; only the frontmatter slug field does.\n const renamedInPlace = oldPath === newPath;\n\n if (!renamedInPlace) {\n // Filename collision: another file already occupies the new path.\n if (await fileExists(newPath)) {\n throw new PlaybookError(\n 'collision',\n `A playbook file already exists at \"${newSlug}.md\".`,\n );\n }\n // Canonical-slug collision: another file declares this slug in its frontmatter.\n const existing = await resolvePlaybookSlug(playbooksDir, newSlug);\n if (existing && resolve(playbooksDir, existing.filename) !== oldPath) {\n throw new PlaybookError(\n 'collision',\n `Another playbook already uses the canonical slug \"${newSlug}\".`,\n );\n }\n }\n\n const raw = await readFile(oldPath, 'utf-8');\n let next = setFrontmatterField(raw, 'slug', newSlug);\n next = setFrontmatterField(next, 'updated', `\"${nowTimestamp()}\"`);\n\n await writeFileForce(newPath, next);\n if (!renamedInPlace) {\n await unlink(oldPath);\n }\n\n // Migrate disabled-list entry if the old canonical slug was disabled.\n const config = await readConfig();\n if (config.playbooks.disabled.includes(resolved.slug)) {\n const nextDisabled = config.playbooks.disabled\n .filter((s) => s !== resolved.slug)\n .concat(newSlug);\n await updatePlaybooksConfig({ disabled: Array.from(new Set(nextDisabled)).sort() });\n }\n\n await rebuildPlaybookManifest(playbooksDir);\n\n return { from: resolved.slug, to: newSlug, renamedInPlace };\n}\n","import { existsSync, statSync } from 'node:fs';\nimport { isAbsolute } from 'node:path';\n\n/**\n * True only for an absolute path that exists and is a directory. Wraps the\n * `statSync` call so a race (deleted between `existsSync` and `statSync`) or a\n * permission error resolves to `false` rather than throwing.\n */\nexport function isExistingDir(p: string | null | undefined): boolean {\n if (!p || !isAbsolute(p)) return false;\n try {\n return existsSync(p) && statSync(p).isDirectory();\n } catch {\n return false;\n }\n}\n\nexport interface WorkspaceCwdInput {\n worktreePath: string | null;\n repository: string | null;\n branch: string | null;\n assignmentSlug: string;\n}\n\nexport interface WorkspaceCwdResult {\n /** Resolved, validated working directory, or `null` when none is valid. */\n cwd: string | null;\n /** Non-fatal warning when falling back from a missing/invalid worktree. */\n fallbackWarning: string | null;\n /** Human-readable reason, set only when `cwd` is `null`. */\n invalidReason: string | null;\n}\n\n/**\n * Resolve the working directory for a launch, preferring a validated\n * `worktreePath`, then a validated `repository`. NEVER returns `process.cwd()`:\n * when neither is an existing directory, returns `{ cwd: null, invalidReason }`\n * so the caller decides whether to fail (assignment launches) or fall back to\n * its own path (session launches keep `session.path`).\n */\nexport function resolveWorkspaceCwd(\n input: WorkspaceCwdInput,\n): WorkspaceCwdResult {\n const { worktreePath, repository, branch, assignmentSlug } = input;\n\n if (isExistingDir(worktreePath)) {\n return { cwd: worktreePath, fallbackWarning: null, invalidReason: null };\n }\n\n if (isExistingDir(repository)) {\n // A present-but-invalid worktreePath gets a dedicated warning; a missing\n // worktreePath reuses the standard missing-field warning so existing\n // behavior (and its tests) are preserved.\n const fallbackWarning = worktreePath\n ? `syntaur: workspace.worktreePath ${worktreePath} is not an existing directory for ${assignmentSlug} — launching in ${repository}`\n : formatFallbackCwdWarning({\n assignmentSlug,\n workspaceDir: repository as string,\n worktreePath,\n branch,\n });\n return { cwd: repository, fallbackWarning, invalidReason: null };\n }\n\n const shown = (p: string | null): string =>\n p && p.trim().length > 0 ? p : '(unset)';\n return {\n cwd: null,\n fallbackWarning: null,\n invalidReason:\n `workspace path invalid for ${assignmentSlug}: tried worktreePath ` +\n `${shown(worktreePath)} and repository ${shown(repository)} — ` +\n `neither is an existing directory`,\n };\n}\n\n/**\n * Build the one-line warning emitted when a launch falls back to a cwd because\n * the assignment is missing `workspace.worktreePath` and/or `workspace.branch`.\n * Returns null when both fields are populated (no warning needed).\n */\nexport function formatFallbackCwdWarning(opts: {\n assignmentSlug: string;\n workspaceDir: string;\n worktreePath: string | null;\n branch: string | null;\n}): string | null {\n const missing: string[] = [];\n if (!opts.worktreePath) missing.push('worktreePath');\n if (!opts.branch) missing.push('branch');\n if (missing.length === 0) return null;\n const fields = missing.map((m) => `workspace.${m}`).join(' and ');\n return `syntaur: ${fields} not set for ${opts.assignmentSlug} — launching in ${opts.workspaceDir}`;\n}\n","import { spawn } from 'node:child_process';\nimport { readFile } from 'node:fs/promises';\nimport { updateAssignmentWorkspace } from '../lifecycle/frontmatter.js';\nimport { writeFileForce } from './fs.js';\nimport { isExistingDir } from '../launch/cwd.js';\n\nexport interface CreateWorktreeOptions {\n repository: string;\n branch: string;\n worktreePath: string;\n parentBranch: string;\n}\n\nfunction run(\n command: string,\n args: string[],\n cwd?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n return new Promise((resolvePromise) => {\n const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });\n let stdout = '';\n let stderr = '';\n child.stdout.on('data', (chunk) => (stdout += chunk.toString()));\n child.stderr.on('data', (chunk) => (stderr += chunk.toString()));\n child.on('error', (err) => {\n resolvePromise({ code: -1, stdout, stderr: stderr + String(err) });\n });\n child.on('close', (code) => {\n resolvePromise({ code: code ?? -1, stdout, stderr });\n });\n });\n}\n\nexport class GitWorktreeError extends Error {\n constructor(message: string, public readonly stderr: string) {\n super(message);\n }\n}\n\n/**\n * Run `git -C <repository> worktree add -b <branch> <worktreePath> <parentBranch>`.\n * Throws GitWorktreeError on non-zero exit, preserving stderr.\n */\nexport async function createWorktree(opts: CreateWorktreeOptions): Promise<void> {\n const { repository, branch, worktreePath, parentBranch } = opts;\n const result = await run(\n 'git',\n ['-C', repository, 'worktree', 'add', '-b', branch, worktreePath, parentBranch],\n );\n if (result.code !== 0) {\n throw new GitWorktreeError(\n `git worktree add failed (exit ${result.code}): ${result.stderr.trim() || '(no stderr)'}`,\n result.stderr,\n );\n }\n}\n\nexport async function removeWorktree(\n repository: string,\n worktreePath: string,\n opts: { force?: boolean } = {},\n): Promise<{ ok: boolean; stderr: string }> {\n const args = ['-C', repository, 'worktree', 'remove'];\n // Without --force git refuses to remove a dirty/locked worktree (intentional).\n if (opts.force) args.push('--force');\n args.push(worktreePath);\n const result = await run('git', args);\n return { ok: result.code === 0, stderr: result.stderr };\n}\n\n/**\n * Best-effort `git worktree prune` — clears stale `.git/worktrees/<name>`\n * registrations for worktree directories that were deleted out-of-band, so the\n * same path/branch can be re-added without git rejecting it\n * (\"already used by worktree at ...\").\n */\nexport async function pruneWorktrees(repository: string): Promise<void> {\n await run('git', ['-C', repository, 'worktree', 'prune']);\n}\n\nexport interface WorktreeEntry {\n worktreePath: string;\n branch: string | null;\n head: string | null;\n bare: boolean;\n detached: boolean;\n}\n\n/**\n * Parse `git worktree list --porcelain` into structured entries (read-only).\n * Returns `[]` if git fails.\n */\nexport async function listWorktrees(repository: string): Promise<WorktreeEntry[]> {\n const result = await run('git', ['-C', repository, 'worktree', 'list', '--porcelain']);\n if (result.code !== 0) return [];\n const entries: WorktreeEntry[] = [];\n let current: Partial<WorktreeEntry> | null = null;\n const flush = () => {\n if (current && current.worktreePath) {\n entries.push({\n worktreePath: current.worktreePath,\n branch: current.branch ?? null,\n head: current.head ?? null,\n bare: current.bare ?? false,\n detached: current.detached ?? false,\n });\n }\n current = null;\n };\n for (const line of result.stdout.split('\\n')) {\n if (line.startsWith('worktree ')) {\n flush();\n current = { worktreePath: line.slice('worktree '.length).trim() };\n } else if (current) {\n if (line.startsWith('HEAD ')) current.head = line.slice('HEAD '.length).trim();\n else if (line.startsWith('branch ')) {\n current.branch = line.slice('branch '.length).trim().replace(/^refs\\/heads\\//, '');\n } else if (line.trim() === 'bare') current.bare = true;\n else if (line.trim() === 'detached') current.detached = true;\n }\n }\n flush();\n return entries;\n}\n\nexport async function deleteBranch(\n repository: string,\n branch: string,\n): Promise<{ ok: boolean; stderr: string }> {\n const result = await run('git', ['-C', repository, 'branch', '-D', branch]);\n return { ok: result.code === 0, stderr: result.stderr };\n}\n\n/**\n * List the local branch names of a repository (read-only).\n * Returns `[]` if git fails (e.g. a bare/empty repo with no branches yet).\n */\nexport async function listBranches(repository: string): Promise<string[]> {\n const result = await run('git', [\n '-C',\n repository,\n 'for-each-ref',\n '--format=%(refname:short)',\n 'refs/heads',\n ]);\n if (result.code !== 0) return [];\n return result.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n}\n\n/**\n * Best-effort detection of a repository's default branch (read-only):\n * 1. `origin/HEAD` symbolic ref (strip the `origin/` prefix), else\n * 2. `main` if it exists locally, else\n * 3. the current branch (ignoring detached HEAD), else\n * 4. the first local branch, else `null`.\n */\nexport async function detectDefaultBranch(repository: string): Promise<string | null> {\n const branches = await listBranches(repository);\n if (branches.length === 0) return null;\n\n // Every candidate below is checked against the local branch list so callers\n // can rely on the result being a real, checkout-able local branch (otherwise\n // `git worktree add ... <parent>` would later fail with \"does not exist\").\n const head = await run('git', [\n '-C',\n repository,\n 'symbolic-ref',\n '--quiet',\n '--short',\n 'refs/remotes/origin/HEAD',\n ]);\n if (head.code === 0) {\n const ref = head.stdout.trim().replace(/^origin\\//, '');\n if (ref && branches.includes(ref)) return ref;\n }\n\n if (branches.includes('main')) return 'main';\n\n const current = await run('git', ['-C', repository, 'rev-parse', '--abbrev-ref', 'HEAD']);\n if (current.code === 0) {\n const name = current.stdout.trim();\n if (name && name !== 'HEAD' && branches.includes(name)) return name;\n }\n\n return branches[0] ?? null;\n}\n\nexport interface RecreateWorktreeOptions {\n repository: string;\n /** The EXACT recorded path to rebuild at. */\n worktreePath: string;\n /** Branch on record, or null for a standalone session with no branch. */\n branch: string | null;\n /** Original HEAD sha captured at creation, for exact recreation. */\n originalHeadSha?: string | null;\n}\n\nexport interface RecreateWorktreeResult {\n /** Branch name or base ref actually used for the worktree add. */\n baseUsed: string;\n /** True when the original branch was restored, or the base === originalHeadSha. */\n exact: boolean;\n /** Resulting branch (null when the worktree was created detached). */\n branch: string | null;\n}\n\n/**\n * Capture the current HEAD sha of a worktree/repo directory (best-effort).\n * Returns the trimmed sha on success, or null if git fails (e.g. the dir is\n * not a git worktree, or git is unavailable). Never throws.\n */\nexport async function captureHeadSha(dir: string): Promise<string | null> {\n const result = await run('git', ['-C', dir, 'rev-parse', 'HEAD']);\n if (result.code !== 0) return null;\n const sha = result.stdout.trim();\n return sha.length > 0 ? sha : null;\n}\n\n/**\n * Resolve a branch's tip to its short SHA in `repository` (read-only). Used to\n * print a recovery hint (`git branch <name> <sha>`) before a branch is deleted.\n * Returns null if the branch can't be resolved.\n */\nexport async function resolveBranchSha(\n repository: string,\n branch: string,\n): Promise<string | null> {\n const result = await run('git', [\n '-C',\n repository,\n 'rev-parse',\n '--short',\n branch,\n ]);\n if (result.code !== 0) return null;\n const sha = result.stdout.trim();\n return sha.length > 0 ? sha : null;\n}\n\n/**\n * True when `branch` is fully merged into `base` (i.e. `base` already contains\n * every commit of `branch`). Uses `git merge-base --is-ancestor` (read-only).\n * Caller must pass a non-null branch.\n */\nexport async function isBranchMerged(\n repository: string,\n branch: string,\n base: string,\n): Promise<boolean> {\n const result = await run('git', [\n '-C',\n repository,\n 'merge-base',\n '--is-ancestor',\n branch,\n base,\n ]);\n return result.code === 0;\n}\n\n/**\n * True when the worktree at `worktreePath` has uncommitted changes (or git can't\n * be queried). Treats an unknown/failed status as dirty so callers stay safe.\n */\nexport async function isWorktreeDirty(worktreePath: string): Promise<boolean> {\n const result = await run('git', ['-C', worktreePath, 'status', '--porcelain']);\n if (result.code !== 0) return true; // unknown -> treat as dirty\n return result.stdout.trim().length > 0;\n}\n\n/**\n * Resolve the top-level directory of the git worktree containing `cwd`\n * (read-only). Returns null if `cwd` is not inside a git repo.\n */\nexport async function repoTopLevel(cwd: string): Promise<string | null> {\n const result = await run('git', ['-C', cwd, 'rev-parse', '--show-toplevel']);\n if (result.code !== 0) return null;\n const top = result.stdout.trim();\n return top.length > 0 ? top : null;\n}\n\n/**\n * Return the worktree path where `branch` is currently checked out, or null if\n * it isn't checked out by any worktree. Parses `git worktree list --porcelain`\n * (blocks of `worktree <path>` / `branch refs/heads/<name>`). Used to decide\n * whether a branch can be re-attached or must be recreated detached.\n */\nasync function branchCheckedOutAt(\n repository: string,\n branch: string,\n): Promise<string | null> {\n const result = await run('git', ['-C', repository, 'worktree', 'list', '--porcelain']);\n if (result.code !== 0) return null;\n let currentPath: string | null = null;\n for (const line of result.stdout.split('\\n')) {\n if (line.startsWith('worktree ')) {\n currentPath = line.slice('worktree '.length).trim();\n } else if (line.startsWith('branch ')) {\n const ref = line.slice('branch '.length).trim();\n if (ref === `refs/heads/${branch}` && currentPath) return currentPath;\n }\n }\n return null;\n}\n\n/**\n * Recreate a worktree at an EXACT recorded path after its directory was deleted\n * (e.g. manually `rm -rf`'d). A manual delete leaves stale\n * `.git/worktrees/<name>/` metadata with the branch still marked checked-out,\n * so `git worktree prune` runs first, then:\n * - branch still exists -> `git worktree add <path> <branch>` (exact: branch restored)\n * - branch was deleted -> `git worktree add -b <branch> <path> <base>` where <base> is, in order:\n * originalHeadSha (if it resolves), refs/remotes/origin/<branch>,\n * or detectDefaultBranch() (a LOCAL branch — never `origin/*`)\n * - no branch on record -> `git worktree add --detach <path> <base>` (a dir at the path is the\n * sufficient condition for `claude --resume <id>`)\n * Throws GitWorktreeError when the `git worktree add` fails or no base ref is available.\n */\nexport async function recreateWorktree(\n opts: RecreateWorktreeOptions,\n): Promise<RecreateWorktreeResult> {\n const { repository, worktreePath, branch } = opts;\n const originalHeadSha = opts.originalHeadSha ?? null;\n\n // Clear stale metadata for the deleted directory so a subsequent add at the\n // same path / branch is not rejected (\"already used by worktree at ...\"). Best\n // effort — any real failure surfaces from the add step below.\n await pruneWorktrees(repository);\n\n const add = async (args: string[]): Promise<void> => {\n const result = await run('git', ['-C', repository, 'worktree', 'add', ...args]);\n if (result.code !== 0) {\n throw new GitWorktreeError(\n `git worktree add failed (exit ${result.code}): ${result.stderr.trim() || '(no stderr)'}`,\n result.stderr,\n );\n }\n };\n\n const refExists = async (ref: string): Promise<boolean> => {\n const result = await run('git', [\n '-C',\n repository,\n 'rev-parse',\n '--verify',\n '--quiet',\n ref,\n ]);\n return result.code === 0;\n };\n\n // 1. Branch still exists — re-attach a worktree to it at the recorded path.\n if (branch && (await listBranches(repository)).includes(branch)) {\n const checkedOutAt = await branchCheckedOutAt(repository, branch);\n if (\n !checkedOutAt ||\n checkedOutAt === worktreePath ||\n !isExistingDir(checkedOutAt)\n ) {\n // Free to attach (post-prune the deleted worktree's claim is gone).\n await add([worktreePath, branch]);\n return { baseUsed: branch, exact: true, branch };\n }\n // The branch is checked out in another LIVE worktree, so it can't be\n // attached here. A directory at the exact path is the sufficient condition\n // for `claude --resume`, so recreate detached at the original sha (exact)\n // or the branch tip.\n const detachBase =\n originalHeadSha && (await refExists(`${originalHeadSha}^{commit}`))\n ? originalHeadSha\n : `refs/heads/${branch}`;\n await add(['--detach', worktreePath, detachBase]);\n return { baseUsed: detachBase, exact: detachBase === originalHeadSha, branch: null };\n }\n\n // 2/3. Branch missing (deleted) or never recorded — choose a base ref.\n let baseUsed: string | null = null;\n if (originalHeadSha && (await refExists(`${originalHeadSha}^{commit}`))) {\n baseUsed = originalHeadSha;\n } else if (branch && (await refExists(`refs/remotes/origin/${branch}`))) {\n baseUsed = `refs/remotes/origin/${branch}`;\n } else {\n baseUsed = await detectDefaultBranch(repository);\n }\n if (!baseUsed) {\n throw new GitWorktreeError(\n `recreateWorktree: no base ref to recreate ${worktreePath} ` +\n `(no original sha, no origin/${branch ?? '<none>'}, no default branch)`,\n '',\n );\n }\n\n const exact = baseUsed === originalHeadSha;\n if (branch) {\n // 2. Recreate the deleted branch at the base ref.\n await add(['-b', branch, worktreePath, baseUsed]);\n return { baseUsed, exact, branch };\n }\n // 3. No branch on record — a detached worktree is enough for `--resume`.\n await add(['--detach', worktreePath, baseUsed]);\n return { baseUsed, exact, branch: null };\n}\n\nexport interface CreateWorktreeAndRecordOptions extends CreateWorktreeOptions {\n assignmentPath: string;\n}\n\n/**\n * Transactional helper:\n * 1. `git worktree add` — on failure throws, nothing else touched.\n * 2. Read assignment.md, update `workspace.*` fields, write back via writeFileForce.\n * 3. If (2) fails, `git worktree remove --force` to undo step 1. If cleanup fails,\n * throw an error naming both the file-write error AND the orphan worktree path.\n */\nexport async function createWorktreeAndRecord(\n opts: CreateWorktreeAndRecordOptions,\n): Promise<void> {\n const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;\n\n await createWorktree({ repository, branch, worktreePath, parentBranch });\n\n try {\n const content = await readFile(assignmentPath, 'utf-8');\n const updated = updateAssignmentWorkspace(content, {\n repository,\n worktreePath,\n branch,\n parentBranch,\n });\n await writeFileForce(assignmentPath, updated);\n } catch (writeErr) {\n const cleanup = await removeWorktree(repository, worktreePath, { force: true });\n // Always try to delete the branch created by -b, even if worktree removal already failed.\n const branchCleanup = await deleteBranch(repository, branch);\n const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);\n throw new Error(\n formatRollbackError({\n writeMsg,\n worktreePath,\n branch,\n worktreeCleanup: cleanup,\n branchCleanup,\n }),\n );\n }\n}\n\nexport function formatRollbackError(opts: {\n writeMsg: string;\n worktreePath: string;\n branch: string;\n worktreeCleanup: { ok: boolean; stderr: string };\n branchCleanup: { ok: boolean; stderr: string };\n subject?: string;\n}): string {\n const { writeMsg, worktreePath, branch, worktreeCleanup, branchCleanup } = opts;\n const subject = opts.subject ?? 'assignment frontmatter';\n const wtMsg = worktreeCleanup.stderr.trim() || '(no stderr)';\n const brMsg = branchCleanup.stderr.trim() || '(no stderr)';\n if (!worktreeCleanup.ok && !branchCleanup.ok) {\n return (\n `Failed to update ${subject} AND failed to clean up both worktree and branch. ` +\n `Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Branch cleanup error: ${brMsg}. ` +\n `Orphan worktree at ${worktreePath} and orphan branch \"${branch}\" — remove them manually.`\n );\n }\n if (!worktreeCleanup.ok) {\n return (\n `Failed to update ${subject} AND failed to clean up worktree. ` +\n `Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. ` +\n `Orphan worktree at ${worktreePath} — remove it manually.`\n );\n }\n if (!branchCleanup.ok) {\n return (\n `Failed to update ${subject}: ${writeMsg}. Rolled back git worktree at ${worktreePath}, ` +\n `but could not delete branch \"${branch}\": ${brMsg}. ` +\n `Remove the branch manually.`\n );\n }\n return `Failed to update ${subject}: ${writeMsg}. Rolled back git worktree at ${worktreePath} and branch \"${branch}\".`;\n}\n\nexport interface CreateWorktreeForBundleOptions extends CreateWorktreeOptions {\n record: () => Promise<void>;\n}\n\n/**\n * Bundle-scoped sibling of createWorktreeAndRecord. Creates the worktree,\n * then runs the caller-supplied record() callback (which writes bundle\n * storage + checklist + .syntaur/context.json). On record() failure, rolls\n * back the worktree and branch and throws a formatted error tagged\n * `subject: 'bundle storage'` so users see a bundle-specific message.\n */\nexport async function createWorktreeForBundle(\n opts: CreateWorktreeForBundleOptions,\n): Promise<void> {\n const { repository, branch, worktreePath, parentBranch, record } = opts;\n await createWorktree({ repository, branch, worktreePath, parentBranch });\n try {\n await record();\n } catch (writeErr) {\n const cleanup = await removeWorktree(repository, worktreePath, { force: true });\n const branchCleanup = await deleteBranch(repository, branch);\n const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);\n throw new Error(\n formatRollbackError({\n writeMsg,\n worktreePath,\n branch,\n worktreeCleanup: cleanup,\n branchCleanup,\n subject: 'bundle storage',\n }),\n );\n }\n}\n","/**\n * Fact computation (derived-status design v3, Piece 1) — Node-side.\n *\n * Materializes an assignment's objective facts from the files already on disk\n * (assignment.md body, sibling plan files, comments.md) plus the asserted\n * frontmatter facts. The browser never runs this — the dashboard ships the\n * result in payloads (loader-derived, NOT stored), mirroring the\n * `deriveStatusVirtuals` pattern.\n */\n\nimport { createHash } from 'node:crypto';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { fileExists } from '../utils/fs.js';\nimport { captureHeadSha } from '../utils/git-worktree.js';\nimport { type AssignmentFacts, factFieldNames } from './derive.js';\nimport { parseAssignmentFrontmatter } from './frontmatter.js';\nimport type { AssignmentFrontmatter, AttestationRecord } from './types.js';\nimport type { FactDeclaration } from '../utils/config.js';\n\n/** Matches the assignment template's placeholder list items / comments. */\nconst HTML_COMMENT_RE = /<!--[\\s\\S]*?-->/g;\n\n/** Extract the body of a `## <heading>` section (up to the next `## `). */\nfunction sectionBody(body: string, heading: string): string | null {\n const re = new RegExp(`^##\\\\s+${heading}\\\\s*$`, 'm');\n const m = body.match(re);\n if (!m || m.index === undefined) return null;\n const start = m.index + m[0].length;\n const rest = body.slice(start);\n const next = rest.search(/^##\\s+/m);\n return next >= 0 ? rest.slice(0, next) : rest;\n}\n\n/** Objective filled with real content (template placeholder comments stripped). */\nexport function hasRealObjective(body: string): boolean {\n const section = sectionBody(body, 'Objective');\n if (section === null) return false;\n return section.replace(HTML_COMMENT_RE, '').trim().length > 0;\n}\n\n/**\n * Count non-placeholder acceptance criteria. The template seeds\n * `- [ ] <!-- criterion N -->` rows — those don't count (a naive `acTotal > 0`\n * would promote every fresh draft; codex design-review finding).\n */\nexport function countRealAcceptanceCriteria(body: string): { total: number; checked: number } {\n const section = sectionBody(body, 'Acceptance Criteria');\n if (section === null) return { total: 0, checked: 0 };\n let total = 0;\n let checked = 0;\n for (const line of section.split('\\n')) {\n const m = line.match(/^\\s*-\\s*\\[([ xX])\\]\\s*(.*)$/);\n if (!m) continue;\n const content = m[2].replace(HTML_COMMENT_RE, '').trim();\n if (content.length === 0) continue; // placeholder or empty\n total++;\n if (m[1].toLowerCase() === 'x') checked++;\n }\n return { total, checked };\n}\n\nconst PLAN_FILE_RE = /^plan(?:-v(\\d+))?\\.md$/;\n\n/** Latest plan revision in an assignment dir (`plan.md` = v1 < `plan-v2.md` < …). */\nexport async function latestPlanFile(assignmentDir: string): Promise<string | null> {\n let entries: string[];\n try {\n entries = await readdir(assignmentDir);\n } catch {\n return null;\n }\n let best: { name: string; version: number } | null = null;\n for (const name of entries) {\n const m = name.match(PLAN_FILE_RE);\n if (!m) continue;\n const version = m[1] ? parseInt(m[1], 10) : 1;\n if (!best || version > best.version) best = { name, version };\n }\n return best?.name ?? null;\n}\n\nexport function planDigest(content: string): string {\n return createHash('sha256').update(content, 'utf-8').digest('hex');\n}\n\n/**\n * Revision-bound approval check: the `planApproval` record must name the\n * CURRENT latest plan file AND its digest must match that file's current\n * content. A replan (new plan-vN) or a post-approval edit auto-invalidates.\n */\nexport async function isPlanApproved(\n assignmentDir: string,\n frontmatter: Pick<AssignmentFrontmatter, 'planApproval'>,\n): Promise<boolean> {\n const approval = frontmatter.planApproval;\n if (!approval) return false;\n const latest = await latestPlanFile(assignmentDir);\n if (!latest || latest !== approval.file) return false;\n try {\n const content = await readFile(resolve(assignmentDir, latest), 'utf-8');\n return planDigest(content) === approval.digest;\n } catch {\n return false;\n }\n}\n\n/** Count open (unresolved) question comments in comments.md. Parity with the\n * dashboard's countOpenQuestions, kept dependency-light for the lifecycle layer. */\nexport async function countUnresolvedQuestions(assignmentDir: string): Promise<number> {\n const commentsPath = resolve(assignmentDir, 'comments.md');\n if (!(await fileExists(commentsPath))) return 0;\n try {\n const content = await readFile(commentsPath, 'utf-8');\n // Each entry: \"## <id>\" block with \"**Type:** question\" and \"**Resolved:** false\"\n let count = 0;\n for (const block of content.split(/^##\\s+/m).slice(1)) {\n if (/^\\*\\*Type:\\*\\*\\s*question\\s*$/m.test(block) && /^\\*\\*Resolved:\\*\\*\\s*false\\s*$/m.test(block)) {\n count++;\n }\n }\n return count;\n } catch {\n return 0;\n }\n}\n\n/** All `dependsOn` targets terminal? Standalone assignments (no project dir)\n * and empty dependency lists are trivially satisfied. */\nexport async function areDependenciesSatisfied(\n projectDir: string | null,\n dependsOn: string[],\n terminalStatuses: ReadonlySet<string>,\n): Promise<boolean> {\n if (dependsOn.length === 0 || projectDir === null) return true;\n for (const depSlug of dependsOn) {\n const depPath = resolve(projectDir, 'assignments', depSlug, 'assignment.md');\n if (!(await fileExists(depPath))) return false;\n try {\n const content = await readFile(depPath, 'utf-8');\n // Use the canonical parser (not a hand-rolled regex) so a QUOTED\n // `status: \"completed\"` — which formatYamlValue can emit — is stripped to\n // its bare value and matches `terminalStatuses`. Parity with the sibling\n // `checkDependencies` in transitions.ts. A parser throw (no frontmatter)\n // still falls through to the catch → `return false` (fail-closed).\n const { status } = parseAssignmentFrontmatter(content);\n if (!terminalStatuses.has(status)) return false;\n } catch {\n return false;\n }\n }\n return true;\n}\n\nexport interface ComputeFactsInput {\n assignmentDir: string;\n frontmatter: AssignmentFrontmatter;\n body: string;\n /** Project dir for dependency checks; null for standalone assignments. */\n projectDir: string | null;\n terminalStatuses: ReadonlySet<string>;\n /** The ACCEPTED custom-fact declarations (normalize→accept output). Absent →\n * only the 14 built-ins materialize. */\n declarations?: FactDeclaration[];\n}\n\n/**\n * Canonical fact-value coercion (Locked Decisions — used by BOTH facts.ts and\n * the CLI). bool → case-insensitive `true`/`false` only; number → trimmed,\n * `Number(value)` finite (rejects NaN/Infinity/empty). Returns the canonical\n * stored form (`'true'`/`'false'` or `String(n)`) or null if invalid. The CLI\n * rejects null with the declared type; computeFacts treats null as absent.\n */\nexport function canonicalizeFactValue(type: 'bool' | 'number', raw: string): string | null {\n const t = raw.trim();\n if (type === 'bool') {\n const low = t.toLowerCase();\n if (low === 'true') return 'true';\n if (low === 'false') return 'false';\n return null;\n }\n if (t === '') return null;\n const n = Number(t);\n return Number.isFinite(n) ? String(n) : null;\n}\n\n/** Read a stored bool fact, degrading absent/invalid to false (never throws). */\nfunction readBoolFact(raw: string | undefined): boolean {\n if (typeof raw !== 'string') return false;\n return canonicalizeFactValue('bool', raw) === 'true';\n}\n\n/** Read a stored number fact, degrading absent/invalid to 0 (never throws). */\nfunction readNumberFact(raw: string | undefined): number {\n if (typeof raw !== 'string') return 0;\n const c = canonicalizeFactValue('number', raw);\n return c === null ? 0 : Number(c);\n}\n\n/** Per-attestation-fact validity detail (one record list per declared fact). */\nexport interface AttestationDetail {\n fact: string;\n binds: 'plan' | 'commit' | 'none';\n records: Array<{ record: AttestationRecord; valid: boolean }>;\n}\n\nexport interface ComputeFactsResult {\n facts: AssignmentFacts;\n attestations: AttestationDetail[];\n}\n\n/** Resolved-once binding environment for attestation validity. */\ninterface AttestationEnv {\n latestPlanFile: string | null;\n /** Digest of the latest plan file's CURRENT content (null when no plan). */\n planDigest: string | null;\n /** Workspace HEAD sha (null when no workspace / not a git dir). */\n headSha: string | null;\n}\n\nfunction isAttestationValid(\n record: AttestationRecord,\n binds: 'plan' | 'commit' | 'none',\n env: AttestationEnv,\n): boolean {\n if (binds === 'none') return true;\n if (binds === 'plan') {\n if (!record.file || !env.latestPlanFile || record.file !== env.latestPlanFile) return false;\n if (!record.digest || !env.planDigest) return false;\n return record.digest === env.planDigest;\n }\n // binds:commit\n if (!record.commit || !env.headSha) return false;\n return record.commit === env.headSha;\n}\n\n/**\n * Materialize the full fact set PLUS per-attestation validity in ONE pass —\n * the dashboard (Task 9) calls this once per detail request so facts and\n * record-level staleness come from the same plan-file / HEAD reads. `computeFacts`\n * is a thin delegate returning just `.facts`.\n */\nexport async function computeFactsDetailed(input: ComputeFactsInput): Promise<ComputeFactsResult> {\n const { assignmentDir, frontmatter, body, projectDir, terminalStatuses } = input;\n const declarations = input.declarations ?? [];\n\n const ac = countRealAcceptanceCriteria(body);\n // Resolve the plan environment ONCE — a single read of the latest plan file's\n // content drives BOTH the built-in `planApproved` fact AND binds:plan\n // attestation validity, so a concurrent replan can't make the two disagree\n // and the plan is read at most once. Read only when something needs the digest.\n const needsPlanDigest =\n frontmatter.planApproval !== null ||\n declarations.some((d) => d.type === 'attestation' && d.binds === 'plan');\n const planFile = await latestPlanFile(assignmentDir);\n const [planFileContent, unresolvedQuestions, depsSatisfied] = await Promise.all([\n needsPlanDigest && planFile\n ? readFile(resolve(assignmentDir, planFile), 'utf-8').catch(() => null)\n : Promise.resolve(null),\n countUnresolvedQuestions(assignmentDir),\n areDependenciesSatisfied(projectDir, frontmatter.dependsOn, terminalStatuses),\n ]);\n const planFileDigest = planFileContent !== null ? planDigest(planFileContent) : null;\n const approval = frontmatter.planApproval;\n const planApproved =\n approval !== null &&\n approval.file === planFile &&\n planFileDigest !== null &&\n approval.digest === planFileDigest;\n\n const facts: AssignmentFacts = {\n hasRealObjective: hasRealObjective(body),\n acRealTotal: ac.total,\n acRealChecked: ac.checked,\n acAllChecked: ac.total > 0 && ac.checked === ac.total,\n planExists: planFile !== null,\n planApproved,\n workspaceSet: frontmatter.workspace.repository !== null && frontmatter.workspace.branch !== null,\n implementationStarted: frontmatter.implementationStarted,\n depsSatisfied,\n unresolvedQuestions,\n blocked: frontmatter.blockedReason !== null,\n parked: frontmatter.parked,\n reviewRequested: frontmatter.reviewRequested,\n pinned: frontmatter.override !== null,\n };\n\n const attestations: AttestationDetail[] = [];\n if (declarations.length > 0) {\n const storedFacts = frontmatter.facts ?? {};\n const records = frontmatter.attestations ?? [];\n\n // Custom bool/number facts (absent/invalid stored values degrade, no throw).\n for (const decl of declarations) {\n if (decl.type === 'bool') facts[decl.name] = readBoolFact(storedFacts[decl.name]);\n else if (decl.type === 'number') facts[decl.name] = readNumberFact(storedFacts[decl.name]);\n }\n\n // Attestation facts — resolve the binding env ONCE, then evaluate each.\n const attestationDecls = declarations.filter(\n (d): d is Extract<FactDeclaration, { type: 'attestation' }> => d.type === 'attestation',\n );\n if (attestationDecls.length > 0) {\n const needsCommit = attestationDecls.some((d) => d.binds === 'commit');\n let headSha: string | null = null;\n if (needsCommit) {\n const dir = frontmatter.workspace.worktreePath ?? frontmatter.workspace.repository;\n headSha = dir ? await captureHeadSha(dir) : null;\n }\n // planFile + planFileDigest were resolved once above (shared with the\n // built-in planApproved fact) — no second read, one consistent snapshot.\n const env: AttestationEnv = { latestPlanFile: planFile, planDigest: planFileDigest, headSha };\n\n for (const decl of attestationDecls) {\n const detailRecords = records\n .filter((r) => r.fact === decl.name)\n .map((record) => ({ record, valid: isAttestationValid(record, decl.binds, env) }));\n attestations.push({ fact: decl.name, binds: decl.binds, records: detailRecords });\n\n const valid = detailRecords.filter((r) => r.valid).map((r) => r.record);\n const validApproved = valid.filter((r) => r.verdict === 'approved');\n const validChanges = valid.filter((r) => r.verdict === 'changes-requested');\n const names = factFieldNames(decl);\n facts[names.exports.fact] = valid.length > 0;\n facts[names.exports.approved] = validApproved.length > 0;\n facts[names.exports.changesRequested] = validChanges.length > 0;\n facts[names.exports.by] = valid.map((r) => r.actor);\n facts[names.exports.approvedBy] = validApproved.map((r) => r.actor);\n }\n }\n }\n\n return { facts, attestations };\n}\n\n/** Materialize the full fact set for one assignment (thin delegate). */\nexport async function computeFacts(input: ComputeFactsInput): Promise<AssignmentFacts> {\n return (await computeFactsDetailed(input)).facts;\n}\n","/**\n * Shared search types — the contract between the indexer, the `SearchProvider`\n * implementations (Fuse default, Semantic stub seam), and both consumers (the\n * `syntaur search` CLI and the dashboard content-search router/palette).\n *\n * The provider returns a NEUTRAL snippet (no highlight markers) plus\n * `matches: MatchRange[]` (snippet-local char offsets) so each caller formats\n * highlighting itself: the CLI wraps with `**…**`, the API/palette wrap with\n * HTML-safe `<mark>`.\n *\n * Aligns with `EntityKind`/scope semantics in `src/utils/search-schema.ts`\n * (that module covers entity-record search; this one covers markdown bodies).\n */\n\n/** The markdown content kinds indexed for full-text body search. */\nexport type FileKind =\n | 'assignment'\n | 'plan'\n | 'progress'\n | 'comments'\n | 'handoff'\n | 'decision-record'\n | 'scratchpad'\n | 'memory'\n | 'resource';\n\nexport const FILE_KINDS: readonly FileKind[] = [\n 'assignment',\n 'plan',\n 'progress',\n 'comments',\n 'handoff',\n 'decision-record',\n 'scratchpad',\n 'memory',\n 'resource',\n];\n\n/**\n * One indexed markdown document. Carries the body to search plus the\n * filter+route identity propagated from the owning assignment / project\n * frontmatter so the provider can pre-filter (`type`/`status`/`project`/`in`)\n * and the route builder can produce a deep-link without re-reading anything.\n */\nexport interface SearchDoc {\n /** Stable id — the absolute file path doubles as the id. */\n id: string;\n /** Absolute file path on disk. */\n path: string;\n fileKind: FileKind;\n /** Human title (assignment/project/memory/resource title), used as a Fuse key. */\n title: string;\n /** The markdown body to full-text search. */\n body: string;\n /** Nearest-heading section, when the indexer can cheaply attribute one. */\n section?: string;\n\n // ── filter + route identity (carried from frontmatter) ──────────────────\n /** Owning project slug; `null` for standalone assignments. */\n projectSlug: string | null;\n /**\n * The owning project's `workspace` field (from project.md) — drives the\n * `/w/<ws>` route prefix the palette applies. `null` for standalone.\n */\n projectWorkspace: string | null;\n /** Owning assignment slug; `null` for memory/resource docs. */\n assignmentSlug: string | null;\n /** Owning assignment id (uuid); `null` for memory/resource docs. */\n assignmentId: string | null;\n /** True when the owning assignment is standalone (no containing project). */\n standalone: boolean;\n /** Memory/resource file slug (filename without `.md`); absent otherwise. */\n itemSlug?: string;\n /** Owning assignment `type` (for `--type` filtering); absent for memory/resource. */\n type?: string;\n /** Owning assignment `status` (for `--status` filtering); absent for memory/resource. */\n status?: string;\n /** Archived flag (from assignment or project frontmatter). */\n archived: boolean;\n}\n\n/** A match range in NEUTRAL char offsets into `SearchHit.snippet`. */\nexport interface MatchRange {\n start: number;\n end: number;\n}\n\n/**\n * One ranked search result. `snippet` is NEUTRAL text (no markers); callers\n * apply `matches` to highlight. `route` is the precomputed UNPREFIXED deep-link\n * (the palette prepends the per-hit `/w/<workspace>` prefix).\n */\nexport interface SearchHit {\n path: string;\n projectSlug: string | null;\n projectWorkspace: string | null;\n assignmentSlug: string | null;\n assignmentId: string | null;\n standalone: boolean;\n itemSlug?: string;\n fileKind: FileKind;\n title: string;\n score: number;\n snippet: string;\n matches: MatchRange[];\n /** 1-based line number of the match in the source body. */\n line: number;\n section?: string;\n /** Precomputed unprefixed app route (see `routeForHit`). */\n route: string;\n}\n\n/** A search request. `in` is the canonical-resolved file-kind filter. */\nexport interface SearchQuery {\n query: string;\n project?: string;\n type?: string[];\n status?: string[];\n in?: FileKind[];\n}\n\n/**\n * The provider seam. `index` ingests the docs; `query` runs a ranked search.\n * `FuseProvider` is the default; `SemanticProvider` is a stub for the future\n * embeddings slot.\n */\nexport interface SearchProvider {\n index(docs: SearchDoc[]): void | Promise<void>;\n query(q: SearchQuery, limit: number): SearchHit[] | Promise<SearchHit[]>;\n}\n\n/**\n * `--in` alias map — both singular and plural/common forms resolve to the\n * canonical `FileKind`. Resolves the `--in comments,plans` mismatch (`plans` →\n * `plan`).\n */\nexport const FILE_KIND_ALIASES: Record<string, FileKind> = {\n assignment: 'assignment',\n assignments: 'assignment',\n plan: 'plan',\n plans: 'plan',\n progress: 'progress',\n comment: 'comments',\n comments: 'comments',\n handoff: 'handoff',\n handoffs: 'handoff',\n decision: 'decision-record',\n decisions: 'decision-record',\n 'decision-record': 'decision-record',\n 'decision-records': 'decision-record',\n scratchpad: 'scratchpad',\n scratchpads: 'scratchpad',\n memory: 'memory',\n memories: 'memory',\n resource: 'resource',\n resources: 'resource',\n};\n\n/**\n * Parse a comma-separated `--in` list into canonical `FileKind[]`. Splits,\n * trims, lowercases, and resolves via {@link FILE_KIND_ALIASES}. Throws on an\n * unknown kind with a message listing the valid kinds. Empty/blank entries are\n * dropped; a fully-empty input returns `[]`.\n */\nexport function parseFileKinds(csv: string): FileKind[] {\n const out: FileKind[] = [];\n for (const raw of csv.split(',')) {\n const token = raw.trim().toLowerCase();\n if (token.length === 0) continue;\n const canonical = FILE_KIND_ALIASES[token];\n if (!canonical) {\n const valid = Array.from(new Set(Object.keys(FILE_KIND_ALIASES))).join(', ');\n throw new Error(`Unknown file kind \"${token}\". Valid kinds: ${valid}`);\n }\n if (!out.includes(canonical)) out.push(canonical);\n }\n return out;\n}\n","/**\n * Deep-link route helper for search hits. Produces UNPREFIXED app paths (the\n * dashboard palette prepends the per-hit `/w/<workspace>` prefix for nested\n * assignment-pane hits). Also exports the shared `slugifyHeading` used both here\n * (for the `#section` anchor) and by the dashboard `MarkdownRenderer` heading\n * ids, so the route hash always matches a real element id.\n */\n\nimport type { FileKind, SearchHit } from './types.js';\n\n/**\n * Content kind → the `AssignmentDetail` `?tab=` pane that renders it. Memory and\n * resource have no assignment pane; they route to their own pages.\n */\nexport const FILE_KIND_TO_TAB: Record<FileKind, string> = {\n assignment: 'summary',\n plan: 'plan',\n scratchpad: 'scratchpad',\n handoff: 'handoff',\n progress: 'progress',\n comments: 'comments',\n 'decision-record': 'decisions',\n // memory/resource never use a tab — routeForHit short-circuits them.\n memory: 'summary',\n resource: 'summary',\n};\n\n/**\n * GitHub-style heading slug — lowercase, strip non-word chars, spaces → `-`.\n * Shared with the dashboard `MarkdownRenderer` heading ids so `#<slug>` anchors\n * resolve.\n */\nexport function slugifyHeading(text: string): string {\n return text\n .trim()\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * File kinds whose dashboard pane renders its WHOLE body through\n * `MarkdownRenderer` and so gets heading `id`s a `#<slug(section)>` anchor can\n * resolve against. Excluded kinds, and why a hash there would dangle:\n * - `comments` / `progress` — render structured components (CommentsThread /\n * progress `<li>` rows), NOT markdown headings.\n * - `assignment` — the `summary` pane transforms `## Acceptance Criteria` /\n * `## Todos` into `SectionCard`s WITHOUT ids (AssignmentDetail.tsx), so its\n * headings never become element ids.\n * These all get the `?tab=` pane WITHOUT a hash.\n */\nconst ANCHORABLE_KINDS: ReadonlySet<FileKind> = new Set<FileKind>([\n 'plan',\n 'scratchpad',\n 'handoff',\n 'decision-record',\n]);\n\n/**\n * Build the UNPREFIXED deep-link for a hit:\n * - memory → `/projects/<projectSlug>/memories/<itemSlug>`\n * - resource → `/projects/<projectSlug>/resources/<itemSlug>`\n * - assignment-scoped kinds → `<base>?tab=<pane>` + optional `#<slug(section)>`,\n * where base is `/assignments/<id>` (standalone) or\n * `/projects/<projectSlug>/assignments/<assignmentSlug>` (nested).\n */\nexport function routeForHit(\n hit: Pick<\n SearchHit,\n | 'fileKind'\n | 'projectSlug'\n | 'assignmentSlug'\n | 'assignmentId'\n | 'standalone'\n | 'itemSlug'\n | 'section'\n >,\n): string {\n if (hit.fileKind === 'memory') {\n return `/projects/${hit.projectSlug}/memories/${hit.itemSlug}`;\n }\n if (hit.fileKind === 'resource') {\n return `/projects/${hit.projectSlug}/resources/${hit.itemSlug}`;\n }\n\n const base = hit.standalone\n ? `/assignments/${hit.assignmentId}`\n : `/projects/${hit.projectSlug}/assignments/${hit.assignmentSlug}`;\n\n const tab = FILE_KIND_TO_TAB[hit.fileKind];\n let route = `${base}?tab=${tab}`;\n if (hit.section && ANCHORABLE_KINDS.has(hit.fileKind)) {\n route += `#${slugifyHeading(hit.section)}`;\n }\n return route;\n}\n","import { resolve } from 'node:path';\nimport { readdir } from 'node:fs/promises';\nimport { fileExists } from './fs.js';\n\nexport interface AssignmentEntry {\n projectDir: string;\n /** `null` for standalone assignments (no containing project). */\n projectSlug: string | null;\n assignmentDir: string;\n /** For standalone, this is the UUID folder name. */\n assignmentSlug: string;\n standalone: boolean;\n}\n\nexport interface AssignmentWalkResult {\n withAssignmentMd: AssignmentEntry[];\n orphanFolders: AssignmentEntry[];\n}\n\nexport async function listAssignmentsByProject(\n projectsDir: string,\n standaloneDir: string | null,\n): Promise<AssignmentWalkResult> {\n const result: AssignmentWalkResult = {\n withAssignmentMd: [],\n orphanFolders: [],\n };\n\n if (await fileExists(projectsDir)) {\n const projects = await readdir(projectsDir, { withFileTypes: true });\n for (const m of projects) {\n if (!m.isDirectory()) continue;\n if (m.name.startsWith('.') || m.name.startsWith('_')) continue;\n const assignmentsDir = resolve(projectsDir, m.name, 'assignments');\n if (!(await fileExists(assignmentsDir))) continue;\n\n const entries = await readdir(assignmentsDir, { withFileTypes: true });\n for (const a of entries) {\n if (!a.isDirectory()) continue;\n if (a.name.startsWith('.') || a.name.startsWith('_')) continue;\n const assignmentDir = resolve(assignmentsDir, a.name);\n const assignmentMd = resolve(assignmentDir, 'assignment.md');\n const entry: AssignmentEntry = {\n projectDir: resolve(projectsDir, m.name),\n projectSlug: m.name,\n assignmentDir,\n assignmentSlug: a.name,\n standalone: false,\n };\n if (await fileExists(assignmentMd)) {\n result.withAssignmentMd.push(entry);\n } else {\n result.orphanFolders.push(entry);\n }\n }\n }\n }\n\n if (standaloneDir !== null && (await fileExists(standaloneDir))) {\n const entries = await readdir(standaloneDir, { withFileTypes: true });\n for (const a of entries) {\n if (!a.isDirectory()) continue;\n if (a.name.startsWith('.') || a.name.startsWith('_')) continue;\n const assignmentDir = resolve(standaloneDir, a.name);\n const assignmentMd = resolve(assignmentDir, 'assignment.md');\n const entry: AssignmentEntry = {\n projectDir: standaloneDir,\n projectSlug: null,\n assignmentDir,\n assignmentSlug: a.name,\n standalone: true,\n };\n if (await fileExists(assignmentMd)) {\n result.withAssignmentMd.push(entry);\n } else {\n result.orphanFolders.push(entry);\n }\n }\n }\n\n return result;\n}\n","/**\n * Content indexer — walks all Syntaur markdown content, reads bodies via the\n * canonical parsers (`src/dashboard/parser.ts`), and emits `SearchDoc[]`.\n *\n * The content dirs are PARAMETERS, never hardcoded `defaultProjectDir()` — the\n * dashboard server and the CLI may use different configured dirs, so hardcoding\n * a default would index a different tree than is displayed (audit finding #8).\n *\n * A module-level cache keyed by `projectsDir|assignmentsDir|includeArchived`\n * makes the expensive body-read happen only on first query and after a content\n * change (detected by a cheap stat-only max-mtime sweep) — never per query.\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { resolve, join } from 'node:path';\nimport { fileExists } from '../utils/fs.js';\nimport { listAssignmentsByProject } from '../utils/assignment-walk.js';\nimport { latestPlanFile } from '../lifecycle/facts.js';\nimport {\n parseAssignmentFull,\n parsePlan,\n parseProgress,\n parseComments,\n parseHandoff,\n parseDecisionRecord,\n parseScratchpad,\n parseMemory,\n parseResource,\n parseProject,\n} from '../dashboard/parser.js';\nimport type { FileKind, SearchDoc } from './types.js';\n\nexport interface IndexOptions {\n projectsDir: string;\n assignmentsDir: string;\n includeArchived?: boolean;\n}\n\n/** Identity carried from the owning assignment onto every sidecar doc. */\ninterface AssignmentIdentity {\n assignmentId: string | null;\n assignmentSlug: string;\n projectSlug: string | null;\n projectWorkspace: string | null;\n standalone: boolean;\n type?: string;\n status?: string;\n archived: boolean;\n}\n\n/** The assignment sidecars, each with its kind + parser → body extractor. */\nconst SIDECARS: Array<{ file: string; kind: FileKind; body: (content: string) => string }> = [\n { file: 'progress.md', kind: 'progress', body: (c) => parseProgress(c).body },\n { file: 'comments.md', kind: 'comments', body: (c) => parseComments(c).body },\n { file: 'handoff.md', kind: 'handoff', body: (c) => parseHandoff(c).body },\n { file: 'decision-record.md', kind: 'decision-record', body: (c) => parseDecisionRecord(c).body },\n { file: 'scratchpad.md', kind: 'scratchpad', body: (c) => parseScratchpad(c).body },\n];\n\n/**\n * Build the full content index for the given dirs. Skips archived\n * assignments/projects unless `includeArchived`.\n */\nexport async function buildIndex(opts: IndexOptions): Promise<SearchDoc[]> {\n const { projectsDir, assignmentsDir, includeArchived = false } = opts;\n const docs: SearchDoc[] = [];\n\n // ── per-project workspace lookup (read each project.md once) ────────────\n const projectWorkspace = new Map<string, string | null>();\n const projectArchived = new Map<string, boolean>();\n if (await fileExists(projectsDir)) {\n const projects = await readdir(projectsDir, { withFileTypes: true });\n for (const m of projects) {\n if (!m.isDirectory()) continue;\n if (m.name.startsWith('.') || m.name.startsWith('_')) continue;\n const projectMdPath = resolve(projectsDir, m.name, 'project.md');\n let workspace: string | null = null;\n let archived = false;\n if (await fileExists(projectMdPath)) {\n try {\n const parsed = parseProject(await readFile(projectMdPath, 'utf-8'));\n workspace = parsed.workspace;\n archived = parsed.archived;\n } catch {\n // tolerate a malformed project.md — workspace stays null\n }\n }\n projectWorkspace.set(m.name, workspace);\n projectArchived.set(m.name, archived);\n }\n }\n\n // ── assignments (project-nested + standalone) ───────────────────────────\n const { withAssignmentMd } = await listAssignmentsByProject(projectsDir, assignmentsDir);\n for (const entry of withAssignmentMd) {\n const assignmentMdPath = resolve(entry.assignmentDir, 'assignment.md');\n let assignmentContent: string;\n try {\n assignmentContent = await readFile(assignmentMdPath, 'utf-8');\n } catch {\n continue;\n }\n const assignment = parseAssignmentFull(assignmentContent);\n\n // An assignment is excluded by default when EITHER it or its owning\n // project is archived. Both flags propagate onto the docs as `archived`.\n const projectIsArchived = entry.projectSlug\n ? projectArchived.get(entry.projectSlug) === true\n : false;\n const archived = assignment.archived || projectIsArchived;\n\n if (!includeArchived && archived) continue;\n\n const workspace = entry.projectSlug ? projectWorkspace.get(entry.projectSlug) ?? null : null;\n const identity: AssignmentIdentity = {\n assignmentId: assignment.id || null,\n assignmentSlug: entry.assignmentSlug,\n projectSlug: entry.projectSlug,\n projectWorkspace: workspace,\n standalone: entry.standalone,\n type: assignment.type ?? undefined,\n status: assignment.status,\n archived,\n };\n\n // assignment.md itself\n docs.push(makeAssignmentDoc(assignmentMdPath, 'assignment', assignment.title, assignment.body, identity));\n\n // latest plan only\n const planName = await latestPlanFile(entry.assignmentDir);\n if (planName) {\n const planPath = join(entry.assignmentDir, planName);\n if (await fileExists(planPath)) {\n try {\n const plan = parsePlan(await readFile(planPath, 'utf-8'));\n docs.push(makeAssignmentDoc(planPath, 'plan', assignment.title, plan.body, identity));\n } catch {\n /* skip unreadable plan */\n }\n }\n }\n\n // sidecars\n for (const sidecar of SIDECARS) {\n const sidecarPath = resolve(entry.assignmentDir, sidecar.file);\n if (!(await fileExists(sidecarPath))) continue;\n try {\n const body = sidecar.body(await readFile(sidecarPath, 'utf-8'));\n docs.push(makeAssignmentDoc(sidecarPath, sidecar.kind, assignment.title, body, identity));\n } catch {\n /* skip unreadable sidecar */\n }\n }\n }\n\n // ── project memories + resources ────────────────────────────────────────\n if (await fileExists(projectsDir)) {\n const projects = await readdir(projectsDir, { withFileTypes: true });\n for (const m of projects) {\n if (!m.isDirectory()) continue;\n if (m.name.startsWith('.') || m.name.startsWith('_')) continue;\n const projectIsArchived = projectArchived.get(m.name) === true;\n if (projectIsArchived && !includeArchived) continue;\n const projectPath = resolve(projectsDir, m.name);\n const workspace = projectWorkspace.get(m.name) ?? null;\n\n await indexItems(\n docs,\n resolve(projectPath, 'memories'),\n 'memory',\n m.name,\n workspace,\n projectIsArchived,\n (content) => {\n const parsed = parseMemory(content);\n return { title: parsed.name, body: parsed.body };\n },\n );\n await indexItems(\n docs,\n resolve(projectPath, 'resources'),\n 'resource',\n m.name,\n workspace,\n projectIsArchived,\n (content) => {\n const parsed = parseResource(content);\n return { title: parsed.name, body: parsed.body };\n },\n );\n }\n }\n\n return docs;\n}\n\nfunction makeAssignmentDoc(\n path: string,\n fileKind: FileKind,\n title: string,\n body: string,\n identity: AssignmentIdentity,\n): SearchDoc {\n return {\n id: path,\n path,\n fileKind,\n title,\n body,\n projectSlug: identity.projectSlug,\n projectWorkspace: identity.projectWorkspace,\n assignmentSlug: identity.assignmentSlug,\n assignmentId: identity.assignmentId,\n standalone: identity.standalone,\n type: identity.type,\n status: identity.status,\n archived: identity.archived,\n };\n}\n\n/** Index every `*.md` (skipping `_index.md` / dot-prefixed) in a memories/resources dir. */\nasync function indexItems(\n docs: SearchDoc[],\n dir: string,\n fileKind: 'memory' | 'resource',\n projectSlug: string,\n projectWorkspace: string | null,\n archived: boolean,\n extract: (content: string) => { title: string; body: string },\n): Promise<void> {\n if (!(await fileExists(dir))) return;\n const entries = await readdir(dir, { withFileTypes: true });\n for (const e of entries) {\n if (!e.isFile()) continue;\n if (!e.name.endsWith('.md')) continue;\n if (e.name.startsWith('.') || e.name.startsWith('_')) continue;\n const itemSlug = e.name.slice(0, -'.md'.length);\n const filePath = resolve(dir, e.name);\n try {\n const { title, body } = extract(await readFile(filePath, 'utf-8'));\n docs.push({\n id: filePath,\n path: filePath,\n fileKind,\n title,\n body,\n projectSlug,\n projectWorkspace,\n assignmentSlug: null,\n assignmentId: null,\n standalone: false,\n itemSlug,\n archived,\n });\n } catch {\n /* skip unreadable item */\n }\n }\n}\n\n// ── cache + invalidation seam ─────────────────────────────────────────────\n\n/**\n * A stat-only fingerprint of the indexed `.md` files. `mtimeMax` alone misses\n * the deletion of a non-newest file (signature unchanged → stale cache), so we\n * also track `count` and `sizeSum` — both of which change on any add OR delete.\n */\ninterface IndexSignature {\n count: number;\n mtimeMax: number;\n sizeSum: number;\n}\n\ninterface CacheEntry {\n docs: SearchDoc[];\n builtAt: number;\n signature: IndexSignature;\n}\n\nconst cache = new Map<string, CacheEntry>();\n\nfunction cacheKey(opts: IndexOptions): string {\n return `${opts.projectsDir}|${opts.assignmentsDir}|${opts.includeArchived ?? false}`;\n}\n\nfunction signaturesEqual(a: IndexSignature, b: IndexSignature): boolean {\n return a.count === b.count && a.mtimeMax === b.mtimeMax && a.sizeSum === b.sizeSum;\n}\n\n/**\n * Cheap stat-only sweep of the content dirs → an {@link IndexSignature}. Walks\n * dirs (O(files) `stat`s, NOT reads). `count` + `sizeSum` change on add/delete;\n * `mtimeMax` changes on modification. Returns all-zeros when nothing exists.\n */\nasync function indexSignature(\n projectsDir: string,\n assignmentsDir: string,\n): Promise<IndexSignature> {\n let count = 0;\n let mtimeMax = 0;\n let sizeSum = 0;\n async function walk(dir: string): Promise<void> {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (e.name.startsWith('.')) continue;\n const full = resolve(dir, e.name);\n if (e.isDirectory()) {\n await walk(full);\n } else if (e.isFile() && e.name.endsWith('.md')) {\n try {\n const s = await stat(full);\n count += 1;\n sizeSum += s.size;\n if (s.mtimeMs > mtimeMax) mtimeMax = s.mtimeMs;\n } catch {\n /* ignore */\n }\n }\n }\n }\n await walk(projectsDir);\n if (assignmentsDir !== projectsDir) await walk(assignmentsDir);\n return { count, mtimeMax, sizeSum };\n}\n\n/**\n * Return the index for the given dirs, rebuilding only when content changed.\n *\n * Semantics: compute the current {@link IndexSignature} via a stat-only sweep;\n * if a cache entry for this key exists AND its signature is unchanged, return\n * the cached docs (no body reads); otherwise do a full `buildIndex`, replace\n * the cache entry, and return it. The signature changes on add, delete, and\n * modification of any indexed `.md` file.\n */\nexport async function getIndex(opts: IndexOptions): Promise<SearchDoc[]> {\n const key = cacheKey(opts);\n const signature = await indexSignature(opts.projectsDir, opts.assignmentsDir);\n const existing = cache.get(key);\n if (existing && signaturesEqual(existing.signature, signature)) {\n return existing.docs;\n }\n const docs = await buildIndex(opts);\n cache.set(key, { docs, builtAt: Date.now(), signature });\n return docs;\n}\n\n/** Clear the whole cache so the next `getIndex` rebuilds (file-change hook). */\nexport function invalidateIndex(): void {\n cache.clear();\n}\n","/**\n * Default `SearchProvider` — fuse.js full-text over indexed markdown bodies.\n *\n * The provider returns a NEUTRAL snippet (no highlight markers) plus\n * `matches: MatchRange[]` in snippet-local coordinates, a 1-based `line`, and\n * the nearest preceding `section` heading. Callers format highlighting\n * themselves (CLI `**…**`, API/palette HTML-safe `<mark>`).\n *\n * `extractSnippet` and `nearestSection` are pure exported helpers so they're\n * directly unit-testable. Fuse construction follows `src/tui/hooks/useSearch.ts`\n * (now with `includeMatches`).\n */\n\nimport Fuse from 'fuse.js';\nimport type { FuseResultMatch } from 'fuse.js';\nimport type { MatchRange, SearchDoc, SearchHit, SearchProvider, SearchQuery } from './types.js';\nimport { routeForHit } from './route.js';\n\n/** Half-window (chars) on each side of the match offset for the snippet. */\nconst SNIPPET_RADIUS = 60;\n\nexport class FuseProvider implements SearchProvider {\n private docs: SearchDoc[] = [];\n\n index(docs: SearchDoc[]): void {\n this.docs = docs;\n }\n\n query(q: SearchQuery, limit: number): SearchHit[] {\n // 1. Pre-filter the doc subset (cheap; keeps Fuse scores undiluted).\n const subset = this.docs.filter((d) => {\n if (q.project !== undefined && d.projectSlug !== q.project) return false;\n if (q.type && q.type.length > 0 && (!d.type || !q.type.includes(d.type))) return false;\n if (q.status && q.status.length > 0 && (!d.status || !q.status.includes(d.status))) return false;\n if (q.in && q.in.length > 0 && !q.in.includes(d.fileKind)) return false;\n return true;\n });\n\n const fuse = new Fuse(subset, {\n keys: ['title', 'body'],\n threshold: 0.4,\n includeScore: true,\n includeMatches: true,\n ignoreLocation: true,\n minMatchCharLength: 2,\n });\n\n const results = fuse.search(q.query);\n\n const hits: SearchHit[] = [];\n for (const result of results) {\n const doc = result.item;\n const score = result.score ?? 0;\n const bodyMatch = pickBodyMatch(result.matches);\n const { snippet, matches, line, section } = extractSnippet(doc.body, bodyMatch, q.query);\n\n const hit: SearchHit = {\n path: doc.path,\n projectSlug: doc.projectSlug,\n projectWorkspace: doc.projectWorkspace,\n assignmentSlug: doc.assignmentSlug,\n assignmentId: doc.assignmentId,\n standalone: doc.standalone,\n fileKind: doc.fileKind,\n title: doc.title,\n score,\n snippet,\n matches,\n line,\n route: '',\n };\n if (doc.itemSlug !== undefined) hit.itemSlug = doc.itemSlug;\n if (section !== undefined) hit.section = section;\n hit.route = routeForHit(hit);\n hits.push(hit);\n }\n\n hits.sort((a, b) => a.score - b.score);\n return hits.slice(0, limit);\n }\n}\n\n/** First Fuse match on the `body` key (the one we can locate in `doc.body`). */\nfunction pickBodyMatch(\n matches: ReadonlyArray<FuseResultMatch> | undefined,\n): FuseResultMatch | undefined {\n if (!matches) return undefined;\n return matches.find((m) => m.key === 'body');\n}\n\nexport interface SnippetResult {\n /** Neutral text window (no highlight markers). */\n snippet: string;\n /** Match ranges in snippet-local coordinates. */\n matches: MatchRange[];\n /** 1-based line of the match in the source body. */\n line: number;\n /** Nearest preceding markdown heading text, if any. */\n section?: string;\n}\n\n/**\n * Produce a neutral snippet window around the first body match (or a substring\n * fallback for `query`), with snippet-local match ranges, the 1-based line, and\n * the nearest preceding `#`-heading section.\n *\n * `bodyMatch` is the Fuse `matches[]` entry for the `body` key (its `indices`\n * are inclusive `[start, end]` tuples). When absent, we fall back to a\n * case-insensitive substring search for `query`. When no offset can be found at\n * all, the snippet is the first window chars with `matches: []` and `line: 1`.\n */\nexport function extractSnippet(\n body: string,\n bodyMatch: FuseResultMatch | undefined,\n query: string,\n): SnippetResult {\n // Resolve the source-body match ranges (inclusive end → exclusive end).\n let ranges: Array<{ start: number; end: number }> = [];\n if (bodyMatch && bodyMatch.indices.length > 0) {\n ranges = bodyMatch.indices\n .map(([s, e]) => ({ start: s, end: e + 1 }))\n .sort((a, b) => a.start - b.start);\n } else {\n const idx = body.toLowerCase().indexOf(query.trim().toLowerCase());\n if (idx >= 0 && query.trim().length > 0) {\n ranges = [{ start: idx, end: idx + query.trim().length }];\n }\n }\n\n if (ranges.length === 0) {\n const snippet = body.slice(0, SNIPPET_RADIUS * 2);\n const section = nearestSection(body, 0);\n const result: SnippetResult = { snippet, matches: [], line: 1 };\n if (section !== undefined) result.section = section;\n return result;\n }\n\n const first = ranges[0];\n const line = countLines(body, first.start);\n const section = nearestSection(body, first.start);\n\n const windowStart = Math.max(0, first.start - SNIPPET_RADIUS);\n const windowEnd = Math.min(body.length, first.start + SNIPPET_RADIUS);\n const snippet = body.slice(windowStart, windowEnd);\n\n // Translate ranges into snippet-local coords, clamped to the window.\n const matches: MatchRange[] = [];\n for (const r of ranges) {\n const start = Math.max(r.start, windowStart);\n const end = Math.min(r.end, windowEnd);\n if (end <= start) continue;\n matches.push({ start: start - windowStart, end: end - windowStart });\n }\n\n const result: SnippetResult = { snippet, matches, line };\n if (section !== undefined) result.section = section;\n return result;\n}\n\n/** 1-based line number of the char at `offset` (count `\\n` before it). */\nfunction countLines(body: string, offset: number): number {\n let line = 1;\n const limit = Math.min(offset, body.length);\n for (let i = 0; i < limit; i++) {\n if (body[i] === '\\n') line++;\n }\n return line;\n}\n\n/**\n * Nearest preceding markdown `#`-heading text at or before `offset`. Returns the\n * heading text (without the `#` markers) or `undefined` when none precedes.\n */\nexport function nearestSection(body: string, offset: number): string | undefined {\n const before = body.slice(0, offset);\n const headingRe = /^#{1,6}\\s+(.+?)\\s*$/gm;\n let match: RegExpExecArray | null;\n let last: string | undefined;\n while ((match = headingRe.exec(before)) !== null) {\n last = match[1].trim();\n }\n return last;\n}\n","/**\n * Semantic search seam — a stub provider for the future embeddings slot, plus\n * the `resolveProvider` resolver that returns the semantic provider only when\n * `--semantic` is set AND it's available, otherwise gracefully falls back to the\n * default `FuseProvider`. No embeddings are configured today, so\n * `SemanticProvider.isAvailable()` is always `false` and we always fall back.\n */\n\nimport type { SearchDoc, SearchHit, SearchProvider, SearchQuery } from './types.js';\nimport { FuseProvider } from './fuse-provider.js';\n\n/** Thrown by the stub's `index`/`query` — the seam is not yet implemented. */\nexport class NotImplementedError extends Error {\n constructor(message = 'SemanticProvider is not implemented yet') {\n super(message);\n this.name = 'NotImplementedError';\n }\n}\n\nexport class SemanticProvider implements SearchProvider {\n /** No embeddings configured → never available in v1. */\n static isAvailable(): boolean {\n return false;\n }\n\n index(_docs: SearchDoc[]): void {\n throw new NotImplementedError();\n }\n\n query(_q: SearchQuery, _limit: number): SearchHit[] {\n throw new NotImplementedError();\n }\n}\n\n/**\n * Pick the search provider. Returns a `SemanticProvider` only when the caller\n * asked for `--semantic` AND it's actually available; otherwise the default\n * `FuseProvider`.\n */\nexport function resolveProvider(opts?: { semantic?: boolean }): SearchProvider {\n if (opts?.semantic && SemanticProvider.isAvailable()) {\n return new SemanticProvider();\n }\n return new FuseProvider();\n}\n","/**\n * Barrel for the shared search core — re-exports everything the CLI\n * (`src/commands/search.ts`) and the dashboard (`src/dashboard/api-search.ts` +\n * palette) consume.\n */\n\nexport type {\n FileKind,\n SearchDoc,\n SearchHit,\n MatchRange,\n SearchQuery,\n SearchProvider,\n} from './types.js';\nexport { FILE_KINDS, FILE_KIND_ALIASES, parseFileKinds } from './types.js';\n\nexport { FILE_KIND_TO_TAB, slugifyHeading, routeForHit } from './route.js';\n\nexport { buildIndex, getIndex, invalidateIndex } from './indexer.js';\nexport type { IndexOptions } from './indexer.js';\n\nexport { FuseProvider, extractSnippet, nearestSection } from './fuse-provider.js';\nexport type { SnippetResult } from './fuse-provider.js';\n\nexport { SemanticProvider, resolveProvider, NotImplementedError } from './semantic-provider.js';\n","import type {\n HelpChecklistItem,\n HelpCommand,\n HelpResponse,\n HelpStatusGuideEntry,\n} from './types.js';\nimport { getStatusConfig } from './api.js';\n\nconst CLI_COMMANDS: HelpCommand[] = [\n // --- Core setup & scaffolding (indices 0-4) ---\n {\n command: 'syntaur setup',\n description: 'Initialize Syntaur and optionally install plugins or launch the dashboard.',\n example: 'syntaur setup',\n },\n {\n command: 'syntaur init',\n description: 'Initialize the local Syntaur home directory and config scaffolding without any prompts.',\n example: 'syntaur init',\n },\n {\n command: 'syntaur create-project',\n description: 'Create a new project folder with the required source and derived files.',\n example: 'syntaur create-project \"Ship dashboard overhaul\"',\n },\n {\n command: 'syntaur create-assignment',\n description: 'Create a new assignment inside a project.',\n example: 'syntaur create-assignment \"Implement overview API\" --project ui-overhaul',\n },\n {\n command: 'syntaur assign',\n description: 'Set the assignee for an assignment before work begins.',\n example: 'syntaur assign implement-overview --project ui-overhaul --agent codex-1',\n },\n\n // --- Lifecycle transitions ---\n {\n command: 'syntaur start',\n description: 'Transition an assignment to in_progress.',\n example: 'syntaur start implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur shape',\n description: 'Transition a draft assignment to ready_for_planning once the Objective and Acceptance Criteria are fleshed out.',\n example: 'syntaur shape implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur plan-ready',\n description: 'Transition a ready_for_planning assignment to ready_to_implement once a plan has been written and approved.',\n example: 'syntaur plan-ready implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur implement',\n description: 'Transition a ready_to_implement assignment to in_progress when coding begins.',\n example: 'syntaur implement implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur migrate-statuses',\n description: 'Suggest pending -> ready_for_planning promotions for fleshed-out assignments. Dry-run by default; pass --apply to write.',\n example: 'syntaur migrate-statuses --apply',\n },\n {\n command: 'syntaur review',\n description: 'Move active work into review once implementation is ready for inspection.',\n example: 'syntaur review implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur complete',\n description: 'Mark an assignment completed after review or direct completion.',\n example: 'syntaur complete implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur block',\n description: 'Mark an assignment blocked and record the explicit reason.',\n example: 'syntaur block implement-overview --project ui-overhaul --reason \"Waiting on API spec\"',\n },\n {\n command: 'syntaur unblock',\n description: 'Move a blocked assignment back to in_progress after the blocker is cleared.',\n example: 'syntaur unblock implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur fail',\n description: 'Mark an assignment failed when it cannot be completed as planned.',\n example: 'syntaur fail implement-overview --project ui-overhaul',\n },\n {\n command: 'syntaur reopen',\n description: 'Reopen a completed or failed assignment back to in_progress.',\n example: 'syntaur reopen implement-overview --project ui-overhaul',\n },\n\n // --- Dashboard (index 12) ---\n {\n command: 'syntaur dashboard',\n description: 'Start the local dashboard UI over the project files on disk.',\n example: 'syntaur dashboard --port 4800',\n },\n\n // --- Plugin & adapter setup (indices 13-16) ---\n {\n command: 'syntaur install-plugin',\n description: 'Install the Syntaur Claude Code plugin, detecting the local Claude marketplace when available and prompting for the target directory when interactive.',\n example: 'syntaur install-plugin --target-dir ~/.claude/plugins/marketplaces/user-plugins/plugins/syntaur',\n },\n {\n command: 'syntaur install-codex-plugin',\n description: 'Install the Syntaur Codex plugin and register its marketplace entry, prompting for both paths when interactive.',\n example: 'syntaur install-codex-plugin --target-dir ~/plugins/syntaur --marketplace-path ~/.agents/plugins/marketplace.json',\n },\n {\n command: 'syntaur uninstall',\n description: 'Remove Syntaur plugins and optionally local ~/.syntaur data.',\n example: 'syntaur uninstall --all',\n },\n {\n command: 'syntaur setup-adapter',\n description: 'Generate adapter instruction files for cursor, codex, or opencode in the current directory.',\n example: 'syntaur setup-adapter cursor --project ui-overhaul --assignment implement-overview',\n },\n\n // --- Session & server tracking (index 17) ---\n {\n command: 'syntaur track-session',\n description:\n 'Register an agent session. Requires --session-id from the agent runtime (real, not generated). Pass --transcript-path for the rollout/transcript file. --project and --assignment are optional.',\n example:\n 'syntaur track-session --agent claude --session-id <real-id> --transcript-path <path> --project ui-overhaul --assignment implement-overview',\n },\n\n // --- Browsing & playbooks (indices 18-20) ---\n {\n command: 'syntaur browse',\n description: 'Interactive TUI browser for projects and assignments.',\n example: 'syntaur browse',\n },\n {\n command: 'syntaur create-playbook',\n description: 'Create a new playbook with behavioral rules for agents.',\n example: 'syntaur create-playbook \"Code Review Standards\"',\n },\n {\n command: 'syntaur list-playbooks',\n description:\n 'List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.',\n example: 'syntaur list-playbooks --all',\n },\n {\n command: 'syntaur enable-playbook',\n description:\n 'Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.',\n example: 'syntaur enable-playbook commit-discipline',\n },\n {\n command: 'syntaur disable-playbook',\n description:\n 'Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.',\n example: 'syntaur disable-playbook commit-discipline',\n },\n {\n command: 'syntaur delete-playbook',\n description:\n 'Delete a playbook from disk and regenerate the manifest. Refuses to delete the manifest itself.',\n example: 'syntaur delete-playbook scratch-foo',\n },\n];\n\nconst WORKFLOW: HelpChecklistItem[] = [\n {\n title: 'Initialize the workspace',\n detail: 'Run setup once so Syntaur can initialize its local home directory and offer plugin installation.',\n command: CLI_COMMANDS[0],\n },\n {\n title: 'Create a project',\n detail: 'Use a project for a higher-level objective. Projects group assignments, shared resources, and memories.',\n command: CLI_COMMANDS[2],\n href: '/create/project',\n },\n {\n title: 'Create the first assignment',\n detail: 'Assignments are the execution unit. Create one for each concrete chunk of work inside the project.',\n command: CLI_COMMANDS[3],\n },\n {\n title: 'Assign the work',\n detail: 'Setting an assignee before starting is recommended for clarity, but not required.',\n command: CLI_COMMANDS[4],\n },\n {\n title: 'Start, review, complete, or block through lifecycle actions',\n detail: 'Status changes happen through lifecycle actions, kanban drag-and-drop, or the status override controls.',\n command: CLI_COMMANDS[5],\n },\n {\n title: 'Use the dashboard for triage and context',\n detail: 'Overview shows the current queue, project pages show health, assignment pages show the execution surface.',\n command: CLI_COMMANDS[12],\n href: '/',\n },\n];\n\nconst DEFAULT_STATUS_GUIDE: Record<string, { meaning: string; useWhen: string }> = {\n draft: {\n meaning: 'The assignment is a just-created stub; objective and acceptance criteria are not yet fleshed out.',\n useWhen: 'Use draft for newly-scaffolded assignments. Transition to ready_for_planning with `syntaur shape` once the Objective and AC are written.',\n },\n pending: {\n meaning: 'The assignment has not started yet.',\n useWhen: 'Use pending while waiting to start. If dependencies are unmet, pending is the normal waiting state.',\n },\n ready_for_planning: {\n meaning: 'The assignment is fully shaped; a plan needs to be written before implementation can begin.',\n useWhen: 'Use ready_for_planning after the Objective and Acceptance Criteria are filled out but before any plan.md exists. Transition to ready_to_implement with `syntaur plan-ready` after the plan is approved.',\n },\n ready_to_implement: {\n meaning: 'The plan has been written and approved; the assignment is ready to start coding.',\n useWhen: 'Use ready_to_implement once a plan.md exists and is approved. Transition to in_progress with `syntaur implement` when coding begins.',\n },\n in_progress: {\n meaning: 'An assigned agent is actively working the assignment.',\n useWhen: 'Use in_progress once the work has started and dependencies are satisfied.',\n },\n blocked: {\n meaning: 'The assignment hit a manual or runtime obstacle.',\n useWhen: 'Use blocked when work hits an obstacle. Adding a blockedReason is recommended for traceability.',\n },\n review: {\n meaning: 'Implementation is ready for inspection or validation.',\n useWhen: 'Use review after active work is ready to be checked before completion.',\n },\n completed: {\n meaning: 'The assignment is done.',\n useWhen: 'Use completed when the acceptance criteria are satisfied.',\n },\n failed: {\n meaning: 'The assignment could not be completed as planned.',\n useWhen: 'Use failed when the work cannot be recovered within the current assignment.',\n },\n};\n\nasync function buildStatusGuide(): Promise<HelpStatusGuideEntry[]> {\n const config = await getStatusConfig();\n\n return config.statuses.map((s) => {\n const defaults = DEFAULT_STATUS_GUIDE[s.id];\n return {\n status: s.id,\n meaning: s.description ?? defaults?.meaning ?? `The assignment is in the \"${s.label}\" state.`,\n useWhen: defaults?.useWhen ?? `Use ${s.id} when appropriate for the \"${s.label}\" workflow state.`,\n };\n });\n}\n\nexport async function getDashboardHelp(): Promise<HelpResponse> {\n return {\n generatedAt: new Date().toISOString(),\n whatIsSyntaur: {\n summary:\n 'Syntaur is a local-first, markdown-backed agent work system. The dashboard is a live view over project folders and files on disk.',\n bullets: [\n 'Markdown files are the source of truth.',\n 'The UI reads project folders, assignment files, and derived indexes from the local filesystem.',\n 'Derived underscore-prefixed files are projections, not the canonical edit target.',\n ],\n },\n coreConcepts: [\n {\n term: 'Project',\n description:\n 'A project is the higher-level objective. It owns assignments, shared resources, and project memories.',\n },\n {\n term: 'Assignment',\n description:\n 'An assignment is a concrete unit of execution. Assignment frontmatter is the source of truth for status, priority, assignee, and dependencies.',\n },\n {\n term: 'Resource',\n description:\n 'A project-level shared reference file that provides source material or constraints for the work.',\n },\n {\n term: 'Memory',\n description:\n 'A project-level learning or pattern captured during execution so future assignments can reuse it.',\n },\n {\n term: 'Manifest',\n description:\n 'A derived navigation file that points agents at the project overview, indexes, and agent instructions.',\n },\n {\n term: 'Derived file',\n description:\n 'An underscore-prefixed file regenerated from canonical markdown sources. Read it, but do not edit it directly.',\n },\n {\n term: 'Handoff',\n description:\n 'An append-only log that records baton-passes between agents or sessions without rewriting prior history.',\n },\n {\n term: 'Decision record',\n description:\n 'An append-only record of important decisions, rationale, and follow-up consequences.',\n },\n {\n term: 'Playbook',\n description:\n 'A behavioral rule set stored in ~/.syntaur/playbooks/. Playbooks define constraints and conventions that agents must follow during execution. Manage them via the CLI or the Playbooks page.',\n },\n {\n term: 'Workspace',\n description:\n 'The repository context for an assignment, including the repository path, worktree path, branch, and parent branch. Workspace fields connect an assignment to the code being worked on and define write boundaries.',\n },\n {\n term: 'Agent Session',\n description:\n 'A tracked AI session tied to assignment work. Sessions are registered via the track-session CLI command or the Claude Code plugin and visible on the Agent Sessions page.',\n },\n {\n term: 'Server',\n description:\n 'A tracked tmux session with automatic port discovery, branch detection, and assignment linking. The Servers page shows all tracked sessions with their windows, panes, and discovered services.',\n },\n ],\n workflow: WORKFLOW,\n statusGuide: await buildStatusGuide(),\n ownershipRules: [\n {\n label: 'Human-authored files',\n files: ['project.md', 'agent.md', 'claude.md'],\n description:\n 'These files define project intent and instructions. The dashboard treats project status as derived except for the archive fields.',\n },\n {\n label: 'Assignment working files',\n files: ['assignment.md', 'plan*.md (optional, versioned)', 'scratchpad.md'],\n description:\n 'These are agent-writable files. The dashboard lets you edit the source markdown while preserving unsupported frontmatter keys.',\n },\n {\n label: 'Append-only logs',\n files: ['handoff.md', 'decision-record.md'],\n description:\n 'These logs preserve history. The dashboard appends new entries instead of rewriting previous ones.',\n },\n {\n label: 'Derived files',\n files: ['_status.md', '_index-assignments.md', '_index-plans.md', '_index-decisions.md'],\n description:\n 'These files are read-only projections. They can lag behind source files, so the dashboard computes source-first state.',\n },\n ],\n commands: CLI_COMMANDS,\n navigation: [\n {\n label: 'Overview',\n description: 'Triage hub showing assignments that need action, recent activity, progress stats, and first-run setup guidance.',\n href: '/',\n },\n {\n label: 'Projects',\n description: 'Browse, search, filter, and sort the project directory. Create new projects and drill into project workspaces.',\n href: '/projects',\n },\n {\n label: 'Assignments',\n description: 'Cross-project kanban board of all assignments. Drag cards between columns to change status, or filter by project, assignee, or status.',\n href: '/assignments',\n },\n {\n label: 'Servers',\n description: 'Tracked tmux sessions with auto-discovered ports, URLs, git branches, and links to related assignments. Register sessions manually or let autodiscovery find them.',\n href: '/servers',\n },\n {\n label: 'Agent Sessions',\n description: 'Monitor which AI agents are currently working, what assignments they are linked to, and session duration. Sessions are registered via the Claude Code plugin or track-session CLI command.',\n href: '/agent-sessions',\n },\n {\n label: 'Playbooks',\n description: 'Create, browse, and edit behavioral rules that agents must follow. The playbook manifest at ~/.syntaur/playbooks/manifest.md is auto-generated for inclusion in agent instructions.',\n href: '/playbooks',\n },\n {\n label: 'Help',\n description: 'This page. Status guide, CLI quick reference, core concepts, and FAQ.',\n href: '/help',\n },\n {\n label: 'Settings',\n description: 'Customize status definitions, labels, colors, display order, and done states. Changes apply globally across the dashboard and CLI.',\n href: '/settings',\n },\n {\n label: 'Project page',\n description: 'The project workspace shows health stats, assignment list, dependency graph, shared resources, and memories.',\n href: '/projects',\n },\n {\n label: 'Assignment page',\n description: 'The assignment workspace shows lifecycle actions, plan editor, scratchpad, handoff log, decision records, and agent sessions.',\n href: '/projects',\n },\n ],\n faq: [\n {\n question: 'Why are some files read-only in the dashboard?',\n answer:\n 'Underscore-prefixed files are derived projections that can be rebuilt from canonical markdown sources. Editing them would create drift, so the UI treats them as read-only.',\n },\n {\n question: 'Why can an assignment be pending even when nothing looks broken?',\n answer:\n 'Pending often just means the work has not started yet or it is waiting on declared dependencies. Blocked is reserved for exceptional runtime obstacles that need intervention.',\n },\n {\n question: 'How do I change an assignment\\'s status?',\n answer:\n 'Use lifecycle CLI commands (syntaur start, syntaur complete, etc.), drag cards on the kanban board, or use the Override Status dropdown on the assignment page. Any status can be set from any other status.',\n },\n {\n question: 'How do I customize statuses?',\n answer:\n 'Open the Settings page from the sidebar. You can add, remove, rename, recolor, and reorder statuses. You can also mark statuses as done states. Changes are saved to ~/.syntaur/config.md and take effect immediately across the dashboard.',\n },\n {\n question: 'What is a done state?',\n answer:\n 'A done state (also called terminal status) means the assignment is finished. Done states fill the completed portion of progress bars and satisfy dependency requirements. By default, \"completed\" and \"failed\" are done states. You can configure which statuses are done states in Settings.',\n },\n {\n question: 'What are playbooks and how do I use them?',\n answer:\n 'Playbooks are markdown files in ~/.syntaur/playbooks/ that define behavioral rules agents must follow. Create them via the CLI (syntaur create-playbook) or the Playbooks page. The auto-generated manifest at ~/.syntaur/playbooks/manifest.md can be included in your CLAUDE.md so agents pick up the rules.',\n },\n {\n question: 'How does agent session tracking work?',\n answer:\n 'When an AI agent starts working on an assignment, it can register a session via the track-session CLI command or the Claude Code plugin\\'s /track-session command. The Agent Sessions page shows active and completed sessions with their linked assignments and duration.',\n },\n {\n question: 'How does server tracking work?',\n answer:\n 'Syntaur tracks tmux sessions to discover running dev servers, their ports, git branches, and linked assignments. Register sessions on the Servers page or let autodiscovery find them. Pane info refreshes automatically.',\n },\n ],\n firstProjectChecklist: [\n {\n title: 'Create the project',\n detail: 'Describe the overall objective in project.md, then add tags and archive metadata only when needed.',\n command: CLI_COMMANDS[1],\n href: '/create/project',\n },\n {\n title: 'Create at least one assignment',\n detail: 'Break the project into executable work units with explicit priority and dependencies.',\n command: CLI_COMMANDS[2],\n },\n {\n title: 'Assign and start the first assignment',\n detail: 'Set an assignee, then start the assignment once prerequisites are complete.',\n command: CLI_COMMANDS[3],\n },\n {\n title: 'Use the assignment workspace for execution',\n detail: 'Keep the objective and todos in assignment.md, implementation plans in optional versioned plan files (plan.md, plan-v2.md, ...), and transient notes in scratchpad.md.',\n href: '/projects',\n },\n {\n title: 'Record handoffs and decisions without rewriting history',\n detail: 'Append new handoff and decision entries instead of editing prior entries.',\n },\n {\n title: 'Return to Overview for triage',\n detail: 'Overview surfaces the queue of assignments that need action next.',\n href: '/',\n },\n ],\n links: [\n { label: 'Overview', href: '/' },\n { label: 'Project Directory', href: '/projects' },\n { label: 'Assignments Board', href: '/assignments' },\n { label: 'Servers', href: '/servers' },\n { label: 'Agent Sessions', href: '/agent-sessions' },\n { label: 'Playbooks', href: '/playbooks' },\n { label: 'Settings', href: '/settings' },\n { label: 'Create Project', href: '/create/project' },\n ],\n };\n}\n\nexport function getHelpCommandNames(): string[] {\n return CLI_COMMANDS.map((command) => command.command.replace(/^syntaur\\s+/, ''));\n}\n","import Database from 'better-sqlite3';\nimport { resolve } from 'node:path';\nimport { readdir } from 'node:fs/promises';\nimport { syntaurRoot } from '../utils/paths.js';\nimport { fileExists } from '../utils/fs.js';\nimport type { AgentSession, AgentSessionStatus } from './types.js';\n\nlet db: Database.Database | null = null;\n\nconst SCHEMA_VERSION = '5';\n\n// The base schema deliberately OMITS the project_slug indexes — they are\n// created after any legacy-schema migrations run below. Older installs may\n// have the `mission_slug` column, and creating an index that references\n// `project_slug` before the rename migration runs would fail.\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS sessions (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n transcript_path TEXT,\n pid INTEGER,\n pid_started_at TEXT,\n original_head_sha TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\nCREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\nCREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);\n`;\n\nconst POST_MIGRATION_INDEXES_SQL = `\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\nCREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n`;\n\n/**\n * Initialize the SQLite database for session tracking.\n * Creates the database file and schema if they don't exist.\n * @param dbPath Optional override for the database file path (used in tests).\n */\nexport function initSessionDb(dbPath?: string): Database.Database {\n if (db) return db;\n\n const finalPath = dbPath ?? resolve(syntaurRoot(), 'syntaur.db');\n db = new Database(finalPath);\n db.pragma('journal_mode = WAL');\n db.exec(SCHEMA_SQL);\n\n // Track schema version\n db.prepare('INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)').run(\n 'schema_version',\n SCHEMA_VERSION,\n );\n\n // Run migrations inside an EXCLUSIVE transaction. This closes two races:\n // 1. Crash between `DROP TABLE` / `RENAME` / `UPDATE meta` leaves the db\n // half-upgraded — the transaction rolls back on failure.\n // 2. Two processes (e.g. `syntaur dashboard` + `syntaur track-session`)\n // both calling initSessionDb() at once — EXCLUSIVE serializes the\n // migration and the version is re-checked inside the transaction so\n // the second process becomes a no-op once the first commits.\n // Narrow for the transaction closure — TS doesn't track the module-level\n // `db` assignment across the closure boundary.\n const database = db;\n const runMigrations = database.transaction(() => {\n // --- v1 → v2: make project/assignment nullable, add description ---\n const vBeforeV2 = (\n database\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined\n )?.value;\n\n if (vBeforeV2 === '1') {\n database.exec(`\n CREATE TABLE sessions_v2 (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;\n DROP TABLE sessions;\n ALTER TABLE sessions_v2 RENAME TO sessions;\n CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\n UPDATE meta SET value = '2' WHERE key = 'schema_version';\n `);\n }\n\n // --- v2 → v3: add transcript_path, normalize legacy mission_slug ---\n // Re-read the version AFTER v1→v2 may have run.\n const vBeforeV3 = (\n database\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined\n )?.value;\n\n if (vBeforeV3 === '2') {\n const v2Columns = database\n .prepare('PRAGMA table_info(sessions)')\n .all() as Array<{ name: string }>;\n const v2ColNames = v2Columns.map((c) => c.name);\n const hasProject = v2ColNames.includes('project_slug');\n const hasMission = v2ColNames.includes('mission_slug');\n\n // If a db somehow has both columns (e.g. a partially-renamed table),\n // prefer project_slug but fall back to mission_slug so rows that only\n // populated mission_slug aren't dropped.\n const projectSlugExpr =\n hasProject && hasMission\n ? 'COALESCE(project_slug, mission_slug)'\n : hasProject\n ? 'project_slug'\n : hasMission\n ? 'mission_slug'\n : null;\n\n if (!projectSlugExpr) {\n throw new Error(\n 'sessions table has neither project_slug nor mission_slug; cannot migrate from v2 to v3',\n );\n }\n\n database.exec(`\n CREATE TABLE sessions_v3 (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n transcript_path TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO sessions_v3\n SELECT session_id, ${projectSlugExpr}, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at\n FROM sessions;\n DROP TABLE sessions;\n ALTER TABLE sessions_v3 RENAME TO sessions;\n CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\n UPDATE meta SET value = '3' WHERE key = 'schema_version';\n `);\n }\n\n // --- v3 → v4: add pid + pid_started_at for liveness detection ---\n const vBeforeV4 = (\n database\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined\n )?.value;\n\n if (vBeforeV4 === '3') {\n database.exec(`\n CREATE TABLE sessions_v4 (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n transcript_path TEXT,\n pid INTEGER,\n pid_started_at TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO sessions_v4\n SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, description, transcript_path, NULL, NULL, created_at, updated_at\n FROM sessions;\n DROP TABLE sessions;\n ALTER TABLE sessions_v4 RENAME TO sessions;\n CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\n UPDATE meta SET value = '4' WHERE key = 'schema_version';\n `);\n }\n\n // --- v4 → v5: add original_head_sha for exact worktree recreation ---\n const vBeforeV5 = (\n database\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined\n )?.value;\n\n if (vBeforeV5 === '4') {\n database.exec(`\n CREATE TABLE sessions_v5 (\n session_id TEXT PRIMARY KEY,\n project_slug TEXT,\n assignment_slug TEXT,\n agent TEXT NOT NULL,\n started TEXT NOT NULL,\n ended TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n path TEXT,\n description TEXT,\n transcript_path TEXT,\n pid INTEGER,\n pid_started_at TEXT,\n original_head_sha TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO sessions_v5\n SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, description, transcript_path, pid, pid_started_at, NULL, created_at, updated_at\n FROM sessions;\n DROP TABLE sessions;\n ALTER TABLE sessions_v5 RENAME TO sessions;\n CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);\n CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\n UPDATE meta SET value = '5' WHERE key = 'schema_version';\n `);\n }\n });\n runMigrations.exclusive();\n\n // Create project-slug-dependent indexes now that we know the column exists.\n db.exec(POST_MIGRATION_INDEXES_SQL);\n\n return db;\n}\n\n/** True once initSessionDb() has run (and the handle wasn't closed/reset). */\nexport function isSessionDbInitialized(): boolean {\n return db !== null;\n}\n\n/**\n * Get the initialized database handle.\n * Throws if initSessionDb() has not been called.\n */\nexport function getSessionDb(): Database.Database {\n if (!db) {\n throw new Error(\n 'Session database not initialized. Call initSessionDb() first.',\n );\n }\n return db;\n}\n\n/**\n * Close the database connection.\n */\nexport function closeSessionDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Reset the singleton for testing purposes.\n */\nexport function resetSessionDb(): void {\n db = null;\n}\n\n/**\n * One-time migration: import sessions from markdown _index-sessions.md files into SQLite.\n * Only runs if the sessions table is empty and markdown files exist.\n */\nexport async function migrateFromMarkdown(projectsDir: string): Promise<number> {\n const database = getSessionDb();\n\n // Skip if sessions already exist in the database\n const count = database.prepare('SELECT COUNT(*) as count FROM sessions').get() as { count: number };\n if (count.count > 0) return 0;\n\n if (!(await fileExists(projectsDir))) return 0;\n\n const entries = await readdir(projectsDir, { withFileTypes: true });\n const allSessions: AgentSession[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const projectDir = resolve(projectsDir, entry.name);\n const indexPath = resolve(projectDir, '_index-sessions.md');\n if (!(await fileExists(indexPath))) continue;\n\n const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);\n allSessions.push(...sessions);\n }\n\n if (allSessions.length === 0) return 0;\n\n const insert = database.prepare(`\n INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n\n const insertAll = database.transaction((sessions: AgentSession[]) => {\n for (const s of sessions) {\n insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);\n }\n });\n\n insertAll(allSessions);\n console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);\n return allSessions.length;\n}\n\n/**\n * Parse an _index-sessions.md file into AgentSession objects.\n * Used only for one-time migration. This is a copy of the old parsing logic.\n */\nasync function parseMarkdownSessionsIndex(\n filePath: string,\n projectSlug: string,\n): Promise<AgentSession[]> {\n const { readFile } = await import('node:fs/promises');\n const raw = await readFile(filePath, 'utf-8');\n const sessions: AgentSession[] = [];\n\n const lines = raw.split('\\n');\n let inTable = false;\n let headerSeen = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n if (trimmed.startsWith('| Assignment') || trimmed.startsWith('|Assignment')) {\n inTable = true;\n headerSeen = false;\n continue;\n }\n\n if (inTable && !headerSeen && trimmed.match(/^\\|[-\\s|]+\\|$/)) {\n headerSeen = true;\n continue;\n }\n\n if (inTable && headerSeen && trimmed.startsWith('|')) {\n const cells = trimmed\n .split('|')\n .slice(1, -1)\n .map((c) => c.trim());\n\n if (cells.length >= 6) {\n sessions.push({\n assignmentSlug: cells[0],\n agent: cells[1],\n sessionId: cells[2],\n started: cells[3],\n status: (cells[4] as AgentSessionStatus) || 'active',\n path: cells[5],\n projectSlug,\n });\n }\n }\n }\n\n return sessions;\n}\n","import { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { fileExists } from '../utils/fs.js';\nimport { getSessionDb } from './session-db.js';\nimport type { AgentSession, AgentSessionStatus } from './types.js';\n\ninterface SessionRow {\n session_id: string;\n project_slug: string | null;\n assignment_slug: string | null;\n agent: string;\n started: string;\n ended: string | null;\n status: string;\n path: string | null;\n description: string | null;\n transcript_path: string | null;\n pid: number | null;\n pid_started_at: string | null;\n original_head_sha: string | null;\n updated_at: string | null;\n}\n\nfunction rowToSession(row: SessionRow): AgentSession {\n return {\n sessionId: row.session_id,\n projectSlug: row.project_slug ?? null,\n assignmentSlug: row.assignment_slug ?? null,\n agent: row.agent,\n started: row.started,\n ended: row.ended ?? null,\n status: row.status as AgentSessionStatus,\n path: row.path ?? '',\n description: row.description ?? null,\n transcriptPath: row.transcript_path ?? null,\n pid: row.pid ?? null,\n pidStartedAt: row.pid_started_at ?? null,\n originalHeadSha: row.original_head_sha ?? null,\n updatedAt: row.updated_at ?? null,\n };\n}\n\n/**\n * Query sessions for a specific project.\n */\nexport async function parseSessionsIndex(\n _projectDir: string,\n projectSlug: string,\n): Promise<AgentSession[]> {\n const db = getSessionDb();\n const rows = db\n .prepare('SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC')\n .all(projectSlug) as SessionRow[];\n return rows.map(rowToSession);\n}\n\n/**\n * Upsert a session keyed on `session_id`.\n *\n * On conflict, non-null fields in the new payload fill in missing values on the\n * existing row (COALESCE). `started` / `created_at` from the first insert are\n * preserved. A session already in a terminal state (`completed` / `stopped`)\n * is NOT revived by re-registration — status only moves forward — with one\n * narrow exception: `opts.reviveStopped` lets an `active` payload flip a\n * `stopped` row back to active. Callers may only pass it on live-process\n * evidence (the scanner seeing a process hold the transcript open).\n * `completed` always sticks.\n *\n * Makes registration idempotent across SessionStart hooks, `/track-session`,\n * and grab-assignment all touching the same real session ID.\n */\nexport async function appendSession(\n _projectDir: string,\n session: AgentSession,\n opts?: { reviveStopped?: boolean },\n): Promise<void> {\n const db = getSessionDb();\n db.prepare(`\n INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(session_id) DO UPDATE SET\n project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),\n assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),\n agent = excluded.agent,\n status = CASE\n WHEN status = 'completed' THEN status\n WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status\n ELSE excluded.status\n END,\n path = COALESCE(NULLIF(excluded.path, ''), path),\n description = COALESCE(NULLIF(excluded.description, ''), description),\n transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),\n pid = COALESCE(excluded.pid, pid),\n pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),\n original_head_sha = COALESCE(NULLIF(original_head_sha, ''), NULLIF(excluded.original_head_sha, '')),\n updated_at = datetime('now')\n `).run(\n session.sessionId,\n session.projectSlug ?? null,\n session.assignmentSlug ?? null,\n session.agent,\n session.started,\n session.status,\n session.path,\n session.description ?? null,\n session.transcriptPath ?? null,\n session.pid ?? null,\n session.pidStartedAt ?? null,\n session.originalHeadSha ?? null,\n opts?.reviveStopped ? 1 : 0,\n );\n}\n\n/**\n * Update a session's status by sessionId.\n * Sets `ended` timestamp for terminal statuses (completed, stopped).\n * `endedAt` (ISO 8601) overrides the default `datetime('now')` so sweeps can\n * backdate `ended` to the transcript's last mtime.\n */\nexport async function updateSessionStatus(\n _projectDir: string,\n sessionId: string,\n status: AgentSessionStatus,\n endedAt?: string,\n): Promise<boolean> {\n const db = getSessionDb();\n const isTerminal = status === 'completed' || status === 'stopped';\n\n const result = isTerminal\n ? db\n .prepare(\n 'UPDATE sessions SET status = ?, ended = COALESCE(?, datetime(\\'now\\')), updated_at = datetime(\\'now\\') WHERE session_id = ?',\n )\n .run(status, endedAt ?? null, sessionId)\n : db\n .prepare(\n 'UPDATE sessions SET status = ?, updated_at = datetime(\\'now\\') WHERE session_id = ?',\n )\n .run(status, sessionId);\n\n return result.changes > 0;\n}\n\n/**\n * List all sessions across all projects.\n */\nexport async function listAllSessions(_projectsDir: string): Promise<AgentSession[]> {\n const db = getSessionDb();\n const rows = db\n .prepare('SELECT * FROM sessions ORDER BY started DESC')\n .all() as SessionRow[];\n return rows.map(rowToSession);\n}\n\n/**\n * Fetch a single session by its agent-assigned session id.\n * Returns null when no row matches. Throws if initSessionDb() has not run.\n */\nexport function getSessionById(sessionId: string): AgentSession | null {\n const db = getSessionDb();\n const row = db\n .prepare('SELECT * FROM sessions WHERE session_id = ? LIMIT 1')\n .get(sessionId) as SessionRow | undefined;\n return row ? rowToSession(row) : null;\n}\n\n/**\n * List sessions for a specific project, optionally filtered by assignment.\n */\nexport async function listProjectSessions(\n _projectsDir: string,\n projectSlug: string,\n assignmentSlug?: string,\n): Promise<AgentSession[]> {\n const db = getSessionDb();\n\n if (assignmentSlug) {\n const rows = db\n .prepare(\n 'SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC',\n )\n .all(projectSlug, assignmentSlug) as SessionRow[];\n return rows.map(rowToSession);\n }\n\n const rows = db\n .prepare('SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC')\n .all(projectSlug) as SessionRow[];\n return rows.map(rowToSession);\n}\n\n/**\n * Delete sessions by their IDs. Returns the number of rows deleted.\n */\nexport async function deleteSessions(sessionIds: string[]): Promise<number> {\n if (sessionIds.length === 0) return 0;\n const db = getSessionDb();\n const placeholders = sessionIds.map(() => '?').join(', ');\n const result = db\n .prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`)\n .run(...sessionIds);\n return result.changes;\n}\n\n// Statuses that imply the working session is done (review means agent finished)\nconst DONE_ASSIGNMENT_STATUSES = new Set(['completed', 'failed', 'review']);\n\n/**\n * Read the status field from an assignment.md frontmatter without full parsing.\n */\nasync function readAssignmentStatusFromPath(\n assignmentMdPath: string,\n): Promise<string | null> {\n if (!(await fileExists(assignmentMdPath))) return null;\n const raw = await readFile(assignmentMdPath, 'utf-8');\n const match = raw.match(/^status:\\s*(.+)$/m);\n return match ? match[1].trim() : null;\n}\n\nasync function readAssignmentStatus(\n projectDir: string,\n assignmentSlug: string,\n): Promise<string | null> {\n return readAssignmentStatusFromPath(\n resolve(projectDir, 'assignments', assignmentSlug, 'assignment.md'),\n );\n}\n\n/**\n * Reconcile active sessions against assignment statuses.\n * Sessions whose assignments have moved to completed/failed/review are\n * marked as completed (or stopped for failed assignments).\n * Standalone sessions (project_slug NULL) are resolved via assignmentsDir.\n * Returns the number of sessions that were updated.\n */\nexport async function reconcileActiveSessions(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<number> {\n const db = getSessionDb();\n\n // Include standalone sessions (project_slug NULL) when assignmentsDir is provided.\n const activeSessions = db\n .prepare('SELECT * FROM sessions WHERE status = \\'active\\' AND assignment_slug IS NOT NULL')\n .all() as SessionRow[];\n\n if (activeSessions.length === 0) return 0;\n\n // Read assignment statuses from disk. Key is `${projectSlug ?? '__standalone__'}/${slug}`.\n const assignmentStatuses = new Map<string, string>();\n const seen = new Set<string>();\n for (const session of activeSessions) {\n const aslug = session.assignment_slug;\n if (!aslug) continue;\n\n const projectKey = session.project_slug ?? '__standalone__';\n const key = `${projectKey}/${aslug}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n if (session.project_slug) {\n const status = await readAssignmentStatus(\n resolve(projectsDir, session.project_slug),\n aslug,\n );\n if (status) assignmentStatuses.set(key, status);\n } else if (assignmentsDir) {\n const status = await readAssignmentStatusFromPath(\n resolve(assignmentsDir, aslug, 'assignment.md'),\n );\n if (status) assignmentStatuses.set(key, status);\n }\n }\n\n // Update stale sessions\n let totalUpdated = 0;\n for (const session of activeSessions) {\n const projectKey = session.project_slug ?? '__standalone__';\n const key = `${projectKey}/${session.assignment_slug}`;\n const assignmentStatus = assignmentStatuses.get(key);\n if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;\n\n const newStatus: AgentSessionStatus =\n assignmentStatus === 'failed' ? 'stopped' : 'completed';\n await updateSessionStatus('', session.session_id, newStatus);\n totalUpdated++;\n }\n\n return totalUpdated;\n}\n\n/**\n * List sessions for a resolved assignment (standalone or project-nested).\n * Standalone: filter by assignment_slug = id AND project_slug IS NULL.\n * Project-nested: filter by project_slug + assignment_slug.\n */\nexport async function listSessionsByAssignment(\n projectSlug: string | null,\n assignmentSlug: string,\n): Promise<AgentSession[]> {\n const db = getSessionDb();\n const rows = projectSlug === null\n ? (db\n .prepare(\n 'SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC',\n )\n .all(assignmentSlug) as SessionRow[])\n : (db\n .prepare(\n 'SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC',\n )\n .all(projectSlug, assignmentSlug) as SessionRow[]);\n return rows.map(rowToSession);\n}\n","/**\n * Locked Overview copy. Single source of truth for hero, segment, and dialog strings.\n *\n * Both the backend (`api.ts` hero + reason emission) and the frontend\n * (`OverviewHero`, `OverviewSegment`, etc.) import from this module so copy\n * cannot drift between the API payload and what the UI renders.\n */\n\nexport type HeroCopyKey =\n | 'review'\n | 'review.singular'\n | 'ready_to_implement'\n | 'ready_to_implement.singular'\n | 'ready_for_planning'\n | 'ready_for_planning.singular'\n | 'in_progress'\n | 'in_progress.singular'\n | 'draft'\n | 'draft.singular'\n | 'blocked'\n | 'blocked.singular'\n | 'stale'\n | 'stale.singular'\n | 'clean';\n\n/**\n * Hero strings. The `{total}` and `{title}` placeholders are substituted at\n * render time. Singular variants are used when `total === 1`.\n */\nexport const HERO_COPY: Record<HeroCopyKey, string> = {\n review: '{total} items ready for your review',\n 'review.singular': 'Review {title}',\n ready_to_implement: '{total} plans ready to implement — start with {title}',\n 'ready_to_implement.singular': 'Start implementing {title}',\n ready_for_planning: '{total} assignments ready to plan — start with {title}',\n 'ready_for_planning.singular': 'Plan {title}',\n in_progress: 'Resume {title} ({total} in progress)',\n 'in_progress.singular': 'Resume {title}',\n draft: 'Shape your {total} drafts — start with {title}',\n 'draft.singular': 'Shape {title}',\n blocked: 'Unblock {title} ({total} blocked)',\n 'blocked.singular': 'Unblock {title}',\n stale: 'Triage {total} stale items',\n 'stale.singular': 'Triage {title} — sitting stale',\n clean: 'You’re all clear. Nothing needs you right now.',\n};\n\nexport type SegmentId =\n | 'readyForReview'\n | 'readyToImplement'\n | 'readyForPlanning'\n | 'inProgress'\n | 'drafts'\n | 'blocked'\n | 'newestCreated'\n | 'stale';\n\n/** Per-segment row reason (the one-liner under the title). */\nexport const SEGMENT_REASON: Record<SegmentId, string> = {\n readyForReview: 'Ready for your review',\n readyToImplement: 'Plan finalized — ready to implement',\n readyForPlanning: 'Ready to plan',\n inProgress: 'In progress',\n drafts: 'Draft — needs shape',\n blocked: 'Blocked',\n newestCreated: 'Newly created',\n stale: 'Sitting stale',\n};\n\n/** Per-segment empty state copy. */\nexport const SEGMENT_EMPTY: Record<SegmentId, string> = {\n readyForReview: 'No assignments waiting for your review.',\n readyToImplement: 'No plans queued for implementation.',\n readyForPlanning: 'No assignments waiting to be planned.',\n inProgress: 'Nothing actively in progress.',\n drafts: 'No drafts — ideas captured here will live until they’re shaped.',\n blocked: 'Nothing is blocked. Good.',\n newestCreated: 'No assignments created recently.',\n stale: 'No stale work — everything is fresh.',\n};\n\n/** Per-segment header titles. */\nexport const SEGMENT_TITLE: Record<SegmentId, string> = {\n readyForReview: 'Ready for Review',\n readyToImplement: 'Ready to Implement',\n readyForPlanning: 'Ready for Planning',\n inProgress: 'In Progress',\n drafts: 'Drafts',\n blocked: 'Blocked',\n newestCreated: 'Newest Created',\n stale: 'Stale',\n};\n\n/** Dialog + button copy used across Overview components. */\nexport const DIALOG_COPY = {\n claimAsTitle: 'Claim assignments as',\n claimAsHint: 'Used when you claim an assignment from this dashboard. You can change it later in settings.',\n claimAsSubmit: 'Save',\n claimAsRemember: 'Remember this choice',\n quickCommentTitle: 'Add a quick note',\n quickCommentPlaceholder: 'Note…',\n quickCommentSubmit: 'Post',\n bulkArchiveLabel: 'Archive selected',\n bulkClearLabel: 'Clear',\n bulkPartialFailureBanner: 'Some items failed to archive. The list has been refreshed.',\n emptyStateCleanTitle: 'You’re all clear',\n emptyStateCleanCTA: 'Browse projects',\n draftsHeaderCTA: 'Shape →',\n staleLoadMore: 'Load more',\n staleLoadMoreRemaining: '{remaining} remaining',\n recentSessionsEmptyTitle: 'No recent sessions',\n recentSessionsEmptyHint: 'Use /grab-assignment or `syntaur track-session` to register one.',\n recentSessionsCopyPathLabel: 'Copy path',\n recentSessionsCopyPathDisabled: 'Session has no path',\n recentSessionsCopyFallbackHint: 'Press ⌘C to copy',\n} as const;\n\n/** Substitute `{key}` placeholders in a template. */\nexport function formatCopy(\n template: string,\n vars: Record<string, string | number>,\n): string {\n return template.replace(/\\{(\\w+)\\}/g, (_, key) => String(vars[key] ?? `{${key}}`));\n}\n","/**\n * Needs-attention / staleness classifier (read-only).\n *\n * The ONE place that decides whether an assignment's status has gone stale —\n * i.e. where the status CONTRADICTS reality — and why. Pure and side-effect\n * free: it never reads files, never writes status, never mutates anything. The\n * dashboard overview, the decision inbox, the CLI, and (later) a read-only\n * watchdog all feed it the same struct so the \"stale\" verdict is computed in\n * exactly one place and can't diverge between surfaces.\n *\n * Two hard rules (decision D1):\n * 1. Never timer-PROMOTE — this only flags, it never advances/regresses status.\n * 2. Contradiction + age, never raw age alone. An old timestamp is not\n * staleness; an old timestamp that disagrees with activity/claim/approval\n * is. And when an input is UNKNOWN we fail safe (do NOT flag) rather than\n * manufacture a false positive — e.g. \"no recent session\" is best-effort\n * and never fires on its own.\n */\n\nexport type StaleReasonKind =\n | 'in_progress_no_activity'\n | 'ready_unclaimed'\n | 'review_aging'\n | 'blocked_aging'\n | 'plan_awaiting_approval'\n | 'deps_unsatisfied';\n\nexport interface StaleReason {\n kind: StaleReasonKind;\n /** Human one-liner for display (dashboard/CLI). */\n label: string;\n /** Severity hint for ordering/badging. */\n severity: 'low' | 'medium' | 'high';\n}\n\n/**\n * Per-reason age gates (ms). Defaults are sensible; Task 5 lets config override\n * these keyed on disposition/phase. `deps_unsatisfied` has no age gate (a hard\n * contradiction), so it is intentionally absent here.\n */\nexport interface StaleThresholds {\n inProgressNoActivityMs: number;\n readyUnclaimedMs: number;\n reviewAgingMs: number;\n blockedAgingMs: number;\n planApprovalAgingMs: number;\n}\n\nconst DAY = 24 * 60 * 60 * 1000;\n\nexport const DEFAULT_STALE_THRESHOLDS: StaleThresholds = {\n inProgressNoActivityMs: 7 * DAY,\n readyUnclaimedMs: 3 * DAY,\n reviewAgingMs: 3 * DAY,\n blockedAgingMs: 3 * DAY,\n planApprovalAgingMs: 3 * DAY,\n};\n\n/**\n * Merge user overrides (from the `staleness:` config block) over the defaults.\n * Defaults-first: an absent or partial config keeps every unspecified gate at\n * its default. Non-positive/non-finite overrides are ignored (defensive — the\n * config parser already validates, but a stray value must never disable a gate).\n */\nexport function resolveStaleThresholds(\n overrides?: Partial<StaleThresholds> | null,\n): StaleThresholds {\n const merged = { ...DEFAULT_STALE_THRESHOLDS };\n if (overrides) {\n for (const key of Object.keys(merged) as (keyof StaleThresholds)[]) {\n const v = overrides[key];\n if (typeof v === 'number' && Number.isFinite(v) && v > 0) merged[key] = v;\n }\n }\n return merged;\n}\n\nexport interface NeedsAttentionInput {\n /** Derived phase (draft/ready_for_planning/ready_to_implement/in_progress/review). */\n phase: string | null;\n /** Derived disposition (active/blocked/parked/terminal). */\n disposition: string | null;\n /** Resolved terminal check — caller passes the config-resolved verdict, NOT a\n * hardcoded set, so renamed terminals are honored. */\n isTerminal: boolean;\n assignee: string | null;\n blockedReason: string | null;\n /** null when unknown/not-applicable (standalone, no deps). */\n depsSatisfied: boolean | null;\n planExists: boolean;\n planApproved: boolean;\n /** ms since the last HEADLINE status change. null → no aging reason fires. */\n statusAgeMs: number | null;\n /** ms since the most recent REAL activity (max-recency of progress.md mtime,\n * workspace files, session liveness). null → unknown → activity reasons never\n * fire (fail safe). NEVER assignment `updated` (recompute bumps that). */\n lastActivityMs: number | null;\n}\n\nconst PLANNING_PHASE = 'ready_for_planning';\nconst READY_PHASE = 'ready_to_implement';\nconst IN_PROGRESS_PHASE = 'in_progress';\nconst REVIEW_PHASE = 'review';\n\n/**\n * Classify why (if at all) an assignment needs attention. Returns [] when the\n * status is consistent with reality (or terminal, or inputs are unknown).\n */\nexport function classifyNeedsAttention(\n input: NeedsAttentionInput,\n thresholds: StaleThresholds = DEFAULT_STALE_THRESHOLDS,\n): StaleReason[] {\n if (input.isTerminal) return [];\n\n const reasons: StaleReason[] = [];\n const age = input.statusAgeMs; // null → aging gates below all fail closed\n const aged = (gate: number): boolean => age !== null && age >= gate;\n const blocked = input.disposition === 'blocked' || input.blockedReason !== null;\n\n // in_progress but nothing is actually happening. Requires BOTH an old status\n // AND a known-old activity signal — \"no recent session\" alone never fires.\n if (\n input.phase === IN_PROGRESS_PHASE &&\n !blocked &&\n aged(thresholds.inProgressNoActivityMs) &&\n input.lastActivityMs !== null &&\n input.lastActivityMs >= thresholds.inProgressNoActivityMs\n ) {\n reasons.push({\n kind: 'in_progress_no_activity',\n label: 'In progress, but no recent activity',\n severity: 'medium',\n });\n }\n\n // Ready to implement but nobody has claimed it.\n if (input.phase === READY_PHASE && input.assignee === null && aged(thresholds.readyUnclaimedMs)) {\n reasons.push({\n kind: 'ready_unclaimed',\n label: 'Ready to implement, unclaimed',\n severity: 'medium',\n });\n }\n\n // Waiting on a human review that no one has actioned.\n if (input.phase === REVIEW_PHASE && aged(thresholds.reviewAgingMs)) {\n reasons.push({ kind: 'review_aging', label: 'Awaiting review', severity: 'high' });\n }\n\n // Blocked and aging — the block may be stale.\n if (blocked && aged(thresholds.blockedAgingMs)) {\n reasons.push({ kind: 'blocked_aging', label: 'Blocked and aging', severity: 'high' });\n }\n\n // A plan exists but has sat unapproved.\n if (\n input.phase === PLANNING_PHASE &&\n input.planExists &&\n !input.planApproved &&\n aged(thresholds.planApprovalAgingMs)\n ) {\n reasons.push({\n kind: 'plan_awaiting_approval',\n label: 'Plan awaiting approval',\n severity: 'medium',\n });\n }\n\n // Working (or ready to work) despite unmet dependencies — a hard\n // contradiction, so no age gate. Not raised during planning (not yet\n // actionable) or when deps state is unknown (null).\n if (\n input.depsSatisfied === false &&\n (input.phase === READY_PHASE || input.phase === IN_PROGRESS_PHASE)\n ) {\n reasons.push({ kind: 'deps_unsatisfied', label: 'Unmet dependencies', severity: 'high' });\n }\n\n return reasons;\n}\n","import { readdir, readFile, writeFile, stat } from 'node:fs/promises';\nimport { resolve, dirname, basename } from 'node:path';\nimport { getTargetStatus, DEFAULT_TRANSITION_TABLE, buildTransitionTable } from '../lifecycle/index.js';\nimport { fileExists, writeFileForce } from '../utils/fs.js';\nimport { nowTimestamp } from '../utils/timestamp.js';\nimport {\n readConfig,\n buildDefaultStatusConfig,\n normalizeFactDeclarations,\n toTitleCase,\n type StatusTransition,\n type DeriveConfig,\n type FactDeclaration,\n type RawFactDeclaration,\n} from '../utils/config.js';\nimport { acceptFactDeclarations, buildDeriveRegistry, buildQueryRegistry } from '../lifecycle/derive.js';\nimport type { FieldRegistry } from '../utils/query/index.js';\nimport { resolvePlaybookSlug } from '../utils/playbooks.js';\nimport { migrateLegacyProjectFiles, migrateLegacyArchivedProjects } from '../utils/fs-migration.js';\nimport { resolveAssignmentById, type ResolvedAssignment } from '../utils/assignment-resolver.js';\nimport { latestPlanFile } from '../lifecycle/facts.js';\nimport { invalidateIndex } from '../search/index.js';\n\n/**\n * Thrown by `deleteWorkspace` when references exist and cascade is false.\n * Routers map this to a 409 response carrying the blocker payload.\n */\nexport class WorkspaceBlockedError extends Error {\n readonly blockedBy: { projects: string[]; standalones: string[] };\n constructor(blockedBy: { projects: string[]; standalones: string[] }) {\n super(\n `Workspace is referenced by ${blockedBy.projects.length} project(s) and ${blockedBy.standalones.length} standalone(s).`,\n );\n this.name = 'WorkspaceBlockedError';\n this.blockedBy = blockedBy;\n }\n}\n\n/**\n * Clear a single top-level frontmatter scalar field (regex-replace; assumes\n * the file already starts with `---` and the field exists). Used by the\n * cascade workspace delete to set `workspace:`/`workspaceGroup:` to `null`.\n */\nfunction clearFrontmatterField(content: string, key: string): string {\n const fieldRegex = new RegExp(`^(${escapeRegExp(key)}:)\\\\s*.*$`, 'm');\n return content.replace(fieldRegex, `$1 null`);\n}\n\nfunction setUpdatedField(content: string, value: string): string {\n const fieldRegex = /^(updated:)\\s*.*$/m;\n if (fieldRegex.test(content)) {\n return content.replace(fieldRegex, `$1 \"${value}\"`);\n }\n return content;\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\nimport {\n parseProject,\n parseStatus,\n parseAssignmentFull,\n parsePlan,\n parseScratchpad,\n parseHandoff,\n parseDecisionRecord,\n parseResource,\n parseMemory,\n parsePlaybook,\n parseProgress,\n parseComments,\n extractMermaidGraph,\n} from './parser.js';\nimport { getDashboardHelp } from './help.js';\nimport type {\n ArchiveResponse,\n ArchivedAssignmentItem,\n ArchivedProjectItem,\n AssignmentBoardItem,\n AssignmentDetail,\n AssignmentReference,\n AssignmentSummary,\n AssignmentsBoardResponse,\n AssignmentTransitionAction,\n AttentionItem,\n EditableDocumentResponse,\n EnrichedLink,\n HelpResponse,\n MemoryDetail,\n MemorySummary,\n MemorySummaryWithProject,\n ProjectDetail,\n ProjectSummary,\n OverviewResponse,\n OverviewSegmentId,\n OverviewSegments,\n OverviewHeroRecommendation,\n OverviewHeroKind,\n OverviewSegmentPayload,\n OverviewStaleSegmentPayload,\n ProgressCounts,\n NeedsAttention,\n RecentActivityItem,\n ResourceDetail,\n ResourceSummary,\n ResourceSummaryWithProject,\n PlaybookSummary,\n PlaybookDetail,\n} from './types.js';\nimport { listAllSessions } from './agent-sessions.js';\nimport { SEGMENT_REASON } from './overviewCopy.js';\nimport {\n classifyNeedsAttention,\n resolveStaleThresholds,\n type StaleReason,\n type StaleThresholds,\n} from '../staleness/classify.js';\nimport type { StaleCandidate } from '../staleness/watchdog.js';\n\nconst RECENT_PROJECTS_LIMIT = 6;\nconst RECENT_ACTIVITY_LIMIT = 12;\nconst RECENT_SESSIONS_LIMIT = 10;\nconst NEWEST_CREATED_LIMIT = 5;\nconst SEGMENT_DISPLAY_CAP = 5;\nconst STALE_LIMIT_DEFAULT = 50;\nconst STALE_LIMIT_MAX = 200;\n\n// --- Archive hiding helpers (cascade) ---\n// \"Hidden from normal views\" is enforced in the aggregating/consuming functions,\n// never in the parser or detail builders (those keep returning everything so the\n// Archive page + restore can read archived items).\n\n/** A project is hidden when its real `archived` flag is set. */\nfunction isProjectArchived(p: { archived?: boolean }): boolean {\n return p.archived === true;\n}\n\n/** Drop individually-archived assignments from a list (for normal/active views). */\nfunction activeAssignments<T extends { archived?: boolean }>(items: T[]): T[] {\n return items.filter((item) => item.archived !== true);\n}\n\n// ---------------------------------------------------------------------------\n// Overview perf instrumentation (opt-in via SYNTAUR_PERF_TRACE=1).\n// Used by getOverview() and helpers it calls. Inactive when traces is undefined.\n// ---------------------------------------------------------------------------\n\ninterface TraceEntry {\n label: string;\n ms: number;\n}\n\ninterface OverviewTraces {\n entries: TraceEntry[];\n subPhases: Map<string, number>;\n}\n\nfunction createTraces(): OverviewTraces {\n return { entries: [], subPhases: new Map() };\n}\n\nasync function timed<T>(\n traces: OverviewTraces | undefined,\n label: string,\n fn: () => Promise<T>,\n): Promise<T> {\n if (!traces) return fn();\n const start = performance.now();\n try {\n return await fn();\n } finally {\n traces.entries.push({ label, ms: performance.now() - start });\n }\n}\n\nfunction accumulatePhase(\n traces: OverviewTraces | undefined,\n label: string,\n ms: number,\n): void {\n if (!traces) return;\n traces.subPhases.set(label, (traces.subPhases.get(label) ?? 0) + ms);\n}\n\nfunction emitTrace(traces: OverviewTraces, meta: Record<string, unknown>): void {\n if (process.env.SYNTAUR_PERF_TRACE !== '1') return;\n const totalMs = traces.entries.reduce((sum, entry) => sum + entry.ms, 0);\n const subPhases = Object.fromEntries(traces.subPhases);\n // eslint-disable-next-line no-console\n console.log(\n JSON.stringify({ kind: 'overview-trace', totalMs, phases: traces.entries, subPhases, ...meta }),\n );\n}\n\nconst STATUS_TO_SEGMENT: Readonly<Record<string, OverviewSegmentId>> = {\n review: 'readyForReview',\n ready_to_implement: 'readyToImplement',\n ready_for_planning: 'readyForPlanning',\n in_progress: 'inProgress',\n draft: 'drafts',\n blocked: 'blocked',\n};\n\nconst HERO_PRIORITY: ReadonlyArray<[OverviewSegmentId, OverviewHeroKind]> = [\n ['readyForReview', 'review'],\n ['readyToImplement', 'ready_to_implement'],\n ['readyForPlanning', 'ready_for_planning'],\n ['inProgress', 'in_progress'],\n ['drafts', 'draft'],\n ['blocked', 'blocked'],\n ['stale', 'stale'],\n];\n\ntype AssignmentRecord = ReturnType<typeof parseAssignmentFull>;\n\ninterface ProjectRecord {\n projectPath: string;\n project: ReturnType<typeof parseProject>;\n assignments: AssignmentRecord[];\n summary: ProjectSummary;\n dependencyGraph: string | null;\n}\n\n/** A standalone assignment lives at `<assignmentsDir>/<uuid>/` and has no containing project. */\ninterface StandaloneRecord {\n assignmentDir: string;\n /** The UUID (folder name). */\n id: string;\n record: AssignmentRecord;\n}\n\n// ---------------------------------------------------------------------------\n// Shared records cache (coarse, clear-all).\n//\n// Parsed project records and standalone records are read on every hot read\n// path — /api/overview, /api/projects, /api/assignments, /api/workspaces, plus\n// the server scanner's workspace lookup. The underlying work is a file fan-out\n// (readdir + readFile + parse for every project, assignment, and comments\n// file), which dominates request latency and is badly amplified by corporate\n// EDR/AV that hooks filesystem syscalls. The dashboard server is long-lived, so\n// we cache the parsed snapshot per directory and reuse it across requests.\n//\n// Granularity is deliberately coarse: a whole-snapshot clear-all (not a\n// per-file map). Rebuild cost is one full scan, amortized across every read\n// until the next mutation. In-flight promises are stored (not just resolved\n// values) so concurrent callers de-duplicate onto a single scan, and a rejected\n// scan is dropped so the next call retries rather than caching a failure.\n//\n// Invalidation is the correctness core: there is no single fs choke-point (the\n// write routers mutate via writeFileForce, executeTransition, rm, and worktree\n// helpers), so every mutating router installs `installRecordsInvalidation`,\n// which clears the cache synchronously once each handler resolves. Non-router\n// mutators (deleteWorkspace) call invalidateRecordsCache() directly, and the\n// file watcher clears it for edits made outside the dashboard.\nconst projectRecordsCache = new Map<string, Promise<ProjectRecord[]>>();\nconst standaloneRecordsCache = new Map<string, Promise<StandaloneRecord[]>>();\n\n/** Drop all cached record snapshots. Cheap and idempotent. */\nexport function invalidateRecordsCache(): void {\n projectRecordsCache.clear();\n standaloneRecordsCache.clear();\n // Content-search index shares this invalidation seam: every record mutation\n // (write routers, file watcher, broadcast, deleteWorkspace) funnels here, so\n // clearing the search index alongside keeps `/api/search` consistent with the\n // displayed records. Cheap + idempotent; the next getIndex() rebuilds lazily.\n invalidateIndex();\n}\n\n/**\n * Install synchronous records-cache invalidation on a mutating Express router.\n * Wraps the terminal handler of every post/put/patch/delete route so the cache\n * is cleared in a `finally` once the handler resolves — before the next request\n * can read it. Centralizes invalidation at registration because the handlers\n * have no shared fs write path to hook. Typed structurally to avoid importing\n * express here.\n */\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype RouterMethod = (...args: any[]) => any;\ntype MutatingRouter = Record<'post' | 'put' | 'patch' | 'delete', RouterMethod>;\nexport function installRecordsInvalidation(router: MutatingRouter): void {\n for (const method of ['post', 'put', 'patch', 'delete'] as const) {\n const original = (router[method] as RouterMethod).bind(router);\n router[method] = (path: any, ...handlers: any[]): any => {\n if (handlers.length > 0) {\n const last = handlers[handlers.length - 1] as RouterMethod;\n handlers[handlers.length - 1] = async (req: any, res: any, next: any) => {\n try {\n return await last(req, res, next);\n } finally {\n invalidateRecordsCache();\n }\n };\n }\n return original(path, ...handlers);\n };\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nasync function listStandaloneRecords(assignmentsDir: string | undefined): Promise<StandaloneRecord[]> {\n const key = assignmentsDir ?? '';\n const cached = standaloneRecordsCache.get(key);\n if (cached) return cached;\n const promise = computeStandaloneRecords(assignmentsDir);\n standaloneRecordsCache.set(key, promise);\n promise.catch(() => standaloneRecordsCache.delete(key));\n return promise;\n}\n\nasync function computeStandaloneRecords(assignmentsDir: string | undefined): Promise<StandaloneRecord[]> {\n if (!assignmentsDir) return [];\n if (!(await fileExists(assignmentsDir))) return [];\n\n const entries = await readdir(assignmentsDir, { withFileTypes: true });\n const records: StandaloneRecord[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) continue;\n const assignmentDir = resolve(assignmentsDir, entry.name);\n const assignmentMdPath = resolve(assignmentDir, 'assignment.md');\n if (!(await fileExists(assignmentMdPath))) continue;\n try {\n const content = await readFile(assignmentMdPath, 'utf-8');\n const record = parseAssignmentFull(content);\n records.push({ assignmentDir, id: entry.name, record });\n } catch {\n // skip unreadable\n }\n }\n\n records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));\n return records;\n}\n\nconst DEFAULT_TRANSITION_DEFINITIONS: Array<{\n command: string;\n label: string;\n description: string;\n requiresReason: boolean;\n}> = [\n {\n command: 'start',\n label: 'Start',\n description: 'Move pending or review work into active execution.',\n requiresReason: false,\n },\n {\n command: 'shape',\n label: 'Shape',\n description: 'Promote a draft assignment to ready_for_planning once the Objective and Acceptance Criteria are fleshed out.',\n requiresReason: false,\n },\n {\n command: 'plan-ready',\n label: 'Plan Ready',\n description: 'Promote a ready_for_planning assignment to ready_to_implement after the plan is written and approved.',\n requiresReason: false,\n },\n {\n command: 'implement',\n label: 'Implement',\n description: 'Move a ready_to_implement assignment into in_progress when coding begins.',\n requiresReason: false,\n },\n {\n command: 'review',\n label: 'Send To Review',\n description: 'Mark the assignment ready for inspection.',\n requiresReason: false,\n },\n {\n command: 'complete',\n label: 'Complete',\n description: 'Mark the assignment done.',\n requiresReason: false,\n },\n {\n command: 'block',\n label: 'Block',\n description: 'Record an exceptional blocker and pause work.',\n requiresReason: true,\n },\n {\n command: 'unblock',\n label: 'Unblock',\n description: 'Resume active work after the blocker is cleared.',\n requiresReason: false,\n },\n {\n command: 'fail',\n label: 'Fail',\n description: 'Mark the assignment as failed when it cannot be completed as planned.',\n requiresReason: false,\n },\n {\n command: 'reopen',\n label: 'Reopen',\n description: 'Reopen a completed or failed assignment to resume work.',\n requiresReason: false,\n },\n];\n\nfunction getTransitionDefinitions(config: ResolvedStatusConfig) {\n if (!config.custom) return DEFAULT_TRANSITION_DEFINITIONS;\n // Deduplicate commands from transitions\n const seen = new Set<string>();\n return config.transitions\n .filter((t) => {\n if (seen.has(t.command)) return false;\n seen.add(t.command);\n return true;\n })\n .map((t) => ({\n command: t.command,\n label: t.label ?? toTitleCase(t.command),\n description: t.description ?? `Transition via ${t.command}.`,\n requiresReason: t.requiresReason ?? false,\n }));\n}\n\ninterface ResolvedStatusConfig {\n custom: boolean;\n statuses: Array<{ id: string; label: string; description?: string; color?: string; terminal?: boolean }>;\n order: string[];\n transitions: StatusTransition[];\n transitionTable: Map<string, string>;\n /** RAW transitions as configured (empty when the user declares none — the\n * Settings editor distinguishes \"user customized\" from \"showing defaults\"\n * via {@link transitionsCustom}, same pattern as {@link derive}). Distinct\n * from {@link transitions}, which is materialized with the default table for\n * the runtime transition guards so the board still offers commands. */\n rawTransitions: StatusTransition[];\n transitionsCustom: boolean;\n terminalStatuses: ReadonlySet<string>;\n /** Derive rules as configured (null when the user has none — resolve to\n * DEFAULT_DERIVE_CONFIG at the derivation call site, NOT here, so the\n * Settings writer can distinguish \"user customized\" from \"defaults\"). */\n derive: DeriveConfig | null;\n /** RAW custom-fact declarations (verbatim) — what the Settings writer passes\n * back to `writeStatusConfig` so a Settings save can't silently delete the\n * user's `statuses.facts` (same bug class as `derive`). */\n facts: RawFactDeclaration[] | null;\n /** ACCEPTED declarations (normalize→accept) — drives `customFacts` extraction\n * and the registry; collision-skipped/malformed rows are absent here. */\n factDeclarations: FactDeclaration[];\n /** Derive registry built ONCE per cached resolution from the accepted list —\n * reused across requests so the WeakMap compile-cache stays warm (no\n * per-request registry construction in buildDerivedDetail). */\n deriveRegistry: FieldRegistry;\n /** Query registry built ONCE per cached resolution from the accepted list —\n * sibling of deriveRegistry; stable object identity keeps the WeakMap\n * compile-cache warm across saved-view query validations. */\n queryRegistry: FieldRegistry;\n}\n\nlet _cachedConfig: ResolvedStatusConfig | null = null;\n\nexport async function getStatusConfig(): Promise<ResolvedStatusConfig> {\n if (_cachedConfig) return _cachedConfig;\n\n const config = await readConfig();\n\n if (config.statuses) {\n const sc = config.statuses;\n // A config may declare facts and/or derive rules without any custom status\n // `definitions` (parseStatusConfig now preserves those rather than dropping\n // the whole block). Fall back to the default statuses/order so the board\n // still renders, while the declared facts/derive ride along — same\n // no-silent-deletion contract as the parser.\n const defaults = sc.statuses.length === 0 ? buildDefaultStatusConfig() : null;\n const effectiveStatuses = defaults ? defaults.statuses : sc.statuses;\n const effectiveOrder = defaults ? defaults.order : sc.order;\n const terminalSet = new Set(\n effectiveStatuses.filter((s) => s.terminal).map((s) => s.id),\n );\n // If a user defines custom statuses but omits the `transitions:` block,\n // fall back to default transitions. Without this, buildTransitionTable([])\n // returns an empty Map and getTargetStatus returns null for every command,\n // so the dashboard would show zero available transitions for any assignment.\n // We materialize a FRESH table from DEFAULT_TRANSITION_TABLE entries (rather\n // than reusing the DEFAULT_TRANSITION_TABLE reference) so getTargetStatus\n // takes the custom-config code path and uses `from:command` lookups — this\n // is what enforces \"only emit transitions valid from current status\" for\n // users whose config has custom statuses but default transitions.\n const hasCustomTransitions = sc.transitions.length > 0;\n const effectiveTransitions = hasCustomTransitions\n ? sc.transitions\n : Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {\n const [from, command] = key.split(':');\n return { from, command, to };\n });\n const accepted = acceptFactDeclarations(normalizeFactDeclarations(sc.facts ?? null));\n _cachedConfig = {\n custom: true,\n statuses: effectiveStatuses,\n order: effectiveOrder,\n transitions: effectiveTransitions,\n transitionTable: buildTransitionTable(effectiveTransitions),\n rawTransitions: sc.transitions,\n transitionsCustom: hasCustomTransitions,\n terminalStatuses: terminalSet.size > 0 ? terminalSet : new Set(['completed', 'failed']),\n derive: sc.derive ?? null,\n facts: sc.facts ?? null,\n factDeclarations: accepted,\n deriveRegistry: buildDeriveRegistry(accepted),\n queryRegistry: buildQueryRegistry(accepted),\n };\n } else {\n // Shared default builder so the dashboard and the `syntaur status` CLI\n // resolve identical default statuses/order/transitions (no drift).\n const def = buildDefaultStatusConfig();\n _cachedConfig = {\n custom: false,\n statuses: def.statuses,\n order: def.order,\n transitions: def.transitions,\n transitionTable: DEFAULT_TRANSITION_TABLE,\n // No custom config at all → the Settings editor shows read-only defaults.\n rawTransitions: [],\n transitionsCustom: false,\n terminalStatuses: new Set(['completed', 'failed']),\n derive: null,\n facts: null,\n factDeclarations: [],\n deriveRegistry: buildDeriveRegistry([]),\n queryRegistry: buildQueryRegistry([]),\n };\n }\n\n return _cachedConfig;\n}\n\nexport function clearStatusConfigCache(): void {\n _cachedConfig = null;\n}\n\n/**\n * List all projects with source-first summary data.\n * GET /api/projects\n */\nexport async function listProjects(projectsDir: string): Promise<ProjectSummary[]> {\n const projectRecords = await listProjectRecords(projectsDir);\n // Archived projects are hidden from normal views; they live only on /archive.\n return projectRecords\n .filter((record) => !isProjectArchived(record.summary))\n .map((record) => record.summary);\n}\n\n/**\n * Read the workspace registry file (~/.syntaur/workspaces.json).\n * Returns an array of explicitly registered workspace names.\n */\nasync function readWorkspaceRegistry(projectsDir: string): Promise<string[]> {\n const registryPath = resolve(dirname(projectsDir), 'workspaces.json');\n try {\n const raw = await readFile(registryPath, 'utf-8');\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? parsed.filter((w): w is string => typeof w === 'string') : [];\n } catch {\n return [];\n }\n}\n\nasync function writeWorkspaceRegistry(projectsDir: string, workspaces: string[]): Promise<void> {\n const registryPath = resolve(dirname(projectsDir), 'workspaces.json');\n await writeFile(registryPath, JSON.stringify(workspaces, null, 2) + '\\n', 'utf-8');\n}\n\n/**\n * List all workspaces: merge registry (explicit) with workspaces discovered from\n * project `workspace:` fields and standalone-assignment `workspaceGroup` fields.\n * Standalones with no `workspaceGroup` contribute to `hasUngrouped`.\n * GET /api/workspaces\n */\nexport async function listWorkspaces(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<{ workspaces: string[]; hasUngrouped: boolean }> {\n const [projectRecords, registered, standaloneRecords] = await Promise.all([\n listProjectRecords(projectsDir),\n readWorkspaceRegistry(projectsDir),\n listStandaloneRecords(assignmentsDir),\n ]);\n const workspaceSet = new Set<string>(registered);\n let hasUngrouped = false;\n for (const record of projectRecords) {\n if (record.project.workspace) {\n workspaceSet.add(record.project.workspace);\n } else {\n hasUngrouped = true;\n }\n }\n for (const sr of standaloneRecords) {\n if (sr.record.workspaceGroup) {\n workspaceSet.add(sr.record.workspaceGroup);\n } else {\n hasUngrouped = true;\n }\n }\n const workspaces = Array.from(workspaceSet).sort();\n return { workspaces, hasUngrouped };\n}\n\n/**\n * Expand a workspace name to the usage rows it owns: member project slugs +\n * standalone assignment ids (folder UUIDs). `_ungrouped` selects projects with a\n * null `workspace` and standalones with no `workspaceGroup` (matching the\n * `/api/projects` and `/api/workspaces` semantics). Archived members are\n * excluded (`listProjects` already drops archived projects; standalones are\n * filtered here). The usage router turns the result into a WHERE clause that is\n * the disjoint union of project-scoped and standalone-scoped rows; unattributed\n * rows (`project_slug = '' AND assignment_slug = ''`) are never members.\n */\nexport async function resolveWorkspaceMembers(\n projectsDir: string,\n assignmentsDir: string | undefined,\n workspace: string,\n): Promise<{ projectSlugs: string[]; standaloneAssignmentIds: string[] }> {\n const [projects, standalones] = await Promise.all([\n listProjects(projectsDir), // archived projects already excluded\n listStandaloneRecords(assignmentsDir),\n ]);\n const ungrouped = workspace === '_ungrouped';\n const projectSlugs = projects\n .filter((p) => (ungrouped ? p.workspace === null : p.workspace === workspace))\n .map((p) => p.slug);\n const standaloneAssignmentIds = standalones\n .filter((sr) => sr.record.archived !== true)\n .filter((sr) => (ungrouped ? !sr.record.workspaceGroup : sr.record.workspaceGroup === workspace))\n .map((sr) => sr.id);\n return { projectSlugs, standaloneAssignmentIds };\n}\n\n/**\n * Worktree/branch records for the server scanner's tmux pane auto-linking,\n * derived from the cached records snapshot instead of a second file fan-out\n * (the scanner previously re-read every assignment.md on each cold scan). A\n * `null` projectSlug marks a standalone assignment. By convention a project\n * assignment's folder name equals its slug, and standalone folders are named by\n * UUID, so `assignmentSlug` matches the scanner's prior folder-name behavior.\n */\nexport async function listWorkspaceRecords(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<\n Array<{\n projectSlug: string | null;\n assignmentSlug: string;\n assignmentTitle: string;\n worktreePath: string | null;\n branch: string | null;\n }>\n> {\n const [projectRecords, standaloneRecords] = await Promise.all([\n listProjectRecords(projectsDir),\n listStandaloneRecords(assignmentsDir),\n ]);\n\n const records: Array<{\n projectSlug: string | null;\n assignmentSlug: string;\n assignmentTitle: string;\n worktreePath: string | null;\n branch: string | null;\n }> = [];\n\n for (const project of projectRecords) {\n for (const assignment of project.assignments) {\n records.push({\n projectSlug: project.summary.slug,\n assignmentSlug: assignment.slug,\n assignmentTitle: assignment.title || assignment.slug,\n worktreePath: assignment.workspace.worktreePath ?? null,\n branch: assignment.workspace.branch ?? null,\n });\n }\n }\n\n for (const standalone of standaloneRecords) {\n records.push({\n projectSlug: null,\n assignmentSlug: standalone.id,\n assignmentTitle: standalone.record.title || standalone.id,\n worktreePath: standalone.record.workspace.worktreePath ?? null,\n branch: standalone.record.workspace.branch ?? null,\n });\n }\n\n return records;\n}\n\n/**\n * Create an empty workspace by registering it.\n * POST /api/workspaces\n */\nexport async function createWorkspace(projectsDir: string, name: string): Promise<void> {\n const registered = await readWorkspaceRegistry(projectsDir);\n if (!registered.includes(name)) {\n registered.push(name);\n registered.sort();\n await writeWorkspaceRegistry(projectsDir, registered);\n }\n}\n\n/**\n * Delete a workspace from the registry.\n *\n * Modes:\n * - `cascade: false` (default): if any project or standalone still references\n * this workspace, throw `WorkspaceBlockedError` with the blocker lists.\n * Otherwise remove from the registry.\n * - `cascade: true`: rewrite every referencing project's `workspace:` field\n * and every referencing standalone's `workspaceGroup:` field to `null`,\n * then remove the registry entry.\n *\n * Returns `{ rewroteFiles }` so callers (server.ts) can decide whether the\n * explicit registry-level broadcast is still needed (watchers already emit\n * project-updated/assignment-updated for rewritten files).\n *\n * DELETE /api/workspaces/:name[?cascade=true]\n */\nexport async function deleteWorkspace(\n projectsDir: string,\n name: string,\n opts: { cascade?: boolean; assignmentsDir?: string } = {},\n): Promise<{ rewroteFiles: boolean }> {\n const cascade = Boolean(opts.cascade);\n const projectRecords = await listProjectRecords(projectsDir);\n const standaloneRecords = await listStandaloneRecords(opts.assignmentsDir);\n\n const projectsReferencing = projectRecords\n .filter((record) => record.project.workspace === name)\n .map((record) => record.project.slug);\n const standalonesReferencing = standaloneRecords\n .filter((record) => record.record.workspaceGroup === name)\n .map((record) => record.id);\n\n if (projectsReferencing.length + standalonesReferencing.length > 0 && !cascade) {\n throw new WorkspaceBlockedError({\n projects: projectsReferencing,\n standalones: standalonesReferencing,\n });\n }\n\n let rewroteFiles = false;\n if (cascade) {\n const timestamp = nowTimestamp();\n\n for (const slug of projectsReferencing) {\n const path = resolve(projectsDir, slug, 'project.md');\n const raw = await readFile(path, 'utf-8');\n let next = clearFrontmatterField(raw, 'workspace');\n next = setUpdatedField(next, timestamp);\n await writeFileForce(path, next);\n rewroteFiles = true;\n }\n\n for (const id of standalonesReferencing) {\n if (!opts.assignmentsDir) break;\n const path = resolve(opts.assignmentsDir, id, 'assignment.md');\n const raw = await readFile(path, 'utf-8');\n let next = clearFrontmatterField(raw, 'workspaceGroup');\n next = setUpdatedField(next, timestamp);\n await writeFileForce(path, next);\n rewroteFiles = true;\n }\n }\n\n const registered = await readWorkspaceRegistry(projectsDir);\n const filtered = registered.filter((w) => w !== name);\n await writeWorkspaceRegistry(projectsDir, filtered);\n\n // Cascade rewrote project/assignment frontmatter (workspace fields), so the\n // cached records snapshot is stale. This is a library function invoked\n // directly by a server.ts route (outside the write router), so invalidate\n // here rather than relying on a router wrapper.\n if (rewroteFiles) {\n invalidateRecordsCache();\n }\n\n return { rewroteFiles };\n}\n\n/**\n * Get overview data used by the app landing page.\n * GET /api/overview?staleLimit=&staleOffset=\n */\nexport async function getOverview(\n projectsDir: string,\n serversDir?: string,\n assignmentsDir?: string,\n options: { staleLimit?: number; staleOffset?: number } = {},\n): Promise<OverviewResponse> {\n const traceEnabled = process.env.SYNTAUR_PERF_TRACE === '1';\n const traces: OverviewTraces | undefined = traceEnabled ? createTraces() : undefined;\n const overallStart = traceEnabled ? performance.now() : 0;\n\n const projectRecords = await timed(traces, 'list-project-records', () =>\n listProjectRecords(projectsDir, traces),\n );\n const standaloneRecords = await timed(traces, 'list-standalone-records', () =>\n listStandaloneRecords(assignmentsDir),\n );\n // Archived projects + individually-archived assignments are hidden from every\n // overview aggregate (stats, recent projects, recent activity). The full record\n // sets are still used for firstRun detection and the segment-bucket builder\n // (which applies its own cascade filtering internally).\n const activeProjectRecords = projectRecords.filter((record) => !isProjectArchived(record.summary));\n const activeStandaloneRecords = standaloneRecords.filter((sr) => sr.record.archived !== true);\n const recentActivity = buildRecentActivity(activeProjectRecords, activeStandaloneRecords);\n\n const staleLimit = clamp(\n Number.isFinite(options.staleLimit) ? Number(options.staleLimit) : STALE_LIMIT_DEFAULT,\n 1,\n STALE_LIMIT_MAX,\n );\n const staleOffset = Math.max(0, Number.isFinite(options.staleOffset) ? Number(options.staleOffset) : 0);\n\n const buckets = await timed(traces, 'build-segment-buckets', () =>\n buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords, traces),\n );\n const segments = toOverviewSegments(buckets, { staleLimit, staleOffset });\n const hero = pickOverviewHero(buckets);\n\n let recentSessions: OverviewResponse['recentSessions'] = [];\n try {\n const all = await timed(traces, 'list-recent-sessions', () => listAllSessions(projectsDir));\n recentSessions = all.slice(0, RECENT_SESSIONS_LIMIT);\n } catch {\n // Sessions failure should not break overview.\n }\n\n let serverStats: OverviewResponse['serverStats'];\n if (serversDir) {\n try {\n const { scanAllSessions } = await import('./scanner.js');\n const servers = await timed(traces, 'scan-tmux-sessions', () =>\n // Overview only needs aggregate counts — never block its render on a\n // live scan; serve last-known stats and let the scan refresh in the\n // background (stale-while-revalidate).\n scanAllSessions(serversDir, projectsDir, { assignmentsDir, nonBlocking: true }),\n );\n if (servers.tmuxAvailable) {\n const alive = servers.sessions.filter(s => s.alive).length;\n const totalPorts = servers.sessions.reduce((sum, s) =>\n sum + s.windows.reduce((ws, w) =>\n ws + w.panes.reduce((ps, p) => ps + p.ports.length, 0), 0), 0);\n serverStats = {\n trackedSessions: servers.sessions.length,\n aliveSessions: alive,\n deadSessions: servers.sessions.length - alive,\n totalPorts,\n };\n }\n } catch {\n // Server scanning failure should not break overview\n }\n }\n\n if (traces) {\n const wallMs = performance.now() - overallStart;\n const totalAssignments =\n projectRecords.reduce((sum, r) => sum + r.assignments.length, 0) + standaloneRecords.length;\n emitTrace(traces, {\n wallMs,\n fixture: { projects: projectRecords.length, assignments: totalAssignments },\n });\n }\n\n return {\n generatedAt: new Date().toISOString(),\n firstRun: projectRecords.length === 0 && standaloneRecords.length === 0,\n stats: {\n activeProjects: activeProjectRecords.filter((record) => record.summary.status === 'active').length,\n inProgressAssignments: activeProjectRecords.reduce(\n (total, record) => total + (record.summary.progress['in_progress'] ?? 0),\n 0,\n ),\n blockedAssignments: activeProjectRecords.reduce(\n (total, record) => total + (record.summary.progress['blocked'] ?? 0),\n 0,\n ),\n reviewAssignments: activeProjectRecords.reduce(\n (total, record) => total + (record.summary.progress['review'] ?? 0),\n 0,\n ),\n failedAssignments: activeProjectRecords.reduce(\n (total, record) => total + (record.summary.progress['failed'] ?? 0),\n 0,\n ),\n // Derived from the SAME classifier verdict as the stale segment (via the\n // pre-cap segment total) so the badge count can never diverge from the\n // listed rows.\n staleAssignments: segments.stale.total,\n },\n hero,\n segments,\n recentSessions,\n recentProjects: activeProjectRecords\n .map((record) => record.summary)\n .sort((left, right) => compareTimestamps(right.updated, left.updated))\n .slice(0, RECENT_PROJECTS_LIMIT),\n recentActivity: recentActivity.slice(0, RECENT_ACTIVITY_LIMIT),\n serverStats,\n };\n}\n\n/**\n * Get all assignments across all projects for the global kanban board.\n * GET /api/assignments\n */\nexport async function listAssignmentsBoard(\n projectsDir: string,\n assignmentsDir?: string,\n options: { archived?: 'exclude' | 'only' } = {},\n): Promise<AssignmentsBoardResponse> {\n const mode = options.archived ?? 'exclude';\n const projectRecords = await listProjectRecords(projectsDir);\n const projectItems = await Promise.all(\n projectRecords.flatMap(async (record) => {\n if (mode === 'only') {\n // Individually-archived assignments only — ignore project-archived cascade.\n return Promise.all(\n record.assignments\n .filter((assignment) => assignment.archived === true)\n .map(async (assignment) => toAssignmentBoardItem(projectsDir, record, assignment)),\n );\n }\n // 'exclude': cascade-hide every child of an archived project, and drop\n // individually-archived children of non-archived projects.\n if (isProjectArchived(record.summary)) return [] as AssignmentBoardItem[];\n return Promise.all(\n activeAssignments(record.assignments).map(async (assignment) =>\n toAssignmentBoardItem(projectsDir, record, assignment),\n ),\n );\n }),\n );\n\n const standaloneRecords = await listStandaloneRecords(assignmentsDir);\n const filteredStandalone =\n mode === 'only'\n ? standaloneRecords.filter((sr) => sr.record.archived === true)\n : standaloneRecords.filter((sr) => sr.record.archived !== true);\n const standaloneItems = await Promise.all(\n filteredStandalone.map(async (sr) => toStandaloneBoardItem(sr)),\n );\n\n return {\n generatedAt: new Date().toISOString(),\n assignments: [...projectItems.flat(), ...standaloneItems]\n .sort((left, right) => compareTimestamps(right.updated, left.updated)),\n };\n}\n\nfunction toArchivedAssignmentItem(\n assignment: AssignmentRecord,\n projectSlug: string | null,\n projectTitle: string | null,\n): ArchivedAssignmentItem {\n return {\n id: assignment.id,\n slug: assignment.slug,\n title: assignment.title,\n status: assignment.status,\n type: assignment.type,\n priority: assignment.priority as ArchivedAssignmentItem['priority'],\n projectSlug,\n projectTitle,\n archived: assignment.archived,\n archivedAt: assignment.archivedAt,\n archivedReason: assignment.archivedReason,\n updated: assignment.updated,\n };\n}\n\n/**\n * Build the canonical archived view for the dashboard Archive page.\n * Returns archived projects (each expandable to ALL its children) plus\n * individually-archived assignments whose parent project is NOT archived\n * (so they are never double-listed) and archived standalone assignments.\n * GET /api/archived\n */\nexport async function listArchived(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<ArchiveResponse> {\n const projectRecords = await listProjectRecords(projectsDir);\n const standaloneRecords = await listStandaloneRecords(assignmentsDir);\n\n const projects: ArchivedProjectItem[] = projectRecords\n .filter((record) => isProjectArchived(record.summary))\n .map((record) => ({\n slug: record.summary.slug,\n title: record.summary.title,\n archivedAt: record.summary.archivedAt,\n archivedReason: record.summary.archivedReason,\n assignments: record.assignments\n .map((assignment) =>\n toArchivedAssignmentItem(assignment, record.summary.slug, record.summary.title),\n )\n .sort((left, right) => compareTimestamps(right.updated, left.updated)),\n }))\n .sort((left, right) => compareTimestamps(right.archivedAt ?? '', left.archivedAt ?? ''));\n\n const individuallyArchived: ArchivedAssignmentItem[] = [];\n for (const record of projectRecords) {\n if (isProjectArchived(record.summary)) continue; // its children belong under the project above\n for (const assignment of record.assignments) {\n if (assignment.archived === true) {\n individuallyArchived.push(\n toArchivedAssignmentItem(assignment, record.summary.slug, record.summary.title),\n );\n }\n }\n }\n for (const sr of standaloneRecords) {\n if (sr.record.archived === true) {\n individuallyArchived.push(toArchivedAssignmentItem(sr.record, null, null));\n }\n }\n individuallyArchived.sort((left, right) => compareTimestamps(right.updated, left.updated));\n\n return { projects, assignments: individuallyArchived };\n}\n\nasync function toStandaloneBoardItem(sr: StandaloneRecord): Promise<AssignmentBoardItem> {\n const config = await getStatusConfig();\n const { terminalStatuses } = config;\n\n let facts: AssignmentBoardItem['facts'];\n try {\n const { computeFacts } = await import('../lifecycle/facts.js');\n facts = await computeFacts({\n assignmentDir: sr.assignmentDir,\n frontmatter: sr.record as unknown as import('../lifecycle/types.js').AssignmentFrontmatter,\n body: sr.record.body,\n projectDir: null,\n terminalStatuses,\n declarations: config.factDeclarations,\n });\n } catch (err) {\n console.warn(`toStandaloneBoardItem: computeFacts failed for ${sr.assignmentDir}:`, err);\n }\n\n return {\n ...toAssignmentSummary(sr.record, terminalStatuses),\n projectSlug: null,\n projectTitle: null,\n blockedReason: sr.record.blockedReason,\n projectWorkspace: sr.record.workspaceGroup ?? null,\n availableTransitions: await getStandaloneAvailableTransitions(sr.record),\n facts,\n };\n}\n\nasync function getStandaloneAvailableTransitions(\n assignment: AssignmentRecord,\n): Promise<AssignmentTransitionAction[]> {\n // Standalone assignments have no dependencies, so skip dependency gating.\n const config = await getStatusConfig();\n const transitionDefs = getTransitionDefinitions(config);\n const actions: AssignmentTransitionAction[] = [];\n\n for (const definition of transitionDefs) {\n const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);\n // Only valid transitions reach the client; the kanban inline picker renders them directly.\n if (target === null) continue;\n\n let warning: string | null = null;\n if (definition.command === 'start' && !assignment.assignee) {\n warning = 'No assignee set — consider assigning before starting.';\n }\n actions.push({\n command: definition.command,\n label: definition.label,\n description: definition.description,\n targetStatus: target,\n disabled: false,\n disabledReason: null,\n warning,\n requiresReason: definition.requiresReason,\n });\n }\n\n return actions;\n}\n\n/**\n * Get the structured help model used by Help and onboarding surfaces.\n * GET /api/help\n */\nexport async function getHelp(): Promise<HelpResponse> {\n return getDashboardHelp();\n}\n\n/**\n * Get a raw editable document for dashboard editor pages.\n */\nexport async function getEditableDocument(\n projectsDir: string,\n documentType: EditableDocumentResponse['documentType'],\n projectSlug: string,\n assignmentSlug?: string,\n): Promise<EditableDocumentResponse | null> {\n const filePath = getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug);\n if (!filePath || !(await fileExists(filePath))) {\n return null;\n }\n\n const content = await readFile(filePath, 'utf-8');\n const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);\n\n return {\n documentType,\n title,\n content,\n projectSlug,\n assignmentSlug,\n appendOnly: documentType === 'handoff' || documentType === 'decision-record',\n };\n}\n\n/**\n * Resolve an assignment by UUID (standalone or project-nested) and return its\n * editable document payload for the given type.\n */\nexport async function getEditableDocumentById(\n projectsDir: string,\n assignmentsDir: string,\n documentType: EditableDocumentResponse['documentType'],\n id: string,\n): Promise<EditableDocumentResponse | null> {\n const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);\n if (!resolved) return null;\n\n if (!resolved.standalone && resolved.projectSlug) {\n return getEditableDocument(\n projectsDir,\n documentType,\n resolved.projectSlug,\n resolved.assignmentSlug,\n );\n }\n\n const fileName =\n documentType === 'assignment'\n ? 'assignment.md'\n : documentType === 'plan'\n ? 'plan.md'\n : documentType === 'scratchpad'\n ? 'scratchpad.md'\n : documentType === 'handoff'\n ? 'handoff.md'\n : documentType === 'decision-record'\n ? 'decision-record.md'\n : null;\n if (!fileName) return null;\n const filePath = resolve(resolved.assignmentDir, fileName);\n if (!(await fileExists(filePath))) return null;\n\n const content = await readFile(filePath, 'utf-8');\n const label = resolved.id;\n const title =\n documentType === 'assignment'\n ? `Edit Assignment: ${label}`\n : documentType === 'plan'\n ? `Edit Plan: ${label}`\n : documentType === 'scratchpad'\n ? `Edit Scratchpad: ${label}`\n : documentType === 'handoff'\n ? `Append Handoff: ${label}`\n : `Append Decision: ${label}`;\n\n return {\n documentType,\n title,\n content,\n projectSlug: null,\n assignmentSlug: undefined,\n assignmentId: resolved.id,\n appendOnly: documentType === 'handoff' || documentType === 'decision-record',\n };\n}\n\n/**\n * Get full project detail with assignments, resources, and memories.\n * GET /api/projects/:slug\n */\nexport async function getProjectDetail(\n projectsDir: string,\n slug: string,\n): Promise<ProjectDetail | null> {\n const projectPath = resolve(projectsDir, slug);\n const projectMdPath = resolve(projectPath, 'project.md');\n\n if (!(await fileExists(projectMdPath))) {\n return null;\n }\n\n const projectContent = await readFile(projectMdPath, 'utf-8');\n const project = parseProject(projectContent);\n const assignments = await listAssignmentRecords(projectPath);\n const rollup = await buildProjectRollup(projectPath, project, assignments);\n const dependencyGraph = await loadDependencyGraph(projectPath, assignments);\n const resources = await listResources(projectPath);\n const memories = await listMemories(projectPath);\n // Consistent with the project summary: the activity timestamp ignores archived\n // children so archiving an old assignment doesn't bump it.\n const updated = getProjectActivityTimestamp(project.updated, activeAssignments(assignments));\n const { terminalStatuses } = await getStatusConfig();\n\n return {\n slug: project.slug || slug,\n title: project.title,\n status: rollup.status,\n statusOverride: project.statusOverride,\n archived: project.archived,\n archivedAt: project.archivedAt,\n archivedReason: project.archivedReason,\n created: project.created,\n updated,\n tags: project.tags,\n externalIds: project.externalIds,\n body: project.body,\n progress: rollup.progress,\n needsAttention: rollup.needsAttention,\n assignments: assignments\n .map((a) => toAssignmentSummary(a, terminalStatuses))\n .sort((left, right) => compareTimestamps(right.updated, left.updated)),\n resources,\n memories,\n dependencyGraph,\n workspace: project.workspace,\n repositories: project.repositories,\n };\n}\n\n/**\n * Get full assignment detail with plan, scratchpad, handoff, and decision record.\n * GET /api/projects/:slug/assignments/:aslug\n */\nexport async function getAssignmentDetail(\n projectsDir: string,\n projectSlug: string,\n assignmentSlug: string,\n): Promise<AssignmentDetail | null> {\n const assignmentDir = resolve(projectsDir, projectSlug, 'assignments', assignmentSlug);\n const assignmentMdPath = resolve(assignmentDir, 'assignment.md');\n\n if (!(await fileExists(assignmentMdPath))) {\n return null;\n }\n\n const assignmentContent = await readFile(assignmentMdPath, 'utf-8');\n const assignment = parseAssignmentFull(assignmentContent);\n\n let projectWorkspace: string | null = null;\n const projectMdPath = resolve(projectsDir, projectSlug, 'project.md');\n if (await fileExists(projectMdPath)) {\n const projectContent = await readFile(projectMdPath, 'utf-8');\n projectWorkspace = parseProject(projectContent).workspace;\n }\n\n let plan: AssignmentDetail['plan'] = null;\n const planFile = await latestPlanFile(assignmentDir);\n if (planFile) {\n const planPath = resolve(assignmentDir, planFile);\n if (await fileExists(planPath)) {\n const planContent = await readFile(planPath, 'utf-8');\n const parsed = parsePlan(planContent);\n plan = {\n status: parsed.status,\n updated: parsed.updated,\n body: parsed.body,\n };\n }\n }\n\n let scratchpad: AssignmentDetail['scratchpad'] = null;\n const scratchpadPath = resolve(assignmentDir, 'scratchpad.md');\n if (await fileExists(scratchpadPath)) {\n const scratchpadContent = await readFile(scratchpadPath, 'utf-8');\n const parsed = parseScratchpad(scratchpadContent);\n scratchpad = {\n updated: parsed.updated,\n body: parsed.body,\n };\n }\n\n let handoff: AssignmentDetail['handoff'] = null;\n const handoffPath = resolve(assignmentDir, 'handoff.md');\n if (await fileExists(handoffPath)) {\n const handoffContent = await readFile(handoffPath, 'utf-8');\n const parsed = parseHandoff(handoffContent);\n handoff = {\n updated: parsed.updated,\n handoffCount: parsed.handoffCount,\n body: parsed.body,\n };\n }\n\n let decisionRecord: AssignmentDetail['decisionRecord'] = null;\n const decisionRecordPath = resolve(assignmentDir, 'decision-record.md');\n if (await fileExists(decisionRecordPath)) {\n const decisionRecordContent = await readFile(decisionRecordPath, 'utf-8');\n const parsed = parseDecisionRecord(decisionRecordContent);\n decisionRecord = {\n updated: parsed.updated,\n decisionCount: parsed.decisionCount,\n body: parsed.body,\n };\n }\n\n let progress: AssignmentDetail['progress'] = null;\n const progressPath = resolve(assignmentDir, 'progress.md');\n if (await fileExists(progressPath)) {\n const progressContent = await readFile(progressPath, 'utf-8');\n const parsed = parseProgress(progressContent);\n progress = {\n updated: parsed.updated,\n entryCount: parsed.entryCount,\n entries: parsed.entries,\n };\n }\n\n let comments: AssignmentDetail['comments'] = null;\n const commentsPath = resolve(assignmentDir, 'comments.md');\n if (await fileExists(commentsPath)) {\n const commentsContent = await readFile(commentsPath, 'utf-8');\n const parsed = parseComments(commentsContent);\n comments = {\n updated: parsed.updated,\n entryCount: parsed.entryCount,\n entries: parsed.entries,\n };\n }\n\n const { terminalStatuses } = await getStatusConfig();\n const detail: AssignmentDetail = {\n id: assignment.id,\n projectSlug,\n slug: assignment.slug || assignmentSlug,\n title: assignment.title,\n status: assignment.status,\n type: assignment.type,\n priority: assignment.priority as AssignmentDetail['priority'],\n assignee: assignment.assignee,\n dependsOn: assignment.dependsOn,\n links: assignment.links,\n reverseLinks: [],\n enrichedLinks: [],\n blockedReason: assignment.blockedReason,\n workspace: assignment.workspace,\n projectWorkspace,\n externalIds: assignment.externalIds,\n tags: assignment.tags,\n archived: assignment.archived,\n archivedAt: assignment.archivedAt,\n archivedReason: assignment.archivedReason,\n ...deriveStatusVirtuals(assignment, terminalStatuses),\n override: assignment.override,\n derived: await buildDerivedDetail(assignment, assignmentDir, resolve(projectsDir, projectSlug)),\n created: assignment.created,\n updated: assignment.updated,\n body: assignment.body,\n plan,\n scratchpad,\n handoff,\n decisionRecord,\n progress,\n comments,\n referencedBy: [],\n availableTransitions: await getAvailableTransitions(\n projectsDir,\n projectSlug,\n assignmentSlug,\n assignment,\n ),\n };\n\n // Compute reverse links and enrich all links\n const selfSlug = `${projectSlug}/${detail.slug}`;\n const projectRecords = await listProjectRecords(projectsDir);\n\n // Find reverse links: assignments across all projects whose links contain this assignment\n const reverseLinks: string[] = [];\n for (const mr of projectRecords) {\n for (const a of mr.assignments) {\n const qualifiedSlug = `${mr.summary.slug}/${a.slug}`;\n if (qualifiedSlug === selfSlug) continue; // skip self\n if (a.links.includes(selfSlug)) {\n reverseLinks.push(qualifiedSlug);\n }\n }\n }\n\n // Filter self-links and malformed links from forward links\n const isValidLinkFormat = (l: string) => {\n const parts = l.split('/');\n return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;\n };\n const forwardLinks = assignment.links.filter((l) => l !== selfSlug && isValidLinkFormat(l));\n\n // Deduplicate: if a slug is in both forward and reverse, keep in forward only\n const forwardSet = new Set(forwardLinks);\n const dedupedReverseLinks = reverseLinks.filter((l) => !forwardSet.has(l));\n\n detail.links = forwardLinks;\n detail.reverseLinks = dedupedReverseLinks;\n\n // Build enriched links for the frontend\n const allProjectAssignments = new Map<string, { title: string; status: string }>();\n for (const mr of projectRecords) {\n for (const a of mr.assignments) {\n allProjectAssignments.set(`${mr.summary.slug}/${a.slug}`, {\n title: a.title,\n status: a.status,\n });\n }\n }\n\n const enrichedLinks: EnrichedLink[] = [];\n for (const linkSlug of forwardLinks) {\n const [ms, as] = linkSlug.split('/');\n const info = allProjectAssignments.get(linkSlug);\n enrichedLinks.push({\n slug: linkSlug,\n projectSlug: ms,\n assignmentSlug: as,\n title: info?.title ?? linkSlug,\n status: info?.status ?? 'pending',\n isReverse: false,\n });\n }\n for (const linkSlug of dedupedReverseLinks) {\n const [ms, as] = linkSlug.split('/');\n const info = allProjectAssignments.get(linkSlug);\n enrichedLinks.push({\n slug: linkSlug,\n projectSlug: ms,\n assignmentSlug: as,\n title: info?.title ?? linkSlug,\n status: info?.status ?? 'pending',\n isReverse: true,\n });\n }\n\n detail.enrichedLinks = enrichedLinks;\n\n // Populate referencedBy — assignments that mention this one.\n detail.referencedBy = await computeReferencedBy(\n { id: assignment.id, projectSlug, slug: detail.slug },\n projectsDir,\n undefined,\n );\n\n return detail;\n}\n\nconst REFERENCED_BY_LIMIT = 50;\n\ninterface ReferenceTarget {\n id: string;\n projectSlug: string | null;\n slug: string;\n}\n\n/**\n * Scan every *other* assignment's Todos, progress, comments, and handoff bodies\n * for markdown links that resolve to `target`, and return an aggregated per-source\n * count (capped at 50).\n */\nasync function computeReferencedBy(\n target: ReferenceTarget,\n projectsDir: string,\n assignmentsDir: string | undefined,\n): Promise<AssignmentReference[]> {\n const sources: Array<{\n id: string;\n slug: string;\n title: string;\n projectSlug: string | null;\n assignmentDir: string;\n }> = [];\n\n // project-nested\n const projectRecords = await listProjectRecords(projectsDir);\n for (const rec of projectRecords) {\n for (const a of rec.assignments) {\n sources.push({\n id: a.id,\n slug: a.slug,\n title: a.title,\n projectSlug: rec.summary.slug,\n assignmentDir: resolve(rec.projectPath, 'assignments', a.slug),\n });\n }\n }\n // standalone\n const standaloneRecords = await listStandaloneRecords(assignmentsDir);\n for (const sr of standaloneRecords) {\n sources.push({\n id: sr.id,\n slug: sr.record.slug || sr.id,\n title: sr.record.title,\n projectSlug: null,\n assignmentDir: sr.assignmentDir,\n });\n }\n\n const references: AssignmentReference[] = [];\n for (const source of sources) {\n if (source.id === target.id) continue; // skip self\n const mentions = await countMentionsInAssignment(source.assignmentDir, target);\n if (mentions > 0) {\n references.push({\n sourceId: source.id,\n sourceSlug: source.slug,\n sourceTitle: source.title,\n sourceProjectSlug: source.projectSlug,\n mentions,\n });\n }\n if (references.length >= REFERENCED_BY_LIMIT) break;\n }\n\n return references.slice(0, REFERENCED_BY_LIMIT);\n}\n\nasync function countMentionsInAssignment(\n sourceDir: string,\n target: ReferenceTarget,\n): Promise<number> {\n const bodies: string[] = [];\n\n // Todos section (from assignment.md)\n const assignmentMd = resolve(sourceDir, 'assignment.md');\n if (await fileExists(assignmentMd)) {\n const content = await readFile(assignmentMd, 'utf-8');\n const todosMatch = content.match(/^## Todos\\s*$([\\s\\S]*?)(?=^## |$(?![\\r\\n]))/m);\n if (todosMatch) bodies.push(todosMatch[1]);\n }\n\n for (const filename of ['progress.md', 'comments.md', 'handoff.md']) {\n const path = resolve(sourceDir, filename);\n if (await fileExists(path)) {\n try {\n bodies.push(await readFile(path, 'utf-8'));\n } catch {\n // ignore\n }\n }\n }\n\n let total = 0;\n const patterns = buildLinkPatternsForTarget(target);\n for (const body of bodies) {\n for (const pattern of patterns) {\n const matches = body.match(pattern);\n if (matches) total += matches.length;\n }\n }\n return total;\n}\n\nfunction buildLinkPatternsForTarget(target: ReferenceTarget): RegExp[] {\n const patterns: RegExp[] = [];\n // Standalone absolute route\n patterns.push(new RegExp(`/assignments/${escapeRegExpLocal(target.id)}(?:/|\\\\b)`, 'g'));\n if (target.projectSlug) {\n // Project-nested absolute route\n patterns.push(\n new RegExp(\n `/projects/${escapeRegExpLocal(target.projectSlug)}/assignments/${escapeRegExpLocal(target.slug)}(?:/|\\\\b)`,\n 'g',\n ),\n );\n // Project-nested relative route\n patterns.push(\n new RegExp(`\\\\.\\\\./${escapeRegExpLocal(target.slug)}(?:/|\\\\b)`, 'g'),\n );\n }\n return patterns;\n}\n\nfunction escapeRegExpLocal(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Resolve an assignment by UUID (standalone or project-nested) and return its full detail payload.\n * GET /api/assignments/:id\n */\nexport async function getAssignmentDetailById(\n projectsDir: string,\n assignmentsDir: string,\n id: string,\n): Promise<AssignmentDetail | null> {\n const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);\n if (!resolved) return null;\n\n if (!resolved.standalone && resolved.projectSlug) {\n // Use the standard detail fetcher, then also scan standalone assignments\n // for backlinks.\n const detail = await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);\n if (!detail) return null;\n detail.referencedBy = await computeReferencedBy(\n { id: detail.id, projectSlug: detail.projectSlug, slug: detail.slug },\n projectsDir,\n assignmentsDir,\n );\n return detail;\n }\n\n // Standalone path — load companion docs directly from the resolved dir.\n const standaloneDetail = await buildStandaloneAssignmentDetail(resolved);\n if (!standaloneDetail) return null;\n standaloneDetail.referencedBy = await computeReferencedBy(\n { id: standaloneDetail.id, projectSlug: null, slug: standaloneDetail.slug },\n projectsDir,\n assignmentsDir,\n );\n return standaloneDetail;\n}\n\nasync function buildStandaloneAssignmentDetail(\n resolved: ResolvedAssignment,\n): Promise<AssignmentDetail | null> {\n const assignmentDir = resolved.assignmentDir;\n const assignmentMdPath = resolve(assignmentDir, 'assignment.md');\n if (!(await fileExists(assignmentMdPath))) return null;\n\n const assignmentContent = await readFile(assignmentMdPath, 'utf-8');\n const assignment = parseAssignmentFull(assignmentContent);\n\n let plan: AssignmentDetail['plan'] = null;\n const planFile = await latestPlanFile(assignmentDir);\n if (planFile) {\n const planPath = resolve(assignmentDir, planFile);\n if (await fileExists(planPath)) {\n const parsed = parsePlan(await readFile(planPath, 'utf-8'));\n plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };\n }\n }\n\n let scratchpad: AssignmentDetail['scratchpad'] = null;\n const scratchpadPath = resolve(assignmentDir, 'scratchpad.md');\n if (await fileExists(scratchpadPath)) {\n const parsed = parseScratchpad(await readFile(scratchpadPath, 'utf-8'));\n scratchpad = { updated: parsed.updated, body: parsed.body };\n }\n\n let handoff: AssignmentDetail['handoff'] = null;\n const handoffPath = resolve(assignmentDir, 'handoff.md');\n if (await fileExists(handoffPath)) {\n const parsed = parseHandoff(await readFile(handoffPath, 'utf-8'));\n handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };\n }\n\n let decisionRecord: AssignmentDetail['decisionRecord'] = null;\n const decisionRecordPath = resolve(assignmentDir, 'decision-record.md');\n if (await fileExists(decisionRecordPath)) {\n const parsed = parseDecisionRecord(await readFile(decisionRecordPath, 'utf-8'));\n decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };\n }\n\n let progress: AssignmentDetail['progress'] = null;\n const progressPath = resolve(assignmentDir, 'progress.md');\n if (await fileExists(progressPath)) {\n const parsed = parseProgress(await readFile(progressPath, 'utf-8'));\n progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };\n }\n\n let comments: AssignmentDetail['comments'] = null;\n const commentsPath = resolve(assignmentDir, 'comments.md');\n if (await fileExists(commentsPath)) {\n const parsed = parseComments(await readFile(commentsPath, 'utf-8'));\n comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };\n }\n\n const { terminalStatuses } = await getStatusConfig();\n const detail: AssignmentDetail = {\n id: assignment.id,\n projectSlug: null,\n slug: assignment.slug || resolved.id,\n title: assignment.title,\n status: assignment.status,\n type: assignment.type,\n priority: assignment.priority as AssignmentDetail['priority'],\n assignee: assignment.assignee,\n dependsOn: [], // standalone cannot declare dependencies\n links: [],\n reverseLinks: [],\n enrichedLinks: [],\n blockedReason: assignment.blockedReason,\n workspace: assignment.workspace,\n projectWorkspace: assignment.workspaceGroup,\n externalIds: assignment.externalIds,\n tags: assignment.tags,\n archived: assignment.archived,\n archivedAt: assignment.archivedAt,\n archivedReason: assignment.archivedReason,\n ...deriveStatusVirtuals(assignment, terminalStatuses),\n override: assignment.override,\n derived: await buildDerivedDetail(assignment, assignmentDir, null),\n created: assignment.created,\n updated: assignment.updated,\n body: assignment.body,\n plan,\n scratchpad,\n handoff,\n decisionRecord,\n progress,\n comments,\n referencedBy: [],\n availableTransitions: await getStandaloneAvailableTransitions(assignment),\n };\n\n return detail;\n}\n\n// Guard so legacy-file renames run at most once per `projectsDir` per process\n// lifetime. Keyed by absolute path to tolerate test suites that open multiple\n// sandboxes in the same process.\nconst migratedProjectsDirs = new Set<string>();\n\nasync function listProjectRecords(\n projectsDir: string,\n traces?: OverviewTraces,\n): Promise<ProjectRecord[]> {\n const cached = projectRecordsCache.get(projectsDir);\n if (cached) return cached;\n // `traces` only flows through on a cache miss; a hit legitimately does ~0\n // fan-out, so the absence of per-phase traces on a hit is the correct signal.\n const promise = computeProjectRecords(projectsDir, traces);\n projectRecordsCache.set(projectsDir, promise);\n promise.catch(() => projectRecordsCache.delete(projectsDir));\n return promise;\n}\n\nasync function computeProjectRecords(\n projectsDir: string,\n traces?: OverviewTraces,\n): Promise<ProjectRecord[]> {\n if (!(await fileExists(projectsDir))) {\n return [];\n }\n\n if (!migratedProjectsDirs.has(projectsDir)) {\n migratedProjectsDirs.add(projectsDir);\n await migrateLegacyProjectFiles(projectsDir);\n // Reconcile legacy \"archived-as-a-status\" projects (statusOverride: 'archived')\n // into the real `archived` flag so there is one source of truth.\n await migrateLegacyArchivedProjects(projectsDir);\n }\n\n const entries = await readdir(projectsDir, { withFileTypes: true });\n const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));\n\n const maybeRecords = await Promise.all(\n projectDirs.map(async (entry): Promise<ProjectRecord | null> => {\n const projectPath = resolve(projectsDir, entry.name);\n const projectMdPath = resolve(projectPath, 'project.md');\n\n if (!(await fileExists(projectMdPath))) {\n return null;\n }\n\n const t0 = traces ? performance.now() : 0;\n const projectContent = await readFile(projectMdPath, 'utf-8');\n const project = parseProject(projectContent);\n if (traces) accumulatePhase(traces, 'parse-project-md', performance.now() - t0);\n\n const t1 = traces ? performance.now() : 0;\n const assignments = await listAssignmentRecords(projectPath, traces);\n if (traces) accumulatePhase(traces, 'list-assignments', performance.now() - t1);\n\n const t2 = traces ? performance.now() : 0;\n const rollup = await buildProjectRollup(projectPath, project, assignments, traces);\n if (traces) accumulatePhase(traces, 'build-rollup', performance.now() - t2);\n\n // Archived children are hidden, so archiving an old one must not bump the\n // project's activity timestamp (which drives list/recent-projects ordering).\n const updated = getProjectActivityTimestamp(project.updated, activeAssignments(assignments));\n\n const t3 = traces ? performance.now() : 0;\n const dependencyGraph = await loadDependencyGraph(projectPath, assignments);\n if (traces) accumulatePhase(traces, 'load-dep-graph', performance.now() - t3);\n\n return {\n projectPath,\n project,\n assignments,\n dependencyGraph,\n summary: {\n slug: project.slug || entry.name,\n title: project.title,\n status: rollup.status,\n statusOverride: project.statusOverride,\n archived: project.archived,\n archivedAt: project.archivedAt,\n archivedReason: project.archivedReason,\n created: project.created,\n updated,\n tags: project.tags,\n externalIds: project.externalIds,\n progress: rollup.progress,\n needsAttention: rollup.needsAttention,\n workspace: project.workspace,\n },\n };\n }),\n );\n\n const records = maybeRecords.filter((r): r is ProjectRecord => r !== null);\n records.sort((left, right) => compareTimestamps(right.summary.updated, left.summary.updated));\n return records;\n}\n\nasync function listAssignmentRecords(\n projectPath: string,\n traces?: OverviewTraces,\n): Promise<AssignmentRecord[]> {\n const assignmentsDir = resolve(projectPath, 'assignments');\n if (!(await fileExists(assignmentsDir))) {\n return [];\n }\n\n const entries = await readdir(assignmentsDir, { withFileTypes: true });\n const dirEntries = entries.filter((entry) => entry.isDirectory());\n\n const maybeRecords = await Promise.all(\n dirEntries.map(async (entry): Promise<AssignmentRecord | null> => {\n const assignmentMd = resolve(assignmentsDir, entry.name, 'assignment.md');\n if (!(await fileExists(assignmentMd))) {\n return null;\n }\n const t0 = traces ? performance.now() : 0;\n const content = await readFile(assignmentMd, 'utf-8');\n const parsed = parseAssignmentFull(content);\n if (traces) accumulatePhase(traces, 'read-assignment-md', performance.now() - t0);\n return parsed;\n }),\n );\n\n const records = maybeRecords.filter((r): r is AssignmentRecord => r !== null);\n records.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return records;\n}\n\nasync function listResources(projectPath: string): Promise<ResourceSummary[]> {\n const resourcesDir = resolve(projectPath, 'resources');\n if (!(await fileExists(resourcesDir))) {\n return [];\n }\n\n const entries = await readdir(resourcesDir, { withFileTypes: true });\n const results: ResourceSummary[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('_')) {\n continue;\n }\n\n const filePath = resolve(resourcesDir, entry.name);\n const content = await readFile(filePath, 'utf-8');\n const parsed = parseResource(content);\n results.push({\n name: parsed.name,\n slug: entry.name.replace(/\\.md$/, ''),\n category: parsed.category,\n source: parsed.source,\n relatedAssignments: parsed.relatedAssignments,\n updated: parsed.updated,\n });\n }\n\n results.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return results;\n}\n\nasync function listMemories(projectPath: string): Promise<MemorySummary[]> {\n const memoriesDir = resolve(projectPath, 'memories');\n if (!(await fileExists(memoriesDir))) {\n return [];\n }\n\n const entries = await readdir(memoriesDir, { withFileTypes: true });\n const results: MemorySummary[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('_')) {\n continue;\n }\n\n const filePath = resolve(memoriesDir, entry.name);\n const content = await readFile(filePath, 'utf-8');\n const parsed = parseMemory(content);\n results.push({\n name: parsed.name,\n slug: entry.name.replace(/\\.md$/, ''),\n source: parsed.source,\n scope: parsed.scope,\n sourceAssignment: parsed.sourceAssignment,\n relatedAssignments: parsed.relatedAssignments,\n updated: parsed.updated,\n });\n }\n\n results.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return results;\n}\n\n/**\n * Walk every project and return its memories enriched with project context.\n *\n * `projectSlug` is the on-disk directory name (used for path-based routes like\n * `/api/projects/:slug/memories/:itemSlug` and the `/projects/:slug/...` UI routes).\n * In typical projects this equals the frontmatter `slug`, but fixtures/legacy projects\n * may differ — and the directory name is what every path-based route resolves against.\n */\nexport async function listAllMemories(\n projectsDir: string,\n): Promise<MemorySummaryWithProject[]> {\n const projectRecords = await listProjectRecords(projectsDir);\n const all: MemorySummaryWithProject[] = [];\n for (const record of projectRecords) {\n const memories = await listMemories(record.projectPath);\n for (const memory of memories) {\n all.push({\n ...memory,\n projectSlug: basename(record.projectPath),\n projectTitle: record.summary.title,\n });\n }\n }\n all.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return all;\n}\n\n/** Walk every project and return its resources enriched with project context. */\nexport async function listAllResources(\n projectsDir: string,\n): Promise<ResourceSummaryWithProject[]> {\n const projectRecords = await listProjectRecords(projectsDir);\n const all: ResourceSummaryWithProject[] = [];\n for (const record of projectRecords) {\n const resources = await listResources(record.projectPath);\n for (const resource of resources) {\n all.push({\n ...resource,\n projectSlug: basename(record.projectPath),\n projectTitle: record.summary.title,\n });\n }\n }\n all.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return all;\n}\n\n/**\n * Resolve a project slug to its on-disk directory path.\n * Tries the dir-name match first (the typical case); falls back to scanning every project\n * for a frontmatter-slug match. Returns `null` when no project matches.\n */\nexport async function resolveProjectPath(\n projectsDir: string,\n projectSlug: string,\n): Promise<string | null> {\n const direct = resolve(projectsDir, projectSlug);\n if (await fileExists(resolve(direct, 'project.md'))) return direct;\n const records = await listProjectRecords(projectsDir);\n const match = records.find((r) => r.summary.slug === projectSlug);\n return match ? match.projectPath : null;\n}\n\nexport async function getMemoryDetail(\n projectsDir: string,\n projectSlug: string,\n itemSlug: string,\n): Promise<MemoryDetail | null> {\n if (itemSlug.startsWith('_')) return null;\n\n const projectRecords = await listProjectRecords(projectsDir);\n // Match by directory name first (the path-based routing convention) and fall back to\n // the frontmatter slug — covers fixtures/legacy projects whose dir name differs from slug.\n const projectRecord = projectRecords.find(\n (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug,\n );\n if (!projectRecord) return null;\n\n const filePath = resolve(projectRecord.projectPath, 'memories', `${itemSlug}.md`);\n if (!(await fileExists(filePath))) return null;\n\n const content = await readFile(filePath, 'utf-8');\n const parsed = parseMemory(content);\n return {\n name: parsed.name,\n slug: itemSlug,\n source: parsed.source,\n scope: parsed.scope,\n sourceAssignment: parsed.sourceAssignment,\n relatedAssignments: parsed.relatedAssignments,\n updated: parsed.updated,\n created: parsed.created,\n body: parsed.body,\n tags: parsed.tags,\n projectSlug: basename(projectRecord.projectPath),\n projectTitle: projectRecord.summary.title,\n };\n}\n\nexport async function getResourceDetail(\n projectsDir: string,\n projectSlug: string,\n itemSlug: string,\n): Promise<ResourceDetail | null> {\n if (itemSlug.startsWith('_')) return null;\n\n const projectRecords = await listProjectRecords(projectsDir);\n const projectRecord = projectRecords.find(\n (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug,\n );\n if (!projectRecord) return null;\n\n const filePath = resolve(projectRecord.projectPath, 'resources', `${itemSlug}.md`);\n if (!(await fileExists(filePath))) return null;\n\n const content = await readFile(filePath, 'utf-8');\n const parsed = parseResource(content);\n return {\n name: parsed.name,\n slug: itemSlug,\n category: parsed.category,\n source: parsed.source,\n relatedAssignments: parsed.relatedAssignments,\n updated: parsed.updated,\n created: parsed.created,\n body: parsed.body,\n projectSlug: basename(projectRecord.projectPath),\n projectTitle: projectRecord.summary.title,\n };\n}\n\nasync function loadDependencyGraph(\n projectPath: string,\n assignments: AssignmentRecord[],\n): Promise<string | null> {\n const statusPath = resolve(projectPath, '_status.md');\n if (await fileExists(statusPath)) {\n const statusContent = await readFile(statusPath, 'utf-8');\n const parsed = parseStatus(statusContent);\n const derivedGraph = extractMermaidGraph(parsed.body);\n if (derivedGraph) {\n return derivedGraph;\n }\n }\n\n return buildDependencyGraph(assignments);\n}\n\nasync function buildProjectRollup(\n projectPath: string,\n project: ReturnType<typeof parseProject>,\n assignments: AssignmentRecord[],\n traces?: OverviewTraces,\n): Promise<{\n progress: ProgressCounts;\n needsAttention: NeedsAttention;\n status: string;\n}> {\n // Archived children are hidden from normal views, so they must not count in\n // the project's progress/totals/status rollup either (cascade consistency).\n const active = activeAssignments(assignments);\n const progress: ProgressCounts = { total: active.length };\n\n // Map: read every comments.md in parallel. Reduce: fold the per-assignment\n // results into progress counters + openQuestions sum.\n const perAssignment = await Promise.all(\n active.map(async (assignment) => {\n const t0 = traces ? performance.now() : 0;\n const openQuestions = await countOpenQuestions(projectPath, assignment.slug);\n if (traces) accumulatePhase(traces, 'count-open-questions', performance.now() - t0);\n return { status: assignment.status, openQuestions };\n }),\n );\n\n let openQuestions = 0;\n for (const entry of perAssignment) {\n progress[entry.status] = (progress[entry.status] ?? 0) + 1;\n openQuestions += entry.openQuestions;\n }\n\n const needsAttention: NeedsAttention = {\n blockedCount: progress['blocked'] ?? 0,\n failedCount: progress['failed'] ?? 0,\n openQuestions,\n };\n\n let status = 'pending';\n if (project.statusOverride) {\n status = project.statusOverride;\n } else if (project.archived) {\n status = 'archived';\n } else if (progress.total > 0 && (progress['completed'] ?? 0) === progress.total) {\n status = 'completed';\n } else if ((progress['in_progress'] ?? 0) > 0 || (progress['review'] ?? 0) > 0) {\n status = 'active';\n } else if ((progress['failed'] ?? 0) > 0) {\n status = 'failed';\n } else if ((progress['blocked'] ?? 0) > 0) {\n status = 'blocked';\n } else if (progress.total === 0 || (progress['pending'] ?? 0) === progress.total) {\n status = 'pending';\n } else {\n status = 'active';\n }\n\n return { progress, needsAttention, status };\n}\n\n/**\n * Derive the loader-only virtual fields from an assignment's `statusHistory`\n * (never stored on disk). `completedAt` is the `at` of the LAST transition into\n * the current status, but only when that status is terminal (lifecycle\n * `completed`/`failed`) — so an assignment reopened after completion reports null,\n * because its current status is no longer terminal. `statusAge` is the elapsed\n * milliseconds since the last entry (time in current status), null when there is\n * no history or the timestamp is unparseable.\n */\nfunction deriveStatusVirtuals(\n assignment: AssignmentRecord,\n terminalStatuses: ReadonlySet<string>,\n): {\n completedAt: string | null;\n statusAge: number | null;\n phaseAge: number | null;\n phase: string | null;\n disposition: string | null;\n pinned: boolean;\n} {\n const hist = assignment.statusHistory ?? [];\n\n let completedAt: string | null = null;\n if (terminalStatuses.has(assignment.status)) {\n for (const entry of hist) {\n if (entry.to === assignment.status) completedAt = entry.at;\n }\n }\n\n // statusAge counts HEADLINE changes only: dimension-only entries (from == to,\n // e.g. phase advanced while blocked) must not reset the clock. The seed\n // entry (from: null) counts as a headline change.\n let statusAge: number | null = null;\n for (let i = hist.length - 1; i >= 0; i--) {\n const entry = hist[i];\n if (entry.from !== entry.to || entry.from === null) {\n const t = Date.parse(entry.at);\n statusAge = Number.isNaN(t) ? null : Date.now() - t;\n break;\n }\n }\n\n let phaseAge: number | null = null;\n for (let i = hist.length - 1; i >= 0; i--) {\n const entry = hist[i];\n if (entry.phaseTo !== undefined && entry.phaseFrom !== entry.phaseTo) {\n const t = Date.parse(entry.at);\n phaseAge = Number.isNaN(t) ? null : Date.now() - t;\n break;\n }\n }\n\n return {\n completedAt,\n statusAge,\n phaseAge,\n phase: assignment.phase,\n disposition: assignment.disposition,\n pinned: assignment.override !== null,\n };\n}\n\n/**\n * Server-side materialization of the derivation detail for one assignment\n * (design v3: the browser never reads the filesystem — facts ship in the\n * payload). Null for terminal assignments (derivation defers entirely).\n */\nasync function buildDerivedDetail(\n assignment: AssignmentRecord,\n assignmentDir: string,\n projectDir: string | null,\n): Promise<AssignmentDetail['derived']> {\n const config = await getStatusConfig();\n if (config.terminalStatuses.has(assignment.status)) return null;\n try {\n const { computeFactsDetailed } = await import('../lifecycle/facts.js');\n const { deriveDimensions } = await import('../lifecycle/derive.js');\n const { DEFAULT_DERIVE_CONFIG } = await import('../utils/config.js');\n // ONE compute pass: facts (custom + attestation exports) and per-record\n // validity come from the same plan-file / HEAD reads. Fresh-per-request is\n // what makes binds:commit lazy convergence honest (Locked Decisions).\n const { facts, attestations } = await computeFactsDetailed({\n assignmentDir,\n frontmatter: {\n ...assignment,\n // AssignmentRecord ⊃ the fields computeFacts reads (incl. facts +\n // attestations from the parser); statusHistory + derived caches ride along.\n } as unknown as import('../lifecycle/types.js').AssignmentFrontmatter,\n body: assignment.body,\n projectDir,\n terminalStatuses: config.terminalStatuses,\n declarations: config.factDeclarations,\n });\n const dims = deriveDimensions({\n facts,\n derive: config.derive ?? DEFAULT_DERIVE_CONFIG,\n currentStatus: assignment.status,\n terminalStatuses: config.terminalStatuses,\n knownStatusIds: new Set(config.statuses.map((s) => s.id)),\n override: assignment.override,\n registry: config.deriveRegistry,\n });\n if (!dims) return null;\n\n // customFacts: declared bool/number values only — the client renders them\n // without guessing which keys are built-ins (the server separated them).\n const customFacts: Record<string, boolean | number> = {};\n for (const decl of config.factDeclarations) {\n if (decl.type === 'bool' || decl.type === 'number') {\n const v = facts[decl.name];\n if (typeof v === 'boolean' || typeof v === 'number') customFacts[decl.name] = v;\n }\n }\n\n return {\n derivedStatus: dims.derivedStatus,\n nextAction: dims.nextAction,\n facts: facts as unknown as Record<string, boolean | number | string[]>,\n customFacts,\n attestations: attestations.map((a) => ({\n fact: a.fact,\n binds: a.binds,\n records: a.records.map(({ record, valid }) => ({\n actor: record.actor,\n verdict: record.verdict,\n at: record.at,\n note: record.note ?? null,\n stale: !valid,\n })),\n })),\n };\n } catch (err) {\n // Best-effort enrichment, never a 500 — but not silent (codex finding 12).\n console.warn(`buildDerivedDetail failed for ${assignmentDir}:`, err);\n return null;\n }\n}\n\nfunction toAssignmentSummary(\n assignment: AssignmentRecord,\n terminalStatuses: ReadonlySet<string>,\n): AssignmentSummary {\n return {\n id: assignment.id,\n slug: assignment.slug,\n title: assignment.title,\n status: assignment.status,\n type: assignment.type,\n priority: assignment.priority as AssignmentSummary['priority'],\n assignee: assignment.assignee,\n dependsOn: assignment.dependsOn,\n links: assignment.links,\n tags: assignment.tags,\n externalIds: assignment.externalIds,\n created: assignment.created,\n updated: assignment.updated,\n archived: assignment.archived,\n archivedAt: assignment.archivedAt,\n archivedReason: assignment.archivedReason,\n ...deriveStatusVirtuals(assignment, terminalStatuses),\n };\n}\n\nasync function toAssignmentBoardItem(\n projectsDir: string,\n projectRecord: ProjectRecord,\n assignment: AssignmentRecord,\n): Promise<AssignmentBoardItem> {\n const config = await getStatusConfig();\n const { terminalStatuses } = config;\n\n const assignmentDir = resolve(projectRecord.projectPath, 'assignments', assignment.slug);\n const projectDir = projectRecord.projectPath;\n\n let facts: AssignmentBoardItem['facts'];\n try {\n const { computeFacts } = await import('../lifecycle/facts.js');\n facts = await computeFacts({\n assignmentDir,\n frontmatter: assignment as unknown as import('../lifecycle/types.js').AssignmentFrontmatter,\n body: assignment.body,\n projectDir,\n terminalStatuses,\n declarations: config.factDeclarations,\n });\n } catch (err) {\n console.warn(`toAssignmentBoardItem: computeFacts failed for ${assignmentDir}:`, err);\n }\n\n return {\n ...toAssignmentSummary(assignment, terminalStatuses),\n projectSlug: projectRecord.summary.slug,\n projectTitle: projectRecord.summary.title,\n blockedReason: assignment.blockedReason,\n projectWorkspace: projectRecord.project.workspace,\n availableTransitions: await getAvailableTransitions(\n projectsDir,\n projectRecord.summary.slug,\n assignment.slug,\n assignment,\n ),\n facts,\n };\n}\n\nconst DEFAULT_GRAPH_COLORS: Record<string, string> = {\n completed: 'fill:#4ea84f,stroke:#1f6b29,color:#ffffff',\n in_progress: 'fill:#1e6fd9,stroke:#0f3f8f,color:#ffffff',\n pending: 'fill:#c0ccd9,stroke:#738399,color:#163047',\n blocked: 'fill:#db5a3f,stroke:#8d2815,color:#ffffff',\n failed: 'fill:#9f2d2d,stroke:#651616,color:#ffffff',\n review: 'fill:#c6911e,stroke:#7a5a10,color:#ffffff',\n};\n\nfunction buildDependencyGraph(assignments: AssignmentRecord[]): string | null {\n const edges: string[] = [];\n const usedStatuses = new Set<string>();\n\n for (const assignment of assignments) {\n for (const dependency of assignment.dependsOn) {\n const depStatus = findAssignmentStatus(assignments, dependency);\n usedStatuses.add(depStatus);\n usedStatuses.add(assignment.status);\n edges.push(\n ` ${dependency}:::${depStatus} --> ${assignment.slug}:::${assignment.status}`,\n );\n }\n }\n\n if (edges.length === 0) {\n return null;\n }\n\n const classDefs: string[] = [];\n for (const status of usedStatuses) {\n const colors = DEFAULT_GRAPH_COLORS[status] ?? 'fill:#94a3b8,stroke:#64748b,color:#ffffff';\n classDefs.push(` classDef ${status} ${colors}`);\n }\n\n return ['graph TD', ...edges, ...classDefs].join('\\n');\n}\n\nfunction findAssignmentStatus(assignments: AssignmentRecord[], slug: string): string {\n return assignments.find((assignment) => assignment.slug === slug)?.status ?? 'pending';\n}\n\nasync function getAvailableTransitions(\n projectsDir: string,\n projectSlug: string,\n assignmentSlug: string,\n assignment: AssignmentRecord,\n options?: {\n dependencyStatusMap?: ReadonlyMap<string, string>;\n traces?: OverviewTraces;\n },\n): Promise<AssignmentTransitionAction[]> {\n const config = await getStatusConfig();\n const transitionDefs = getTransitionDefinitions(config);\n const actions: AssignmentTransitionAction[] = [];\n const projectPath = resolve(projectsDir, projectSlug);\n const traces = options?.traces;\n\n for (const definition of transitionDefs) {\n const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);\n // Only valid transitions reach the client; the kanban inline picker renders them directly.\n if (target === null) continue;\n\n let warning: string | null = null;\n\n if (definition.command === 'start' && !assignment.assignee) {\n warning = 'No assignee set — consider assigning before starting.';\n }\n\n if (definition.command === 'start' && assignment.dependsOn.length > 0) {\n const t0 = traces ? performance.now() : 0;\n const unmetDependencies = await getUnmetDependencies(\n projectPath,\n assignment.dependsOn,\n config.terminalStatuses,\n options?.dependencyStatusMap,\n );\n if (traces) accumulatePhase(traces, 'get-unmet-dependencies', performance.now() - t0);\n if (unmetDependencies.length > 0) {\n warning = `Unmet dependencies: ${unmetDependencies.join(', ')}.`;\n }\n }\n\n actions.push({\n command: definition.command,\n label: definition.label,\n description: definition.description,\n targetStatus: target,\n disabled: false,\n disabledReason: null,\n warning,\n requiresReason: definition.requiresReason,\n });\n }\n\n return actions;\n}\n\nasync function getUnmetDependencies(\n projectPath: string,\n dependsOn: string[],\n terminalStatuses?: ReadonlySet<string>,\n dependencyStatusMap?: ReadonlyMap<string, string>,\n): Promise<string[]> {\n const terminals = terminalStatuses ?? new Set(['completed']);\n const unmet: string[] = [];\n\n for (const dependency of dependsOn) {\n // Fast path: in-memory map (built once by the overview pass over already-parsed records).\n if (dependencyStatusMap) {\n const mappedStatus = dependencyStatusMap.get(dependency);\n if (mappedStatus !== undefined) {\n if (!terminals.has(mappedStatus)) {\n unmet.push(`${dependency} (${mappedStatus})`);\n }\n continue;\n }\n // Fall through to disk read only if the map didn't know about this dependency.\n }\n\n const dependencyPath = resolve(projectPath, 'assignments', dependency, 'assignment.md');\n if (!(await fileExists(dependencyPath))) {\n unmet.push(`${dependency} (missing)`);\n continue;\n }\n\n const content = await readFile(dependencyPath, 'utf-8');\n const parsed = parseAssignmentFull(content);\n if (!terminals.has(parsed.status)) {\n unmet.push(`${dependency} (${parsed.status})`);\n }\n }\n\n return unmet;\n}\n\ninterface OverviewSegmentBuckets {\n readyForReview: AttentionItem[];\n readyToImplement: AttentionItem[];\n readyForPlanning: AttentionItem[];\n inProgress: AttentionItem[];\n drafts: AttentionItem[];\n blocked: AttentionItem[];\n newestCreated: AttentionItem[];\n stale: AttentionItem[];\n}\n\nfunction emptyBuckets(): OverviewSegmentBuckets {\n return {\n readyForReview: [],\n readyToImplement: [],\n readyForPlanning: [],\n inProgress: [],\n drafts: [],\n blocked: [],\n newestCreated: [],\n stale: [],\n };\n}\n\nfunction segmentSeverity(segment: OverviewSegmentId): AttentionItem['severity'] {\n switch (segment) {\n case 'blocked':\n return 'high';\n case 'readyForReview':\n return 'medium';\n case 'stale':\n return 'low';\n default:\n return 'medium';\n }\n}\n\nconst STALE_SEVERITY_RANK: Record<StaleReason['severity'], number> = { high: 3, medium: 2, low: 1 };\n\n/** Highest-severity reason (drives the displayed stale reason line). */\nfunction topStaleReason(reasons: StaleReason[]): StaleReason | null {\n if (reasons.length === 0) return null;\n return reasons\n .slice()\n .sort((a, b) => STALE_SEVERITY_RANK[b.severity] - STALE_SEVERITY_RANK[a.severity])[0];\n}\n\n/** Activity age from `progress.md` mtime (the honest signal — NOT assignment\n * `updated`, which recompute bumps). `null` when there is no progress.md, so the\n * classifier's activity-based reason fails safe (never fires on unknown). */\nasync function readProgressActivityMs(progressPath: string, now: number): Promise<number | null> {\n try {\n const s = await stat(progressPath);\n return Math.max(0, now - s.mtimeMs);\n } catch {\n return null;\n }\n}\n\n/** Run the shared staleness classifier for one assignment record. */\nfunction classifyAssignmentRecord(\n assignment: AssignmentRecord,\n terminalStatuses: ReadonlySet<string>,\n depsSatisfied: boolean | null,\n lastActivityMs: number | null,\n thresholds: StaleThresholds,\n): StaleReason[] {\n const virtuals = deriveStatusVirtuals(assignment, terminalStatuses);\n return classifyNeedsAttention(\n {\n phase: virtuals.phase,\n disposition: virtuals.disposition,\n isTerminal: terminalStatuses.has(assignment.status),\n assignee: assignment.assignee ?? null,\n blockedReason: assignment.blockedReason,\n depsSatisfied,\n // plan_awaiting_approval is deferred to the decision inbox's plan-approval\n // category for now; pass values that keep that reason dormant.\n planExists: false,\n planApproved: true,\n statusAgeMs: virtuals.statusAge,\n lastActivityMs,\n },\n thresholds,\n );\n}\n\n/**\n * Read-only scan of EVERY active assignment (project + standalone, unpaged) for\n * the staleness watchdog. Reuses the same classifier + resolved terminals +\n * config thresholds as the overview, keyed by assignment id (stable UUID). Never\n * writes anything.\n */\nexport async function collectStaleCandidates(\n projectsDir: string,\n assignmentsDir?: string,\n): Promise<StaleCandidate[]> {\n const [projectRecords, standaloneRecords] = await Promise.all([\n listProjectRecords(projectsDir),\n listStandaloneRecords(assignmentsDir),\n ]);\n const { terminalStatuses } = await getStatusConfig();\n const thresholds = resolveStaleThresholds((await readConfig()).staleness);\n const now = Date.now();\n const out: StaleCandidate[] = [];\n\n for (const record of projectRecords) {\n if (isProjectArchived(record.summary)) continue;\n const projectPath = resolve(projectsDir, record.summary.slug);\n const depMap = new Map<string, string>();\n for (const a of record.assignments) depMap.set(a.slug, a.status);\n for (const assignment of activeAssignments(record.assignments)) {\n const depsSatisfied =\n assignment.dependsOn.length === 0\n ? true\n : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;\n const lastActivityMs = await readProgressActivityMs(\n resolve(projectPath, 'assignments', assignment.slug, 'progress.md'),\n now,\n );\n const reasons = classifyAssignmentRecord(assignment, terminalStatuses, depsSatisfied, lastActivityMs, thresholds);\n if (reasons.length > 0) {\n out.push({ assignmentId: assignment.id, projectSlug: record.summary.slug, reasons });\n }\n }\n }\n\n for (const sr of standaloneRecords) {\n if (sr.record.archived === true) continue;\n const lastActivityMs = await readProgressActivityMs(resolve(sr.assignmentDir, 'progress.md'), now);\n const reasons = classifyAssignmentRecord(sr.record, terminalStatuses, true, lastActivityMs, thresholds);\n if (reasons.length > 0) out.push({ assignmentId: sr.record.id, projectSlug: null, reasons });\n }\n\n return out;\n}\n\nasync function buildOverviewSegmentBuckets(\n projectsDir: string,\n projectRecords: ProjectRecord[],\n standaloneRecords: StandaloneRecord[],\n traces?: OverviewTraces,\n): Promise<OverviewSegmentBuckets> {\n const now = Date.now();\n const buckets = emptyBuckets();\n // Resolved terminal statuses (honors renamed/custom terminals — not the\n // module-local hardcoded TERMINAL_STATUSES) for the staleness classifier.\n const { terminalStatuses } = await getStatusConfig();\n // Staleness age-gates: config overrides merged over defaults (defaults-first).\n const staleThresholds = resolveStaleThresholds((await readConfig()).staleness);\n // Pool of all non-terminal rows (across primary segments) used to seed\n // `newestCreated`. Each entry remembers its `created` timestamp + the row\n // we'd clone into the segment.\n const newestPool: Array<{ created: string; clone: AttentionItem }> = [];\n\n for (const record of projectRecords) {\n // Cascade-hide: an archived project contributes none of its assignments to\n // the overview segments.\n if (isProjectArchived(record.summary)) continue;\n\n // Build a dep-status map once per project so getUnmetDependencies can resolve\n // dependency status from memory instead of re-reading each dep's assignment.md.\n // (Built over ALL assignments so dependency resolution is unaffected by hiding.)\n const depMap = new Map<string, string>();\n for (const a of record.assignments) {\n depMap.set(a.slug, a.status);\n }\n\n // Individually-archived assignments are hidden from the overview segments.\n const visibleAssignments = activeAssignments(record.assignments);\n\n // Resolve every per-assignment getAvailableTransitions call for this project\n // in parallel, then run the synchronous classification logic below over the results.\n const projectPath = resolve(projectsDir, record.summary.slug);\n const resolvedTransitions = await Promise.all(\n visibleAssignments.map(async (assignment) => {\n const t0 = traces ? performance.now() : 0;\n const availableTransitions = await getAvailableTransitions(\n projectsDir,\n record.summary.slug,\n assignment.slug,\n assignment,\n { traces, dependencyStatusMap: depMap },\n );\n if (traces) accumulatePhase(traces, 'get-available-transitions', performance.now() - t0);\n // Inputs for the staleness classifier (resolved off already-parsed data\n // + one progress.md stat). depsSatisfied via the in-memory depMap; no\n // extra disk read when there are no deps.\n const depsSatisfied =\n assignment.dependsOn.length === 0\n ? true\n : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap))\n .length === 0;\n const lastActivityMs = await readProgressActivityMs(\n resolve(projectPath, 'assignments', assignment.slug, 'progress.md'),\n now,\n );\n return { assignment, availableTransitions, depsSatisfied, lastActivityMs };\n }),\n );\n\n for (const { assignment, availableTransitions, depsSatisfied, lastActivityMs } of resolvedTransitions) {\n const segmentId = STATUS_TO_SEGMENT[assignment.status];\n const isTerminal = terminalStatuses.has(assignment.status);\n const staleReasons = classifyAssignmentRecord(\n assignment,\n terminalStatuses,\n depsSatisfied,\n lastActivityMs,\n staleThresholds,\n );\n const stale = staleReasons.length > 0;\n const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));\n const baseId = `${record.summary.slug}:${assignment.slug}`;\n\n const shared = {\n projectSlug: record.summary.slug,\n projectTitle: record.summary.title,\n assignmentSlug: assignment.slug,\n assignmentTitle: assignment.title,\n status: assignment.status,\n updated: assignment.updated,\n href: `/projects/${record.summary.slug}/assignments/${assignment.slug}`,\n blockedReason: assignment.blockedReason,\n stale,\n agingMs,\n assignee: assignment.assignee ?? null,\n availableTransitions,\n };\n\n if (segmentId) {\n const reason =\n segmentId === 'blocked' && assignment.blockedReason\n ? assignment.blockedReason\n : SEGMENT_REASON[segmentId];\n const primary: AttentionItem = {\n ...shared,\n id: `${baseId}:${segmentId}`,\n severity: segmentSeverity(segmentId),\n reason,\n segment: segmentId,\n };\n buckets[segmentId].push(primary);\n }\n\n if (stale && !isTerminal) {\n const top = topStaleReason(staleReasons);\n const staleItem: AttentionItem = {\n ...shared,\n id: `${baseId}:stale`,\n severity: 'low',\n reason: top?.label ?? SEGMENT_REASON.stale,\n segment: 'stale',\n };\n buckets.stale.push(staleItem);\n }\n\n if (!isTerminal) {\n newestPool.push({\n created: assignment.created,\n clone: {\n ...shared,\n id: `${baseId}:newest`,\n severity: 'low',\n reason: SEGMENT_REASON.newestCreated,\n segment: 'newestCreated',\n },\n });\n }\n }\n }\n\n const resolvedStandaloneTransitions = await Promise.all(\n standaloneRecords\n .filter((sr) => sr.record.archived !== true)\n .map(async (sr) => {\n const t0 = traces ? performance.now() : 0;\n const availableTransitions = await getStandaloneAvailableTransitions(sr.record);\n if (traces) accumulatePhase(traces, 'get-available-transitions', performance.now() - t0);\n const lastActivityMs = await readProgressActivityMs(resolve(sr.assignmentDir, 'progress.md'), now);\n return { sr, availableTransitions, lastActivityMs };\n }),\n );\n\n for (const { sr, availableTransitions, lastActivityMs } of resolvedStandaloneTransitions) {\n const assignment = sr.record;\n const segmentId = STATUS_TO_SEGMENT[assignment.status];\n const isTerminal = terminalStatuses.has(assignment.status);\n // Standalone assignments cannot declare dependencies → depsSatisfied is true.\n const staleReasons = classifyAssignmentRecord(\n assignment,\n terminalStatuses,\n true,\n lastActivityMs,\n staleThresholds,\n );\n const stale = staleReasons.length > 0;\n const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));\n const baseId = `standalone:${sr.id}`;\n\n const shared = {\n projectSlug: null,\n projectTitle: null,\n assignmentSlug: assignment.slug || sr.id,\n assignmentTitle: assignment.title,\n status: assignment.status,\n updated: assignment.updated,\n href: `/assignments/${sr.id}`,\n blockedReason: assignment.blockedReason,\n stale,\n agingMs,\n assignee: assignment.assignee ?? null,\n availableTransitions,\n };\n\n if (segmentId) {\n const reason =\n segmentId === 'blocked' && assignment.blockedReason\n ? assignment.blockedReason\n : SEGMENT_REASON[segmentId];\n buckets[segmentId].push({\n ...shared,\n id: `${baseId}:${segmentId}`,\n severity: segmentSeverity(segmentId),\n reason,\n segment: segmentId,\n });\n }\n\n if (stale && !isTerminal) {\n const top = topStaleReason(staleReasons);\n buckets.stale.push({\n ...shared,\n id: `${baseId}:stale`,\n severity: 'low',\n reason: top?.label ?? SEGMENT_REASON.stale,\n segment: 'stale',\n });\n }\n\n if (!isTerminal) {\n newestPool.push({\n created: assignment.created,\n clone: {\n ...shared,\n id: `${baseId}:newest`,\n severity: 'low',\n reason: SEGMENT_REASON.newestCreated,\n segment: 'newestCreated',\n },\n });\n }\n }\n\n newestPool.sort((a, b) => compareTimestamps(b.created, a.created));\n buckets.newestCreated = newestPool.slice(0, NEWEST_CREATED_LIMIT).map((entry) => entry.clone);\n\n for (const key of Object.keys(buckets) as OverviewSegmentId[]) {\n if (key === 'newestCreated') continue; // already sorted by `created`\n if (key === 'stale') {\n buckets[key].sort((a, b) => b.agingMs - a.agingMs);\n continue;\n }\n buckets[key].sort((a, b) => compareTimestamps(b.updated, a.updated));\n }\n\n return buckets;\n}\n\nfunction toOverviewSegments(\n buckets: OverviewSegmentBuckets,\n staleOpts: { staleLimit: number; staleOffset: number },\n): OverviewSegments {\n const sliceCap = (items: AttentionItem[]): OverviewSegmentPayload => ({\n items: items.slice(0, SEGMENT_DISPLAY_CAP),\n total: items.length,\n });\n\n const stale = buckets.stale;\n const staleSlice = stale.slice(staleOpts.staleOffset, staleOpts.staleOffset + staleOpts.staleLimit);\n const staleSegment: OverviewStaleSegmentPayload = {\n items: staleSlice,\n total: stale.length,\n limit: staleOpts.staleLimit,\n offset: staleOpts.staleOffset,\n hasMore: staleOpts.staleOffset + staleSlice.length < stale.length,\n };\n\n return {\n readyForReview: sliceCap(buckets.readyForReview),\n readyToImplement: sliceCap(buckets.readyToImplement),\n readyForPlanning: sliceCap(buckets.readyForPlanning),\n inProgress: sliceCap(buckets.inProgress),\n drafts: sliceCap(buckets.drafts),\n blocked: sliceCap(buckets.blocked),\n newestCreated: { items: buckets.newestCreated, total: buckets.newestCreated.length },\n stale: staleSegment,\n };\n}\n\nfunction pickOverviewHero(buckets: OverviewSegmentBuckets): OverviewHeroRecommendation {\n for (const [segmentId, kind] of HERO_PRIORITY) {\n const bucket = buckets[segmentId];\n if (bucket.length === 0) continue;\n const top = bucket[0];\n const total = bucket.length;\n const copyKey = total === 1 ? `${kind}.singular` : kind;\n return { kind, copyKey, itemId: top.id, total };\n }\n return { kind: 'clean', copyKey: 'clean', itemId: null, total: 0 };\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) return min;\n return Math.min(Math.max(value, min), max);\n}\n\nfunction buildRecentActivity(\n projectRecords: ProjectRecord[],\n standaloneRecords: StandaloneRecord[] = [],\n): RecentActivityItem[] {\n const activity: RecentActivityItem[] = [];\n\n for (const record of projectRecords) {\n activity.push({\n id: `project:${record.summary.slug}`,\n type: 'project',\n title: record.summary.title,\n updated: record.summary.updated,\n href: `/projects/${record.summary.slug}`,\n projectSlug: record.summary.slug,\n projectTitle: record.summary.title,\n assignmentSlug: null,\n summary: `Project status is ${record.summary.status}.`,\n });\n\n for (const assignment of activeAssignments(record.assignments)) {\n activity.push({\n id: `assignment:${record.summary.slug}:${assignment.slug}`,\n type: 'assignment',\n title: assignment.title,\n updated: assignment.updated,\n href: `/projects/${record.summary.slug}/assignments/${assignment.slug}`,\n projectSlug: record.summary.slug,\n projectTitle: record.summary.title,\n assignmentSlug: assignment.slug,\n summary: `Assignment is ${assignment.status} with ${assignment.priority} priority.`,\n });\n }\n }\n\n for (const sr of standaloneRecords) {\n const assignment = sr.record;\n activity.push({\n id: `standalone-assignment:${sr.id}`,\n type: 'assignment',\n title: assignment.title,\n updated: assignment.updated,\n href: `/assignments/${sr.id}`,\n projectSlug: null,\n projectTitle: null,\n assignmentSlug: assignment.slug || sr.id,\n summary: `Standalone assignment is ${assignment.status} with ${assignment.priority} priority.`,\n });\n }\n\n activity.sort((left, right) => compareTimestamps(right.updated, left.updated));\n return activity;\n}\n\nfunction compareTimestamps(left: string, right: string): number {\n return parseTimestamp(left) - parseTimestamp(right);\n}\n\nfunction parseTimestamp(timestamp: string): number {\n const parsed = Date.parse(timestamp);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction countPendingAnswers(body: string): number {\n const matches = body.match(/^\\*\\*A:\\*\\*\\s+pending\\s*$/gim);\n return matches ? matches.length : 0;\n}\n\nasync function countOpenQuestions(\n projectPath: string,\n assignmentSlug: string,\n): Promise<number> {\n const commentsPath = resolve(\n projectPath,\n 'assignments',\n assignmentSlug,\n 'comments.md',\n );\n if (!(await fileExists(commentsPath))) {\n return 0;\n }\n try {\n const content = await readFile(commentsPath, 'utf-8');\n const parsed = parseComments(content);\n return parsed.entries.filter(\n (e) => e.type === 'question' && e.resolved !== true,\n ).length;\n } catch {\n return 0;\n }\n}\n\nfunction getProjectActivityTimestamp(projectUpdated: string, assignments: AssignmentRecord[]): string {\n let latest = projectUpdated;\n for (const assignment of assignments) {\n if (compareTimestamps(assignment.updated, latest) > 0) {\n latest = assignment.updated;\n }\n }\n return latest;\n}\n\nfunction getDocumentPath(\n projectsDir: string,\n documentType: EditableDocumentResponse['documentType'],\n projectSlug: string,\n assignmentSlug?: string,\n): string | null {\n switch (documentType) {\n case 'project':\n return resolve(projectsDir, projectSlug, 'project.md');\n case 'assignment':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'assignment.md')\n : null;\n case 'plan':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'plan.md')\n : null;\n case 'scratchpad':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'scratchpad.md')\n : null;\n case 'handoff':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'handoff.md')\n : null;\n case 'decision-record':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'assignments', assignmentSlug, 'decision-record.md')\n : null;\n case 'memory':\n // For memory/resource, the second positional is the item slug.\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'memories', `${assignmentSlug}.md`)\n : null;\n case 'resource':\n return assignmentSlug\n ? resolve(projectsDir, projectSlug, 'resources', `${assignmentSlug}.md`)\n : null;\n default:\n return null;\n }\n}\n\nfunction getEditableDocumentTitle(\n documentType: EditableDocumentResponse['documentType'],\n projectSlug: string,\n assignmentSlug?: string,\n): string {\n switch (documentType) {\n case 'project':\n return `Edit Project: ${projectSlug}`;\n case 'assignment':\n return `Edit Assignment: ${assignmentSlug || 'assignment'}`;\n case 'plan':\n return `Edit Plan: ${assignmentSlug || 'assignment'}`;\n case 'scratchpad':\n return `Edit Scratchpad: ${assignmentSlug || 'assignment'}`;\n case 'handoff':\n return `Append Handoff: ${assignmentSlug || 'assignment'}`;\n case 'decision-record':\n return `Append Decision: ${assignmentSlug || 'assignment'}`;\n case 'playbook':\n return `Edit Playbook: ${projectSlug}`;\n case 'memory':\n return `Edit Memory: ${assignmentSlug || 'memory'}`;\n case 'resource':\n return `Edit Resource: ${assignmentSlug || 'resource'}`;\n default:\n return projectSlug;\n }\n}\n\n// --- Playbook API ---\n\nexport async function listPlaybooks(playbooksDir: string): Promise<PlaybookSummary[]> {\n if (!(await fileExists(playbooksDir))) return [];\n\n const config = await readConfig();\n const disabledSet = new Set(config.playbooks.disabled);\n\n const entries = await readdir(playbooksDir, { withFileTypes: true });\n const playbooks: PlaybookSummary[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('_') || entry.name === 'manifest.md') continue;\n\n const filePath = resolve(playbooksDir, entry.name);\n const raw = await readFile(filePath, 'utf-8');\n const parsed = parsePlaybook(raw);\n\n const slug = parsed.slug || entry.name.replace(/\\.md$/, '');\n playbooks.push({\n slug,\n name: parsed.name || slug,\n description: parsed.description,\n whenToUse: parsed.whenToUse,\n tags: parsed.tags,\n created: parsed.created,\n updated: parsed.updated,\n enabled: !disabledSet.has(slug),\n });\n }\n\n return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));\n}\n\nexport async function getPlaybookDetail(\n playbooksDir: string,\n slug: string,\n): Promise<PlaybookDetail | null> {\n const resolved = await resolvePlaybookSlug(playbooksDir, slug);\n if (!resolved) return null;\n\n const config = await readConfig();\n const enabled = !config.playbooks.disabled.includes(resolved.slug);\n\n const parsed = resolved.parsed;\n return {\n slug: resolved.slug,\n name: parsed.name || resolved.slug,\n description: parsed.description,\n whenToUse: parsed.whenToUse,\n tags: parsed.tags,\n created: parsed.created,\n updated: parsed.updated,\n body: parsed.body,\n enabled,\n };\n}\n","import {\n TERMINAL_CHOICES,\n type TerminalChoice,\n} from '../utils/terminal-schema.js';\n\nexport type OpenUrlErrorCode =\n | 'bad-scheme'\n | 'bad-host'\n | 'missing-id'\n | 'both-ids'\n | 'malformed'\n | 'duplicate-param'\n | 'bad-terminal'\n | 'bad-mode'\n | 'invalid-prompt';\n\n/**\n * Maximum length of a `prompt=` launch-prompt override. Bounds the\n * `syntaur://` URL and is enforced server-side in `parseOpenUrl` so a\n * hand-crafted direct URL can't bypass the dashboard dialog's cap.\n */\nexport const MAX_OPEN_PROMPT_LENGTH = 2000;\n\nexport class OpenUrlError extends Error {\n readonly code: OpenUrlErrorCode;\n constructor(code: OpenUrlErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'OpenUrlError';\n }\n}\n\nexport type SessionMode = 'resume' | 'fork';\n\nconst SESSION_MODES: readonly SessionMode[] = ['resume', 'fork'];\n\nexport interface ParsedOpenUrl {\n kind: 'assignment' | 'session';\n id: string;\n /**\n * Optional one-shot terminal override. When present, the launch plan uses\n * this instead of the configured `terminal:`. The dashboard's\n * missing-terminal fallback dialog sets this so a confirm-to-fallback flow\n * doesn't require mutating user config.\n */\n terminal?: TerminalChoice;\n /**\n * Only set when `kind === 'session'`. Defaults to `'resume'` when the URL\n * has no `mode` query param. Distinguishes \"continue this session under the\n * same id\" (resume) from \"branch a new session id at this point in\n * history\" (fork) so the dashboard can disable Resume while the original\n * process may still be writing the transcript.\n */\n mode?: SessionMode;\n /**\n * Optional agent id to launch with (the `agent=` query param). Lets the\n * dashboard's \"Open in agent\" picker launch a specific runner profile instead\n * of the configured default. Only honored for `kind === 'assignment'`; for\n * sessions the agent is pinned by the session record, so the value is\n * parsed-but-ignored rather than rejected (keeps the parser simple).\n */\n agent?: string;\n /**\n * Optional one-shot launch-prompt override (the `prompt=` query param) — the\n * dashboard's editable prompt box sends the (possibly edited) template here.\n * Only set for `kind === 'assignment'` (sessions take their first message\n * from history). Length-bounded (`MAX_OPEN_PROMPT_LENGTH`).\n * Presence-significant: an empty string is a deliberate override (re-resolves\n * to the fallback seed), distinct from `undefined` (no override).\n */\n prompt?: string;\n}\n\n/**\n * Parse a `syntaur://open?assignment=<id>` or `syntaur://open?session=<id>` URL.\n *\n * Validation:\n * - scheme must be `syntaur:`\n * - host must be `open`\n * - exactly one of `assignment` or `session` query params must be present\n * - neither param may be duplicated\n * - when `session` is present, optional `mode=resume|fork` (default `resume`)\n * - optional `terminal=<choice>` one-shot override (validated against\n * `TERMINAL_CHOICES`)\n *\n * Throws OpenUrlError with a structured code on any failure.\n */\nexport function parseOpenUrl(input: string): ParsedOpenUrl {\n let url: URL;\n try {\n url = new URL(input);\n } catch {\n throw new OpenUrlError(\n 'malformed',\n `Could not parse URL: ${JSON.stringify(input)}`,\n );\n }\n\n if (url.protocol !== 'syntaur:') {\n throw new OpenUrlError(\n 'bad-scheme',\n `Expected scheme 'syntaur:' but got '${url.protocol}'`,\n );\n }\n\n if (url.hostname !== 'open') {\n throw new OpenUrlError(\n 'bad-host',\n `Expected host 'open' but got '${url.hostname}'`,\n );\n }\n\n const assignmentVals = url.searchParams.getAll('assignment');\n const sessionVals = url.searchParams.getAll('session');\n\n if (assignmentVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `assignment` query param',\n );\n }\n if (sessionVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `session` query param',\n );\n }\n\n // Both-ids is decided by PARAM PRESENCE (`...length === 1`), not by value\n // truthiness. `?assignment=&session=x` has BOTH params present even though\n // assignment's value is empty — that must error as both-ids, not silently\n // fall through to the session branch.\n if (assignmentVals.length === 1 && sessionVals.length === 1) {\n throw new OpenUrlError(\n 'both-ids',\n 'URL has both `assignment` and `session` query params — only one is allowed',\n );\n }\n\n const terminalVals = url.searchParams.getAll('terminal');\n if (terminalVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `terminal` query param',\n );\n }\n let terminal: TerminalChoice | undefined;\n if (terminalVals.length === 1 && terminalVals[0].trim() !== '') {\n const candidate = terminalVals[0];\n if (!(TERMINAL_CHOICES as readonly string[]).includes(candidate)) {\n throw new OpenUrlError(\n 'bad-terminal',\n `\\`terminal\\` query param must be one of: ${TERMINAL_CHOICES.join(', ')}`,\n );\n }\n terminal = candidate as TerminalChoice;\n }\n\n const agentVals = url.searchParams.getAll('agent');\n if (agentVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `agent` query param',\n );\n }\n let agent: string | undefined;\n if (agentVals.length === 1 && agentVals[0].trim() !== '') {\n agent = agentVals[0];\n }\n\n // `prompt=` is presence-significant: keep an empty value (a deliberate clear)\n // distinct from absent. Bounded so it can't bloat the URL.\n const promptVals = url.searchParams.getAll('prompt');\n if (promptVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `prompt` query param',\n );\n }\n let prompt: string | undefined;\n if (promptVals.length === 1) {\n const value = promptVals[0];\n if (value.length > MAX_OPEN_PROMPT_LENGTH) {\n throw new OpenUrlError(\n 'invalid-prompt',\n `\\`prompt\\` query param exceeds ${MAX_OPEN_PROMPT_LENGTH} characters`,\n );\n }\n prompt = value;\n }\n\n if (assignmentVals.length === 1) {\n const id = assignmentVals[0];\n if (id.trim() === '') {\n throw new OpenUrlError(\n 'missing-id',\n '`assignment` query param is empty',\n );\n }\n return {\n kind: 'assignment',\n id,\n ...(terminal ? { terminal } : {}),\n ...(agent ? { agent } : {}),\n // assignment-only; keep '' (presence-significant) — hence !== undefined.\n ...(prompt !== undefined ? { prompt } : {}),\n };\n }\n\n if (sessionVals.length === 1) {\n const id = sessionVals[0];\n if (id.trim() === '') {\n throw new OpenUrlError('missing-id', '`session` query param is empty');\n }\n\n const modeVals = url.searchParams.getAll('mode');\n if (modeVals.length > 1) {\n throw new OpenUrlError(\n 'duplicate-param',\n 'URL has more than one `mode` query param',\n );\n }\n let mode: SessionMode = 'resume';\n if (modeVals.length === 1) {\n const raw = modeVals[0];\n if (!SESSION_MODES.includes(raw as SessionMode)) {\n throw new OpenUrlError(\n 'bad-mode',\n `\\`mode\\` must be one of ${SESSION_MODES.join('|')} (got \"${raw}\")`,\n );\n }\n mode = raw as SessionMode;\n }\n return {\n kind: 'session',\n id,\n mode,\n ...(terminal ? { terminal } : {}),\n ...(agent ? { agent } : {}),\n };\n }\n\n throw new OpenUrlError(\n 'missing-id',\n 'URL must include either `assignment=<id>` or `session=<id>`',\n );\n}\n","import {\n type AgentConfig,\n type SyntaurConfig,\n type TerminalChoice,\n getAgents,\n getTerminal,\n} from '../utils/config.js';\nimport { resolveAssignmentById } from '../utils/assignment-resolver.js';\nimport {\n getAssignmentDetail,\n getAssignmentDetailById,\n} from '../dashboard/api.js';\n// Import the resolver directly (not via ./index.js) to avoid the\n// argv→tui/launch import cycle the barrel would introduce.\nimport { resolveLaunchPrompt } from './launch-prompt.js';\nimport { playbooksDir } from '../utils/paths.js';\nimport { listPlaybookSlugs } from '../utils/playbooks.js';\nimport { formatFallbackCwdWarning, isExistingDir, resolveWorkspaceCwd } from './cwd.js';\nimport { getSessionById } from '../dashboard/agent-sessions.js';\nimport { buildFreshArgv, buildSessionArgv } from './argv.js';\nimport type { ResolvedArgv } from './types.js';\nimport type { SessionMode } from './url.js';\n\nexport type LaunchErrorCode =\n | 'no-agents-configured'\n | 'assignment-not-found'\n | 'session-not-found'\n | 'agent-not-configured'\n | 'mode-not-supported'\n | 'workspace-path-invalid';\n\nexport class LaunchError extends Error {\n readonly code: LaunchErrorCode;\n constructor(code: LaunchErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'LaunchError';\n }\n}\n\nexport interface LaunchPlan {\n terminal: TerminalChoice;\n cwd: string;\n argv: ResolvedArgv;\n env: NodeJS.ProcessEnv;\n agentId: string;\n /** Non-fatal warning about a fallback cwd (worktree path missing). */\n fallbackWarning: string | null;\n /** Non-fatal warning from shell-alias resolution falling back to /bin/sh. */\n shellFallbackWarning: string | null;\n /** Non-fatal launch-prompt token warnings (unknown/malformed `@`-tokens). */\n promptWarnings?: string[];\n /**\n * Session identity at launch time, for register-at-birth. `sessionId` is only\n * known for resume-mode session launches; fresh/fork launches mint a NEW id\n * inside the agent, so they carry `null` and rely on the pending runtime\n * marker + scanner to close the gap. Absent on assignment launches.\n */\n session?: { sessionId: string | null };\n}\n\nexport interface ResolveLaunchPlanInput {\n kind: 'assignment' | 'session';\n id: string;\n /**\n * Only consulted when `kind === 'session'`. Defaults to `'resume'` so\n * callers that haven't been updated to thread the URL mode through still\n * get the prior behavior (continue the same session id).\n */\n mode?: SessionMode;\n config: SyntaurConfig;\n projectsDir: string;\n assignmentsDir: string;\n /**\n * One-shot terminal override. When set, used in place of\n * `getTerminal(config)`. Wired through from `?terminal=<choice>` on the\n * incoming `syntaur://` URL so the dashboard's missing-terminal fallback\n * dialog can confirm a different terminal without mutating user config.\n */\n terminalOverride?: TerminalChoice;\n /**\n * Only consulted when `kind === 'assignment'`. The agent id to launch with,\n * wired from `?agent=<id>` on the incoming `syntaur://` URL so the dashboard's\n * \"Open in agent\" picker can launch a specific runner profile. When unset,\n * falls back to `pickAgent(config)` (the default agent). An unknown id throws\n * `LaunchError('agent-not-configured')`.\n */\n agentId?: string;\n /**\n * Only consulted when `kind === 'assignment'`. A one-shot launch-prompt\n * override, wired from `?prompt=<text>` on the incoming `syntaur://` URL (the\n * dashboard's editable prompt box). **Presence-significant:** when defined\n * (including `''`) it is used as the `template` for `resolveLaunchPrompt`\n * instead of `agent.launchPrompt`, so its `@`-tokens re-resolve and an empty\n * value falls back through the normal empty-template path (never silently\n * reusing `agent.launchPrompt`). Per-launch only — never written to config.\n */\n promptOverride?: string;\n}\n\n/**\n * Pick the agent the \"Open in agent\" flow should use. Order of preference:\n * - the first agent with `default: true`\n * - else the first entry in the list\n * Throws LaunchError('no-agents-configured') if the list is empty (only\n * possible when the user explicitly wrote `agents: []` — absence falls back to\n * BUILTIN_AGENTS via `getAgents()`).\n */\nexport function pickAgent(config: SyntaurConfig): AgentConfig {\n const agents = getAgents(config);\n if (agents.length === 0) {\n throw new LaunchError(\n 'no-agents-configured',\n 'No agents in ~/.syntaur/config.md. Run `syntaur agents add` to configure one.',\n );\n }\n return agents.find((a) => a.default) ?? agents[0];\n}\n\n/**\n * Resolve the launch plan for a \"Open in agent\" click. Reads the assignment or\n * session record, picks the cwd + agent, builds the argv, and returns a\n * structured plan that `executeLaunchPlan` (or an Electron caller) can run.\n */\nexport async function resolveLaunchPlan(\n input: ResolveLaunchPlanInput,\n): Promise<LaunchPlan> {\n const terminal = input.terminalOverride ?? getTerminal(input.config);\n\n if (input.kind === 'assignment') {\n return resolveAssignmentPlan(input, terminal);\n }\n return resolveSessionPlan(input, terminal);\n}\n\nasync function resolveAssignmentPlan(\n input: ResolveLaunchPlanInput,\n terminal: TerminalChoice,\n): Promise<LaunchPlan> {\n const resolved = await resolveAssignmentById(\n input.projectsDir,\n input.assignmentsDir,\n input.id,\n );\n if (!resolved) {\n throw new LaunchError(\n 'assignment-not-found',\n `Assignment with id ${JSON.stringify(input.id)} not found`,\n );\n }\n\n const detail = await getAssignmentDetailById(\n input.projectsDir,\n input.assignmentsDir,\n input.id,\n );\n if (!detail) {\n throw new LaunchError(\n 'assignment-not-found',\n `Assignment ${input.id} resolver returned a directory but detail could not be loaded`,\n );\n }\n\n const picked = resolveWorkspaceCwd({\n worktreePath: detail.workspace.worktreePath,\n repository: detail.workspace.repository,\n branch: detail.workspace.branch,\n assignmentSlug: resolved.assignmentSlug,\n });\n if (picked.cwd === null) {\n // No valid worktree or repository directory — refuse rather than silently\n // launching in the dashboard process cwd.\n throw new LaunchError('workspace-path-invalid', picked.invalidReason as string);\n }\n const cwd = picked.cwd;\n const fallbackWarning = picked.fallbackWarning;\n\n let agent: AgentConfig;\n if (input.agentId) {\n const found = getAgents(input.config).find((a) => a.id === input.agentId);\n if (!found) {\n throw new LaunchError(\n 'agent-not-configured',\n `Agent \"${input.agentId}\" requested in the open URL is not in your agents list.`,\n );\n }\n agent = found;\n } else {\n agent = pickAgent(input.config);\n }\n const knownPlaybookSlugs = await listPlaybookSlugs(playbooksDir());\n // A defined promptOverride (incl. '') wins over the stored template by\n // presence — clearing the box must not silently reuse agent.launchPrompt.\n const template =\n input.promptOverride !== undefined ? input.promptOverride : agent.launchPrompt;\n const { prompt, warnings: promptWarnings } = resolveLaunchPrompt({\n template,\n playbook: agent.playbook,\n id: resolved.id,\n assignmentDir: resolved.assignmentDir,\n projectSlug: resolved.projectSlug,\n assignmentSlug: resolved.assignmentSlug,\n knownPlaybookSlugs,\n });\n const { argv, shellFallbackWarning } = buildFreshArgv(agent, prompt);\n\n return {\n terminal,\n cwd,\n argv,\n env: process.env,\n agentId: agent.id,\n fallbackWarning,\n shellFallbackWarning,\n promptWarnings,\n };\n}\n\nasync function resolveSessionPlan(\n input: ResolveLaunchPlanInput,\n terminal: TerminalChoice,\n): Promise<LaunchPlan> {\n const session = getSessionById(input.id);\n if (!session) {\n throw new LaunchError(\n 'session-not-found',\n `Session with id ${JSON.stringify(input.id)} not found`,\n );\n }\n\n let cwd = session.path;\n let fallbackWarning: string | null = null;\n\n // The session's recorded cwd is where the agent indexed its transcript\n // (Claude Code: ~/.claude/projects/<encoded-cwd>/<id>.jsonl); `--resume <id>`\n // only finds the session from THAT cwd. So when session.path is a real\n // directory it is authoritative and must win over the assignment's *current*\n // workspace — otherwise two sessions on one assignment that ran in different\n // cwds (e.g. repo root vs a worktree) both resolve to the same worktree and\n // collapse onto one transcript. This mirrors resolveRecreateTarget, which\n // already treats session.path as the authoritative path to rebuild. Only when\n // session.path is missing/invalid do we fall back to the linked assignment's\n // workspace cwd (and let preflight/recreate recover a deleted worktree).\n if (!isExistingDir(session.path)) {\n // Resolve the linked assignment the same way resolveRecreateTarget does:\n // project-nested sessions key on (projectSlug, assignmentSlug); standalone\n // sessions store the assignment UUID in assignmentSlug (project_slug IS\n // NULL) and resolve by id. Either may be absent (a session with no linked\n // assignment) → no fallback, keep session.path.\n const detail =\n session.projectSlug && session.assignmentSlug\n ? await getAssignmentDetail(\n input.projectsDir,\n session.projectSlug,\n session.assignmentSlug,\n )\n : session.assignmentSlug\n ? await getAssignmentDetailById(\n input.projectsDir,\n input.assignmentsDir,\n session.assignmentSlug,\n )\n : null;\n if (detail) {\n const picked = resolveWorkspaceCwd({\n worktreePath: detail.workspace.worktreePath,\n repository: detail.workspace.repository,\n branch: detail.workspace.branch,\n assignmentSlug: detail.slug,\n });\n if (picked.cwd !== null) {\n cwd = picked.cwd;\n fallbackWarning = picked.fallbackWarning;\n } else {\n // Neither worktree nor repository is a valid directory. Sessions keep\n // their recorded `session.path` (may be '') rather than failing the\n // launch — only assignment launches hard-error on an invalid workspace.\n fallbackWarning = formatFallbackCwdWarning({\n assignmentSlug: detail.slug,\n workspaceDir: session.path,\n worktreePath: detail.workspace.worktreePath,\n branch: detail.workspace.branch,\n });\n }\n }\n }\n\n // Refuse a blank cwd: buildShellCommandLine would render `cd '' && <agent>`,\n // a POSIX no-op that silently runs in the spawner's cwd (wrong directory, no\n // error). A non-empty-but-stale `session.path` is intentionally preserved\n // above — preflight/recreate recovers a deleted worktree and the literal\n // path is visible in copied commands — so ONLY a blank cwd is unrecoverable\n // and hard-fails, mirroring resolveAssignmentPlan's workspace-path-invalid.\n if (!cwd.trim()) {\n throw new LaunchError(\n 'workspace-path-invalid',\n `Session ${input.id} has no recorded working directory and no linked assignment workspace resolved — refusing to launch in an unknown directory.`,\n );\n }\n\n const agent = getAgents(input.config).find((a) => a.id === session.agent);\n if (!agent) {\n throw new LaunchError(\n 'agent-not-configured',\n `Session ${input.id} was started with agent \"${session.agent}\" which is not in your agents list. Run \\`syntaur agents add ${session.agent}\\` or pick a different session.`,\n );\n }\n\n const { argv, shellFallbackWarning } = buildSessionArgv(\n agent,\n session.sessionId,\n input.mode ?? 'resume',\n );\n\n return {\n terminal,\n cwd,\n argv,\n env: process.env,\n agentId: agent.id,\n fallbackWarning,\n shellFallbackWarning,\n // Resume continues the SAME session id; fork mints a new one in-agent.\n session: { sessionId: (input.mode ?? 'resume') === 'resume' ? session.sessionId : null },\n };\n}\n\n","import { isValidSlug } from '../utils/slug.js';\n\n/**\n * Editable launch-prompt resolution.\n *\n * An agent profile may carry an editable `launchPrompt` template whose\n * `@`-tokens are expanded at launch time. This module is the single, pure home\n * for that expansion plus the low-level seed builders shared with the\n * (deprecated) `INITIAL_PROMPT`. It deliberately imports nothing from\n * `../tui/launch.js` so there is no import cycle (`argv.ts` imports `tui/launch`,\n * and `launch/index.ts` re-exports `argv.ts`) — callers import this module\n * directly, not via the `launch/index.js` barrel.\n */\n\n/**\n * The bare `/grab-assignment` seed — today's zero-config launch behavior. This\n * is the single source of these strings, shared by `resolveLaunchPrompt`'s\n * no-template fallback and `INITIAL_PROMPT`'s no-playbook branch (kept\n * byte-identical for back-compat).\n */\nexport function bareGrabSeed(params: {\n projectSlug: string | null;\n assignmentSlug: string;\n id?: string;\n}): string {\n if (params.projectSlug) {\n return `/grab-assignment ${params.projectSlug} ${params.assignmentSlug}`;\n }\n if (params.id) {\n return `/grab-assignment --id ${params.id}`;\n }\n // No project and no id — fall back to the slug. Should be rare; only happens\n // if a caller forgot to pass the id for a standalone assignment.\n return `/grab-assignment ${params.assignmentSlug}`;\n}\n\n/**\n * The noun phrase a `@<playbook-slug>` token resolves to. The template author\n * writes the surrounding verbs. NOTE: this is the new \"via the /run-playbook\n * skill\" wording used ONLY by the resolver — `INITIAL_PROMPT`'s legacy playbook\n * branch keeps its own \"using the /run-playbook skill\" sentence.\n */\nexport function runPlaybookClause(slug: string): string {\n return `the \\`${slug}\\` playbook via the /run-playbook skill`;\n}\n\n/** The `@assignment` expansion: a pointer to the records dir, not a snapshot. */\nfunction assignmentPointer(id: string | undefined, assignmentDir: string): string {\n const subject = id\n ? `This session is Syntaur assignment ${id}, with records at ${assignmentDir}.`\n : `This session's Syntaur assignment records are at ${assignmentDir}.`;\n return (\n `${subject} Claim and bind it with the /grab-assignment skill if available; ` +\n `otherwise read assignment.md, plan*.md, and progress.md in that directory for full context.`\n );\n}\n\nexport interface ResolveLaunchPromptInput {\n /** The agent's editable launch prompt template (may contain `@`-tokens). */\n template?: string | null;\n /** Back-compat playbook slug; used only when `template` is empty. */\n playbook?: string | null;\n /** Assignment id (optional only to represent the rare slug-fallback seed). */\n id?: string;\n /** Records directory (where assignment.md lives), for `@assignment`. */\n assignmentDir: string;\n /** Null for a standalone assignment. */\n projectSlug: string | null;\n assignmentSlug: string;\n /**\n * Installed playbook slugs, injected by the call site. When provided, a\n * well-formed `@<slug>` not in this set warns and is left literal. When\n * undefined, every well-formed slug resolves without validation.\n */\n knownPlaybookSlugs?: ReadonlySet<string>;\n}\n\nexport interface ResolveLaunchPromptResult {\n prompt: string;\n /** Non-fatal warnings (unknown/malformed `@`-tokens). Never throws. */\n warnings: string[];\n}\n\n/** `@` at start-of-string or after whitespace, then a maximal token run. */\nconst TOKEN_RE = /(^|\\s)@([A-Za-z0-9_-]+)/g;\n\nfunction resolveTemplate(\n template: string,\n ctx: { id?: string; assignmentDir: string; knownPlaybookSlugs?: ReadonlySet<string> },\n): ResolveLaunchPromptResult {\n const warnings: string[] = [];\n const prompt = template.replace(TOKEN_RE, (_match, boundary: string, token: string) => {\n if (token === 'assignment') {\n return boundary + assignmentPointer(ctx.id, ctx.assignmentDir);\n }\n if (!isValidSlug(token)) {\n warnings.push(`launchPrompt: \"@${token}\" is not a valid playbook token — left as literal text.`);\n return boundary + '@' + token;\n }\n if (ctx.knownPlaybookSlugs !== undefined && !ctx.knownPlaybookSlugs.has(token)) {\n warnings.push(`launchPrompt: playbook \"${token}\" (from \"@${token}\") is not installed — left as literal text.`);\n return boundary + '@' + token;\n }\n return boundary + runPlaybookClause(token);\n });\n return { prompt, warnings };\n}\n\n/**\n * Resolve the launch seed for a fresh \"Open in agent\" launch. Pure: never reads\n * the filesystem, never prints. The caller owns warning output.\n *\n * Fallback chain:\n * 1. `template` (trimmed non-empty) → resolve its `@`-tokens.\n * 2. else `playbook` set → synthesize `<@assignment pointer> Run <clause> end-to-end.`\n * (built directly — no `@`-token re-resolution, so a playbook literally named\n * `assignment` cannot collide with the reserved token).\n * 3. else → today's bare `/grab-assignment` seed.\n * `template` wins over `playbook`.\n */\nexport function resolveLaunchPrompt(input: ResolveLaunchPromptInput): ResolveLaunchPromptResult {\n const { template, playbook, id, assignmentDir, projectSlug, assignmentSlug, knownPlaybookSlugs } =\n input;\n\n if (template && template.trim()) {\n return resolveTemplate(template, { id, assignmentDir, knownPlaybookSlugs });\n }\n\n const pb = playbook?.trim();\n if (pb) {\n const pointer = assignmentPointer(id, assignmentDir);\n return { prompt: `${pointer} Run ${runPlaybookClause(pb)} end-to-end.`, warnings: [] };\n }\n\n return { prompt: bareGrabSeed({ projectSlug, assignmentSlug, id }), warnings: [] };\n}\n\n/**\n * The editable **template** to prefill the dashboard's \"Open in agent\" prompt\n * box — NOT the resolved text. Returns:\n * - `launchPrompt` verbatim when set (non-empty after trim); else\n * - a synth template `@assignment Run <runPlaybookClause(playbook)> end-to-end.`\n * when `playbook` is set — the playbook clause is LITERAL (only `@assignment`\n * is a token), so re-resolving this through `resolveLaunchPrompt` reproduces\n * this module's playbook synth (above) byte-for-byte for ANY playbook\n * (installed / disabled / uninstalled / the reserved `assignment`); else\n * - the bare `/grab-assignment` seed (no `@`-tokens).\n *\n * Prefilling the template (not resolved text) and resolving exactly once at\n * launch avoids re-tokenizing an `@<slug>` that may appear inside an expanded\n * records-dir path.\n */\nexport function effectiveLaunchTemplate(input: {\n launchPrompt?: string | null;\n playbook?: string | null;\n projectSlug: string | null;\n assignmentSlug: string;\n id?: string;\n}): string {\n if (input.launchPrompt && input.launchPrompt.trim()) {\n return input.launchPrompt;\n }\n const pb = input.playbook?.trim();\n if (pb) {\n return `@assignment Run ${runPlaybookClause(pb)} end-to-end.`;\n }\n return bareGrabSeed({\n projectSlug: input.projectSlug,\n assignmentSlug: input.assignmentSlug,\n id: input.id,\n });\n}\n","import { isAbsolute } from 'node:path';\nimport type { AgentConfig } from '../utils/config.js';\nimport { applyModelFlag } from '../utils/agents-schema.js';\nimport { buildAgentArgv, shellQuote } from '../tui/launch.js';\nimport type { BuiltArgv } from './types.js';\nimport { LaunchError } from './plan.js';\nimport type { SessionMode } from './url.js';\n\n/**\n * Re-export the fresh-launch argv builder under a parallel name so the launch\n * core has a single import surface: `buildFreshArgv` (new run) and\n * `buildSessionArgv` (resume/fork an existing session).\n */\nexport const buildFreshArgv = buildAgentArgv;\n\n/**\n * Build argv for continuing an existing agent session under a specific mode.\n *\n * The argv shape per agent is declared in `AgentConfig.resume` / `.fork`\n * (`SessionInvocation`):\n * - `args` is a literal argv list. The substring `{id}` is replaced with\n * `sessionId`.\n * - `command` optionally overrides `agent.command` for subcommand-style\n * agents whose binary differs (none in builtins).\n *\n * Existing `agent.args` (the base flags applied to a fresh launch — e.g.\n * `--dangerously-skip-permissions`) are preserved and prefixed before the\n * invocation args, matching the prior `buildResumeArgv` behavior.\n *\n * The `resolveFromShellAliases` rewriting is preserved identically: the\n * command is rewritten to `$SHELL`/`/bin/sh` and args become\n * `['-i', '-c', '<quoted>']`. The quoted command line uses the (possibly\n * overridden) executable.\n *\n * Throws `LaunchError('mode-not-supported', ...)` when the agent has no\n * entry for the requested mode.\n */\nexport function buildSessionArgv(\n agent: AgentConfig,\n sessionId: string,\n mode: SessionMode,\n env: NodeJS.ProcessEnv = process.env,\n): BuiltArgv {\n const invocation = agent[mode];\n if (!invocation) {\n throw new LaunchError(\n 'mode-not-supported',\n `Agent \"${agent.id}\" does not support ${mode} (no agent.${mode} configured)`,\n );\n }\n\n const substituted = invocation.args.map((a) =>\n a === '{id}' ? sessionId : a,\n );\n const command = invocation.command ?? agent.command;\n // Profile model is applied to the agent's own args (stripping any pre-existing\n // `--model` so we never emit a duplicate) and sits BEFORE the resume/fork\n // `substituted` args — subcommand-style agents need `--model <v>` ahead of the\n // `resume`/`fork` token (e.g. `codex --model <v> resume <id>`).\n const agentArgs = [...applyModelFlag(agent, [...(agent.args ?? [])]), ...substituted];\n\n if (agent.resolveFromShellAliases) {\n const requested = env.SHELL;\n let shell = requested;\n let warning: string | null = null;\n if (!shell || !isAbsolute(shell)) {\n warning = `syntaur: $SHELL ${\n requested ? `(\"${requested}\") is not absolute` : 'is unset'\n } — falling back to /bin/sh for shell-alias resolution`;\n shell = '/bin/sh';\n }\n const quoted = [command, ...agentArgs].map(shellQuote).join(' ');\n return {\n argv: { command: shell, args: ['-i', '-c', quoted] },\n shellFallbackWarning: warning,\n };\n }\n\n return {\n argv: { command, args: agentArgs },\n shellFallbackWarning: null,\n };\n}\n","import { spawn } from 'node:child_process';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { isAbsolute, resolve } from 'node:path';\nimport { getAssignmentDetail } from '../dashboard/api.js';\nimport type { AgentConfig } from '../utils/config.js';\nimport { applyModelFlag } from '../utils/agents-schema.js';\nimport type { BuiltArgv } from '../launch/types.js';\nimport {\n formatFallbackCwdWarning,\n isExistingDir,\n resolveWorkspaceCwd,\n} from '../launch/cwd.js';\nimport type { SpawnFn } from '../launch/execute.js';\nimport { bareGrabSeed, resolveLaunchPrompt } from '../launch/launch-prompt.js';\nimport { playbooksDir } from '../utils/paths.js';\nimport { listPlaybookSlugs } from '../utils/playbooks.js';\n\nexport type { ResolvedArgv, BuiltArgv } from '../launch/types.js';\n// `formatFallbackCwdWarning` now lives in ../launch/cwd.ts (a neutral module so\n// plan.ts can import the cwd helpers without a cycle). Re-exported here so the\n// existing `import { formatFallbackCwdWarning } from '../tui/launch.js'` sites\n// (e.g. launch-argv.test.ts) keep working.\nexport { formatFallbackCwdWarning } from '../launch/cwd.js';\n\nexport interface LaunchOptions {\n projectsDir: string;\n projectSlug: string;\n assignmentSlug: string;\n agent: AgentConfig;\n cwdOverride?: string;\n /**\n * Test hook: called with the exit code of the spawned child instead of\n * `process.exit(code)`. Default behavior is `process.exit`. Production\n * callers should leave this unset.\n */\n onExit?: (code: number) => void;\n /**\n * Test hook: replaces `child_process.spawn` so unit tests can assert exactly\n * what (and with which cwd) the launcher invoked without spawning a real\n * process. Default is the real `spawn`. Production callers leave this unset.\n */\n spawnFn?: SpawnFn;\n}\n\n/**\n * Initial message sent to the agent the first time it starts up at an\n * assignment. This is the protocol entry point: `/grab-assignment` is the\n * Claude Code skill that loads project/playbook/memory context for the\n * assignment and (per its pre-flight check) prompts the user if a different\n * assignment is already active in this workspace.\n *\n * Argument shapes match the skill's documented input:\n * - project-nested: `/grab-assignment <project-slug> <assignment-slug>`\n * - standalone: `/grab-assignment --id <uuid>`\n *\n * When `playbook` is set (an agent runner profile), the seed switches to an\n * instruction-style message that chains BOTH `/grab-assignment` and\n * `/run-playbook`. This is deliberate: a Claude Code message fires only ONE\n * leading slash-command — everything after it is swallowed as that command's\n * arguments — so two slash-commands cannot be issued from a single seed. A\n * plain-language instruction lets the agent invoke both skills itself\n * (grab-assignment loads playbook *context*; run-playbook *executes* a specific\n * enabled playbook end-to-end — complementary, not redundant). The no-playbook\n * path keeps the exact, well-tested `/grab-assignment` invocation unchanged.\n */\n/**\n * @deprecated Both launch call sites now route through `resolveLaunchPrompt`\n * (`../launch/launch-prompt.js`), which supports the editable `launchPrompt`\n * field. `INITIAL_PROMPT` is retained only for its existing tests / transitional\n * reference; its no-playbook branch shares `bareGrabSeed` with the resolver so\n * those bare-seed strings stay byte-identical.\n */\nexport const INITIAL_PROMPT = (params: {\n projectSlug: string | null;\n assignmentSlug: string;\n id?: string;\n playbook?: string | null;\n}): string => {\n const playbook = params.playbook?.trim();\n\n if (!playbook) {\n return bareGrabSeed({\n projectSlug: params.projectSlug,\n assignmentSlug: params.assignmentSlug,\n id: params.id,\n });\n }\n\n // Playbook profile: chain grab + run-playbook via a plain-language seed.\n const grabClause = params.projectSlug\n ? `the assignment \\`${params.projectSlug}/${params.assignmentSlug}\\` using the /grab-assignment skill`\n : params.id\n ? `the assignment id \\`${params.id}\\` using /grab-assignment --id ${params.id}`\n : `the assignment \\`${params.assignmentSlug}\\` using the /grab-assignment skill`;\n return (\n `Grab ${grabClause}, then load and run the \\`${playbook}\\` playbook ` +\n `using the /run-playbook skill and carry it out end-to-end.`\n );\n};\n\n/**\n * POSIX single-quote shell escaping. Safe to embed in `sh -c '<result>'`.\n * Replaces ' with '\\'' and wraps the whole value in single quotes.\n */\nexport function shellQuote(arg: string): string {\n if (arg === '') return \"''\";\n return `'${arg.replace(/'/g, `'\\\\''`)}'`;\n}\n\n/**\n * Build argv for an agent launch. Handles:\n * - `resolveFromShellAliases: true` → `$SHELL -i -c '<quoted...>'`\n * - `promptArgPosition: 'first' | 'last' | 'none'`\n * - plain absolute or bare-name command.\n */\nexport function buildAgentArgv(\n agent: AgentConfig,\n prompt: string,\n env: NodeJS.ProcessEnv = process.env,\n): BuiltArgv {\n const position = agent.promptArgPosition ?? 'first';\n // Profile model is appended after the agent's own args (and any pre-existing\n // `--model` in those args is stripped first) so exactly one authoritative\n // `--model` is emitted — never a duplicate, which some CLIs reject.\n const baseArgs = applyModelFlag(agent, [...(agent.args ?? [])]);\n const agentArgs =\n position === 'first'\n ? [prompt, ...baseArgs]\n : position === 'last'\n ? [...baseArgs, prompt]\n : baseArgs;\n\n if (agent.resolveFromShellAliases) {\n const requested = env.SHELL;\n let shell = requested;\n let warning: string | null = null;\n if (!shell || !isAbsolute(shell)) {\n warning = `syntaur: $SHELL ${\n requested ? `(\"${requested}\") is not absolute` : 'is unset'\n } — falling back to /bin/sh for shell-alias resolution`;\n shell = '/bin/sh';\n }\n const quoted = [agent.command, ...agentArgs].map(shellQuote).join(' ');\n return {\n argv: { command: shell, args: ['-i', '-c', quoted] },\n shellFallbackWarning: warning,\n };\n }\n\n return {\n argv: { command: agent.command, args: agentArgs },\n shellFallbackWarning: null,\n };\n}\n\nexport async function launchAgent(options: LaunchOptions): Promise<void> {\n const { projectsDir, projectSlug, assignmentSlug, agent, cwdOverride } = options;\n const exitWith = options.onExit ?? ((code: number) => process.exit(code));\n\n const detail = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);\n if (!detail) {\n console.error(`Assignment not found: ${projectSlug}/${assignmentSlug}`);\n process.exit(1);\n }\n\n const projectDir = resolve(projectsDir, projectSlug);\n const assignmentDir = resolve(projectDir, 'assignments', assignmentSlug);\n\n // Resolve + VALIDATE the working directory before writing context.json or\n // spawning. Never silently fall back to process.cwd() — refuse the launch so\n // we don't open the agent (or write context) in the wrong directory.\n let workspaceDir: string;\n if (cwdOverride) {\n // An explicit, present-but-invalid override is a caller bug — hard error\n // rather than silently falling through to the workspace fields.\n if (!isExistingDir(cwdOverride)) {\n console.error(\n `syntaur: --cwd ${cwdOverride} is not an existing directory — refusing to launch.`,\n );\n exitWith(1);\n return;\n }\n workspaceDir = cwdOverride;\n } else {\n const picked = resolveWorkspaceCwd({\n worktreePath: detail.workspace.worktreePath,\n repository: detail.workspace.repository,\n branch: detail.workspace.branch,\n assignmentSlug,\n });\n if (picked.cwd === null) {\n console.error(`syntaur: ${picked.invalidReason} — refusing to launch.`);\n exitWith(1);\n return;\n }\n workspaceDir = picked.cwd;\n // Preserve the existing missing-field warning behavior: when worktree is\n // valid but `branch` (or worktreePath) is unset we still nudge the user.\n // `picked.fallbackWarning` covers the worktree→repository fallback cases.\n const warning =\n picked.fallbackWarning ??\n formatFallbackCwdWarning({\n assignmentSlug,\n workspaceDir,\n worktreePath: detail.workspace.worktreePath,\n branch: detail.workspace.branch,\n });\n if (warning) console.warn(warning);\n }\n\n const contextDir = resolve(workspaceDir, '.syntaur');\n await mkdir(contextDir, { recursive: true });\n\n const context = {\n projectSlug,\n assignmentSlug,\n projectDir,\n assignmentDir,\n workspaceRoot: workspaceDir,\n title: detail.title,\n branch: detail.workspace.branch ?? null,\n grabbedAt: new Date().toISOString(),\n };\n\n await writeFile(\n resolve(contextDir, 'context.json'),\n JSON.stringify(context, null, 2) + '\\n',\n );\n\n const knownPlaybookSlugs = await listPlaybookSlugs(playbooksDir());\n const { prompt, warnings } = resolveLaunchPrompt({\n template: agent.launchPrompt,\n playbook: agent.playbook,\n id: detail.id,\n assignmentDir,\n projectSlug,\n assignmentSlug,\n knownPlaybookSlugs,\n });\n for (const warning of warnings) console.warn(warning);\n\n const { argv, shellFallbackWarning } = buildAgentArgv(agent, prompt);\n if (shellFallbackWarning) {\n console.warn(shellFallbackWarning);\n }\n\n const spawnImpl = options.spawnFn ?? spawn;\n return new Promise<void>((resolvePromise) => {\n const child = spawnImpl(argv.command, argv.args, {\n cwd: workspaceDir,\n stdio: 'inherit',\n });\n\n child.on('error', (err) => {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') {\n console.error(\n `syntaur: agent \"${agent.id}\" command \"${agent.command}\" not found. ` +\n `If \"${agent.command}\" is a shell alias, set resolveFromShellAliases: true on this agent in ~/.syntaur/config.md.`,\n );\n } else if (code === 'EACCES') {\n console.error(\n `syntaur: agent \"${agent.id}\" command \"${agent.command}\" is not executable (EACCES). ` +\n `Check file permissions.`,\n );\n } else {\n console.error(\n `syntaur: failed to launch agent \"${agent.id}\" (${code ?? 'unknown'}): ${err.message}`,\n );\n }\n resolvePromise();\n exitWith(1);\n });\n\n child.on('exit', (code) => {\n resolvePromise();\n exitWith(code ?? 0);\n });\n });\n}\n","import { spawn, type ChildProcess, type SpawnOptions } from 'node:child_process';\nimport { homedir } from 'node:os';\nimport { basename, join, resolve } from 'node:path';\nimport { shellQuote } from '../tui/launch.js';\nimport { resolveCmuxCli } from '../utils/terminal-probe.js';\nimport { fileExists } from '../utils/fs.js';\nimport { readConfig } from '../utils/config.js';\nimport { writeRuntimeMarker } from '../utils/session-id.js';\nimport { captureProcessStartedAt } from '../utils/process-info.js';\nimport type { LaunchPlan } from './plan.js';\nimport type { TerminalChoice } from '../utils/config.js';\n\nexport class TerminalNotFoundError extends Error {\n readonly terminal: TerminalChoice;\n readonly remediation: string;\n constructor(terminal: TerminalChoice, remediation: string) {\n super(\n `Terminal \"${terminal}\" is not installed or not invokable. ${remediation}`,\n );\n this.terminal = terminal;\n this.remediation = remediation;\n this.name = 'TerminalNotFoundError';\n }\n}\n\n/**\n * Test hook: a function that replaces `child_process.spawn` so unit tests can\n * assert exactly what the launcher invoked without spawning real processes.\n * Must return a `ChildProcess`-shaped object — `executeLaunchPlan` listens for\n * `'error'`, `'spawn'`, and `'exit'` events to detect missing terminals.\n */\nexport type SpawnFn = (\n command: string,\n args: readonly string[],\n options: SpawnOptions,\n) => ChildProcess;\n\nconst realSpawn: SpawnFn = (command, args, options) =>\n spawn(command, args as string[], options);\n\n/**\n * Result of a launch — enough for the scheduler to acknowledge the launch\n * (poll for a runtime marker / session row attributable to it). For wrapper\n * terminals (osascript/open/sh) `pid` is the wrapper's pid, not the agent's;\n * the launch-ack scanner matches by cwd + write-time rather than pid alone.\n * Interactive callers ignore this value (the return was previously `void`).\n */\nexport interface LaunchHandle {\n pid: number | undefined;\n plan: LaunchPlan;\n /** ISO timestamp captured once the spawn settled. */\n startedAt: string;\n}\n\n/**\n * Commands we treat as \"wrappers\" that synchronously delegate to the actual\n * terminal app. These fail fast (non-zero exit + stderr) when the target app\n * or URL scheme isn't installed, so we monitor their exit code briefly.\n *\n * Membership is tested by BASENAME (see `isWrapperCommand`): `osascript`/`open`\n * are spawned by bare name, but cmux launches via an absolute `/bin/sh -c`\n * cold-start wrapper, so a plain set-membership check on the full command\n * (`/bin/sh`) would miss `'sh'` and wrongly classify it as a long-running\n * launcher (skipping exit-code monitoring).\n */\nconst WRAPPER_COMMANDS = new Set(['osascript', 'open', 'sh']);\n\n/**\n * A command is a wrapper if its basename is in `WRAPPER_COMMANDS`. Using the\n * basename lets cmux's absolute `/bin/sh` interpreter match `'sh'` while leaving\n * the bare `osascript`/`open` names (basename === name) unaffected.\n */\nfunction isWrapperCommand(command: string): boolean {\n return WRAPPER_COMMANDS.has(basename(command));\n}\n\n/**\n * How long we wait for a wrapper (osascript/open) to exit before assuming it\n * spawned the target app successfully and detaching. Wrappers that succeed\n * usually exit in tens of milliseconds; wrappers that fail exit even faster.\n * A small window keeps the CLI responsive without missing legitimate failures.\n *\n * Per-invocation override via `TerminalInvocation.wrapperTimeoutMs` — cmux needs\n * a larger window because its cold-start script can poll for socket readiness\n * for several seconds before it exits with the real success/failure code.\n */\nconst WRAPPER_EXIT_TIMEOUT_MS = 1500;\n\n/**\n * Run the launch plan: spawn the configured terminal in a new window with the\n * resolved cwd + agent argv. Returns once the spawn has been initiated and\n * confirmed; for wrapper commands (osascript/open) it briefly waits for the\n * wrapper to exit so that missing apps surface as a non-zero CLI exit.\n *\n * Throws `TerminalNotFoundError` when the spawn errors (ENOENT on direct CLI\n * launchers) or when a wrapper exits non-zero (target app missing).\n */\nexport async function executeLaunchPlan(\n plan: LaunchPlan,\n spawnFn: SpawnFn = realSpawn,\n): Promise<LaunchHandle> {\n if (plan.terminal === 'warp') {\n // Warp's URI scheme opens a window at the cwd but does not auto-start a\n // command — there is no documented `command=` parameter. Surface this so\n // the user knows to start the agent themselves once the window opens.\n console.error(\n `syntaur: Warp will open a window at ${plan.cwd} but cannot auto-start ${plan.argv.command} — run it yourself once the window appears`,\n );\n }\n const invocation = buildTerminalInvocation(plan);\n const isWrapper = isWrapperCommand(invocation.command);\n\n // Capture the launch timestamp at SPAWN time, not after the wrapper-exit wait:\n // a fast agent can write its real session marker before the wrapper safety-net\n // resolves, and launch-ack matches markers written at/after this instant.\n const startedAt = new Date().toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n\n let child: ChildProcess;\n try {\n child = spawnFn(invocation.command, invocation.args, {\n detached: true,\n // Wrappers: capture stderr so we can surface error text. Direct CLI\n // launchers: ignore all streams so they keep running after we detach.\n stdio: isWrapper ? ['ignore', 'ignore', 'pipe'] : 'ignore',\n env: plan.env,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new TerminalNotFoundError(\n plan.terminal,\n `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`,\n );\n }\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n let stderr = '';\n\n const finishOk = () => {\n if (settled) return;\n settled = true;\n try { child.unref(); } catch { /* unref can throw if already exited */ }\n resolve();\n };\n\n const finishErr = (remediation: string) => {\n if (settled) return;\n settled = true;\n reject(new TerminalNotFoundError(plan.terminal, remediation));\n };\n\n if (child.stderr) {\n child.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n }\n\n child.once('error', (err: Error) => {\n finishErr(\n `Spawn failed: ${err.message}. Verify the terminal is installed and on PATH.`,\n );\n });\n\n if (isWrapper) {\n child.once('exit', (code, signal) => {\n if (code === 0 || code === null) {\n finishOk();\n } else {\n const detail = stderr.trim() || (\n signal\n ? `terminated by signal ${signal}`\n : 'check that the terminal app is installed and the URL scheme handler is registered'\n );\n finishErr(`${invocation.command} exited with code ${code}: ${detail}`);\n }\n });\n // Safety net: if the wrapper hasn't exited within the window, assume\n // success and detach. This is the normal \"Terminal.app spawned, wrapper\n // is keeping the connection open\" case. cmux overrides the window\n // (wrapperTimeoutMs) so it exceeds the cold-start readiness poll —\n // otherwise a slow cold-start FAILURE would be masked as success here.\n setTimeout(\n finishOk,\n invocation.wrapperTimeoutMs ?? WRAPPER_EXIT_TIMEOUT_MS,\n ).unref();\n } else {\n child.once('spawn', () => {\n finishOk();\n });\n }\n });\n\n await registerLaunchAtBirth(plan, child);\n\n return { pid: child.pid, plan, startedAt };\n}\n\n/**\n * Register-at-birth: anything Syntaur spawns leaves a generic runtime marker\n * at `~/.syntaur/runtime/sessions/<pid>.json` regardless of the agent's hook\n * support, and — when the session id is already known (resume-mode launches) —\n * an active row in the sessions DB. Fresh/fork launches write a PENDING marker\n * (no sessionId; ids are never synthesized) and the scanner closes the gap on\n * its next tick. For wrapper terminals (osascript/open/sh) the pid is the\n * wrapper's, not the agent's — acceptable: the ancestor-walk in\n * `resolveOwnSessionId` tolerates intermediate pids and the scanner is the\n * guaranteed floor. Best-effort: never throws, never fails the launch.\n */\nasync function registerLaunchAtBirth(plan: LaunchPlan, child: ChildProcess): Promise<void> {\n try {\n const pid = child.pid;\n if (!pid) return;\n\n const autoTrack = (await readConfig()).session.autoTrack;\n if (autoTrack === 'off') return;\n\n const sessionId = plan.session?.sessionId ?? null;\n const procStart = captureProcessStartedAt(pid);\n\n const envDir = process.env.SYNTAUR_RUNTIME_SESSIONS_DIR;\n const markerDir = envDir && envDir.length > 0\n ? envDir\n : join(homedir(), '.syntaur', 'runtime', 'sessions');\n writeRuntimeMarker(\n pid,\n {\n ...(sessionId ? { sessionId } : {}),\n agent: plan.agentId,\n cwd: plan.cwd,\n ...(procStart ? { procStart } : {}),\n writtenAt: Date.now(),\n },\n markerDir,\n );\n\n if (!sessionId) return;\n if (\n autoTrack === 'workspaces-only' &&\n !(await fileExists(resolve(plan.cwd, '.syntaur', 'context.json')))\n ) {\n return;\n }\n\n const { initSessionDb } = await import('../dashboard/session-db.js');\n const { appendSession } = await import('../dashboard/agent-sessions.js');\n initSessionDb();\n await appendSession(\n '',\n {\n sessionId,\n projectSlug: null,\n assignmentSlug: null,\n agent: plan.agentId,\n started: new Date().toISOString(),\n status: 'active',\n path: plan.cwd,\n description: null,\n transcriptPath: null,\n pid,\n pidStartedAt: procStart,\n originalHeadSha: null,\n },\n // Resuming a stopped session IS live-process evidence — we just spawned it.\n { reviveStopped: true },\n );\n } catch {\n // Best-effort only — a tracking failure must never fail the launch.\n }\n}\n\ninterface TerminalInvocation {\n command: string;\n args: string[];\n /**\n * Override for the wrapper-exit safety-net window (ms). Set only by terminals\n * whose wrapper legitimately runs longer than `WRAPPER_EXIT_TIMEOUT_MS`\n * before exiting (cmux's cold-start readiness poll). Omitted = default.\n */\n wrapperTimeoutMs?: number;\n}\n\n/** cmux app bundle id, used to launch it on a cold start via `open -b`. */\nconst CMUX_BUNDLE_ID = 'com.cmuxterm.app';\n\n/**\n * Upper bound (ms) on cmux's cold-start readiness poll: CMUX_LAUNCH_SCRIPT tries\n * 20 times at 0.25s = 5s. Keep these two in sync if the script's loop changes.\n */\nconst CMUX_READINESS_MAX_MS = 20 * 250;\n\n/**\n * Wrapper safety-net window for cmux. Must exceed CMUX_READINESS_MAX_MS (plus\n * app-launch + workspace-create overhead) so that a cold-start failure exits\n * with its real non-zero code and surfaces as a TerminalNotFoundError, rather\n * than the safety net falsely resolving success mid-poll.\n */\nconst CMUX_LAUNCH_TIMEOUT_MS = CMUX_READINESS_MAX_MS + 3000;\n\n/**\n * POSIX-sh cold-start orchestration for cmux, run as a single monitored\n * `/bin/sh -c` spawn. `workspace create` is a socket-control command that needs\n * the cmux app running, so on a cold start (app closed) it would fail. This\n * script: (1) launches cmux if needed via `open -b` (a no-op when already\n * running; PATH-independent — `open` is at /usr/bin even under the applet's\n * stripped PATH); (2) polls `cmux ping` for socket readiness, bounded so it\n * never hangs; (3) `exec`s `workspace create` so its exit code is the script's\n * exit code (a failure surfaces as TerminalNotFoundError via the wrapper path).\n *\n * Values are passed as positional args ($1=cli, $2=cwd, $3=command) rather than\n * interpolated, so no second layer of shell-quoting is needed and a hostile cwd\n * or command cannot break out of the script.\n */\nconst CMUX_LAUNCH_SCRIPT = [\n `open -b ${CMUX_BUNDLE_ID} >/dev/null 2>&1 || true`,\n 'i=0',\n 'while [ \"$i\" -lt 20 ]; do',\n ' \"$1\" ping >/dev/null 2>&1 && break',\n ' i=$((i + 1))',\n ' sleep 0.25',\n 'done',\n 'exec \"$1\" workspace create --cwd \"$2\" --command \"$3\" --focus true',\n].join('\\n');\n\n/**\n * The agent command line with every token shell-quoted, WITHOUT a `cd` prefix:\n * `'<command>' '<arg>' …`. cmux uses this directly (it sets the workspace cwd\n * via `--cwd`, so it must not prepend `cd`); `buildShellCommandLine` adds the\n * `cd` for the terminals that drop the user into a shell.\n */\nexport function buildAgentCommandLine(plan: LaunchPlan): string {\n return [plan.argv.command, ...plan.argv.args].map(shellQuote).join(' ');\n}\n\n/**\n * Build the plain POSIX shell command line that actually runs inside the\n * terminal: `cd '<cwd>' && '<command>' '<arg>' …` with every token\n * shell-quoted. This is the single source of truth for \"the command the launch\n * button runs\" — consumed by `buildTerminalInvocation` (which wraps it per\n * terminal app) and by the dashboard's copy-launch-command endpoint. Exported\n * for reuse + unit testing.\n */\nexport function buildShellCommandLine(plan: LaunchPlan): string {\n return `cd ${shellQuote(plan.cwd)} && ${buildAgentCommandLine(plan)}`;\n}\n\n/**\n * Build the argv that will be handed to `spawn` to open `plan.argv` in a new\n * window of `plan.terminal` at `plan.cwd`. Exported for unit testing.\n */\nexport function buildTerminalInvocation(plan: LaunchPlan): TerminalInvocation {\n const cdAndRun = buildShellCommandLine(plan);\n\n switch (plan.terminal) {\n case 'terminal-app':\n // Terminal.app cold-start quirk: launching it auto-opens a blank window,\n // and `do script` opens ANOTHER — two windows, one blank. Capture the\n // running state BEFORE the `tell` block (addressing Terminal would launch\n // it), then on a cold start run the command in the blank launch window\n // instead of opening a second one. Warm starts still get a fresh window.\n return {\n command: 'osascript',\n args: [\n '-e',\n 'set wasRunning to application \"Terminal\" is running',\n '-e',\n 'tell application \"Terminal\"',\n '-e',\n 'activate',\n '-e',\n 'if wasRunning then',\n '-e',\n `do script ${appleScriptString(cdAndRun)}`,\n '-e',\n 'else',\n '-e',\n 'repeat until (count of windows) > 0',\n '-e',\n 'delay 0.1',\n '-e',\n 'end repeat',\n '-e',\n `do script ${appleScriptString(cdAndRun)} in window 1`,\n '-e',\n 'end if',\n '-e',\n 'end tell',\n ],\n };\n\n case 'iterm':\n // iTerm2's AppleScript dictionary uses the application name `iTerm` in\n // tell blocks (per https://iterm2.com/documentation-scripting.html),\n // even though the bundle id is `com.googlecode.iterm2`. If a future\n // iTerm release switches to \"iTerm2\", the doctor check's bundle-id\n // lookup will still succeed; only this script would need updating.\n return {\n command: 'osascript',\n args: [\n '-e',\n 'tell application \"iTerm\"',\n '-e',\n 'activate',\n '-e',\n 'set newWindow to (create window with default profile)',\n '-e',\n `tell current session of newWindow to write text ${appleScriptString(cdAndRun)}`,\n '-e',\n 'end tell',\n ],\n };\n\n case 'ghostty':\n // Ghostty's AppleScript dictionary doesn't actually expose\n // `new window` / `terminal` / `input text` / `send key` as usable\n // verbs at runtime — calls fail with \"Can't make new window into\n // integer\" / \"can't get terminal 1\". Drive Ghostty via synthesized\n // key events instead: activate the app, press Cmd-N for a new\n // window, type the command, then press Return.\n //\n // Requires Accessibility permission for the process that emits the\n // Apple Events (here: `osascript` itself). macOS will prompt the\n // first time this code path fires.\n return {\n command: 'osascript',\n args: [\n '-e',\n 'tell application \"Ghostty\" to activate',\n '-e',\n 'delay 0.3',\n '-e',\n 'tell application \"System Events\"',\n '-e',\n 'keystroke \"n\" using command down',\n '-e',\n 'delay 0.4',\n '-e',\n `keystroke ${appleScriptString(cdAndRun)}`,\n '-e',\n 'key code 36',\n '-e',\n 'end tell',\n ],\n };\n\n case 'alacritty':\n return {\n command: 'alacritty',\n args: [\n '--working-directory',\n plan.cwd,\n '-e',\n plan.argv.command,\n ...plan.argv.args,\n ],\n };\n\n case 'warp': {\n // Warp's URI scheme (https://docs.warp.dev/terminal/more-features/uri-scheme)\n // supports `warp://action/new_window?path=...` but does NOT accept a\n // `command=` param — the agent is not auto-started. `executeLaunchPlan`\n // emits a console.error warning above so the user knows to start the\n // agent manually once the Warp window appears. If a future Warp version\n // adds `command=` (or a documented alternative), update this branch\n // and drop the warning.\n const params = new URLSearchParams({ path: plan.cwd });\n return {\n command: 'open',\n args: [`warp://action/new_window?${params.toString()}`],\n };\n }\n\n case 'kitty':\n // Two-path strategy from the plan: prefer `kitty @ launch` when remote\n // control is enabled (gated by the doctor `terminal.kitty-remote-control`\n // check; if disabled the agent still gets launched, just via the\n // simpler path here). The `--` separator is required so `-`-prefixed\n // args like `--resume` reach the agent rather than kitty itself.\n return {\n command: 'kitty',\n args: [\n '--directory',\n plan.cwd,\n '--',\n plan.argv.command,\n ...plan.argv.args,\n ],\n };\n\n case 'cmux':\n // cmux is a socket-controlled workspace multiplexer driven by its\n // first-party CLI, which lives INSIDE the app bundle and is not on a\n // standard PATH dir. The macOS URL-handler applet launches with a\n // stripped LaunchServices PATH, so we resolve the CLI to an absolute path\n // (resolveCmuxCli: bundle → canonical dir → running-app via lsappinfo →\n // `which` off-darwin) rather than relying on a bare `cmux` (which would\n // ENOENT there). Canonical hits keep priority over the running-app lookup:\n // when a canonical copy exists but a different copy is running (e.g. off a\n // DMG), the canonical CLI still drives the running app over the shared\n // socket — fine while versions match, could skew after an update. Because\n // `workspace create` is\n // a socket command that needs the app running, we wrap it in a cold-start\n // `/bin/sh -c` script (CMUX_LAUNCH_SCRIPT): launch-if-needed via `open\n // -b`, await socket readiness, then `workspace create --cwd <cwd>\n // --command <cmd> --focus true` (which makes a workspace at --cwd and\n // sends the agent command text+Enter to it). The command is the bare\n // shell-quoted agent command (NO `cd` prefix — cmux sets the cwd via\n // --cwd) because cmux types it into the new workspace's shell. The /bin/sh\n // interpreter is registered in WRAPPER_COMMANDS (matched by basename\n // 'sh'), so a missing binary or dead socket surfaces as a\n // TerminalNotFoundError.\n return {\n command: '/bin/sh',\n args: [\n '-c',\n CMUX_LAUNCH_SCRIPT,\n 'syntaur-cmux-launch', // $0 (label in ps / error messages)\n resolveCmuxCli() ?? 'cmux', // $1\n plan.cwd, // $2\n buildAgentCommandLine(plan), // $3\n ],\n // Exceed the cold-start readiness poll so a failed cold launch surfaces\n // as an error instead of being masked by the wrapper safety net.\n wrapperTimeoutMs: CMUX_LAUNCH_TIMEOUT_MS,\n };\n }\n}\n\n/**\n * Quote a string for embedding inside an AppleScript double-quoted literal.\n * AppleScript interprets a literal backslash and a literal double-quote inside\n * \"...\" strings; everything else passes through.\n */\nfunction appleScriptString(value: string): string {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n","import { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { TerminalChoice } from './terminal-schema.js';\n\n/**\n * macOS bundle identifiers for Apple-Event-driven terminals. Used with\n * `mdfind kMDItemCFBundleIdentifier == '<id>'` to confirm install.\n */\nexport const APP_BUNDLE_IDS: Partial<Record<TerminalChoice, string>> = {\n 'terminal-app': 'com.apple.Terminal',\n iterm: 'com.googlecode.iterm2',\n ghostty: 'com.mitchellh.ghostty',\n warp: 'dev.warp.Warp-Stable',\n};\n\n/**\n * Standard `.app` bundle locations, used as a fallback when `mdfind` returns\n * nothing — `mdfind` exits 0 with empty stdout in non-indexed / launchd /\n * background contexts, falsely reporting an installed app as missing. For\n * non-system terminals these are bundle *names* resolved against the\n * applications directories; Terminal.app ships at a fixed system path.\n *\n * Keep in sync with `detectInstalledTerminals()` in\n * scripts/install-macos-url-handler.mjs — EXCEPT `cmux`, which is intentionally\n * present here but absent from `detectInstalledTerminals()`. That function lists\n * the *AppleScript-driven* terminals (the applet runs `tell application` blocks\n * for them); cmux is CLI-driven and falls through to `executeLaunchPlan`, so it\n * must not be added there. `cmux.app` is listed here only so `findAppBundle`\n * (and `resolveCmuxCli`) can locate the bundle that contains the cmux CLI.\n */\nexport const APP_BUNDLE_NAMES: Partial<Record<TerminalChoice, string>> = {\n iterm: 'iTerm.app',\n ghostty: 'Ghostty.app',\n warp: 'Warp.app',\n cmux: 'cmux.app',\n};\n\n/** Fixed absolute paths for apps not found under the applications dirs. */\nconst APP_FIXED_PATHS: Partial<Record<TerminalChoice, string>> = {\n 'terminal-app': '/System/Applications/Utilities/Terminal.app',\n};\n\n/** Default macOS application directories searched for `.app` bundles. */\nfunction defaultApplicationsDirs(): string[] {\n return ['/Applications', join(homedir(), 'Applications')];\n}\n\n/**\n * Find an installed `.app` bundle for a terminal by checking standard\n * locations on disk. Returns the absolute path to the bundle, or null. The\n * `dirs` parameter is injectable so tests can point at a temp directory\n * instead of the host's real /Applications.\n */\nexport function findAppBundle(\n terminal: TerminalChoice,\n dirs: string[] = defaultApplicationsDirs(),\n): string | null {\n const fixed = APP_FIXED_PATHS[terminal];\n if (fixed && existsSync(fixed)) return fixed;\n\n const bundleName = APP_BUNDLE_NAMES[terminal];\n if (bundleName) {\n for (const dir of dirs) {\n const candidate = join(dir, bundleName);\n if (existsSync(candidate)) return candidate;\n }\n }\n return null;\n}\n\n/**\n * CLI names for shell-out-driven terminals. Used with `which <name>` to\n * confirm install on PATH.\n */\nexport const CLI_NAMES: Partial<Record<TerminalChoice, string>> = {\n alacritty: 'alacritty',\n kitty: 'kitty',\n // cmux is CLI-driven, but its CLI lives inside the app bundle (not on a\n // standard PATH dir), so detection uses `resolveCmuxCli` rather than a bare\n // `which cmux`. The entry here is for doctor messaging (e.g. \"resolved cmux\n // → <path>\") and to document cmux as CLI-driven.\n cmux: 'cmux',\n};\n\n/**\n * Canonical absolute directories a cmux CLI symlink/binary may live in, checked\n * by `existsSync` (PATH-independent) so resolution agrees between the dashboard\n * server (full PATH) and the macOS applet (stripped PATH). Exported for tests.\n */\nexport const CMUX_CLI_DIRS: readonly string[] = [\n '/usr/local/bin',\n '/opt/homebrew/bin',\n];\n\n/**\n * Queries LaunchServices for the bundle path of the RUNNING cmux app. Returns\n * the raw `lsappinfo` stdout, or null when the query fails or cmux is not\n * running. Injectable (see `resolveCmuxCli`) so tests never shell out to the\n * host's real `/usr/bin/lsappinfo`.\n */\nexport type CmuxLsappinfoRunner = () => string | null;\n\n/**\n * Default running-app lookup: ask LaunchServices where the running cmux lives.\n * Invokes `/usr/bin/lsappinfo` by ABSOLUTE path so it resolves under the\n * `syntaur://` URL-handler applet's stripped LaunchServices PATH\n * (`/usr/bin:/bin:/usr/sbin:/sbin`), where a bare `lsappinfo` is unresolvable.\n * Returns stdout on a clean exit, else null (non-zero exit / spawn failure).\n */\nconst defaultCmuxLsappinfoRunner: CmuxLsappinfoRunner = () => {\n const result = spawnSync(\n '/usr/bin/lsappinfo',\n ['info', '-only', 'bundlepath', 'com.cmuxterm.app'],\n { encoding: 'utf-8' },\n );\n return result.status === 0 ? result.stdout : null;\n};\n\n/**\n * Absolute path to the cmux CLI (inside the app bundle, a canonical install\n * dir, the running app's bundle, or on PATH), or null when cmux is not\n * installed.\n *\n * cmux is controlled by a first-party CLI that ships *inside* the app bundle at\n * `Contents/Resources/bin/cmux` and is NOT on any standard PATH dir. The macOS\n * `syntaur://` URL-handler applet, which drives the production \"Open in agent\"\n * flow, launches with a stripped LaunchServices PATH\n * (`/usr/bin:/bin:/usr/sbin:/sbin`), where a bare `cmux` is unresolvable. So\n * BOTH detection (this probe) and launch (`buildTerminalInvocation`) must\n * resolve an absolute path; sharing one resolver keeps server-side preflight\n * and the actual applet launch consistent.\n *\n * Resolution order, chosen so resolution is PATH-independent on macOS (where the\n * applet launch is the only stripped-PATH context) — server preflight and the\n * applet then agree by construction:\n * 1. the bundle CLI (`findAppBundle` → `Contents/Resources/bin/cmux`)\n * 2. a canonical install dir (`CMUX_CLI_DIRS`, via `existsSync` — covers a\n * Homebrew/`/usr/local` symlink even under the applet's stripped PATH)\n * 3. the RUNNING app via LaunchServices (`/usr/bin/lsappinfo info -only\n * bundlepath com.cmuxterm.app` → `<bundle>/Contents/Resources/bin/cmux`),\n * darwin-only. This catches a cmux launched from a non-canonical location\n * (e.g. straight off a mounted DMG at `/Volumes/cmux/cmux.app`) that misses\n * every fixed location above. Steps 1–2 keep priority — when a canonical\n * copy exists but a DIFFERENT copy is running, the canonical CLI drives the\n * running app over the shared control socket: fine while versions match\n * (both 0.64.13 today), could skew after an update. That caveat is\n * documented, not guarded at runtime. Not-running + non-canonical\n * legitimately stays unresolved (a cold start from nowhere is unlaunchable)\n * and surfaces as not-installed.\n * 4. `which cmux` — PATH-DEPENDENT, so it is consulted ONLY off macOS. On\n * darwin we deliberately skip it: a cmux reachable only via a non-canonical\n * PATH dir would pass the full-PATH server preflight but be invisible to\n * the stripped-PATH applet, so accepting it would be a false positive.\n * Skipping it makes macOS detection consistent with the applet (real macOS\n * installs are the .app bundle, or now the running-app lookup above). Off\n * macOS there is no stripped-PATH applet, so launch and preflight share the\n * same PATH.\n *\n * `applicationsDirsOverride` / `cliDirsOverride` / `lsappinfoRunnerOverride`\n * REPLACE the defaults so tests stay hermetic from the host's real\n * `/Applications`, `/usr/local/bin`, and `/usr/bin/lsappinfo`.\n */\nexport function resolveCmuxCli(\n applicationsDirsOverride?: string[],\n cliDirsOverride?: string[],\n lsappinfoRunnerOverride?: CmuxLsappinfoRunner,\n): string | null {\n const bundle = findAppBundle('cmux', applicationsDirsOverride);\n if (bundle) {\n const cli = join(bundle, 'Contents/Resources/bin/cmux');\n if (existsSync(cli)) return cli;\n }\n for (const dir of cliDirsOverride ?? CMUX_CLI_DIRS) {\n const cli = join(dir, 'cmux');\n if (existsSync(cli)) return cli;\n }\n if (process.platform === 'darwin') {\n // Running-app fallback: a cmux launched from a non-canonical location is\n // invisible to the fixed checks above. Ask LaunchServices where it lives.\n const runner = lsappinfoRunnerOverride ?? defaultCmuxLsappinfoRunner;\n const stdout = runner();\n if (stdout) {\n // lsappinfo emits e.g. ` \"LSBundlePath\"=\"/Volumes/cmux/cmux.app\"\\n` with\n // leading whitespace and a trailing newline — do NOT anchor the match.\n const match = stdout.match(/\"LSBundlePath\"=\"([^\"]+)\"/);\n if (match) {\n const cli = join(match[1], 'Contents/Resources/bin/cmux');\n if (existsSync(cli)) return cli;\n }\n }\n // darwin deliberately skips `which` (see step 4) — fall through to null.\n return null;\n }\n const which = spawnSync('which', ['cmux'], { encoding: 'utf-8' });\n if (which.status === 0 && which.stdout.trim().length > 0) {\n return which.stdout.trim();\n }\n return null;\n}\n\nexport interface ProbeResult {\n ok: boolean;\n /** Absolute path to the .app bundle or CLI binary, when found. */\n foundPath?: string;\n /** Why the probe returned ok:false. */\n reason?: 'not-installed' | 'no-probe-available';\n}\n\n/**\n * Probe whether a terminal is installed on this machine, using the same\n * primitives as the doctor `terminal.installed` check:\n * - `mdfind` for Apple-Event terminals registered with LaunchServices\n * - `which` for CLI terminals on PATH\n *\n * Returns `{ ok: false, reason: 'no-probe-available' }` when the terminal id\n * has no entry in either map — this should be impossible for known\n * `TerminalChoice` values but lets callers handle a future terminal addition\n * gracefully.\n */\nexport function probeTerminalInstalled(\n terminal: TerminalChoice,\n /**\n * `applicationsDirsOverride` REPLACES (does not extend) the default\n * applications directories for the `.app` fallback. Production never sets it;\n * it exists so tests can point the fallback at a temp dir and stay isolated\n * from the host's real /Applications (merging with the defaults would make a\n * host that actually has the app produce a false positive).\n */\n opts: {\n applicationsDirsOverride?: string[];\n /** REPLACES `CMUX_CLI_DIRS` for the cmux resolver; tests only. */\n cmuxCliDirsOverride?: string[];\n /** REPLACES the default `/usr/bin/lsappinfo` runner; tests only. */\n cmuxLsappinfoRunnerOverride?: CmuxLsappinfoRunner;\n } = {},\n): ProbeResult {\n // cmux is special-cased before the generic bundle-id / CLI-name paths: its\n // control CLI lives inside the app bundle and is not on a standard PATH dir,\n // so neither the `mdfind` bundle-id path (cmux is deliberately absent from\n // APP_BUNDLE_IDS — it would resolve the `.app`, not the CLI) nor a bare\n // `which cmux` is correct. `resolveCmuxCli` finds the bundle CLI (or canonical\n // dir / PATH fallback) and is the same resolver `buildTerminalInvocation`\n // uses, so detection and launch agree under a stripped PATH.\n if (terminal === 'cmux') {\n const cli = resolveCmuxCli(\n opts.applicationsDirsOverride,\n opts.cmuxCliDirsOverride,\n opts.cmuxLsappinfoRunnerOverride,\n );\n return cli\n ? { ok: true, foundPath: cli }\n : { ok: false, reason: 'not-installed' };\n }\n\n const bundleId = APP_BUNDLE_IDS[terminal];\n if (bundleId) {\n const result = spawnSync(\n 'mdfind',\n [`kMDItemCFBundleIdentifier == '${bundleId}'`],\n { encoding: 'utf-8' },\n );\n if (result.status === 0 && result.stdout.trim().length > 0) {\n return { ok: true, foundPath: result.stdout.trim().split('\\n')[0] };\n }\n // `mdfind` yielded no path. This covers BOTH a non-zero exit AND an exit 0\n // with empty stdout (Spotlight not indexing, e.g. background/launchd\n // contexts). Fall back to the standard `.app` locations before declaring\n // the terminal not installed.\n const bundlePath = findAppBundle(terminal, opts.applicationsDirsOverride);\n if (bundlePath) {\n return { ok: true, foundPath: bundlePath };\n }\n return { ok: false, reason: 'not-installed' };\n }\n\n const cliName = CLI_NAMES[terminal];\n if (cliName) {\n const result = spawnSync('which', [cliName], { encoding: 'utf-8' });\n if (result.status === 0 && result.stdout.trim().length > 0) {\n return { ok: true, foundPath: result.stdout.trim() };\n }\n return { ok: false, reason: 'not-installed' };\n }\n\n return { ok: false, reason: 'no-probe-available' };\n}\n","/**\n * Resolve the *real* agent session id for the currently-running process.\n *\n * Session identity is an ambient property of the running process — it must\n * never be looked up from shared mutable state (`.syntaur/context.json`'s\n * `sessionId` scalar), because a co-tenant sharing the same workspace clobbers\n * that scalar and a long-lived session would then read the *wrong* id.\n *\n * `resolveOwnSessionId` returns the first non-empty hit across six layers,\n * ordered by trustworthiness:\n * 1. explicit `--session-id` override (`opts.sessionId`)\n * 2. injected env var: CLAUDE_CODE_SESSION_ID / OPENCODE_SESSION_ID / PI_SESSION_ID\n * 3. agent side channel (Cursor nonce → conversation_id; seam, see Phase E)\n * 4. ancestor-pid → runtime marker (`~/.claude/sessions/<pid>.json`,\n * then `~/.syntaur/runtime/sessions/<pid>.json`), pid-reuse-guarded\n * 5. cwd/mtime transcript scan (last automatic resort; ambiguous under\n * co-tenancy — same caveat as platforms/codex/scripts/resolve-session.sh)\n * 6. legacy hint (`opts.legacyHint`, i.e. the context.json scalar)\n *\n * Callers that must stay *exact* (the Codex/Claude cleanup paths and the\n * `session resolve-id` subcommand) simply omit `opts.legacyHint`, so they never\n * re-introduce the clobbered scalar. Identity-with-fallback callers\n * (`session save`) pass `legacyHint: ctx?.sessionId`.\n *\n * The function is `async` because layer 5 delegates to `cwd-extractor` file I/O;\n * layers 1, 2, and 4 are effectively synchronous.\n *\n * All process/env/fs touch points are injectable via `ResolverDeps` so unit\n * tests can drive every layer deterministically (mirrors the `LivenessDeps`\n * pattern in `src/dashboard/session-liveness.ts`).\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { captureProcessStartedAt } from './process-info.js';\nimport { walkClaudeProjects, walkCodexSessions } from '../usage/cwd-extractor.js';\n\n/** Env vars (in precedence order) that agent runtimes inject with the real id. */\nexport const SESSION_ID_ENV_VARS = [\n 'CLAUDE_CODE_SESSION_ID',\n 'OPENCODE_SESSION_ID',\n 'PI_SESSION_ID',\n] as const;\n\n// Resolved ids become filesystem path segments (sessions/<id>/summary.md) and\n// URL path segments in the cleanup hooks. Now that ids come from widened sources\n// (env vars, ancestor markers, transcript scans), validate them so a malformed\n// value can't traverse out of the sessions dir or inject into a URL. Real agent\n// ids are UUIDs/ULIDs — alphanumerics, hyphens, underscores — so this is strict\n// but never rejects a legitimate id. Invalid candidates are treated as a miss\n// and the resolver falls through to the next layer.\nconst SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;\nexport function isSafeSessionId(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= 256 && SAFE_SESSION_ID.test(value);\n}\n\n/**\n * Shape of a per-process runtime marker file at\n * `<claudeSessionsDir | runtimeSessionsDir>/<pid>.json`. Claude Code writes its\n * native `~/.claude/sessions/<pid>.json` in (a superset of) this shape; the\n * generic `~/.syntaur/runtime/sessions/<pid>.json` is written by a\n * capture-at-birth hook for agents that learn the real id but cannot inject env.\n * Extra fields are tolerated. `sessionId` may be absent on a PENDING marker —\n * written at launch time before the agent has minted its real id (fresh/fork\n * launches); `readRuntimeMarker` rejects those, so pending markers never\n * resolve ids until something backfills them.\n */\nexport interface RuntimeSessionMarker {\n sessionId?: string;\n agent?: string;\n cwd?: string;\n /** `ps -o lstart=`-style start time, used to guard against pid reuse. */\n procStart?: string;\n writtenAt?: number;\n}\n\n/** Injectable dependencies; production callers pass nothing. */\nexport interface ResolverDeps {\n /** Environment to read layer-2 vars from. Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /** Where the ancestor walk starts. Defaults to `process.ppid` (the agent). */\n startPid?: number;\n /** Home directory for the default marker dirs. Defaults to `os.homedir()`. */\n homeDir?: string;\n /** Returns the parent pid of `pid`, or null. Defaults to `ps -o ppid=`. */\n readPpid?: (pid: number) => number | null;\n /** Returns the start-time of `pid`, or null. Defaults to `captureProcessStartedAt`. */\n pidStartedAt?: (pid: number) => string | null;\n /** Returns a file's mtime in ms, or null. Defaults to `statSync(path).mtimeMs`. */\n statMtimeMs?: (path: string) => number | null;\n /** Claude's native marker dir. Defaults to `<home>/.claude/sessions`. */\n claudeSessionsDir?: string;\n /** Generic agent-neutral marker dir. Defaults to `<home>/.syntaur/runtime/sessions`. */\n runtimeSessionsDir?: string;\n /** Max ancestor-chain depth to walk. Defaults to 12. */\n maxDepth?: number;\n}\n\nexport interface ResolveSessionOpts {\n /** Explicit override (layer 1). */\n sessionId?: string;\n /** Working directory for the layer-5 transcript scan. */\n cwd?: string;\n /** Legacy `context.json.sessionId` hint (layer 6). Omit to stay exact-only. */\n legacyHint?: string | null;\n}\n\n/** Parent pid of `pid` via `ps -o ppid=`, or null. Exported for callers that\n * need the hook-equivalent \"shell that owns the agent\" fallback pid. */\nexport function readPpid(pid: number): number | null {\n if (!Number.isFinite(pid) || pid <= 1) return null;\n try {\n const out = execFileSync('ps', ['-o', 'ppid=', '-p', String(pid)], {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n const parent = Number.parseInt(out.trim(), 10);\n return Number.isInteger(parent) && parent > 0 ? parent : null;\n } catch {\n return null;\n }\n}\n\nfunction defaultStatMtimeMs(path: string): number | null {\n try {\n return statSync(path).mtimeMs;\n } catch {\n return null;\n }\n}\n\n/** Read + validate a runtime marker for `pid` under `dir`. Returns null on any miss. */\nexport function readRuntimeMarker(pid: number, dir: string): RuntimeSessionMarker | null {\n if (!Number.isInteger(pid) || pid <= 0) return null;\n const path = join(dir, `${pid}.json`);\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));\n if (\n parsed &&\n typeof parsed === 'object' &&\n typeof (parsed as Record<string, unknown>).sessionId === 'string' &&\n ((parsed as Record<string, unknown>).sessionId as string).length > 0\n ) {\n return parsed as RuntimeSessionMarker;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** Write a generic runtime marker for `pid` (used by tests and capture-at-birth). */\nexport function writeRuntimeMarker(pid: number, marker: RuntimeSessionMarker, dir: string): void {\n const path = join(dir, `${pid}.json`);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, JSON.stringify(marker));\n}\n\n/**\n * Layer 3 seam — agent side channels that key on a per-invocation nonce rather\n * than cwd (so they stay co-tenant-safe). Cursor's nonce→conversation_id\n * handshake plugs in here (Phase E). Returns undefined until implemented.\n */\nasync function resolveSideChannelSessionId(\n _opts: ResolveSessionOpts,\n _deps: ResolverDeps,\n): Promise<string | undefined> {\n return undefined;\n}\n\n/** Layer 4 — walk the ancestor-pid chain, returning the nearest valid marker's id. */\nfunction resolveFromAncestorMarkers(\n startPid: number,\n claudeSessionsDir: string,\n runtimeSessionsDir: string,\n readPpid: (pid: number) => number | null,\n pidStartedAt: (pid: number) => string | null,\n maxDepth: number,\n): string | undefined {\n let pid = startPid;\n for (let depth = 0; depth < maxDepth; depth += 1) {\n if (!Number.isInteger(pid) || pid <= 1) break;\n for (const dir of [claudeSessionsDir, runtimeSessionsDir]) {\n // Read the marker FIRST; only probe `ps` for the pid-reuse guard when a\n // marker actually exists (avoids a `ps` call per level on empty levels).\n const marker = readRuntimeMarker(pid, dir);\n if (!marker) continue;\n if (marker.procStart) {\n // Fail CLOSED: a recorded procStart must be PROVEN to still match. If we\n // can't read the live start time, we can't prove the pid wasn't recycled\n // (a stale marker for a reused pid), so skip rather than trust it.\n const actual = pidStartedAt(pid);\n if (!actual || actual !== marker.procStart) continue;\n }\n if (isSafeSessionId(marker.sessionId)) return marker.sessionId;\n }\n const parent = readPpid(pid);\n if (parent === null) break;\n pid = parent;\n }\n return undefined;\n}\n\n/** Layer 5 — scan transcripts for `cwd`, pick the most-recently-written. */\nasync function resolveFromCwdScan(\n cwd: string,\n statMtimeMs: (path: string) => number | null,\n): Promise<string | undefined> {\n const candidates: Array<{ sessionId: string; mtime: number }> = [];\n for await (const meta of walkClaudeProjects()) {\n if (meta.cwd === cwd && isSafeSessionId(meta.sessionId)) {\n candidates.push({ sessionId: meta.sessionId, mtime: statMtimeMs(meta.path) ?? 0 });\n }\n }\n for await (const meta of walkCodexSessions()) {\n if (meta.cwd === cwd && isSafeSessionId(meta.sessionId)) {\n candidates.push({ sessionId: meta.sessionId, mtime: statMtimeMs(meta.path) ?? 0 });\n }\n }\n if (candidates.length === 0) return undefined;\n // Deterministic: newest mtime wins; ties broken by sessionId descending.\n candidates.sort((a, b) => b.mtime - a.mtime || (a.sessionId < b.sessionId ? 1 : a.sessionId > b.sessionId ? -1 : 0));\n return candidates[0].sessionId;\n}\n\nexport async function resolveOwnSessionId(\n opts: ResolveSessionOpts = {},\n deps: ResolverDeps = {},\n): Promise<string | undefined> {\n // Layer 1 — explicit override.\n if (isSafeSessionId(opts.sessionId)) return opts.sessionId;\n\n // Layer 2 — injected env var (clobber-proof, per-process).\n const env = deps.env ?? process.env;\n for (const key of SESSION_ID_ENV_VARS) {\n const value = env[key];\n if (isSafeSessionId(value)) return value;\n }\n\n // Layer 3 — agent side channel (seam).\n const sideChannel = await resolveSideChannelSessionId(opts, deps);\n if (sideChannel) return sideChannel;\n\n // Layer 4 — ancestor-pid runtime marker.\n const home = deps.homeDir ?? homedir();\n const claudeSessionsDir = deps.claudeSessionsDir ?? join(home, '.claude', 'sessions');\n const runtimeSessionsDir = deps.runtimeSessionsDir ?? join(home, '.syntaur', 'runtime', 'sessions');\n const startPid = deps.startPid ?? process.ppid;\n const fromMarker = resolveFromAncestorMarkers(\n startPid,\n claudeSessionsDir,\n runtimeSessionsDir,\n deps.readPpid ?? readPpid,\n deps.pidStartedAt ?? captureProcessStartedAt,\n deps.maxDepth ?? 12,\n );\n if (fromMarker) return fromMarker;\n\n // Layer 5 — cwd/mtime transcript scan (last automatic resort).\n if (opts.cwd) {\n const fromScan = await resolveFromCwdScan(opts.cwd, deps.statMtimeMs ?? defaultStatMtimeMs);\n if (fromScan) return fromScan;\n }\n\n // Layer 6 — legacy context.json hint (only when the caller opts in).\n if (isSafeSessionId(opts.legacyHint)) return opts.legacyHint;\n\n return undefined;\n}\n","import { execFileSync } from 'node:child_process';\n\n/**\n * Capture the start-time of a process via `ps -o lstart=`. Used as the\n * recycling-defense baseline for session liveness detection: the server later\n * compares this stored start-time to the current `ps -o lstart=` output, so a\n * recycled PID with the same number but different start-time correctly\n * reports as not-live.\n *\n * Returns null when `ps` fails (process already gone, or `ps` not on PATH).\n * Null is the expected sentinel for \"no recycling baseline available\" — the\n * liveness check trusts `kill -0` alone in that case (small false-positive\n * risk on PID reuse, acceptable).\n */\nexport function captureProcessStartedAt(pid: number): string | null {\n if (!Number.isFinite(pid) || pid <= 0) return null;\n try {\n const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n const trimmed = out.trim();\n return trimmed === '' ? null : trimmed;\n } catch {\n return null;\n }\n}\n","/**\n * Session metadata extractor.\n *\n * Reads Claude Code, Codex, and Pi JSONL session files to produce\n * `(sessionId, cwd, startTs, endTs)` tuples for use in the attribution\n * join. Mutates nothing; touches no DB.\n *\n * - Claude Code: `~/.claude/projects/<cwd-slug>/<session-id>.jsonl`. The\n * directory name is treated as opaque (slug decoding is unsafe because\n * legitimate directory names can contain `-`). `cwd` is read from inside\n * the transcript via the existing `derivePathFromTranscript` utility.\n * `sessionId` is the basename without `.jsonl`.\n *\n * - Codex: `<sessions-root>/YYYY/MM/DD/rollout-*.jsonl` (or flat at the\n * sessions-root for older Codex versions). Line 1 is a `session_meta`\n * envelope with `{type, timestamp, payload:{id, cwd, ...}}` — `timestamp`\n * is at the TOP LEVEL (verified against\n * `src/__tests__/codex-resolve-session.test.ts:30-34`), NOT inside\n * `payload`. Sessions root resolves via:\n * CODEX_SESSIONS_DIR\n * ?? path.join(CODEX_HOME, 'sessions')\n * ?? ~/.codex/sessions\n *\n * - Pi: `<sessions-root>/<encoded-cwd>/<ts>_<uuid>.jsonl`. Line 1 is a\n * session-start envelope `{type, version, id, timestamp, cwd}`. The\n * sessionId is the UUID suffix of the filename (after the last `_`,\n * `.jsonl` stripped). Sessions root resolves via:\n * PI_AGENT_DIR (treated as Pi home) → <PI_AGENT_DIR>/sessions\n * ?? ~/.pi/agent/sessions\n */\n\nimport { open, readdir, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { expandHome } from '../utils/paths.js';\nimport { derivePathFromTranscript } from '../utils/transcript.js';\n\nconst SCAN_LINE_CAP = 50;\nconst TAIL_READ_BYTES = 8 * 1024;\nconst TAIL_READ_BYTES_MAX = 64 * 1024;\n\nexport interface ClaudeSessionMeta {\n tool: 'claude';\n sessionId: string;\n cwd: string;\n startTs: string | null;\n endTs: string | null;\n /** Absolute path to the transcript file (for mtime-based ordering). */\n path: string;\n}\n\nexport interface CodexSessionMeta {\n tool: 'codex';\n sessionId: string;\n cwd: string;\n startTs: string;\n endTs: string;\n /** Absolute path to the rollout file (for mtime-based ordering). */\n path: string;\n}\n\nexport interface PiSessionMeta {\n tool: 'pi';\n sessionId: string;\n cwd: string;\n startTs: string | null;\n endTs: string | null;\n /** Absolute path to the transcript file (for mtime-based ordering). */\n path: string;\n}\n\nexport type SessionMeta = ClaudeSessionMeta | CodexSessionMeta | PiSessionMeta;\n\n// --- Claude Code ----------------------------------------------------------\n\n/**\n * Extract session metadata from a Claude Code transcript file. Returns\n * `null` when the file is unreadable, has no `cwd`, or fails to parse.\n */\nexport async function extractClaudeSessionMeta(\n jsonlPath: string,\n): Promise<ClaudeSessionMeta | null> {\n const cwd = await derivePathFromTranscript(jsonlPath);\n if (!cwd) return null;\n\n const basename = jsonlPath.split('/').pop() ?? '';\n const sessionId = basename.replace(/\\.jsonl$/, '');\n if (!sessionId) return null;\n\n const startTs = await readFirstTimestamp(jsonlPath);\n const endTs = await readLastTimestamp(jsonlPath);\n\n return {\n tool: 'claude',\n sessionId,\n cwd,\n startTs,\n endTs,\n path: jsonlPath,\n };\n}\n\n// --- Codex ----------------------------------------------------------------\n\n/**\n * Extract session metadata from a Codex rollout file. Returns `null` if line\n * 1 isn't a valid `session_meta` envelope.\n */\nexport async function extractCodexSessionMeta(\n jsonlPath: string,\n): Promise<CodexSessionMeta | null> {\n let handle;\n try {\n handle = await open(jsonlPath, 'r');\n } catch {\n return null;\n }\n try {\n const stream = handle.createReadStream({ encoding: 'utf-8' });\n let buffer = '';\n let firstLine: string | null = null;\n for await (const chunk of stream) {\n buffer += chunk;\n const nl = buffer.indexOf('\\n');\n if (nl !== -1) {\n firstLine = buffer.slice(0, nl);\n stream.destroy();\n break;\n }\n }\n if (!firstLine && buffer.length > 0) firstLine = buffer;\n if (!firstLine) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(firstLine);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object') return null;\n\n const obj = parsed as Record<string, unknown>;\n if (obj.type !== 'session_meta') return null;\n\n const timestamp = typeof obj.timestamp === 'string' ? obj.timestamp : null;\n const payload = obj.payload as Record<string, unknown> | undefined;\n const id = payload && typeof payload.id === 'string' ? payload.id : null;\n const cwd = payload && typeof payload.cwd === 'string' ? payload.cwd : null;\n\n if (!timestamp || !id || !cwd) return null;\n\n const endTs = (await readLastTimestamp(jsonlPath)) ?? timestamp;\n\n return {\n tool: 'codex',\n sessionId: id,\n cwd,\n startTs: timestamp,\n endTs,\n path: jsonlPath,\n };\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\n// --- Walkers --------------------------------------------------------------\n\n/**\n * Yield session metadata for every Claude Code transcript under `root`\n * (default `~/.claude/projects`). One `cwd` is cached per directory after\n * the first session in it produces a hit — every Claude session under a\n * `<cwd-slug>` directory launched from the same cwd.\n *\n * Optional `sinceMtimeMs` bounds the walk to files modified at or after\n * the given epoch ms (matching the CLI's first-run 30-day window).\n */\nexport async function* walkClaudeProjects(opts: {\n root?: string;\n sinceMtimeMs?: number;\n} = {}): AsyncGenerator<ClaudeSessionMeta> {\n const root = expandHome(opts.root ?? '~/.claude/projects');\n const dirs = await listDirSafe(root);\n for (const dirent of dirs) {\n if (!dirent.isDirectory) continue;\n const dirPath = join(root, dirent.name);\n const files = await listDirSafe(dirPath);\n let cachedCwd: string | null = null;\n for (const f of files) {\n if (!f.isFile || !f.name.endsWith('.jsonl')) continue;\n const filePath = join(dirPath, f.name);\n if (opts.sinceMtimeMs !== undefined) {\n const mtime = await mtimeMs(filePath);\n if (mtime !== null && mtime < opts.sinceMtimeMs) continue;\n }\n let meta: ClaudeSessionMeta | null;\n if (cachedCwd) {\n // Still need timestamps + sessionId from this file.\n const sessionId = f.name.replace(/\\.jsonl$/, '');\n const startTs = await readFirstTimestamp(filePath);\n const endTs = await readLastTimestamp(filePath);\n meta = { tool: 'claude', sessionId, cwd: cachedCwd, startTs, endTs, path: filePath };\n } else {\n meta = await extractClaudeSessionMeta(filePath);\n if (meta) cachedCwd = meta.cwd;\n }\n if (meta) yield meta;\n }\n }\n}\n\n/**\n * Yield session metadata for every Codex rollout file under the resolved\n * sessions root.\n */\nexport async function* walkCodexSessions(opts: {\n root?: string;\n sinceMtimeMs?: number;\n} = {}): AsyncGenerator<CodexSessionMeta> {\n const root = resolveCodexSessionsRoot(opts.root);\n for await (const filePath of walkJsonlRecursive(root)) {\n const basename = filePath.split('/').pop() ?? '';\n // Codex names files like `rollout-*.jsonl`; tolerate but prefer that prefix.\n if (!basename.endsWith('.jsonl')) continue;\n if (opts.sinceMtimeMs !== undefined) {\n const mtime = await mtimeMs(filePath);\n if (mtime !== null && mtime < opts.sinceMtimeMs) continue;\n }\n const meta = await extractCodexSessionMeta(filePath);\n if (meta) yield meta;\n }\n}\n\nexport function resolveCodexSessionsRoot(override?: string): string {\n if (override) return expandHome(override);\n const fromSessionsEnv = process.env.CODEX_SESSIONS_DIR;\n if (fromSessionsEnv && fromSessionsEnv.length > 0) return expandHome(fromSessionsEnv);\n const fromHomeEnv = process.env.CODEX_HOME;\n if (fromHomeEnv && fromHomeEnv.length > 0) return join(expandHome(fromHomeEnv), 'sessions');\n return join(homedir(), '.codex', 'sessions');\n}\n\n// --- Pi -------------------------------------------------------------------\n\n/**\n * Extract session metadata from a Pi agent transcript file. Returns `null`\n * when the file is unreadable, has no `cwd` on line 1, or fails to parse.\n *\n * Filename format: `<ts>_<uuid>.jsonl` — `sessionId` is the UUID suffix\n * (after the last `_`, with `.jsonl` stripped).\n * Line 1 format: `{type, version, id, timestamp, cwd}`.\n */\nexport async function extractPiSessionMeta(\n jsonlPath: string,\n): Promise<PiSessionMeta | null> {\n // Derive sessionId from filename: substring after last '_', strip '.jsonl'.\n const basename = jsonlPath.split('/').pop() ?? '';\n const underscoreIdx = basename.lastIndexOf('_');\n if (underscoreIdx === -1) return null;\n const sessionId = basename.slice(underscoreIdx + 1).replace(/\\.jsonl$/, '');\n if (!sessionId) return null;\n\n // Read cwd from first line.\n let handle;\n try {\n handle = await open(jsonlPath, 'r');\n } catch {\n return null;\n }\n try {\n const stream = handle.createReadStream({ encoding: 'utf-8' });\n let buffer = '';\n let firstLine: string | null = null;\n for await (const chunk of stream) {\n buffer += chunk;\n const nl = buffer.indexOf('\\n');\n if (nl !== -1) {\n firstLine = buffer.slice(0, nl);\n stream.destroy();\n break;\n }\n }\n if (!firstLine && buffer.length > 0) firstLine = buffer;\n if (!firstLine) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(firstLine);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object') return null;\n const obj = parsed as Record<string, unknown>;\n if (typeof obj.cwd !== 'string' || obj.cwd.length === 0) return null;\n const cwd = obj.cwd;\n\n const startTs = await readFirstTimestamp(jsonlPath);\n const endTs = await readLastTimestamp(jsonlPath);\n\n return {\n tool: 'pi',\n sessionId,\n cwd,\n startTs,\n endTs,\n path: jsonlPath,\n };\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\nexport function resolvePiSessionsRoot(override?: string): string {\n if (override) return expandHome(override);\n const fromHomeEnv = process.env.PI_AGENT_DIR;\n if (fromHomeEnv && fromHomeEnv.length > 0) return join(expandHome(fromHomeEnv), 'sessions');\n return join(homedir(), '.pi', 'agent', 'sessions');\n}\n\n/**\n * Yield session metadata for every Pi agent transcript under `root`\n * (default `~/.pi/agent/sessions`). Pi organises files as\n * `<root>/<encoded-cwd>/<ts>_<uuid>.jsonl`.\n *\n * One `cwd` is cached per directory after the first file in it is parsed\n * (every file under a given dir shares the same cwd).\n *\n * Optional `sinceMtimeMs` bounds the walk to files modified at or after\n * the given epoch ms.\n */\nexport async function* walkPiSessions(opts: {\n root?: string;\n sinceMtimeMs?: number;\n} = {}): AsyncGenerator<PiSessionMeta> {\n const root = resolvePiSessionsRoot(opts.root);\n const dirs = await listDirSafe(root);\n for (const dirent of dirs) {\n if (!dirent.isDirectory) continue;\n const dirPath = join(root, dirent.name);\n const files = await listDirSafe(dirPath);\n let cachedCwd: string | null = null;\n for (const f of files) {\n if (!f.isFile || !f.name.endsWith('.jsonl')) continue;\n const filePath = join(dirPath, f.name);\n if (opts.sinceMtimeMs !== undefined) {\n const mtime = await mtimeMs(filePath);\n if (mtime !== null && mtime < opts.sinceMtimeMs) continue;\n }\n let meta: PiSessionMeta | null;\n if (cachedCwd) {\n // Derive sessionId from filename; reuse cached cwd.\n const underscoreIdx = f.name.lastIndexOf('_');\n if (underscoreIdx === -1) continue;\n const sessionId = f.name.slice(underscoreIdx + 1).replace(/\\.jsonl$/, '');\n if (!sessionId) continue;\n const startTs = await readFirstTimestamp(filePath);\n const endTs = await readLastTimestamp(filePath);\n meta = { tool: 'pi', sessionId, cwd: cachedCwd, startTs, endTs, path: filePath };\n } else {\n meta = await extractPiSessionMeta(filePath);\n if (meta) cachedCwd = meta.cwd;\n }\n if (meta) yield meta;\n }\n }\n}\n\n// --- Internals ------------------------------------------------------------\n\nasync function listDirSafe(\n path: string,\n): Promise<Array<{ name: string; isFile: boolean; isDirectory: boolean }>> {\n try {\n const entries = await readdir(path, { withFileTypes: true });\n return entries.map((e) => ({\n name: e.name,\n isFile: e.isFile(),\n isDirectory: e.isDirectory(),\n }));\n } catch {\n return [];\n }\n}\n\nasync function* walkJsonlRecursive(root: string): AsyncGenerator<string> {\n const stack: string[] = [root];\n while (stack.length > 0) {\n const current = stack.pop()!;\n const entries = await listDirSafe(current);\n for (const e of entries) {\n const full = join(current, e.name);\n if (e.isDirectory) {\n stack.push(full);\n } else if (e.isFile && e.name.endsWith('.jsonl')) {\n yield full;\n }\n }\n }\n}\n\nasync function mtimeMs(path: string): Promise<number | null> {\n try {\n const s = await stat(path);\n return s.mtimeMs;\n } catch {\n return null;\n }\n}\n\n/**\n * Bounded forward scan for the first JSON line carrying a `timestamp` field.\n * Returns `null` if none found within `SCAN_LINE_CAP` lines.\n */\nasync function readFirstTimestamp(path: string): Promise<string | null> {\n let handle;\n try {\n handle = await open(path, 'r');\n } catch {\n return null;\n }\n try {\n const stream = handle.createReadStream({ encoding: 'utf-8' });\n let buffer = '';\n let scanned = 0;\n for await (const chunk of stream) {\n buffer += chunk;\n let nl = buffer.indexOf('\\n');\n while (nl !== -1) {\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n const ts = extractTimestamp(line);\n if (ts) {\n stream.destroy();\n return ts;\n }\n scanned++;\n if (scanned >= SCAN_LINE_CAP) {\n stream.destroy();\n return null;\n }\n nl = buffer.indexOf('\\n');\n }\n }\n if (buffer.length > 0) return extractTimestamp(buffer);\n return null;\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\n/**\n * Bounded reverse scan: read the last `TAIL_READ_BYTES` of the file, walk\n * lines from end to start, return the first parsed `timestamp` found. Falls\n * back to expanding the window once to `TAIL_READ_BYTES_MAX`.\n */\nasync function readLastTimestamp(path: string): Promise<string | null> {\n let handle;\n try {\n handle = await open(path, 'r');\n } catch {\n return null;\n }\n try {\n const stats = await handle.stat();\n const size = stats.size;\n if (size === 0) return null;\n\n for (const windowBytes of [TAIL_READ_BYTES, TAIL_READ_BYTES_MAX]) {\n const start = Math.max(0, size - windowBytes);\n const length = size - start;\n const buf = Buffer.alloc(length);\n await handle.read(buf, 0, length, start);\n const text = buf.toString('utf-8');\n const lines = text.split('\\n');\n // If we didn't read from byte 0, the first line may be partial — drop it.\n if (start > 0) lines.shift();\n for (let i = lines.length - 1; i >= 0; i--) {\n const ts = extractTimestamp(lines[i]);\n if (ts) return ts;\n }\n if (start === 0) break; // already read the whole file\n }\n return null;\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\nfunction extractTimestamp(line: string): string | null {\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed[0] !== '{') return null;\n try {\n const parsed = JSON.parse(trimmed) as { timestamp?: unknown };\n if (typeof parsed.timestamp === 'string' && parsed.timestamp.length > 0) {\n return parsed.timestamp;\n }\n } catch {\n // Truncated or non-JSON; ignore.\n }\n return null;\n}\n","import { open } from 'node:fs/promises';\n\n// Cap on lines we'll scan looking for `cwd`. The launch cwd is recorded in the\n// first few entries of every Claude Code transcript; 50 lines is generous\n// enough to absorb leading non-JSON noise (blank lines, permission-mode rows\n// without a cwd) without slurping multi-MB transcripts into memory.\nconst MAX_LINES_SCANNED = 50;\n\n/**\n * Read the first `cwd` field from a Claude Code transcript JSONL file.\n *\n * Claude Code derives `~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`\n * from the *launch* cwd of the session, and `claude --resume <id>` only finds\n * the transcript when invoked from a matching cwd. The transcript itself is\n * therefore the authoritative source of truth for \"which directory does this\n * session belong to\" — read it once, prefer it over whatever a registering\n * caller might have happened to be sitting in.\n *\n * Returns `null` when the path is empty, the file doesn't exist or can't be\n * read, no JSON line within the scan window contains a `cwd` field, or the\n * value is not a non-empty string. Never throws.\n */\nexport async function derivePathFromTranscript(\n transcriptPath: string | null | undefined,\n): Promise<string | null> {\n if (!transcriptPath) return null;\n\n let handle;\n try {\n handle = await open(transcriptPath, 'r');\n } catch {\n return null;\n }\n\n try {\n const stream = handle.createReadStream({ encoding: 'utf-8' });\n let buffer = '';\n let scanned = 0;\n\n for await (const chunk of stream) {\n buffer += chunk;\n let nl = buffer.indexOf('\\n');\n while (nl !== -1) {\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n\n const cwd = extractCwd(line);\n if (cwd) {\n stream.destroy();\n return cwd;\n }\n\n scanned++;\n if (scanned >= MAX_LINES_SCANNED) {\n stream.destroy();\n return null;\n }\n nl = buffer.indexOf('\\n');\n }\n }\n\n // Trailing line without a newline (rare, but handle it).\n if (buffer.length > 0) {\n const cwd = extractCwd(buffer);\n if (cwd) return cwd;\n }\n return null;\n } finally {\n await handle.close().catch(() => {});\n }\n}\n\nfunction extractCwd(line: string): string | null {\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed[0] !== '{') return null;\n try {\n const parsed = JSON.parse(trimmed) as { cwd?: unknown };\n if (typeof parsed.cwd === 'string' && parsed.cwd.length > 0) {\n return parsed.cwd;\n }\n } catch {\n // Non-JSON or truncated line — keep scanning.\n }\n return null;\n}\n","import { fileURLToPath } from 'node:url';\nimport { dirname, resolve, join } from 'node:path';\nimport { realpathSync, readFileSync, mkdirSync } from 'node:fs';\nimport { syntaurRoot } from '../utils/paths.js';\nimport { fileExists, writeFileForce } from '../utils/fs.js';\n\nexport type InstallKind = 'npx' | 'global' | 'local' | 'unknown';\n\ninterface DetectOptions {\n realpath?: (p: string) => string;\n readFile?: (p: string) => string;\n envUserAgent?: string;\n}\n\n/**\n * Anchored cache-layout regexes. All require a `/node_modules/` suffix after\n * the cache hash segment to avoid false positives on user dirs that happen\n * to contain `_npx` / `dlx` / `bunx-` literals. Order matches the legacy\n * `isRunningViaNpx` in `src/utils/npx-prompt.ts` so behavior is consistent.\n */\nconst NPX_PATTERNS: { kind: 'npm' | 'pnpm' | 'bun'; re: RegExp }[] = [\n { kind: 'npm', re: /\\/_npx\\/([^/]+)\\/node_modules(?:\\/|$)/ },\n { kind: 'pnpm', re: /\\/pnpm\\/dlx\\/([^/]+)\\/node_modules(?:\\/|$)/ },\n { kind: 'bun', re: /\\/bunx-([^/]+)\\/node_modules(?:\\/|$)/ },\n];\n\n/**\n * Canonical npm global layout: `<prefix>/lib/node_modules/syntaur/...`\n * Matches /usr/local, nvm's `<v>/lib/node_modules/syntaur/`, Homebrew, etc.\n */\nconst GLOBAL_PATTERN = /\\/lib\\/node_modules\\/syntaur(?:\\/|$)/;\n\n/**\n * Resolve a file:// URL to an absolute filesystem path, applying realpath\n * so symlinks (e.g. an npm `bin/` symlink pointing into a cached node_modules)\n * resolve to the actual install location. Returns null on parse errors.\n */\nfunction resolveScriptPath(\n scriptUrl: string,\n realpath: (p: string) => string,\n): string | null {\n let p: string;\n try {\n p = fileURLToPath(scriptUrl);\n } catch {\n return null;\n }\n try {\n return realpath(p);\n } catch {\n // Path doesn't exist (test fixtures, deleted file). Fall back to the\n // unresolved path so classifier can still match by pattern.\n return p;\n }\n}\n\nfunction normalizeSlashes(p: string): string {\n return p.replace(/\\\\/g, '/');\n}\n\n/**\n * Classify the install origin of the running CLI.\n *\n * Decision order:\n * 1. npx-style cache patterns (anchored to `/node_modules/` suffix).\n * 2. `npm_config_user_agent` containing `npx/` (some pnpm-shim invocations\n * don't put dlx in the path).\n * 3. Canonical npm global layout `/lib/node_modules/syntaur/`.\n * 4. Local checkout (walks up to find a `package.json` named `syntaur`\n * whose dir is not under any `node_modules/`).\n * 5. `unknown` — the subcommand refuses these alongside `npx` to avoid\n * registering a bundle path that may not survive.\n */\nexport function detectInstallKind(\n scriptUrl: string,\n opts: DetectOptions = {},\n): InstallKind {\n const realpath = opts.realpath ?? realpathSync.native;\n const readFile = opts.readFile ?? ((p) => readFileSync(p, 'utf-8'));\n const ua =\n opts.envUserAgent !== undefined\n ? opts.envUserAgent\n : (process.env.npm_config_user_agent ?? '');\n\n const resolved = resolveScriptPath(scriptUrl, realpath);\n if (resolved === null) {\n return 'unknown';\n }\n const norm = normalizeSlashes(resolved);\n\n for (const pat of NPX_PATTERNS) {\n if (pat.re.test(norm)) return 'npx';\n }\n if (ua.includes('npx/')) {\n return 'npx';\n }\n\n if (GLOBAL_PATTERN.test(norm)) {\n return 'global';\n }\n\n // Walk up looking for a syntaur package.json that is NOT inside a\n // node_modules/ — that pattern indicates a local source checkout.\n let dir = dirname(resolved);\n for (let depth = 0; depth < 8; depth++) {\n const pkgJsonPath = join(dir, 'package.json');\n let raw: string;\n try {\n raw = readFile(pkgJsonPath);\n } catch {\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n continue;\n }\n try {\n const pkg = JSON.parse(raw) as { name?: unknown };\n if (\n typeof pkg.name === 'string' &&\n pkg.name === 'syntaur' &&\n !normalizeSlashes(dir).includes('/node_modules/')\n ) {\n return 'local';\n }\n } catch {\n // Malformed package.json on the way up — ignore and keep walking.\n }\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n return 'unknown';\n}\n\n/**\n * Extract the cache-hash segment from an npx-style script URL.\n * Returns null for global/local/unknown installs.\n */\nexport function extractNpxHash(\n scriptUrl: string,\n opts: DetectOptions = {},\n): string | null {\n const realpath = opts.realpath ?? realpathSync.native;\n const resolved = resolveScriptPath(scriptUrl, realpath);\n if (resolved === null) return null;\n const norm = normalizeSlashes(resolved);\n for (const pat of NPX_PATTERNS) {\n const m = norm.match(pat.re);\n if (m) return m[1] ?? null;\n }\n return null;\n}\n\nexport function nudgeStampDir(): string {\n return resolve(syntaurRoot(), 'npx-handler-nudge');\n}\n\n/**\n * Sanitize the hash to a safe filename: anything outside [A-Za-z0-9_-] is\n * replaced with `_`. The npx-cache regex captures already exclude `/`, but\n * this is defense-in-depth against future upstream cache layouts.\n */\nfunction sanitizeHash(hash: string): string {\n return hash.replace(/[^A-Za-z0-9_-]/g, '_') || '_';\n}\n\nexport function nudgeStampPath(hash: string): string {\n return join(nudgeStampDir(), sanitizeHash(hash));\n}\n\nexport async function hasNudgedHash(hash: string): Promise<boolean> {\n return fileExists(nudgeStampPath(hash));\n}\n\nexport async function recordNudge(hash: string): Promise<void> {\n try {\n mkdirSync(nudgeStampDir(), { recursive: true });\n } catch {\n // Best-effort; if mkdir fails (e.g. a regular file at the path), the\n // writeFileForce below will surface its own error which we also swallow.\n }\n try {\n await writeFileForce(nudgeStampPath(hash), '');\n } catch {\n // Best-effort. Worst case the nudge fires again on next invocation,\n // which is annoying but not destructive.\n }\n}\n\n/**\n * Truthiness rules for `SYNTAUR_SKIP_HANDLER_NUDGE`:\n * - `'1'`, `'true'`, `'yes'` (case-insensitive, trimmed) → disabled\n * - empty, unset, `'0'`, `'false'`, whitespace → enabled\n *\n * Deliberately narrow so users who set the var to `'0'` to mean \"off the\n * skip\" don't accidentally disable the nudge.\n */\nexport function isHandlerNudgeDisabled(): boolean {\n const raw = process.env.SYNTAUR_SKIP_HANDLER_NUDGE;\n if (raw === undefined) return false;\n const trimmed = raw.trim();\n return /^(1|true|yes)$/i.test(trimmed);\n}\n\nexport function nudgeMessage(): string {\n return 'syntaur: running from npx — the syntaur:// deep-link handler is not registered. Install durably with `npm i -g syntaur` to enable \"Open in agent\" buttons.';\n}\n\nexport async function shouldNudgeForNpx(hash: string | null): Promise<boolean> {\n if (isHandlerNudgeDisabled()) return false;\n if (hash === null) return false;\n if (await hasNudgedHash(hash)) return false;\n return true;\n}\n\n/**\n * Args that short-circuit the nudge: when the user invoked `--help` or\n * `--version`, they're not running the CLI for real, so don't bother them\n * with the install-durably banner. Mirrors `META_ARGS` in\n * `src/utils/npx-prompt.ts`.\n */\nconst META_ARGS = new Set(['-h', '--help', '-V', '--version', 'help']);\n\n/**\n * Pre-Commander startup hook. Mirrors `maybePromptInstall` from\n * `src/utils/npx-prompt.ts` in shape so `src/index.ts` can call them\n * back-to-back.\n */\nexport async function maybeNudgeForNpxInstall(scriptUrl: string): Promise<void> {\n if (detectInstallKind(scriptUrl) !== 'npx') return;\n const args = process.argv.slice(2);\n if (args.some((a) => META_ARGS.has(a))) return;\n const hash = extractNpxHash(scriptUrl);\n if (!(await shouldNudgeForNpx(hash))) return;\n // hash is non-null here — shouldNudgeForNpx returned false for null above.\n console.error(nudgeMessage());\n if (hash !== null) {\n await recordNudge(hash);\n }\n}\n"],"mappings":"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","names":["readFile","rename","writeFile","resolve","getField","resolve","extractFrontmatter","parseSimpleValue","parseExternalIds","parseStatusHistory","parseFactsMap","parseAttestations","readFile","resolve","init_parser","readdir","resolve","init_parser","resolve","readFile","init_parser","init_parser","readFile","resolve","init_config","resolve","readdir","readFile","extractFrontmatter","resolve","readdir","readFile","playbooksDir","init_config","isAbsolute","readFile","readdir","readFile","resolve","init_types","resolve","readdir","readdir","readFile","resolve","join","init_types","Database","resolve","readdir","readFile","readFile","resolve","db","rows","readdir","readFile","writeFile","stat","resolve","dirname","openQuestions","computeFactsDetailed","deriveDimensions","DEFAULT_DERIVE_CONFIG","init_config","init_config","isAbsolute","spawn","mkdir","writeFile","isAbsolute","resolve","isAbsolute","isAbsolute","spawn","homedir","basename","join","resolve","spawnSync","existsSync","homedir","join","join","homedir","existsSync","spawnSync","join","existsSync","init_config","execFileSync","statSync","homedir","dirname","join","open","readdir","stat","join","homedir","join","dirname","spawn","basename","resolve","join","homedir","initSessionDb","appendSession","dirname","resolve","join","readFileSync","mkdirSync","readFile","parent"]}