syntaur 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.claude-plugin/plugin.json +5 -1
  2. package/README.md +23 -0
  3. package/dashboard/dist/assets/{_basePickBy-XOV2F7dX.js → _basePickBy-qEDLAY_J.js} +1 -1
  4. package/dashboard/dist/assets/{_baseUniq-Dccfk3fK.js → _baseUniq-7tDs18Gn.js} +1 -1
  5. package/dashboard/dist/assets/{arc-TwJ5RF-H.js → arc-Brdw0dYW.js} +1 -1
  6. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CpkVJudw.js → architectureDiagram-2XIMDMQ5-BiEdLT9m.js} +1 -1
  7. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-Ck6VQ4Fh.js → blockDiagram-WCTKOSBZ-HC-j_hPZ.js} +1 -1
  8. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-B5V7YG03.js → c4Diagram-IC4MRINW-DA_rWbl7.js} +1 -1
  9. package/dashboard/dist/assets/channel-D2-caOn6.js +1 -0
  10. package/dashboard/dist/assets/{chunk-4BX2VUAB-rBdyOxAe.js → chunk-4BX2VUAB-CF3hguyj.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-55IACEB6-DKlNXjYt.js → chunk-55IACEB6-BTeCeIc_.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-FMBD7UC4-Cqm0PtUI.js → chunk-FMBD7UC4-8qVvxmks.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-JSJVCQXG-CQyXQcG3.js → chunk-JSJVCQXG-DCf8n7EO.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-KX2RTZJC-DmqjHPpJ.js → chunk-KX2RTZJC-CcpjTmME.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-NQ4KR5QH-CHXoBcYa.js → chunk-NQ4KR5QH-BnFPKo4-.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-QZHKN3VN-Dd4sChBS.js → chunk-QZHKN3VN-IPMVLLv2.js} +1 -1
  17. package/dashboard/dist/assets/{chunk-WL4C6EOR-D-QLnkY8.js → chunk-WL4C6EOR-D1pQmHqX.js} +1 -1
  18. package/dashboard/dist/assets/classDiagram-VBA2DB6C-HFOb_XEt.js +1 -0
  19. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-HFOb_XEt.js +1 -0
  20. package/dashboard/dist/assets/clone-BrYOwgIq.js +1 -0
  21. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-VZi6uV56.js → cose-bilkent-S5V4N54A-tvJjvBQr.js} +1 -1
  22. package/dashboard/dist/assets/{dagre-KLK3FWXG-CtT97Qhx.js → dagre-KLK3FWXG-qp1vt6LY.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-E7M64L7V-BjYTNNHb.js → diagram-E7M64L7V-VF5eizNr.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-IFDJBPK2-qj8_r6fb.js → diagram-IFDJBPK2-C3pCbJ1e.js} +1 -1
  25. package/dashboard/dist/assets/{diagram-P4PSJMXO-DlfWo7Vt.js → diagram-P4PSJMXO-D-taXZwH.js} +1 -1
  26. package/dashboard/dist/assets/{erDiagram-INFDFZHY-Ch_nApTv.js → erDiagram-INFDFZHY-QjY-NHVs.js} +1 -1
  27. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-EOVsqNyi.js → flowDiagram-PKNHOUZH-FjO8P5Tq.js} +1 -1
  28. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CS-lx0B-.js → ganttDiagram-A5KZAMGK-Bsn7CyK3.js} +1 -1
  29. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-Ba6RDzPy.js → gitGraphDiagram-K3NZZRJ6-BZ8DjhnF.js} +1 -1
  30. package/dashboard/dist/assets/{graph-BKaAyLk-.js → graph-AdliIYqX.js} +1 -1
  31. package/dashboard/dist/assets/{index-BTWPZaIA.css → index-BoR_oyUZ.css} +1 -1
  32. package/dashboard/dist/assets/index-RCBI4aX2.js +525 -0
  33. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Cr20xEsd.js → infoDiagram-LFFYTUFH-BfZjLA86.js} +1 -1
  34. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CuILV9ph.js → ishikawaDiagram-PHBUUO56-BSgSGMp5.js} +1 -1
  35. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CO-l7Mpm.js → journeyDiagram-4ABVD52K-BlQ7qeMC.js} +1 -1
  36. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-CcirPw_S.js → kanban-definition-K7BYSVSG-CifO0AUu.js} +1 -1
  37. package/dashboard/dist/assets/{layout-BXvr8dMw.js → layout-B9r-gpD9.js} +1 -1
  38. package/dashboard/dist/assets/{linear-B85hgYnX.js → linear-CaaIRxVZ.js} +1 -1
  39. package/dashboard/dist/assets/{mermaid.core-DcZN9Xp2.js → mermaid.core-C8wCWYw-.js} +4 -4
  40. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-Bxi6p5_3.js → mindmap-definition-YRQLILUH-BpX5T7iT.js} +1 -1
  41. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-DOSvU0UD.js → pieDiagram-SKSYHLDU-BNGYtWrc.js} +1 -1
  42. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-Cmo-5tW9.js → quadrantDiagram-337W2JSQ-CR4GeM9D.js} +1 -1
  43. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-Dw8alwuI.js → requirementDiagram-Z7DCOOCP-CXrPkMLl.js} +1 -1
  44. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-ZPB-rzOg.js → sankeyDiagram-WA2Y5GQK-CIzof7Dm.js} +1 -1
  45. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-Ct8bkK2h.js → sequenceDiagram-2WXFIKYE-BJZLt0O2.js} +1 -1
  46. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DnGOXlmx.js → stateDiagram-RAJIS63D-x9gC6zy6.js} +1 -1
  47. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-CN5Dcv7u.js +1 -0
  48. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-AUFmbJjr.js → timeline-definition-YZTLITO2-D_ZSQn1K.js} +1 -1
  49. package/dashboard/dist/assets/{treemap-KZPCXAKY-Dw5FDFAV.js → treemap-KZPCXAKY-LPpMEco4.js} +1 -1
  50. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-DfxicU4N.js → vennDiagram-LZ73GAT5-CRFZQpr7.js} +1 -1
  51. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW--9N0V1r_.js → xychartDiagram-JWTSCODW-Bivutynw.js} +1 -1
  52. package/dashboard/dist/index.html +2 -2
  53. package/dist/dashboard/server.js +419 -84
  54. package/dist/dashboard/server.js.map +1 -1
  55. package/dist/db/leases-db.d.ts +227 -0
  56. package/dist/db/leases-db.js +679 -0
  57. package/dist/db/leases-db.js.map +1 -0
  58. package/dist/index.js +1825 -445
  59. package/dist/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/platforms/claude-code/.claude-plugin/plugin.json +5 -1
  62. package/platforms/claude-code/skills/claim-resource/SKILL.md +89 -0
  63. package/platforms/claude-code/skills/extend-resource/SKILL.md +83 -0
  64. package/platforms/claude-code/skills/list-resources/SKILL.md +74 -0
  65. package/platforms/claude-code/skills/release-resource/SKILL.md +65 -0
  66. package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +1 -0
  67. package/platforms/codex/skills/claim-resource/SKILL.md +89 -0
  68. package/platforms/codex/skills/extend-resource/SKILL.md +83 -0
  69. package/platforms/codex/skills/list-resources/SKILL.md +74 -0
  70. package/platforms/codex/skills/release-resource/SKILL.md +65 -0
  71. package/platforms/codex/skills/syntaur-protocol/SKILL.md +1 -0
  72. package/skills/claim-resource/SKILL.md +89 -0
  73. package/skills/extend-resource/SKILL.md +83 -0
  74. package/skills/list-resources/SKILL.md +74 -0
  75. package/skills/release-resource/SKILL.md +65 -0
  76. package/skills/syntaur-protocol/SKILL.md +1 -0
  77. package/dashboard/dist/assets/channel-AEMTX70R.js +0 -1
  78. package/dashboard/dist/assets/classDiagram-VBA2DB6C-LzuOAGzu.js +0 -1
  79. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-LzuOAGzu.js +0 -1
  80. package/dashboard/dist/assets/clone-Cw63QGUJ.js +0 -1
  81. package/dashboard/dist/assets/index-Dk9PBm5B.js +0 -510
  82. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-COGzlZxn.js +0 -1
@@ -5258,7 +5258,7 @@ __export(parser_exports, {
5258
5258
  });
5259
5259
  import { randomBytes } from "crypto";
5260
5260
  import { readFile as readFile12 } from "fs/promises";
5261
- import { resolve as resolve15 } from "path";
5261
+ import { resolve as resolve16 } from "path";
5262
5262
  function generateShortId() {
5263
5263
  return randomBytes(2).toString("hex");
5264
5264
  }
@@ -5496,10 +5496,10 @@ function serializeLogEntry(entry) {
5496
5496
  return lines.join("\n");
5497
5497
  }
5498
5498
  function checklistPath(todosDir2, workspace) {
5499
- return resolve15(todosDir2, `${workspace}.md`);
5499
+ return resolve16(todosDir2, `${workspace}.md`);
5500
5500
  }
5501
5501
  function logPath(todosDir2, workspace) {
5502
- return resolve15(todosDir2, `${workspace}-log.md`);
5502
+ return resolve16(todosDir2, `${workspace}-log.md`);
5503
5503
  }
5504
5504
  function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
5505
5505
  const year = now.getFullYear();
@@ -5523,7 +5523,7 @@ function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new D
5523
5523
  default:
5524
5524
  suffix = `${year}-${month}-${day}`;
5525
5525
  }
5526
- return resolve15(todosDir2, "archive", `${workspace}-${suffix}.md`);
5526
+ return resolve16(todosDir2, "archive", `${workspace}-${suffix}.md`);
5527
5527
  }
5528
5528
  async function readChecklist(todosDir2, workspace) {
5529
5529
  const path = checklistPath(todosDir2, workspace);
@@ -5638,7 +5638,7 @@ var create_assignment_exports = {};
5638
5638
  __export(create_assignment_exports, {
5639
5639
  createAssignmentCommand: () => createAssignmentCommand
5640
5640
  });
5641
- import { resolve as resolve16 } from "path";
5641
+ import { resolve as resolve17 } from "path";
5642
5642
  async function createAssignmentCommand(title, options) {
5643
5643
  if (!title.trim()) {
5644
5644
  throw new Error("Assignment title cannot be empty.");
@@ -5713,14 +5713,14 @@ async function createAssignmentCommand(title, options) {
5713
5713
  if (options.oneOff) {
5714
5714
  const standaloneRoot = assignmentsDir();
5715
5715
  folderName = id;
5716
- assignmentDir = resolve16(standaloneRoot, folderName);
5716
+ assignmentDir = resolve17(standaloneRoot, folderName);
5717
5717
  projectSlug = null;
5718
5718
  await ensureDir(standaloneRoot);
5719
5719
  } else {
5720
5720
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
5721
5721
  projectSlug = options.project;
5722
- const projectDir = resolve16(baseDir, projectSlug);
5723
- const projectMdPath = resolve16(projectDir, "project.md");
5722
+ const projectDir = resolve17(baseDir, projectSlug);
5723
+ const projectMdPath = resolve17(projectDir, "project.md");
5724
5724
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
5725
5725
  throw new Error(
5726
5726
  `Project "${projectSlug}" not found at ${projectDir}.
@@ -5728,9 +5728,9 @@ Run 'syntaur create-project' first or use --one-off.`
5728
5728
  );
5729
5729
  }
5730
5730
  if (dependsOn.length > 0) {
5731
- const depDirBase = resolve16(projectDir, "assignments");
5731
+ const depDirBase = resolve17(projectDir, "assignments");
5732
5732
  for (const dep of dependsOn) {
5733
- const depDir = resolve16(depDirBase, dep);
5733
+ const depDir = resolve17(depDirBase, dep);
5734
5734
  if (!await fileExists(depDir)) {
5735
5735
  console.warn(
5736
5736
  `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
@@ -5739,7 +5739,7 @@ Run 'syntaur create-project' first or use --one-off.`
5739
5739
  }
5740
5740
  }
5741
5741
  folderName = assignmentSlug;
5742
- assignmentDir = resolve16(projectDir, "assignments", folderName);
5742
+ assignmentDir = resolve17(projectDir, "assignments", folderName);
5743
5743
  }
5744
5744
  if (await fileExists(assignmentDir)) {
5745
5745
  throw new Error(
@@ -5751,7 +5751,7 @@ Use --slug to specify a different slug.`
5751
5751
  const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
5752
5752
  const files = [
5753
5753
  [
5754
- resolve16(assignmentDir, "assignment.md"),
5754
+ resolve17(assignmentDir, "assignment.md"),
5755
5755
  renderAssignment({
5756
5756
  id,
5757
5757
  slug: assignmentSlug,
@@ -5767,35 +5767,35 @@ Use --slug to specify a different slug.`
5767
5767
  })
5768
5768
  ],
5769
5769
  [
5770
- resolve16(assignmentDir, "scratchpad.md"),
5770
+ resolve17(assignmentDir, "scratchpad.md"),
5771
5771
  renderScratchpad({
5772
5772
  assignmentSlug: companionAssignmentRef,
5773
5773
  timestamp
5774
5774
  })
5775
5775
  ],
5776
5776
  [
5777
- resolve16(assignmentDir, "handoff.md"),
5777
+ resolve17(assignmentDir, "handoff.md"),
5778
5778
  renderHandoff({
5779
5779
  assignmentSlug: companionAssignmentRef,
5780
5780
  timestamp
5781
5781
  })
5782
5782
  ],
5783
5783
  [
5784
- resolve16(assignmentDir, "decision-record.md"),
5784
+ resolve17(assignmentDir, "decision-record.md"),
5785
5785
  renderDecisionRecord({
5786
5786
  assignmentSlug: companionAssignmentRef,
5787
5787
  timestamp
5788
5788
  })
5789
5789
  ],
5790
5790
  [
5791
- resolve16(assignmentDir, "progress.md"),
5791
+ resolve17(assignmentDir, "progress.md"),
5792
5792
  renderProgress({
5793
5793
  assignment: companionAssignmentRef,
5794
5794
  timestamp
5795
5795
  })
5796
5796
  ],
5797
5797
  [
5798
- resolve16(assignmentDir, "comments.md"),
5798
+ resolve17(assignmentDir, "comments.md"),
5799
5799
  renderComments({
5800
5800
  assignment: companionAssignmentRef,
5801
5801
  timestamp
@@ -5860,7 +5860,7 @@ init_api();
5860
5860
  init_assignment_resolver();
5861
5861
  import express from "express";
5862
5862
  import { createServer } from "http";
5863
- import { resolve as resolve19 } from "path";
5863
+ import { resolve as resolve20 } from "path";
5864
5864
  import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
5865
5865
  import { WebSocketServer, WebSocket } from "ws";
5866
5866
 
@@ -6075,8 +6075,8 @@ function rowToSession(row) {
6075
6075
  };
6076
6076
  }
6077
6077
  async function appendSession(_projectDir, session) {
6078
- const db2 = getSessionDb();
6079
- db2.prepare(`
6078
+ const db3 = getSessionDb();
6079
+ db3.prepare(`
6080
6080
  INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
6081
6081
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
6082
6082
  ON CONFLICT(session_id) DO UPDATE SET
@@ -6101,36 +6101,36 @@ async function appendSession(_projectDir, session) {
6101
6101
  );
6102
6102
  }
6103
6103
  async function updateSessionStatus(_projectDir, sessionId, status) {
6104
- const db2 = getSessionDb();
6104
+ const db3 = getSessionDb();
6105
6105
  const isTerminal = status === "completed" || status === "stopped";
6106
- const result = isTerminal ? db2.prepare(
6106
+ const result = isTerminal ? db3.prepare(
6107
6107
  "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
6108
- ).run(status, sessionId) : db2.prepare(
6108
+ ).run(status, sessionId) : db3.prepare(
6109
6109
  "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
6110
6110
  ).run(status, sessionId);
6111
6111
  return result.changes > 0;
6112
6112
  }
6113
6113
  async function listAllSessions(_projectsDir) {
6114
- const db2 = getSessionDb();
6115
- const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
6114
+ const db3 = getSessionDb();
6115
+ const rows = db3.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
6116
6116
  return rows.map(rowToSession);
6117
6117
  }
6118
6118
  async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
6119
- const db2 = getSessionDb();
6119
+ const db3 = getSessionDb();
6120
6120
  if (assignmentSlug) {
6121
- const rows2 = db2.prepare(
6121
+ const rows2 = db3.prepare(
6122
6122
  "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
6123
6123
  ).all(projectSlug, assignmentSlug);
6124
6124
  return rows2.map(rowToSession);
6125
6125
  }
6126
- const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
6126
+ const rows = db3.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
6127
6127
  return rows.map(rowToSession);
6128
6128
  }
6129
6129
  async function deleteSessions(sessionIds) {
6130
6130
  if (sessionIds.length === 0) return 0;
6131
- const db2 = getSessionDb();
6131
+ const db3 = getSessionDb();
6132
6132
  const placeholders = sessionIds.map(() => "?").join(", ");
6133
- const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
6133
+ const result = db3.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
6134
6134
  return result.changes;
6135
6135
  }
6136
6136
  var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
@@ -6146,8 +6146,8 @@ async function readAssignmentStatus(projectDir, assignmentSlug) {
6146
6146
  );
6147
6147
  }
6148
6148
  async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
6149
- const db2 = getSessionDb();
6150
- const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
6149
+ const db3 = getSessionDb();
6150
+ const activeSessions = db3.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
6151
6151
  if (activeSessions.length === 0) return 0;
6152
6152
  const assignmentStatuses = /* @__PURE__ */ new Map();
6153
6153
  const seen = /* @__PURE__ */ new Set();
@@ -6184,10 +6184,10 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
6184
6184
  return totalUpdated;
6185
6185
  }
6186
6186
  async function listSessionsByAssignment(projectSlug, assignmentSlug) {
6187
- const db2 = getSessionDb();
6188
- const rows = projectSlug === null ? db2.prepare(
6187
+ const db3 = getSessionDb();
6188
+ const rows = projectSlug === null ? db3.prepare(
6189
6189
  "SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
6190
- ).all(assignmentSlug) : db2.prepare(
6190
+ ).all(assignmentSlug) : db3.prepare(
6191
6191
  "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
6192
6192
  ).all(projectSlug, assignmentSlug);
6193
6193
  return rows.map(rowToSession);
@@ -6195,9 +6195,9 @@ async function listSessionsByAssignment(projectSlug, assignmentSlug) {
6195
6195
 
6196
6196
  // src/dashboard/watcher.ts
6197
6197
  import { watch } from "chokidar";
6198
- import { relative, sep } from "path";
6198
+ import { basename as basename2, dirname as dirname3, relative, sep } from "path";
6199
6199
  function createWatcher(options) {
6200
- const { projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
6200
+ const { projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, dbPath, onMessage, debounceMs = 300 } = options;
6201
6201
  const pendingEvents = /* @__PURE__ */ new Map();
6202
6202
  const projectsWatcher = watch(projectsDir, {
6203
6203
  ignoreInitial: true,
@@ -6365,6 +6365,38 @@ function createWatcher(options) {
6365
6365
  todosWatcher.on("add", handleTodoChange2);
6366
6366
  todosWatcher.on("unlink", handleTodoChange2);
6367
6367
  }
6368
+ let leasesDbWatcher = null;
6369
+ if (dbPath) {
6370
+ let handleDbChange2 = function(filePath) {
6371
+ if (!basename2(filePath).startsWith(dbBase)) return;
6372
+ const debounceKey = "__leases-db__";
6373
+ const existing = pendingEvents.get(debounceKey);
6374
+ if (existing) clearTimeout(existing);
6375
+ pendingEvents.set(
6376
+ debounceKey,
6377
+ setTimeout(() => {
6378
+ pendingEvents.delete(debounceKey);
6379
+ const message = {
6380
+ type: "leases-updated",
6381
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6382
+ };
6383
+ onMessage(message);
6384
+ }, debounceMs)
6385
+ );
6386
+ };
6387
+ var handleDbChange = handleDbChange2;
6388
+ const dbDir = dirname3(dbPath);
6389
+ const dbBase = basename2(dbPath);
6390
+ leasesDbWatcher = watch(dbDir, {
6391
+ ignoreInitial: true,
6392
+ persistent: true,
6393
+ depth: 0,
6394
+ ignored: /(^|[\/\\])\../
6395
+ });
6396
+ leasesDbWatcher.on("change", handleDbChange2);
6397
+ leasesDbWatcher.on("add", handleDbChange2);
6398
+ leasesDbWatcher.on("unlink", handleDbChange2);
6399
+ }
6368
6400
  return {
6369
6401
  close: async () => {
6370
6402
  pendingEvents.forEach((timeout) => {
@@ -6376,6 +6408,7 @@ function createWatcher(options) {
6376
6408
  if (serversWatcher) await serversWatcher.close();
6377
6409
  if (playbooksWatcher) await playbooksWatcher.close();
6378
6410
  if (todosWatcher) await todosWatcher.close();
6411
+ if (leasesDbWatcher) await leasesDbWatcher.close();
6379
6412
  }
6380
6413
  };
6381
6414
  }
@@ -6393,7 +6426,7 @@ init_timestamp();
6393
6426
  init_fs();
6394
6427
  init_parser();
6395
6428
  import { Router } from "express";
6396
- import { resolve as resolve12, basename as basename2 } from "path";
6429
+ import { resolve as resolve12, basename as basename3 } from "path";
6397
6430
  import { rm, readFile as readFile10, open as fsOpen } from "fs/promises";
6398
6431
 
6399
6432
  // src/dashboard/acceptance-criteria.ts
@@ -6678,7 +6711,7 @@ function createWriteRouter(projectsDir, assignmentsDir2) {
6678
6711
  res.status(404).json({ error: `Project "${slug}" not found` });
6679
6712
  return;
6680
6713
  }
6681
- const document = await getEditableDocument(projectsDir, "memory", basename2(projectDir), itemSlug);
6714
+ const document = await getEditableDocument(projectsDir, "memory", basename3(projectDir), itemSlug);
6682
6715
  if (!document) {
6683
6716
  res.status(404).json({ error: "Memory not found" });
6684
6717
  return;
@@ -6697,7 +6730,7 @@ function createWriteRouter(projectsDir, assignmentsDir2) {
6697
6730
  res.status(404).json({ error: `Project "${slug}" not found` });
6698
6731
  return;
6699
6732
  }
6700
- const document = await getEditableDocument(projectsDir, "resource", basename2(projectDir), itemSlug);
6733
+ const document = await getEditableDocument(projectsDir, "resource", basename3(projectDir), itemSlug);
6701
6734
  if (!document) {
6702
6735
  res.status(404).json({ error: "Resource not found" });
6703
6736
  return;
@@ -6782,7 +6815,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
6782
6815
  let content = renderItemStub(kind, {
6783
6816
  slug: requestedSlug,
6784
6817
  name,
6785
- projectSlug: basename2(projectDir),
6818
+ projectSlug: basename3(projectDir),
6786
6819
  timestamp
6787
6820
  });
6788
6821
  const customBody = typeof body.body === "string" ? body.body : "";
@@ -6799,13 +6832,13 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
6799
6832
  } catch (err) {
6800
6833
  if (err.code === "EEXIST") {
6801
6834
  res.status(409).json({
6802
- error: `${kind === "memory" ? "Memory" : "Resource"} with slug "${requestedSlug}" already exists in project "${basename2(projectDir)}".`
6835
+ error: `${kind === "memory" ? "Memory" : "Resource"} with slug "${requestedSlug}" already exists in project "${basename3(projectDir)}".`
6803
6836
  });
6804
6837
  return;
6805
6838
  }
6806
6839
  throw err;
6807
6840
  }
6808
- res.status(201).json({ slug: requestedSlug, projectSlug: basename2(projectDir), content });
6841
+ res.status(201).json({ slug: requestedSlug, projectSlug: basename3(projectDir), content });
6809
6842
  } catch (error) {
6810
6843
  console.error(`Error creating ${kind}:`, error);
6811
6844
  res.status(500).json({ error: `Failed to create ${kind}: ${error.message}` });
@@ -6840,7 +6873,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
6840
6873
  ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
6841
6874
  merged = setTopLevelField(merged, "updated", nowTimestamp());
6842
6875
  await writeFileForce(filePath, merged);
6843
- const detail = await getItemDetail(kind, basename2(projectDir), itemSlug);
6876
+ const detail = await getItemDetail(kind, basename3(projectDir), itemSlug);
6844
6877
  res.json({ [kind]: detail, content: merged });
6845
6878
  } catch (error) {
6846
6879
  console.error(`Error updating ${kind}:`, error);
@@ -8250,6 +8283,65 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
8250
8283
  import { Router as Router3 } from "express";
8251
8284
  import { resolve as resolve13 } from "path";
8252
8285
  init_fs();
8286
+
8287
+ // src/utils/transcript.ts
8288
+ import { open } from "fs/promises";
8289
+ var MAX_LINES_SCANNED = 50;
8290
+ async function derivePathFromTranscript(transcriptPath) {
8291
+ if (!transcriptPath) return null;
8292
+ let handle;
8293
+ try {
8294
+ handle = await open(transcriptPath, "r");
8295
+ } catch {
8296
+ return null;
8297
+ }
8298
+ try {
8299
+ const stream = handle.createReadStream({ encoding: "utf-8" });
8300
+ let buffer = "";
8301
+ let scanned = 0;
8302
+ for await (const chunk of stream) {
8303
+ buffer += chunk;
8304
+ let nl = buffer.indexOf("\n");
8305
+ while (nl !== -1) {
8306
+ const line = buffer.slice(0, nl);
8307
+ buffer = buffer.slice(nl + 1);
8308
+ const cwd = extractCwd(line);
8309
+ if (cwd) {
8310
+ stream.destroy();
8311
+ return cwd;
8312
+ }
8313
+ scanned++;
8314
+ if (scanned >= MAX_LINES_SCANNED) {
8315
+ stream.destroy();
8316
+ return null;
8317
+ }
8318
+ nl = buffer.indexOf("\n");
8319
+ }
8320
+ }
8321
+ if (buffer.length > 0) {
8322
+ const cwd = extractCwd(buffer);
8323
+ if (cwd) return cwd;
8324
+ }
8325
+ return null;
8326
+ } finally {
8327
+ await handle.close().catch(() => {
8328
+ });
8329
+ }
8330
+ }
8331
+ function extractCwd(line) {
8332
+ const trimmed = line.trim();
8333
+ if (trimmed.length === 0 || trimmed[0] !== "{") return null;
8334
+ try {
8335
+ const parsed = JSON.parse(trimmed);
8336
+ if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
8337
+ return parsed.cwd;
8338
+ }
8339
+ } catch {
8340
+ }
8341
+ return null;
8342
+ }
8343
+
8344
+ // src/dashboard/api-agent-sessions.ts
8253
8345
  function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
8254
8346
  const router = Router3();
8255
8347
  router.get("/", async (_req, res) => {
@@ -8297,6 +8389,8 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
8297
8389
  return;
8298
8390
  }
8299
8391
  }
8392
+ const derivedPath = await derivePathFromTranscript(transcriptPath);
8393
+ const recordedPath = derivedPath ?? path ?? "";
8300
8394
  const session = {
8301
8395
  projectSlug: projectSlug || null,
8302
8396
  assignmentSlug: assignmentSlug || null,
@@ -8304,7 +8398,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
8304
8398
  sessionId,
8305
8399
  started: (/* @__PURE__ */ new Date()).toISOString(),
8306
8400
  status: "active",
8307
- path: path || "",
8401
+ path: recordedPath,
8308
8402
  description: description || null,
8309
8403
  transcriptPath: transcriptPath || null
8310
8404
  };
@@ -8355,6 +8449,243 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
8355
8449
  return router;
8356
8450
  }
8357
8451
 
8452
+ // src/dashboard/api-leases.ts
8453
+ import { Router as Router4 } from "express";
8454
+
8455
+ // src/db/leases-db.ts
8456
+ init_paths();
8457
+ import Database2 from "better-sqlite3";
8458
+ import { randomUUID as randomUUID2 } from "crypto";
8459
+ import { resolve as resolve14 } from "path";
8460
+ var db2 = null;
8461
+ var LEASE_SCHEMA_VERSION = "1";
8462
+ var SCHEMA_SQL2 = `
8463
+ CREATE TABLE IF NOT EXISTS meta (
8464
+ key TEXT PRIMARY KEY,
8465
+ value TEXT
8466
+ );
8467
+
8468
+ CREATE TABLE IF NOT EXISTS inventories (
8469
+ slug TEXT PRIMARY KEY,
8470
+ kind TEXT NOT NULL,
8471
+ display_name TEXT,
8472
+ default_ttl_s INTEGER NOT NULL CHECK (default_ttl_s > 0),
8473
+ created_at TEXT NOT NULL
8474
+ );
8475
+
8476
+ CREATE TABLE IF NOT EXISTS inventory_members (
8477
+ inventory_slug TEXT NOT NULL,
8478
+ member_id TEXT NOT NULL,
8479
+ status TEXT NOT NULL DEFAULT 'idle'
8480
+ CHECK (status IN ('idle','leased','retired')),
8481
+ generation INTEGER NOT NULL DEFAULT 0,
8482
+ metadata_json TEXT,
8483
+ last_used_at TEXT,
8484
+ retired_at TEXT,
8485
+ PRIMARY KEY (inventory_slug, member_id),
8486
+ FOREIGN KEY (inventory_slug) REFERENCES inventories(slug)
8487
+ );
8488
+
8489
+ CREATE INDEX IF NOT EXISTS idx_members_idle
8490
+ ON inventory_members (inventory_slug, status, last_used_at);
8491
+
8492
+ CREATE TABLE IF NOT EXISTS leases (
8493
+ lease_id TEXT PRIMARY KEY,
8494
+ inventory_slug TEXT NOT NULL,
8495
+ member_id TEXT NOT NULL,
8496
+ member_gen INTEGER NOT NULL,
8497
+ state TEXT NOT NULL
8498
+ CHECK (state IN ('active','released','expired','revoked')),
8499
+ granted_at TEXT NOT NULL,
8500
+ expires_at TEXT NOT NULL,
8501
+ released_at TEXT,
8502
+ requested_for TEXT,
8503
+ FOREIGN KEY (inventory_slug, member_id)
8504
+ REFERENCES inventory_members(inventory_slug, member_id)
8505
+ );
8506
+
8507
+ CREATE INDEX IF NOT EXISTS idx_leases_gc
8508
+ ON leases (state, expires_at);
8509
+
8510
+ CREATE TABLE IF NOT EXISTS lease_events (
8511
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8512
+ lease_id TEXT NOT NULL,
8513
+ event TEXT NOT NULL,
8514
+ at TEXT NOT NULL,
8515
+ detail_json TEXT,
8516
+ FOREIGN KEY (lease_id) REFERENCES leases(lease_id)
8517
+ );
8518
+
8519
+ CREATE INDEX IF NOT EXISTS idx_events_lease
8520
+ ON lease_events (lease_id, at);
8521
+ `;
8522
+ var NotFoundError = class extends Error {
8523
+ constructor(id) {
8524
+ super(`lease ${id} not found`);
8525
+ this.id = id;
8526
+ this.name = "NotFoundError";
8527
+ }
8528
+ };
8529
+ var LeaseContentionError = class extends Error {
8530
+ constructor(inventorySlug) {
8531
+ super(`contention timeout on '${inventorySlug}'; retry`);
8532
+ this.inventorySlug = inventorySlug;
8533
+ this.name = "LeaseContentionError";
8534
+ }
8535
+ };
8536
+ function nowIso() {
8537
+ return (/* @__PURE__ */ new Date()).toISOString();
8538
+ }
8539
+ function isBusyError(err) {
8540
+ if (!err || typeof err !== "object") return false;
8541
+ const e = err;
8542
+ return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
8543
+ }
8544
+ function initLeasesDb(dbPath) {
8545
+ if (db2) return db2;
8546
+ const finalPath = dbPath ?? resolve14(syntaurRoot(), "syntaur.db");
8547
+ db2 = new Database2(finalPath);
8548
+ db2.pragma("journal_mode = WAL");
8549
+ db2.pragma("busy_timeout = 5000");
8550
+ db2.pragma("foreign_keys = ON");
8551
+ const database = db2;
8552
+ const runMigrations = database.transaction(() => {
8553
+ database.exec(SCHEMA_SQL2);
8554
+ database.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run("lease_schema_version", LEASE_SCHEMA_VERSION);
8555
+ });
8556
+ runMigrations.exclusive();
8557
+ return db2;
8558
+ }
8559
+ function getLeasesDb() {
8560
+ if (!db2) {
8561
+ throw new Error("Leases database not initialized. Call initLeasesDb() first.");
8562
+ }
8563
+ return db2;
8564
+ }
8565
+ function closeLeasesDb() {
8566
+ if (db2) {
8567
+ db2.close();
8568
+ db2 = null;
8569
+ }
8570
+ }
8571
+ function listInventories() {
8572
+ const database = getLeasesDb();
8573
+ return database.prepare(
8574
+ `SELECT slug, kind, display_name, default_ttl_s, created_at
8575
+ FROM inventories ORDER BY slug`
8576
+ ).all();
8577
+ }
8578
+ function getInventoryDetail(slug) {
8579
+ const database = getLeasesDb();
8580
+ const inv = database.prepare(
8581
+ `SELECT slug, kind, display_name, default_ttl_s, created_at
8582
+ FROM inventories WHERE slug = ?`
8583
+ ).get(slug);
8584
+ if (!inv) return null;
8585
+ const members = database.prepare(
8586
+ `SELECT inventory_slug, member_id, status, generation, metadata_json, last_used_at, retired_at
8587
+ FROM inventory_members WHERE inventory_slug = ?
8588
+ ORDER BY member_id`
8589
+ ).all(slug);
8590
+ const active_leases = database.prepare(
8591
+ `SELECT lease_id, inventory_slug, member_id, member_gen, state, granted_at, expires_at, released_at, requested_for
8592
+ FROM leases WHERE inventory_slug = ? AND state = 'active'
8593
+ ORDER BY granted_at`
8594
+ ).all(slug);
8595
+ return { inventory: inv, members, active_leases };
8596
+ }
8597
+ function forceReleaseLease(lease_id) {
8598
+ const database = getLeasesDb();
8599
+ try {
8600
+ const fn = database.transaction(() => {
8601
+ const now = nowIso();
8602
+ const lookup = database.prepare(
8603
+ `SELECT inventory_slug, member_id, member_gen, state
8604
+ FROM leases WHERE lease_id = ?`
8605
+ ).get(lease_id);
8606
+ if (!lookup) throw new NotFoundError(lease_id);
8607
+ if (lookup.state === "revoked") return { member_freed: false };
8608
+ const memberRes = database.prepare(
8609
+ `UPDATE inventory_members
8610
+ SET status = 'idle', generation = generation + 1
8611
+ WHERE inventory_slug = ? AND member_id = ?
8612
+ AND generation = ?`
8613
+ ).run(lookup.inventory_slug, lookup.member_id, lookup.member_gen);
8614
+ database.prepare(
8615
+ `UPDATE leases
8616
+ SET state = 'revoked', released_at = ?
8617
+ WHERE lease_id = ? AND state != 'revoked'`
8618
+ ).run(now, lease_id);
8619
+ const member_freed = memberRes.changes > 0;
8620
+ database.prepare(
8621
+ `INSERT INTO lease_events (lease_id, event, at, detail_json)
8622
+ VALUES (?, 'force_released', ?, ?)`
8623
+ ).run(lease_id, now, JSON.stringify({ member_freed }));
8624
+ return { member_freed };
8625
+ });
8626
+ return fn.immediate();
8627
+ } catch (err) {
8628
+ if (isBusyError(err)) throw new LeaseContentionError(lease_id);
8629
+ throw err;
8630
+ }
8631
+ }
8632
+
8633
+ // src/dashboard/api-leases.ts
8634
+ function createLeasesRouter(broadcast) {
8635
+ const router = Router4();
8636
+ router.get("/", (_req, res) => {
8637
+ try {
8638
+ initLeasesDb();
8639
+ const inventories = listInventories();
8640
+ const details = inventories.map((inv) => getInventoryDetail(inv.slug)).filter((d) => d !== null);
8641
+ res.json({ inventories: details });
8642
+ } catch (error) {
8643
+ res.status(500).json({
8644
+ error: error instanceof Error ? error.message : "List failed"
8645
+ });
8646
+ }
8647
+ });
8648
+ router.get("/:slug", (req, res) => {
8649
+ try {
8650
+ initLeasesDb();
8651
+ const detail = getInventoryDetail(req.params.slug);
8652
+ if (!detail) {
8653
+ res.status(404).json({ error: "Inventory not found" });
8654
+ return;
8655
+ }
8656
+ res.json(detail);
8657
+ } catch (error) {
8658
+ res.status(500).json({
8659
+ error: error instanceof Error ? error.message : "Read failed"
8660
+ });
8661
+ }
8662
+ });
8663
+ router.post("/:slug/force-release/:lease_id", (req, res) => {
8664
+ try {
8665
+ initLeasesDb();
8666
+ const result = forceReleaseLease(req.params.lease_id);
8667
+ broadcast?.({
8668
+ type: "leases-updated",
8669
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8670
+ });
8671
+ res.json({ ...result, lease_id: req.params.lease_id });
8672
+ } catch (error) {
8673
+ if (error instanceof NotFoundError) {
8674
+ res.status(404).json({ error: "Lease not found" });
8675
+ return;
8676
+ }
8677
+ if (error instanceof LeaseContentionError) {
8678
+ res.status(503).json({ error: "Contention timeout; retry" });
8679
+ return;
8680
+ }
8681
+ res.status(500).json({
8682
+ error: error instanceof Error ? error.message : "Force release failed"
8683
+ });
8684
+ }
8685
+ });
8686
+ return router;
8687
+ }
8688
+
8358
8689
  // src/dashboard/api-playbooks.ts
8359
8690
  init_api();
8360
8691
  init_parser();
@@ -8363,11 +8694,11 @@ init_timestamp();
8363
8694
  init_fs();
8364
8695
  init_playbook();
8365
8696
  init_playbooks();
8366
- import { Router as Router4 } from "express";
8367
- import { resolve as resolve14 } from "path";
8697
+ import { Router as Router5 } from "express";
8698
+ import { resolve as resolve15 } from "path";
8368
8699
  import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
8369
8700
  function createPlaybooksRouter(playbooksDir2) {
8370
- const router = Router4();
8701
+ const router = Router5();
8371
8702
  router.get("/", async (_req, res) => {
8372
8703
  try {
8373
8704
  const playbooks = await listPlaybooks(playbooksDir2);
@@ -8434,7 +8765,7 @@ function createPlaybooksRouter(playbooksDir2) {
8434
8765
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
8435
8766
  return;
8436
8767
  }
8437
- const filePath = resolve14(playbooksDir2, resolved.filename);
8768
+ const filePath = resolve15(playbooksDir2, resolved.filename);
8438
8769
  const content = await readFile11(filePath, "utf-8");
8439
8770
  res.json({
8440
8771
  documentType: "playbook",
@@ -8460,7 +8791,7 @@ function createPlaybooksRouter(playbooksDir2) {
8460
8791
  return;
8461
8792
  }
8462
8793
  await ensureDir(playbooksDir2);
8463
- const filePath = resolve14(playbooksDir2, `${slug}.md`);
8794
+ const filePath = resolve15(playbooksDir2, `${slug}.md`);
8464
8795
  if (await fileExists(filePath)) {
8465
8796
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
8466
8797
  return;
@@ -8484,7 +8815,7 @@ function createPlaybooksRouter(playbooksDir2) {
8484
8815
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
8485
8816
  return;
8486
8817
  }
8487
- const filePath = resolve14(playbooksDir2, resolved.filename);
8818
+ const filePath = resolve15(playbooksDir2, resolved.filename);
8488
8819
  await writeFileForce(filePath, content);
8489
8820
  await rebuildPlaybookManifest(playbooksDir2);
8490
8821
  res.json({ slug: resolved.slug, path: filePath });
@@ -8503,7 +8834,7 @@ function createPlaybooksRouter(playbooksDir2) {
8503
8834
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
8504
8835
  return;
8505
8836
  }
8506
- const filePath = resolve14(playbooksDir2, resolved.filename);
8837
+ const filePath = resolve15(playbooksDir2, resolved.filename);
8507
8838
  await unlink2(filePath);
8508
8839
  await removeFromDisabledList(resolved.slug);
8509
8840
  await rebuildPlaybookManifest(playbooksDir2);
@@ -8522,9 +8853,9 @@ init_fs_migration();
8522
8853
  init_parser2();
8523
8854
  init_fs();
8524
8855
  init_paths();
8525
- import { Router as Router5 } from "express";
8856
+ import { Router as Router6 } from "express";
8526
8857
  import { readdir as readdir8 } from "fs/promises";
8527
- import { resolve as resolvePath, dirname as dirname3 } from "path";
8858
+ import { resolve as resolvePath, dirname as dirname4 } from "path";
8528
8859
  import { rename as rename3, mkdir as mkdir2 } from "fs/promises";
8529
8860
 
8530
8861
  // src/dashboard/todos-locks.ts
@@ -8570,7 +8901,7 @@ function touchItem(item) {
8570
8901
  item.updatedAt = now;
8571
8902
  }
8572
8903
  function createTodosRouter(todosDir2, broadcast, projectsDir) {
8573
- const router = Router5();
8904
+ const router = Router6();
8574
8905
  function broadcastUpdate() {
8575
8906
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8576
8907
  }
@@ -8700,7 +9031,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
8700
9031
  router.post("/:workspace/archive", async (req, res) => {
8701
9032
  try {
8702
9033
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
8703
- const { resolve: resolve20 } = await import("path");
9034
+ const { resolve: resolve21 } = await import("path");
8704
9035
  const { readFile: readFile15 } = await import("fs/promises");
8705
9036
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
8706
9037
  const workspace = getWorkspaceParam(req.params.workspace);
@@ -8717,7 +9048,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
8717
9048
  (e) => e.itemIds.every((id) => completedIds.has(id))
8718
9049
  );
8719
9050
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
8720
- await ensureDir(resolve20(todosDir2, "archive"));
9051
+ await ensureDir(resolve21(todosDir2, "archive"));
8721
9052
  let archContent = "";
8722
9053
  if (await fileExists(archFile)) {
8723
9054
  archContent = await readFile15(archFile, "utf-8");
@@ -9124,7 +9455,7 @@ workspace: ${workspace}
9124
9455
  if (await fileExists(newPlanDir)) {
9125
9456
  return { status: 409, error: "plan dir already exists in target" };
9126
9457
  }
9127
- await mkdir2(dirname3(newPlanDir), { recursive: true });
9458
+ await mkdir2(dirname4(newPlanDir), { recursive: true });
9128
9459
  await rename3(item.planDir, newPlanDir);
9129
9460
  item.planDir = newPlanDir;
9130
9461
  }
@@ -9200,9 +9531,9 @@ init_parser2();
9200
9531
  init_fs();
9201
9532
  init_paths();
9202
9533
  init_slug();
9203
- import { Router as Router6 } from "express";
9534
+ import { Router as Router7 } from "express";
9204
9535
  import { mkdir as mkdir3, readFile as readFile13, rename as rename4 } from "fs/promises";
9205
- import { resolve as resolve17, dirname as dirname4 } from "path";
9536
+ import { resolve as resolve18, dirname as dirname5 } from "path";
9206
9537
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
9207
9538
  function touchItem2(item) {
9208
9539
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -9217,7 +9548,7 @@ function params(req) {
9217
9548
  return req.params;
9218
9549
  }
9219
9550
  async function projectExists(projectsDir, slug) {
9220
- return fileExists(resolve17(projectsDir, slug, "project.md"));
9551
+ return fileExists(resolve18(projectsDir, slug, "project.md"));
9221
9552
  }
9222
9553
  async function ensureProjectTodosDir(projectsDir, slug) {
9223
9554
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -9234,7 +9565,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
9234
9565
  throw err;
9235
9566
  }
9236
9567
  try {
9237
- await mkdir3(resolve17(todosDir2, "archive"), { recursive: false });
9568
+ await mkdir3(resolve18(todosDir2, "archive"), { recursive: false });
9238
9569
  } catch (err) {
9239
9570
  const code = err.code;
9240
9571
  if (code === "EEXIST") return;
@@ -9250,7 +9581,7 @@ function notFound(res, slug) {
9250
9581
  res.status(404).json({ error: `Project "${slug}" not found` });
9251
9582
  }
9252
9583
  function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
9253
- const router = Router6({ mergeParams: true });
9584
+ const router = Router7({ mergeParams: true });
9254
9585
  function broadcastUpdate(projectSlug) {
9255
9586
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9256
9587
  }
@@ -9858,18 +10189,18 @@ workspace: ${slug}
9858
10189
  if (tg.includes("/")) {
9859
10190
  const parts = tg.split("/");
9860
10191
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
9861
- assignmentDir = resolve17(projectsDir, parts[0], "assignments", parts[1]);
10192
+ assignmentDir = resolve18(projectsDir, parts[0], "assignments", parts[1]);
9862
10193
  assignmentRef = `${parts[0]}/${parts[1]}`;
9863
10194
  } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
9864
- assignmentDir = resolve17(assignmentsDirFn(), tg);
10195
+ assignmentDir = resolve18(assignmentsDirFn(), tg);
9865
10196
  assignmentRef = tg;
9866
10197
  } else {
9867
10198
  return { error: `Invalid target.assignment "${tg}"` };
9868
10199
  }
9869
- const assignmentMdPath2 = resolve17(assignmentDir, "assignment.md");
10200
+ const assignmentMdPath2 = resolve18(assignmentDir, "assignment.md");
9870
10201
  if (!await fileExists(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
9871
10202
  }
9872
- const assignmentMdPath = resolve17(assignmentDir, "assignment.md");
10203
+ const assignmentMdPath = resolve18(assignmentDir, "assignment.md");
9873
10204
  let content = await readFile13(assignmentMdPath, "utf-8");
9874
10205
  content = appendTodosToAssignmentBody2(
9875
10206
  content,
@@ -9999,7 +10330,7 @@ workspace: ${slug}
9999
10330
  if (await fileExists(newPlanDir)) {
10000
10331
  return { status: 409, error: "plan dir already exists in target" };
10001
10332
  }
10002
- await mkdir3(dirname4(newPlanDir), { recursive: true });
10333
+ await mkdir3(dirname5(newPlanDir), { recursive: true });
10003
10334
  await rename4(item.planDir, newPlanDir);
10004
10335
  item.planDir = newPlanDir;
10005
10336
  }
@@ -10062,7 +10393,7 @@ workspace: ${slug}
10062
10393
 
10063
10394
  // src/dashboard/api-backup.ts
10064
10395
  init_config2();
10065
- import { Router as Router7 } from "express";
10396
+ import { Router as Router8 } from "express";
10066
10397
 
10067
10398
  // src/utils/github-backup.ts
10068
10399
  init_paths();
@@ -10070,8 +10401,8 @@ init_fs();
10070
10401
  init_config2();
10071
10402
  import { execFile as execFile2 } from "child_process";
10072
10403
  import { promisify as promisify2 } from "util";
10073
- import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename5 } from "fs/promises";
10074
- import { resolve as resolve18, join as join2 } from "path";
10404
+ import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open as open2, rename as rename5 } from "fs/promises";
10405
+ import { resolve as resolve19, join as join2 } from "path";
10075
10406
  import { tmpdir } from "os";
10076
10407
  var exec2 = promisify2(execFile2);
10077
10408
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -10111,7 +10442,7 @@ async function resolveCategoryPath(category) {
10111
10442
  case "servers":
10112
10443
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
10113
10444
  case "config":
10114
- return { sourcePath: resolve18(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
10445
+ return { sourcePath: resolve19(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
10115
10446
  }
10116
10447
  }
10117
10448
  async function checkGitInstalled() {
@@ -10122,10 +10453,10 @@ async function checkGitInstalled() {
10122
10453
  }
10123
10454
  }
10124
10455
  async function acquireLock() {
10125
- const lockPath = resolve18(syntaurRoot(), LOCK_FILE_NAME);
10456
+ const lockPath = resolve19(syntaurRoot(), LOCK_FILE_NAME);
10126
10457
  await ensureDir(syntaurRoot());
10127
10458
  try {
10128
- const handle = await open(lockPath, "wx");
10459
+ const handle = await open2(lockPath, "wx");
10129
10460
  await handle.write(String(process.pid));
10130
10461
  await handle.close();
10131
10462
  return lockPath;
@@ -10169,7 +10500,7 @@ async function copyRecursive(src, dest) {
10169
10500
  await ensureDir(dest);
10170
10501
  await cp(src, dest, { recursive: true, force: true });
10171
10502
  } else {
10172
- await ensureDir(resolve18(dest, ".."));
10503
+ await ensureDir(resolve19(dest, ".."));
10173
10504
  await cp(src, dest, { force: true });
10174
10505
  }
10175
10506
  }
@@ -10217,7 +10548,7 @@ async function backupToGithub(overrides) {
10217
10548
  }
10218
10549
  if (category === "config") {
10219
10550
  const sanitized = await readSanitizedConfig(sourcePath);
10220
- await ensureDir(resolve18(destPath, ".."));
10551
+ await ensureDir(resolve19(destPath, ".."));
10221
10552
  await writeFile4(destPath, sanitized, "utf-8");
10222
10553
  } else {
10223
10554
  await copyRecursive(sourcePath, destPath);
@@ -10271,7 +10602,7 @@ async function backupToGithub(overrides) {
10271
10602
  }
10272
10603
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
10273
10604
  if (isFile) {
10274
- await ensureDir(resolve18(localPath, ".."));
10605
+ await ensureDir(resolve19(localPath, ".."));
10275
10606
  await cp(repoSrcPath, localPath, { force: true });
10276
10607
  return;
10277
10608
  }
@@ -10372,7 +10703,7 @@ async function restoreFromGithub(overrides) {
10372
10703
  }
10373
10704
  async function getBackupStatus() {
10374
10705
  const config = await readConfig();
10375
- const lockPath = resolve18(syntaurRoot(), LOCK_FILE_NAME);
10706
+ const lockPath = resolve19(syntaurRoot(), LOCK_FILE_NAME);
10376
10707
  const locked = await fileExists(lockPath);
10377
10708
  return {
10378
10709
  repo: config.backup?.repo ?? null,
@@ -10385,7 +10716,7 @@ async function getBackupStatus() {
10385
10716
 
10386
10717
  // src/dashboard/api-backup.ts
10387
10718
  function createBackupRouter() {
10388
- const router = Router7();
10719
+ const router = Router8();
10389
10720
  router.get("/", async (_req, res) => {
10390
10721
  try {
10391
10722
  const status = await getBackupStatus();
@@ -10706,10 +11037,11 @@ function createDashboardServer(options) {
10706
11037
  migrateFromMarkdown(projectsDir).catch((err) => {
10707
11038
  console.error("Session migration from markdown failed:", err);
10708
11039
  });
11040
+ initLeasesDb();
10709
11041
  (async () => {
10710
11042
  try {
10711
11043
  const configResult = await migrateLegacyConfig(
10712
- resolve19(syntaurRoot(), "config.md")
11044
+ resolve20(syntaurRoot(), "config.md")
10713
11045
  );
10714
11046
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
10715
11047
  const summary = summarizeMigration(projectResult, configResult);
@@ -11039,6 +11371,7 @@ function createDashboardServer(options) {
11039
11371
  });
11040
11372
  app.use(createWriteRouter(projectsDir, assignmentsDir2));
11041
11373
  app.use("/api/servers", createServersRouter(serversDir2, projectsDir, assignmentsDir2));
11374
+ app.use("/api/leases", createLeasesRouter(broadcast));
11042
11375
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2));
11043
11376
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
11044
11377
  app.get("/api/memories", async (_req, res) => {
@@ -11064,14 +11397,14 @@ function createDashboardServer(options) {
11064
11397
  app.use("/api/backup", createBackupRouter());
11065
11398
  if (serveStaticUi && dashboardDistPath) {
11066
11399
  const sendOpts = { dotfiles: "allow" };
11067
- app.use("/assets", express.static(resolve19(dashboardDistPath, "assets"), sendOpts));
11400
+ app.use("/assets", express.static(resolve20(dashboardDistPath, "assets"), sendOpts));
11068
11401
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
11069
11402
  app.get("{*path}", async (req, res) => {
11070
11403
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
11071
11404
  res.status(404).json({ error: "Not Found" });
11072
11405
  return;
11073
11406
  }
11074
- const indexPath = resolve19(dashboardDistPath, "index.html");
11407
+ const indexPath = resolve20(dashboardDistPath, "index.html");
11075
11408
  if (!await fileExists(indexPath)) {
11076
11409
  res.status(503).send(
11077
11410
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -11095,6 +11428,7 @@ function createDashboardServer(options) {
11095
11428
  serversDir: serversDir2,
11096
11429
  playbooksDir: playbooksDir2,
11097
11430
  todosDir: todosDir2,
11431
+ dbPath: resolve20(syntaurRoot(), "syntaur.db"),
11098
11432
  onMessage: broadcast
11099
11433
  });
11100
11434
  startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
@@ -11109,7 +11443,7 @@ function createDashboardServer(options) {
11109
11443
  }
11110
11444
  });
11111
11445
  server.listen(port, () => {
11112
- const portFile = resolve19(syntaurRoot(), "dashboard-port");
11446
+ const portFile = resolve20(syntaurRoot(), "dashboard-port");
11113
11447
  writeFile5(portFile, String(port), "utf-8").catch(() => {
11114
11448
  });
11115
11449
  resolvePromise();
@@ -11122,11 +11456,12 @@ function createDashboardServer(options) {
11122
11456
  await watcherHandle.close();
11123
11457
  }
11124
11458
  closeSessionDb();
11459
+ closeLeasesDb();
11125
11460
  for (const client of clients) {
11126
11461
  client.terminate();
11127
11462
  }
11128
11463
  clients.clear();
11129
- const portFile = resolve19(syntaurRoot(), "dashboard-port");
11464
+ const portFile = resolve20(syntaurRoot(), "dashboard-port");
11130
11465
  await unlink4(portFile).catch(() => {
11131
11466
  });
11132
11467
  server.closeAllConnections?.();