syntaur 0.17.0 → 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 +379 -217
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +512 -349
  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;
@@ -4270,13 +4270,16 @@ __export(scanner_exports, {
4270
4270
  autoLinkPane: () => autoLinkPane,
4271
4271
  checkTmuxAvailable: () => checkTmuxAvailable,
4272
4272
  clearScanCache: () => clearScanCache,
4273
+ descendantPidsFromSnapshot: () => descendantPidsFromSnapshot,
4273
4274
  execQuiet: () => execQuiet,
4274
4275
  findListeningPorts: () => findListeningPorts,
4275
4276
  getDescendantPids: () => getDescendantPids,
4276
4277
  getGitInfo: () => getGitInfo,
4277
4278
  getLsofOutput: () => getLsofOutput,
4279
+ getProcessSnapshot: () => getProcessSnapshot,
4278
4280
  listTmuxPanes: () => listTmuxPanes,
4279
4281
  loadWorkspaceRecords: () => loadWorkspaceRecords,
4282
+ parseProcessSnapshot: () => parseProcessSnapshot,
4280
4283
  parseTmuxPaneOutput: () => parseTmuxPaneOutput,
4281
4284
  resolveAndNormalize: () => resolveAndNormalize,
4282
4285
  scanAllSessions: () => scanAllSessions,
@@ -4286,9 +4289,27 @@ __export(scanner_exports, {
4286
4289
  import { execFile } from "child_process";
4287
4290
  import { promisify } from "util";
4288
4291
  import { resolve as resolve12 } from "path";
4289
- import { realpath, readdir as readdir7, readFile as readFile9 } from "fs/promises";
4292
+ import { realpath } from "fs/promises";
4290
4293
  function clearScanCache() {
4291
4294
  cache = null;
4295
+ forceFreshNext = true;
4296
+ scanEpoch++;
4297
+ inFlight = null;
4298
+ tmuxAvailableCache = null;
4299
+ }
4300
+ function emptyScan() {
4301
+ return { sessions: [], tmuxAvailable: false };
4302
+ }
4303
+ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
4304
+ return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
4305
+ }
4306
+ function delay(ms) {
4307
+ return new Promise((resolve29) => {
4308
+ const timer2 = setTimeout(resolve29, ms);
4309
+ if (typeof timer2.unref === "function") {
4310
+ timer2.unref();
4311
+ }
4312
+ });
4292
4313
  }
4293
4314
  function parseTmuxPaneOutput(output) {
4294
4315
  return output.trim().split("\n").filter((line) => line.length > 0).map((line) => {
@@ -4322,15 +4343,20 @@ function findListeningPorts(lsofOutput, pids) {
4322
4343
  }
4323
4344
  async function execQuiet(cmd, args) {
4324
4345
  try {
4325
- const { stdout } = await exec(cmd, args);
4346
+ const { stdout } = await exec(cmd, args, {
4347
+ timeout: PROBE_TIMEOUT_MS,
4348
+ maxBuffer: PROBE_MAX_BUFFER
4349
+ });
4326
4350
  return stdout.trim();
4327
4351
  } catch {
4328
4352
  return "";
4329
4353
  }
4330
4354
  }
4331
4355
  async function checkTmuxAvailable() {
4332
- const result = await execQuiet("which", ["tmux"]);
4333
- return result.length > 0;
4356
+ if (!tmuxAvailableCache) {
4357
+ tmuxAvailableCache = execQuiet("which", ["tmux"]).then((result) => result.length > 0);
4358
+ }
4359
+ return tmuxAvailableCache;
4334
4360
  }
4335
4361
  async function sessionAlive(name) {
4336
4362
  try {
@@ -4370,6 +4396,40 @@ async function getDescendantPids(rootPid, maxDepth = 4) {
4370
4396
  }
4371
4397
  return all;
4372
4398
  }
4399
+ function parseProcessSnapshot(psOutput) {
4400
+ const childrenOf = /* @__PURE__ */ new Map();
4401
+ for (const line of psOutput.split("\n")) {
4402
+ const parts = line.trim().split(/\s+/);
4403
+ if (parts.length < 2) continue;
4404
+ const pid = parseInt(parts[0], 10);
4405
+ const ppid = parseInt(parts[1], 10);
4406
+ if (isNaN(pid) || isNaN(ppid)) continue;
4407
+ const siblings = childrenOf.get(ppid);
4408
+ if (siblings) siblings.push(pid);
4409
+ else childrenOf.set(ppid, [pid]);
4410
+ }
4411
+ return { childrenOf };
4412
+ }
4413
+ async function getProcessSnapshot() {
4414
+ return parseProcessSnapshot(await execQuiet("ps", ["-axo", "pid=,ppid="]));
4415
+ }
4416
+ function descendantPidsFromSnapshot(rootPid, snapshot, maxDepth = 4) {
4417
+ const all = /* @__PURE__ */ new Set([rootPid]);
4418
+ let frontier = [rootPid];
4419
+ for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
4420
+ const nextFrontier = [];
4421
+ for (const pid of frontier) {
4422
+ for (const child of snapshot.childrenOf.get(pid) ?? []) {
4423
+ if (!all.has(child)) {
4424
+ all.add(child);
4425
+ nextFrontier.push(child);
4426
+ }
4427
+ }
4428
+ }
4429
+ frontier = nextFrontier;
4430
+ }
4431
+ return all;
4432
+ }
4373
4433
  async function getLsofOutput() {
4374
4434
  return execQuiet("lsof", ["-i", "-P", "-n", "-sTCP:LISTEN"]);
4375
4435
  }
@@ -4391,62 +4451,11 @@ async function getGitInfo(cwd) {
4391
4451
  return { branch: branch || null, worktree: isWorktree };
4392
4452
  }
4393
4453
  async function loadWorkspaceRecords(projectsDir, assignmentsDir2) {
4394
- const records = [];
4395
4454
  try {
4396
- const projects = await listProjects(projectsDir);
4397
- for (const project of projects) {
4398
- const projectAssignmentsDir = resolve12(projectsDir, project.slug, "assignments");
4399
- let slugs;
4400
- try {
4401
- slugs = await readdir7(projectAssignmentsDir);
4402
- } catch {
4403
- continue;
4404
- }
4405
- for (const aslug of slugs) {
4406
- const aFile = resolve12(projectAssignmentsDir, aslug, "assignment.md");
4407
- try {
4408
- const raw = await readFile9(aFile, "utf-8");
4409
- const [fm] = extractFrontmatter2(raw);
4410
- if (!fm) continue;
4411
- records.push({
4412
- projectSlug: project.slug,
4413
- assignmentSlug: aslug,
4414
- assignmentTitle: getField(fm, "title") ?? aslug,
4415
- worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
4416
- branch: getNestedField(fm, "workspace", "branch") ?? null
4417
- });
4418
- } catch {
4419
- continue;
4420
- }
4421
- }
4422
- }
4455
+ return await listWorkspaceRecords(projectsDir, assignmentsDir2);
4423
4456
  } catch {
4457
+ return [];
4424
4458
  }
4425
- if (assignmentsDir2) {
4426
- try {
4427
- const entries = await readdir7(assignmentsDir2);
4428
- for (const id of entries) {
4429
- if (id.startsWith(".") || id.startsWith("_")) continue;
4430
- const aFile = resolve12(assignmentsDir2, id, "assignment.md");
4431
- try {
4432
- const raw = await readFile9(aFile, "utf-8");
4433
- const [fm] = extractFrontmatter2(raw);
4434
- if (!fm) continue;
4435
- records.push({
4436
- projectSlug: null,
4437
- assignmentSlug: id,
4438
- assignmentTitle: getField(fm, "title") ?? id,
4439
- worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
4440
- branch: getNestedField(fm, "workspace", "branch") ?? null
4441
- });
4442
- } catch {
4443
- continue;
4444
- }
4445
- }
4446
- } catch {
4447
- }
4448
- }
4449
- return records;
4450
4459
  }
4451
4460
  async function resolveAndNormalize(p) {
4452
4461
  try {
@@ -4475,7 +4484,7 @@ async function autoLinkPane(cwd, branch, records) {
4475
4484
  }
4476
4485
  return null;
4477
4486
  }
4478
- async function scanSession(sessionData, lsofOutput, workspaceRecords) {
4487
+ async function scanSession(sessionData, lsofOutput, workspaceRecords, procSnapshot) {
4479
4488
  const now = (/* @__PURE__ */ new Date()).toISOString();
4480
4489
  const alive = await sessionAlive(sessionData.session);
4481
4490
  if (!alive) {
@@ -4498,13 +4507,13 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords) {
4498
4507
  windowMap.get(rp.windowIndex).panes.push(rp);
4499
4508
  }
4500
4509
  const cwdSet = new Set(rawPanes.map((p) => p.cwd));
4501
- const gitInfoCache = /* @__PURE__ */ new Map();
4502
- for (const cwd of cwdSet) {
4503
- gitInfoCache.set(cwd, await getGitInfo(cwd));
4504
- }
4510
+ const gitInfoEntries = await Promise.all(
4511
+ [...cwdSet].map(async (cwd) => [cwd, await getGitInfo(cwd)])
4512
+ );
4513
+ const gitInfoCache = new Map(gitInfoEntries);
4505
4514
  const pidToPaneKey = /* @__PURE__ */ new Map();
4506
4515
  for (const rp of rawPanes) {
4507
- const descendants = await getDescendantPids(rp.pid);
4516
+ const descendants = descendantPidsFromSnapshot(rp.pid, procSnapshot);
4508
4517
  const key = `${rp.windowIndex}:${rp.paneIndex}`;
4509
4518
  for (const pid of descendants) {
4510
4519
  pidToPaneKey.set(pid, key);
@@ -4529,37 +4538,38 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords) {
4529
4538
  }
4530
4539
  const windows = [];
4531
4540
  for (const [windowIndex, { name, panes: rawPanesInWindow }] of windowMap) {
4532
- const panes = [];
4533
- for (const rp of rawPanesInWindow) {
4534
- const key = `${rp.windowIndex}:${rp.paneIndex}`;
4535
- const gitInfo = gitInfoCache.get(rp.cwd) ?? { branch: null, worktree: false };
4536
- const ports = panePorts.get(key) ?? [];
4537
- const urls = ports.map((p) => `http://localhost:${p}`);
4538
- const override = sessionData.overrides[key];
4539
- let assignment = null;
4540
- if (override) {
4541
- const rec = workspaceRecords.find(
4542
- (r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
4543
- );
4544
- assignment = {
4545
- project: override.project,
4546
- slug: override.assignment,
4547
- title: rec?.assignmentTitle ?? override.assignment
4541
+ const panes = await Promise.all(
4542
+ rawPanesInWindow.map(async (rp) => {
4543
+ const key = `${rp.windowIndex}:${rp.paneIndex}`;
4544
+ const gitInfo = gitInfoCache.get(rp.cwd) ?? { branch: null, worktree: false };
4545
+ const ports = panePorts.get(key) ?? [];
4546
+ const urls = ports.map((p) => `http://localhost:${p}`);
4547
+ const override = sessionData.overrides[key];
4548
+ let assignment = null;
4549
+ if (override) {
4550
+ const rec = workspaceRecords.find(
4551
+ (r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
4552
+ );
4553
+ assignment = {
4554
+ project: override.project,
4555
+ slug: override.assignment,
4556
+ title: rec?.assignmentTitle ?? override.assignment
4557
+ };
4558
+ } else {
4559
+ assignment = await autoLinkPane(rp.cwd, gitInfo.branch, workspaceRecords);
4560
+ }
4561
+ return {
4562
+ index: rp.paneIndex,
4563
+ command: rp.command,
4564
+ cwd: rp.cwd,
4565
+ branch: gitInfo.branch,
4566
+ worktree: gitInfo.worktree,
4567
+ ports,
4568
+ urls,
4569
+ assignment
4548
4570
  };
4549
- } else {
4550
- assignment = await autoLinkPane(rp.cwd, gitInfo.branch, workspaceRecords);
4551
- }
4552
- panes.push({
4553
- index: rp.paneIndex,
4554
- command: rp.command,
4555
- cwd: rp.cwd,
4556
- branch: gitInfo.branch,
4557
- worktree: gitInfo.worktree,
4558
- ports,
4559
- urls,
4560
- assignment
4561
- });
4562
- }
4571
+ })
4572
+ );
4563
4573
  windows.push({ index: windowIndex, name, panes });
4564
4574
  }
4565
4575
  windows.sort((a, b) => a.index - b.index);
@@ -4631,53 +4641,114 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
4631
4641
  windows: [{ index: 0, name: "process", panes: [pane] }]
4632
4642
  };
4633
4643
  }
4644
+ async function performScan(serversDir2, projectsDir, options) {
4645
+ const epoch = scanEpoch;
4646
+ const [tmuxAvailable, names, lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
4647
+ checkTmuxAvailable(),
4648
+ listSessionFiles(serversDir2),
4649
+ getLsofOutput(),
4650
+ loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
4651
+ getProcessSnapshot()
4652
+ ]);
4653
+ const datas = await Promise.all(names.map((name) => readSessionFile(serversDir2, name)));
4654
+ const scanned = await Promise.all(
4655
+ datas.map(async (data) => {
4656
+ if (!data) return null;
4657
+ if (data.kind === "process") {
4658
+ return scanProcessSession(data, lsofOutput, workspaceRecords);
4659
+ }
4660
+ if (tmuxAvailable) {
4661
+ return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
4662
+ }
4663
+ return null;
4664
+ })
4665
+ );
4666
+ const result = {
4667
+ sessions: scanned.filter((s) => s !== null),
4668
+ tmuxAvailable
4669
+ };
4670
+ if (epoch === scanEpoch) {
4671
+ cache = { data: result, expiry: Date.now() + CACHE_TTL_MS };
4672
+ lastKnown = result;
4673
+ forceFreshNext = false;
4674
+ }
4675
+ return result;
4676
+ }
4677
+ function refreshInBackground(serversDir2, projectsDir, options) {
4678
+ if (!inFlight) {
4679
+ const p = performScan(serversDir2, projectsDir, options);
4680
+ p.finally(() => {
4681
+ if (inFlight === p) inFlight = null;
4682
+ }).catch(() => {
4683
+ });
4684
+ inFlight = p;
4685
+ }
4686
+ return inFlight;
4687
+ }
4634
4688
  async function scanAllSessions(serversDir2, projectsDir, options) {
4689
+ const key = scanKey(serversDir2, projectsDir, options?.assignmentsDir);
4690
+ if (key !== cacheKey) {
4691
+ cache = null;
4692
+ lastKnown = null;
4693
+ inFlight = null;
4694
+ forceFreshNext = false;
4695
+ scanEpoch++;
4696
+ cacheKey = key;
4697
+ }
4635
4698
  if (!options?.bypassCache && cache && Date.now() < cache.expiry) {
4636
4699
  return cache.data;
4637
4700
  }
4638
- const tmuxAvailable = await checkTmuxAvailable();
4639
- const names = await listSessionFiles(serversDir2);
4640
- const lsofOutput = await getLsofOutput();
4641
- const workspaceRecords = await loadWorkspaceRecords(projectsDir, options?.assignmentsDir);
4642
- const sessions = [];
4643
- for (const name of names) {
4644
- const data = await readSessionFile(serversDir2, name);
4645
- if (!data) continue;
4646
- if (data.kind === "process") {
4647
- sessions.push(await scanProcessSession(data, lsofOutput, workspaceRecords));
4648
- } else if (tmuxAvailable) {
4649
- sessions.push(await scanSession(data, lsofOutput, workspaceRecords));
4650
- }
4701
+ const refresh = refreshInBackground(serversDir2, projectsDir, options);
4702
+ const canServeStale = lastKnown !== null && !options?.bypassCache && !forceFreshNext;
4703
+ if (canServeStale) {
4704
+ void refresh.catch(() => {
4705
+ });
4706
+ return lastKnown;
4651
4707
  }
4652
- const result = { sessions, tmuxAvailable };
4653
- cache = { data: result, expiry: Date.now() + CACHE_TTL_MS };
4654
- return result;
4708
+ if (options?.nonBlocking) {
4709
+ return Promise.race([
4710
+ refresh.catch(() => emptyScan()),
4711
+ delay(COLD_WAIT_BUDGET_MS).then(() => emptyScan())
4712
+ ]);
4713
+ }
4714
+ return refresh;
4655
4715
  }
4656
4716
  async function scanSingleSession(serversDir2, projectsDir, name, options) {
4657
4717
  const data = await readSessionFile(serversDir2, name);
4658
4718
  if (!data) return null;
4659
- const lsofOutput = await getLsofOutput();
4660
- const workspaceRecords = await loadWorkspaceRecords(projectsDir, options?.assignmentsDir);
4719
+ const [lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
4720
+ getLsofOutput(),
4721
+ loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
4722
+ getProcessSnapshot()
4723
+ ]);
4661
4724
  if (data.kind === "process") {
4662
4725
  return scanProcessSession(data, lsofOutput, workspaceRecords);
4663
4726
  }
4664
- return scanSession(data, lsofOutput, workspaceRecords);
4727
+ return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
4665
4728
  }
4666
- var exec, cache, CACHE_TTL_MS;
4729
+ var exec, cache, lastKnown, inFlight, forceFreshNext, scanEpoch, cacheKey, CACHE_TTL_MS, COLD_WAIT_BUDGET_MS, PROBE_TIMEOUT_MS, PROBE_MAX_BUFFER, tmuxAvailableCache;
4667
4730
  var init_scanner = __esm({
4668
4731
  "src/dashboard/scanner.ts"() {
4669
4732
  "use strict";
4670
4733
  init_api();
4671
4734
  init_servers();
4672
- init_parser();
4673
4735
  exec = promisify(execFile);
4674
4736
  cache = null;
4737
+ lastKnown = null;
4738
+ inFlight = null;
4739
+ forceFreshNext = false;
4740
+ scanEpoch = 0;
4741
+ cacheKey = null;
4675
4742
  CACHE_TTL_MS = 1e4;
4743
+ COLD_WAIT_BUDGET_MS = 2500;
4744
+ PROBE_TIMEOUT_MS = 15e3;
4745
+ PROBE_MAX_BUFFER = 32 * 1024 * 1024;
4746
+ tmuxAvailableCache = null;
4676
4747
  }
4677
4748
  });
4678
4749
 
4679
4750
  // src/dashboard/api.ts
4680
- 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";
4681
4752
  import { resolve as resolve13, dirname as dirname2, basename } from "path";
4682
4753
  function clearFrontmatterField(content, key) {
4683
4754
  const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
@@ -4717,10 +4788,41 @@ function emitTrace(traces, meta) {
4717
4788
  JSON.stringify({ kind: "overview-trace", totalMs, phases: traces.entries, subPhases, ...meta })
4718
4789
  );
4719
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
+ }
4720
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) {
4721
4823
  if (!assignmentsDir2) return [];
4722
4824
  if (!await fileExists(assignmentsDir2)) return [];
4723
- const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
4825
+ const entries = await readdir7(assignmentsDir2, { withFileTypes: true });
4724
4826
  const records = [];
4725
4827
  for (const entry of entries) {
4726
4828
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
@@ -4728,7 +4830,7 @@ async function listStandaloneRecords(assignmentsDir2) {
4728
4830
  const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
4729
4831
  if (!await fileExists(assignmentMdPath)) continue;
4730
4832
  try {
4731
- const content = await readFile10(assignmentMdPath, "utf-8");
4833
+ const content = await readFile9(assignmentMdPath, "utf-8");
4732
4834
  const record = parseAssignmentFull(content);
4733
4835
  records.push({ assignmentDir, id: entry.name, record });
4734
4836
  } catch {
@@ -4804,7 +4906,7 @@ async function listProjects(projectsDir) {
4804
4906
  async function readWorkspaceRegistry(projectsDir) {
4805
4907
  const registryPath = resolve13(dirname2(projectsDir), "workspaces.json");
4806
4908
  try {
4807
- const raw = await readFile10(registryPath, "utf-8");
4909
+ const raw = await readFile9(registryPath, "utf-8");
4808
4910
  const parsed = JSON.parse(raw);
4809
4911
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
4810
4912
  } catch {
@@ -4840,6 +4942,34 @@ async function listWorkspaces(projectsDir, assignmentsDir2) {
4840
4942
  const workspaces = Array.from(workspaceSet).sort();
4841
4943
  return { workspaces, hasUngrouped };
4842
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
+ }
4843
4973
  async function createWorkspace(projectsDir, name) {
4844
4974
  const registered = await readWorkspaceRegistry(projectsDir);
4845
4975
  if (!registered.includes(name)) {
@@ -4865,7 +4995,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
4865
4995
  const timestamp = nowTimestamp();
4866
4996
  for (const slug of projectsReferencing) {
4867
4997
  const path = resolve13(projectsDir, slug, "project.md");
4868
- const raw = await readFile10(path, "utf-8");
4998
+ const raw = await readFile9(path, "utf-8");
4869
4999
  let next = clearFrontmatterField(raw, "workspace");
4870
5000
  next = setUpdatedField(next, timestamp);
4871
5001
  await writeFileForce(path, next);
@@ -4874,7 +5004,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
4874
5004
  for (const id of standalonesReferencing) {
4875
5005
  if (!opts.assignmentsDir) break;
4876
5006
  const path = resolve13(opts.assignmentsDir, id, "assignment.md");
4877
- const raw = await readFile10(path, "utf-8");
5007
+ const raw = await readFile9(path, "utf-8");
4878
5008
  let next = clearFrontmatterField(raw, "workspaceGroup");
4879
5009
  next = setUpdatedField(next, timestamp);
4880
5010
  await writeFileForce(path, next);
@@ -4884,6 +5014,9 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
4884
5014
  const registered = await readWorkspaceRegistry(projectsDir);
4885
5015
  const filtered = registered.filter((w) => w !== name);
4886
5016
  await writeWorkspaceRegistry(projectsDir, filtered);
5017
+ if (rewroteFiles) {
5018
+ invalidateRecordsCache();
5019
+ }
4887
5020
  return { rewroteFiles };
4888
5021
  }
4889
5022
  async function getOverview(projectsDir, serversDir2, assignmentsDir2, options = {}) {
@@ -4927,7 +5060,12 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
4927
5060
  const servers = await timed(
4928
5061
  traces,
4929
5062
  "scan-tmux-sessions",
4930
- () => scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 })
5063
+ () => (
5064
+ // Overview only needs aggregate counts — never block its render on a
5065
+ // live scan; serve last-known stats and let the scan refresh in the
5066
+ // background (stale-while-revalidate).
5067
+ scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2, nonBlocking: true })
5068
+ )
4931
5069
  );
4932
5070
  if (servers.tmuxAvailable) {
4933
5071
  const alive = servers.sessions.filter((s) => s.alive).length;
@@ -5046,7 +5184,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
5046
5184
  if (!filePath || !await fileExists(filePath)) {
5047
5185
  return null;
5048
5186
  }
5049
- const content = await readFile10(filePath, "utf-8");
5187
+ const content = await readFile9(filePath, "utf-8");
5050
5188
  const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
5051
5189
  return {
5052
5190
  documentType,
@@ -5072,7 +5210,7 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
5072
5210
  if (!fileName) return null;
5073
5211
  const filePath = resolve13(resolved.assignmentDir, fileName);
5074
5212
  if (!await fileExists(filePath)) return null;
5075
- const content = await readFile10(filePath, "utf-8");
5213
+ const content = await readFile9(filePath, "utf-8");
5076
5214
  const label = resolved.id;
5077
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}`;
5078
5216
  return {
@@ -5091,7 +5229,7 @@ async function getProjectDetail(projectsDir, slug) {
5091
5229
  if (!await fileExists(projectMdPath)) {
5092
5230
  return null;
5093
5231
  }
5094
- const projectContent = await readFile10(projectMdPath, "utf-8");
5232
+ const projectContent = await readFile9(projectMdPath, "utf-8");
5095
5233
  const project = parseProject(projectContent);
5096
5234
  const assignments = await listAssignmentRecords(projectPath);
5097
5235
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -5128,18 +5266,18 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5128
5266
  if (!await fileExists(assignmentMdPath)) {
5129
5267
  return null;
5130
5268
  }
5131
- const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
5269
+ const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
5132
5270
  const assignment = parseAssignmentFull(assignmentContent);
5133
5271
  let projectWorkspace = null;
5134
5272
  const projectMdPath = resolve13(projectsDir, projectSlug, "project.md");
5135
5273
  if (await fileExists(projectMdPath)) {
5136
- const projectContent = await readFile10(projectMdPath, "utf-8");
5274
+ const projectContent = await readFile9(projectMdPath, "utf-8");
5137
5275
  projectWorkspace = parseProject(projectContent).workspace;
5138
5276
  }
5139
5277
  let plan = null;
5140
5278
  const planPath = resolve13(assignmentDir, "plan.md");
5141
5279
  if (await fileExists(planPath)) {
5142
- const planContent = await readFile10(planPath, "utf-8");
5280
+ const planContent = await readFile9(planPath, "utf-8");
5143
5281
  const parsed = parsePlan(planContent);
5144
5282
  plan = {
5145
5283
  status: parsed.status,
@@ -5150,7 +5288,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5150
5288
  let scratchpad = null;
5151
5289
  const scratchpadPath = resolve13(assignmentDir, "scratchpad.md");
5152
5290
  if (await fileExists(scratchpadPath)) {
5153
- const scratchpadContent = await readFile10(scratchpadPath, "utf-8");
5291
+ const scratchpadContent = await readFile9(scratchpadPath, "utf-8");
5154
5292
  const parsed = parseScratchpad(scratchpadContent);
5155
5293
  scratchpad = {
5156
5294
  updated: parsed.updated,
@@ -5160,7 +5298,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5160
5298
  let handoff = null;
5161
5299
  const handoffPath = resolve13(assignmentDir, "handoff.md");
5162
5300
  if (await fileExists(handoffPath)) {
5163
- const handoffContent = await readFile10(handoffPath, "utf-8");
5301
+ const handoffContent = await readFile9(handoffPath, "utf-8");
5164
5302
  const parsed = parseHandoff(handoffContent);
5165
5303
  handoff = {
5166
5304
  updated: parsed.updated,
@@ -5171,7 +5309,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5171
5309
  let decisionRecord = null;
5172
5310
  const decisionRecordPath = resolve13(assignmentDir, "decision-record.md");
5173
5311
  if (await fileExists(decisionRecordPath)) {
5174
- const decisionRecordContent = await readFile10(decisionRecordPath, "utf-8");
5312
+ const decisionRecordContent = await readFile9(decisionRecordPath, "utf-8");
5175
5313
  const parsed = parseDecisionRecord(decisionRecordContent);
5176
5314
  decisionRecord = {
5177
5315
  updated: parsed.updated,
@@ -5182,7 +5320,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5182
5320
  let progress = null;
5183
5321
  const progressPath = resolve13(assignmentDir, "progress.md");
5184
5322
  if (await fileExists(progressPath)) {
5185
- const progressContent = await readFile10(progressPath, "utf-8");
5323
+ const progressContent = await readFile9(progressPath, "utf-8");
5186
5324
  const parsed = parseProgress(progressContent);
5187
5325
  progress = {
5188
5326
  updated: parsed.updated,
@@ -5193,7 +5331,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5193
5331
  let comments = null;
5194
5332
  const commentsPath = resolve13(assignmentDir, "comments.md");
5195
5333
  if (await fileExists(commentsPath)) {
5196
- const commentsContent = await readFile10(commentsPath, "utf-8");
5334
+ const commentsContent = await readFile9(commentsPath, "utf-8");
5197
5335
  const parsed = parseComments(commentsContent);
5198
5336
  comments = {
5199
5337
  updated: parsed.updated,
@@ -5344,7 +5482,7 @@ async function countMentionsInAssignment(sourceDir, target) {
5344
5482
  const bodies = [];
5345
5483
  const assignmentMd = resolve13(sourceDir, "assignment.md");
5346
5484
  if (await fileExists(assignmentMd)) {
5347
- const content = await readFile10(assignmentMd, "utf-8");
5485
+ const content = await readFile9(assignmentMd, "utf-8");
5348
5486
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
5349
5487
  if (todosMatch) bodies.push(todosMatch[1]);
5350
5488
  }
@@ -5352,7 +5490,7 @@ async function countMentionsInAssignment(sourceDir, target) {
5352
5490
  const path = resolve13(sourceDir, filename);
5353
5491
  if (await fileExists(path)) {
5354
5492
  try {
5355
- bodies.push(await readFile10(path, "utf-8"));
5493
+ bodies.push(await readFile9(path, "utf-8"));
5356
5494
  } catch {
5357
5495
  }
5358
5496
  }
@@ -5412,42 +5550,42 @@ async function buildStandaloneAssignmentDetail(resolved) {
5412
5550
  const assignmentDir = resolved.assignmentDir;
5413
5551
  const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
5414
5552
  if (!await fileExists(assignmentMdPath)) return null;
5415
- const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
5553
+ const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
5416
5554
  const assignment = parseAssignmentFull(assignmentContent);
5417
5555
  let plan = null;
5418
5556
  const planPath = resolve13(assignmentDir, "plan.md");
5419
5557
  if (await fileExists(planPath)) {
5420
- const parsed = parsePlan(await readFile10(planPath, "utf-8"));
5558
+ const parsed = parsePlan(await readFile9(planPath, "utf-8"));
5421
5559
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
5422
5560
  }
5423
5561
  let scratchpad = null;
5424
5562
  const scratchpadPath = resolve13(assignmentDir, "scratchpad.md");
5425
5563
  if (await fileExists(scratchpadPath)) {
5426
- const parsed = parseScratchpad(await readFile10(scratchpadPath, "utf-8"));
5564
+ const parsed = parseScratchpad(await readFile9(scratchpadPath, "utf-8"));
5427
5565
  scratchpad = { updated: parsed.updated, body: parsed.body };
5428
5566
  }
5429
5567
  let handoff = null;
5430
5568
  const handoffPath = resolve13(assignmentDir, "handoff.md");
5431
5569
  if (await fileExists(handoffPath)) {
5432
- const parsed = parseHandoff(await readFile10(handoffPath, "utf-8"));
5570
+ const parsed = parseHandoff(await readFile9(handoffPath, "utf-8"));
5433
5571
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
5434
5572
  }
5435
5573
  let decisionRecord = null;
5436
5574
  const decisionRecordPath = resolve13(assignmentDir, "decision-record.md");
5437
5575
  if (await fileExists(decisionRecordPath)) {
5438
- const parsed = parseDecisionRecord(await readFile10(decisionRecordPath, "utf-8"));
5576
+ const parsed = parseDecisionRecord(await readFile9(decisionRecordPath, "utf-8"));
5439
5577
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
5440
5578
  }
5441
5579
  let progress = null;
5442
5580
  const progressPath = resolve13(assignmentDir, "progress.md");
5443
5581
  if (await fileExists(progressPath)) {
5444
- const parsed = parseProgress(await readFile10(progressPath, "utf-8"));
5582
+ const parsed = parseProgress(await readFile9(progressPath, "utf-8"));
5445
5583
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
5446
5584
  }
5447
5585
  let comments = null;
5448
5586
  const commentsPath = resolve13(assignmentDir, "comments.md");
5449
5587
  if (await fileExists(commentsPath)) {
5450
- const parsed = parseComments(await readFile10(commentsPath, "utf-8"));
5588
+ const parsed = parseComments(await readFile9(commentsPath, "utf-8"));
5451
5589
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
5452
5590
  }
5453
5591
  const detail = {
@@ -5484,6 +5622,14 @@ async function buildStandaloneAssignmentDetail(resolved) {
5484
5622
  return detail;
5485
5623
  }
5486
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) {
5487
5633
  if (!await fileExists(projectsDir)) {
5488
5634
  return [];
5489
5635
  }
@@ -5491,7 +5637,7 @@ async function listProjectRecords(projectsDir, traces) {
5491
5637
  migratedProjectsDirs.add(projectsDir);
5492
5638
  await migrateLegacyProjectFiles(projectsDir);
5493
5639
  }
5494
- const entries = await readdir8(projectsDir, { withFileTypes: true });
5640
+ const entries = await readdir7(projectsDir, { withFileTypes: true });
5495
5641
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
5496
5642
  const maybeRecords = await Promise.all(
5497
5643
  projectDirs.map(async (entry) => {
@@ -5501,7 +5647,7 @@ async function listProjectRecords(projectsDir, traces) {
5501
5647
  return null;
5502
5648
  }
5503
5649
  const t0 = traces ? performance.now() : 0;
5504
- const projectContent = await readFile10(projectMdPath, "utf-8");
5650
+ const projectContent = await readFile9(projectMdPath, "utf-8");
5505
5651
  const project = parseProject(projectContent);
5506
5652
  if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
5507
5653
  const t1 = traces ? performance.now() : 0;
@@ -5546,7 +5692,7 @@ async function listAssignmentRecords(projectPath, traces) {
5546
5692
  if (!await fileExists(assignmentsDir2)) {
5547
5693
  return [];
5548
5694
  }
5549
- const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
5695
+ const entries = await readdir7(assignmentsDir2, { withFileTypes: true });
5550
5696
  const dirEntries = entries.filter((entry) => entry.isDirectory());
5551
5697
  const maybeRecords = await Promise.all(
5552
5698
  dirEntries.map(async (entry) => {
@@ -5555,7 +5701,7 @@ async function listAssignmentRecords(projectPath, traces) {
5555
5701
  return null;
5556
5702
  }
5557
5703
  const t0 = traces ? performance.now() : 0;
5558
- const content = await readFile10(assignmentMd, "utf-8");
5704
+ const content = await readFile9(assignmentMd, "utf-8");
5559
5705
  const parsed = parseAssignmentFull(content);
5560
5706
  if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
5561
5707
  return parsed;
@@ -5570,14 +5716,14 @@ async function listResources(projectPath) {
5570
5716
  if (!await fileExists(resourcesDir)) {
5571
5717
  return [];
5572
5718
  }
5573
- const entries = await readdir8(resourcesDir, { withFileTypes: true });
5719
+ const entries = await readdir7(resourcesDir, { withFileTypes: true });
5574
5720
  const results = [];
5575
5721
  for (const entry of entries) {
5576
5722
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
5577
5723
  continue;
5578
5724
  }
5579
5725
  const filePath = resolve13(resourcesDir, entry.name);
5580
- const content = await readFile10(filePath, "utf-8");
5726
+ const content = await readFile9(filePath, "utf-8");
5581
5727
  const parsed = parseResource(content);
5582
5728
  results.push({
5583
5729
  name: parsed.name,
@@ -5596,14 +5742,14 @@ async function listMemories(projectPath) {
5596
5742
  if (!await fileExists(memoriesDir)) {
5597
5743
  return [];
5598
5744
  }
5599
- const entries = await readdir8(memoriesDir, { withFileTypes: true });
5745
+ const entries = await readdir7(memoriesDir, { withFileTypes: true });
5600
5746
  const results = [];
5601
5747
  for (const entry of entries) {
5602
5748
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
5603
5749
  continue;
5604
5750
  }
5605
5751
  const filePath = resolve13(memoriesDir, entry.name);
5606
- const content = await readFile10(filePath, "utf-8");
5752
+ const content = await readFile9(filePath, "utf-8");
5607
5753
  const parsed = parseMemory(content);
5608
5754
  results.push({
5609
5755
  name: parsed.name,
@@ -5666,7 +5812,7 @@ async function getMemoryDetail(projectsDir, projectSlug, itemSlug) {
5666
5812
  if (!projectRecord) return null;
5667
5813
  const filePath = resolve13(projectRecord.projectPath, "memories", `${itemSlug}.md`);
5668
5814
  if (!await fileExists(filePath)) return null;
5669
- const content = await readFile10(filePath, "utf-8");
5815
+ const content = await readFile9(filePath, "utf-8");
5670
5816
  const parsed = parseMemory(content);
5671
5817
  return {
5672
5818
  name: parsed.name,
@@ -5692,7 +5838,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
5692
5838
  if (!projectRecord) return null;
5693
5839
  const filePath = resolve13(projectRecord.projectPath, "resources", `${itemSlug}.md`);
5694
5840
  if (!await fileExists(filePath)) return null;
5695
- const content = await readFile10(filePath, "utf-8");
5841
+ const content = await readFile9(filePath, "utf-8");
5696
5842
  const parsed = parseResource(content);
5697
5843
  return {
5698
5844
  name: parsed.name,
@@ -5710,7 +5856,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
5710
5856
  async function loadDependencyGraph(projectPath, assignments) {
5711
5857
  const statusPath = resolve13(projectPath, "_status.md");
5712
5858
  if (await fileExists(statusPath)) {
5713
- const statusContent = await readFile10(statusPath, "utf-8");
5859
+ const statusContent = await readFile9(statusPath, "utf-8");
5714
5860
  const parsed = parseStatus(statusContent);
5715
5861
  const derivedGraph = extractMermaidGraph(parsed.body);
5716
5862
  if (derivedGraph) {
@@ -5871,7 +6017,7 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
5871
6017
  unmet.push(`${dependency} (missing)`);
5872
6018
  continue;
5873
6019
  }
5874
- const content = await readFile10(dependencyPath, "utf-8");
6020
+ const content = await readFile9(dependencyPath, "utf-8");
5875
6021
  const parsed = parseAssignmentFull(content);
5876
6022
  if (!terminals.has(parsed.status)) {
5877
6023
  unmet.push(`${dependency} (${parsed.status})`);
@@ -6164,7 +6310,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
6164
6310
  return 0;
6165
6311
  }
6166
6312
  try {
6167
- const content = await readFile10(commentsPath, "utf-8");
6313
+ const content = await readFile9(commentsPath, "utf-8");
6168
6314
  const parsed = parseComments(content);
6169
6315
  return parsed.entries.filter(
6170
6316
  (e) => e.type === "question" && e.resolved !== true
@@ -6232,12 +6378,12 @@ async function listPlaybooks(playbooksDir2) {
6232
6378
  if (!await fileExists(playbooksDir2)) return [];
6233
6379
  const config = await readConfig();
6234
6380
  const disabledSet = new Set(config.playbooks.disabled);
6235
- const entries = await readdir8(playbooksDir2, { withFileTypes: true });
6381
+ const entries = await readdir7(playbooksDir2, { withFileTypes: true });
6236
6382
  const playbooks = [];
6237
6383
  for (const entry of entries) {
6238
6384
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
6239
6385
  const filePath = resolve13(playbooksDir2, entry.name);
6240
- const raw = await readFile10(filePath, "utf-8");
6386
+ const raw = await readFile9(filePath, "utf-8");
6241
6387
  const parsed = parsePlaybook(raw);
6242
6388
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
6243
6389
  playbooks.push({
@@ -6271,7 +6417,7 @@ async function getPlaybookDetail(playbooksDir2, slug) {
6271
6417
  enabled
6272
6418
  };
6273
6419
  }
6274
- 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;
6275
6421
  var init_api = __esm({
6276
6422
  "src/dashboard/api.ts"() {
6277
6423
  "use strict";
@@ -6322,6 +6468,8 @@ var init_api = __esm({
6322
6468
  ["blocked", "blocked"],
6323
6469
  ["stale", "stale"]
6324
6470
  ];
6471
+ projectRecordsCache = /* @__PURE__ */ new Map();
6472
+ standaloneRecordsCache = /* @__PURE__ */ new Map();
6325
6473
  DEFAULT_TRANSITION_DEFINITIONS = [
6326
6474
  {
6327
6475
  command: "start",
@@ -6870,7 +7018,7 @@ function mergePatch(current, patch) {
6870
7018
  // src/utils/view-prefs.ts
6871
7019
  init_paths();
6872
7020
  init_fs();
6873
- 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";
6874
7022
  import { resolve as resolve14 } from "path";
6875
7023
  function corruptFilePath() {
6876
7024
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -6893,7 +7041,7 @@ async function readViewPrefsFile() {
6893
7041
  }
6894
7042
  let raw;
6895
7043
  try {
6896
- raw = await readFile11(path, "utf-8");
7044
+ raw = await readFile10(path, "utf-8");
6897
7045
  } catch {
6898
7046
  return { ...DEFAULT_VIEW_PREFS_FILE };
6899
7047
  }
@@ -7076,7 +7224,7 @@ function isDashboardSlot(value) {
7076
7224
  // src/utils/saved-views.ts
7077
7225
  init_paths();
7078
7226
  init_fs();
7079
- 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";
7080
7228
  import { randomUUID } from "crypto";
7081
7229
  import { resolve as resolve15 } from "path";
7082
7230
  function corruptFilePath2() {
@@ -7106,7 +7254,7 @@ async function readSavedViewsFile() {
7106
7254
  }
7107
7255
  let raw;
7108
7256
  try {
7109
- raw = await readFile12(path, "utf-8");
7257
+ raw = await readFile11(path, "utf-8");
7110
7258
  } catch {
7111
7259
  return cloneDefault();
7112
7260
  }
@@ -7422,7 +7570,7 @@ init_lifecycle();
7422
7570
  init_slug();
7423
7571
  import { Router as Router2 } from "express";
7424
7572
  import { resolve as resolve18, basename as basename3, isAbsolute as isAbsolute2 } from "path";
7425
- 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";
7426
7574
  import { spawnSync as spawnSync3 } from "child_process";
7427
7575
 
7428
7576
  // src/utils/uuid.ts
@@ -7439,7 +7587,7 @@ init_fs();
7439
7587
  init_frontmatter();
7440
7588
  init_fs();
7441
7589
  import { spawn } from "child_process";
7442
- import { readFile as readFile13 } from "fs/promises";
7590
+ import { readFile as readFile12 } from "fs/promises";
7443
7591
  function run(command, args, cwd) {
7444
7592
  return new Promise((resolvePromise) => {
7445
7593
  const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -7489,7 +7637,7 @@ async function createWorktreeAndRecord(opts) {
7489
7637
  const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
7490
7638
  await createWorktree({ repository, branch, worktreePath, parentBranch });
7491
7639
  try {
7492
- const content = await readFile13(assignmentPath, "utf-8");
7640
+ const content = await readFile12(assignmentPath, "utf-8");
7493
7641
  const updated = updateAssignmentWorkspace(content, {
7494
7642
  repository,
7495
7643
  worktreePath,
@@ -7573,14 +7721,14 @@ function detectCurrentBranch(cwd) {
7573
7721
  // src/dashboard/repository-candidates.ts
7574
7722
  init_fs();
7575
7723
  init_parser();
7576
- import { readdir as readdir9, readFile as readFile14 } from "fs/promises";
7724
+ import { readdir as readdir8, readFile as readFile13 } from "fs/promises";
7577
7725
  import { resolve as resolve17 } from "path";
7578
7726
  async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
7579
7727
  const seen = /* @__PURE__ */ new Set();
7580
7728
  const out = [];
7581
7729
  const projectPath = resolve17(projectsDir, projectSlug, "project.md");
7582
7730
  if (await fileExists(projectPath)) {
7583
- const project = parseProject(await readFile14(projectPath, "utf-8"));
7731
+ const project = parseProject(await readFile13(projectPath, "utf-8"));
7584
7732
  for (const raw of project.repositories) {
7585
7733
  const path = raw.trim();
7586
7734
  if (!path) continue;
@@ -7592,12 +7740,12 @@ async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
7592
7740
  }
7593
7741
  const assignmentsDir2 = resolve17(projectsDir, projectSlug, "assignments");
7594
7742
  if (await fileExists(assignmentsDir2)) {
7595
- const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
7743
+ const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
7596
7744
  for (const entry of entries) {
7597
7745
  if (!entry.isDirectory()) continue;
7598
7746
  const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
7599
7747
  if (!await fileExists(assignmentMd)) continue;
7600
- const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
7748
+ const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
7601
7749
  const repo = parsed.workspace.repository?.trim();
7602
7750
  if (!repo) continue;
7603
7751
  const abs = resolve17(repo);
@@ -7614,13 +7762,13 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
7614
7762
  }
7615
7763
  const seen = /* @__PURE__ */ new Set();
7616
7764
  const out = [];
7617
- const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
7765
+ const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
7618
7766
  for (const entry of entries) {
7619
7767
  if (!entry.isDirectory()) continue;
7620
7768
  if (entry.name === excludeAssignmentId) continue;
7621
7769
  const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
7622
7770
  if (!await fileExists(assignmentMd)) continue;
7623
- const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
7771
+ const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
7624
7772
  const repo = parsed.workspace.repository?.trim();
7625
7773
  if (!repo) continue;
7626
7774
  const abs = resolve17(repo);
@@ -8186,14 +8334,14 @@ async function readCurrentDocument(filePath) {
8186
8334
  if (!await fileExists(filePath)) {
8187
8335
  return null;
8188
8336
  }
8189
- return readFile15(filePath, "utf-8");
8337
+ return readFile14(filePath, "utf-8");
8190
8338
  }
8191
8339
  async function handleWorktreeCreate(req, res, ctx) {
8192
8340
  if (!await fileExists(ctx.assignmentPath)) {
8193
8341
  res.status(404).json({ error: "Assignment not found" });
8194
8342
  return;
8195
8343
  }
8196
- const parsed = parseAssignmentFull(await readFile15(ctx.assignmentPath, "utf-8"));
8344
+ const parsed = parseAssignmentFull(await readFile14(ctx.assignmentPath, "utf-8"));
8197
8345
  if (parsed.workspace.worktreePath) {
8198
8346
  res.status(409).json({ error: "Worktree already configured for this assignment" });
8199
8347
  return;
@@ -8285,6 +8433,7 @@ async function handleWorktreeCreate(req, res, ctx) {
8285
8433
  function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
8286
8434
  const linkedTodosLookup = todosDir2 ? { todosDir: todosDir2, projectsDir } : void 0;
8287
8435
  const router = Router2();
8436
+ installRecordsInvalidation(router);
8288
8437
  router.get("/api/templates/project", (_req, res) => {
8289
8438
  const content = renderProject({
8290
8439
  id: generateId(),
@@ -8562,7 +8711,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
8562
8711
  }
8563
8712
  const nextContentRaw = requireContent(req, res);
8564
8713
  if (!nextContentRaw) return;
8565
- const currentContent = await readFile15(filePath, "utf-8");
8714
+ const currentContent = await readFile14(filePath, "utf-8");
8566
8715
  const frontmatterBlock = extractFrontmatterBlock(currentContent);
8567
8716
  if (!frontmatterBlock) {
8568
8717
  res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
@@ -9013,7 +9162,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9013
9162
  let currentContent;
9014
9163
  let currentCount = 0;
9015
9164
  if (await fileExists(commentsPath)) {
9016
- currentContent = await readFile15(commentsPath, "utf-8");
9165
+ currentContent = await readFile14(commentsPath, "utf-8");
9017
9166
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
9018
9167
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
9019
9168
  } else {
@@ -9070,7 +9219,7 @@ ${entry}`;
9070
9219
  res.status(400).json({ error: "resolved (boolean) is required" });
9071
9220
  return;
9072
9221
  }
9073
- const content = await readFile15(commentsPath, "utf-8");
9222
+ const content = await readFile14(commentsPath, "utf-8");
9074
9223
  const parsed = parseComments(content);
9075
9224
  const target = parsed.entries.find((e) => e.id === commentId);
9076
9225
  if (!target) {
@@ -9117,7 +9266,7 @@ ${entry}`;
9117
9266
  });
9118
9267
  return;
9119
9268
  }
9120
- let content = await readFile15(projectPath, "utf-8");
9269
+ let content = await readFile14(projectPath, "utf-8");
9121
9270
  content = setTopLevelField(content, "workspace", workspace ?? null);
9122
9271
  content = setTopLevelField(content, "updated", nowTimestamp());
9123
9272
  await writeFileForce(projectPath, content);
@@ -9154,7 +9303,7 @@ ${entry}`;
9154
9303
  return;
9155
9304
  }
9156
9305
  const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9157
- let content = await readFile15(assignmentPath, "utf-8");
9306
+ let content = await readFile14(assignmentPath, "utf-8");
9158
9307
  content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
9159
9308
  content = setTopLevelField(content, "updated", nowTimestamp());
9160
9309
  await writeFileForce(assignmentPath, content);
@@ -9249,7 +9398,7 @@ ${entry}`;
9249
9398
  return;
9250
9399
  }
9251
9400
  const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9252
- const parsedForSlug = parseAssignmentFull(await readFile15(assignmentPath, "utf-8"));
9401
+ const parsedForSlug = parseAssignmentFull(await readFile14(assignmentPath, "utf-8"));
9253
9402
  const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
9254
9403
  await handleWorktreeCreate(req, res, {
9255
9404
  assignmentPath,
@@ -9278,7 +9427,7 @@ ${entry}`;
9278
9427
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
9279
9428
  return;
9280
9429
  }
9281
- let content = await readFile15(projectPath, "utf-8");
9430
+ let content = await readFile14(projectPath, "utf-8");
9282
9431
  content = setTopLevelField(content, "statusOverride", status ?? null);
9283
9432
  content = setTopLevelField(content, "updated", nowTimestamp());
9284
9433
  await writeFileForce(projectPath, content);
@@ -9311,7 +9460,7 @@ ${entry}`;
9311
9460
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
9312
9461
  return;
9313
9462
  }
9314
- let content = await readFile15(assignmentPath, "utf-8");
9463
+ let content = await readFile14(assignmentPath, "utf-8");
9315
9464
  content = setTopLevelField(content, "status", status);
9316
9465
  content = setTopLevelField(content, "updated", nowTimestamp());
9317
9466
  if (status !== "blocked") {
@@ -9345,7 +9494,7 @@ ${entry}`;
9345
9494
  res.status(400).json({ error: validation.error });
9346
9495
  return;
9347
9496
  }
9348
- let content = await readFile15(assignmentPath, "utf-8");
9497
+ let content = await readFile14(assignmentPath, "utf-8");
9349
9498
  content = setTopLevelField(content, "assignee", validation.value);
9350
9499
  content = setTopLevelField(content, "updated", nowTimestamp());
9351
9500
  await writeFileForce(assignmentPath, content);
@@ -9376,7 +9525,7 @@ ${entry}`;
9376
9525
  res.status(400).json({ error: validation.error });
9377
9526
  return;
9378
9527
  }
9379
- let content = await readFile15(assignmentPath, "utf-8");
9528
+ let content = await readFile14(assignmentPath, "utf-8");
9380
9529
  content = setTopLevelField(content, "title", validation.value);
9381
9530
  content = setTopLevelField(content, "updated", nowTimestamp());
9382
9531
  await writeFileForce(assignmentPath, content);
@@ -9894,7 +10043,7 @@ ${entry}`;
9894
10043
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
9895
10044
  return;
9896
10045
  }
9897
- let content = await readFile15(assignmentPath, "utf-8");
10046
+ let content = await readFile14(assignmentPath, "utf-8");
9898
10047
  content = setTopLevelField(content, "status", status);
9899
10048
  content = setTopLevelField(content, "updated", nowTimestamp());
9900
10049
  if (status !== "blocked") {
@@ -9930,7 +10079,7 @@ ${entry}`;
9930
10079
  res.status(400).json({ error: validation.error });
9931
10080
  return;
9932
10081
  }
9933
- let content = await readFile15(assignmentPath, "utf-8");
10082
+ let content = await readFile14(assignmentPath, "utf-8");
9934
10083
  content = setTopLevelField(content, "assignee", validation.value);
9935
10084
  content = setTopLevelField(content, "updated", nowTimestamp());
9936
10085
  await writeFileForce(assignmentPath, content);
@@ -9963,7 +10112,7 @@ ${entry}`;
9963
10112
  res.status(400).json({ error: validation.error });
9964
10113
  return;
9965
10114
  }
9966
- let content = await readFile15(assignmentPath, "utf-8");
10115
+ let content = await readFile14(assignmentPath, "utf-8");
9967
10116
  content = setTopLevelField(content, "title", validation.value);
9968
10117
  content = setTopLevelField(content, "updated", nowTimestamp());
9969
10118
  await writeFileForce(assignmentPath, content);
@@ -10032,7 +10181,7 @@ ${entry}`;
10032
10181
  if (!await fileExists(assignmentPath)) {
10033
10182
  throw new Error("assignment file not found");
10034
10183
  }
10035
- let content = await readFile15(assignmentPath, "utf-8");
10184
+ let content = await readFile14(assignmentPath, "utf-8");
10036
10185
  content = setTopLevelField(content, "status", status);
10037
10186
  content = setTopLevelField(content, "updated", timestamp);
10038
10187
  if (status !== "blocked") {
@@ -10188,7 +10337,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
10188
10337
  let currentContent;
10189
10338
  let currentCount = 0;
10190
10339
  if (await fileExists(commentsPath)) {
10191
- currentContent = await readFile15(commentsPath, "utf-8");
10340
+ currentContent = await readFile14(commentsPath, "utf-8");
10192
10341
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
10193
10342
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
10194
10343
  } else {
@@ -10228,7 +10377,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
10228
10377
  res.status(400).json({ error: "resolved (boolean) is required" });
10229
10378
  return;
10230
10379
  }
10231
- const content = await readFile15(commentsPath, "utf-8");
10380
+ const content = await readFile14(commentsPath, "utf-8");
10232
10381
  const parsed = parseComments(content);
10233
10382
  const target = parsed.entries.find((e) => e.id === commentId);
10234
10383
  if (!target) {
@@ -10956,26 +11105,26 @@ import { Router as Router8 } from "express";
10956
11105
 
10957
11106
  // src/utils/status-config-resolution.ts
10958
11107
  init_frontmatter();
10959
- 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";
10960
11109
  import { dirname as dirname4 } from "path";
10961
11110
 
10962
11111
  // src/utils/assignment-walk.ts
10963
11112
  init_fs();
10964
11113
  import { resolve as resolve20 } from "path";
10965
- import { readdir as readdir10 } from "fs/promises";
11114
+ import { readdir as readdir9 } from "fs/promises";
10966
11115
  async function listAssignmentsByProject(projectsDir, standaloneDir) {
10967
11116
  const result = {
10968
11117
  withAssignmentMd: [],
10969
11118
  orphanFolders: []
10970
11119
  };
10971
11120
  if (await fileExists(projectsDir)) {
10972
- const projects = await readdir10(projectsDir, { withFileTypes: true });
11121
+ const projects = await readdir9(projectsDir, { withFileTypes: true });
10973
11122
  for (const m of projects) {
10974
11123
  if (!m.isDirectory()) continue;
10975
11124
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
10976
11125
  const assignmentsDir2 = resolve20(projectsDir, m.name, "assignments");
10977
11126
  if (!await fileExists(assignmentsDir2)) continue;
10978
- const entries = await readdir10(assignmentsDir2, { withFileTypes: true });
11127
+ const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
10979
11128
  for (const a of entries) {
10980
11129
  if (!a.isDirectory()) continue;
10981
11130
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
@@ -10997,7 +11146,7 @@ async function listAssignmentsByProject(projectsDir, standaloneDir) {
10997
11146
  }
10998
11147
  }
10999
11148
  if (standaloneDir !== null && await fileExists(standaloneDir)) {
11000
- const entries = await readdir10(standaloneDir, { withFileTypes: true });
11149
+ const entries = await readdir9(standaloneDir, { withFileTypes: true });
11001
11150
  for (const a of entries) {
11002
11151
  if (!a.isDirectory()) continue;
11003
11152
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
@@ -11039,7 +11188,7 @@ async function scanAssignmentsByStatus(projectsDir, standaloneDir, ids) {
11039
11188
  const assignmentPath = `${entry.assignmentDir}/assignment.md`;
11040
11189
  let content;
11041
11190
  try {
11042
- content = await readFile16(assignmentPath, "utf-8");
11191
+ content = await readFile15(assignmentPath, "utf-8");
11043
11192
  } catch (err) {
11044
11193
  const code = err?.code;
11045
11194
  if (code === "ENOENT") {
@@ -11102,7 +11251,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
11102
11251
  const list = affected.get(r.id) ?? [];
11103
11252
  for (const a of list) {
11104
11253
  try {
11105
- const content = await readFile16(a.path, "utf-8");
11254
+ const content = await readFile15(a.path, "utf-8");
11106
11255
  buffer.set(a.path, content);
11107
11256
  } catch (err) {
11108
11257
  const code = err?.code;
@@ -11130,7 +11279,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
11130
11279
  for (const a of list) {
11131
11280
  let current;
11132
11281
  try {
11133
- current = await readFile16(a.path, "utf-8");
11282
+ current = await readFile15(a.path, "utf-8");
11134
11283
  } catch (err) {
11135
11284
  const code = err?.code;
11136
11285
  if (code === "ENOENT") {
@@ -11179,7 +11328,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
11179
11328
  const list = affected.get(r.id) ?? [];
11180
11329
  for (const a of list) {
11181
11330
  try {
11182
- const current = await readFile16(a.path, "utf-8");
11331
+ const current = await readFile15(a.path, "utf-8");
11183
11332
  const fm = parseAssignmentFrontmatter(current);
11184
11333
  if (fm.status !== r.id) {
11185
11334
  console.warn(
@@ -11315,6 +11464,7 @@ function mapResolutionErrorToHttp(err, applied) {
11315
11464
  }
11316
11465
  function createStatusConfigRouter(projectsDir, assignmentsDir2) {
11317
11466
  const router = Router8();
11467
+ installRecordsInvalidation(router);
11318
11468
  router.get("/", async (_req, res) => {
11319
11469
  try {
11320
11470
  const config = await getStatusConfig();
@@ -12025,7 +12175,7 @@ init_timestamp();
12025
12175
  init_fs();
12026
12176
  import { Router as Router11 } from "express";
12027
12177
  import { resolve as resolve23 } from "path";
12028
- import { readFile as readFile17 } from "fs/promises";
12178
+ import { readFile as readFile16 } from "fs/promises";
12029
12179
  init_playbooks();
12030
12180
  function statusForPlaybookError(code) {
12031
12181
  switch (code) {
@@ -12108,7 +12258,7 @@ function createPlaybooksRouter(playbooksDir2) {
12108
12258
  return;
12109
12259
  }
12110
12260
  const filePath = resolve23(playbooksDir2, resolved.filename);
12111
- const content = await readFile17(filePath, "utf-8");
12261
+ const content = await readFile16(filePath, "utf-8");
12112
12262
  res.json({
12113
12263
  documentType: "playbook",
12114
12264
  title: `Edit Playbook: ${resolved.slug}`,
@@ -12205,7 +12355,7 @@ init_parser2();
12205
12355
  init_fs();
12206
12356
  init_paths();
12207
12357
  import { Router as Router12 } from "express";
12208
- import { readdir as readdir11 } from "fs/promises";
12358
+ import { readdir as readdir10 } from "fs/promises";
12209
12359
  import { resolve as resolvePath, dirname as dirname5 } from "path";
12210
12360
  import { rename as rename5, mkdir as mkdir2 } from "fs/promises";
12211
12361
  init_slug();
@@ -12545,6 +12695,7 @@ async function promoteTodosToNewAssignment(groups, options) {
12545
12695
  }
12546
12696
 
12547
12697
  // src/dashboard/api-todos.ts
12698
+ init_api();
12548
12699
  var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
12549
12700
  function getWorkspaceParam(value) {
12550
12701
  if (Array.isArray(value)) {
@@ -12559,6 +12710,7 @@ function touchItem3(item) {
12559
12710
  }
12560
12711
  function createTodosRouter(todosDir2, broadcast, projectsDir) {
12561
12712
  const router = Router12();
12713
+ installRecordsInvalidation(router);
12562
12714
  function broadcastUpdate() {
12563
12715
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
12564
12716
  }
@@ -12678,7 +12830,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
12678
12830
  router.get("/", async (_req, res) => {
12679
12831
  try {
12680
12832
  await ensureDir(todosDir2);
12681
- const files = await readdir11(todosDir2).catch(() => []);
12833
+ const files = await readdir10(todosDir2).catch(() => []);
12682
12834
  const workspaces = [];
12683
12835
  for (const file of files) {
12684
12836
  if (typeof file !== "string") continue;
@@ -12793,7 +12945,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
12793
12945
  try {
12794
12946
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
12795
12947
  const { resolve: resolve29 } = await import("path");
12796
- const { readFile: readFile21 } = await import("fs/promises");
12948
+ const { readFile: readFile20 } = await import("fs/promises");
12797
12949
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
12798
12950
  const workspace = getWorkspaceParam(req.params.workspace);
12799
12951
  const checklist = await readChecklist(todosDir2, workspace);
@@ -12812,7 +12964,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
12812
12964
  await ensureDir(resolve29(todosDir2, "archive"));
12813
12965
  let archContent = "";
12814
12966
  if (await fileExists(archFile)) {
12815
- archContent = await readFile21(archFile, "utf-8");
12967
+ archContent = await readFile20(archFile, "utf-8");
12816
12968
  archContent = archContent.trimEnd() + "\n\n";
12817
12969
  } else {
12818
12970
  archContent = `---
@@ -13094,7 +13246,7 @@ workspace: ${workspace}
13094
13246
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
13095
13247
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
13096
13248
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
13097
- const { readFile: readFile21 } = await import("fs/promises");
13249
+ const { readFile: readFile20 } = await import("fs/promises");
13098
13250
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
13099
13251
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
13100
13252
  let assignmentRef;
@@ -13115,7 +13267,7 @@ workspace: ${workspace}
13115
13267
  }
13116
13268
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
13117
13269
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
13118
- let content = await readFile21(assignmentMdPath, "utf-8");
13270
+ let content = await readFile20(assignmentMdPath, "utf-8");
13119
13271
  content = appendTodosToAssignmentBody2(
13120
13272
  content,
13121
13273
  items.map((it) => ({
@@ -13303,8 +13455,9 @@ init_fs();
13303
13455
  init_paths();
13304
13456
  init_slug();
13305
13457
  import { Router as Router13 } from "express";
13306
- 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";
13307
13459
  import { resolve as resolve25, dirname as dirname6 } from "path";
13460
+ init_api();
13308
13461
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
13309
13462
  function touchItem4(item) {
13310
13463
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -13353,6 +13506,7 @@ function notFound(res, slug) {
13353
13506
  }
13354
13507
  function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
13355
13508
  const router = Router13({ mergeParams: true });
13509
+ installRecordsInvalidation(router);
13356
13510
  function broadcastUpdate(projectSlug) {
13357
13511
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
13358
13512
  }
@@ -13525,7 +13679,7 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
13525
13679
  const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
13526
13680
  let archContent = "";
13527
13681
  if (await fileExists(archFile)) {
13528
- archContent = await readFile18(archFile, "utf-8");
13682
+ archContent = await readFile17(archFile, "utf-8");
13529
13683
  archContent = archContent.trimEnd() + "\n\n";
13530
13684
  } else {
13531
13685
  archContent = `---
@@ -13982,7 +14136,7 @@ workspace: ${slug}
13982
14136
  }
13983
14137
  const assignmentMdPath = resolve25(assignmentDir, "assignment.md");
13984
14138
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
13985
- let content = await readFile18(assignmentMdPath, "utf-8");
14139
+ let content = await readFile17(assignmentMdPath, "utf-8");
13986
14140
  content = appendTodosToAssignmentBody2(
13987
14141
  content,
13988
14142
  items.map((it) => ({
@@ -14178,7 +14332,7 @@ workspace: ${slug}
14178
14332
 
14179
14333
  // src/dashboard/api-bundles.ts
14180
14334
  import { Router as Router14 } from "express";
14181
- import { readdir as readdir12 } from "fs/promises";
14335
+ import { readdir as readdir11 } from "fs/promises";
14182
14336
 
14183
14337
  // src/todos/bundle-parser.ts
14184
14338
  init_parser();
@@ -14186,7 +14340,7 @@ init_fs();
14186
14340
  init_paths();
14187
14341
  init_parser2();
14188
14342
  import { randomBytes as randomBytes2 } from "crypto";
14189
- import { readFile as readFile19 } from "fs/promises";
14343
+ import { readFile as readFile18 } from "fs/promises";
14190
14344
  var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
14191
14345
  var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
14192
14346
  var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
@@ -14253,7 +14407,7 @@ function parseBundles(content) {
14253
14407
  async function readBundles(todosDir2) {
14254
14408
  const path = bundlesPath(todosDir2);
14255
14409
  if (!await fileExists(path)) return [];
14256
- const content = await readFile19(path, "utf-8");
14410
+ const content = await readFile18(path, "utf-8");
14257
14411
  return parseBundles(content).bundles;
14258
14412
  }
14259
14413
 
@@ -14302,7 +14456,7 @@ function createBundlesRouter(todosDir2, broadcast) {
14302
14456
  try {
14303
14457
  await ensureDir(todosDir2);
14304
14458
  const bundles = await readBundles(todosDir2);
14305
- const workspaceFiles = await readdir12(todosDir2).catch(() => []);
14459
+ const workspaceFiles = await readdir11(todosDir2).catch(() => []);
14306
14460
  const itemsByKey = /* @__PURE__ */ new Map();
14307
14461
  for (const f of workspaceFiles) {
14308
14462
  if (typeof f !== "string") continue;
@@ -14416,6 +14570,8 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
14416
14570
 
14417
14571
  // src/dashboard/api-backup.ts
14418
14572
  init_config2();
14573
+ init_api();
14574
+ init_scanner();
14419
14575
  import { Router as Router16 } from "express";
14420
14576
 
14421
14577
  // src/utils/github-backup.ts
@@ -14424,7 +14580,7 @@ init_fs();
14424
14580
  init_config2();
14425
14581
  import { execFile as execFile2 } from "child_process";
14426
14582
  import { promisify as promisify2 } from "util";
14427
- 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";
14428
14584
  import { resolve as resolve27, join as join2 } from "path";
14429
14585
  import { tmpdir } from "os";
14430
14586
  var exec2 = promisify2(execFile2);
@@ -14485,7 +14641,7 @@ async function acquireLock() {
14485
14641
  return lockPath;
14486
14642
  } catch (err) {
14487
14643
  if (err.code === "EEXIST") {
14488
- const pid = await readFile20(lockPath, "utf-8").catch(() => "");
14644
+ const pid = await readFile19(lockPath, "utf-8").catch(() => "");
14489
14645
  throw new Error(
14490
14646
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
14491
14647
  );
@@ -14532,7 +14688,7 @@ function resolveCategoriesStrict(csv) {
14532
14688
  return parseCategoriesStrict(parts);
14533
14689
  }
14534
14690
  async function readSanitizedConfig(configPath) {
14535
- const content = await readFile20(configPath, "utf-8");
14691
+ const content = await readFile19(configPath, "utf-8");
14536
14692
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
14537
14693
  }
14538
14694
  async function backupToGithub(overrides) {
@@ -14814,6 +14970,9 @@ function createBackupRouter() {
14814
14970
  success: false,
14815
14971
  error: error instanceof Error ? error.message : String(error)
14816
14972
  });
14973
+ } finally {
14974
+ invalidateRecordsCache();
14975
+ clearScanCache();
14817
14976
  }
14818
14977
  });
14819
14978
  return router;
@@ -15052,6 +15211,9 @@ function createDashboardServer(options) {
15052
15211
  });
15053
15212
  });
15054
15213
  function broadcast(message) {
15214
+ if (message.type === "project-updated" || message.type === "assignment-updated") {
15215
+ invalidateRecordsCache();
15216
+ }
15055
15217
  const data = JSON.stringify(message);
15056
15218
  for (const client of clients) {
15057
15219
  if (client.readyState === WebSocket.OPEN) {