syntaur 0.3.0 → 0.4.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 (41) hide show
  1. package/README.md +26 -3
  2. package/dist/dashboard/server.js +497 -315
  3. package/dist/dashboard/server.js.map +1 -1
  4. package/dist/index.js +1240 -550
  5. package/dist/index.js.map +1 -1
  6. package/package.json +8 -2
  7. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  8. package/platforms/claude-code/agents/syntaur-expert.md +35 -20
  9. package/platforms/claude-code/commands/complete-assignment/complete-assignment.md +20 -0
  10. package/platforms/claude-code/commands/create-assignment/create-assignment.md +20 -0
  11. package/platforms/claude-code/commands/create-project/create-project.md +20 -0
  12. package/platforms/claude-code/commands/grab-assignment/grab-assignment.md +20 -0
  13. package/platforms/claude-code/commands/plan-assignment/plan-assignment.md +20 -0
  14. package/platforms/claude-code/commands/track-session/track-session.md +43 -18
  15. package/platforms/claude-code/hooks/hooks.json +11 -0
  16. package/platforms/claude-code/hooks/session-cleanup.sh +13 -23
  17. package/platforms/claude-code/hooks/session-start.sh +80 -0
  18. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  19. package/platforms/codex/agents/syntaur-operator.md +6 -4
  20. package/platforms/codex/scripts/resolve-session.sh +49 -0
  21. package/statusline/statusline.sh +133 -0
  22. package/vendor/syntaur-skills/LICENSE +21 -0
  23. package/vendor/syntaur-skills/README.md +43 -0
  24. package/vendor/syntaur-skills/skills/complete-assignment/SKILL.md +146 -0
  25. package/vendor/syntaur-skills/skills/create-assignment/SKILL.md +72 -0
  26. package/vendor/syntaur-skills/skills/create-project/SKILL.md +56 -0
  27. package/vendor/syntaur-skills/skills/grab-assignment/SKILL.md +158 -0
  28. package/vendor/syntaur-skills/skills/plan-assignment/SKILL.md +137 -0
  29. package/vendor/syntaur-skills/skills/syntaur-protocol/SKILL.md +119 -0
  30. package/vendor/syntaur-skills/skills/syntaur-protocol/references/file-ownership.md +67 -0
  31. package/vendor/syntaur-skills/skills/syntaur-protocol/references/protocol-summary.md +82 -0
  32. package/platforms/claude-code/skills/complete-assignment/SKILL.md +0 -155
  33. package/platforms/claude-code/skills/create-assignment/SKILL.md +0 -67
  34. package/platforms/claude-code/skills/grab-assignment/SKILL.md +0 -187
  35. package/platforms/claude-code/skills/plan-assignment/SKILL.md +0 -148
  36. package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +0 -86
  37. package/platforms/codex/skills/complete-assignment/SKILL.md +0 -64
  38. package/platforms/codex/skills/create-assignment/SKILL.md +0 -49
  39. package/platforms/codex/skills/grab-assignment/SKILL.md +0 -71
  40. package/platforms/codex/skills/plan-assignment/SKILL.md +0 -57
  41. package/platforms/codex/skills/syntaur-protocol/SKILL.md +0 -102
@@ -484,9 +484,144 @@ var init_config = __esm({
484
484
  }
485
485
  });
486
486
 
487
+ // src/utils/fs-migration.ts
488
+ import { readdir, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
489
+ import { resolve as resolve3 } from "path";
490
+ async function migrateLegacyProjectFiles(projectsDir) {
491
+ const result = {
492
+ renamedProjectFiles: [],
493
+ legacyExtras: []
494
+ };
495
+ if (!await fileExists(projectsDir)) return result;
496
+ let entries;
497
+ try {
498
+ entries = await readdir(projectsDir, { withFileTypes: true });
499
+ } catch {
500
+ return result;
501
+ }
502
+ for (const entry of entries) {
503
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
504
+ const projectDir = resolve3(projectsDir, entry.name);
505
+ const legacy = resolve3(projectDir, "mission.md");
506
+ const target = resolve3(projectDir, "project.md");
507
+ try {
508
+ if (await fileExists(legacy) && !await fileExists(target)) {
509
+ await rename2(legacy, target);
510
+ result.renamedProjectFiles.push(`${entry.name}/mission.md`);
511
+ }
512
+ } catch {
513
+ continue;
514
+ }
515
+ for (const stale of ["agent.md", "claude.md"]) {
516
+ try {
517
+ if (await fileExists(resolve3(projectDir, stale))) {
518
+ result.legacyExtras.push(`${entry.name}/${stale}`);
519
+ }
520
+ } catch {
521
+ }
522
+ }
523
+ }
524
+ return result;
525
+ }
526
+ async function migrateLegacyConfig(configPath) {
527
+ const result = {
528
+ renamedField: false,
529
+ renamedDir: false,
530
+ resolvedProjectsDir: null
531
+ };
532
+ if (!await fileExists(configPath)) return result;
533
+ let content;
534
+ try {
535
+ content = await readFile2(configPath, "utf-8");
536
+ } catch {
537
+ return result;
538
+ }
539
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
540
+ if (!fmMatch) return result;
541
+ const fmBlock = fmMatch[1];
542
+ const afterFm = content.slice(fmMatch[0].length);
543
+ const missionLineRe = /^(\s*)defaultMissionDir\s*:\s*(.*)$/m;
544
+ const missionLineMatch = fmBlock.match(missionLineRe);
545
+ const hasProjectLine = /^\s*defaultProjectDir\s*:/m.test(fmBlock);
546
+ let newFmBlock = fmBlock;
547
+ let missionValue = null;
548
+ if (missionLineMatch) {
549
+ missionValue = missionLineMatch[2].trim();
550
+ if (!hasProjectLine) {
551
+ newFmBlock = fmBlock.replace(
552
+ missionLineRe,
553
+ `$1defaultProjectDir: ${missionValue}`
554
+ );
555
+ result.renamedField = true;
556
+ } else {
557
+ newFmBlock = fmBlock.replace(missionLineRe, "").replace(/\n{2,}/g, "\n");
558
+ result.renamedField = true;
559
+ }
560
+ }
561
+ const projectLineRe = /^\s*defaultProjectDir\s*:\s*(.*)$/m;
562
+ const projectLineMatch = newFmBlock.match(projectLineRe);
563
+ const projectsDirRaw = projectLineMatch ? projectLineMatch[1].trim().replace(/^['"]|['"]$/g, "") : missionValue;
564
+ const expand = (p) => p.startsWith("~") ? resolve3(process.env.HOME ?? "/", p.slice(p.startsWith("~/") ? 2 : 1)) : p;
565
+ let resolvedProjectsDir = projectsDirRaw ? expand(projectsDirRaw) : null;
566
+ if (resolvedProjectsDir && resolvedProjectsDir.endsWith("/missions")) {
567
+ const siblingProjectsDir = resolvedProjectsDir.replace(/\/missions$/, "/projects");
568
+ if (await fileExists(resolvedProjectsDir) && !await fileExists(siblingProjectsDir)) {
569
+ try {
570
+ await rename2(resolvedProjectsDir, siblingProjectsDir);
571
+ const newValue = projectsDirRaw.endsWith("/missions") ? projectsDirRaw.replace(/\/missions$/, "/projects") : siblingProjectsDir;
572
+ newFmBlock = newFmBlock.replace(
573
+ projectLineRe,
574
+ `defaultProjectDir: ${newValue}`
575
+ );
576
+ resolvedProjectsDir = siblingProjectsDir;
577
+ result.renamedDir = true;
578
+ } catch {
579
+ }
580
+ }
581
+ }
582
+ result.resolvedProjectsDir = resolvedProjectsDir;
583
+ if (result.renamedField || result.renamedDir) {
584
+ const newContent = `---
585
+ ${newFmBlock.replace(/\n+$/, "")}
586
+ ---
587
+ ${afterFm.startsWith("\n") ? afterFm.slice(1) : afterFm}`;
588
+ try {
589
+ await writeFile2(configPath, newContent, "utf-8");
590
+ } catch {
591
+ result.renamedField = false;
592
+ result.renamedDir = false;
593
+ }
594
+ }
595
+ return result;
596
+ }
597
+ function summarizeMigration(project, config) {
598
+ const parts = [];
599
+ if (project.renamedProjectFiles.length > 0) {
600
+ const firstThree = project.renamedProjectFiles.map((p) => p.split("/")[0]).slice(0, 3).join(", ");
601
+ const more = project.renamedProjectFiles.length > 3 ? ` and ${project.renamedProjectFiles.length - 3} more` : "";
602
+ parts.push(
603
+ `renamed mission.md \u2192 project.md in ${project.renamedProjectFiles.length} project${project.renamedProjectFiles.length === 1 ? "" : "s"} (${firstThree}${more})`
604
+ );
605
+ }
606
+ if (config?.renamedField) parts.push("updated config defaultMissionDir \u2192 defaultProjectDir");
607
+ if (config?.renamedDir) parts.push("renamed projects directory on disk");
608
+ if (project.legacyExtras.length > 0) {
609
+ parts.push(
610
+ `${project.legacyExtras.length} legacy agent.md / claude.md file${project.legacyExtras.length === 1 ? "" : "s"} left in place (no longer read)`
611
+ );
612
+ }
613
+ return parts.length ? `[syntaur] legacy migration: ${parts.join("; ")}` : "";
614
+ }
615
+ var init_fs_migration = __esm({
616
+ "src/utils/fs-migration.ts"() {
617
+ "use strict";
618
+ init_fs();
619
+ }
620
+ });
621
+
487
622
  // src/utils/config.ts
488
- import { readFile as readFile2 } from "fs/promises";
489
- import { resolve as resolve3, isAbsolute } from "path";
623
+ import { readFile as readFile3 } from "fs/promises";
624
+ import { resolve as resolve4, isAbsolute } from "path";
490
625
  function parseFrontmatter(content) {
491
626
  const match = content.match(/^---\n([\s\S]*?)\n---/);
492
627
  if (!match) return {};
@@ -673,10 +808,10 @@ function parseOptionalAbsolutePath(value, fieldName) {
673
808
  );
674
809
  return null;
675
810
  }
676
- return resolve3(expanded);
811
+ return resolve4(expanded);
677
812
  }
678
813
  async function writeStatusConfig(statuses) {
679
- const configPath = resolve3(syntaurRoot(), "config.md");
814
+ const configPath = resolve4(syntaurRoot(), "config.md");
680
815
  const statusBlock = serializeStatusConfig(statuses);
681
816
  if (!await fileExists(configPath)) {
682
817
  const content = `---
@@ -688,7 +823,7 @@ ${statusBlock}
688
823
  await writeFileForce(configPath, content);
689
824
  return;
690
825
  }
691
- const existing = await readFile2(configPath, "utf-8");
826
+ const existing = await readFile3(configPath, "utf-8");
692
827
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
693
828
  if (!fmMatch) {
694
829
  const content = `---
@@ -730,9 +865,9 @@ ${statusBlock}
730
865
  await writeFileForce(configPath, newContent);
731
866
  }
732
867
  async function deleteStatusConfig() {
733
- const configPath = resolve3(syntaurRoot(), "config.md");
868
+ const configPath = resolve4(syntaurRoot(), "config.md");
734
869
  if (!await fileExists(configPath)) return;
735
- const existing = await readFile2(configPath, "utf-8");
870
+ const existing = await readFile3(configPath, "utf-8");
736
871
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
737
872
  if (!fmMatch) return;
738
873
  const fmBlock = fmMatch[2];
@@ -744,7 +879,7 @@ ${cleanedFm}
744
879
  await writeFileForce(configPath, newContent);
745
880
  }
746
881
  async function updateBackupConfig(backup) {
747
- const configPath = resolve3(syntaurRoot(), "config.md");
882
+ const configPath = resolve4(syntaurRoot(), "config.md");
748
883
  const current = (await readConfig()).backup;
749
884
  const nextBackup = {
750
885
  repo: current?.repo ?? null,
@@ -754,7 +889,7 @@ async function updateBackupConfig(backup) {
754
889
  ...backup
755
890
  };
756
891
  const backupBlock = serializeBackupConfig(nextBackup);
757
- const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
892
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
758
893
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
759
894
  if (!fmMatch) {
760
895
  const content = `---
@@ -778,11 +913,15 @@ ${normalizedFm}
778
913
  await writeFileForce(configPath, newContent);
779
914
  }
780
915
  async function readConfig() {
781
- const configPath = resolve3(syntaurRoot(), "config.md");
916
+ const configPath = resolve4(syntaurRoot(), "config.md");
782
917
  if (!await fileExists(configPath)) {
783
918
  return { ...DEFAULT_CONFIG };
784
919
  }
785
- const content = await readFile2(configPath, "utf-8");
920
+ if (!migratedConfigPaths.has(configPath)) {
921
+ migratedConfigPaths.add(configPath);
922
+ await migrateLegacyConfig(configPath);
923
+ }
924
+ const content = await readFile3(configPath, "utf-8");
786
925
  const fm = parseFrontmatter(content);
787
926
  if (Object.keys(fm).length === 0) {
788
927
  console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
@@ -829,13 +968,14 @@ async function readConfig() {
829
968
  types: null
830
969
  };
831
970
  }
832
- var DEFAULT_CONFIG;
971
+ var DEFAULT_CONFIG, migratedConfigPaths;
833
972
  var init_config2 = __esm({
834
973
  "src/utils/config.ts"() {
835
974
  "use strict";
836
975
  init_paths();
837
976
  init_fs();
838
977
  init_config();
978
+ init_fs_migration();
839
979
  DEFAULT_CONFIG = {
840
980
  version: "2.0",
841
981
  defaultProjectDir: defaultProjectDir(),
@@ -855,6 +995,7 @@ var init_config2 = __esm({
855
995
  statuses: null,
856
996
  types: null
857
997
  };
998
+ migratedConfigPaths = /* @__PURE__ */ new Set();
858
999
  }
859
1000
  });
860
1001
 
@@ -908,9 +1049,10 @@ function parseListField(frontmatter, fieldName) {
908
1049
  }
909
1050
  function parseProject(fileContent) {
910
1051
  const [fm, body] = extractFrontmatter2(fileContent);
1052
+ const slug = getField(fm, "slug") ?? getField(fm, "mission") ?? "";
911
1053
  return {
912
1054
  id: getField(fm, "id") ?? "",
913
- slug: getField(fm, "slug") ?? "",
1055
+ slug,
914
1056
  title: getField(fm, "title") ?? "",
915
1057
  archived: getField(fm, "archived") === "true",
916
1058
  archivedAt: getField(fm, "archivedAt"),
@@ -1143,13 +1285,13 @@ var init_parser = __esm({
1143
1285
  });
1144
1286
 
1145
1287
  // src/utils/assignment-resolver.ts
1146
- import { resolve as resolve4 } from "path";
1147
- import { readdir, readFile as readFile3 } from "fs/promises";
1288
+ import { resolve as resolve5 } from "path";
1289
+ import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
1148
1290
  async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
1149
1291
  let standaloneMatch = null;
1150
1292
  let projectMatch = null;
1151
- const standaloneDir = resolve4(assignmentsDir, id);
1152
- const standalonePath = resolve4(standaloneDir, "assignment.md");
1293
+ const standaloneDir = resolve5(assignmentsDir, id);
1294
+ const standalonePath = resolve5(standaloneDir, "assignment.md");
1153
1295
  if (await fileExists(standalonePath)) {
1154
1296
  standaloneMatch = {
1155
1297
  assignmentDir: standaloneDir,
@@ -1161,24 +1303,24 @@ async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
1161
1303
  }
1162
1304
  if (await fileExists(projectsDir)) {
1163
1305
  try {
1164
- const projects = await readdir(projectsDir, { withFileTypes: true });
1306
+ const projects = await readdir2(projectsDir, { withFileTypes: true });
1165
1307
  for (const p of projects) {
1166
1308
  if (!p.isDirectory()) continue;
1167
1309
  if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
1168
- const assignmentsPath = resolve4(projectsDir, p.name, "assignments");
1310
+ const assignmentsPath = resolve5(projectsDir, p.name, "assignments");
1169
1311
  if (!await fileExists(assignmentsPath)) continue;
1170
- const entries = await readdir(assignmentsPath, { withFileTypes: true });
1312
+ const entries = await readdir2(assignmentsPath, { withFileTypes: true });
1171
1313
  for (const a of entries) {
1172
1314
  if (!a.isDirectory()) continue;
1173
- const aPath = resolve4(assignmentsPath, a.name, "assignment.md");
1315
+ const aPath = resolve5(assignmentsPath, a.name, "assignment.md");
1174
1316
  if (!await fileExists(aPath)) continue;
1175
1317
  try {
1176
- const content = await readFile3(aPath, "utf-8");
1318
+ const content = await readFile4(aPath, "utf-8");
1177
1319
  const [fm] = extractFrontmatter2(content);
1178
1320
  const fileId = getField(fm, "id");
1179
1321
  if (fileId === id) {
1180
1322
  projectMatch = {
1181
- assignmentDir: resolve4(assignmentsPath, a.name),
1323
+ assignmentDir: resolve5(assignmentsPath, a.name),
1182
1324
  projectSlug: p.name,
1183
1325
  assignmentSlug: a.name,
1184
1326
  id,
@@ -1542,8 +1684,8 @@ var init_help = __esm({
1542
1684
  // --- Session & server tracking (index 17) ---
1543
1685
  {
1544
1686
  command: "syntaur track-session",
1545
- description: "Register an agent session, optionally linked to a project and assignment.",
1546
- example: "syntaur track-session --agent claude --project ui-overhaul --assignment implement-overview"
1687
+ description: "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.",
1688
+ example: "syntaur track-session --agent claude --session-id <real-id> --transcript-path <path> --project ui-overhaul --assignment implement-overview"
1547
1689
  },
1548
1690
  // --- Browsing & playbooks (indices 18-20) ---
1549
1691
  {
@@ -1626,8 +1768,8 @@ var init_help = __esm({
1626
1768
  });
1627
1769
 
1628
1770
  // src/dashboard/servers.ts
1629
- import { readdir as readdir2, readFile as readFile4, unlink } from "fs/promises";
1630
- import { resolve as resolve5 } from "path";
1771
+ import { readdir as readdir3, readFile as readFile5, unlink } from "fs/promises";
1772
+ import { resolve as resolve6 } from "path";
1631
1773
  function sanitizeSessionName(name) {
1632
1774
  return name.replace(/[^a-zA-Z0-9_-]/g, "-");
1633
1775
  }
@@ -1675,18 +1817,18 @@ async function registerSession(dir, rawName) {
1675
1817
  lastRefreshed: now,
1676
1818
  overrides: {}
1677
1819
  });
1678
- await writeFileForce(resolve5(dir, `${name}.md`), content);
1820
+ await writeFileForce(resolve6(dir, `${name}.md`), content);
1679
1821
  return name;
1680
1822
  }
1681
1823
  async function listSessionFiles(dir) {
1682
1824
  if (!await fileExists(dir)) return [];
1683
- const entries = await readdir2(dir);
1825
+ const entries = await readdir3(dir);
1684
1826
  return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
1685
1827
  }
1686
1828
  async function readSessionFile(dir, name) {
1687
- const filePath = resolve5(dir, `${sanitizeSessionName(name)}.md`);
1829
+ const filePath = resolve6(dir, `${sanitizeSessionName(name)}.md`);
1688
1830
  if (!await fileExists(filePath)) return null;
1689
- const raw = await readFile4(filePath, "utf-8");
1831
+ const raw = await readFile5(filePath, "utf-8");
1690
1832
  const [frontmatter] = extractFrontmatter2(raw);
1691
1833
  if (!frontmatter) return null;
1692
1834
  const session = getField(frontmatter, "session") ?? name;
@@ -1726,7 +1868,7 @@ async function readSessionFile(dir, name) {
1726
1868
  };
1727
1869
  }
1728
1870
  async function removeSession(dir, name) {
1729
- const filePath = resolve5(dir, `${sanitizeSessionName(name)}.md`);
1871
+ const filePath = resolve6(dir, `${sanitizeSessionName(name)}.md`);
1730
1872
  if (await fileExists(filePath)) {
1731
1873
  await unlink(filePath);
1732
1874
  }
@@ -1735,7 +1877,7 @@ async function updateLastRefreshed(dir, name) {
1735
1877
  const data = await readSessionFile(dir, name);
1736
1878
  if (!data) return;
1737
1879
  const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
1738
- await writeFileForce(resolve5(dir, `${sanitizeSessionName(name)}.md`), content);
1880
+ await writeFileForce(resolve6(dir, `${sanitizeSessionName(name)}.md`), content);
1739
1881
  }
1740
1882
  async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
1741
1883
  const data = await readSessionFile(dir, sessionName);
@@ -1747,7 +1889,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
1747
1889
  delete data.overrides[key];
1748
1890
  }
1749
1891
  const content = buildSessionContent({ ...data });
1750
- await writeFileForce(resolve5(dir, `${sanitizeSessionName(sessionName)}.md`), content);
1892
+ await writeFileForce(resolve6(dir, `${sanitizeSessionName(sessionName)}.md`), content);
1751
1893
  }
1752
1894
  async function registerAutoSession(dir, rawName, opts) {
1753
1895
  const name = sanitizeSessionName(rawName);
@@ -1764,7 +1906,7 @@ async function registerAutoSession(dir, rawName, opts) {
1764
1906
  ports: opts.ports,
1765
1907
  cwd: opts.cwd
1766
1908
  });
1767
- await writeFileForce(resolve5(dir, `${name}.md`), content);
1909
+ await writeFileForce(resolve6(dir, `${name}.md`), content);
1768
1910
  return name;
1769
1911
  }
1770
1912
  var init_servers = __esm({
@@ -1796,8 +1938,8 @@ __export(scanner_exports, {
1796
1938
  });
1797
1939
  import { execFile } from "child_process";
1798
1940
  import { promisify } from "util";
1799
- import { resolve as resolve6 } from "path";
1800
- import { realpath, readdir as readdir3, readFile as readFile5 } from "fs/promises";
1941
+ import { resolve as resolve7 } from "path";
1942
+ import { realpath, readdir as readdir4, readFile as readFile6 } from "fs/promises";
1801
1943
  function clearScanCache() {
1802
1944
  cache = null;
1803
1945
  }
@@ -1892,8 +2034,8 @@ async function getGitInfo(cwd) {
1892
2034
  let isWorktree = false;
1893
2035
  if (commonDir && gitDir && commonDir !== gitDir) {
1894
2036
  try {
1895
- const resolvedCommon = await realpath(resolve6(cwd, commonDir));
1896
- const resolvedGit = await realpath(resolve6(cwd, gitDir));
2037
+ const resolvedCommon = await realpath(resolve7(cwd, commonDir));
2038
+ const resolvedGit = await realpath(resolve7(cwd, gitDir));
1897
2039
  isWorktree = resolvedCommon !== resolvedGit;
1898
2040
  } catch {
1899
2041
  isWorktree = false;
@@ -1906,17 +2048,17 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
1906
2048
  try {
1907
2049
  const projects = await listProjects(projectsDir);
1908
2050
  for (const project of projects) {
1909
- const projectAssignmentsDir = resolve6(projectsDir, project.slug, "assignments");
2051
+ const projectAssignmentsDir = resolve7(projectsDir, project.slug, "assignments");
1910
2052
  let slugs;
1911
2053
  try {
1912
- slugs = await readdir3(projectAssignmentsDir);
2054
+ slugs = await readdir4(projectAssignmentsDir);
1913
2055
  } catch {
1914
2056
  continue;
1915
2057
  }
1916
2058
  for (const aslug of slugs) {
1917
- const aFile = resolve6(projectAssignmentsDir, aslug, "assignment.md");
2059
+ const aFile = resolve7(projectAssignmentsDir, aslug, "assignment.md");
1918
2060
  try {
1919
- const raw = await readFile5(aFile, "utf-8");
2061
+ const raw = await readFile6(aFile, "utf-8");
1920
2062
  const [fm] = extractFrontmatter2(raw);
1921
2063
  if (!fm) continue;
1922
2064
  records.push({
@@ -1935,12 +2077,12 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
1935
2077
  }
1936
2078
  if (assignmentsDir) {
1937
2079
  try {
1938
- const entries = await readdir3(assignmentsDir);
2080
+ const entries = await readdir4(assignmentsDir);
1939
2081
  for (const id of entries) {
1940
2082
  if (id.startsWith(".") || id.startsWith("_")) continue;
1941
- const aFile = resolve6(assignmentsDir, id, "assignment.md");
2083
+ const aFile = resolve7(assignmentsDir, id, "assignment.md");
1942
2084
  try {
1943
- const raw = await readFile5(aFile, "utf-8");
2085
+ const raw = await readFile6(aFile, "utf-8");
1944
2086
  const [fm] = extractFrontmatter2(raw);
1945
2087
  if (!fm) continue;
1946
2088
  records.push({
@@ -2188,20 +2330,20 @@ var init_scanner = __esm({
2188
2330
  });
2189
2331
 
2190
2332
  // src/dashboard/api.ts
2191
- import { readdir as readdir4, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2192
- import { resolve as resolve7, dirname as dirname2 } from "path";
2333
+ import { readdir as readdir5, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
2334
+ import { resolve as resolve8, dirname as dirname2 } from "path";
2193
2335
  async function listStandaloneRecords(assignmentsDir) {
2194
2336
  if (!assignmentsDir) return [];
2195
2337
  if (!await fileExists(assignmentsDir)) return [];
2196
- const entries = await readdir4(assignmentsDir, { withFileTypes: true });
2338
+ const entries = await readdir5(assignmentsDir, { withFileTypes: true });
2197
2339
  const records = [];
2198
2340
  for (const entry of entries) {
2199
2341
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
2200
- const assignmentDir = resolve7(assignmentsDir, entry.name);
2201
- const assignmentMdPath = resolve7(assignmentDir, "assignment.md");
2342
+ const assignmentDir = resolve8(assignmentsDir, entry.name);
2343
+ const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
2202
2344
  if (!await fileExists(assignmentMdPath)) continue;
2203
2345
  try {
2204
- const content = await readFile6(assignmentMdPath, "utf-8");
2346
+ const content = await readFile7(assignmentMdPath, "utf-8");
2205
2347
  const record = parseAssignmentFull(content);
2206
2348
  records.push({ assignmentDir, id: entry.name, record });
2207
2349
  } catch {
@@ -2270,9 +2412,9 @@ async function listProjects(projectsDir) {
2270
2412
  return projectRecords.map((record) => record.summary);
2271
2413
  }
2272
2414
  async function readWorkspaceRegistry(projectsDir) {
2273
- const registryPath = resolve7(dirname2(projectsDir), "workspaces.json");
2415
+ const registryPath = resolve8(dirname2(projectsDir), "workspaces.json");
2274
2416
  try {
2275
- const raw = await readFile6(registryPath, "utf-8");
2417
+ const raw = await readFile7(registryPath, "utf-8");
2276
2418
  const parsed = JSON.parse(raw);
2277
2419
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
2278
2420
  } catch {
@@ -2280,8 +2422,8 @@ async function readWorkspaceRegistry(projectsDir) {
2280
2422
  }
2281
2423
  }
2282
2424
  async function writeWorkspaceRegistry(projectsDir, workspaces) {
2283
- const registryPath = resolve7(dirname2(projectsDir), "workspaces.json");
2284
- await writeFile2(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
2425
+ const registryPath = resolve8(dirname2(projectsDir), "workspaces.json");
2426
+ await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
2285
2427
  }
2286
2428
  async function listWorkspaces(projectsDir) {
2287
2429
  const [projectRecords, registered] = await Promise.all([
@@ -2480,7 +2622,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
2480
2622
  if (!filePath || !await fileExists(filePath)) {
2481
2623
  return null;
2482
2624
  }
2483
- const content = await readFile6(filePath, "utf-8");
2625
+ const content = await readFile7(filePath, "utf-8");
2484
2626
  const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
2485
2627
  return {
2486
2628
  documentType,
@@ -2504,9 +2646,9 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
2504
2646
  }
2505
2647
  const fileName = documentType === "assignment" ? "assignment.md" : documentType === "plan" ? "plan.md" : documentType === "scratchpad" ? "scratchpad.md" : documentType === "handoff" ? "handoff.md" : documentType === "decision-record" ? "decision-record.md" : null;
2506
2648
  if (!fileName) return null;
2507
- const filePath = resolve7(resolved.assignmentDir, fileName);
2649
+ const filePath = resolve8(resolved.assignmentDir, fileName);
2508
2650
  if (!await fileExists(filePath)) return null;
2509
- const content = await readFile6(filePath, "utf-8");
2651
+ const content = await readFile7(filePath, "utf-8");
2510
2652
  const label = resolved.id;
2511
2653
  const title = documentType === "assignment" ? `Edit Assignment: ${label}` : documentType === "plan" ? `Edit Plan: ${label}` : documentType === "scratchpad" ? `Edit Scratchpad: ${label}` : documentType === "handoff" ? `Append Handoff: ${label}` : `Append Decision: ${label}`;
2512
2654
  return {
@@ -2520,12 +2662,12 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
2520
2662
  };
2521
2663
  }
2522
2664
  async function getProjectDetail(projectsDir, slug) {
2523
- const projectPath = resolve7(projectsDir, slug);
2524
- const projectMdPath = resolve7(projectPath, "project.md");
2665
+ const projectPath = resolve8(projectsDir, slug);
2666
+ const projectMdPath = resolve8(projectPath, "project.md");
2525
2667
  if (!await fileExists(projectMdPath)) {
2526
2668
  return null;
2527
2669
  }
2528
- const projectContent = await readFile6(projectMdPath, "utf-8");
2670
+ const projectContent = await readFile7(projectMdPath, "utf-8");
2529
2671
  const project = parseProject(projectContent);
2530
2672
  const assignments = await listAssignmentRecords(projectPath);
2531
2673
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -2555,17 +2697,17 @@ async function getProjectDetail(projectsDir, slug) {
2555
2697
  };
2556
2698
  }
2557
2699
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2558
- const assignmentDir = resolve7(projectsDir, projectSlug, "assignments", assignmentSlug);
2559
- const assignmentMdPath = resolve7(assignmentDir, "assignment.md");
2700
+ const assignmentDir = resolve8(projectsDir, projectSlug, "assignments", assignmentSlug);
2701
+ const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
2560
2702
  if (!await fileExists(assignmentMdPath)) {
2561
2703
  return null;
2562
2704
  }
2563
- const assignmentContent = await readFile6(assignmentMdPath, "utf-8");
2705
+ const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
2564
2706
  const assignment = parseAssignmentFull(assignmentContent);
2565
2707
  let plan = null;
2566
- const planPath = resolve7(assignmentDir, "plan.md");
2708
+ const planPath = resolve8(assignmentDir, "plan.md");
2567
2709
  if (await fileExists(planPath)) {
2568
- const planContent = await readFile6(planPath, "utf-8");
2710
+ const planContent = await readFile7(planPath, "utf-8");
2569
2711
  const parsed = parsePlan(planContent);
2570
2712
  plan = {
2571
2713
  status: parsed.status,
@@ -2574,9 +2716,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2574
2716
  };
2575
2717
  }
2576
2718
  let scratchpad = null;
2577
- const scratchpadPath = resolve7(assignmentDir, "scratchpad.md");
2719
+ const scratchpadPath = resolve8(assignmentDir, "scratchpad.md");
2578
2720
  if (await fileExists(scratchpadPath)) {
2579
- const scratchpadContent = await readFile6(scratchpadPath, "utf-8");
2721
+ const scratchpadContent = await readFile7(scratchpadPath, "utf-8");
2580
2722
  const parsed = parseScratchpad(scratchpadContent);
2581
2723
  scratchpad = {
2582
2724
  updated: parsed.updated,
@@ -2584,9 +2726,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2584
2726
  };
2585
2727
  }
2586
2728
  let handoff = null;
2587
- const handoffPath = resolve7(assignmentDir, "handoff.md");
2729
+ const handoffPath = resolve8(assignmentDir, "handoff.md");
2588
2730
  if (await fileExists(handoffPath)) {
2589
- const handoffContent = await readFile6(handoffPath, "utf-8");
2731
+ const handoffContent = await readFile7(handoffPath, "utf-8");
2590
2732
  const parsed = parseHandoff(handoffContent);
2591
2733
  handoff = {
2592
2734
  updated: parsed.updated,
@@ -2595,9 +2737,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2595
2737
  };
2596
2738
  }
2597
2739
  let decisionRecord = null;
2598
- const decisionRecordPath = resolve7(assignmentDir, "decision-record.md");
2740
+ const decisionRecordPath = resolve8(assignmentDir, "decision-record.md");
2599
2741
  if (await fileExists(decisionRecordPath)) {
2600
- const decisionRecordContent = await readFile6(decisionRecordPath, "utf-8");
2742
+ const decisionRecordContent = await readFile7(decisionRecordPath, "utf-8");
2601
2743
  const parsed = parseDecisionRecord(decisionRecordContent);
2602
2744
  decisionRecord = {
2603
2745
  updated: parsed.updated,
@@ -2606,9 +2748,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2606
2748
  };
2607
2749
  }
2608
2750
  let progress = null;
2609
- const progressPath = resolve7(assignmentDir, "progress.md");
2751
+ const progressPath = resolve8(assignmentDir, "progress.md");
2610
2752
  if (await fileExists(progressPath)) {
2611
- const progressContent = await readFile6(progressPath, "utf-8");
2753
+ const progressContent = await readFile7(progressPath, "utf-8");
2612
2754
  const parsed = parseProgress(progressContent);
2613
2755
  progress = {
2614
2756
  updated: parsed.updated,
@@ -2617,9 +2759,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2617
2759
  };
2618
2760
  }
2619
2761
  let comments = null;
2620
- const commentsPath = resolve7(assignmentDir, "comments.md");
2762
+ const commentsPath = resolve8(assignmentDir, "comments.md");
2621
2763
  if (await fileExists(commentsPath)) {
2622
- const commentsContent = await readFile6(commentsPath, "utf-8");
2764
+ const commentsContent = await readFile7(commentsPath, "utf-8");
2623
2765
  const parsed = parseComments(commentsContent);
2624
2766
  comments = {
2625
2767
  updated: parsed.updated,
@@ -2733,7 +2875,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2733
2875
  slug: a.slug,
2734
2876
  title: a.title,
2735
2877
  projectSlug: rec.summary.slug,
2736
- assignmentDir: resolve7(rec.projectPath, "assignments", a.slug)
2878
+ assignmentDir: resolve8(rec.projectPath, "assignments", a.slug)
2737
2879
  });
2738
2880
  }
2739
2881
  }
@@ -2766,17 +2908,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2766
2908
  }
2767
2909
  async function countMentionsInAssignment(sourceDir, target) {
2768
2910
  const bodies = [];
2769
- const assignmentMd = resolve7(sourceDir, "assignment.md");
2911
+ const assignmentMd = resolve8(sourceDir, "assignment.md");
2770
2912
  if (await fileExists(assignmentMd)) {
2771
- const content = await readFile6(assignmentMd, "utf-8");
2913
+ const content = await readFile7(assignmentMd, "utf-8");
2772
2914
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
2773
2915
  if (todosMatch) bodies.push(todosMatch[1]);
2774
2916
  }
2775
2917
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
2776
- const path = resolve7(sourceDir, filename);
2918
+ const path = resolve8(sourceDir, filename);
2777
2919
  if (await fileExists(path)) {
2778
2920
  try {
2779
- bodies.push(await readFile6(path, "utf-8"));
2921
+ bodies.push(await readFile7(path, "utf-8"));
2780
2922
  } catch {
2781
2923
  }
2782
2924
  }
@@ -2834,44 +2976,44 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
2834
2976
  }
2835
2977
  async function buildStandaloneAssignmentDetail(resolved) {
2836
2978
  const assignmentDir = resolved.assignmentDir;
2837
- const assignmentMdPath = resolve7(assignmentDir, "assignment.md");
2979
+ const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
2838
2980
  if (!await fileExists(assignmentMdPath)) return null;
2839
- const assignmentContent = await readFile6(assignmentMdPath, "utf-8");
2981
+ const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
2840
2982
  const assignment = parseAssignmentFull(assignmentContent);
2841
2983
  let plan = null;
2842
- const planPath = resolve7(assignmentDir, "plan.md");
2984
+ const planPath = resolve8(assignmentDir, "plan.md");
2843
2985
  if (await fileExists(planPath)) {
2844
- const parsed = parsePlan(await readFile6(planPath, "utf-8"));
2986
+ const parsed = parsePlan(await readFile7(planPath, "utf-8"));
2845
2987
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
2846
2988
  }
2847
2989
  let scratchpad = null;
2848
- const scratchpadPath = resolve7(assignmentDir, "scratchpad.md");
2990
+ const scratchpadPath = resolve8(assignmentDir, "scratchpad.md");
2849
2991
  if (await fileExists(scratchpadPath)) {
2850
- const parsed = parseScratchpad(await readFile6(scratchpadPath, "utf-8"));
2992
+ const parsed = parseScratchpad(await readFile7(scratchpadPath, "utf-8"));
2851
2993
  scratchpad = { updated: parsed.updated, body: parsed.body };
2852
2994
  }
2853
2995
  let handoff = null;
2854
- const handoffPath = resolve7(assignmentDir, "handoff.md");
2996
+ const handoffPath = resolve8(assignmentDir, "handoff.md");
2855
2997
  if (await fileExists(handoffPath)) {
2856
- const parsed = parseHandoff(await readFile6(handoffPath, "utf-8"));
2998
+ const parsed = parseHandoff(await readFile7(handoffPath, "utf-8"));
2857
2999
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
2858
3000
  }
2859
3001
  let decisionRecord = null;
2860
- const decisionRecordPath = resolve7(assignmentDir, "decision-record.md");
3002
+ const decisionRecordPath = resolve8(assignmentDir, "decision-record.md");
2861
3003
  if (await fileExists(decisionRecordPath)) {
2862
- const parsed = parseDecisionRecord(await readFile6(decisionRecordPath, "utf-8"));
3004
+ const parsed = parseDecisionRecord(await readFile7(decisionRecordPath, "utf-8"));
2863
3005
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
2864
3006
  }
2865
3007
  let progress = null;
2866
- const progressPath = resolve7(assignmentDir, "progress.md");
3008
+ const progressPath = resolve8(assignmentDir, "progress.md");
2867
3009
  if (await fileExists(progressPath)) {
2868
- const parsed = parseProgress(await readFile6(progressPath, "utf-8"));
3010
+ const parsed = parseProgress(await readFile7(progressPath, "utf-8"));
2869
3011
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2870
3012
  }
2871
3013
  let comments = null;
2872
- const commentsPath = resolve7(assignmentDir, "comments.md");
3014
+ const commentsPath = resolve8(assignmentDir, "comments.md");
2873
3015
  if (await fileExists(commentsPath)) {
2874
- const parsed = parseComments(await readFile6(commentsPath, "utf-8"));
3016
+ const parsed = parseComments(await readFile7(commentsPath, "utf-8"));
2875
3017
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2876
3018
  }
2877
3019
  const detail = {
@@ -2909,16 +3051,20 @@ async function listProjectRecords(projectsDir) {
2909
3051
  if (!await fileExists(projectsDir)) {
2910
3052
  return [];
2911
3053
  }
2912
- const entries = await readdir4(projectsDir, { withFileTypes: true });
3054
+ if (!migratedProjectsDirs.has(projectsDir)) {
3055
+ migratedProjectsDirs.add(projectsDir);
3056
+ await migrateLegacyProjectFiles(projectsDir);
3057
+ }
3058
+ const entries = await readdir5(projectsDir, { withFileTypes: true });
2913
3059
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
2914
3060
  const records = [];
2915
3061
  for (const entry of projectDirs) {
2916
- const projectPath = resolve7(projectsDir, entry.name);
2917
- const projectMdPath = resolve7(projectPath, "project.md");
3062
+ const projectPath = resolve8(projectsDir, entry.name);
3063
+ const projectMdPath = resolve8(projectPath, "project.md");
2918
3064
  if (!await fileExists(projectMdPath)) {
2919
3065
  continue;
2920
3066
  }
2921
- const projectContent = await readFile6(projectMdPath, "utf-8");
3067
+ const projectContent = await readFile7(projectMdPath, "utf-8");
2922
3068
  const project = parseProject(projectContent);
2923
3069
  const assignments = await listAssignmentRecords(projectPath);
2924
3070
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -2949,39 +3095,39 @@ async function listProjectRecords(projectsDir) {
2949
3095
  return records;
2950
3096
  }
2951
3097
  async function listAssignmentRecords(projectPath) {
2952
- const assignmentsDir = resolve7(projectPath, "assignments");
3098
+ const assignmentsDir = resolve8(projectPath, "assignments");
2953
3099
  if (!await fileExists(assignmentsDir)) {
2954
3100
  return [];
2955
3101
  }
2956
- const entries = await readdir4(assignmentsDir, { withFileTypes: true });
3102
+ const entries = await readdir5(assignmentsDir, { withFileTypes: true });
2957
3103
  const records = [];
2958
3104
  for (const entry of entries) {
2959
3105
  if (!entry.isDirectory()) {
2960
3106
  continue;
2961
3107
  }
2962
- const assignmentMd = resolve7(assignmentsDir, entry.name, "assignment.md");
3108
+ const assignmentMd = resolve8(assignmentsDir, entry.name, "assignment.md");
2963
3109
  if (!await fileExists(assignmentMd)) {
2964
3110
  continue;
2965
3111
  }
2966
- const content = await readFile6(assignmentMd, "utf-8");
3112
+ const content = await readFile7(assignmentMd, "utf-8");
2967
3113
  records.push(parseAssignmentFull(content));
2968
3114
  }
2969
3115
  records.sort((left, right) => compareTimestamps(right.updated, left.updated));
2970
3116
  return records;
2971
3117
  }
2972
3118
  async function listResources(projectPath) {
2973
- const resourcesDir = resolve7(projectPath, "resources");
3119
+ const resourcesDir = resolve8(projectPath, "resources");
2974
3120
  if (!await fileExists(resourcesDir)) {
2975
3121
  return [];
2976
3122
  }
2977
- const entries = await readdir4(resourcesDir, { withFileTypes: true });
3123
+ const entries = await readdir5(resourcesDir, { withFileTypes: true });
2978
3124
  const results = [];
2979
3125
  for (const entry of entries) {
2980
3126
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
2981
3127
  continue;
2982
3128
  }
2983
- const filePath = resolve7(resourcesDir, entry.name);
2984
- const content = await readFile6(filePath, "utf-8");
3129
+ const filePath = resolve8(resourcesDir, entry.name);
3130
+ const content = await readFile7(filePath, "utf-8");
2985
3131
  const parsed = parseResource(content);
2986
3132
  results.push({
2987
3133
  name: parsed.name,
@@ -2996,18 +3142,18 @@ async function listResources(projectPath) {
2996
3142
  return results;
2997
3143
  }
2998
3144
  async function listMemories(projectPath) {
2999
- const memoriesDir = resolve7(projectPath, "memories");
3145
+ const memoriesDir = resolve8(projectPath, "memories");
3000
3146
  if (!await fileExists(memoriesDir)) {
3001
3147
  return [];
3002
3148
  }
3003
- const entries = await readdir4(memoriesDir, { withFileTypes: true });
3149
+ const entries = await readdir5(memoriesDir, { withFileTypes: true });
3004
3150
  const results = [];
3005
3151
  for (const entry of entries) {
3006
3152
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
3007
3153
  continue;
3008
3154
  }
3009
- const filePath = resolve7(memoriesDir, entry.name);
3010
- const content = await readFile6(filePath, "utf-8");
3155
+ const filePath = resolve8(memoriesDir, entry.name);
3156
+ const content = await readFile7(filePath, "utf-8");
3011
3157
  const parsed = parseMemory(content);
3012
3158
  results.push({
3013
3159
  name: parsed.name,
@@ -3022,9 +3168,9 @@ async function listMemories(projectPath) {
3022
3168
  return results;
3023
3169
  }
3024
3170
  async function loadDependencyGraph(projectPath, assignments) {
3025
- const statusPath = resolve7(projectPath, "_status.md");
3171
+ const statusPath = resolve8(projectPath, "_status.md");
3026
3172
  if (await fileExists(statusPath)) {
3027
- const statusContent = await readFile6(statusPath, "utf-8");
3173
+ const statusContent = await readFile7(statusPath, "utf-8");
3028
3174
  const parsed = parseStatus(statusContent);
3029
3175
  const derivedGraph = extractMermaidGraph(parsed.body);
3030
3176
  if (derivedGraph) {
@@ -3124,7 +3270,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
3124
3270
  const config = await getStatusConfig();
3125
3271
  const transitionDefs = getTransitionDefinitions(config);
3126
3272
  const actions = [];
3127
- const projectPath = resolve7(projectsDir, projectSlug);
3273
+ const projectPath = resolve8(projectsDir, projectSlug);
3128
3274
  for (const definition of transitionDefs) {
3129
3275
  let warning = null;
3130
3276
  if (definition.command === "start" && !assignment.assignee) {
@@ -3154,12 +3300,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
3154
3300
  const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
3155
3301
  const unmet = [];
3156
3302
  for (const dependency of dependsOn) {
3157
- const dependencyPath = resolve7(projectPath, "assignments", dependency, "assignment.md");
3303
+ const dependencyPath = resolve8(projectPath, "assignments", dependency, "assignment.md");
3158
3304
  if (!await fileExists(dependencyPath)) {
3159
3305
  unmet.push(`${dependency} (missing)`);
3160
3306
  continue;
3161
3307
  }
3162
- const content = await readFile6(dependencyPath, "utf-8");
3308
+ const content = await readFile7(dependencyPath, "utf-8");
3163
3309
  const parsed = parseAssignmentFull(content);
3164
3310
  if (!terminals.has(parsed.status)) {
3165
3311
  unmet.push(`${dependency} (${parsed.status})`);
@@ -3314,7 +3460,7 @@ function isStale(updated) {
3314
3460
  return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
3315
3461
  }
3316
3462
  async function countOpenQuestions(projectPath, assignmentSlug) {
3317
- const commentsPath = resolve7(
3463
+ const commentsPath = resolve8(
3318
3464
  projectPath,
3319
3465
  "assignments",
3320
3466
  assignmentSlug,
@@ -3324,7 +3470,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
3324
3470
  return 0;
3325
3471
  }
3326
3472
  try {
3327
- const content = await readFile6(commentsPath, "utf-8");
3473
+ const content = await readFile7(commentsPath, "utf-8");
3328
3474
  const parsed = parseComments(content);
3329
3475
  return parsed.entries.filter(
3330
3476
  (e) => e.type === "question" && e.resolved !== true
@@ -3345,17 +3491,17 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
3345
3491
  function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
3346
3492
  switch (documentType) {
3347
3493
  case "project":
3348
- return resolve7(projectsDir, projectSlug, "project.md");
3494
+ return resolve8(projectsDir, projectSlug, "project.md");
3349
3495
  case "assignment":
3350
- return assignmentSlug ? resolve7(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
3496
+ return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
3351
3497
  case "plan":
3352
- return assignmentSlug ? resolve7(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
3498
+ return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
3353
3499
  case "scratchpad":
3354
- return assignmentSlug ? resolve7(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
3500
+ return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
3355
3501
  case "handoff":
3356
- return assignmentSlug ? resolve7(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
3502
+ return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
3357
3503
  case "decision-record":
3358
- return assignmentSlug ? resolve7(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
3504
+ return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
3359
3505
  default:
3360
3506
  return null;
3361
3507
  }
@@ -3382,12 +3528,12 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
3382
3528
  }
3383
3529
  async function listPlaybooks(playbooksDir2) {
3384
3530
  if (!await fileExists(playbooksDir2)) return [];
3385
- const entries = await readdir4(playbooksDir2, { withFileTypes: true });
3531
+ const entries = await readdir5(playbooksDir2, { withFileTypes: true });
3386
3532
  const playbooks = [];
3387
3533
  for (const entry of entries) {
3388
3534
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
3389
- const filePath = resolve7(playbooksDir2, entry.name);
3390
- const raw = await readFile6(filePath, "utf-8");
3535
+ const filePath = resolve8(playbooksDir2, entry.name);
3536
+ const raw = await readFile7(filePath, "utf-8");
3391
3537
  const parsed = parsePlaybook(raw);
3392
3538
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
3393
3539
  playbooks.push({
@@ -3403,9 +3549,9 @@ async function listPlaybooks(playbooksDir2) {
3403
3549
  return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
3404
3550
  }
3405
3551
  async function getPlaybookDetail(playbooksDir2, slug) {
3406
- const filePath = resolve7(playbooksDir2, `${slug}.md`);
3552
+ const filePath = resolve8(playbooksDir2, `${slug}.md`);
3407
3553
  if (!await fileExists(filePath)) return null;
3408
- const raw = await readFile6(filePath, "utf-8");
3554
+ const raw = await readFile7(filePath, "utf-8");
3409
3555
  const parsed = parsePlaybook(raw);
3410
3556
  return {
3411
3557
  slug: parsed.slug || slug,
@@ -3418,13 +3564,14 @@ async function getPlaybookDetail(playbooksDir2, slug) {
3418
3564
  body: parsed.body
3419
3565
  };
3420
3566
  }
3421
- var STALE_ASSIGNMENT_MS, ATTENTION_PAGE_LIMIT, OVERVIEW_ATTENTION_LIMIT, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, DEFAULT_GRAPH_COLORS;
3567
+ var STALE_ASSIGNMENT_MS, ATTENTION_PAGE_LIMIT, OVERVIEW_ATTENTION_LIMIT, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
3422
3568
  var init_api = __esm({
3423
3569
  "src/dashboard/api.ts"() {
3424
3570
  "use strict";
3425
3571
  init_lifecycle();
3426
3572
  init_fs();
3427
3573
  init_config2();
3574
+ init_fs_migration();
3428
3575
  init_assignment_resolver();
3429
3576
  init_parser();
3430
3577
  init_help();
@@ -3487,6 +3634,7 @@ var init_api = __esm({
3487
3634
  };
3488
3635
  _cachedConfig = null;
3489
3636
  REFERENCED_BY_LIMIT = 50;
3637
+ migratedProjectsDirs = /* @__PURE__ */ new Set();
3490
3638
  DEFAULT_GRAPH_COLORS = {
3491
3639
  completed: "fill:#4ea84f,stroke:#1f6b29,color:#ffffff",
3492
3640
  in_progress: "fill:#1e6fd9,stroke:#0f3f8f,color:#ffffff",
@@ -3519,8 +3667,8 @@ __export(parser_exports, {
3519
3667
  writeChecklist: () => writeChecklist
3520
3668
  });
3521
3669
  import { randomBytes } from "crypto";
3522
- import { readFile as readFile11 } from "fs/promises";
3523
- import { resolve as resolve14 } from "path";
3670
+ import { readFile as readFile12 } from "fs/promises";
3671
+ import { resolve as resolve15 } from "path";
3524
3672
  function generateShortId() {
3525
3673
  return randomBytes(2).toString("hex");
3526
3674
  }
@@ -3680,10 +3828,10 @@ function serializeLogEntry(entry) {
3680
3828
  return lines.join("\n");
3681
3829
  }
3682
3830
  function checklistPath(todosDir2, workspace) {
3683
- return resolve14(todosDir2, `${workspace}.md`);
3831
+ return resolve15(todosDir2, `${workspace}.md`);
3684
3832
  }
3685
3833
  function logPath(todosDir2, workspace) {
3686
- return resolve14(todosDir2, `${workspace}-log.md`);
3834
+ return resolve15(todosDir2, `${workspace}-log.md`);
3687
3835
  }
3688
3836
  function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
3689
3837
  const year = now.getFullYear();
@@ -3707,14 +3855,14 @@ function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new D
3707
3855
  default:
3708
3856
  suffix = `${year}-${month}-${day}`;
3709
3857
  }
3710
- return resolve14(todosDir2, "archive", `${workspace}-${suffix}.md`);
3858
+ return resolve15(todosDir2, "archive", `${workspace}-${suffix}.md`);
3711
3859
  }
3712
3860
  async function readChecklist(todosDir2, workspace) {
3713
3861
  const path = checklistPath(todosDir2, workspace);
3714
3862
  if (!await fileExists(path)) {
3715
3863
  return { workspace, archiveInterval: "weekly", items: [] };
3716
3864
  }
3717
- const content = await readFile11(path, "utf-8");
3865
+ const content = await readFile12(path, "utf-8");
3718
3866
  return parseChecklist(content);
3719
3867
  }
3720
3868
  async function writeChecklist(todosDir2, checklist) {
@@ -3727,7 +3875,7 @@ async function readLog(todosDir2, workspace) {
3727
3875
  if (!await fileExists(path)) {
3728
3876
  return { workspace, entries: [] };
3729
3877
  }
3730
- const content = await readFile11(path, "utf-8");
3878
+ const content = await readFile12(path, "utf-8");
3731
3879
  return parseLog(content);
3732
3880
  }
3733
3881
  async function appendLogEntry2(todosDir2, workspace, entry) {
@@ -3735,7 +3883,7 @@ async function appendLogEntry2(todosDir2, workspace, entry) {
3735
3883
  const path = logPath(todosDir2, workspace);
3736
3884
  let content;
3737
3885
  if (await fileExists(path)) {
3738
- content = await readFile11(path, "utf-8");
3886
+ content = await readFile12(path, "utf-8");
3739
3887
  content = content.trimEnd() + "\n\n" + serializeLogEntry(entry) + "\n";
3740
3888
  } else {
3741
3889
  const fm = `---
@@ -3774,21 +3922,21 @@ init_api();
3774
3922
  init_assignment_resolver();
3775
3923
  import express from "express";
3776
3924
  import { createServer } from "http";
3777
- import { resolve as resolve16 } from "path";
3778
- import { writeFile as writeFile4, unlink as unlink4 } from "fs/promises";
3925
+ import { resolve as resolve17 } from "path";
3926
+ import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
3779
3927
  import { WebSocketServer, WebSocket } from "ws";
3780
3928
 
3781
3929
  // src/dashboard/agent-sessions.ts
3782
3930
  init_fs();
3783
- import { readFile as readFile7 } from "fs/promises";
3784
- import { resolve as resolve9 } from "path";
3931
+ import { readFile as readFile8 } from "fs/promises";
3932
+ import { resolve as resolve10 } from "path";
3785
3933
 
3786
3934
  // src/dashboard/session-db.ts
3787
3935
  init_paths();
3788
3936
  init_fs();
3789
3937
  import Database from "better-sqlite3";
3790
- import { resolve as resolve8 } from "path";
3791
- import { readdir as readdir5 } from "fs/promises";
3938
+ import { resolve as resolve9 } from "path";
3939
+ import { readdir as readdir6 } from "fs/promises";
3792
3940
  var db = null;
3793
3941
  var SCHEMA_VERSION = "3";
3794
3942
  var SCHEMA_SQL = `
@@ -3806,14 +3954,16 @@ CREATE TABLE IF NOT EXISTS sessions (
3806
3954
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
3807
3955
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3808
3956
  );
3809
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
3810
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
3811
3957
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
3812
3958
  CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
3813
3959
  `;
3960
+ var POST_MIGRATION_INDEXES_SQL = `
3961
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
3962
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
3963
+ `;
3814
3964
  function initSessionDb(dbPath) {
3815
3965
  if (db) return db;
3816
- const finalPath = dbPath ?? resolve8(syntaurRoot(), "syntaur.db");
3966
+ const finalPath = dbPath ?? resolve9(syntaurRoot(), "syntaur.db");
3817
3967
  db = new Database(finalPath);
3818
3968
  db.pragma("journal_mode = WAL");
3819
3969
  db.exec(SCHEMA_SQL);
@@ -3821,57 +3971,74 @@ function initSessionDb(dbPath) {
3821
3971
  "schema_version",
3822
3972
  SCHEMA_VERSION
3823
3973
  );
3824
- const currentVersion = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
3825
- if (currentVersion?.value === "1") {
3826
- db.exec(`
3827
- CREATE TABLE sessions_v2 (
3828
- session_id TEXT PRIMARY KEY,
3829
- project_slug TEXT,
3830
- assignment_slug TEXT,
3831
- agent TEXT NOT NULL,
3832
- started TEXT NOT NULL,
3833
- ended TEXT,
3834
- status TEXT NOT NULL DEFAULT 'active',
3835
- path TEXT,
3836
- description TEXT,
3837
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
3838
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3839
- );
3840
- INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
3841
- DROP TABLE sessions;
3842
- ALTER TABLE sessions_v2 RENAME TO sessions;
3843
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
3844
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
3845
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
3846
- UPDATE meta SET value = '2' WHERE key = 'schema_version';
3847
- `);
3848
- }
3849
- const versionAfterV1 = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
3850
- if (versionAfterV1?.value === "2") {
3851
- db.exec(`
3852
- CREATE TABLE sessions_v3 (
3853
- session_id TEXT PRIMARY KEY,
3854
- project_slug TEXT,
3855
- assignment_slug TEXT,
3856
- agent TEXT NOT NULL,
3857
- started TEXT NOT NULL,
3858
- ended TEXT,
3859
- status TEXT NOT NULL DEFAULT 'active',
3860
- path TEXT,
3861
- description TEXT,
3862
- transcript_path TEXT,
3863
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
3864
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3865
- );
3866
- INSERT INTO sessions_v3 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at FROM sessions;
3867
- DROP TABLE sessions;
3868
- ALTER TABLE sessions_v3 RENAME TO sessions;
3869
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
3870
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
3871
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
3872
- UPDATE meta SET value = '3' WHERE key = 'schema_version';
3873
- `);
3874
- }
3974
+ const database = db;
3975
+ const runMigrations = database.transaction(() => {
3976
+ const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
3977
+ if (vBeforeV2 === "1") {
3978
+ database.exec(`
3979
+ CREATE TABLE sessions_v2 (
3980
+ session_id TEXT PRIMARY KEY,
3981
+ project_slug TEXT,
3982
+ assignment_slug TEXT,
3983
+ agent TEXT NOT NULL,
3984
+ started TEXT NOT NULL,
3985
+ ended TEXT,
3986
+ status TEXT NOT NULL DEFAULT 'active',
3987
+ path TEXT,
3988
+ description TEXT,
3989
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3990
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3991
+ );
3992
+ INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
3993
+ DROP TABLE sessions;
3994
+ ALTER TABLE sessions_v2 RENAME TO sessions;
3995
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
3996
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
3997
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
3998
+ UPDATE meta SET value = '2' WHERE key = 'schema_version';
3999
+ `);
4000
+ }
4001
+ const vBeforeV3 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
4002
+ if (vBeforeV3 === "2") {
4003
+ const v2Columns = database.prepare("PRAGMA table_info(sessions)").all();
4004
+ const v2ColNames = v2Columns.map((c) => c.name);
4005
+ const hasProject = v2ColNames.includes("project_slug");
4006
+ const hasMission = v2ColNames.includes("mission_slug");
4007
+ const projectSlugExpr = hasProject && hasMission ? "COALESCE(project_slug, mission_slug)" : hasProject ? "project_slug" : hasMission ? "mission_slug" : null;
4008
+ if (!projectSlugExpr) {
4009
+ throw new Error(
4010
+ "sessions table has neither project_slug nor mission_slug; cannot migrate from v2 to v3"
4011
+ );
4012
+ }
4013
+ database.exec(`
4014
+ CREATE TABLE sessions_v3 (
4015
+ session_id TEXT PRIMARY KEY,
4016
+ project_slug TEXT,
4017
+ assignment_slug TEXT,
4018
+ agent TEXT NOT NULL,
4019
+ started TEXT NOT NULL,
4020
+ ended TEXT,
4021
+ status TEXT NOT NULL DEFAULT 'active',
4022
+ path TEXT,
4023
+ description TEXT,
4024
+ transcript_path TEXT,
4025
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4026
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4027
+ );
4028
+ INSERT INTO sessions_v3
4029
+ SELECT session_id, ${projectSlugExpr}, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at
4030
+ FROM sessions;
4031
+ DROP TABLE sessions;
4032
+ ALTER TABLE sessions_v3 RENAME TO sessions;
4033
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
4034
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
4035
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
4036
+ UPDATE meta SET value = '3' WHERE key = 'schema_version';
4037
+ `);
4038
+ }
4039
+ });
4040
+ runMigrations.exclusive();
4041
+ db.exec(POST_MIGRATION_INDEXES_SQL);
3875
4042
  return db;
3876
4043
  }
3877
4044
  function getSessionDb() {
@@ -3893,12 +4060,12 @@ async function migrateFromMarkdown(projectsDir) {
3893
4060
  const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
3894
4061
  if (count.count > 0) return 0;
3895
4062
  if (!await fileExists(projectsDir)) return 0;
3896
- const entries = await readdir5(projectsDir, { withFileTypes: true });
4063
+ const entries = await readdir6(projectsDir, { withFileTypes: true });
3897
4064
  const allSessions = [];
3898
4065
  for (const entry of entries) {
3899
4066
  if (!entry.isDirectory()) continue;
3900
- const projectDir = resolve8(projectsDir, entry.name);
3901
- const indexPath = resolve8(projectDir, "_index-sessions.md");
4067
+ const projectDir = resolve9(projectsDir, entry.name);
4068
+ const indexPath = resolve9(projectDir, "_index-sessions.md");
3902
4069
  if (!await fileExists(indexPath)) continue;
3903
4070
  const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
3904
4071
  allSessions.push(...sessions);
@@ -3918,8 +4085,8 @@ async function migrateFromMarkdown(projectsDir) {
3918
4085
  return allSessions.length;
3919
4086
  }
3920
4087
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
3921
- const { readFile: readFile13 } = await import("fs/promises");
3922
- const raw = await readFile13(filePath, "utf-8");
4088
+ const { readFile: readFile14 } = await import("fs/promises");
4089
+ const raw = await readFile14(filePath, "utf-8");
3923
4090
  const sessions = [];
3924
4091
  const lines = raw.split("\n");
3925
4092
  let inTable = false;
@@ -3975,13 +4142,13 @@ async function appendSession(_projectDir, session) {
3975
4142
  INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
3976
4143
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3977
4144
  ON CONFLICT(session_id) DO UPDATE SET
3978
- project_slug = COALESCE(excluded.project_slug, project_slug),
3979
- assignment_slug = COALESCE(excluded.assignment_slug, assignment_slug),
4145
+ project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
4146
+ assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
3980
4147
  agent = excluded.agent,
3981
4148
  status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
3982
- path = COALESCE(excluded.path, path),
3983
- description = COALESCE(excluded.description, description),
3984
- transcript_path = COALESCE(excluded.transcript_path, transcript_path),
4149
+ path = COALESCE(NULLIF(excluded.path, ''), path),
4150
+ description = COALESCE(NULLIF(excluded.description, ''), description),
4151
+ transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
3985
4152
  updated_at = datetime('now')
3986
4153
  `).run(
3987
4154
  session.sessionId,
@@ -4031,13 +4198,13 @@ async function deleteSessions(sessionIds) {
4031
4198
  var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
4032
4199
  async function readAssignmentStatusFromPath(assignmentMdPath) {
4033
4200
  if (!await fileExists(assignmentMdPath)) return null;
4034
- const raw = await readFile7(assignmentMdPath, "utf-8");
4201
+ const raw = await readFile8(assignmentMdPath, "utf-8");
4035
4202
  const match = raw.match(/^status:\s*(.+)$/m);
4036
4203
  return match ? match[1].trim() : null;
4037
4204
  }
4038
4205
  async function readAssignmentStatus(projectDir, assignmentSlug) {
4039
4206
  return readAssignmentStatusFromPath(
4040
- resolve9(projectDir, "assignments", assignmentSlug, "assignment.md")
4207
+ resolve10(projectDir, "assignments", assignmentSlug, "assignment.md")
4041
4208
  );
4042
4209
  }
4043
4210
  async function reconcileActiveSessions(projectsDir, assignmentsDir) {
@@ -4055,13 +4222,13 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir) {
4055
4222
  seen.add(key);
4056
4223
  if (session.project_slug) {
4057
4224
  const status = await readAssignmentStatus(
4058
- resolve9(projectsDir, session.project_slug),
4225
+ resolve10(projectsDir, session.project_slug),
4059
4226
  aslug
4060
4227
  );
4061
4228
  if (status) assignmentStatuses.set(key, status);
4062
4229
  } else if (assignmentsDir) {
4063
4230
  const status = await readAssignmentStatusFromPath(
4064
- resolve9(assignmentsDir, aslug, "assignment.md")
4231
+ resolve10(assignmentsDir, aslug, "assignment.md")
4065
4232
  );
4066
4233
  if (status) assignmentStatuses.set(key, status);
4067
4234
  }
@@ -4275,8 +4442,8 @@ init_config2();
4275
4442
  // src/dashboard/api-write.ts
4276
4443
  init_lifecycle();
4277
4444
  import { Router } from "express";
4278
- import { resolve as resolve10 } from "path";
4279
- import { rm, readFile as readFile8 } from "fs/promises";
4445
+ import { resolve as resolve11 } from "path";
4446
+ import { rm, readFile as readFile9 } from "fs/promises";
4280
4447
 
4281
4448
  // src/utils/slug.ts
4282
4449
  function isValidSlug(slug) {
@@ -4799,7 +4966,7 @@ async function readCurrentDocument(filePath) {
4799
4966
  if (!await fileExists(filePath)) {
4800
4967
  return null;
4801
4968
  }
4802
- return readFile8(filePath, "utf-8");
4969
+ return readFile9(filePath, "utf-8");
4803
4970
  }
4804
4971
  function createWriteRouter(projectsDir, assignmentsDir) {
4805
4972
  const router = Router();
@@ -4929,26 +5096,26 @@ function createWriteRouter(projectsDir, assignmentsDir) {
4929
5096
  res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
4930
5097
  return;
4931
5098
  }
4932
- const projectDir = resolve10(projectsDir, slug);
5099
+ const projectDir = resolve11(projectsDir, slug);
4933
5100
  if (await fileExists(projectDir)) {
4934
5101
  res.status(409).json({ error: `Project "${slug}" already exists` });
4935
5102
  return;
4936
5103
  }
4937
5104
  const title = fields.title;
4938
5105
  const timestamp = fields.created || nowTimestamp();
4939
- await ensureDir(resolve10(projectDir, "assignments"));
4940
- await ensureDir(resolve10(projectDir, "resources"));
4941
- await ensureDir(resolve10(projectDir, "memories"));
4942
- await writeFileForce(resolve10(projectDir, "project.md"), content);
5106
+ await ensureDir(resolve11(projectDir, "assignments"));
5107
+ await ensureDir(resolve11(projectDir, "resources"));
5108
+ await ensureDir(resolve11(projectDir, "memories"));
5109
+ await writeFileForce(resolve11(projectDir, "project.md"), content);
4943
5110
  try {
4944
5111
  const companions = [
4945
- [resolve10(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
4946
- [resolve10(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
4947
- [resolve10(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
4948
- [resolve10(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
4949
- [resolve10(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
4950
- [resolve10(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
4951
- [resolve10(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
5112
+ [resolve11(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
5113
+ [resolve11(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
5114
+ [resolve11(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
5115
+ [resolve11(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
5116
+ [resolve11(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
5117
+ [resolve11(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
5118
+ [resolve11(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
4952
5119
  ];
4953
5120
  for (const [filePath, fileContent] of companions) {
4954
5121
  await writeFileForce(filePath, fileContent);
@@ -4969,8 +5136,8 @@ function createWriteRouter(projectsDir, assignmentsDir) {
4969
5136
  router.post("/api/projects/:slug/assignments", async (req, res) => {
4970
5137
  try {
4971
5138
  const projectSlug = getParam(req.params.slug);
4972
- const projectDir = resolve10(projectsDir, projectSlug);
4973
- const projectMdPath = resolve10(projectDir, "project.md");
5139
+ const projectDir = resolve11(projectsDir, projectSlug);
5140
+ const projectMdPath = resolve11(projectDir, "project.md");
4974
5141
  if (!await fileExists(projectMdPath)) {
4975
5142
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
4976
5143
  return;
@@ -5000,7 +5167,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5000
5167
  res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
5001
5168
  return;
5002
5169
  }
5003
- const assignmentDir = resolve10(projectDir, "assignments", assignmentSlug);
5170
+ const assignmentDir = resolve11(projectDir, "assignments", assignmentSlug);
5004
5171
  if (await fileExists(assignmentDir)) {
5005
5172
  res.status(409).json({
5006
5173
  error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
@@ -5009,12 +5176,12 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5009
5176
  }
5010
5177
  const timestamp = fields.created || nowTimestamp();
5011
5178
  await ensureDir(assignmentDir);
5012
- await writeFileForce(resolve10(assignmentDir, "assignment.md"), content);
5179
+ await writeFileForce(resolve11(assignmentDir, "assignment.md"), content);
5013
5180
  try {
5014
5181
  const companions = [
5015
- [resolve10(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
5016
- [resolve10(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
5017
- [resolve10(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
5182
+ [resolve11(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
5183
+ [resolve11(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
5184
+ [resolve11(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
5018
5185
  ];
5019
5186
  for (const [filePath, fileContent] of companions) {
5020
5187
  await writeFileForce(filePath, fileContent);
@@ -5035,7 +5202,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5035
5202
  router.patch("/api/projects/:slug", async (req, res) => {
5036
5203
  try {
5037
5204
  const projectSlug = getParam(req.params.slug);
5038
- const projectPath = resolve10(projectsDir, projectSlug, "project.md");
5205
+ const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5039
5206
  const currentContent = await readCurrentDocument(projectPath);
5040
5207
  if (!currentContent) {
5041
5208
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
@@ -5068,7 +5235,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5068
5235
  try {
5069
5236
  const projectSlug = getParam(req.params.slug);
5070
5237
  const assignmentSlug = getParam(req.params.aslug);
5071
- const assignmentPath = resolve10(
5238
+ const assignmentPath = resolve11(
5072
5239
  projectsDir,
5073
5240
  projectSlug,
5074
5241
  "assignments",
@@ -5111,7 +5278,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5111
5278
  try {
5112
5279
  const projectSlug = getParam(req.params.slug);
5113
5280
  const assignmentSlug = getParam(req.params.aslug);
5114
- const assignmentPath = resolve10(
5281
+ const assignmentPath = resolve11(
5115
5282
  projectsDir,
5116
5283
  projectSlug,
5117
5284
  "assignments",
@@ -5147,7 +5314,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5147
5314
  try {
5148
5315
  const projectSlug = getParam(req.params.slug);
5149
5316
  const assignmentSlug = getParam(req.params.aslug);
5150
- const planPath = resolve10(
5317
+ const planPath = resolve11(
5151
5318
  projectsDir,
5152
5319
  projectSlug,
5153
5320
  "assignments",
@@ -5185,7 +5352,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5185
5352
  try {
5186
5353
  const projectSlug = getParam(req.params.slug);
5187
5354
  const assignmentSlug = getParam(req.params.aslug);
5188
- const scratchpadPath = resolve10(
5355
+ const scratchpadPath = resolve11(
5189
5356
  projectsDir,
5190
5357
  projectSlug,
5191
5358
  "assignments",
@@ -5223,7 +5390,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5223
5390
  try {
5224
5391
  const projectSlug = getParam(req.params.slug);
5225
5392
  const assignmentSlug = getParam(req.params.aslug);
5226
- const handoffPath = resolve10(
5393
+ const handoffPath = resolve11(
5227
5394
  projectsDir,
5228
5395
  projectSlug,
5229
5396
  "assignments",
@@ -5261,7 +5428,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5261
5428
  try {
5262
5429
  const projectSlug = getParam(req.params.slug);
5263
5430
  const assignmentSlug = getParam(req.params.aslug);
5264
- const decisionPath = resolve10(
5431
+ const decisionPath = resolve11(
5265
5432
  projectsDir,
5266
5433
  projectSlug,
5267
5434
  "assignments",
@@ -5299,7 +5466,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5299
5466
  try {
5300
5467
  const projectSlug = getParam(req.params.slug);
5301
5468
  const assignmentSlug = getParam(req.params.aslug);
5302
- const commentsPath = resolve10(
5469
+ const commentsPath = resolve11(
5303
5470
  projectsDir,
5304
5471
  projectSlug,
5305
5472
  "assignments",
@@ -5317,7 +5484,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5317
5484
  let currentContent;
5318
5485
  let currentCount = 0;
5319
5486
  if (await fileExists(commentsPath)) {
5320
- currentContent = await readFile8(commentsPath, "utf-8");
5487
+ currentContent = await readFile9(commentsPath, "utf-8");
5321
5488
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
5322
5489
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
5323
5490
  } else {
@@ -5358,7 +5525,7 @@ ${entry}`;
5358
5525
  const projectSlug = getParam(req.params.slug);
5359
5526
  const assignmentSlug = getParam(req.params.aslug);
5360
5527
  const commentId = getParam(req.params.commentId);
5361
- const commentsPath = resolve10(
5528
+ const commentsPath = resolve11(
5362
5529
  projectsDir,
5363
5530
  projectSlug,
5364
5531
  "assignments",
@@ -5374,7 +5541,7 @@ ${entry}`;
5374
5541
  res.status(400).json({ error: "resolved (boolean) is required" });
5375
5542
  return;
5376
5543
  }
5377
- const content = await readFile8(commentsPath, "utf-8");
5544
+ const content = await readFile9(commentsPath, "utf-8");
5378
5545
  const parsed = parseComments(content);
5379
5546
  const target = parsed.entries.find((e) => e.id === commentId);
5380
5547
  if (!target) {
@@ -5409,7 +5576,7 @@ ${entry}`;
5409
5576
  router.post("/api/projects/:slug/move-workspace", async (req, res) => {
5410
5577
  try {
5411
5578
  const projectSlug = getParam(req.params.slug);
5412
- const projectPath = resolve10(projectsDir, projectSlug, "project.md");
5579
+ const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5413
5580
  if (!await fileExists(projectPath)) {
5414
5581
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5415
5582
  return;
@@ -5419,7 +5586,7 @@ ${entry}`;
5419
5586
  res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
5420
5587
  return;
5421
5588
  }
5422
- let content = await readFile8(projectPath, "utf-8");
5589
+ let content = await readFile9(projectPath, "utf-8");
5423
5590
  content = setTopLevelField(content, "workspace", workspace ?? null);
5424
5591
  content = setTopLevelField(content, "updated", nowTimestamp());
5425
5592
  await writeFileForce(projectPath, content);
@@ -5433,7 +5600,7 @@ ${entry}`;
5433
5600
  router.post("/api/projects/:slug/status-override", async (req, res) => {
5434
5601
  try {
5435
5602
  const projectSlug = getParam(req.params.slug);
5436
- const projectPath = resolve10(projectsDir, projectSlug, "project.md");
5603
+ const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5437
5604
  if (!await fileExists(projectPath)) {
5438
5605
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5439
5606
  return;
@@ -5445,7 +5612,7 @@ ${entry}`;
5445
5612
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
5446
5613
  return;
5447
5614
  }
5448
- let content = await readFile8(projectPath, "utf-8");
5615
+ let content = await readFile9(projectPath, "utf-8");
5449
5616
  content = setTopLevelField(content, "statusOverride", status ?? null);
5450
5617
  content = setTopLevelField(content, "updated", nowTimestamp());
5451
5618
  await writeFileForce(projectPath, content);
@@ -5460,7 +5627,7 @@ ${entry}`;
5460
5627
  try {
5461
5628
  const projectSlug = getParam(req.params.slug);
5462
5629
  const assignmentSlug = getParam(req.params.aslug);
5463
- const assignmentPath = resolve10(
5630
+ const assignmentPath = resolve11(
5464
5631
  projectsDir,
5465
5632
  projectSlug,
5466
5633
  "assignments",
@@ -5478,7 +5645,7 @@ ${entry}`;
5478
5645
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
5479
5646
  return;
5480
5647
  }
5481
- let content = await readFile8(assignmentPath, "utf-8");
5648
+ let content = await readFile9(assignmentPath, "utf-8");
5482
5649
  content = setTopLevelField(content, "status", status);
5483
5650
  content = setTopLevelField(content, "updated", nowTimestamp());
5484
5651
  if (status !== "blocked") {
@@ -5503,8 +5670,8 @@ ${entry}`;
5503
5670
  res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
5504
5671
  return;
5505
5672
  }
5506
- const projectDir = resolve10(projectsDir, projectSlug);
5507
- const assignmentPath = resolve10(projectDir, "assignments", assignmentSlug, "assignment.md");
5673
+ const projectDir = resolve11(projectsDir, projectSlug);
5674
+ const assignmentPath = resolve11(projectDir, "assignments", assignmentSlug, "assignment.md");
5508
5675
  if (!await fileExists(assignmentPath)) {
5509
5676
  res.status(404).json({ error: "Assignment not found" });
5510
5677
  return;
@@ -5530,8 +5697,8 @@ ${entry}`;
5530
5697
  try {
5531
5698
  const projectSlug = getParam(req.params.slug);
5532
5699
  const assignmentSlug = getParam(req.params.aslug);
5533
- const assignmentDir = resolve10(projectsDir, projectSlug, "assignments", assignmentSlug);
5534
- const assignmentPath = resolve10(assignmentDir, "assignment.md");
5700
+ const assignmentDir = resolve11(projectsDir, projectSlug, "assignments", assignmentSlug);
5701
+ const assignmentPath = resolve11(assignmentDir, "assignment.md");
5535
5702
  if (!await fileExists(assignmentPath)) {
5536
5703
  res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
5537
5704
  return;
@@ -5560,7 +5727,7 @@ ${entry}`;
5560
5727
  return;
5561
5728
  }
5562
5729
  const id = generateId();
5563
- const assignmentDir = resolve10(assignmentsDir, id);
5730
+ const assignmentDir = resolve11(assignmentsDir, id);
5564
5731
  if (await fileExists(assignmentDir)) {
5565
5732
  res.status(500).json({ error: "UUID collision \u2014 try again" });
5566
5733
  return;
@@ -5580,25 +5747,25 @@ ${entry}`;
5580
5747
  project: null,
5581
5748
  type: typeof type === "string" ? type : void 0
5582
5749
  });
5583
- await writeFileForce(resolve10(assignmentDir, "assignment.md"), assignmentContent);
5750
+ await writeFileForce(resolve11(assignmentDir, "assignment.md"), assignmentContent);
5584
5751
  await writeFileForce(
5585
- resolve10(assignmentDir, "scratchpad.md"),
5752
+ resolve11(assignmentDir, "scratchpad.md"),
5586
5753
  renderScratchpad({ assignmentSlug: id, timestamp })
5587
5754
  );
5588
5755
  await writeFileForce(
5589
- resolve10(assignmentDir, "handoff.md"),
5756
+ resolve11(assignmentDir, "handoff.md"),
5590
5757
  renderHandoff({ assignmentSlug: id, timestamp })
5591
5758
  );
5592
5759
  await writeFileForce(
5593
- resolve10(assignmentDir, "decision-record.md"),
5760
+ resolve11(assignmentDir, "decision-record.md"),
5594
5761
  renderDecisionRecord({ assignmentSlug: id, timestamp })
5595
5762
  );
5596
5763
  await writeFileForce(
5597
- resolve10(assignmentDir, "progress.md"),
5764
+ resolve11(assignmentDir, "progress.md"),
5598
5765
  renderProgress({ assignment: id, timestamp })
5599
5766
  );
5600
5767
  await writeFileForce(
5601
- resolve10(assignmentDir, "comments.md"),
5768
+ resolve11(assignmentDir, "comments.md"),
5602
5769
  renderComments({ assignment: id, timestamp })
5603
5770
  );
5604
5771
  const detail = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
@@ -5726,7 +5893,7 @@ ${entry}`;
5726
5893
  res.status(404).json({ error: `Assignment "${id}" not found` });
5727
5894
  return;
5728
5895
  }
5729
- const assignmentPath = resolve10(resolved.assignmentDir, "assignment.md");
5896
+ const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
5730
5897
  const currentContent = await readCurrentDocument(assignmentPath);
5731
5898
  if (!currentContent) {
5732
5899
  res.status(404).json({ error: "Assignment not found" });
@@ -5768,7 +5935,7 @@ ${entry}`;
5768
5935
  res.status(404).json({ error: `Assignment "${id}" not found` });
5769
5936
  return;
5770
5937
  }
5771
- const planPath = resolve10(resolved.assignmentDir, "plan.md");
5938
+ const planPath = resolve11(resolved.assignmentDir, "plan.md");
5772
5939
  const currentContent = await readCurrentDocument(planPath);
5773
5940
  if (!currentContent) {
5774
5941
  res.status(404).json({ error: "Plan not found" });
@@ -5802,7 +5969,7 @@ ${entry}`;
5802
5969
  res.status(404).json({ error: `Assignment "${id}" not found` });
5803
5970
  return;
5804
5971
  }
5805
- const scratchpadPath = resolve10(resolved.assignmentDir, "scratchpad.md");
5972
+ const scratchpadPath = resolve11(resolved.assignmentDir, "scratchpad.md");
5806
5973
  const currentContent = await readCurrentDocument(scratchpadPath);
5807
5974
  if (!currentContent) {
5808
5975
  res.status(404).json({ error: "Scratchpad not found" });
@@ -5836,7 +6003,7 @@ ${entry}`;
5836
6003
  res.status(404).json({ error: `Assignment "${id}" not found` });
5837
6004
  return;
5838
6005
  }
5839
- const handoffPath = resolve10(resolved.assignmentDir, "handoff.md");
6006
+ const handoffPath = resolve11(resolved.assignmentDir, "handoff.md");
5840
6007
  const currentContent = await readCurrentDocument(handoffPath);
5841
6008
  if (!currentContent) {
5842
6009
  res.status(404).json({ error: "Handoff log not found" });
@@ -5876,7 +6043,7 @@ ${entry}`;
5876
6043
  res.status(404).json({ error: `Assignment "${id}" not found` });
5877
6044
  return;
5878
6045
  }
5879
- const decisionPath = resolve10(resolved.assignmentDir, "decision-record.md");
6046
+ const decisionPath = resolve11(resolved.assignmentDir, "decision-record.md");
5880
6047
  const currentContent = await readCurrentDocument(decisionPath);
5881
6048
  if (!currentContent) {
5882
6049
  res.status(404).json({ error: "Decision record not found" });
@@ -5916,7 +6083,7 @@ ${entry}`;
5916
6083
  res.status(404).json({ error: `Assignment "${id}" not found` });
5917
6084
  return;
5918
6085
  }
5919
- const assignmentPath = resolve10(resolved.assignmentDir, "assignment.md");
6086
+ const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
5920
6087
  if (!await fileExists(assignmentPath)) {
5921
6088
  res.status(404).json({ error: "Assignment not found" });
5922
6089
  return;
@@ -5928,7 +6095,7 @@ ${entry}`;
5928
6095
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
5929
6096
  return;
5930
6097
  }
5931
- let content = await readFile8(assignmentPath, "utf-8");
6098
+ let content = await readFile9(assignmentPath, "utf-8");
5932
6099
  content = setTopLevelField(content, "status", status);
5933
6100
  content = setTopLevelField(content, "updated", nowTimestamp());
5934
6101
  if (status !== "blocked") {
@@ -5954,7 +6121,7 @@ ${entry}`;
5954
6121
  res.status(404).json({ error: `Assignment "${id}" not found` });
5955
6122
  return;
5956
6123
  }
5957
- const assignmentPath = resolve10(resolved.assignmentDir, "assignment.md");
6124
+ const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
5958
6125
  const currentContent = await readCurrentDocument(assignmentPath);
5959
6126
  if (!currentContent) {
5960
6127
  res.status(404).json({ error: "Assignment not found" });
@@ -6019,7 +6186,7 @@ function slugifyLocal(input) {
6019
6186
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
6020
6187
  }
6021
6188
  async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
6022
- const commentsPath = resolve10(assignmentDir, "comments.md");
6189
+ const commentsPath = resolve11(assignmentDir, "comments.md");
6023
6190
  const { body, author, type, replyTo } = req.body || {};
6024
6191
  if (!body || typeof body !== "string" || !body.trim()) {
6025
6192
  res.status(400).json({ error: "body is required" });
@@ -6031,7 +6198,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
6031
6198
  let currentContent;
6032
6199
  let currentCount = 0;
6033
6200
  if (await fileExists(commentsPath)) {
6034
- currentContent = await readFile8(commentsPath, "utf-8");
6201
+ currentContent = await readFile9(commentsPath, "utf-8");
6035
6202
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
6036
6203
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
6037
6204
  } else {
@@ -6061,7 +6228,7 @@ ${entry}`;
6061
6228
  res.status(201).json({ assignment, comment: { id: comment.id } });
6062
6229
  }
6063
6230
  async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
6064
- const commentsPath = resolve10(assignmentDir, "comments.md");
6231
+ const commentsPath = resolve11(assignmentDir, "comments.md");
6065
6232
  if (!await fileExists(commentsPath)) {
6066
6233
  res.status(404).json({ error: "Comments file not found" });
6067
6234
  return;
@@ -6071,7 +6238,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
6071
6238
  res.status(400).json({ error: "resolved (boolean) is required" });
6072
6239
  return;
6073
6240
  }
6074
- const content = await readFile8(commentsPath, "utf-8");
6241
+ const content = await readFile9(commentsPath, "utf-8");
6075
6242
  const parsed = parseComments(content);
6076
6243
  const target = parsed.entries.find((e) => e.id === commentId);
6077
6244
  if (!target) {
@@ -6216,7 +6383,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
6216
6383
 
6217
6384
  // src/dashboard/api-agent-sessions.ts
6218
6385
  import { Router as Router3 } from "express";
6219
- import { resolve as resolve11 } from "path";
6386
+ import { resolve as resolve12 } from "path";
6220
6387
  init_fs();
6221
6388
  function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6222
6389
  const router = Router3();
@@ -6233,7 +6400,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6233
6400
  try {
6234
6401
  const { projectSlug } = req.params;
6235
6402
  const assignment = req.query.assignment;
6236
- const projectDir = resolve11(projectsDir, projectSlug);
6403
+ const projectDir = resolve12(projectsDir, projectSlug);
6237
6404
  if (!await fileExists(projectDir)) {
6238
6405
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
6239
6406
  return;
@@ -6259,7 +6426,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6259
6426
  return;
6260
6427
  }
6261
6428
  if (projectSlug) {
6262
- const projectDir = resolve11(projectsDir, projectSlug);
6429
+ const projectDir = resolve12(projectsDir, projectSlug);
6263
6430
  if (!await fileExists(projectDir)) {
6264
6431
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
6265
6432
  return;
@@ -6327,8 +6494,8 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6327
6494
  init_api();
6328
6495
  init_parser();
6329
6496
  import { Router as Router4 } from "express";
6330
- import { resolve as resolve13 } from "path";
6331
- import { readFile as readFile10, unlink as unlink2 } from "fs/promises";
6497
+ import { resolve as resolve14 } from "path";
6498
+ import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
6332
6499
  init_timestamp();
6333
6500
  init_fs();
6334
6501
 
@@ -6336,15 +6503,15 @@ init_fs();
6336
6503
  init_fs();
6337
6504
  init_parser();
6338
6505
  init_timestamp();
6339
- import { resolve as resolve12 } from "path";
6340
- import { readdir as readdir6, readFile as readFile9 } from "fs/promises";
6506
+ import { resolve as resolve13 } from "path";
6507
+ import { readdir as readdir7, readFile as readFile10 } from "fs/promises";
6341
6508
  async function rebuildPlaybookManifest(playbooksDir2) {
6342
6509
  if (!await fileExists(playbooksDir2)) return;
6343
- const entries = await readdir6(playbooksDir2, { withFileTypes: true });
6510
+ const entries = await readdir7(playbooksDir2, { withFileTypes: true });
6344
6511
  const rows = [];
6345
6512
  for (const entry of entries) {
6346
6513
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
6347
- const raw = await readFile9(resolve12(playbooksDir2, entry.name), "utf-8");
6514
+ const raw = await readFile10(resolve13(playbooksDir2, entry.name), "utf-8");
6348
6515
  const parsed = parsePlaybook(raw);
6349
6516
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
6350
6517
  rows.push({
@@ -6374,7 +6541,7 @@ async function rebuildPlaybookManifest(playbooksDir2) {
6374
6541
  }
6375
6542
  }
6376
6543
  lines.push("");
6377
- await writeFileForce(resolve12(playbooksDir2, "manifest.md"), lines.join("\n"));
6544
+ await writeFileForce(resolve13(playbooksDir2, "manifest.md"), lines.join("\n"));
6378
6545
  }
6379
6546
 
6380
6547
  // src/dashboard/api-playbooks.ts
@@ -6415,12 +6582,12 @@ function createPlaybooksRouter(playbooksDir2) {
6415
6582
  });
6416
6583
  router.get("/:slug/edit", async (req, res) => {
6417
6584
  try {
6418
- const filePath = resolve13(playbooksDir2, `${req.params.slug}.md`);
6585
+ const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6419
6586
  if (!await fileExists(filePath)) {
6420
6587
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6421
6588
  return;
6422
6589
  }
6423
- const content = await readFile10(filePath, "utf-8");
6590
+ const content = await readFile11(filePath, "utf-8");
6424
6591
  res.json({
6425
6592
  documentType: "playbook",
6426
6593
  title: `Edit Playbook: ${req.params.slug}`,
@@ -6445,7 +6612,7 @@ function createPlaybooksRouter(playbooksDir2) {
6445
6612
  return;
6446
6613
  }
6447
6614
  await ensureDir(playbooksDir2);
6448
- const filePath = resolve13(playbooksDir2, `${slug}.md`);
6615
+ const filePath = resolve14(playbooksDir2, `${slug}.md`);
6449
6616
  if (await fileExists(filePath)) {
6450
6617
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
6451
6618
  return;
@@ -6464,7 +6631,7 @@ function createPlaybooksRouter(playbooksDir2) {
6464
6631
  res.status(400).json({ error: "content is required" });
6465
6632
  return;
6466
6633
  }
6467
- const filePath = resolve13(playbooksDir2, `${req.params.slug}.md`);
6634
+ const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6468
6635
  if (!await fileExists(filePath)) {
6469
6636
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6470
6637
  return;
@@ -6482,7 +6649,7 @@ function createPlaybooksRouter(playbooksDir2) {
6482
6649
  res.status(403).json({ error: "The playbook manifest cannot be deleted" });
6483
6650
  return;
6484
6651
  }
6485
- const filePath = resolve13(playbooksDir2, `${req.params.slug}.md`);
6652
+ const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6486
6653
  if (!await fileExists(filePath)) {
6487
6654
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6488
6655
  return;
@@ -6497,11 +6664,14 @@ function createPlaybooksRouter(playbooksDir2) {
6497
6664
  return router;
6498
6665
  }
6499
6666
 
6667
+ // src/dashboard/server.ts
6668
+ init_fs_migration();
6669
+
6500
6670
  // src/dashboard/api-todos.ts
6501
6671
  init_parser2();
6502
6672
  init_fs();
6503
6673
  import { Router as Router5 } from "express";
6504
- import { readdir as readdir7 } from "fs/promises";
6674
+ import { readdir as readdir8 } from "fs/promises";
6505
6675
  var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
6506
6676
  function getWorkspaceParam(value) {
6507
6677
  if (Array.isArray(value)) {
@@ -6535,7 +6705,7 @@ function createTodosRouter(todosDir2, broadcast) {
6535
6705
  router.get("/", async (_req, res) => {
6536
6706
  try {
6537
6707
  await ensureDir(todosDir2);
6538
- const files = await readdir7(todosDir2).catch(() => []);
6708
+ const files = await readdir8(todosDir2).catch(() => []);
6539
6709
  const workspaces = [];
6540
6710
  for (const file of files) {
6541
6711
  if (typeof file !== "string") continue;
@@ -6640,8 +6810,8 @@ function createTodosRouter(todosDir2, broadcast) {
6640
6810
  router.post("/:workspace/archive", async (req, res) => {
6641
6811
  try {
6642
6812
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
6643
- const { resolve: resolve17 } = await import("path");
6644
- const { readFile: readFile13 } = await import("fs/promises");
6813
+ const { resolve: resolve18 } = await import("path");
6814
+ const { readFile: readFile14 } = await import("fs/promises");
6645
6815
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
6646
6816
  const workspace = getWorkspaceParam(req.params.workspace);
6647
6817
  const checklist = await readChecklist(todosDir2, workspace);
@@ -6657,10 +6827,10 @@ function createTodosRouter(todosDir2, broadcast) {
6657
6827
  (e) => e.itemIds.every((id) => completedIds.has(id))
6658
6828
  );
6659
6829
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
6660
- await ensureDir(resolve17(todosDir2, "archive"));
6830
+ await ensureDir(resolve18(todosDir2, "archive"));
6661
6831
  let archContent = "";
6662
6832
  if (await fileExists(archFile)) {
6663
- archContent = await readFile13(archFile, "utf-8");
6833
+ archContent = await readFile14(archFile, "utf-8");
6664
6834
  archContent = archContent.trimEnd() + "\n\n";
6665
6835
  } else {
6666
6836
  archContent = `---
@@ -6920,8 +7090,8 @@ init_fs();
6920
7090
  init_config2();
6921
7091
  import { execFile as execFile2 } from "child_process";
6922
7092
  import { promisify as promisify2 } from "util";
6923
- import { cp, mkdtemp, rm as rm2, readFile as readFile12, writeFile as writeFile3, unlink as unlink3, stat, open, rename as rename2 } from "fs/promises";
6924
- import { resolve as resolve15, join as join2 } from "path";
7093
+ import { cp, mkdtemp, rm as rm2, readFile as readFile13, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
7094
+ import { resolve as resolve16, join as join2 } from "path";
6925
7095
  import { tmpdir } from "os";
6926
7096
  var exec2 = promisify2(execFile2);
6927
7097
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -6961,7 +7131,7 @@ async function resolveCategoryPath(category) {
6961
7131
  case "servers":
6962
7132
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
6963
7133
  case "config":
6964
- return { sourcePath: resolve15(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
7134
+ return { sourcePath: resolve16(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
6965
7135
  }
6966
7136
  }
6967
7137
  async function checkGitInstalled() {
@@ -6972,7 +7142,7 @@ async function checkGitInstalled() {
6972
7142
  }
6973
7143
  }
6974
7144
  async function acquireLock() {
6975
- const lockPath = resolve15(syntaurRoot(), LOCK_FILE_NAME);
7145
+ const lockPath = resolve16(syntaurRoot(), LOCK_FILE_NAME);
6976
7146
  await ensureDir(syntaurRoot());
6977
7147
  try {
6978
7148
  const handle = await open(lockPath, "wx");
@@ -6981,7 +7151,7 @@ async function acquireLock() {
6981
7151
  return lockPath;
6982
7152
  } catch (err) {
6983
7153
  if (err.code === "EEXIST") {
6984
- const pid = await readFile12(lockPath, "utf-8").catch(() => "");
7154
+ const pid = await readFile13(lockPath, "utf-8").catch(() => "");
6985
7155
  throw new Error(
6986
7156
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
6987
7157
  );
@@ -7019,7 +7189,7 @@ async function copyRecursive(src, dest) {
7019
7189
  await ensureDir(dest);
7020
7190
  await cp(src, dest, { recursive: true, force: true });
7021
7191
  } else {
7022
- await ensureDir(resolve15(dest, ".."));
7192
+ await ensureDir(resolve16(dest, ".."));
7023
7193
  await cp(src, dest, { force: true });
7024
7194
  }
7025
7195
  }
@@ -7028,7 +7198,7 @@ function resolveCategoriesStrict(csv) {
7028
7198
  return parseCategoriesStrict(parts);
7029
7199
  }
7030
7200
  async function readSanitizedConfig(configPath) {
7031
- const content = await readFile12(configPath, "utf-8");
7201
+ const content = await readFile13(configPath, "utf-8");
7032
7202
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
7033
7203
  }
7034
7204
  async function backupToGithub(overrides) {
@@ -7067,8 +7237,8 @@ async function backupToGithub(overrides) {
7067
7237
  }
7068
7238
  if (category === "config") {
7069
7239
  const sanitized = await readSanitizedConfig(sourcePath);
7070
- await ensureDir(resolve15(destPath, ".."));
7071
- await writeFile3(destPath, sanitized, "utf-8");
7240
+ await ensureDir(resolve16(destPath, ".."));
7241
+ await writeFile4(destPath, sanitized, "utf-8");
7072
7242
  } else {
7073
7243
  await copyRecursive(sourcePath, destPath);
7074
7244
  }
@@ -7121,7 +7291,7 @@ async function backupToGithub(overrides) {
7121
7291
  }
7122
7292
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
7123
7293
  if (isFile) {
7124
- await ensureDir(resolve15(localPath, ".."));
7294
+ await ensureDir(resolve16(localPath, ".."));
7125
7295
  await cp(repoSrcPath, localPath, { force: true });
7126
7296
  return;
7127
7297
  }
@@ -7132,7 +7302,7 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
7132
7302
  const localExistsBefore = await fileExists(localPath);
7133
7303
  if (backupExistsBefore) {
7134
7304
  if (!localExistsBefore) {
7135
- await rename2(backupPath, localPath);
7305
+ await rename3(backupPath, localPath);
7136
7306
  } else {
7137
7307
  throw new Error(
7138
7308
  `Cannot restore "${localPath}": a stale crash-recovery backup exists at ${backupPath} while the current path also exists. Inspect both and remove the one you don't need, then retry.`
@@ -7144,15 +7314,15 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
7144
7314
  await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
7145
7315
  const localExists = await fileExists(localPath);
7146
7316
  if (localExists) {
7147
- await rename2(localPath, backupPath);
7317
+ await rename3(localPath, backupPath);
7148
7318
  localMovedAside = true;
7149
7319
  }
7150
- await rename2(stagingPath, localPath);
7320
+ await rename3(stagingPath, localPath);
7151
7321
  await rm2(backupPath, { recursive: true, force: true }).catch(() => {
7152
7322
  });
7153
7323
  } catch (err) {
7154
7324
  if (localMovedAside && await fileExists(backupPath)) {
7155
- await rename2(backupPath, localPath).catch(() => {
7325
+ await rename3(backupPath, localPath).catch(() => {
7156
7326
  });
7157
7327
  }
7158
7328
  await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
@@ -7222,7 +7392,7 @@ async function restoreFromGithub(overrides) {
7222
7392
  }
7223
7393
  async function getBackupStatus() {
7224
7394
  const config = await readConfig();
7225
- const lockPath = resolve15(syntaurRoot(), LOCK_FILE_NAME);
7395
+ const lockPath = resolve16(syntaurRoot(), LOCK_FILE_NAME);
7226
7396
  const locked = await fileExists(lockPath);
7227
7397
  return {
7228
7398
  repo: config.backup?.repo ?? null,
@@ -7556,6 +7726,18 @@ function createDashboardServer(options) {
7556
7726
  migrateFromMarkdown(projectsDir).catch((err) => {
7557
7727
  console.error("Session migration from markdown failed:", err);
7558
7728
  });
7729
+ (async () => {
7730
+ try {
7731
+ const configResult = await migrateLegacyConfig(
7732
+ resolve17(syntaurRoot(), "config.md")
7733
+ );
7734
+ const projectResult = await migrateLegacyProjectFiles(projectsDir);
7735
+ const summary = summarizeMigration(projectResult, configResult);
7736
+ if (summary) console.log(summary);
7737
+ } catch (err) {
7738
+ console.error("Legacy filesystem migration failed:", err);
7739
+ }
7740
+ })();
7559
7741
  app.use(express.json());
7560
7742
  app.get("/api/overview", async (_req, res) => {
7561
7743
  try {
@@ -7775,7 +7957,7 @@ function createDashboardServer(options) {
7775
7957
  if (serveStaticUi && dashboardDistPath) {
7776
7958
  app.use(express.static(dashboardDistPath));
7777
7959
  app.get("{*path}", async (_req, res) => {
7778
- const indexPath = resolve16(dashboardDistPath, "index.html");
7960
+ const indexPath = resolve17(dashboardDistPath, "index.html");
7779
7961
  if (await fileExists(indexPath)) {
7780
7962
  res.sendFile(indexPath);
7781
7963
  } else {
@@ -7808,8 +7990,8 @@ function createDashboardServer(options) {
7808
7990
  }
7809
7991
  });
7810
7992
  server.listen(port, () => {
7811
- const portFile = resolve16(syntaurRoot(), "dashboard-port");
7812
- writeFile4(portFile, String(port), "utf-8").catch(() => {
7993
+ const portFile = resolve17(syntaurRoot(), "dashboard-port");
7994
+ writeFile5(portFile, String(port), "utf-8").catch(() => {
7813
7995
  });
7814
7996
  resolvePromise();
7815
7997
  });
@@ -7825,7 +8007,7 @@ function createDashboardServer(options) {
7825
8007
  client.terminate();
7826
8008
  }
7827
8009
  clients.clear();
7828
- const portFile = resolve16(syntaurRoot(), "dashboard-port");
8010
+ const portFile = resolve17(syntaurRoot(), "dashboard-port");
7829
8011
  await unlink4(portFile).catch(() => {
7830
8012
  });
7831
8013
  server.closeAllConnections?.();