syntaur 0.17.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/dashboard/dist/assets/{_basePickBy-Dek5-ACb.js → _basePickBy-DiPrdxAJ.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-BeLddDKU.js → _baseUniq-CChFfAnf.js} +1 -1
  4. package/dashboard/dist/assets/{arc-kotdj4Uh.js → arc-CfkGktFw.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BaqFXQ-q.js → architectureDiagram-2XIMDMQ5-hFcMu5eO.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BSyGne6h.js → blockDiagram-WCTKOSBZ-GZDth9sZ.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-iYjZFKOW.js → c4Diagram-IC4MRINW-BVajqiYz.js} +1 -1
  8. package/dashboard/dist/assets/channel-C3CI1Xiu.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-DvY9Y7jB.js → chunk-4BX2VUAB-L7ajwHyw.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-2vCDm2cJ.js → chunk-55IACEB6-BACmeqT4.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-BVPaoRtV.js → chunk-FMBD7UC4-BYQ2kuuH.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-B9uJpMzd.js → chunk-JSJVCQXG-akulVsXk.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-CHcjjGaq.js → chunk-KX2RTZJC-4ibFxLB_.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-B8n19pSs.js → chunk-NQ4KR5QH-DU4iJf0Y.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-CF9ZYFGK.js → chunk-QZHKN3VN-ouyGBhQB.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-mY_1Vmdf.js → chunk-WL4C6EOR-Pq-ySwIY.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-Ck2tXWRf.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Ck2tXWRf.js +1 -0
  19. package/dashboard/dist/assets/clone-BEaO4u3F.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-DOYoG3gB.js → cose-bilkent-S5V4N54A-BfkMWgQb.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CZFrp-Yq.js → dagre-KLK3FWXG-0yhY8ZsO.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-F3rF5YwM.js → diagram-E7M64L7V-DBkanpFi.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-BynWv-Vo.js → diagram-IFDJBPK2-CrucCpxV.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-NIyzhm_M.js → diagram-P4PSJMXO-BJrQ6lK3.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-Ca9NbihE.js → erDiagram-INFDFZHY--joDpxAm.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-YhS0MOu4.js → flowDiagram-PKNHOUZH-BUXiVwju.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-G7P3qq_2.js → ganttDiagram-A5KZAMGK-u2XuIMRA.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-BJAK7M7E.js → gitGraphDiagram-K3NZZRJ6-Ayf0-gwT.js} +1 -1
  29. package/dashboard/dist/assets/{graph-11geMy3f.js → graph-BUGVXq3c.js} +1 -1
  30. package/dashboard/dist/assets/index-Bv6vn4_x.js +535 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-COcQYcLf.js → infoDiagram-LFFYTUFH-DYehx6g7.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-B7Jjyw8J.js → ishikawaDiagram-PHBUUO56-Bo8-BClf.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K--snU-QAX.js → journeyDiagram-4ABVD52K-DLm-bRYd.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-BucLoc89.js → kanban-definition-K7BYSVSG-stPdEvds.js} +1 -1
  35. package/dashboard/dist/assets/{layout-Bcgxdz8A.js → layout-B7aPbDvN.js} +1 -1
  36. package/dashboard/dist/assets/{linear-CRF8xwvk.js → linear-CtQ_RMAp.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-D5JD5uYg.js → mermaid.core-CcJoQ5At.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-BqGcTcbk.js → mindmap-definition-YRQLILUH-DahXO6Ui.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CnQ2EkyX.js → pieDiagram-SKSYHLDU-DXv973_0.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-CKCfmTRc.js → quadrantDiagram-337W2JSQ-5U0ryvjr.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-B9J4bvUK.js → requirementDiagram-Z7DCOOCP-CvGflSyl.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-CMSzI5rg.js → sankeyDiagram-WA2Y5GQK-AeetdpCi.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BFQDtxd0.js → sequenceDiagram-2WXFIKYE-BhRR4qzn.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DgWYKT02.js → stateDiagram-RAJIS63D-CxxF58nH.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DHCNHbs7.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-jANu0e7M.js → timeline-definition-YZTLITO2-BEQIeygp.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-BFjWMkyM.js → treemap-KZPCXAKY-BCWNTFfj.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-DGwrAJYj.js → vennDiagram-LZ73GAT5-BcuC_UIz.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BLCPfoFM.js → xychartDiagram-JWTSCODW-CWGez3_f.js} +1 -1
  50. package/dashboard/dist/index.html +1 -1
  51. package/dist/dashboard/server.js +198 -160
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +329 -290
  54. package/dist/index.js.map +1 -1
  55. package/dist/launch/index.js +20 -1
  56. package/dist/launch/index.js.map +1 -1
  57. package/package.json +1 -1
  58. package/platforms/claude-code/.claude-plugin/plugin.json +2 -1
  59. package/platforms/claude-code/commands/run-playbook/run-playbook.md +16 -0
  60. package/platforms/claude-code/skills/run-playbook/SKILL.md +87 -0
  61. package/platforms/codex/skills/run-playbook/SKILL.md +87 -0
  62. package/skills/run-playbook/SKILL.md +87 -0
  63. package/dashboard/dist/assets/channel-eXdj7ayV.js +0 -1
  64. package/dashboard/dist/assets/classDiagram-VBA2DB6C-B-bBQACr.js +0 -1
  65. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-B-bBQACr.js +0 -1
  66. package/dashboard/dist/assets/clone-CL04pv9J.js +0 -1
  67. package/dashboard/dist/assets/index--qwybK3W.js +0 -535
  68. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Nmwft4cR.js +0 -1
@@ -3871,8 +3871,8 @@ async function migrateFromMarkdown(projectsDir) {
3871
3871
  return allSessions.length;
3872
3872
  }
3873
3873
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
3874
- const { readFile: readFile21 } = await import("fs/promises");
3875
- const raw = await readFile21(filePath, "utf-8");
3874
+ const { readFile: readFile20 } = await import("fs/promises");
3875
+ const raw = await readFile20(filePath, "utf-8");
3876
3876
  const sessions = [];
3877
3877
  const lines = raw.split("\n");
3878
3878
  let inTable = false;
@@ -4289,12 +4289,13 @@ __export(scanner_exports, {
4289
4289
  import { execFile } from "child_process";
4290
4290
  import { promisify } from "util";
4291
4291
  import { resolve as resolve12 } from "path";
4292
- import { realpath, readdir as readdir7, readFile as readFile9 } from "fs/promises";
4292
+ import { realpath } from "fs/promises";
4293
4293
  function clearScanCache() {
4294
4294
  cache = null;
4295
4295
  forceFreshNext = true;
4296
4296
  scanEpoch++;
4297
4297
  inFlight = null;
4298
+ tmuxAvailableCache = null;
4298
4299
  }
4299
4300
  function emptyScan() {
4300
4301
  return { sessions: [], tmuxAvailable: false };
@@ -4352,8 +4353,10 @@ async function execQuiet(cmd, args) {
4352
4353
  }
4353
4354
  }
4354
4355
  async function checkTmuxAvailable() {
4355
- const result = await execQuiet("which", ["tmux"]);
4356
- return result.length > 0;
4356
+ if (!tmuxAvailableCache) {
4357
+ tmuxAvailableCache = execQuiet("which", ["tmux"]).then((result) => result.length > 0);
4358
+ }
4359
+ return tmuxAvailableCache;
4357
4360
  }
4358
4361
  async function sessionAlive(name) {
4359
4362
  try {
@@ -4448,62 +4451,11 @@ async function getGitInfo(cwd) {
4448
4451
  return { branch: branch || null, worktree: isWorktree };
4449
4452
  }
4450
4453
  async function loadWorkspaceRecords(projectsDir, assignmentsDir2) {
4451
- const records = [];
4452
4454
  try {
4453
- const projects = await listProjects(projectsDir);
4454
- for (const project of projects) {
4455
- const projectAssignmentsDir = resolve12(projectsDir, project.slug, "assignments");
4456
- let slugs;
4457
- try {
4458
- slugs = await readdir7(projectAssignmentsDir);
4459
- } catch {
4460
- continue;
4461
- }
4462
- for (const aslug of slugs) {
4463
- const aFile = resolve12(projectAssignmentsDir, aslug, "assignment.md");
4464
- try {
4465
- const raw = await readFile9(aFile, "utf-8");
4466
- const [fm] = extractFrontmatter2(raw);
4467
- if (!fm) continue;
4468
- records.push({
4469
- projectSlug: project.slug,
4470
- assignmentSlug: aslug,
4471
- assignmentTitle: getField(fm, "title") ?? aslug,
4472
- worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
4473
- branch: getNestedField(fm, "workspace", "branch") ?? null
4474
- });
4475
- } catch {
4476
- continue;
4477
- }
4478
- }
4479
- }
4455
+ return await listWorkspaceRecords(projectsDir, assignmentsDir2);
4480
4456
  } catch {
4457
+ return [];
4481
4458
  }
4482
- if (assignmentsDir2) {
4483
- try {
4484
- const entries = await readdir7(assignmentsDir2);
4485
- for (const id of entries) {
4486
- if (id.startsWith(".") || id.startsWith("_")) continue;
4487
- const aFile = resolve12(assignmentsDir2, id, "assignment.md");
4488
- try {
4489
- const raw = await readFile9(aFile, "utf-8");
4490
- const [fm] = extractFrontmatter2(raw);
4491
- if (!fm) continue;
4492
- records.push({
4493
- projectSlug: null,
4494
- assignmentSlug: id,
4495
- assignmentTitle: getField(fm, "title") ?? id,
4496
- worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
4497
- branch: getNestedField(fm, "workspace", "branch") ?? null
4498
- });
4499
- } catch {
4500
- continue;
4501
- }
4502
- }
4503
- } catch {
4504
- }
4505
- }
4506
- return records;
4507
4459
  }
4508
4460
  async function resolveAndNormalize(p) {
4509
4461
  try {
@@ -4774,13 +4726,12 @@ async function scanSingleSession(serversDir2, projectsDir, name, options) {
4774
4726
  }
4775
4727
  return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
4776
4728
  }
4777
- var exec, cache, lastKnown, inFlight, forceFreshNext, scanEpoch, cacheKey, CACHE_TTL_MS, COLD_WAIT_BUDGET_MS, PROBE_TIMEOUT_MS, PROBE_MAX_BUFFER;
4729
+ var exec, cache, lastKnown, inFlight, forceFreshNext, scanEpoch, cacheKey, CACHE_TTL_MS, COLD_WAIT_BUDGET_MS, PROBE_TIMEOUT_MS, PROBE_MAX_BUFFER, tmuxAvailableCache;
4778
4730
  var init_scanner = __esm({
4779
4731
  "src/dashboard/scanner.ts"() {
4780
4732
  "use strict";
4781
4733
  init_api();
4782
4734
  init_servers();
4783
- init_parser();
4784
4735
  exec = promisify(execFile);
4785
4736
  cache = null;
4786
4737
  lastKnown = null;
@@ -4792,11 +4743,12 @@ var init_scanner = __esm({
4792
4743
  COLD_WAIT_BUDGET_MS = 2500;
4793
4744
  PROBE_TIMEOUT_MS = 15e3;
4794
4745
  PROBE_MAX_BUFFER = 32 * 1024 * 1024;
4746
+ tmuxAvailableCache = null;
4795
4747
  }
4796
4748
  });
4797
4749
 
4798
4750
  // src/dashboard/api.ts
4799
- import { readdir as readdir8, readFile as readFile10, writeFile as writeFile3 } from "fs/promises";
4751
+ import { readdir as readdir7, readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
4800
4752
  import { resolve as resolve13, dirname as dirname2, basename } from "path";
4801
4753
  function clearFrontmatterField(content, key) {
4802
4754
  const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
@@ -4836,10 +4788,41 @@ function emitTrace(traces, meta) {
4836
4788
  JSON.stringify({ kind: "overview-trace", totalMs, phases: traces.entries, subPhases, ...meta })
4837
4789
  );
4838
4790
  }
4791
+ function invalidateRecordsCache() {
4792
+ projectRecordsCache.clear();
4793
+ standaloneRecordsCache.clear();
4794
+ }
4795
+ function installRecordsInvalidation(router) {
4796
+ for (const method of ["post", "put", "patch", "delete"]) {
4797
+ const original = router[method].bind(router);
4798
+ router[method] = (path, ...handlers) => {
4799
+ if (handlers.length > 0) {
4800
+ const last = handlers[handlers.length - 1];
4801
+ handlers[handlers.length - 1] = async (req, res, next) => {
4802
+ try {
4803
+ return await last(req, res, next);
4804
+ } finally {
4805
+ invalidateRecordsCache();
4806
+ }
4807
+ };
4808
+ }
4809
+ return original(path, ...handlers);
4810
+ };
4811
+ }
4812
+ }
4839
4813
  async function listStandaloneRecords(assignmentsDir2) {
4814
+ const key = assignmentsDir2 ?? "";
4815
+ const cached = standaloneRecordsCache.get(key);
4816
+ if (cached) return cached;
4817
+ const promise = computeStandaloneRecords(assignmentsDir2);
4818
+ standaloneRecordsCache.set(key, promise);
4819
+ promise.catch(() => standaloneRecordsCache.delete(key));
4820
+ return promise;
4821
+ }
4822
+ async function computeStandaloneRecords(assignmentsDir2) {
4840
4823
  if (!assignmentsDir2) return [];
4841
4824
  if (!await fileExists(assignmentsDir2)) return [];
4842
- const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
4825
+ const entries = await readdir7(assignmentsDir2, { withFileTypes: true });
4843
4826
  const records = [];
4844
4827
  for (const entry of entries) {
4845
4828
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
@@ -4847,7 +4830,7 @@ async function listStandaloneRecords(assignmentsDir2) {
4847
4830
  const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
4848
4831
  if (!await fileExists(assignmentMdPath)) continue;
4849
4832
  try {
4850
- const content = await readFile10(assignmentMdPath, "utf-8");
4833
+ const content = await readFile9(assignmentMdPath, "utf-8");
4851
4834
  const record = parseAssignmentFull(content);
4852
4835
  records.push({ assignmentDir, id: entry.name, record });
4853
4836
  } catch {
@@ -4923,7 +4906,7 @@ async function listProjects(projectsDir) {
4923
4906
  async function readWorkspaceRegistry(projectsDir) {
4924
4907
  const registryPath = resolve13(dirname2(projectsDir), "workspaces.json");
4925
4908
  try {
4926
- const raw = await readFile10(registryPath, "utf-8");
4909
+ const raw = await readFile9(registryPath, "utf-8");
4927
4910
  const parsed = JSON.parse(raw);
4928
4911
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
4929
4912
  } catch {
@@ -4959,6 +4942,34 @@ async function listWorkspaces(projectsDir, assignmentsDir2) {
4959
4942
  const workspaces = Array.from(workspaceSet).sort();
4960
4943
  return { workspaces, hasUngrouped };
4961
4944
  }
4945
+ async function listWorkspaceRecords(projectsDir, assignmentsDir2) {
4946
+ const [projectRecords, standaloneRecords] = await Promise.all([
4947
+ listProjectRecords(projectsDir),
4948
+ listStandaloneRecords(assignmentsDir2)
4949
+ ]);
4950
+ const records = [];
4951
+ for (const project of projectRecords) {
4952
+ for (const assignment of project.assignments) {
4953
+ records.push({
4954
+ projectSlug: project.summary.slug,
4955
+ assignmentSlug: assignment.slug,
4956
+ assignmentTitle: assignment.title || assignment.slug,
4957
+ worktreePath: assignment.workspace.worktreePath ?? null,
4958
+ branch: assignment.workspace.branch ?? null
4959
+ });
4960
+ }
4961
+ }
4962
+ for (const standalone of standaloneRecords) {
4963
+ records.push({
4964
+ projectSlug: null,
4965
+ assignmentSlug: standalone.id,
4966
+ assignmentTitle: standalone.record.title || standalone.id,
4967
+ worktreePath: standalone.record.workspace.worktreePath ?? null,
4968
+ branch: standalone.record.workspace.branch ?? null
4969
+ });
4970
+ }
4971
+ return records;
4972
+ }
4962
4973
  async function createWorkspace(projectsDir, name) {
4963
4974
  const registered = await readWorkspaceRegistry(projectsDir);
4964
4975
  if (!registered.includes(name)) {
@@ -4984,7 +4995,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
4984
4995
  const timestamp = nowTimestamp();
4985
4996
  for (const slug of projectsReferencing) {
4986
4997
  const path = resolve13(projectsDir, slug, "project.md");
4987
- const raw = await readFile10(path, "utf-8");
4998
+ const raw = await readFile9(path, "utf-8");
4988
4999
  let next = clearFrontmatterField(raw, "workspace");
4989
5000
  next = setUpdatedField(next, timestamp);
4990
5001
  await writeFileForce(path, next);
@@ -4993,7 +5004,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
4993
5004
  for (const id of standalonesReferencing) {
4994
5005
  if (!opts.assignmentsDir) break;
4995
5006
  const path = resolve13(opts.assignmentsDir, id, "assignment.md");
4996
- const raw = await readFile10(path, "utf-8");
5007
+ const raw = await readFile9(path, "utf-8");
4997
5008
  let next = clearFrontmatterField(raw, "workspaceGroup");
4998
5009
  next = setUpdatedField(next, timestamp);
4999
5010
  await writeFileForce(path, next);
@@ -5003,6 +5014,9 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
5003
5014
  const registered = await readWorkspaceRegistry(projectsDir);
5004
5015
  const filtered = registered.filter((w) => w !== name);
5005
5016
  await writeWorkspaceRegistry(projectsDir, filtered);
5017
+ if (rewroteFiles) {
5018
+ invalidateRecordsCache();
5019
+ }
5006
5020
  return { rewroteFiles };
5007
5021
  }
5008
5022
  async function getOverview(projectsDir, serversDir2, assignmentsDir2, options = {}) {
@@ -5170,7 +5184,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
5170
5184
  if (!filePath || !await fileExists(filePath)) {
5171
5185
  return null;
5172
5186
  }
5173
- const content = await readFile10(filePath, "utf-8");
5187
+ const content = await readFile9(filePath, "utf-8");
5174
5188
  const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
5175
5189
  return {
5176
5190
  documentType,
@@ -5196,7 +5210,7 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
5196
5210
  if (!fileName) return null;
5197
5211
  const filePath = resolve13(resolved.assignmentDir, fileName);
5198
5212
  if (!await fileExists(filePath)) return null;
5199
- const content = await readFile10(filePath, "utf-8");
5213
+ const content = await readFile9(filePath, "utf-8");
5200
5214
  const label = resolved.id;
5201
5215
  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}`;
5202
5216
  return {
@@ -5215,7 +5229,7 @@ async function getProjectDetail(projectsDir, slug) {
5215
5229
  if (!await fileExists(projectMdPath)) {
5216
5230
  return null;
5217
5231
  }
5218
- const projectContent = await readFile10(projectMdPath, "utf-8");
5232
+ const projectContent = await readFile9(projectMdPath, "utf-8");
5219
5233
  const project = parseProject(projectContent);
5220
5234
  const assignments = await listAssignmentRecords(projectPath);
5221
5235
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -5252,18 +5266,18 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5252
5266
  if (!await fileExists(assignmentMdPath)) {
5253
5267
  return null;
5254
5268
  }
5255
- const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
5269
+ const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
5256
5270
  const assignment = parseAssignmentFull(assignmentContent);
5257
5271
  let projectWorkspace = null;
5258
5272
  const projectMdPath = resolve13(projectsDir, projectSlug, "project.md");
5259
5273
  if (await fileExists(projectMdPath)) {
5260
- const projectContent = await readFile10(projectMdPath, "utf-8");
5274
+ const projectContent = await readFile9(projectMdPath, "utf-8");
5261
5275
  projectWorkspace = parseProject(projectContent).workspace;
5262
5276
  }
5263
5277
  let plan = null;
5264
5278
  const planPath = resolve13(assignmentDir, "plan.md");
5265
5279
  if (await fileExists(planPath)) {
5266
- const planContent = await readFile10(planPath, "utf-8");
5280
+ const planContent = await readFile9(planPath, "utf-8");
5267
5281
  const parsed = parsePlan(planContent);
5268
5282
  plan = {
5269
5283
  status: parsed.status,
@@ -5274,7 +5288,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5274
5288
  let scratchpad = null;
5275
5289
  const scratchpadPath = resolve13(assignmentDir, "scratchpad.md");
5276
5290
  if (await fileExists(scratchpadPath)) {
5277
- const scratchpadContent = await readFile10(scratchpadPath, "utf-8");
5291
+ const scratchpadContent = await readFile9(scratchpadPath, "utf-8");
5278
5292
  const parsed = parseScratchpad(scratchpadContent);
5279
5293
  scratchpad = {
5280
5294
  updated: parsed.updated,
@@ -5284,7 +5298,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5284
5298
  let handoff = null;
5285
5299
  const handoffPath = resolve13(assignmentDir, "handoff.md");
5286
5300
  if (await fileExists(handoffPath)) {
5287
- const handoffContent = await readFile10(handoffPath, "utf-8");
5301
+ const handoffContent = await readFile9(handoffPath, "utf-8");
5288
5302
  const parsed = parseHandoff(handoffContent);
5289
5303
  handoff = {
5290
5304
  updated: parsed.updated,
@@ -5295,7 +5309,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5295
5309
  let decisionRecord = null;
5296
5310
  const decisionRecordPath = resolve13(assignmentDir, "decision-record.md");
5297
5311
  if (await fileExists(decisionRecordPath)) {
5298
- const decisionRecordContent = await readFile10(decisionRecordPath, "utf-8");
5312
+ const decisionRecordContent = await readFile9(decisionRecordPath, "utf-8");
5299
5313
  const parsed = parseDecisionRecord(decisionRecordContent);
5300
5314
  decisionRecord = {
5301
5315
  updated: parsed.updated,
@@ -5306,7 +5320,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5306
5320
  let progress = null;
5307
5321
  const progressPath = resolve13(assignmentDir, "progress.md");
5308
5322
  if (await fileExists(progressPath)) {
5309
- const progressContent = await readFile10(progressPath, "utf-8");
5323
+ const progressContent = await readFile9(progressPath, "utf-8");
5310
5324
  const parsed = parseProgress(progressContent);
5311
5325
  progress = {
5312
5326
  updated: parsed.updated,
@@ -5317,7 +5331,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5317
5331
  let comments = null;
5318
5332
  const commentsPath = resolve13(assignmentDir, "comments.md");
5319
5333
  if (await fileExists(commentsPath)) {
5320
- const commentsContent = await readFile10(commentsPath, "utf-8");
5334
+ const commentsContent = await readFile9(commentsPath, "utf-8");
5321
5335
  const parsed = parseComments(commentsContent);
5322
5336
  comments = {
5323
5337
  updated: parsed.updated,
@@ -5468,7 +5482,7 @@ async function countMentionsInAssignment(sourceDir, target) {
5468
5482
  const bodies = [];
5469
5483
  const assignmentMd = resolve13(sourceDir, "assignment.md");
5470
5484
  if (await fileExists(assignmentMd)) {
5471
- const content = await readFile10(assignmentMd, "utf-8");
5485
+ const content = await readFile9(assignmentMd, "utf-8");
5472
5486
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
5473
5487
  if (todosMatch) bodies.push(todosMatch[1]);
5474
5488
  }
@@ -5476,7 +5490,7 @@ async function countMentionsInAssignment(sourceDir, target) {
5476
5490
  const path = resolve13(sourceDir, filename);
5477
5491
  if (await fileExists(path)) {
5478
5492
  try {
5479
- bodies.push(await readFile10(path, "utf-8"));
5493
+ bodies.push(await readFile9(path, "utf-8"));
5480
5494
  } catch {
5481
5495
  }
5482
5496
  }
@@ -5536,42 +5550,42 @@ async function buildStandaloneAssignmentDetail(resolved) {
5536
5550
  const assignmentDir = resolved.assignmentDir;
5537
5551
  const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
5538
5552
  if (!await fileExists(assignmentMdPath)) return null;
5539
- const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
5553
+ const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
5540
5554
  const assignment = parseAssignmentFull(assignmentContent);
5541
5555
  let plan = null;
5542
5556
  const planPath = resolve13(assignmentDir, "plan.md");
5543
5557
  if (await fileExists(planPath)) {
5544
- const parsed = parsePlan(await readFile10(planPath, "utf-8"));
5558
+ const parsed = parsePlan(await readFile9(planPath, "utf-8"));
5545
5559
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
5546
5560
  }
5547
5561
  let scratchpad = null;
5548
5562
  const scratchpadPath = resolve13(assignmentDir, "scratchpad.md");
5549
5563
  if (await fileExists(scratchpadPath)) {
5550
- const parsed = parseScratchpad(await readFile10(scratchpadPath, "utf-8"));
5564
+ const parsed = parseScratchpad(await readFile9(scratchpadPath, "utf-8"));
5551
5565
  scratchpad = { updated: parsed.updated, body: parsed.body };
5552
5566
  }
5553
5567
  let handoff = null;
5554
5568
  const handoffPath = resolve13(assignmentDir, "handoff.md");
5555
5569
  if (await fileExists(handoffPath)) {
5556
- const parsed = parseHandoff(await readFile10(handoffPath, "utf-8"));
5570
+ const parsed = parseHandoff(await readFile9(handoffPath, "utf-8"));
5557
5571
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
5558
5572
  }
5559
5573
  let decisionRecord = null;
5560
5574
  const decisionRecordPath = resolve13(assignmentDir, "decision-record.md");
5561
5575
  if (await fileExists(decisionRecordPath)) {
5562
- const parsed = parseDecisionRecord(await readFile10(decisionRecordPath, "utf-8"));
5576
+ const parsed = parseDecisionRecord(await readFile9(decisionRecordPath, "utf-8"));
5563
5577
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
5564
5578
  }
5565
5579
  let progress = null;
5566
5580
  const progressPath = resolve13(assignmentDir, "progress.md");
5567
5581
  if (await fileExists(progressPath)) {
5568
- const parsed = parseProgress(await readFile10(progressPath, "utf-8"));
5582
+ const parsed = parseProgress(await readFile9(progressPath, "utf-8"));
5569
5583
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
5570
5584
  }
5571
5585
  let comments = null;
5572
5586
  const commentsPath = resolve13(assignmentDir, "comments.md");
5573
5587
  if (await fileExists(commentsPath)) {
5574
- const parsed = parseComments(await readFile10(commentsPath, "utf-8"));
5588
+ const parsed = parseComments(await readFile9(commentsPath, "utf-8"));
5575
5589
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
5576
5590
  }
5577
5591
  const detail = {
@@ -5608,6 +5622,14 @@ async function buildStandaloneAssignmentDetail(resolved) {
5608
5622
  return detail;
5609
5623
  }
5610
5624
  async function listProjectRecords(projectsDir, traces) {
5625
+ const cached = projectRecordsCache.get(projectsDir);
5626
+ if (cached) return cached;
5627
+ const promise = computeProjectRecords(projectsDir, traces);
5628
+ projectRecordsCache.set(projectsDir, promise);
5629
+ promise.catch(() => projectRecordsCache.delete(projectsDir));
5630
+ return promise;
5631
+ }
5632
+ async function computeProjectRecords(projectsDir, traces) {
5611
5633
  if (!await fileExists(projectsDir)) {
5612
5634
  return [];
5613
5635
  }
@@ -5615,7 +5637,7 @@ async function listProjectRecords(projectsDir, traces) {
5615
5637
  migratedProjectsDirs.add(projectsDir);
5616
5638
  await migrateLegacyProjectFiles(projectsDir);
5617
5639
  }
5618
- const entries = await readdir8(projectsDir, { withFileTypes: true });
5640
+ const entries = await readdir7(projectsDir, { withFileTypes: true });
5619
5641
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
5620
5642
  const maybeRecords = await Promise.all(
5621
5643
  projectDirs.map(async (entry) => {
@@ -5625,7 +5647,7 @@ async function listProjectRecords(projectsDir, traces) {
5625
5647
  return null;
5626
5648
  }
5627
5649
  const t0 = traces ? performance.now() : 0;
5628
- const projectContent = await readFile10(projectMdPath, "utf-8");
5650
+ const projectContent = await readFile9(projectMdPath, "utf-8");
5629
5651
  const project = parseProject(projectContent);
5630
5652
  if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
5631
5653
  const t1 = traces ? performance.now() : 0;
@@ -5670,7 +5692,7 @@ async function listAssignmentRecords(projectPath, traces) {
5670
5692
  if (!await fileExists(assignmentsDir2)) {
5671
5693
  return [];
5672
5694
  }
5673
- const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
5695
+ const entries = await readdir7(assignmentsDir2, { withFileTypes: true });
5674
5696
  const dirEntries = entries.filter((entry) => entry.isDirectory());
5675
5697
  const maybeRecords = await Promise.all(
5676
5698
  dirEntries.map(async (entry) => {
@@ -5679,7 +5701,7 @@ async function listAssignmentRecords(projectPath, traces) {
5679
5701
  return null;
5680
5702
  }
5681
5703
  const t0 = traces ? performance.now() : 0;
5682
- const content = await readFile10(assignmentMd, "utf-8");
5704
+ const content = await readFile9(assignmentMd, "utf-8");
5683
5705
  const parsed = parseAssignmentFull(content);
5684
5706
  if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
5685
5707
  return parsed;
@@ -5694,14 +5716,14 @@ async function listResources(projectPath) {
5694
5716
  if (!await fileExists(resourcesDir)) {
5695
5717
  return [];
5696
5718
  }
5697
- const entries = await readdir8(resourcesDir, { withFileTypes: true });
5719
+ const entries = await readdir7(resourcesDir, { withFileTypes: true });
5698
5720
  const results = [];
5699
5721
  for (const entry of entries) {
5700
5722
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
5701
5723
  continue;
5702
5724
  }
5703
5725
  const filePath = resolve13(resourcesDir, entry.name);
5704
- const content = await readFile10(filePath, "utf-8");
5726
+ const content = await readFile9(filePath, "utf-8");
5705
5727
  const parsed = parseResource(content);
5706
5728
  results.push({
5707
5729
  name: parsed.name,
@@ -5720,14 +5742,14 @@ async function listMemories(projectPath) {
5720
5742
  if (!await fileExists(memoriesDir)) {
5721
5743
  return [];
5722
5744
  }
5723
- const entries = await readdir8(memoriesDir, { withFileTypes: true });
5745
+ const entries = await readdir7(memoriesDir, { withFileTypes: true });
5724
5746
  const results = [];
5725
5747
  for (const entry of entries) {
5726
5748
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
5727
5749
  continue;
5728
5750
  }
5729
5751
  const filePath = resolve13(memoriesDir, entry.name);
5730
- const content = await readFile10(filePath, "utf-8");
5752
+ const content = await readFile9(filePath, "utf-8");
5731
5753
  const parsed = parseMemory(content);
5732
5754
  results.push({
5733
5755
  name: parsed.name,
@@ -5790,7 +5812,7 @@ async function getMemoryDetail(projectsDir, projectSlug, itemSlug) {
5790
5812
  if (!projectRecord) return null;
5791
5813
  const filePath = resolve13(projectRecord.projectPath, "memories", `${itemSlug}.md`);
5792
5814
  if (!await fileExists(filePath)) return null;
5793
- const content = await readFile10(filePath, "utf-8");
5815
+ const content = await readFile9(filePath, "utf-8");
5794
5816
  const parsed = parseMemory(content);
5795
5817
  return {
5796
5818
  name: parsed.name,
@@ -5816,7 +5838,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
5816
5838
  if (!projectRecord) return null;
5817
5839
  const filePath = resolve13(projectRecord.projectPath, "resources", `${itemSlug}.md`);
5818
5840
  if (!await fileExists(filePath)) return null;
5819
- const content = await readFile10(filePath, "utf-8");
5841
+ const content = await readFile9(filePath, "utf-8");
5820
5842
  const parsed = parseResource(content);
5821
5843
  return {
5822
5844
  name: parsed.name,
@@ -5834,7 +5856,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
5834
5856
  async function loadDependencyGraph(projectPath, assignments) {
5835
5857
  const statusPath = resolve13(projectPath, "_status.md");
5836
5858
  if (await fileExists(statusPath)) {
5837
- const statusContent = await readFile10(statusPath, "utf-8");
5859
+ const statusContent = await readFile9(statusPath, "utf-8");
5838
5860
  const parsed = parseStatus(statusContent);
5839
5861
  const derivedGraph = extractMermaidGraph(parsed.body);
5840
5862
  if (derivedGraph) {
@@ -5995,7 +6017,7 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
5995
6017
  unmet.push(`${dependency} (missing)`);
5996
6018
  continue;
5997
6019
  }
5998
- const content = await readFile10(dependencyPath, "utf-8");
6020
+ const content = await readFile9(dependencyPath, "utf-8");
5999
6021
  const parsed = parseAssignmentFull(content);
6000
6022
  if (!terminals.has(parsed.status)) {
6001
6023
  unmet.push(`${dependency} (${parsed.status})`);
@@ -6288,7 +6310,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
6288
6310
  return 0;
6289
6311
  }
6290
6312
  try {
6291
- const content = await readFile10(commentsPath, "utf-8");
6313
+ const content = await readFile9(commentsPath, "utf-8");
6292
6314
  const parsed = parseComments(content);
6293
6315
  return parsed.entries.filter(
6294
6316
  (e) => e.type === "question" && e.resolved !== true
@@ -6356,12 +6378,12 @@ async function listPlaybooks(playbooksDir2) {
6356
6378
  if (!await fileExists(playbooksDir2)) return [];
6357
6379
  const config = await readConfig();
6358
6380
  const disabledSet = new Set(config.playbooks.disabled);
6359
- const entries = await readdir8(playbooksDir2, { withFileTypes: true });
6381
+ const entries = await readdir7(playbooksDir2, { withFileTypes: true });
6360
6382
  const playbooks = [];
6361
6383
  for (const entry of entries) {
6362
6384
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
6363
6385
  const filePath = resolve13(playbooksDir2, entry.name);
6364
- const raw = await readFile10(filePath, "utf-8");
6386
+ const raw = await readFile9(filePath, "utf-8");
6365
6387
  const parsed = parsePlaybook(raw);
6366
6388
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
6367
6389
  playbooks.push({
@@ -6395,7 +6417,7 @@ async function getPlaybookDetail(playbooksDir2, slug) {
6395
6417
  enabled
6396
6418
  };
6397
6419
  }
6398
- var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
6420
+ var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
6399
6421
  var init_api = __esm({
6400
6422
  "src/dashboard/api.ts"() {
6401
6423
  "use strict";
@@ -6446,6 +6468,8 @@ var init_api = __esm({
6446
6468
  ["blocked", "blocked"],
6447
6469
  ["stale", "stale"]
6448
6470
  ];
6471
+ projectRecordsCache = /* @__PURE__ */ new Map();
6472
+ standaloneRecordsCache = /* @__PURE__ */ new Map();
6449
6473
  DEFAULT_TRANSITION_DEFINITIONS = [
6450
6474
  {
6451
6475
  command: "start",
@@ -6994,7 +7018,7 @@ function mergePatch(current, patch) {
6994
7018
  // src/utils/view-prefs.ts
6995
7019
  init_paths();
6996
7020
  init_fs();
6997
- import { readFile as readFile11, rename as rename3, unlink as unlink3 } from "fs/promises";
7021
+ import { readFile as readFile10, rename as rename3, unlink as unlink3 } from "fs/promises";
6998
7022
  import { resolve as resolve14 } from "path";
6999
7023
  function corruptFilePath() {
7000
7024
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -7017,7 +7041,7 @@ async function readViewPrefsFile() {
7017
7041
  }
7018
7042
  let raw;
7019
7043
  try {
7020
- raw = await readFile11(path, "utf-8");
7044
+ raw = await readFile10(path, "utf-8");
7021
7045
  } catch {
7022
7046
  return { ...DEFAULT_VIEW_PREFS_FILE };
7023
7047
  }
@@ -7200,7 +7224,7 @@ function isDashboardSlot(value) {
7200
7224
  // src/utils/saved-views.ts
7201
7225
  init_paths();
7202
7226
  init_fs();
7203
- import { readFile as readFile12, rename as rename4, unlink as unlink4 } from "fs/promises";
7227
+ import { readFile as readFile11, rename as rename4, unlink as unlink4 } from "fs/promises";
7204
7228
  import { randomUUID } from "crypto";
7205
7229
  import { resolve as resolve15 } from "path";
7206
7230
  function corruptFilePath2() {
@@ -7230,7 +7254,7 @@ async function readSavedViewsFile() {
7230
7254
  }
7231
7255
  let raw;
7232
7256
  try {
7233
- raw = await readFile12(path, "utf-8");
7257
+ raw = await readFile11(path, "utf-8");
7234
7258
  } catch {
7235
7259
  return cloneDefault();
7236
7260
  }
@@ -7546,7 +7570,7 @@ init_lifecycle();
7546
7570
  init_slug();
7547
7571
  import { Router as Router2 } from "express";
7548
7572
  import { resolve as resolve18, basename as basename3, isAbsolute as isAbsolute2 } from "path";
7549
- import { rm, readFile as readFile15, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
7573
+ import { rm, readFile as readFile14, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
7550
7574
  import { spawnSync as spawnSync3 } from "child_process";
7551
7575
 
7552
7576
  // src/utils/uuid.ts
@@ -7563,7 +7587,7 @@ init_fs();
7563
7587
  init_frontmatter();
7564
7588
  init_fs();
7565
7589
  import { spawn } from "child_process";
7566
- import { readFile as readFile13 } from "fs/promises";
7590
+ import { readFile as readFile12 } from "fs/promises";
7567
7591
  function run(command, args, cwd) {
7568
7592
  return new Promise((resolvePromise) => {
7569
7593
  const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -7613,7 +7637,7 @@ async function createWorktreeAndRecord(opts) {
7613
7637
  const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
7614
7638
  await createWorktree({ repository, branch, worktreePath, parentBranch });
7615
7639
  try {
7616
- const content = await readFile13(assignmentPath, "utf-8");
7640
+ const content = await readFile12(assignmentPath, "utf-8");
7617
7641
  const updated = updateAssignmentWorkspace(content, {
7618
7642
  repository,
7619
7643
  worktreePath,
@@ -7697,14 +7721,14 @@ function detectCurrentBranch(cwd) {
7697
7721
  // src/dashboard/repository-candidates.ts
7698
7722
  init_fs();
7699
7723
  init_parser();
7700
- import { readdir as readdir9, readFile as readFile14 } from "fs/promises";
7724
+ import { readdir as readdir8, readFile as readFile13 } from "fs/promises";
7701
7725
  import { resolve as resolve17 } from "path";
7702
7726
  async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
7703
7727
  const seen = /* @__PURE__ */ new Set();
7704
7728
  const out = [];
7705
7729
  const projectPath = resolve17(projectsDir, projectSlug, "project.md");
7706
7730
  if (await fileExists(projectPath)) {
7707
- const project = parseProject(await readFile14(projectPath, "utf-8"));
7731
+ const project = parseProject(await readFile13(projectPath, "utf-8"));
7708
7732
  for (const raw of project.repositories) {
7709
7733
  const path = raw.trim();
7710
7734
  if (!path) continue;
@@ -7716,12 +7740,12 @@ async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
7716
7740
  }
7717
7741
  const assignmentsDir2 = resolve17(projectsDir, projectSlug, "assignments");
7718
7742
  if (await fileExists(assignmentsDir2)) {
7719
- const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
7743
+ const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
7720
7744
  for (const entry of entries) {
7721
7745
  if (!entry.isDirectory()) continue;
7722
7746
  const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
7723
7747
  if (!await fileExists(assignmentMd)) continue;
7724
- const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
7748
+ const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
7725
7749
  const repo = parsed.workspace.repository?.trim();
7726
7750
  if (!repo) continue;
7727
7751
  const abs = resolve17(repo);
@@ -7738,13 +7762,13 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
7738
7762
  }
7739
7763
  const seen = /* @__PURE__ */ new Set();
7740
7764
  const out = [];
7741
- const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
7765
+ const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
7742
7766
  for (const entry of entries) {
7743
7767
  if (!entry.isDirectory()) continue;
7744
7768
  if (entry.name === excludeAssignmentId) continue;
7745
7769
  const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
7746
7770
  if (!await fileExists(assignmentMd)) continue;
7747
- const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
7771
+ const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
7748
7772
  const repo = parsed.workspace.repository?.trim();
7749
7773
  if (!repo) continue;
7750
7774
  const abs = resolve17(repo);
@@ -8310,14 +8334,14 @@ async function readCurrentDocument(filePath) {
8310
8334
  if (!await fileExists(filePath)) {
8311
8335
  return null;
8312
8336
  }
8313
- return readFile15(filePath, "utf-8");
8337
+ return readFile14(filePath, "utf-8");
8314
8338
  }
8315
8339
  async function handleWorktreeCreate(req, res, ctx) {
8316
8340
  if (!await fileExists(ctx.assignmentPath)) {
8317
8341
  res.status(404).json({ error: "Assignment not found" });
8318
8342
  return;
8319
8343
  }
8320
- const parsed = parseAssignmentFull(await readFile15(ctx.assignmentPath, "utf-8"));
8344
+ const parsed = parseAssignmentFull(await readFile14(ctx.assignmentPath, "utf-8"));
8321
8345
  if (parsed.workspace.worktreePath) {
8322
8346
  res.status(409).json({ error: "Worktree already configured for this assignment" });
8323
8347
  return;
@@ -8409,6 +8433,7 @@ async function handleWorktreeCreate(req, res, ctx) {
8409
8433
  function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
8410
8434
  const linkedTodosLookup = todosDir2 ? { todosDir: todosDir2, projectsDir } : void 0;
8411
8435
  const router = Router2();
8436
+ installRecordsInvalidation(router);
8412
8437
  router.get("/api/templates/project", (_req, res) => {
8413
8438
  const content = renderProject({
8414
8439
  id: generateId(),
@@ -8686,7 +8711,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
8686
8711
  }
8687
8712
  const nextContentRaw = requireContent(req, res);
8688
8713
  if (!nextContentRaw) return;
8689
- const currentContent = await readFile15(filePath, "utf-8");
8714
+ const currentContent = await readFile14(filePath, "utf-8");
8690
8715
  const frontmatterBlock = extractFrontmatterBlock(currentContent);
8691
8716
  if (!frontmatterBlock) {
8692
8717
  res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
@@ -9137,7 +9162,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9137
9162
  let currentContent;
9138
9163
  let currentCount = 0;
9139
9164
  if (await fileExists(commentsPath)) {
9140
- currentContent = await readFile15(commentsPath, "utf-8");
9165
+ currentContent = await readFile14(commentsPath, "utf-8");
9141
9166
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
9142
9167
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
9143
9168
  } else {
@@ -9194,7 +9219,7 @@ ${entry}`;
9194
9219
  res.status(400).json({ error: "resolved (boolean) is required" });
9195
9220
  return;
9196
9221
  }
9197
- const content = await readFile15(commentsPath, "utf-8");
9222
+ const content = await readFile14(commentsPath, "utf-8");
9198
9223
  const parsed = parseComments(content);
9199
9224
  const target = parsed.entries.find((e) => e.id === commentId);
9200
9225
  if (!target) {
@@ -9241,7 +9266,7 @@ ${entry}`;
9241
9266
  });
9242
9267
  return;
9243
9268
  }
9244
- let content = await readFile15(projectPath, "utf-8");
9269
+ let content = await readFile14(projectPath, "utf-8");
9245
9270
  content = setTopLevelField(content, "workspace", workspace ?? null);
9246
9271
  content = setTopLevelField(content, "updated", nowTimestamp());
9247
9272
  await writeFileForce(projectPath, content);
@@ -9278,7 +9303,7 @@ ${entry}`;
9278
9303
  return;
9279
9304
  }
9280
9305
  const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9281
- let content = await readFile15(assignmentPath, "utf-8");
9306
+ let content = await readFile14(assignmentPath, "utf-8");
9282
9307
  content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
9283
9308
  content = setTopLevelField(content, "updated", nowTimestamp());
9284
9309
  await writeFileForce(assignmentPath, content);
@@ -9373,7 +9398,7 @@ ${entry}`;
9373
9398
  return;
9374
9399
  }
9375
9400
  const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9376
- const parsedForSlug = parseAssignmentFull(await readFile15(assignmentPath, "utf-8"));
9401
+ const parsedForSlug = parseAssignmentFull(await readFile14(assignmentPath, "utf-8"));
9377
9402
  const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
9378
9403
  await handleWorktreeCreate(req, res, {
9379
9404
  assignmentPath,
@@ -9402,7 +9427,7 @@ ${entry}`;
9402
9427
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
9403
9428
  return;
9404
9429
  }
9405
- let content = await readFile15(projectPath, "utf-8");
9430
+ let content = await readFile14(projectPath, "utf-8");
9406
9431
  content = setTopLevelField(content, "statusOverride", status ?? null);
9407
9432
  content = setTopLevelField(content, "updated", nowTimestamp());
9408
9433
  await writeFileForce(projectPath, content);
@@ -9435,7 +9460,7 @@ ${entry}`;
9435
9460
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
9436
9461
  return;
9437
9462
  }
9438
- let content = await readFile15(assignmentPath, "utf-8");
9463
+ let content = await readFile14(assignmentPath, "utf-8");
9439
9464
  content = setTopLevelField(content, "status", status);
9440
9465
  content = setTopLevelField(content, "updated", nowTimestamp());
9441
9466
  if (status !== "blocked") {
@@ -9469,7 +9494,7 @@ ${entry}`;
9469
9494
  res.status(400).json({ error: validation.error });
9470
9495
  return;
9471
9496
  }
9472
- let content = await readFile15(assignmentPath, "utf-8");
9497
+ let content = await readFile14(assignmentPath, "utf-8");
9473
9498
  content = setTopLevelField(content, "assignee", validation.value);
9474
9499
  content = setTopLevelField(content, "updated", nowTimestamp());
9475
9500
  await writeFileForce(assignmentPath, content);
@@ -9500,7 +9525,7 @@ ${entry}`;
9500
9525
  res.status(400).json({ error: validation.error });
9501
9526
  return;
9502
9527
  }
9503
- let content = await readFile15(assignmentPath, "utf-8");
9528
+ let content = await readFile14(assignmentPath, "utf-8");
9504
9529
  content = setTopLevelField(content, "title", validation.value);
9505
9530
  content = setTopLevelField(content, "updated", nowTimestamp());
9506
9531
  await writeFileForce(assignmentPath, content);
@@ -10018,7 +10043,7 @@ ${entry}`;
10018
10043
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
10019
10044
  return;
10020
10045
  }
10021
- let content = await readFile15(assignmentPath, "utf-8");
10046
+ let content = await readFile14(assignmentPath, "utf-8");
10022
10047
  content = setTopLevelField(content, "status", status);
10023
10048
  content = setTopLevelField(content, "updated", nowTimestamp());
10024
10049
  if (status !== "blocked") {
@@ -10054,7 +10079,7 @@ ${entry}`;
10054
10079
  res.status(400).json({ error: validation.error });
10055
10080
  return;
10056
10081
  }
10057
- let content = await readFile15(assignmentPath, "utf-8");
10082
+ let content = await readFile14(assignmentPath, "utf-8");
10058
10083
  content = setTopLevelField(content, "assignee", validation.value);
10059
10084
  content = setTopLevelField(content, "updated", nowTimestamp());
10060
10085
  await writeFileForce(assignmentPath, content);
@@ -10087,7 +10112,7 @@ ${entry}`;
10087
10112
  res.status(400).json({ error: validation.error });
10088
10113
  return;
10089
10114
  }
10090
- let content = await readFile15(assignmentPath, "utf-8");
10115
+ let content = await readFile14(assignmentPath, "utf-8");
10091
10116
  content = setTopLevelField(content, "title", validation.value);
10092
10117
  content = setTopLevelField(content, "updated", nowTimestamp());
10093
10118
  await writeFileForce(assignmentPath, content);
@@ -10156,7 +10181,7 @@ ${entry}`;
10156
10181
  if (!await fileExists(assignmentPath)) {
10157
10182
  throw new Error("assignment file not found");
10158
10183
  }
10159
- let content = await readFile15(assignmentPath, "utf-8");
10184
+ let content = await readFile14(assignmentPath, "utf-8");
10160
10185
  content = setTopLevelField(content, "status", status);
10161
10186
  content = setTopLevelField(content, "updated", timestamp);
10162
10187
  if (status !== "blocked") {
@@ -10312,7 +10337,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
10312
10337
  let currentContent;
10313
10338
  let currentCount = 0;
10314
10339
  if (await fileExists(commentsPath)) {
10315
- currentContent = await readFile15(commentsPath, "utf-8");
10340
+ currentContent = await readFile14(commentsPath, "utf-8");
10316
10341
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
10317
10342
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
10318
10343
  } else {
@@ -10352,7 +10377,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
10352
10377
  res.status(400).json({ error: "resolved (boolean) is required" });
10353
10378
  return;
10354
10379
  }
10355
- const content = await readFile15(commentsPath, "utf-8");
10380
+ const content = await readFile14(commentsPath, "utf-8");
10356
10381
  const parsed = parseComments(content);
10357
10382
  const target = parsed.entries.find((e) => e.id === commentId);
10358
10383
  if (!target) {
@@ -11080,26 +11105,26 @@ import { Router as Router8 } from "express";
11080
11105
 
11081
11106
  // src/utils/status-config-resolution.ts
11082
11107
  init_frontmatter();
11083
- import { readFile as readFile16, writeFile as writeFile4, rm as rm2 } from "fs/promises";
11108
+ import { readFile as readFile15, writeFile as writeFile4, rm as rm2 } from "fs/promises";
11084
11109
  import { dirname as dirname4 } from "path";
11085
11110
 
11086
11111
  // src/utils/assignment-walk.ts
11087
11112
  init_fs();
11088
11113
  import { resolve as resolve20 } from "path";
11089
- import { readdir as readdir10 } from "fs/promises";
11114
+ import { readdir as readdir9 } from "fs/promises";
11090
11115
  async function listAssignmentsByProject(projectsDir, standaloneDir) {
11091
11116
  const result = {
11092
11117
  withAssignmentMd: [],
11093
11118
  orphanFolders: []
11094
11119
  };
11095
11120
  if (await fileExists(projectsDir)) {
11096
- const projects = await readdir10(projectsDir, { withFileTypes: true });
11121
+ const projects = await readdir9(projectsDir, { withFileTypes: true });
11097
11122
  for (const m of projects) {
11098
11123
  if (!m.isDirectory()) continue;
11099
11124
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
11100
11125
  const assignmentsDir2 = resolve20(projectsDir, m.name, "assignments");
11101
11126
  if (!await fileExists(assignmentsDir2)) continue;
11102
- const entries = await readdir10(assignmentsDir2, { withFileTypes: true });
11127
+ const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
11103
11128
  for (const a of entries) {
11104
11129
  if (!a.isDirectory()) continue;
11105
11130
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
@@ -11121,7 +11146,7 @@ async function listAssignmentsByProject(projectsDir, standaloneDir) {
11121
11146
  }
11122
11147
  }
11123
11148
  if (standaloneDir !== null && await fileExists(standaloneDir)) {
11124
- const entries = await readdir10(standaloneDir, { withFileTypes: true });
11149
+ const entries = await readdir9(standaloneDir, { withFileTypes: true });
11125
11150
  for (const a of entries) {
11126
11151
  if (!a.isDirectory()) continue;
11127
11152
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
@@ -11163,7 +11188,7 @@ async function scanAssignmentsByStatus(projectsDir, standaloneDir, ids) {
11163
11188
  const assignmentPath = `${entry.assignmentDir}/assignment.md`;
11164
11189
  let content;
11165
11190
  try {
11166
- content = await readFile16(assignmentPath, "utf-8");
11191
+ content = await readFile15(assignmentPath, "utf-8");
11167
11192
  } catch (err) {
11168
11193
  const code = err?.code;
11169
11194
  if (code === "ENOENT") {
@@ -11226,7 +11251,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
11226
11251
  const list = affected.get(r.id) ?? [];
11227
11252
  for (const a of list) {
11228
11253
  try {
11229
- const content = await readFile16(a.path, "utf-8");
11254
+ const content = await readFile15(a.path, "utf-8");
11230
11255
  buffer.set(a.path, content);
11231
11256
  } catch (err) {
11232
11257
  const code = err?.code;
@@ -11254,7 +11279,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
11254
11279
  for (const a of list) {
11255
11280
  let current;
11256
11281
  try {
11257
- current = await readFile16(a.path, "utf-8");
11282
+ current = await readFile15(a.path, "utf-8");
11258
11283
  } catch (err) {
11259
11284
  const code = err?.code;
11260
11285
  if (code === "ENOENT") {
@@ -11303,7 +11328,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
11303
11328
  const list = affected.get(r.id) ?? [];
11304
11329
  for (const a of list) {
11305
11330
  try {
11306
- const current = await readFile16(a.path, "utf-8");
11331
+ const current = await readFile15(a.path, "utf-8");
11307
11332
  const fm = parseAssignmentFrontmatter(current);
11308
11333
  if (fm.status !== r.id) {
11309
11334
  console.warn(
@@ -11439,6 +11464,7 @@ function mapResolutionErrorToHttp(err, applied) {
11439
11464
  }
11440
11465
  function createStatusConfigRouter(projectsDir, assignmentsDir2) {
11441
11466
  const router = Router8();
11467
+ installRecordsInvalidation(router);
11442
11468
  router.get("/", async (_req, res) => {
11443
11469
  try {
11444
11470
  const config = await getStatusConfig();
@@ -12149,7 +12175,7 @@ init_timestamp();
12149
12175
  init_fs();
12150
12176
  import { Router as Router11 } from "express";
12151
12177
  import { resolve as resolve23 } from "path";
12152
- import { readFile as readFile17 } from "fs/promises";
12178
+ import { readFile as readFile16 } from "fs/promises";
12153
12179
  init_playbooks();
12154
12180
  function statusForPlaybookError(code) {
12155
12181
  switch (code) {
@@ -12232,7 +12258,7 @@ function createPlaybooksRouter(playbooksDir2) {
12232
12258
  return;
12233
12259
  }
12234
12260
  const filePath = resolve23(playbooksDir2, resolved.filename);
12235
- const content = await readFile17(filePath, "utf-8");
12261
+ const content = await readFile16(filePath, "utf-8");
12236
12262
  res.json({
12237
12263
  documentType: "playbook",
12238
12264
  title: `Edit Playbook: ${resolved.slug}`,
@@ -12329,7 +12355,7 @@ init_parser2();
12329
12355
  init_fs();
12330
12356
  init_paths();
12331
12357
  import { Router as Router12 } from "express";
12332
- import { readdir as readdir11 } from "fs/promises";
12358
+ import { readdir as readdir10 } from "fs/promises";
12333
12359
  import { resolve as resolvePath, dirname as dirname5 } from "path";
12334
12360
  import { rename as rename5, mkdir as mkdir2 } from "fs/promises";
12335
12361
  init_slug();
@@ -12669,6 +12695,7 @@ async function promoteTodosToNewAssignment(groups, options) {
12669
12695
  }
12670
12696
 
12671
12697
  // src/dashboard/api-todos.ts
12698
+ init_api();
12672
12699
  var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
12673
12700
  function getWorkspaceParam(value) {
12674
12701
  if (Array.isArray(value)) {
@@ -12683,6 +12710,7 @@ function touchItem3(item) {
12683
12710
  }
12684
12711
  function createTodosRouter(todosDir2, broadcast, projectsDir) {
12685
12712
  const router = Router12();
12713
+ installRecordsInvalidation(router);
12686
12714
  function broadcastUpdate() {
12687
12715
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
12688
12716
  }
@@ -12802,7 +12830,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
12802
12830
  router.get("/", async (_req, res) => {
12803
12831
  try {
12804
12832
  await ensureDir(todosDir2);
12805
- const files = await readdir11(todosDir2).catch(() => []);
12833
+ const files = await readdir10(todosDir2).catch(() => []);
12806
12834
  const workspaces = [];
12807
12835
  for (const file of files) {
12808
12836
  if (typeof file !== "string") continue;
@@ -12917,7 +12945,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
12917
12945
  try {
12918
12946
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
12919
12947
  const { resolve: resolve29 } = await import("path");
12920
- const { readFile: readFile21 } = await import("fs/promises");
12948
+ const { readFile: readFile20 } = await import("fs/promises");
12921
12949
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
12922
12950
  const workspace = getWorkspaceParam(req.params.workspace);
12923
12951
  const checklist = await readChecklist(todosDir2, workspace);
@@ -12936,7 +12964,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
12936
12964
  await ensureDir(resolve29(todosDir2, "archive"));
12937
12965
  let archContent = "";
12938
12966
  if (await fileExists(archFile)) {
12939
- archContent = await readFile21(archFile, "utf-8");
12967
+ archContent = await readFile20(archFile, "utf-8");
12940
12968
  archContent = archContent.trimEnd() + "\n\n";
12941
12969
  } else {
12942
12970
  archContent = `---
@@ -13218,7 +13246,7 @@ workspace: ${workspace}
13218
13246
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
13219
13247
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
13220
13248
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
13221
- const { readFile: readFile21 } = await import("fs/promises");
13249
+ const { readFile: readFile20 } = await import("fs/promises");
13222
13250
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
13223
13251
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
13224
13252
  let assignmentRef;
@@ -13239,7 +13267,7 @@ workspace: ${workspace}
13239
13267
  }
13240
13268
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
13241
13269
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
13242
- let content = await readFile21(assignmentMdPath, "utf-8");
13270
+ let content = await readFile20(assignmentMdPath, "utf-8");
13243
13271
  content = appendTodosToAssignmentBody2(
13244
13272
  content,
13245
13273
  items.map((it) => ({
@@ -13427,8 +13455,9 @@ init_fs();
13427
13455
  init_paths();
13428
13456
  init_slug();
13429
13457
  import { Router as Router13 } from "express";
13430
- import { mkdir as mkdir3, readFile as readFile18, rename as rename6 } from "fs/promises";
13458
+ import { mkdir as mkdir3, readFile as readFile17, rename as rename6 } from "fs/promises";
13431
13459
  import { resolve as resolve25, dirname as dirname6 } from "path";
13460
+ init_api();
13432
13461
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
13433
13462
  function touchItem4(item) {
13434
13463
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -13477,6 +13506,7 @@ function notFound(res, slug) {
13477
13506
  }
13478
13507
  function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
13479
13508
  const router = Router13({ mergeParams: true });
13509
+ installRecordsInvalidation(router);
13480
13510
  function broadcastUpdate(projectSlug) {
13481
13511
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
13482
13512
  }
@@ -13649,7 +13679,7 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
13649
13679
  const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
13650
13680
  let archContent = "";
13651
13681
  if (await fileExists(archFile)) {
13652
- archContent = await readFile18(archFile, "utf-8");
13682
+ archContent = await readFile17(archFile, "utf-8");
13653
13683
  archContent = archContent.trimEnd() + "\n\n";
13654
13684
  } else {
13655
13685
  archContent = `---
@@ -14106,7 +14136,7 @@ workspace: ${slug}
14106
14136
  }
14107
14137
  const assignmentMdPath = resolve25(assignmentDir, "assignment.md");
14108
14138
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
14109
- let content = await readFile18(assignmentMdPath, "utf-8");
14139
+ let content = await readFile17(assignmentMdPath, "utf-8");
14110
14140
  content = appendTodosToAssignmentBody2(
14111
14141
  content,
14112
14142
  items.map((it) => ({
@@ -14302,7 +14332,7 @@ workspace: ${slug}
14302
14332
 
14303
14333
  // src/dashboard/api-bundles.ts
14304
14334
  import { Router as Router14 } from "express";
14305
- import { readdir as readdir12 } from "fs/promises";
14335
+ import { readdir as readdir11 } from "fs/promises";
14306
14336
 
14307
14337
  // src/todos/bundle-parser.ts
14308
14338
  init_parser();
@@ -14310,7 +14340,7 @@ init_fs();
14310
14340
  init_paths();
14311
14341
  init_parser2();
14312
14342
  import { randomBytes as randomBytes2 } from "crypto";
14313
- import { readFile as readFile19 } from "fs/promises";
14343
+ import { readFile as readFile18 } from "fs/promises";
14314
14344
  var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
14315
14345
  var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
14316
14346
  var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
@@ -14377,7 +14407,7 @@ function parseBundles(content) {
14377
14407
  async function readBundles(todosDir2) {
14378
14408
  const path = bundlesPath(todosDir2);
14379
14409
  if (!await fileExists(path)) return [];
14380
- const content = await readFile19(path, "utf-8");
14410
+ const content = await readFile18(path, "utf-8");
14381
14411
  return parseBundles(content).bundles;
14382
14412
  }
14383
14413
 
@@ -14426,7 +14456,7 @@ function createBundlesRouter(todosDir2, broadcast) {
14426
14456
  try {
14427
14457
  await ensureDir(todosDir2);
14428
14458
  const bundles = await readBundles(todosDir2);
14429
- const workspaceFiles = await readdir12(todosDir2).catch(() => []);
14459
+ const workspaceFiles = await readdir11(todosDir2).catch(() => []);
14430
14460
  const itemsByKey = /* @__PURE__ */ new Map();
14431
14461
  for (const f of workspaceFiles) {
14432
14462
  if (typeof f !== "string") continue;
@@ -14540,6 +14570,8 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
14540
14570
 
14541
14571
  // src/dashboard/api-backup.ts
14542
14572
  init_config2();
14573
+ init_api();
14574
+ init_scanner();
14543
14575
  import { Router as Router16 } from "express";
14544
14576
 
14545
14577
  // src/utils/github-backup.ts
@@ -14548,7 +14580,7 @@ init_fs();
14548
14580
  init_config2();
14549
14581
  import { execFile as execFile2 } from "child_process";
14550
14582
  import { promisify as promisify2 } from "util";
14551
- import { cp, mkdtemp, rm as rm3, readFile as readFile20, writeFile as writeFile5, unlink as unlink5, stat, open as open2, rename as rename7 } from "fs/promises";
14583
+ import { cp, mkdtemp, rm as rm3, readFile as readFile19, writeFile as writeFile5, unlink as unlink5, stat, open as open2, rename as rename7 } from "fs/promises";
14552
14584
  import { resolve as resolve27, join as join2 } from "path";
14553
14585
  import { tmpdir } from "os";
14554
14586
  var exec2 = promisify2(execFile2);
@@ -14609,7 +14641,7 @@ async function acquireLock() {
14609
14641
  return lockPath;
14610
14642
  } catch (err) {
14611
14643
  if (err.code === "EEXIST") {
14612
- const pid = await readFile20(lockPath, "utf-8").catch(() => "");
14644
+ const pid = await readFile19(lockPath, "utf-8").catch(() => "");
14613
14645
  throw new Error(
14614
14646
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
14615
14647
  );
@@ -14656,7 +14688,7 @@ function resolveCategoriesStrict(csv) {
14656
14688
  return parseCategoriesStrict(parts);
14657
14689
  }
14658
14690
  async function readSanitizedConfig(configPath) {
14659
- const content = await readFile20(configPath, "utf-8");
14691
+ const content = await readFile19(configPath, "utf-8");
14660
14692
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
14661
14693
  }
14662
14694
  async function backupToGithub(overrides) {
@@ -14938,6 +14970,9 @@ function createBackupRouter() {
14938
14970
  success: false,
14939
14971
  error: error instanceof Error ? error.message : String(error)
14940
14972
  });
14973
+ } finally {
14974
+ invalidateRecordsCache();
14975
+ clearScanCache();
14941
14976
  }
14942
14977
  });
14943
14978
  return router;
@@ -15176,6 +15211,9 @@ function createDashboardServer(options) {
15176
15211
  });
15177
15212
  });
15178
15213
  function broadcast(message) {
15214
+ if (message.type === "project-updated" || message.type === "assignment-updated") {
15215
+ invalidateRecordsCache();
15216
+ }
15179
15217
  const data = JSON.stringify(message);
15180
15218
  for (const client of clients) {
15181
15219
  if (client.readyState === WebSocket.OPEN) {