syntaur 0.16.3 → 0.17.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 (77) hide show
  1. package/dashboard/dist/assets/{_basePickBy-DTuHiZCP.js → _basePickBy-Dek5-ACb.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-BjbISCNN.js → _baseUniq-BeLddDKU.js} +1 -1
  3. package/dashboard/dist/assets/{arc-D9mCa8If.js → arc-kotdj4Uh.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CLWheiZu.js → architectureDiagram-2XIMDMQ5-BaqFXQ-q.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BBxrVTWB.js → blockDiagram-WCTKOSBZ-BSyGne6h.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-DnhDZ2W3.js → c4Diagram-IC4MRINW-iYjZFKOW.js} +1 -1
  7. package/dashboard/dist/assets/channel-eXdj7ayV.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-DeAfVeDa.js → chunk-4BX2VUAB-DvY9Y7jB.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-cKEeGDNI.js → chunk-55IACEB6-2vCDm2cJ.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-7RuZ6HEq.js → chunk-FMBD7UC4-BVPaoRtV.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-B5dwG0bv.js → chunk-JSJVCQXG-B9uJpMzd.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-DeyQYDVj.js → chunk-KX2RTZJC-CHcjjGaq.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-NYo4hSqA.js → chunk-NQ4KR5QH-B8n19pSs.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-CtJFICF8.js → chunk-QZHKN3VN-CF9ZYFGK.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-DZ3WYdab.js → chunk-WL4C6EOR-mY_1Vmdf.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-B-bBQACr.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-B-bBQACr.js +1 -0
  18. package/dashboard/dist/assets/clone-CL04pv9J.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-DaYOsf-M.js → cose-bilkent-S5V4N54A-DOYoG3gB.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-DzLc7ykF.js → dagre-KLK3FWXG-CZFrp-Yq.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-kpjEdOqb.js → diagram-E7M64L7V-F3rF5YwM.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-D3EHoh6j.js → diagram-IFDJBPK2-BynWv-Vo.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-Cp6Un2ys.js → diagram-P4PSJMXO-NIyzhm_M.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-DDjOUPWk.js → erDiagram-INFDFZHY-Ca9NbihE.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-CfO51xge.js → flowDiagram-PKNHOUZH-YhS0MOu4.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-BDzAtkcp.js → ganttDiagram-A5KZAMGK-G7P3qq_2.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-JPMFN2zF.js → gitGraphDiagram-K3NZZRJ6-BJAK7M7E.js} +1 -1
  28. package/dashboard/dist/assets/{graph-DXDryilu.js → graph-11geMy3f.js} +1 -1
  29. package/dashboard/dist/assets/index--qwybK3W.js +535 -0
  30. package/dashboard/dist/assets/index-DwrhlK8r.css +1 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-CYi5kkvz.js → infoDiagram-LFFYTUFH-COcQYcLf.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DQl_IUe8.js → ishikawaDiagram-PHBUUO56-B7Jjyw8J.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-BXXGPcrS.js → journeyDiagram-4ABVD52K--snU-QAX.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-BqJestUY.js → kanban-definition-K7BYSVSG-BucLoc89.js} +1 -1
  35. package/dashboard/dist/assets/{layout-D5VdYmWn.js → layout-Bcgxdz8A.js} +1 -1
  36. package/dashboard/dist/assets/{linear-dZA_O_GN.js → linear-CRF8xwvk.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-B0Ixd1yP.js → mermaid.core-D5JD5uYg.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-CSJYdSMG.js → mindmap-definition-YRQLILUH-BqGcTcbk.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-DmYrRZHN.js → pieDiagram-SKSYHLDU-CnQ2EkyX.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-La3ce5kE.js → quadrantDiagram-337W2JSQ-CKCfmTRc.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-DPitIZQl.js → requirementDiagram-Z7DCOOCP-B9J4bvUK.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-CAmCqGkr.js → sankeyDiagram-WA2Y5GQK-CMSzI5rg.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-6lEzNTWG.js → sequenceDiagram-2WXFIKYE-BFQDtxd0.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DyGKCS2C.js → stateDiagram-RAJIS63D-DgWYKT02.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Nmwft4cR.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-D9RxQE3j.js → timeline-definition-YZTLITO2-jANu0e7M.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-TuXbGNN4.js → treemap-KZPCXAKY-BFjWMkyM.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Bly5knr-.js → vennDiagram-LZ73GAT5-DGwrAJYj.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-CA-5z7-4.js → xychartDiagram-JWTSCODW-BLCPfoFM.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +1945 -312
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/db/leases-db.js.map +1 -1
  54. package/dist/index.js +4390 -1126
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.js +30 -8
  57. package/dist/launch/index.js.map +1 -1
  58. package/package.json +1 -1
  59. package/platforms/claude-code/skills/bundle-worktree/SKILL.md +107 -0
  60. package/platforms/claude-code/skills/complete-bundle/SKILL.md +113 -0
  61. package/platforms/claude-code/skills/grab-bundle/SKILL.md +111 -0
  62. package/platforms/claude-code/skills/plan-bundle/SKILL.md +88 -0
  63. package/platforms/codex/skills/bundle-worktree/SKILL.md +107 -0
  64. package/platforms/codex/skills/complete-bundle/SKILL.md +113 -0
  65. package/platforms/codex/skills/grab-bundle/SKILL.md +111 -0
  66. package/platforms/codex/skills/plan-bundle/SKILL.md +88 -0
  67. package/skills/bundle-worktree/SKILL.md +107 -0
  68. package/skills/complete-bundle/SKILL.md +113 -0
  69. package/skills/grab-bundle/SKILL.md +111 -0
  70. package/skills/plan-bundle/SKILL.md +88 -0
  71. package/dashboard/dist/assets/channel-CN5VmjNa.js +0 -1
  72. package/dashboard/dist/assets/classDiagram-VBA2DB6C-CqLb9GuU.js +0 -1
  73. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CqLb9GuU.js +0 -1
  74. package/dashboard/dist/assets/clone-DwvrjCQs.js +0 -1
  75. package/dashboard/dist/assets/index-D7UtkCYM.js +0 -515
  76. package/dashboard/dist/assets/index-DTXSWSzH.css +0 -1
  77. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BOmRICoY.js +0 -1
@@ -12,11 +12,15 @@ var __export = (target, all) => {
12
12
  var paths_exports = {};
13
13
  __export(paths_exports, {
14
14
  assignmentsDir: () => assignmentsDir,
15
+ bundlePlanDir: () => bundlePlanDir,
16
+ bundlesDir: () => bundlesDir,
17
+ bundlesPath: () => bundlesPath,
15
18
  defaultProjectDir: () => defaultProjectDir,
16
19
  expandHome: () => expandHome,
17
20
  playbooksDir: () => playbooksDir,
18
21
  projectTodosDir: () => projectTodosDir,
19
22
  proofDir: () => proofDir,
23
+ savedViewsFile: () => savedViewsFile,
20
24
  serversDir: () => serversDir,
21
25
  syntaurRoot: () => syntaurRoot,
22
26
  todoPlanDir: () => todoPlanDir,
@@ -56,12 +60,24 @@ function todosDir() {
56
60
  function viewPrefsFile() {
57
61
  return resolve(syntaurRoot(), "view-prefs.json");
58
62
  }
63
+ function savedViewsFile() {
64
+ return resolve(syntaurRoot(), "saved-views.json");
65
+ }
59
66
  function projectTodosDir(projectsDir, projectSlug) {
60
67
  return resolve(projectsDir, projectSlug, "todos");
61
68
  }
62
69
  function todoPlanDir(todosDir2, workspaceOrProject, todoId) {
63
70
  return resolve(todosDir2, "plans", workspaceOrProject, todoId);
64
71
  }
72
+ function bundlePlanDir(todosDir2, scopeOrProject, bundleId) {
73
+ return resolve(todosDir2, "plans", scopeOrProject, "bundles", bundleId);
74
+ }
75
+ function bundlesDir(todosDir2) {
76
+ return resolve(todosDir2, "bundles");
77
+ }
78
+ function bundlesPath(todosDir2) {
79
+ return resolve(todosDir2, "bundles", "index.md");
80
+ }
65
81
  function proofDir(assignmentDir) {
66
82
  return resolve(assignmentDir, "proof");
67
83
  }
@@ -99,7 +115,7 @@ function buildTransitionTable(transitions) {
99
115
  return table;
100
116
  }
101
117
  function getTargetStatus(_from, command, table) {
102
- if (!table || table === DEFAULT_TRANSITION_TABLE) {
118
+ if (!table) {
103
119
  return DEFAULT_COMMAND_TARGETS.get(command) ?? null;
104
120
  }
105
121
  return table.get(command) ?? table.get(`${_from}:${command}`) ?? null;
@@ -772,7 +788,8 @@ function emptyMetaFields() {
772
788
  updatedAt: null,
773
789
  planDir: null,
774
790
  linkedAssignmentId: null,
775
- linkedAssignmentRef: null
791
+ linkedAssignmentRef: null,
792
+ bundleId: null
776
793
  };
777
794
  }
778
795
  function parseMetaToken(line) {
@@ -811,6 +828,9 @@ function parseMetaToken(line) {
811
828
  case "lr":
812
829
  fields.linkedAssignmentRef = value;
813
830
  break;
831
+ case "bn":
832
+ fields.bundleId = value;
833
+ break;
814
834
  }
815
835
  }
816
836
  return fields;
@@ -824,6 +844,7 @@ function serializeMetaToken(item) {
824
844
  if (item.planDir !== null) pairs.push(`p=${encodeMetaValue(item.planDir)}`);
825
845
  if (item.linkedAssignmentId !== null) pairs.push(`l=${encodeMetaValue(item.linkedAssignmentId)}`);
826
846
  if (item.linkedAssignmentRef !== null) pairs.push(`lr=${encodeMetaValue(item.linkedAssignmentRef)}`);
847
+ if (item.bundleId !== null) pairs.push(`bn=${encodeMetaValue(item.bundleId)}`);
827
848
  if (pairs.length === 0) return "";
828
849
  return `<${pairs.join(";")}>`;
829
850
  }
@@ -884,7 +905,8 @@ function parseChecklistItem(line) {
884
905
  updatedAt: meta.updatedAt,
885
906
  planDir: meta.planDir,
886
907
  linkedAssignmentId: meta.linkedAssignmentId,
887
- linkedAssignmentRef: meta.linkedAssignmentRef
908
+ linkedAssignmentRef: meta.linkedAssignmentRef,
909
+ bundleId: meta.bundleId
888
910
  };
889
911
  }
890
912
  function serializeChecklistItem(item) {
@@ -2835,7 +2857,20 @@ function getAssignmentTypes(config) {
2835
2857
  return config.types ?? DEFAULT_ASSIGNMENT_TYPES;
2836
2858
  }
2837
2859
  function getAgents(config) {
2838
- return config.agents ?? BUILTIN_AGENTS;
2860
+ if (config.agents === null) return BUILTIN_AGENTS;
2861
+ const builtinById = new Map(BUILTIN_AGENTS.map((a) => [a.id, a]));
2862
+ return config.agents.map((agent) => {
2863
+ const builtin = builtinById.get(agent.id);
2864
+ if (!builtin) return agent;
2865
+ const resume = agent.resume ?? builtin.resume;
2866
+ const fork = agent.fork ?? builtin.fork;
2867
+ if (resume === agent.resume && fork === agent.fork) return agent;
2868
+ return {
2869
+ ...agent,
2870
+ ...resume ? { resume } : {},
2871
+ ...fork ? { fork } : {}
2872
+ };
2873
+ });
2839
2874
  }
2840
2875
  function parseTerminalConfig(value) {
2841
2876
  if (value === void 0 || value === null || value === "") return null;
@@ -3836,8 +3871,8 @@ async function migrateFromMarkdown(projectsDir) {
3836
3871
  return allSessions.length;
3837
3872
  }
3838
3873
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
3839
- const { readFile: readFile18 } = await import("fs/promises");
3840
- const raw = await readFile18(filePath, "utf-8");
3874
+ const { readFile: readFile21 } = await import("fs/promises");
3875
+ const raw = await readFile21(filePath, "utf-8");
3841
3876
  const sessions = [];
3842
3877
  const lines = raw.split("\n");
3843
3878
  let inTable = false;
@@ -3926,8 +3961,8 @@ function rowToSession(row) {
3926
3961
  };
3927
3962
  }
3928
3963
  async function appendSession(_projectDir, session) {
3929
- const db3 = getSessionDb();
3930
- db3.prepare(`
3964
+ const db4 = getSessionDb();
3965
+ db4.prepare(`
3931
3966
  INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at)
3932
3967
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3933
3968
  ON CONFLICT(session_id) DO UPDATE SET
@@ -3956,41 +3991,41 @@ async function appendSession(_projectDir, session) {
3956
3991
  );
3957
3992
  }
3958
3993
  async function updateSessionStatus(_projectDir, sessionId, status) {
3959
- const db3 = getSessionDb();
3994
+ const db4 = getSessionDb();
3960
3995
  const isTerminal = status === "completed" || status === "stopped";
3961
- const result = isTerminal ? db3.prepare(
3996
+ const result = isTerminal ? db4.prepare(
3962
3997
  "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
3963
- ).run(status, sessionId) : db3.prepare(
3998
+ ).run(status, sessionId) : db4.prepare(
3964
3999
  "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
3965
4000
  ).run(status, sessionId);
3966
4001
  return result.changes > 0;
3967
4002
  }
3968
4003
  async function listAllSessions(_projectsDir) {
3969
- const db3 = getSessionDb();
3970
- const rows = db3.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
4004
+ const db4 = getSessionDb();
4005
+ const rows = db4.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
3971
4006
  return rows.map(rowToSession);
3972
4007
  }
3973
4008
  function getSessionById(sessionId) {
3974
- const db3 = getSessionDb();
3975
- const row = db3.prepare("SELECT * FROM sessions WHERE session_id = ? LIMIT 1").get(sessionId);
4009
+ const db4 = getSessionDb();
4010
+ const row = db4.prepare("SELECT * FROM sessions WHERE session_id = ? LIMIT 1").get(sessionId);
3976
4011
  return row ? rowToSession(row) : null;
3977
4012
  }
3978
4013
  async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
3979
- const db3 = getSessionDb();
4014
+ const db4 = getSessionDb();
3980
4015
  if (assignmentSlug) {
3981
- const rows2 = db3.prepare(
4016
+ const rows2 = db4.prepare(
3982
4017
  "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
3983
4018
  ).all(projectSlug, assignmentSlug);
3984
4019
  return rows2.map(rowToSession);
3985
4020
  }
3986
- const rows = db3.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
4021
+ const rows = db4.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
3987
4022
  return rows.map(rowToSession);
3988
4023
  }
3989
4024
  async function deleteSessions(sessionIds) {
3990
4025
  if (sessionIds.length === 0) return 0;
3991
- const db3 = getSessionDb();
4026
+ const db4 = getSessionDb();
3992
4027
  const placeholders = sessionIds.map(() => "?").join(", ");
3993
- const result = db3.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
4028
+ const result = db4.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
3994
4029
  return result.changes;
3995
4030
  }
3996
4031
  async function readAssignmentStatusFromPath(assignmentMdPath) {
@@ -4005,8 +4040,8 @@ async function readAssignmentStatus(projectDir, assignmentSlug) {
4005
4040
  );
4006
4041
  }
4007
4042
  async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
4008
- const db3 = getSessionDb();
4009
- const activeSessions = db3.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
4043
+ const db4 = getSessionDb();
4044
+ const activeSessions = db4.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
4010
4045
  if (activeSessions.length === 0) return 0;
4011
4046
  const assignmentStatuses = /* @__PURE__ */ new Map();
4012
4047
  const seen = /* @__PURE__ */ new Set();
@@ -4043,10 +4078,10 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
4043
4078
  return totalUpdated;
4044
4079
  }
4045
4080
  async function listSessionsByAssignment(projectSlug, assignmentSlug) {
4046
- const db3 = getSessionDb();
4047
- const rows = projectSlug === null ? db3.prepare(
4081
+ const db4 = getSessionDb();
4082
+ const rows = projectSlug === null ? db4.prepare(
4048
4083
  "SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
4049
- ).all(assignmentSlug) : db3.prepare(
4084
+ ).all(assignmentSlug) : db4.prepare(
4050
4085
  "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
4051
4086
  ).all(projectSlug, assignmentSlug);
4052
4087
  return rows.map(rowToSession);
@@ -4726,12 +4761,17 @@ async function getStatusConfig() {
4726
4761
  const terminalSet = new Set(
4727
4762
  config.statuses.statuses.filter((s) => s.terminal).map((s) => s.id)
4728
4763
  );
4764
+ const hasCustomTransitions = config.statuses.transitions.length > 0;
4765
+ const effectiveTransitions = hasCustomTransitions ? config.statuses.transitions : Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
4766
+ const [from, command] = key.split(":");
4767
+ return { from, command, to };
4768
+ });
4729
4769
  _cachedConfig = {
4730
4770
  custom: true,
4731
4771
  statuses: config.statuses.statuses,
4732
4772
  order: config.statuses.order,
4733
- transitions: config.statuses.transitions,
4734
- transitionTable: buildTransitionTable(config.statuses.transitions),
4773
+ transitions: effectiveTransitions,
4774
+ transitionTable: buildTransitionTable(effectiveTransitions),
4735
4775
  terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
4736
4776
  };
4737
4777
  } else {
@@ -4979,16 +5019,17 @@ async function getStandaloneAvailableTransitions(assignment) {
4979
5019
  const transitionDefs = getTransitionDefinitions(config);
4980
5020
  const actions = [];
4981
5021
  for (const definition of transitionDefs) {
5022
+ const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
5023
+ if (target === null) continue;
4982
5024
  let warning = null;
4983
5025
  if (definition.command === "start" && !assignment.assignee) {
4984
5026
  warning = "No assignee set \u2014 consider assigning before starting.";
4985
5027
  }
4986
- const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
4987
5028
  actions.push({
4988
5029
  command: definition.command,
4989
5030
  label: definition.label,
4990
5031
  description: definition.description,
4991
- targetStatus: target ?? definition.command,
5032
+ targetStatus: target,
4992
5033
  disabled: false,
4993
5034
  disabledReason: null,
4994
5035
  warning,
@@ -5166,6 +5207,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5166
5207
  slug: assignment.slug || assignmentSlug,
5167
5208
  title: assignment.title,
5168
5209
  status: assignment.status,
5210
+ type: assignment.type,
5169
5211
  priority: assignment.priority,
5170
5212
  assignee: assignment.assignee,
5171
5213
  dependsOn: assignment.dependsOn,
@@ -5414,6 +5456,7 @@ async function buildStandaloneAssignmentDetail(resolved) {
5414
5456
  slug: assignment.slug || resolved.id,
5415
5457
  title: assignment.title,
5416
5458
  status: assignment.status,
5459
+ type: assignment.type,
5417
5460
  priority: assignment.priority,
5418
5461
  assignee: assignment.assignee,
5419
5462
  dependsOn: [],
@@ -5722,6 +5765,7 @@ function toAssignmentSummary(assignment) {
5722
5765
  slug: assignment.slug,
5723
5766
  title: assignment.title,
5724
5767
  status: assignment.status,
5768
+ type: assignment.type,
5725
5769
  priority: assignment.priority,
5726
5770
  assignee: assignment.assignee,
5727
5771
  dependsOn: assignment.dependsOn,
@@ -5777,6 +5821,8 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
5777
5821
  const projectPath = resolve13(projectsDir, projectSlug);
5778
5822
  const traces = options?.traces;
5779
5823
  for (const definition of transitionDefs) {
5824
+ const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
5825
+ if (target === null) continue;
5780
5826
  let warning = null;
5781
5827
  if (definition.command === "start" && !assignment.assignee) {
5782
5828
  warning = "No assignee set \u2014 consider assigning before starting.";
@@ -5794,12 +5840,11 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
5794
5840
  warning = `Unmet dependencies: ${unmetDependencies.join(", ")}.`;
5795
5841
  }
5796
5842
  }
5797
- const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
5798
5843
  actions.push({
5799
5844
  command: definition.command,
5800
5845
  label: definition.label,
5801
5846
  description: definition.description,
5802
- targetStatus: target ?? definition.command,
5847
+ targetStatus: target,
5803
5848
  disabled: false,
5804
5849
  disabledReason: null,
5805
5850
  warning,
@@ -6415,8 +6460,8 @@ init_assignment_resolver();
6415
6460
  init_agent_sessions();
6416
6461
  import express from "express";
6417
6462
  import { createServer } from "http";
6418
- import { resolve as resolve24 } from "path";
6419
- import { writeFile as writeFile5, unlink as unlink5 } from "fs/promises";
6463
+ import { resolve as resolve28 } from "path";
6464
+ import { writeFile as writeFile6, unlink as unlink6 } from "fs/promises";
6420
6465
  import { WebSocketServer, WebSocket } from "ws";
6421
6466
 
6422
6467
  // src/dashboard/session-liveness.ts
@@ -6740,6 +6785,7 @@ var DENSITIES = ["comfortable", "compact"];
6740
6785
  var GROUPINGS = [
6741
6786
  "none",
6742
6787
  "status",
6788
+ "type",
6743
6789
  "priority",
6744
6790
  "assignee",
6745
6791
  "project"
@@ -6753,6 +6799,7 @@ var DEFAULT_VIEW_PREFS = {
6753
6799
  grouping: "none",
6754
6800
  filters: {
6755
6801
  status: "all",
6802
+ type: "all",
6756
6803
  priority: "all",
6757
6804
  assignee: "all",
6758
6805
  project: "all",
@@ -6909,6 +6956,7 @@ function isViewPrefsDefaults(file) {
6909
6956
  const gf = g.filters;
6910
6957
  const df = d.filters;
6911
6958
  if (gf.status !== df.status) return false;
6959
+ if (gf.type !== df.type) return false;
6912
6960
  if (gf.priority !== df.priority) return false;
6913
6961
  if (gf.assignee !== df.assignee) return false;
6914
6962
  if (gf.project !== df.project) return false;
@@ -6916,6 +6964,250 @@ function isViewPrefsDefaults(file) {
6916
6964
  return true;
6917
6965
  }
6918
6966
 
6967
+ // src/dashboard/api-saved-views.ts
6968
+ import { Router } from "express";
6969
+
6970
+ // src/utils/saved-views-schema.ts
6971
+ var TABLE_COLUMN_IDS = [
6972
+ "title",
6973
+ "status",
6974
+ "priority",
6975
+ "assignee",
6976
+ "dependencies",
6977
+ "updated"
6978
+ ];
6979
+ function isTableColumnId(value) {
6980
+ return typeof value === "string" && TABLE_COLUMN_IDS.includes(value);
6981
+ }
6982
+ var DEFAULT_FILTERS = {
6983
+ status: "all",
6984
+ priority: "all",
6985
+ assignee: "all",
6986
+ project: "all",
6987
+ activity: "all"
6988
+ };
6989
+ var SEED_TIMESTAMP = "2026-05-21T00:00:00Z";
6990
+ function makeSeededView(id, name, filterOverrides) {
6991
+ return {
6992
+ id,
6993
+ name,
6994
+ workspace: null,
6995
+ config: {
6996
+ viewMode: "list",
6997
+ filters: { ...DEFAULT_FILTERS, ...filterOverrides },
6998
+ sortField: "updated",
6999
+ sortDirection: "desc",
7000
+ listSectionVisibility: { collapsed: [] },
7001
+ kanbanColumnVisibility: { hidden: [] },
7002
+ tableColumnVisibility: { hidden: [] }
7003
+ },
7004
+ createdAt: SEED_TIMESTAMP,
7005
+ updatedAt: SEED_TIMESTAMP
7006
+ };
7007
+ }
7008
+ var DEFAULT_SAVED_VIEWS_FILE = {
7009
+ version: 1,
7010
+ views: [
7011
+ makeSeededView("default-recently-updated", "Recently updated", {}),
7012
+ makeSeededView("default-high-priority", "High priority", { priority: "high" }),
7013
+ makeSeededView("default-stale", "Stale", { activity: "stale" })
7014
+ ],
7015
+ dashboard: {
7016
+ version: 1,
7017
+ slots: [
7018
+ { id: "slot-0", widget: { kind: "agent-sessions" } },
7019
+ { id: "slot-1", widget: { kind: "saved-view", viewId: "default-recently-updated" } },
7020
+ { id: "slot-2", widget: { kind: "saved-view", viewId: "default-high-priority" } },
7021
+ { id: "slot-3", widget: { kind: "saved-view", viewId: "default-stale" } },
7022
+ { id: "slot-4", widget: { kind: "inventories" } }
7023
+ ]
7024
+ }
7025
+ };
7026
+ function isWidgetConfig(value) {
7027
+ if (!value || typeof value !== "object") return false;
7028
+ const obj = value;
7029
+ if (obj.kind === "saved-view") {
7030
+ return typeof obj.viewId === "string" && obj.viewId.length > 0;
7031
+ }
7032
+ return obj.kind === "agent-sessions" || obj.kind === "inventories";
7033
+ }
7034
+ function isListSectionVisibility(value) {
7035
+ if (!value || typeof value !== "object") return false;
7036
+ const obj = value;
7037
+ return Array.isArray(obj.collapsed) && obj.collapsed.every((s) => typeof s === "string");
7038
+ }
7039
+ function isKanbanColumnVisibility(value) {
7040
+ if (!value || typeof value !== "object") return false;
7041
+ const obj = value;
7042
+ return Array.isArray(obj.hidden) && obj.hidden.every((s) => typeof s === "string");
7043
+ }
7044
+ function isTableColumnVisibility(value) {
7045
+ if (!value || typeof value !== "object") return false;
7046
+ const obj = value;
7047
+ return Array.isArray(obj.hidden) && obj.hidden.every((s) => isTableColumnId(s));
7048
+ }
7049
+ function isViewFilters(value) {
7050
+ if (!value || typeof value !== "object") return false;
7051
+ const obj = value;
7052
+ for (const key of ["status", "priority", "assignee", "project", "activity"]) {
7053
+ const v = obj[key];
7054
+ if (v !== void 0 && typeof v !== "string") return false;
7055
+ }
7056
+ return true;
7057
+ }
7058
+ function isSavedViewConfig(value) {
7059
+ if (!value || typeof value !== "object") return false;
7060
+ const obj = value;
7061
+ return isViewMode(obj.viewMode) && isViewFilters(obj.filters) && isSortField(obj.sortField) && isSortDirection(obj.sortDirection) && isListSectionVisibility(obj.listSectionVisibility) && isKanbanColumnVisibility(obj.kanbanColumnVisibility) && isTableColumnVisibility(obj.tableColumnVisibility);
7062
+ }
7063
+ function isSavedView(value) {
7064
+ if (!value || typeof value !== "object") return false;
7065
+ const obj = value;
7066
+ return typeof obj.id === "string" && obj.id.length > 0 && typeof obj.name === "string" && (obj.workspace === null || typeof obj.workspace === "string") && isSavedViewConfig(obj.config) && typeof obj.createdAt === "string" && typeof obj.updatedAt === "string";
7067
+ }
7068
+ function isDashboardSlot(value) {
7069
+ if (!value || typeof value !== "object") return false;
7070
+ const obj = value;
7071
+ if (typeof obj.id !== "string" || obj.id.length === 0) return false;
7072
+ if (obj.widget === null) return true;
7073
+ return isWidgetConfig(obj.widget);
7074
+ }
7075
+
7076
+ // src/utils/saved-views.ts
7077
+ init_paths();
7078
+ init_fs();
7079
+ import { readFile as readFile12, rename as rename4, unlink as unlink4 } from "fs/promises";
7080
+ import { randomUUID } from "crypto";
7081
+ import { resolve as resolve15 } from "path";
7082
+ function corruptFilePath2() {
7083
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7084
+ return resolve15(syntaurRoot(), `saved-views.corrupt-${ts}.json`);
7085
+ }
7086
+ function isSavedViewsFileShape(value) {
7087
+ if (!value || typeof value !== "object") return false;
7088
+ const obj = value;
7089
+ if (obj.version !== void 0 && obj.version !== 1) return false;
7090
+ if (!Array.isArray(obj.views)) return false;
7091
+ if (!obj.views.every(isSavedView)) return false;
7092
+ if (!obj.dashboard || typeof obj.dashboard !== "object") return false;
7093
+ const dash = obj.dashboard;
7094
+ if (dash.version !== void 0 && dash.version !== 1) return false;
7095
+ if (!Array.isArray(dash.slots)) return false;
7096
+ if (!dash.slots.every(isDashboardSlot)) return false;
7097
+ return true;
7098
+ }
7099
+ function cloneDefault() {
7100
+ return JSON.parse(JSON.stringify(DEFAULT_SAVED_VIEWS_FILE));
7101
+ }
7102
+ async function readSavedViewsFile() {
7103
+ const path = savedViewsFile();
7104
+ if (!await fileExists(path)) {
7105
+ return cloneDefault();
7106
+ }
7107
+ let raw;
7108
+ try {
7109
+ raw = await readFile12(path, "utf-8");
7110
+ } catch {
7111
+ return cloneDefault();
7112
+ }
7113
+ let parsed;
7114
+ try {
7115
+ parsed = JSON.parse(raw);
7116
+ } catch {
7117
+ await backupCorrupt2(path);
7118
+ return cloneDefault();
7119
+ }
7120
+ if (parsed && typeof parsed === "object" && parsed.version !== void 0) {
7121
+ const v = parsed.version;
7122
+ if (typeof v === "number" && v > 1) {
7123
+ console.warn(
7124
+ `[saved-views] Unknown version ${v} at ${path}; returning defaults (file left intact).`
7125
+ );
7126
+ return cloneDefault();
7127
+ }
7128
+ }
7129
+ if (!isSavedViewsFileShape(parsed)) {
7130
+ await backupCorrupt2(path);
7131
+ return cloneDefault();
7132
+ }
7133
+ const normalized = {
7134
+ ...parsed,
7135
+ version: 1,
7136
+ dashboard: { ...parsed.dashboard, version: 1 }
7137
+ };
7138
+ return normalized;
7139
+ }
7140
+ async function backupCorrupt2(path) {
7141
+ const backup = corruptFilePath2();
7142
+ try {
7143
+ await rename4(path, backup);
7144
+ console.warn(`[saved-views] Corrupt file moved to ${backup}; using defaults.`);
7145
+ } catch (err) {
7146
+ console.warn(`[saved-views] Failed to back up corrupt file ${path}: ${String(err)}`);
7147
+ }
7148
+ }
7149
+ async function writeSavedViewsFile(file) {
7150
+ await writeFileForce(savedViewsFile(), `${JSON.stringify(file, null, 2)}
7151
+ `);
7152
+ }
7153
+ function createSavedView(file, input, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
7154
+ const ts = now();
7155
+ const view = {
7156
+ id: randomUUID(),
7157
+ name: input.name,
7158
+ workspace: input.workspace,
7159
+ config: input.config,
7160
+ createdAt: ts,
7161
+ updatedAt: ts
7162
+ };
7163
+ const next = {
7164
+ ...file,
7165
+ views: [...file.views, view]
7166
+ };
7167
+ return { file: next, view };
7168
+ }
7169
+ function updateSavedView(file, id, patch, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
7170
+ const idx = file.views.findIndex((v) => v.id === id);
7171
+ if (idx < 0) return { error: "not-found" };
7172
+ const prev = file.views[idx];
7173
+ const updated = {
7174
+ ...prev,
7175
+ name: patch.name ?? prev.name,
7176
+ workspace: patch.workspace !== void 0 ? patch.workspace : prev.workspace,
7177
+ config: patch.config ?? prev.config,
7178
+ updatedAt: now()
7179
+ };
7180
+ const views = [...file.views];
7181
+ views[idx] = updated;
7182
+ return { file: { ...file, views }, view: updated };
7183
+ }
7184
+ function deleteSavedView(file, id) {
7185
+ const idx = file.views.findIndex((v) => v.id === id);
7186
+ if (idx < 0) return { file, deleted: false };
7187
+ const views = file.views.filter((v) => v.id !== id);
7188
+ const slots = file.dashboard.slots.map((s) => {
7189
+ if (s.widget && s.widget.kind === "saved-view" && s.widget.viewId === id) {
7190
+ return { ...s, widget: null };
7191
+ }
7192
+ return s;
7193
+ });
7194
+ return {
7195
+ file: { ...file, views, dashboard: { ...file.dashboard, slots } },
7196
+ deleted: true
7197
+ };
7198
+ }
7199
+ function setDashboardLayout(file, slots) {
7200
+ const knownIds = new Set(file.views.map((v) => v.id));
7201
+ for (const slot of slots) {
7202
+ if (slot.widget && slot.widget.kind === "saved-view" && !knownIds.has(slot.widget.viewId)) {
7203
+ return { error: "unknown-view-id", viewId: slot.widget.viewId };
7204
+ }
7205
+ }
7206
+ return {
7207
+ file: { ...file, dashboard: { ...file.dashboard, slots } }
7208
+ };
7209
+ }
7210
+
6919
7211
  // src/dashboard/todos-locks.ts
6920
7212
  var writeLocks = /* @__PURE__ */ new Map();
6921
7213
  function withLock(lockKey, fn) {
@@ -6944,18 +7236,199 @@ function withTwoLocks(keyA, keyB, fn) {
6944
7236
  return withLock(first, () => withLock(second, fn));
6945
7237
  }
6946
7238
 
7239
+ // src/dashboard/api-saved-views.ts
7240
+ var SAVED_VIEWS_LOCK = "sv:global";
7241
+ function validateCreateBody(body) {
7242
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
7243
+ return { ok: false, error: "body must be an object with `name`, `workspace`, and `config`" };
7244
+ }
7245
+ const obj = body;
7246
+ if (typeof obj.name !== "string" || obj.name.trim().length === 0 || obj.name.length > 200) {
7247
+ return { ok: false, error: "name must be a non-empty string up to 200 characters" };
7248
+ }
7249
+ if (obj.workspace !== null && (typeof obj.workspace !== "string" || obj.workspace.length === 0)) {
7250
+ return { ok: false, error: "workspace must be a non-empty string or null" };
7251
+ }
7252
+ if (!isSavedViewConfig(obj.config)) {
7253
+ return { ok: false, error: "config must be a valid SavedViewConfig" };
7254
+ }
7255
+ return {
7256
+ ok: true,
7257
+ value: {
7258
+ name: obj.name.trim(),
7259
+ workspace: obj.workspace,
7260
+ config: obj.config
7261
+ }
7262
+ };
7263
+ }
7264
+ function validateUpdateBody(body) {
7265
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
7266
+ return { ok: false, error: "body must be an object" };
7267
+ }
7268
+ const obj = body;
7269
+ const patch = {};
7270
+ if (obj.name !== void 0) {
7271
+ if (typeof obj.name !== "string" || obj.name.trim().length === 0 || obj.name.length > 200) {
7272
+ return { ok: false, error: "name must be a non-empty string up to 200 characters" };
7273
+ }
7274
+ patch.name = obj.name.trim();
7275
+ }
7276
+ if (obj.workspace !== void 0) {
7277
+ if (obj.workspace !== null && (typeof obj.workspace !== "string" || obj.workspace.length === 0)) {
7278
+ return { ok: false, error: "workspace must be a non-empty string or null" };
7279
+ }
7280
+ patch.workspace = obj.workspace;
7281
+ }
7282
+ if (obj.config !== void 0) {
7283
+ if (!isSavedViewConfig(obj.config)) {
7284
+ return { ok: false, error: "config must be a valid SavedViewConfig" };
7285
+ }
7286
+ patch.config = obj.config;
7287
+ }
7288
+ if (Object.keys(patch).length === 0) {
7289
+ return { ok: false, error: "patch must include at least one of name, workspace, or config" };
7290
+ }
7291
+ return { ok: true, value: patch };
7292
+ }
7293
+ function validateDashboardBody(body) {
7294
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
7295
+ return { ok: false, error: "body must be an object with a `slots` array" };
7296
+ }
7297
+ const obj = body;
7298
+ if (!Array.isArray(obj.slots)) {
7299
+ return { ok: false, error: "slots must be an array" };
7300
+ }
7301
+ if (!obj.slots.every(isDashboardSlot)) {
7302
+ return { ok: false, error: "every slot must be { id: string, widget: WidgetConfig | null }" };
7303
+ }
7304
+ return { ok: true, value: obj.slots };
7305
+ }
7306
+ function createSavedViewsRouter() {
7307
+ const router = Router();
7308
+ router.get("/", async (_req, res) => {
7309
+ try {
7310
+ const file = await readSavedViewsFile();
7311
+ res.json(file);
7312
+ } catch (error) {
7313
+ console.error("Error reading saved-views:", error);
7314
+ res.status(500).json({ error: "Failed to read saved-views" });
7315
+ }
7316
+ });
7317
+ router.post("/", async (req, res) => {
7318
+ const result = validateCreateBody(req.body);
7319
+ if (!result.ok) {
7320
+ res.status(400).json({ error: result.error });
7321
+ return;
7322
+ }
7323
+ try {
7324
+ const file = await withLock(SAVED_VIEWS_LOCK, async () => {
7325
+ const current = await readSavedViewsFile();
7326
+ const next = createSavedView(current, result.value);
7327
+ await writeSavedViewsFile(next.file);
7328
+ return next.file;
7329
+ });
7330
+ res.status(201).json(file);
7331
+ } catch (error) {
7332
+ console.error("Error creating saved-view:", error);
7333
+ res.status(500).json({ error: "Failed to create saved-view" });
7334
+ }
7335
+ });
7336
+ router.patch("/:id", async (req, res) => {
7337
+ const idParam = req.params.id;
7338
+ if (typeof idParam !== "string" || idParam.length === 0) {
7339
+ res.status(400).json({ error: "id required" });
7340
+ return;
7341
+ }
7342
+ const result = validateUpdateBody(req.body);
7343
+ if (!result.ok) {
7344
+ res.status(400).json({ error: result.error });
7345
+ return;
7346
+ }
7347
+ try {
7348
+ const outcome = await withLock(SAVED_VIEWS_LOCK, async () => {
7349
+ const current = await readSavedViewsFile();
7350
+ const next = updateSavedView(current, idParam, result.value);
7351
+ if ("error" in next) return { kind: "not-found" };
7352
+ await writeSavedViewsFile(next.file);
7353
+ return { kind: "ok", file: next.file };
7354
+ });
7355
+ if (outcome.kind === "not-found") {
7356
+ res.status(404).json({ error: "view-not-found" });
7357
+ return;
7358
+ }
7359
+ res.json(outcome.file);
7360
+ } catch (error) {
7361
+ console.error("Error updating saved-view:", error);
7362
+ res.status(500).json({ error: "Failed to update saved-view" });
7363
+ }
7364
+ });
7365
+ router.delete("/:id", async (req, res) => {
7366
+ const idParam = req.params.id;
7367
+ if (typeof idParam !== "string" || idParam.length === 0) {
7368
+ res.status(400).json({ error: "id required" });
7369
+ return;
7370
+ }
7371
+ try {
7372
+ const outcome = await withLock(SAVED_VIEWS_LOCK, async () => {
7373
+ const current = await readSavedViewsFile();
7374
+ const next = deleteSavedView(current, idParam);
7375
+ if (!next.deleted) return { kind: "not-found" };
7376
+ await writeSavedViewsFile(next.file);
7377
+ return { kind: "ok", file: next.file };
7378
+ });
7379
+ if (outcome.kind === "not-found") {
7380
+ res.status(404).json({ error: "view-not-found" });
7381
+ return;
7382
+ }
7383
+ res.json(outcome.file);
7384
+ } catch (error) {
7385
+ console.error("Error deleting saved-view:", error);
7386
+ res.status(500).json({ error: "Failed to delete saved-view" });
7387
+ }
7388
+ });
7389
+ return router;
7390
+ }
7391
+ function createDashboardLayoutRouter() {
7392
+ const router = Router();
7393
+ router.put("/", async (req, res) => {
7394
+ const result = validateDashboardBody(req.body);
7395
+ if (!result.ok) {
7396
+ res.status(400).json({ error: result.error });
7397
+ return;
7398
+ }
7399
+ try {
7400
+ const outcome = await withLock(SAVED_VIEWS_LOCK, async () => {
7401
+ const current = await readSavedViewsFile();
7402
+ const next = setDashboardLayout(current, result.value);
7403
+ if ("error" in next) return { kind: "bad-ref", viewId: next.viewId };
7404
+ await writeSavedViewsFile(next.file);
7405
+ return { kind: "ok", file: next.file };
7406
+ });
7407
+ if (outcome.kind === "bad-ref") {
7408
+ res.status(400).json({ error: "unknown-view-id", viewId: outcome.viewId });
7409
+ return;
7410
+ }
7411
+ res.json(outcome.file);
7412
+ } catch (error) {
7413
+ console.error("Error updating dashboard:", error);
7414
+ res.status(500).json({ error: "Failed to update dashboard" });
7415
+ }
7416
+ });
7417
+ return router;
7418
+ }
7419
+
6947
7420
  // src/dashboard/api-write.ts
6948
7421
  init_lifecycle();
6949
7422
  init_slug();
6950
- import { Router } from "express";
6951
- import { resolve as resolve17, basename as basename3, isAbsolute as isAbsolute2 } from "path";
6952
- import { rm, readFile as readFile14, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
7423
+ import { Router as Router2 } from "express";
7424
+ 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";
6953
7426
  import { spawnSync as spawnSync3 } from "child_process";
6954
7427
 
6955
7428
  // src/utils/uuid.ts
6956
- import { randomUUID } from "crypto";
7429
+ import { randomUUID as randomUUID2 } from "crypto";
6957
7430
  function generateId() {
6958
- return randomUUID();
7431
+ return randomUUID2();
6959
7432
  }
6960
7433
 
6961
7434
  // src/dashboard/api-write.ts
@@ -6966,7 +7439,7 @@ init_fs();
6966
7439
  init_frontmatter();
6967
7440
  init_fs();
6968
7441
  import { spawn } from "child_process";
6969
- import { readFile as readFile12 } from "fs/promises";
7442
+ import { readFile as readFile13 } from "fs/promises";
6970
7443
  function run(command, args, cwd) {
6971
7444
  return new Promise((resolvePromise) => {
6972
7445
  const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -7016,7 +7489,7 @@ async function createWorktreeAndRecord(opts) {
7016
7489
  const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
7017
7490
  await createWorktree({ repository, branch, worktreePath, parentBranch });
7018
7491
  try {
7019
- const content = await readFile12(assignmentPath, "utf-8");
7492
+ const content = await readFile13(assignmentPath, "utf-8");
7020
7493
  const updated = updateAssignmentWorkspace(content, {
7021
7494
  repository,
7022
7495
  worktreePath,
@@ -7041,29 +7514,30 @@ async function createWorktreeAndRecord(opts) {
7041
7514
  }
7042
7515
  function formatRollbackError(opts) {
7043
7516
  const { writeMsg, worktreePath, branch, worktreeCleanup, branchCleanup } = opts;
7517
+ const subject = opts.subject ?? "assignment frontmatter";
7044
7518
  const wtMsg = worktreeCleanup.stderr.trim() || "(no stderr)";
7045
7519
  const brMsg = branchCleanup.stderr.trim() || "(no stderr)";
7046
7520
  if (!worktreeCleanup.ok && !branchCleanup.ok) {
7047
- return `Failed to update assignment frontmatter AND failed to clean up both worktree and branch. Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Branch cleanup error: ${brMsg}. Orphan worktree at ${worktreePath} and orphan branch "${branch}" \u2014 remove them manually.`;
7521
+ return `Failed to update ${subject} AND failed to clean up both worktree and branch. Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Branch cleanup error: ${brMsg}. Orphan worktree at ${worktreePath} and orphan branch "${branch}" \u2014 remove them manually.`;
7048
7522
  }
7049
7523
  if (!worktreeCleanup.ok) {
7050
- return `Failed to update assignment frontmatter AND failed to clean up worktree. Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Orphan worktree at ${worktreePath} \u2014 remove it manually.`;
7524
+ return `Failed to update ${subject} AND failed to clean up worktree. Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Orphan worktree at ${worktreePath} \u2014 remove it manually.`;
7051
7525
  }
7052
7526
  if (!branchCleanup.ok) {
7053
- return `Failed to update assignment frontmatter: ${writeMsg}. Rolled back git worktree at ${worktreePath}, but could not delete branch "${branch}": ${brMsg}. Remove the branch manually.`;
7527
+ return `Failed to update ${subject}: ${writeMsg}. Rolled back git worktree at ${worktreePath}, but could not delete branch "${branch}": ${brMsg}. Remove the branch manually.`;
7054
7528
  }
7055
- return `Failed to update assignment frontmatter: ${writeMsg}. Rolled back git worktree at ${worktreePath} and branch "${branch}".`;
7529
+ return `Failed to update ${subject}: ${writeMsg}. Rolled back git worktree at ${worktreePath} and branch "${branch}".`;
7056
7530
  }
7057
7531
 
7058
7532
  // src/utils/worktree-defaults.ts
7059
7533
  init_paths();
7060
7534
  import { spawnSync as spawnSync2 } from "child_process";
7061
- import { resolve as resolve15 } from "path";
7535
+ import { resolve as resolve16 } from "path";
7062
7536
  function computeWorktreeDefaults(opts) {
7063
7537
  const repository = opts.existing.repository ?? detectCurrentGitRoot(opts.cwd);
7064
7538
  const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
7065
7539
  const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch(opts.cwd) ?? "main";
7066
- const worktreeBase = repository ? resolve15(repository, ".worktrees", branch) : resolve15(
7540
+ const worktreeBase = repository ? resolve16(repository, ".worktrees", branch) : resolve16(
7067
7541
  syntaurRoot(),
7068
7542
  "worktrees",
7069
7543
  opts.projectSlug || "standalone",
@@ -7099,34 +7573,34 @@ function detectCurrentBranch(cwd) {
7099
7573
  // src/dashboard/repository-candidates.ts
7100
7574
  init_fs();
7101
7575
  init_parser();
7102
- import { readdir as readdir9, readFile as readFile13 } from "fs/promises";
7103
- import { resolve as resolve16 } from "path";
7576
+ import { readdir as readdir9, readFile as readFile14 } from "fs/promises";
7577
+ import { resolve as resolve17 } from "path";
7104
7578
  async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
7105
7579
  const seen = /* @__PURE__ */ new Set();
7106
7580
  const out = [];
7107
- const projectPath = resolve16(projectsDir, projectSlug, "project.md");
7581
+ const projectPath = resolve17(projectsDir, projectSlug, "project.md");
7108
7582
  if (await fileExists(projectPath)) {
7109
- const project = parseProject(await readFile13(projectPath, "utf-8"));
7583
+ const project = parseProject(await readFile14(projectPath, "utf-8"));
7110
7584
  for (const raw of project.repositories) {
7111
7585
  const path = raw.trim();
7112
7586
  if (!path) continue;
7113
- const abs = resolve16(path);
7587
+ const abs = resolve17(path);
7114
7588
  if (seen.has(abs)) continue;
7115
7589
  seen.add(abs);
7116
7590
  out.push({ path: abs, source: "project", sourceAssignmentSlug: null });
7117
7591
  }
7118
7592
  }
7119
- const assignmentsDir2 = resolve16(projectsDir, projectSlug, "assignments");
7593
+ const assignmentsDir2 = resolve17(projectsDir, projectSlug, "assignments");
7120
7594
  if (await fileExists(assignmentsDir2)) {
7121
7595
  const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
7122
7596
  for (const entry of entries) {
7123
7597
  if (!entry.isDirectory()) continue;
7124
- const assignmentMd = resolve16(assignmentsDir2, entry.name, "assignment.md");
7598
+ const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
7125
7599
  if (!await fileExists(assignmentMd)) continue;
7126
- const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
7600
+ const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
7127
7601
  const repo = parsed.workspace.repository?.trim();
7128
7602
  if (!repo) continue;
7129
- const abs = resolve16(repo);
7603
+ const abs = resolve17(repo);
7130
7604
  if (seen.has(abs)) continue;
7131
7605
  seen.add(abs);
7132
7606
  out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
@@ -7144,12 +7618,12 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
7144
7618
  for (const entry of entries) {
7145
7619
  if (!entry.isDirectory()) continue;
7146
7620
  if (entry.name === excludeAssignmentId) continue;
7147
- const assignmentMd = resolve16(assignmentsDir2, entry.name, "assignment.md");
7621
+ const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
7148
7622
  if (!await fileExists(assignmentMd)) continue;
7149
- const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
7623
+ const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
7150
7624
  const repo = parsed.workspace.repository?.trim();
7151
7625
  if (!repo) continue;
7152
- const abs = resolve16(repo);
7626
+ const abs = resolve17(repo);
7153
7627
  if (seen.has(abs)) continue;
7154
7628
  seen.add(abs);
7155
7629
  out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
@@ -7712,14 +8186,14 @@ async function readCurrentDocument(filePath) {
7712
8186
  if (!await fileExists(filePath)) {
7713
8187
  return null;
7714
8188
  }
7715
- return readFile14(filePath, "utf-8");
8189
+ return readFile15(filePath, "utf-8");
7716
8190
  }
7717
8191
  async function handleWorktreeCreate(req, res, ctx) {
7718
8192
  if (!await fileExists(ctx.assignmentPath)) {
7719
8193
  res.status(404).json({ error: "Assignment not found" });
7720
8194
  return;
7721
8195
  }
7722
- const parsed = parseAssignmentFull(await readFile14(ctx.assignmentPath, "utf-8"));
8196
+ const parsed = parseAssignmentFull(await readFile15(ctx.assignmentPath, "utf-8"));
7723
8197
  if (parsed.workspace.worktreePath) {
7724
8198
  res.status(409).json({ error: "Worktree already configured for this assignment" });
7725
8199
  return;
@@ -7769,7 +8243,7 @@ async function handleWorktreeCreate(req, res, ctx) {
7769
8243
  });
7770
8244
  const branch = typeof bodyBranch === "string" && bodyBranch.trim() ? bodyBranch.trim() : defaults.branch;
7771
8245
  const parentBranch = typeof bodyParent === "string" && bodyParent.trim() ? bodyParent.trim() : defaults.parentBranch;
7772
- const worktreePath = resolve17(repository, ".worktrees", branch);
8246
+ const worktreePath = resolve18(repository, ".worktrees", branch);
7773
8247
  try {
7774
8248
  await fsStat(worktreePath);
7775
8249
  res.status(409).json({
@@ -7810,7 +8284,7 @@ async function handleWorktreeCreate(req, res, ctx) {
7810
8284
  }
7811
8285
  function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
7812
8286
  const linkedTodosLookup = todosDir2 ? { todosDir: todosDir2, projectsDir } : void 0;
7813
- const router = Router();
8287
+ const router = Router2();
7814
8288
  router.get("/api/templates/project", (_req, res) => {
7815
8289
  const content = renderProject({
7816
8290
  id: generateId(),
@@ -8035,9 +8509,9 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
8035
8509
  });
8036
8510
  return;
8037
8511
  }
8038
- const folderPath = resolve17(projectDir, folder);
8512
+ const folderPath = resolve18(projectDir, folder);
8039
8513
  await ensureDir(folderPath);
8040
- const filePath = resolve17(folderPath, `${requestedSlug}.md`);
8514
+ const filePath = resolve18(folderPath, `${requestedSlug}.md`);
8041
8515
  const timestamp = nowTimestamp();
8042
8516
  let content = renderItemStub(kind, {
8043
8517
  slug: requestedSlug,
@@ -8081,14 +8555,14 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
8081
8555
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
8082
8556
  return;
8083
8557
  }
8084
- const filePath = resolve17(projectDir, folder, `${itemSlug}.md`);
8558
+ const filePath = resolve18(projectDir, folder, `${itemSlug}.md`);
8085
8559
  if (!await fileExists(filePath)) {
8086
8560
  res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
8087
8561
  return;
8088
8562
  }
8089
8563
  const nextContentRaw = requireContent(req, res);
8090
8564
  if (!nextContentRaw) return;
8091
- const currentContent = await readFile14(filePath, "utf-8");
8565
+ const currentContent = await readFile15(filePath, "utf-8");
8092
8566
  const frontmatterBlock = extractFrontmatterBlock(currentContent);
8093
8567
  if (!frontmatterBlock) {
8094
8568
  res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
@@ -8117,7 +8591,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8117
8591
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
8118
8592
  return;
8119
8593
  }
8120
- const filePath = resolve17(projectDir, folder, `${itemSlug}.md`);
8594
+ const filePath = resolve18(projectDir, folder, `${itemSlug}.md`);
8121
8595
  if (!await fileExists(filePath)) {
8122
8596
  res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
8123
8597
  return;
@@ -8151,26 +8625,26 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8151
8625
  res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
8152
8626
  return;
8153
8627
  }
8154
- const projectDir = resolve17(projectsDir, slug);
8628
+ const projectDir = resolve18(projectsDir, slug);
8155
8629
  if (await fileExists(projectDir)) {
8156
8630
  res.status(409).json({ error: `Project "${slug}" already exists` });
8157
8631
  return;
8158
8632
  }
8159
8633
  const title = fields.title;
8160
8634
  const timestamp = fields.created || nowTimestamp();
8161
- await ensureDir(resolve17(projectDir, "assignments"));
8162
- await ensureDir(resolve17(projectDir, "resources"));
8163
- await ensureDir(resolve17(projectDir, "memories"));
8164
- await writeFileForce(resolve17(projectDir, "project.md"), content);
8635
+ await ensureDir(resolve18(projectDir, "assignments"));
8636
+ await ensureDir(resolve18(projectDir, "resources"));
8637
+ await ensureDir(resolve18(projectDir, "memories"));
8638
+ await writeFileForce(resolve18(projectDir, "project.md"), content);
8165
8639
  try {
8166
8640
  const companions = [
8167
- [resolve17(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
8168
- [resolve17(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
8169
- [resolve17(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
8170
- [resolve17(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
8171
- [resolve17(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
8172
- [resolve17(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
8173
- [resolve17(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
8641
+ [resolve18(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
8642
+ [resolve18(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
8643
+ [resolve18(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
8644
+ [resolve18(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
8645
+ [resolve18(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
8646
+ [resolve18(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
8647
+ [resolve18(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
8174
8648
  ];
8175
8649
  for (const [filePath, fileContent] of companions) {
8176
8650
  await writeFileForce(filePath, fileContent);
@@ -8191,8 +8665,8 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8191
8665
  router.post("/api/projects/:slug/assignments", async (req, res) => {
8192
8666
  try {
8193
8667
  const projectSlug = getParam(req.params.slug);
8194
- const projectDir = resolve17(projectsDir, projectSlug);
8195
- const projectMdPath = resolve17(projectDir, "project.md");
8668
+ const projectDir = resolve18(projectsDir, projectSlug);
8669
+ const projectMdPath = resolve18(projectDir, "project.md");
8196
8670
  if (!await fileExists(projectMdPath)) {
8197
8671
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
8198
8672
  return;
@@ -8222,7 +8696,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8222
8696
  res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
8223
8697
  return;
8224
8698
  }
8225
- const assignmentDir = resolve17(projectDir, "assignments", assignmentSlug);
8699
+ const assignmentDir = resolve18(projectDir, "assignments", assignmentSlug);
8226
8700
  if (await fileExists(assignmentDir)) {
8227
8701
  res.status(409).json({
8228
8702
  error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
@@ -8231,12 +8705,12 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8231
8705
  }
8232
8706
  const timestamp = fields.created || nowTimestamp();
8233
8707
  await ensureDir(assignmentDir);
8234
- await writeFileForce(resolve17(assignmentDir, "assignment.md"), content);
8708
+ await writeFileForce(resolve18(assignmentDir, "assignment.md"), content);
8235
8709
  try {
8236
8710
  const companions = [
8237
- [resolve17(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
8238
- [resolve17(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
8239
- [resolve17(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
8711
+ [resolve18(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
8712
+ [resolve18(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
8713
+ [resolve18(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
8240
8714
  ];
8241
8715
  for (const [filePath, fileContent] of companions) {
8242
8716
  await writeFileForce(filePath, fileContent);
@@ -8257,7 +8731,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8257
8731
  router.patch("/api/projects/:slug", async (req, res) => {
8258
8732
  try {
8259
8733
  const projectSlug = getParam(req.params.slug);
8260
- const projectPath = resolve17(projectsDir, projectSlug, "project.md");
8734
+ const projectPath = resolve18(projectsDir, projectSlug, "project.md");
8261
8735
  const currentContent = await readCurrentDocument(projectPath);
8262
8736
  if (!currentContent) {
8263
8737
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
@@ -8290,7 +8764,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8290
8764
  try {
8291
8765
  const projectSlug = getParam(req.params.slug);
8292
8766
  const assignmentSlug = getParam(req.params.aslug);
8293
- const assignmentPath = resolve17(
8767
+ const assignmentPath = resolve18(
8294
8768
  projectsDir,
8295
8769
  projectSlug,
8296
8770
  "assignments",
@@ -8333,7 +8807,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8333
8807
  try {
8334
8808
  const projectSlug = getParam(req.params.slug);
8335
8809
  const assignmentSlug = getParam(req.params.aslug);
8336
- const assignmentPath = resolve17(
8810
+ const assignmentPath = resolve18(
8337
8811
  projectsDir,
8338
8812
  projectSlug,
8339
8813
  "assignments",
@@ -8369,7 +8843,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8369
8843
  try {
8370
8844
  const projectSlug = getParam(req.params.slug);
8371
8845
  const assignmentSlug = getParam(req.params.aslug);
8372
- const planPath = resolve17(
8846
+ const planPath = resolve18(
8373
8847
  projectsDir,
8374
8848
  projectSlug,
8375
8849
  "assignments",
@@ -8407,7 +8881,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8407
8881
  try {
8408
8882
  const projectSlug = getParam(req.params.slug);
8409
8883
  const assignmentSlug = getParam(req.params.aslug);
8410
- const scratchpadPath = resolve17(
8884
+ const scratchpadPath = resolve18(
8411
8885
  projectsDir,
8412
8886
  projectSlug,
8413
8887
  "assignments",
@@ -8445,7 +8919,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8445
8919
  try {
8446
8920
  const projectSlug = getParam(req.params.slug);
8447
8921
  const assignmentSlug = getParam(req.params.aslug);
8448
- const handoffPath = resolve17(
8922
+ const handoffPath = resolve18(
8449
8923
  projectsDir,
8450
8924
  projectSlug,
8451
8925
  "assignments",
@@ -8483,7 +8957,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8483
8957
  try {
8484
8958
  const projectSlug = getParam(req.params.slug);
8485
8959
  const assignmentSlug = getParam(req.params.aslug);
8486
- const decisionPath = resolve17(
8960
+ const decisionPath = resolve18(
8487
8961
  projectsDir,
8488
8962
  projectSlug,
8489
8963
  "assignments",
@@ -8521,7 +8995,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8521
8995
  try {
8522
8996
  const projectSlug = getParam(req.params.slug);
8523
8997
  const assignmentSlug = getParam(req.params.aslug);
8524
- const commentsPath = resolve17(
8998
+ const commentsPath = resolve18(
8525
8999
  projectsDir,
8526
9000
  projectSlug,
8527
9001
  "assignments",
@@ -8539,7 +9013,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8539
9013
  let currentContent;
8540
9014
  let currentCount = 0;
8541
9015
  if (await fileExists(commentsPath)) {
8542
- currentContent = await readFile14(commentsPath, "utf-8");
9016
+ currentContent = await readFile15(commentsPath, "utf-8");
8543
9017
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
8544
9018
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
8545
9019
  } else {
@@ -8580,7 +9054,7 @@ ${entry}`;
8580
9054
  const projectSlug = getParam(req.params.slug);
8581
9055
  const assignmentSlug = getParam(req.params.aslug);
8582
9056
  const commentId = getParam(req.params.commentId);
8583
- const commentsPath = resolve17(
9057
+ const commentsPath = resolve18(
8584
9058
  projectsDir,
8585
9059
  projectSlug,
8586
9060
  "assignments",
@@ -8596,7 +9070,7 @@ ${entry}`;
8596
9070
  res.status(400).json({ error: "resolved (boolean) is required" });
8597
9071
  return;
8598
9072
  }
8599
- const content = await readFile14(commentsPath, "utf-8");
9073
+ const content = await readFile15(commentsPath, "utf-8");
8600
9074
  const parsed = parseComments(content);
8601
9075
  const target = parsed.entries.find((e) => e.id === commentId);
8602
9076
  if (!target) {
@@ -8631,7 +9105,7 @@ ${entry}`;
8631
9105
  router.post("/api/projects/:slug/move-workspace", async (req, res) => {
8632
9106
  try {
8633
9107
  const projectSlug = getParam(req.params.slug);
8634
- const projectPath = resolve17(projectsDir, projectSlug, "project.md");
9108
+ const projectPath = resolve18(projectsDir, projectSlug, "project.md");
8635
9109
  if (!await fileExists(projectPath)) {
8636
9110
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
8637
9111
  return;
@@ -8643,7 +9117,7 @@ ${entry}`;
8643
9117
  });
8644
9118
  return;
8645
9119
  }
8646
- let content = await readFile14(projectPath, "utf-8");
9120
+ let content = await readFile15(projectPath, "utf-8");
8647
9121
  content = setTopLevelField(content, "workspace", workspace ?? null);
8648
9122
  content = setTopLevelField(content, "updated", nowTimestamp());
8649
9123
  await writeFileForce(projectPath, content);
@@ -8679,8 +9153,8 @@ ${entry}`;
8679
9153
  });
8680
9154
  return;
8681
9155
  }
8682
- const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8683
- let content = await readFile14(assignmentPath, "utf-8");
9156
+ const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9157
+ let content = await readFile15(assignmentPath, "utf-8");
8684
9158
  content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
8685
9159
  content = setTopLevelField(content, "updated", nowTimestamp());
8686
9160
  await writeFileForce(assignmentPath, content);
@@ -8696,7 +9170,7 @@ ${entry}`;
8696
9170
  async (req, res) => {
8697
9171
  try {
8698
9172
  const projectSlug = getParam(req.params.slug);
8699
- const projectPath = resolve17(projectsDir, projectSlug, "project.md");
9173
+ const projectPath = resolve18(projectsDir, projectSlug, "project.md");
8700
9174
  if (!await fileExists(projectPath)) {
8701
9175
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
8702
9176
  return;
@@ -8741,7 +9215,7 @@ ${entry}`;
8741
9215
  try {
8742
9216
  const projectSlug = getParam(req.params.slug);
8743
9217
  const assignmentSlug = getParam(req.params.aslug);
8744
- const assignmentPath = resolve17(
9218
+ const assignmentPath = resolve18(
8745
9219
  projectsDir,
8746
9220
  projectSlug,
8747
9221
  "assignments",
@@ -8774,8 +9248,8 @@ ${entry}`;
8774
9248
  res.status(404).json({ error: `Assignment "${id}" not found` });
8775
9249
  return;
8776
9250
  }
8777
- const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8778
- const parsedForSlug = parseAssignmentFull(await readFile14(assignmentPath, "utf-8"));
9251
+ const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9252
+ const parsedForSlug = parseAssignmentFull(await readFile15(assignmentPath, "utf-8"));
8779
9253
  const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
8780
9254
  await handleWorktreeCreate(req, res, {
8781
9255
  assignmentPath,
@@ -8792,7 +9266,7 @@ ${entry}`;
8792
9266
  router.post("/api/projects/:slug/status-override", async (req, res) => {
8793
9267
  try {
8794
9268
  const projectSlug = getParam(req.params.slug);
8795
- const projectPath = resolve17(projectsDir, projectSlug, "project.md");
9269
+ const projectPath = resolve18(projectsDir, projectSlug, "project.md");
8796
9270
  if (!await fileExists(projectPath)) {
8797
9271
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
8798
9272
  return;
@@ -8804,7 +9278,7 @@ ${entry}`;
8804
9278
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
8805
9279
  return;
8806
9280
  }
8807
- let content = await readFile14(projectPath, "utf-8");
9281
+ let content = await readFile15(projectPath, "utf-8");
8808
9282
  content = setTopLevelField(content, "statusOverride", status ?? null);
8809
9283
  content = setTopLevelField(content, "updated", nowTimestamp());
8810
9284
  await writeFileForce(projectPath, content);
@@ -8819,7 +9293,7 @@ ${entry}`;
8819
9293
  try {
8820
9294
  const projectSlug = getParam(req.params.slug);
8821
9295
  const assignmentSlug = getParam(req.params.aslug);
8822
- const assignmentPath = resolve17(
9296
+ const assignmentPath = resolve18(
8823
9297
  projectsDir,
8824
9298
  projectSlug,
8825
9299
  "assignments",
@@ -8837,7 +9311,7 @@ ${entry}`;
8837
9311
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
8838
9312
  return;
8839
9313
  }
8840
- let content = await readFile14(assignmentPath, "utf-8");
9314
+ let content = await readFile15(assignmentPath, "utf-8");
8841
9315
  content = setTopLevelField(content, "status", status);
8842
9316
  content = setTopLevelField(content, "updated", nowTimestamp());
8843
9317
  if (status !== "blocked") {
@@ -8855,7 +9329,7 @@ ${entry}`;
8855
9329
  try {
8856
9330
  const projectSlug = getParam(req.params.slug);
8857
9331
  const assignmentSlug = getParam(req.params.aslug);
8858
- const assignmentPath = resolve17(
9332
+ const assignmentPath = resolve18(
8859
9333
  projectsDir,
8860
9334
  projectSlug,
8861
9335
  "assignments",
@@ -8871,7 +9345,7 @@ ${entry}`;
8871
9345
  res.status(400).json({ error: validation.error });
8872
9346
  return;
8873
9347
  }
8874
- let content = await readFile14(assignmentPath, "utf-8");
9348
+ let content = await readFile15(assignmentPath, "utf-8");
8875
9349
  content = setTopLevelField(content, "assignee", validation.value);
8876
9350
  content = setTopLevelField(content, "updated", nowTimestamp());
8877
9351
  await writeFileForce(assignmentPath, content);
@@ -8882,6 +9356,37 @@ ${entry}`;
8882
9356
  res.status(500).json({ error: `Failed to update assignee: ${error.message}` });
8883
9357
  }
8884
9358
  });
9359
+ router.patch("/api/projects/:slug/assignments/:aslug/title", async (req, res) => {
9360
+ try {
9361
+ const projectSlug = getParam(req.params.slug);
9362
+ const assignmentSlug = getParam(req.params.aslug);
9363
+ const assignmentPath = resolve18(
9364
+ projectsDir,
9365
+ projectSlug,
9366
+ "assignments",
9367
+ assignmentSlug,
9368
+ "assignment.md"
9369
+ );
9370
+ if (!await fileExists(assignmentPath)) {
9371
+ res.status(404).json({ error: "Assignment not found" });
9372
+ return;
9373
+ }
9374
+ const validation = validateTitleBody(req.body);
9375
+ if (!validation.ok) {
9376
+ res.status(400).json({ error: validation.error });
9377
+ return;
9378
+ }
9379
+ let content = await readFile15(assignmentPath, "utf-8");
9380
+ content = setTopLevelField(content, "title", validation.value);
9381
+ content = setTopLevelField(content, "updated", nowTimestamp());
9382
+ await writeFileForce(assignmentPath, content);
9383
+ const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
9384
+ res.json({ assignment });
9385
+ } catch (error) {
9386
+ console.error("Error updating title:", error);
9387
+ res.status(500).json({ error: `Failed to update title: ${error.message}` });
9388
+ }
9389
+ });
8885
9390
  router.post("/api/projects/:slug/assignments/:aslug/transitions/:command", async (req, res) => {
8886
9391
  try {
8887
9392
  const projectSlug = getParam(req.params.slug);
@@ -8893,8 +9398,8 @@ ${entry}`;
8893
9398
  res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
8894
9399
  return;
8895
9400
  }
8896
- const projectDir = resolve17(projectsDir, projectSlug);
8897
- const assignmentPath = resolve17(projectDir, "assignments", assignmentSlug, "assignment.md");
9401
+ const projectDir = resolve18(projectsDir, projectSlug);
9402
+ const assignmentPath = resolve18(projectDir, "assignments", assignmentSlug, "assignment.md");
8898
9403
  if (!await fileExists(assignmentPath)) {
8899
9404
  res.status(404).json({ error: "Assignment not found" });
8900
9405
  return;
@@ -8921,8 +9426,8 @@ ${entry}`;
8921
9426
  try {
8922
9427
  const projectSlug = getParam(req.params.slug);
8923
9428
  const assignmentSlug = getParam(req.params.aslug);
8924
- const assignmentDir = resolve17(projectsDir, projectSlug, "assignments", assignmentSlug);
8925
- const assignmentPath = resolve17(assignmentDir, "assignment.md");
9429
+ const assignmentDir = resolve18(projectsDir, projectSlug, "assignments", assignmentSlug);
9430
+ const assignmentPath = resolve18(assignmentDir, "assignment.md");
8926
9431
  if (!await fileExists(assignmentPath)) {
8927
9432
  res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
8928
9433
  return;
@@ -8977,7 +9482,7 @@ ${entry}`;
8977
9482
  return;
8978
9483
  }
8979
9484
  const id2 = generateId();
8980
- const assignmentDir2 = resolve17(assignmentsDir2, id2);
9485
+ const assignmentDir2 = resolve18(assignmentsDir2, id2);
8981
9486
  if (await fileExists(assignmentDir2)) {
8982
9487
  res.status(500).json({ error: "UUID collision \u2014 try again" });
8983
9488
  return;
@@ -8985,25 +9490,25 @@ ${entry}`;
8985
9490
  const timestamp2 = fields.created || nowTimestamp();
8986
9491
  await ensureDir(assignmentDir2);
8987
9492
  const normalizedContent = setTopLevelField(rawContent, "id", id2);
8988
- await writeFileForce(resolve17(assignmentDir2, "assignment.md"), normalizedContent);
9493
+ await writeFileForce(resolve18(assignmentDir2, "assignment.md"), normalizedContent);
8989
9494
  await writeFileForce(
8990
- resolve17(assignmentDir2, "scratchpad.md"),
9495
+ resolve18(assignmentDir2, "scratchpad.md"),
8991
9496
  renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
8992
9497
  );
8993
9498
  await writeFileForce(
8994
- resolve17(assignmentDir2, "handoff.md"),
9499
+ resolve18(assignmentDir2, "handoff.md"),
8995
9500
  renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
8996
9501
  );
8997
9502
  await writeFileForce(
8998
- resolve17(assignmentDir2, "decision-record.md"),
9503
+ resolve18(assignmentDir2, "decision-record.md"),
8999
9504
  renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
9000
9505
  );
9001
9506
  await writeFileForce(
9002
- resolve17(assignmentDir2, "progress.md"),
9507
+ resolve18(assignmentDir2, "progress.md"),
9003
9508
  renderProgress({ assignment: id2, timestamp: timestamp2 })
9004
9509
  );
9005
9510
  await writeFileForce(
9006
- resolve17(assignmentDir2, "comments.md"),
9511
+ resolve18(assignmentDir2, "comments.md"),
9007
9512
  renderComments({ assignment: id2, timestamp: timestamp2 })
9008
9513
  );
9009
9514
  const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir2, id2);
@@ -9021,7 +9526,7 @@ ${entry}`;
9021
9526
  return;
9022
9527
  }
9023
9528
  const id = generateId();
9024
- const assignmentDir = resolve17(assignmentsDir2, id);
9529
+ const assignmentDir = resolve18(assignmentsDir2, id);
9025
9530
  if (await fileExists(assignmentDir)) {
9026
9531
  res.status(500).json({ error: "UUID collision \u2014 try again" });
9027
9532
  return;
@@ -9041,25 +9546,25 @@ ${entry}`;
9041
9546
  project: null,
9042
9547
  type: typeof type === "string" ? type : void 0
9043
9548
  });
9044
- await writeFileForce(resolve17(assignmentDir, "assignment.md"), assignmentContent);
9549
+ await writeFileForce(resolve18(assignmentDir, "assignment.md"), assignmentContent);
9045
9550
  await writeFileForce(
9046
- resolve17(assignmentDir, "scratchpad.md"),
9551
+ resolve18(assignmentDir, "scratchpad.md"),
9047
9552
  renderScratchpad({ assignmentSlug: id, timestamp })
9048
9553
  );
9049
9554
  await writeFileForce(
9050
- resolve17(assignmentDir, "handoff.md"),
9555
+ resolve18(assignmentDir, "handoff.md"),
9051
9556
  renderHandoff({ assignmentSlug: id, timestamp })
9052
9557
  );
9053
9558
  await writeFileForce(
9054
- resolve17(assignmentDir, "decision-record.md"),
9559
+ resolve18(assignmentDir, "decision-record.md"),
9055
9560
  renderDecisionRecord({ assignmentSlug: id, timestamp })
9056
9561
  );
9057
9562
  await writeFileForce(
9058
- resolve17(assignmentDir, "progress.md"),
9563
+ resolve18(assignmentDir, "progress.md"),
9059
9564
  renderProgress({ assignment: id, timestamp })
9060
9565
  );
9061
9566
  await writeFileForce(
9062
- resolve17(assignmentDir, "comments.md"),
9567
+ resolve18(assignmentDir, "comments.md"),
9063
9568
  renderComments({ assignment: id, timestamp })
9064
9569
  );
9065
9570
  const detail = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
@@ -9187,7 +9692,7 @@ ${entry}`;
9187
9692
  res.status(404).json({ error: `Assignment "${id}" not found` });
9188
9693
  return;
9189
9694
  }
9190
- const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
9695
+ const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9191
9696
  const currentContent = await readCurrentDocument(assignmentPath);
9192
9697
  if (!currentContent) {
9193
9698
  res.status(404).json({ error: "Assignment not found" });
@@ -9229,7 +9734,7 @@ ${entry}`;
9229
9734
  res.status(404).json({ error: `Assignment "${id}" not found` });
9230
9735
  return;
9231
9736
  }
9232
- const planPath = resolve17(resolved.assignmentDir, "plan.md");
9737
+ const planPath = resolve18(resolved.assignmentDir, "plan.md");
9233
9738
  const currentContent = await readCurrentDocument(planPath);
9234
9739
  if (!currentContent) {
9235
9740
  res.status(404).json({ error: "Plan not found" });
@@ -9263,7 +9768,7 @@ ${entry}`;
9263
9768
  res.status(404).json({ error: `Assignment "${id}" not found` });
9264
9769
  return;
9265
9770
  }
9266
- const scratchpadPath = resolve17(resolved.assignmentDir, "scratchpad.md");
9771
+ const scratchpadPath = resolve18(resolved.assignmentDir, "scratchpad.md");
9267
9772
  const currentContent = await readCurrentDocument(scratchpadPath);
9268
9773
  if (!currentContent) {
9269
9774
  res.status(404).json({ error: "Scratchpad not found" });
@@ -9297,7 +9802,7 @@ ${entry}`;
9297
9802
  res.status(404).json({ error: `Assignment "${id}" not found` });
9298
9803
  return;
9299
9804
  }
9300
- const handoffPath = resolve17(resolved.assignmentDir, "handoff.md");
9805
+ const handoffPath = resolve18(resolved.assignmentDir, "handoff.md");
9301
9806
  const currentContent = await readCurrentDocument(handoffPath);
9302
9807
  if (!currentContent) {
9303
9808
  res.status(404).json({ error: "Handoff log not found" });
@@ -9337,7 +9842,7 @@ ${entry}`;
9337
9842
  res.status(404).json({ error: `Assignment "${id}" not found` });
9338
9843
  return;
9339
9844
  }
9340
- const decisionPath = resolve17(resolved.assignmentDir, "decision-record.md");
9845
+ const decisionPath = resolve18(resolved.assignmentDir, "decision-record.md");
9341
9846
  const currentContent = await readCurrentDocument(decisionPath);
9342
9847
  if (!currentContent) {
9343
9848
  res.status(404).json({ error: "Decision record not found" });
@@ -9377,7 +9882,7 @@ ${entry}`;
9377
9882
  res.status(404).json({ error: `Assignment "${id}" not found` });
9378
9883
  return;
9379
9884
  }
9380
- const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
9885
+ const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9381
9886
  if (!await fileExists(assignmentPath)) {
9382
9887
  res.status(404).json({ error: "Assignment not found" });
9383
9888
  return;
@@ -9389,7 +9894,7 @@ ${entry}`;
9389
9894
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
9390
9895
  return;
9391
9896
  }
9392
- let content = await readFile14(assignmentPath, "utf-8");
9897
+ let content = await readFile15(assignmentPath, "utf-8");
9393
9898
  content = setTopLevelField(content, "status", status);
9394
9899
  content = setTopLevelField(content, "updated", nowTimestamp());
9395
9900
  if (status !== "blocked") {
@@ -9415,7 +9920,7 @@ ${entry}`;
9415
9920
  res.status(404).json({ error: `Assignment "${id}" not found` });
9416
9921
  return;
9417
9922
  }
9418
- const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
9923
+ const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9419
9924
  if (!await fileExists(assignmentPath)) {
9420
9925
  res.status(404).json({ error: "Assignment not found" });
9421
9926
  return;
@@ -9425,7 +9930,7 @@ ${entry}`;
9425
9930
  res.status(400).json({ error: validation.error });
9426
9931
  return;
9427
9932
  }
9428
- let content = await readFile14(assignmentPath, "utf-8");
9933
+ let content = await readFile15(assignmentPath, "utf-8");
9429
9934
  content = setTopLevelField(content, "assignee", validation.value);
9430
9935
  content = setTopLevelField(content, "updated", nowTimestamp());
9431
9936
  await writeFileForce(assignmentPath, content);
@@ -9436,29 +9941,62 @@ ${entry}`;
9436
9941
  res.status(500).json({ error: `Failed to update assignee: ${error.message}` });
9437
9942
  }
9438
9943
  });
9439
- router.post("/api/assignments/bulk-status-override", async (req, res) => {
9944
+ router.patch("/api/assignments/:id/title", async (req, res) => {
9440
9945
  try {
9441
- const body = req.body;
9442
- if (!body || typeof body !== "object" || !Array.isArray(body.items)) {
9443
- res.status(400).json({ error: "Request body must include `items` (array)." });
9946
+ if (!assignmentsDir2) {
9947
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
9444
9948
  return;
9445
9949
  }
9446
- const items = body.items;
9447
- if (items.length === 0) {
9448
- res.status(400).json({ error: "`items` must contain at least one entry." });
9950
+ const id = getParam(req.params.id);
9951
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
9952
+ if (!resolved) {
9953
+ res.status(404).json({ error: `Assignment "${id}" not found` });
9449
9954
  return;
9450
9955
  }
9451
- if (items.length > 200) {
9452
- res.status(400).json({ error: "`items` is capped at 200 entries per call." });
9956
+ const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9957
+ if (!await fileExists(assignmentPath)) {
9958
+ res.status(404).json({ error: "Assignment not found" });
9453
9959
  return;
9454
9960
  }
9455
- const config = await getStatusConfig();
9456
- const validStatuses = new Set(config.statuses.map((s) => s.id));
9457
- const timestamp = nowTimestamp();
9458
- const results = [];
9459
- let succeeded = 0;
9460
- let failed = 0;
9461
- for (let index = 0; index < items.length; index += 1) {
9961
+ const validation = validateTitleBody(req.body);
9962
+ if (!validation.ok) {
9963
+ res.status(400).json({ error: validation.error });
9964
+ return;
9965
+ }
9966
+ let content = await readFile15(assignmentPath, "utf-8");
9967
+ content = setTopLevelField(content, "title", validation.value);
9968
+ content = setTopLevelField(content, "updated", nowTimestamp());
9969
+ await writeFileForce(assignmentPath, content);
9970
+ const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
9971
+ res.json({ assignment });
9972
+ } catch (error) {
9973
+ console.error("Error updating standalone title:", error);
9974
+ res.status(500).json({ error: `Failed to update title: ${error.message}` });
9975
+ }
9976
+ });
9977
+ router.post("/api/assignments/bulk-status-override", async (req, res) => {
9978
+ try {
9979
+ const body = req.body;
9980
+ if (!body || typeof body !== "object" || !Array.isArray(body.items)) {
9981
+ res.status(400).json({ error: "Request body must include `items` (array)." });
9982
+ return;
9983
+ }
9984
+ const items = body.items;
9985
+ if (items.length === 0) {
9986
+ res.status(400).json({ error: "`items` must contain at least one entry." });
9987
+ return;
9988
+ }
9989
+ if (items.length > 200) {
9990
+ res.status(400).json({ error: "`items` is capped at 200 entries per call." });
9991
+ return;
9992
+ }
9993
+ const config = await getStatusConfig();
9994
+ const validStatuses = new Set(config.statuses.map((s) => s.id));
9995
+ const timestamp = nowTimestamp();
9996
+ const results = [];
9997
+ let succeeded = 0;
9998
+ let failed = 0;
9999
+ for (let index = 0; index < items.length; index += 1) {
9462
10000
  const raw = items[index];
9463
10001
  const itemKey = buildBulkItemKey(raw, index);
9464
10002
  try {
@@ -9479,9 +10017,9 @@ ${entry}`;
9479
10017
  if (!resolved) {
9480
10018
  throw new Error(`assignment "${item.id}" not found`);
9481
10019
  }
9482
- assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
10020
+ assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9483
10021
  } else if (typeof item.projectSlug === "string" && typeof item.assignmentSlug === "string" && item.projectSlug && item.assignmentSlug) {
9484
- assignmentPath = resolve17(
10022
+ assignmentPath = resolve18(
9485
10023
  projectsDir,
9486
10024
  item.projectSlug,
9487
10025
  "assignments",
@@ -9494,7 +10032,7 @@ ${entry}`;
9494
10032
  if (!await fileExists(assignmentPath)) {
9495
10033
  throw new Error("assignment file not found");
9496
10034
  }
9497
- let content = await readFile14(assignmentPath, "utf-8");
10035
+ let content = await readFile15(assignmentPath, "utf-8");
9498
10036
  content = setTopLevelField(content, "status", status);
9499
10037
  content = setTopLevelField(content, "updated", timestamp);
9500
10038
  if (status !== "blocked") {
@@ -9526,7 +10064,7 @@ ${entry}`;
9526
10064
  res.status(404).json({ error: `Assignment "${id}" not found` });
9527
10065
  return;
9528
10066
  }
9529
- const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
10067
+ const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
9530
10068
  const currentContent = await readCurrentDocument(assignmentPath);
9531
10069
  if (!currentContent) {
9532
10070
  res.status(404).json({ error: "Assignment not found" });
@@ -9607,6 +10145,26 @@ function validateAssigneeBody(body) {
9607
10145
  }
9608
10146
  return { ok: true, value: trimmed };
9609
10147
  }
10148
+ function validateTitleBody(body) {
10149
+ if (!body || typeof body !== "object") {
10150
+ return { ok: false, error: "Body must include `title` (non-empty string)." };
10151
+ }
10152
+ const title = body.title;
10153
+ if (typeof title !== "string") {
10154
+ return { ok: false, error: "`title` must be a string." };
10155
+ }
10156
+ const trimmed = title.trim();
10157
+ if (trimmed.length === 0) {
10158
+ return { ok: false, error: "`title` is required." };
10159
+ }
10160
+ if (trimmed.length > 200) {
10161
+ return { ok: false, error: "`title` must be 200 characters or fewer." };
10162
+ }
10163
+ if (/["\r\n]/.test(trimmed)) {
10164
+ return { ok: false, error: "`title` may not contain double quotes or line breaks." };
10165
+ }
10166
+ return { ok: true, value: trimmed };
10167
+ }
9610
10168
  function buildBulkItemKey(raw, index) {
9611
10169
  if (raw && typeof raw === "object") {
9612
10170
  const item = raw;
@@ -9618,7 +10176,7 @@ function buildBulkItemKey(raw, index) {
9618
10176
  return `#${index}`;
9619
10177
  }
9620
10178
  async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
9621
- const commentsPath = resolve17(assignmentDir, "comments.md");
10179
+ const commentsPath = resolve18(assignmentDir, "comments.md");
9622
10180
  const { body, author, type, replyTo } = req.body || {};
9623
10181
  if (!body || typeof body !== "string" || !body.trim()) {
9624
10182
  res.status(400).json({ error: "body is required" });
@@ -9630,7 +10188,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
9630
10188
  let currentContent;
9631
10189
  let currentCount = 0;
9632
10190
  if (await fileExists(commentsPath)) {
9633
- currentContent = await readFile14(commentsPath, "utf-8");
10191
+ currentContent = await readFile15(commentsPath, "utf-8");
9634
10192
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
9635
10193
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
9636
10194
  } else {
@@ -9660,7 +10218,7 @@ ${entry}`;
9660
10218
  res.status(201).json({ assignment, comment: { id: comment.id } });
9661
10219
  }
9662
10220
  async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
9663
- const commentsPath = resolve17(assignmentDir, "comments.md");
10221
+ const commentsPath = resolve18(assignmentDir, "comments.md");
9664
10222
  if (!await fileExists(commentsPath)) {
9665
10223
  res.status(404).json({ error: "Comments file not found" });
9666
10224
  return;
@@ -9670,7 +10228,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
9670
10228
  res.status(400).json({ error: "resolved (boolean) is required" });
9671
10229
  return;
9672
10230
  }
9673
- const content = await readFile14(commentsPath, "utf-8");
10231
+ const content = await readFile15(commentsPath, "utf-8");
9674
10232
  const parsed = parseComments(content);
9675
10233
  const target = parsed.entries.find((e) => e.id === commentId);
9676
10234
  if (!target) {
@@ -9699,9 +10257,9 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
9699
10257
  // src/dashboard/api-servers.ts
9700
10258
  init_servers();
9701
10259
  init_scanner();
9702
- import { Router as Router2 } from "express";
10260
+ import { Router as Router3 } from "express";
9703
10261
  function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
9704
- const router = Router2();
10262
+ const router = Router3();
9705
10263
  router.get("/", async (_req, res) => {
9706
10264
  try {
9707
10265
  const result = await scanAllSessions(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 });
@@ -9816,8 +10374,8 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
9816
10374
  // src/dashboard/api-agent-sessions.ts
9817
10375
  init_agent_sessions();
9818
10376
  init_fs();
9819
- import { Router as Router3 } from "express";
9820
- import { resolve as resolve18 } from "path";
10377
+ import { Router as Router4 } from "express";
10378
+ import { resolve as resolve19 } from "path";
9821
10379
 
9822
10380
  // src/utils/transcript.ts
9823
10381
  import { open } from "fs/promises";
@@ -9897,7 +10455,7 @@ function captureProcessStartedAt(pid) {
9897
10455
 
9898
10456
  // src/dashboard/api-agent-sessions.ts
9899
10457
  function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
9900
- const router = Router3();
10458
+ const router = Router4();
9901
10459
  router.get("/", async (_req, res) => {
9902
10460
  try {
9903
10461
  await reconcileActiveSessions(projectsDir, assignmentsDir2);
@@ -9915,7 +10473,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
9915
10473
  try {
9916
10474
  const { projectSlug } = req.params;
9917
10475
  const assignment = req.query.assignment;
9918
- const projectDir = resolve18(projectsDir, projectSlug);
10476
+ const projectDir = resolve19(projectsDir, projectSlug);
9919
10477
  if (!await fileExists(projectDir)) {
9920
10478
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
9921
10479
  return;
@@ -9945,7 +10503,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
9945
10503
  return;
9946
10504
  }
9947
10505
  if (projectSlug) {
9948
- const projectDir = resolve18(projectsDir, projectSlug);
10506
+ const projectDir = resolve19(projectsDir, projectSlug);
9949
10507
  if (!await fileExists(projectDir)) {
9950
10508
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
9951
10509
  return;
@@ -10043,7 +10601,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
10043
10601
  // src/dashboard/api-agents.ts
10044
10602
  init_config2();
10045
10603
  init_agents_schema();
10046
- import { Router as Router4 } from "express";
10604
+ import { Router as Router5 } from "express";
10047
10605
  function mapAgentErrorToFieldErrors(err) {
10048
10606
  const message = err.message;
10049
10607
  let match = message.match(/^agent id "([^"]*)" is invalid/);
@@ -10213,7 +10771,7 @@ function coerceAgentRow(raw, index) {
10213
10771
  return { ok: true, value: cleaned };
10214
10772
  }
10215
10773
  function createAgentsRouter() {
10216
- const router = Router4();
10774
+ const router = Router5();
10217
10775
  router.get("/", async (_req, res) => {
10218
10776
  try {
10219
10777
  const config = await readConfig();
@@ -10271,7 +10829,7 @@ function createAgentsRouter() {
10271
10829
 
10272
10830
  // src/dashboard/api-launch-preflight.ts
10273
10831
  init_config2();
10274
- import { Router as Router5 } from "express";
10832
+ import { Router as Router6 } from "express";
10275
10833
 
10276
10834
  // src/utils/terminal-probe.ts
10277
10835
  import { spawnSync as spawnSync4 } from "child_process";
@@ -10311,7 +10869,7 @@ function probeTerminalInstalled(terminal) {
10311
10869
 
10312
10870
  // src/dashboard/api-launch-preflight.ts
10313
10871
  function createLaunchPreflightRouter() {
10314
- const router = Router5();
10872
+ const router = Router6();
10315
10873
  router.post("/preflight", async (req, res) => {
10316
10874
  try {
10317
10875
  const body = req.body ?? {};
@@ -10347,9 +10905,9 @@ function createLaunchPreflightRouter() {
10347
10905
 
10348
10906
  // src/dashboard/api-terminal-config.ts
10349
10907
  init_config2();
10350
- import { Router as Router6 } from "express";
10908
+ import { Router as Router7 } from "express";
10351
10909
  function createTerminalConfigRouter() {
10352
- const router = Router6();
10910
+ const router = Router7();
10353
10911
  router.get("/", async (_req, res) => {
10354
10912
  try {
10355
10913
  const config = await readConfig();
@@ -10384,21 +10942,565 @@ function createTerminalConfigRouter() {
10384
10942
  const config = await readConfig();
10385
10943
  res.json({ terminal: getTerminal(config), custom: false });
10386
10944
  } catch (error) {
10387
- console.error("Error resetting terminal config:", error);
10388
- res.status(500).json({ error: "Failed to reset terminal config" });
10945
+ console.error("Error resetting terminal config:", error);
10946
+ res.status(500).json({ error: "Failed to reset terminal config" });
10947
+ }
10948
+ });
10949
+ return router;
10950
+ }
10951
+
10952
+ // src/dashboard/api-status-config.ts
10953
+ init_config2();
10954
+ init_api();
10955
+ import { Router as Router8 } from "express";
10956
+
10957
+ // src/utils/status-config-resolution.ts
10958
+ init_frontmatter();
10959
+ import { readFile as readFile16, writeFile as writeFile4, rm as rm2 } from "fs/promises";
10960
+ import { dirname as dirname4 } from "path";
10961
+
10962
+ // src/utils/assignment-walk.ts
10963
+ init_fs();
10964
+ import { resolve as resolve20 } from "path";
10965
+ import { readdir as readdir10 } from "fs/promises";
10966
+ async function listAssignmentsByProject(projectsDir, standaloneDir) {
10967
+ const result = {
10968
+ withAssignmentMd: [],
10969
+ orphanFolders: []
10970
+ };
10971
+ if (await fileExists(projectsDir)) {
10972
+ const projects = await readdir10(projectsDir, { withFileTypes: true });
10973
+ for (const m of projects) {
10974
+ if (!m.isDirectory()) continue;
10975
+ if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
10976
+ const assignmentsDir2 = resolve20(projectsDir, m.name, "assignments");
10977
+ if (!await fileExists(assignmentsDir2)) continue;
10978
+ const entries = await readdir10(assignmentsDir2, { withFileTypes: true });
10979
+ for (const a of entries) {
10980
+ if (!a.isDirectory()) continue;
10981
+ if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
10982
+ const assignmentDir = resolve20(assignmentsDir2, a.name);
10983
+ const assignmentMd = resolve20(assignmentDir, "assignment.md");
10984
+ const entry = {
10985
+ projectDir: resolve20(projectsDir, m.name),
10986
+ projectSlug: m.name,
10987
+ assignmentDir,
10988
+ assignmentSlug: a.name,
10989
+ standalone: false
10990
+ };
10991
+ if (await fileExists(assignmentMd)) {
10992
+ result.withAssignmentMd.push(entry);
10993
+ } else {
10994
+ result.orphanFolders.push(entry);
10995
+ }
10996
+ }
10997
+ }
10998
+ }
10999
+ if (standaloneDir !== null && await fileExists(standaloneDir)) {
11000
+ const entries = await readdir10(standaloneDir, { withFileTypes: true });
11001
+ for (const a of entries) {
11002
+ if (!a.isDirectory()) continue;
11003
+ if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
11004
+ const assignmentDir = resolve20(standaloneDir, a.name);
11005
+ const assignmentMd = resolve20(assignmentDir, "assignment.md");
11006
+ const entry = {
11007
+ projectDir: standaloneDir,
11008
+ projectSlug: null,
11009
+ assignmentDir,
11010
+ assignmentSlug: a.name,
11011
+ standalone: true
11012
+ };
11013
+ if (await fileExists(assignmentMd)) {
11014
+ result.withAssignmentMd.push(entry);
11015
+ } else {
11016
+ result.orphanFolders.push(entry);
11017
+ }
11018
+ }
11019
+ }
11020
+ return result;
11021
+ }
11022
+
11023
+ // src/utils/status-config-resolution.ts
11024
+ init_timestamp();
11025
+ var StatusResolutionError = class extends Error {
11026
+ constructor(message, code) {
11027
+ super(message);
11028
+ this.code = code;
11029
+ this.name = "StatusResolutionError";
11030
+ }
11031
+ };
11032
+ async function scanAssignmentsByStatus(projectsDir, standaloneDir, ids) {
11033
+ const result = /* @__PURE__ */ new Map();
11034
+ for (const id of ids) result.set(id, []);
11035
+ if (ids.length === 0) return result;
11036
+ const idSet = new Set(ids);
11037
+ const walk = await listAssignmentsByProject(projectsDir, standaloneDir);
11038
+ for (const entry of walk.withAssignmentMd) {
11039
+ const assignmentPath = `${entry.assignmentDir}/assignment.md`;
11040
+ let content;
11041
+ try {
11042
+ content = await readFile16(assignmentPath, "utf-8");
11043
+ } catch (err) {
11044
+ const code = err?.code;
11045
+ if (code === "ENOENT") {
11046
+ continue;
11047
+ }
11048
+ throw new StatusResolutionError(
11049
+ `failed to read ${assignmentPath}: ${err instanceof Error ? err.message : String(err)}`,
11050
+ "scan-failed"
11051
+ );
11052
+ }
11053
+ const fm = parseAssignmentFrontmatter(content);
11054
+ if (!idSet.has(fm.status)) continue;
11055
+ const display = entry.standalone ? `(standalone) ${entry.assignmentSlug}` : `${entry.projectSlug}/${entry.assignmentSlug}`;
11056
+ const affected = {
11057
+ path: assignmentPath,
11058
+ display,
11059
+ projectSlug: entry.projectSlug,
11060
+ assignmentSlug: entry.assignmentSlug,
11061
+ status: fm.status
11062
+ };
11063
+ const bucket = result.get(fm.status);
11064
+ if (bucket) bucket.push(affected);
11065
+ }
11066
+ return result;
11067
+ }
11068
+ async function applyStatusResolutions(resolutions, affected, validTargets) {
11069
+ const seenIds = /* @__PURE__ */ new Set();
11070
+ for (const r of resolutions) {
11071
+ if (seenIds.has(r.id)) {
11072
+ throw new StatusResolutionError(
11073
+ `duplicate resolution for status id "${r.id}"`,
11074
+ "duplicate-id"
11075
+ );
11076
+ }
11077
+ seenIds.add(r.id);
11078
+ if (!affected.has(r.id)) {
11079
+ throw new StatusResolutionError(
11080
+ `stale resolution: status id "${r.id}" was not scanned`,
11081
+ "stale-resolution"
11082
+ );
11083
+ }
11084
+ if (r.mode === "remap") {
11085
+ if (r.target === r.id) {
11086
+ throw new StatusResolutionError(
11087
+ `invalid remap target "${r.target}" \u2014 same as source`,
11088
+ "invalid-target"
11089
+ );
11090
+ }
11091
+ if (!validTargets.has(r.target)) {
11092
+ throw new StatusResolutionError(
11093
+ `invalid remap target "${r.target}" \u2014 not in valid targets`,
11094
+ "invalid-target"
11095
+ );
11096
+ }
11097
+ }
11098
+ }
11099
+ const buffer = /* @__PURE__ */ new Map();
11100
+ for (const r of resolutions) {
11101
+ if (r.mode !== "remap") continue;
11102
+ const list = affected.get(r.id) ?? [];
11103
+ for (const a of list) {
11104
+ try {
11105
+ const content = await readFile16(a.path, "utf-8");
11106
+ buffer.set(a.path, content);
11107
+ } catch (err) {
11108
+ const code = err?.code;
11109
+ if (code === "ENOENT") {
11110
+ continue;
11111
+ }
11112
+ throw new StatusResolutionError(
11113
+ `failed to buffer ${a.path}: ${err instanceof Error ? err.message : String(err)}`,
11114
+ "scan-failed"
11115
+ );
11116
+ }
11117
+ }
11118
+ }
11119
+ const writtenPaths = [];
11120
+ let remapped = 0;
11121
+ const byId = /* @__PURE__ */ new Map();
11122
+ for (const r of resolutions) {
11123
+ if (r.mode === "remap") byId.set(r.id, { mode: "remap", count: 0, target: r.target });
11124
+ else byId.set(r.id, { mode: "delete", count: 0 });
11125
+ }
11126
+ try {
11127
+ for (const r of resolutions) {
11128
+ if (r.mode !== "remap") continue;
11129
+ const list = affected.get(r.id) ?? [];
11130
+ for (const a of list) {
11131
+ let current;
11132
+ try {
11133
+ current = await readFile16(a.path, "utf-8");
11134
+ } catch (err) {
11135
+ const code = err?.code;
11136
+ if (code === "ENOENT") {
11137
+ continue;
11138
+ }
11139
+ throw err;
11140
+ }
11141
+ const fm = parseAssignmentFrontmatter(current);
11142
+ if (fm.status !== r.id) {
11143
+ console.warn(
11144
+ `status-config-resolution: skipping remap of ${a.display} \u2014 status drifted from "${r.id}" to "${fm.status}"`
11145
+ );
11146
+ continue;
11147
+ }
11148
+ const next = updateAssignmentFile(current, {
11149
+ status: r.target,
11150
+ updated: nowTimestamp()
11151
+ });
11152
+ await writeFile4(a.path, next, "utf-8");
11153
+ writtenPaths.push(a.path);
11154
+ remapped++;
11155
+ const bucket = byId.get(r.id);
11156
+ if (bucket) bucket.count++;
11157
+ }
11158
+ }
11159
+ } catch (err) {
11160
+ for (const p of writtenPaths) {
11161
+ const original = buffer.get(p);
11162
+ if (original !== void 0) {
11163
+ await writeFile4(p, original, "utf-8").catch((rollbackErr) => {
11164
+ console.error(
11165
+ `status-config-resolution: rollback write failed for ${p}:`,
11166
+ rollbackErr
11167
+ );
11168
+ });
11169
+ }
11170
+ }
11171
+ throw new StatusResolutionError(
11172
+ `remap write failed: ${err instanceof Error ? err.message : String(err)}`,
11173
+ "write-failed"
11174
+ );
11175
+ }
11176
+ let deleted = 0;
11177
+ for (const r of resolutions) {
11178
+ if (r.mode !== "delete") continue;
11179
+ const list = affected.get(r.id) ?? [];
11180
+ for (const a of list) {
11181
+ try {
11182
+ const current = await readFile16(a.path, "utf-8");
11183
+ const fm = parseAssignmentFrontmatter(current);
11184
+ if (fm.status !== r.id) {
11185
+ console.warn(
11186
+ `status-config-resolution: skipping delete of ${a.display} \u2014 status drifted from "${r.id}" to "${fm.status}"`
11187
+ );
11188
+ continue;
11189
+ }
11190
+ } catch {
11191
+ continue;
11192
+ }
11193
+ const assignmentDir = dirname4(a.path);
11194
+ try {
11195
+ await rm2(assignmentDir, { recursive: true, force: true });
11196
+ deleted++;
11197
+ const bucket = byId.get(r.id);
11198
+ if (bucket) bucket.count++;
11199
+ } catch (err) {
11200
+ throw new StatusResolutionError(
11201
+ `delete failed for ${a.display}: ${err instanceof Error ? err.message : String(err)}`,
11202
+ "delete-failed"
11203
+ );
11204
+ }
11205
+ }
11206
+ }
11207
+ return { remapped, deleted, byId };
11208
+ }
11209
+ async function verifyNoDriftedOrphans(projectsDir, standaloneDir, droppedIds) {
11210
+ if (droppedIds.length === 0) return;
11211
+ const finalScan = await scanAssignmentsByStatus(projectsDir, standaloneDir, droppedIds);
11212
+ const remaining = [];
11213
+ for (const id of droppedIds) {
11214
+ const list = finalScan.get(id) ?? [];
11215
+ for (const a of list) {
11216
+ remaining.push(`${a.display} (status: ${a.status})`);
11217
+ }
11218
+ }
11219
+ if (remaining.length > 0) {
11220
+ throw new StatusResolutionError(
11221
+ `concurrent edit detected: ${remaining.length} assignment(s) still reference a dropped status after resolutions applied: ${remaining.join(", ")}`,
11222
+ "drift-detected"
11223
+ );
11224
+ }
11225
+ }
11226
+
11227
+ // src/dashboard/api-status-config.ts
11228
+ var AFFECTED_SAMPLE_CAP = 50;
11229
+ function toSummary(a) {
11230
+ return {
11231
+ display: a.display,
11232
+ projectSlug: a.projectSlug,
11233
+ assignmentSlug: a.assignmentSlug,
11234
+ status: a.status
11235
+ };
11236
+ }
11237
+ function buildAffectedResponse(id, list) {
11238
+ const truncated = list.length > AFFECTED_SAMPLE_CAP;
11239
+ return {
11240
+ id,
11241
+ count: list.length,
11242
+ truncated,
11243
+ assignments: list.slice(0, AFFECTED_SAMPLE_CAP).map(toSummary)
11244
+ };
11245
+ }
11246
+ function isString(x) {
11247
+ return typeof x === "string" && x.length > 0;
11248
+ }
11249
+ function parseResolutions(raw) {
11250
+ if (raw === void 0) {
11251
+ return { resolutions: [], malformed: null, duplicateIds: null };
11252
+ }
11253
+ if (!Array.isArray(raw)) {
11254
+ return { resolutions: [], malformed: "resolutions must be an array", duplicateIds: null };
11255
+ }
11256
+ const out = [];
11257
+ const seen = /* @__PURE__ */ new Set();
11258
+ const dups = /* @__PURE__ */ new Set();
11259
+ for (let i = 0; i < raw.length; i++) {
11260
+ const r = raw[i];
11261
+ if (!r || typeof r !== "object") {
11262
+ return { resolutions: [], malformed: `resolutions[${i}] must be an object`, duplicateIds: null };
11263
+ }
11264
+ const obj = r;
11265
+ if (!isString(obj.id)) {
11266
+ return { resolutions: [], malformed: `resolutions[${i}].id must be a non-empty string`, duplicateIds: null };
11267
+ }
11268
+ if (obj.mode === "remap") {
11269
+ if (!isString(obj.target)) {
11270
+ return { resolutions: [], malformed: `resolutions[${i}].target must be a non-empty string for mode=remap`, duplicateIds: null };
11271
+ }
11272
+ if (seen.has(obj.id)) dups.add(obj.id);
11273
+ seen.add(obj.id);
11274
+ out.push({ id: obj.id, mode: "remap", target: obj.target });
11275
+ } else if (obj.mode === "delete") {
11276
+ if (seen.has(obj.id)) dups.add(obj.id);
11277
+ seen.add(obj.id);
11278
+ out.push({ id: obj.id, mode: "delete" });
11279
+ } else {
11280
+ return { resolutions: [], malformed: `resolutions[${i}].mode must be 'remap' or 'delete'`, duplicateIds: null };
11281
+ }
11282
+ }
11283
+ if (dups.size > 0) {
11284
+ return { resolutions: [], malformed: null, duplicateIds: [...dups] };
11285
+ }
11286
+ return { resolutions: out, malformed: null, duplicateIds: null };
11287
+ }
11288
+ function mapResolutionErrorToHttp(err, applied) {
11289
+ switch (err.code) {
11290
+ case "duplicate-id":
11291
+ return { status: 400, body: { error: "duplicate-resolution-ids", message: err.message } };
11292
+ case "stale-resolution":
11293
+ return { status: 400, body: { error: "stale-resolution", message: err.message } };
11294
+ case "invalid-target":
11295
+ return { status: 400, body: { error: "invalid-remap-target", message: err.message } };
11296
+ case "scan-failed":
11297
+ return { status: 500, body: { error: "scan-failed", cause: err.message } };
11298
+ case "write-failed":
11299
+ return { status: 500, body: { error: "remap-write-failed", cause: err.message } };
11300
+ case "delete-failed":
11301
+ return {
11302
+ status: 500,
11303
+ body: { error: "delete-failed", cause: err.message, applied: applied ?? void 0 }
11304
+ };
11305
+ case "drift-detected":
11306
+ return {
11307
+ status: 409,
11308
+ body: {
11309
+ error: "concurrent-edit",
11310
+ cause: err.message,
11311
+ applied: applied ?? void 0
11312
+ }
11313
+ };
11314
+ }
11315
+ }
11316
+ function createStatusConfigRouter(projectsDir, assignmentsDir2) {
11317
+ const router = Router8();
11318
+ router.get("/", async (_req, res) => {
11319
+ try {
11320
+ const config = await getStatusConfig();
11321
+ res.json({
11322
+ statuses: config.statuses,
11323
+ order: config.order,
11324
+ transitions: config.transitions,
11325
+ custom: config.custom
11326
+ });
11327
+ } catch (error) {
11328
+ console.error("Error getting status config:", error);
11329
+ res.status(500).json({ error: "Failed to get status config" });
11330
+ }
11331
+ });
11332
+ router.get("/affected/:id", async (req, res) => {
11333
+ try {
11334
+ const id = req.params.id;
11335
+ if (!isString(id)) {
11336
+ res.status(400).json({ error: "malformed-id" });
11337
+ return;
11338
+ }
11339
+ const affected = await scanAssignmentsByStatus(projectsDir, assignmentsDir2, [id]);
11340
+ const list = affected.get(id) ?? [];
11341
+ res.json(buildAffectedResponse(id, list));
11342
+ } catch (error) {
11343
+ console.error("Error getting affected assignments:", error);
11344
+ res.status(500).json({ error: "Failed to get affected assignments" });
11345
+ }
11346
+ });
11347
+ router.post("/", async (req, res) => {
11348
+ try {
11349
+ const { statuses, order, transitions, resolutions: rawResolutions } = req.body ?? {};
11350
+ if (!Array.isArray(statuses) || !Array.isArray(order) || !Array.isArray(transitions)) {
11351
+ res.status(400).json({ error: "malformed-statuses", message: "Request body must include statuses, order, and transitions arrays" });
11352
+ return;
11353
+ }
11354
+ const parsed = parseResolutions(rawResolutions);
11355
+ if (parsed.malformed) {
11356
+ res.status(400).json({ error: "malformed-resolutions", message: parsed.malformed });
11357
+ return;
11358
+ }
11359
+ if (parsed.duplicateIds) {
11360
+ res.status(400).json({ error: "duplicate-resolution-ids", ids: parsed.duplicateIds });
11361
+ return;
11362
+ }
11363
+ const resolutions = parsed.resolutions;
11364
+ const currentConfig = await getStatusConfig();
11365
+ const oldIds = new Set(currentConfig.statuses.map((s) => s.id));
11366
+ const newIds = /* @__PURE__ */ new Set();
11367
+ for (const s of statuses) {
11368
+ if (s && typeof s === "object" && isString(s.id)) {
11369
+ newIds.add(s.id);
11370
+ }
11371
+ }
11372
+ const droppedIds = [];
11373
+ for (const id of oldIds) {
11374
+ if (!newIds.has(id)) droppedIds.push(id);
11375
+ }
11376
+ for (const r of resolutions) {
11377
+ if (!droppedIds.includes(r.id)) {
11378
+ res.status(400).json({ error: "stale-resolution", id: r.id });
11379
+ return;
11380
+ }
11381
+ }
11382
+ const validTargets = /* @__PURE__ */ new Set();
11383
+ for (const id of newIds) {
11384
+ if (oldIds.has(id)) validTargets.add(id);
11385
+ }
11386
+ for (const r of resolutions) {
11387
+ if (r.mode !== "remap") continue;
11388
+ if (r.target === r.id) {
11389
+ res.status(400).json({ error: "invalid-remap-target", reason: "same-as-source", id: r.id, target: r.target });
11390
+ return;
11391
+ }
11392
+ if (!newIds.has(r.target)) {
11393
+ res.status(400).json({ error: "invalid-remap-target", reason: "not-in-new-config", id: r.id, target: r.target });
11394
+ return;
11395
+ }
11396
+ if (!oldIds.has(r.target)) {
11397
+ res.status(400).json({ error: "invalid-remap-target", reason: "not-in-old-config", id: r.id, target: r.target });
11398
+ return;
11399
+ }
11400
+ }
11401
+ let affectedMap;
11402
+ try {
11403
+ affectedMap = await scanAssignmentsByStatus(projectsDir, assignmentsDir2, droppedIds);
11404
+ } catch (err) {
11405
+ if (err instanceof StatusResolutionError) {
11406
+ const mapped = mapResolutionErrorToHttp(err, null);
11407
+ res.status(mapped.status).json(mapped.body);
11408
+ return;
11409
+ }
11410
+ throw err;
11411
+ }
11412
+ const unresolved = [];
11413
+ const resolvedById = new Set(resolutions.map((r) => r.id));
11414
+ for (const id of droppedIds) {
11415
+ const list = affectedMap.get(id) ?? [];
11416
+ if (list.length > 0 && !resolvedById.has(id)) {
11417
+ unresolved.push(buildAffectedResponse(id, list));
11418
+ }
11419
+ }
11420
+ if (unresolved.length > 0) {
11421
+ res.status(409).json({ error: "unresolved-orphans", unresolved });
11422
+ return;
11423
+ }
11424
+ let applied;
11425
+ try {
11426
+ applied = await applyStatusResolutions(resolutions, affectedMap, validTargets);
11427
+ } catch (err) {
11428
+ if (err instanceof StatusResolutionError) {
11429
+ const mapped = mapResolutionErrorToHttp(err, null);
11430
+ res.status(mapped.status).json(mapped.body);
11431
+ return;
11432
+ }
11433
+ throw err;
11434
+ }
11435
+ try {
11436
+ await verifyNoDriftedOrphans(projectsDir, assignmentsDir2, droppedIds);
11437
+ } catch (err) {
11438
+ if (err instanceof StatusResolutionError) {
11439
+ const mapped = mapResolutionErrorToHttp(err, {
11440
+ remapped: applied.remapped,
11441
+ deleted: applied.deleted
11442
+ });
11443
+ res.status(mapped.status).json(mapped.body);
11444
+ return;
11445
+ }
11446
+ throw err;
11447
+ }
11448
+ try {
11449
+ await writeStatusConfig({ statuses, order, transitions });
11450
+ } catch (err) {
11451
+ console.error("Error saving status config after applying resolutions:", err);
11452
+ res.status(500).json({
11453
+ error: "config-write-failed",
11454
+ message: err instanceof Error ? err.message : String(err),
11455
+ applied: { remapped: applied.remapped, deleted: applied.deleted }
11456
+ });
11457
+ return;
11458
+ }
11459
+ clearStatusConfigCache();
11460
+ const config = await getStatusConfig();
11461
+ const byId = {};
11462
+ for (const [id, entry] of applied.byId) {
11463
+ byId[id] = entry.target !== void 0 ? { mode: entry.mode, count: entry.count, target: entry.target } : { mode: entry.mode, count: entry.count };
11464
+ }
11465
+ res.json({
11466
+ statuses: config.statuses,
11467
+ order: config.order,
11468
+ transitions: config.transitions,
11469
+ custom: config.custom,
11470
+ applied: { remapped: applied.remapped, deleted: applied.deleted, byId }
11471
+ });
11472
+ } catch (error) {
11473
+ console.error("Error saving status config:", error);
11474
+ res.status(500).json({ error: "Failed to save status config" });
11475
+ }
11476
+ });
11477
+ router.delete("/", async (_req, res) => {
11478
+ try {
11479
+ await deleteStatusConfig();
11480
+ clearStatusConfigCache();
11481
+ const config = await getStatusConfig();
11482
+ res.json({
11483
+ statuses: config.statuses,
11484
+ order: config.order,
11485
+ transitions: config.transitions,
11486
+ custom: config.custom
11487
+ });
11488
+ } catch (error) {
11489
+ console.error("Error resetting status config:", error);
11490
+ res.status(500).json({ error: "Failed to reset status config" });
10389
11491
  }
10390
11492
  });
10391
11493
  return router;
10392
11494
  }
10393
11495
 
10394
11496
  // src/dashboard/api-leases.ts
10395
- import { Router as Router7 } from "express";
11497
+ import { Router as Router9 } from "express";
10396
11498
 
10397
11499
  // src/db/leases-db.ts
10398
11500
  init_paths();
10399
11501
  import Database2 from "better-sqlite3";
10400
- import { randomUUID as randomUUID2 } from "crypto";
10401
- import { resolve as resolve19 } from "path";
11502
+ import { randomUUID as randomUUID3 } from "crypto";
11503
+ import { resolve as resolve21 } from "path";
10402
11504
  var db2 = null;
10403
11505
  var LEASE_SCHEMA_VERSION = "1";
10404
11506
  var SCHEMA_SQL2 = `
@@ -10485,7 +11587,7 @@ function isBusyError(err) {
10485
11587
  }
10486
11588
  function initLeasesDb(dbPath) {
10487
11589
  if (db2) return db2;
10488
- const finalPath = dbPath ?? resolve19(syntaurRoot(), "syntaur.db");
11590
+ const finalPath = dbPath ?? resolve21(syntaurRoot(), "syntaur.db");
10489
11591
  db2 = new Database2(finalPath);
10490
11592
  db2.pragma("journal_mode = WAL");
10491
11593
  db2.pragma("busy_timeout = 5000");
@@ -10574,7 +11676,7 @@ function forceReleaseLease(lease_id) {
10574
11676
 
10575
11677
  // src/dashboard/api-leases.ts
10576
11678
  function createLeasesRouter(broadcast) {
10577
- const router = Router7();
11679
+ const router = Router9();
10578
11680
  router.get("/", (_req, res) => {
10579
11681
  try {
10580
11682
  initLeasesDb();
@@ -10628,15 +11730,302 @@ function createLeasesRouter(broadcast) {
10628
11730
  return router;
10629
11731
  }
10630
11732
 
11733
+ // src/dashboard/api-usage.ts
11734
+ import { Router as Router10 } from "express";
11735
+
11736
+ // src/db/usage-db.ts
11737
+ init_paths();
11738
+ import Database3 from "better-sqlite3";
11739
+ import { resolve as resolve22 } from "path";
11740
+ var db3 = null;
11741
+ var USAGE_SCHEMA_VERSION = "1";
11742
+ var SCHEMA_SQL3 = `
11743
+ CREATE TABLE IF NOT EXISTS meta (
11744
+ key TEXT PRIMARY KEY,
11745
+ value TEXT
11746
+ );
11747
+
11748
+ CREATE TABLE IF NOT EXISTS usage_events (
11749
+ session_id TEXT NOT NULL,
11750
+ model TEXT NOT NULL,
11751
+ tool TEXT NOT NULL,
11752
+ event_ts TEXT NOT NULL,
11753
+ input_tokens INTEGER NOT NULL DEFAULT 0,
11754
+ output_tokens INTEGER NOT NULL DEFAULT 0,
11755
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
11756
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
11757
+ total_tokens INTEGER NOT NULL DEFAULT 0,
11758
+ total_cost REAL NOT NULL DEFAULT 0,
11759
+ cwd TEXT,
11760
+ project_slug TEXT NOT NULL DEFAULT '',
11761
+ assignment_slug TEXT NOT NULL DEFAULT '',
11762
+ raw_json TEXT,
11763
+ updated_at TEXT NOT NULL,
11764
+ PRIMARY KEY (session_id, model)
11765
+ );
11766
+
11767
+ CREATE INDEX IF NOT EXISTS idx_usage_events_ts
11768
+ ON usage_events (event_ts);
11769
+ CREATE INDEX IF NOT EXISTS idx_usage_events_attribution
11770
+ ON usage_events (project_slug, assignment_slug);
11771
+ CREATE INDEX IF NOT EXISTS idx_usage_events_cwd
11772
+ ON usage_events (cwd, event_ts);
11773
+
11774
+ CREATE TABLE IF NOT EXISTS usage_daily (
11775
+ day TEXT NOT NULL,
11776
+ tool TEXT NOT NULL,
11777
+ model TEXT NOT NULL,
11778
+ project_slug TEXT NOT NULL DEFAULT '',
11779
+ assignment_slug TEXT NOT NULL DEFAULT '',
11780
+ input_tokens INTEGER NOT NULL DEFAULT 0,
11781
+ output_tokens INTEGER NOT NULL DEFAULT 0,
11782
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
11783
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
11784
+ total_tokens INTEGER NOT NULL DEFAULT 0,
11785
+ total_cost REAL NOT NULL DEFAULT 0,
11786
+ frozen INTEGER NOT NULL DEFAULT 0,
11787
+ computed_at TEXT NOT NULL,
11788
+ PRIMARY KEY (day, tool, model, project_slug, assignment_slug)
11789
+ );
11790
+
11791
+ CREATE INDEX IF NOT EXISTS idx_usage_daily_day
11792
+ ON usage_daily (day);
11793
+ `;
11794
+ function initUsageDb(dbPath) {
11795
+ if (db3) return db3;
11796
+ const finalPath = dbPath ?? resolve22(syntaurRoot(), "syntaur.db");
11797
+ db3 = new Database3(finalPath);
11798
+ db3.pragma("journal_mode = WAL");
11799
+ db3.pragma("busy_timeout = 5000");
11800
+ db3.pragma("foreign_keys = ON");
11801
+ const database = db3;
11802
+ const runMigrations = database.transaction(() => {
11803
+ database.exec(SCHEMA_SQL3);
11804
+ database.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run("usage_schema_version", USAGE_SCHEMA_VERSION);
11805
+ });
11806
+ runMigrations.exclusive();
11807
+ return db3;
11808
+ }
11809
+ function getUsageDb() {
11810
+ if (!db3) {
11811
+ throw new Error("Usage database not initialized. Call initUsageDb() first.");
11812
+ }
11813
+ return db3;
11814
+ }
11815
+ function closeUsageDb() {
11816
+ if (db3) {
11817
+ db3.close();
11818
+ db3 = null;
11819
+ }
11820
+ }
11821
+ function listEvents(filter = {}) {
11822
+ const database = getUsageDb();
11823
+ const where = [];
11824
+ const params2 = [];
11825
+ if (filter.since) {
11826
+ where.push("event_ts >= ?");
11827
+ params2.push(filter.since);
11828
+ }
11829
+ if (filter.until) {
11830
+ where.push("event_ts <= ?");
11831
+ params2.push(filter.until);
11832
+ }
11833
+ if (filter.projectSlug !== void 0) {
11834
+ where.push("project_slug = ?");
11835
+ params2.push(filter.projectSlug);
11836
+ }
11837
+ if (filter.assignmentSlug !== void 0) {
11838
+ where.push("assignment_slug = ?");
11839
+ params2.push(filter.assignmentSlug);
11840
+ }
11841
+ if (filter.tool) {
11842
+ where.push("tool = ?");
11843
+ params2.push(filter.tool);
11844
+ }
11845
+ const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
11846
+ return database.prepare(
11847
+ `SELECT session_id, model, tool, event_ts,
11848
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
11849
+ total_tokens, total_cost,
11850
+ cwd, project_slug, assignment_slug, raw_json, updated_at
11851
+ FROM usage_events ${whereSql}
11852
+ ORDER BY event_ts DESC`
11853
+ ).all(...params2);
11854
+ }
11855
+ function listDaily(filter = {}) {
11856
+ const database = getUsageDb();
11857
+ const where = [];
11858
+ const params2 = [];
11859
+ if (filter.since) {
11860
+ where.push("day >= ?");
11861
+ params2.push(filter.since);
11862
+ }
11863
+ if (filter.until) {
11864
+ where.push("day <= ?");
11865
+ params2.push(filter.until);
11866
+ }
11867
+ if (filter.projectSlug !== void 0) {
11868
+ where.push("project_slug = ?");
11869
+ params2.push(filter.projectSlug);
11870
+ }
11871
+ if (filter.assignmentSlug !== void 0) {
11872
+ where.push("assignment_slug = ?");
11873
+ params2.push(filter.assignmentSlug);
11874
+ }
11875
+ if (filter.tool) {
11876
+ where.push("tool = ?");
11877
+ params2.push(filter.tool);
11878
+ }
11879
+ const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
11880
+ return database.prepare(
11881
+ `SELECT day, tool, model, project_slug, assignment_slug,
11882
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
11883
+ total_tokens, total_cost, frozen, computed_at
11884
+ FROM usage_daily ${whereSql}
11885
+ ORDER BY day DESC, project_slug, assignment_slug, tool, model`
11886
+ ).all(...params2);
11887
+ }
11888
+
11889
+ // src/dashboard/api-usage.ts
11890
+ function createUsageRouter() {
11891
+ const router = Router10();
11892
+ router.get("/", (req, res) => {
11893
+ try {
11894
+ initUsageDb();
11895
+ const filter = extractCommonFilter(req.query);
11896
+ const rows = listDaily(filter);
11897
+ res.json({
11898
+ daily: rows,
11899
+ summary: summarize(rows, groupByMode(req.query.groupBy))
11900
+ });
11901
+ } catch (error) {
11902
+ res.status(500).json({
11903
+ error: error instanceof Error ? error.message : "List failed"
11904
+ });
11905
+ }
11906
+ });
11907
+ router.get("/projects/:projectSlug", (req, res) => {
11908
+ try {
11909
+ initUsageDb();
11910
+ const projectSlug = req.params.projectSlug;
11911
+ const rows = listDaily({
11912
+ ...extractCommonFilter(req.query),
11913
+ projectSlug
11914
+ });
11915
+ res.json({
11916
+ projectSlug,
11917
+ daily: rows,
11918
+ summary: summarize(rows, "assignment")
11919
+ });
11920
+ } catch (error) {
11921
+ res.status(500).json({
11922
+ error: error instanceof Error ? error.message : "List failed"
11923
+ });
11924
+ }
11925
+ });
11926
+ router.get("/projects/:projectSlug/assignments/:assignmentSlug", (req, res) => {
11927
+ try {
11928
+ initUsageDb();
11929
+ const { projectSlug, assignmentSlug } = req.params;
11930
+ const common = extractCommonFilter(req.query);
11931
+ const dailyRows = listDaily({
11932
+ ...common,
11933
+ projectSlug,
11934
+ assignmentSlug
11935
+ });
11936
+ const eventRows = listEvents(
11937
+ eventsFilterFromDaily({ ...common, projectSlug, assignmentSlug })
11938
+ );
11939
+ res.json({
11940
+ projectSlug,
11941
+ assignmentSlug,
11942
+ daily: dailyRows,
11943
+ events: eventRows
11944
+ });
11945
+ } catch (error) {
11946
+ res.status(500).json({
11947
+ error: error instanceof Error ? error.message : "List failed"
11948
+ });
11949
+ }
11950
+ });
11951
+ router.get("/standalone/:assignmentId", (req, res) => {
11952
+ try {
11953
+ initUsageDb();
11954
+ const assignmentSlug = req.params.assignmentId;
11955
+ const common = extractCommonFilter(req.query);
11956
+ const dailyRows = listDaily({
11957
+ ...common,
11958
+ projectSlug: "",
11959
+ assignmentSlug
11960
+ });
11961
+ const eventRows = listEvents(
11962
+ eventsFilterFromDaily({ ...common, projectSlug: "", assignmentSlug })
11963
+ );
11964
+ res.json({
11965
+ assignmentId: assignmentSlug,
11966
+ daily: dailyRows,
11967
+ events: eventRows
11968
+ });
11969
+ } catch (error) {
11970
+ res.status(500).json({
11971
+ error: error instanceof Error ? error.message : "List failed"
11972
+ });
11973
+ }
11974
+ });
11975
+ return router;
11976
+ }
11977
+ function extractCommonFilter(query) {
11978
+ const out = {};
11979
+ if (typeof query.since === "string") out.since = query.since;
11980
+ if (typeof query.until === "string") out.until = query.until;
11981
+ if (typeof query.tool === "string") out.tool = query.tool;
11982
+ if (typeof query.project === "string") out.projectSlug = query.project;
11983
+ if (typeof query.assignment === "string") out.assignmentSlug = query.assignment;
11984
+ return out;
11985
+ }
11986
+ function eventsFilterFromDaily(common) {
11987
+ const out = {};
11988
+ if (common.since) out.since = `${common.since}T00:00:00.000Z`;
11989
+ if (common.until) out.until = `${common.until}T23:59:59.999Z`;
11990
+ if (common.tool) out.tool = common.tool;
11991
+ if (common.projectSlug !== void 0) out.projectSlug = common.projectSlug;
11992
+ if (common.assignmentSlug !== void 0) out.assignmentSlug = common.assignmentSlug;
11993
+ return out;
11994
+ }
11995
+ function groupByMode(q) {
11996
+ return q === "assignment" ? "assignment" : "project";
11997
+ }
11998
+ function summarize(rows, mode) {
11999
+ const map = /* @__PURE__ */ new Map();
12000
+ for (const r of rows) {
12001
+ const key = mode === "project" ? r.project_slug : `${r.project_slug}\0${r.assignment_slug}`;
12002
+ const existing = map.get(key);
12003
+ if (existing) {
12004
+ existing.totalTokens += r.total_tokens;
12005
+ existing.totalCost += r.total_cost;
12006
+ if (r.day > existing.lastEventDay) existing.lastEventDay = r.day;
12007
+ } else {
12008
+ map.set(key, {
12009
+ projectSlug: r.project_slug,
12010
+ assignmentSlug: mode === "project" ? "" : r.assignment_slug,
12011
+ totalTokens: r.total_tokens,
12012
+ totalCost: r.total_cost,
12013
+ lastEventDay: r.day
12014
+ });
12015
+ }
12016
+ }
12017
+ return [...map.values()].sort((a, b) => b.totalTokens - a.totalTokens);
12018
+ }
12019
+
10631
12020
  // src/dashboard/api-playbooks.ts
10632
12021
  init_api();
10633
12022
  init_parser();
10634
12023
  init_slug();
10635
12024
  init_timestamp();
10636
12025
  init_fs();
10637
- import { Router as Router8 } from "express";
10638
- import { resolve as resolve20 } from "path";
10639
- import { readFile as readFile15 } from "fs/promises";
12026
+ import { Router as Router11 } from "express";
12027
+ import { resolve as resolve23 } from "path";
12028
+ import { readFile as readFile17 } from "fs/promises";
10640
12029
  init_playbooks();
10641
12030
  function statusForPlaybookError(code) {
10642
12031
  switch (code) {
@@ -10651,7 +12040,7 @@ function statusForPlaybookError(code) {
10651
12040
  }
10652
12041
  }
10653
12042
  function createPlaybooksRouter(playbooksDir2) {
10654
- const router = Router8();
12043
+ const router = Router11();
10655
12044
  router.get("/", async (_req, res) => {
10656
12045
  try {
10657
12046
  const playbooks = await listPlaybooks(playbooksDir2);
@@ -10718,8 +12107,8 @@ function createPlaybooksRouter(playbooksDir2) {
10718
12107
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
10719
12108
  return;
10720
12109
  }
10721
- const filePath = resolve20(playbooksDir2, resolved.filename);
10722
- const content = await readFile15(filePath, "utf-8");
12110
+ const filePath = resolve23(playbooksDir2, resolved.filename);
12111
+ const content = await readFile17(filePath, "utf-8");
10723
12112
  res.json({
10724
12113
  documentType: "playbook",
10725
12114
  title: `Edit Playbook: ${resolved.slug}`,
@@ -10744,7 +12133,7 @@ function createPlaybooksRouter(playbooksDir2) {
10744
12133
  return;
10745
12134
  }
10746
12135
  await ensureDir(playbooksDir2);
10747
- const filePath = resolve20(playbooksDir2, `${slug}.md`);
12136
+ const filePath = resolve23(playbooksDir2, `${slug}.md`);
10748
12137
  if (await fileExists(filePath)) {
10749
12138
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
10750
12139
  return;
@@ -10768,7 +12157,7 @@ function createPlaybooksRouter(playbooksDir2) {
10768
12157
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
10769
12158
  return;
10770
12159
  }
10771
- const filePath = resolve20(playbooksDir2, resolved.filename);
12160
+ const filePath = resolve23(playbooksDir2, resolved.filename);
10772
12161
  await writeFileForce(filePath, content);
10773
12162
  await rebuildPlaybookManifest(playbooksDir2);
10774
12163
  res.json({ slug: resolved.slug, path: filePath });
@@ -10815,10 +12204,10 @@ init_fs_migration();
10815
12204
  init_parser2();
10816
12205
  init_fs();
10817
12206
  init_paths();
10818
- import { Router as Router9 } from "express";
10819
- import { readdir as readdir10 } from "fs/promises";
10820
- import { resolve as resolvePath, dirname as dirname4 } from "path";
10821
- import { rename as rename4, mkdir as mkdir2 } from "fs/promises";
12207
+ import { Router as Router12 } from "express";
12208
+ import { readdir as readdir11 } from "fs/promises";
12209
+ import { resolve as resolvePath, dirname as dirname5 } from "path";
12210
+ import { rename as rename5, mkdir as mkdir2 } from "fs/promises";
10822
12211
  init_slug();
10823
12212
 
10824
12213
  // src/utils/promote-todos.ts
@@ -10827,7 +12216,7 @@ init_parser2();
10827
12216
  // src/commands/create-assignment.ts
10828
12217
  init_slug();
10829
12218
  init_timestamp();
10830
- import { resolve as resolve21 } from "path";
12219
+ import { resolve as resolve24 } from "path";
10831
12220
  init_paths();
10832
12221
  init_fs();
10833
12222
  init_config2();
@@ -10905,14 +12294,14 @@ async function createAssignmentCommand(title, options) {
10905
12294
  if (options.oneOff) {
10906
12295
  const standaloneRoot = assignmentsDir();
10907
12296
  folderName = id;
10908
- assignmentDir = resolve21(standaloneRoot, folderName);
12297
+ assignmentDir = resolve24(standaloneRoot, folderName);
10909
12298
  projectSlug = null;
10910
12299
  await ensureDir(standaloneRoot);
10911
12300
  } else {
10912
12301
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
10913
12302
  projectSlug = options.project;
10914
- const projectDir = resolve21(baseDir, projectSlug);
10915
- const projectMdPath = resolve21(projectDir, "project.md");
12303
+ const projectDir = resolve24(baseDir, projectSlug);
12304
+ const projectMdPath = resolve24(projectDir, "project.md");
10916
12305
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
10917
12306
  throw new Error(
10918
12307
  `Project "${projectSlug}" not found at ${projectDir}.
@@ -10920,9 +12309,9 @@ Run 'syntaur create-project' first or use --one-off.`
10920
12309
  );
10921
12310
  }
10922
12311
  if (dependsOn.length > 0) {
10923
- const depDirBase = resolve21(projectDir, "assignments");
12312
+ const depDirBase = resolve24(projectDir, "assignments");
10924
12313
  for (const dep of dependsOn) {
10925
- const depDir = resolve21(depDirBase, dep);
12314
+ const depDir = resolve24(depDirBase, dep);
10926
12315
  if (!await fileExists(depDir)) {
10927
12316
  console.warn(
10928
12317
  `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
@@ -10931,7 +12320,7 @@ Run 'syntaur create-project' first or use --one-off.`
10931
12320
  }
10932
12321
  }
10933
12322
  folderName = assignmentSlug;
10934
- assignmentDir = resolve21(projectDir, "assignments", folderName);
12323
+ assignmentDir = resolve24(projectDir, "assignments", folderName);
10935
12324
  }
10936
12325
  if (await fileExists(assignmentDir)) {
10937
12326
  throw new Error(
@@ -10943,7 +12332,7 @@ Use --slug to specify a different slug.`
10943
12332
  const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
10944
12333
  const files = [
10945
12334
  [
10946
- resolve21(assignmentDir, "assignment.md"),
12335
+ resolve24(assignmentDir, "assignment.md"),
10947
12336
  renderAssignment({
10948
12337
  id,
10949
12338
  slug: assignmentSlug,
@@ -10961,35 +12350,35 @@ Use --slug to specify a different slug.`
10961
12350
  })
10962
12351
  ],
10963
12352
  [
10964
- resolve21(assignmentDir, "scratchpad.md"),
12353
+ resolve24(assignmentDir, "scratchpad.md"),
10965
12354
  renderScratchpad({
10966
12355
  assignmentSlug: companionAssignmentRef,
10967
12356
  timestamp
10968
12357
  })
10969
12358
  ],
10970
12359
  [
10971
- resolve21(assignmentDir, "handoff.md"),
12360
+ resolve24(assignmentDir, "handoff.md"),
10972
12361
  renderHandoff({
10973
12362
  assignmentSlug: companionAssignmentRef,
10974
12363
  timestamp
10975
12364
  })
10976
12365
  ],
10977
12366
  [
10978
- resolve21(assignmentDir, "decision-record.md"),
12367
+ resolve24(assignmentDir, "decision-record.md"),
10979
12368
  renderDecisionRecord({
10980
12369
  assignmentSlug: companionAssignmentRef,
10981
12370
  timestamp
10982
12371
  })
10983
12372
  ],
10984
12373
  [
10985
- resolve21(assignmentDir, "progress.md"),
12374
+ resolve24(assignmentDir, "progress.md"),
10986
12375
  renderProgress({
10987
12376
  assignment: companionAssignmentRef,
10988
12377
  timestamp
10989
12378
  })
10990
12379
  ],
10991
12380
  [
10992
- resolve21(assignmentDir, "comments.md"),
12381
+ resolve24(assignmentDir, "comments.md"),
10993
12382
  renderComments({
10994
12383
  assignment: companionAssignmentRef,
10995
12384
  timestamp
@@ -11038,6 +12427,12 @@ Use --slug to specify a different slug.`
11038
12427
 
11039
12428
  // src/utils/promote-todos.ts
11040
12429
  init_slug();
12430
+ var BundlePromoteError = class extends Error {
12431
+ constructor(message) {
12432
+ super(message);
12433
+ this.name = "BundlePromoteError";
12434
+ }
12435
+ };
11041
12436
  function parsePromoteTarget(target) {
11042
12437
  if (!target || typeof target !== "object") {
11043
12438
  return { ok: false, error: "target is required for new-assignment mode" };
@@ -11076,6 +12471,15 @@ async function promoteTodosToNewAssignment(groups, options) {
11076
12471
  if (!options.title.trim()) {
11077
12472
  throw new Error("Title is required.");
11078
12473
  }
12474
+ for (const group of groups) {
12475
+ for (const it of group.items) {
12476
+ if (it.bundleId !== null) {
12477
+ throw new BundlePromoteError(
12478
+ `Todo [t:${it.id}] is part of bundle b:${it.bundleId}; run \`syntaur todo bundle remove b:${it.bundleId} ${it.id}\` first.`
12479
+ );
12480
+ }
12481
+ }
12482
+ }
11079
12483
  const acceptanceCriteria = [];
11080
12484
  for (const g of groups) {
11081
12485
  for (const it of g.items) acceptanceCriteria.push(it.description);
@@ -11154,7 +12558,7 @@ function touchItem3(item) {
11154
12558
  item.updatedAt = now;
11155
12559
  }
11156
12560
  function createTodosRouter(todosDir2, broadcast, projectsDir) {
11157
- const router = Router9();
12561
+ const router = Router12();
11158
12562
  function broadcastUpdate() {
11159
12563
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11160
12564
  }
@@ -11264,13 +12668,17 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
11264
12668
  broadcastUpdate();
11265
12669
  res.json(out.result);
11266
12670
  } catch (error) {
12671
+ if (error instanceof BundlePromoteError) {
12672
+ res.status(400).json({ error: error.message });
12673
+ return;
12674
+ }
11267
12675
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to bulk-promote todos" });
11268
12676
  }
11269
12677
  });
11270
12678
  router.get("/", async (_req, res) => {
11271
12679
  try {
11272
12680
  await ensureDir(todosDir2);
11273
- const files = await readdir10(todosDir2).catch(() => []);
12681
+ const files = await readdir11(todosDir2).catch(() => []);
11274
12682
  const workspaces = [];
11275
12683
  for (const file of files) {
11276
12684
  if (typeof file !== "string") continue;
@@ -11328,7 +12736,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
11328
12736
  updatedAt: now,
11329
12737
  planDir: null,
11330
12738
  linkedAssignmentId: null,
11331
- linkedAssignmentRef: null
12739
+ linkedAssignmentRef: null,
12740
+ bundleId: null
11332
12741
  };
11333
12742
  checklist.items.push(newItem);
11334
12743
  await writeChecklist(todosDir2, checklist);
@@ -11383,8 +12792,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
11383
12792
  router.post("/:workspace/archive", async (req, res) => {
11384
12793
  try {
11385
12794
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
11386
- const { resolve: resolve25 } = await import("path");
11387
- const { readFile: readFile18 } = await import("fs/promises");
12795
+ const { resolve: resolve29 } = await import("path");
12796
+ const { readFile: readFile21 } = await import("fs/promises");
11388
12797
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
11389
12798
  const workspace = getWorkspaceParam(req.params.workspace);
11390
12799
  const checklist = await readChecklist(todosDir2, workspace);
@@ -11400,10 +12809,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
11400
12809
  (e) => e.itemIds.every((id) => completedIds.has(id))
11401
12810
  );
11402
12811
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
11403
- await ensureDir(resolve25(todosDir2, "archive"));
12812
+ await ensureDir(resolve29(todosDir2, "archive"));
11404
12813
  let archContent = "";
11405
12814
  if (await fileExists(archFile)) {
11406
- archContent = await readFile18(archFile, "utf-8");
12815
+ archContent = await readFile21(archFile, "utf-8");
11407
12816
  archContent = archContent.trimEnd() + "\n\n";
11408
12817
  } else {
11409
12818
  archContent = `---
@@ -11655,6 +13064,9 @@ workspace: ${workspace}
11655
13064
  const item = checklist.items.find((i) => i.id === id);
11656
13065
  if (!item) return { error: `Todo "${id}" not found` };
11657
13066
  if (item.status === "completed") return { error: `Todo "${id}" is already completed` };
13067
+ if (item.bundleId !== null) {
13068
+ return { error: `Todo [t:${id}] is part of bundle b:${item.bundleId}; run \`syntaur todo bundle remove b:${item.bundleId} ${id}\` first.` };
13069
+ }
11658
13070
  items.push(item);
11659
13071
  }
11660
13072
  const scopeLabel = workspace === "_global" ? "_global" : `workspace:${workspace}`;
@@ -11682,7 +13094,7 @@ workspace: ${workspace}
11682
13094
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
11683
13095
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
11684
13096
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
11685
- const { readFile: readFile18 } = await import("fs/promises");
13097
+ const { readFile: readFile21 } = await import("fs/promises");
11686
13098
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
11687
13099
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
11688
13100
  let assignmentRef;
@@ -11703,7 +13115,7 @@ workspace: ${workspace}
11703
13115
  }
11704
13116
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
11705
13117
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
11706
- let content = await readFile18(assignmentMdPath, "utf-8");
13118
+ let content = await readFile21(assignmentMdPath, "utf-8");
11707
13119
  content = appendTodosToAssignmentBody2(
11708
13120
  content,
11709
13121
  items.map((it) => ({
@@ -11743,6 +13155,10 @@ workspace: ${workspace}
11743
13155
  broadcastUpdate();
11744
13156
  res.json(result);
11745
13157
  } catch (error) {
13158
+ if (error instanceof BundlePromoteError) {
13159
+ res.status(400).json({ error: error.message });
13160
+ return;
13161
+ }
11746
13162
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
11747
13163
  }
11748
13164
  });
@@ -11810,8 +13226,8 @@ workspace: ${workspace}
11810
13226
  if (await fileExists(newPlanDir)) {
11811
13227
  return { status: 409, error: "plan dir already exists in target" };
11812
13228
  }
11813
- await mkdir2(dirname4(newPlanDir), { recursive: true });
11814
- await rename4(item.planDir, newPlanDir);
13229
+ await mkdir2(dirname5(newPlanDir), { recursive: true });
13230
+ await rename5(item.planDir, newPlanDir);
11815
13231
  item.planDir = newPlanDir;
11816
13232
  }
11817
13233
  sourceChecklist.items.splice(idx, 1);
@@ -11886,9 +13302,9 @@ init_parser2();
11886
13302
  init_fs();
11887
13303
  init_paths();
11888
13304
  init_slug();
11889
- import { Router as Router10 } from "express";
11890
- import { mkdir as mkdir3, readFile as readFile16, rename as rename5 } from "fs/promises";
11891
- import { resolve as resolve22, dirname as dirname5 } from "path";
13305
+ import { Router as Router13 } from "express";
13306
+ import { mkdir as mkdir3, readFile as readFile18, rename as rename6 } from "fs/promises";
13307
+ import { resolve as resolve25, dirname as dirname6 } from "path";
11892
13308
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
11893
13309
  function touchItem4(item) {
11894
13310
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -11903,7 +13319,7 @@ function params(req) {
11903
13319
  return req.params;
11904
13320
  }
11905
13321
  async function projectExists(projectsDir, slug) {
11906
- return fileExists(resolve22(projectsDir, slug, "project.md"));
13322
+ return fileExists(resolve25(projectsDir, slug, "project.md"));
11907
13323
  }
11908
13324
  async function ensureProjectTodosDir(projectsDir, slug) {
11909
13325
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -11920,7 +13336,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
11920
13336
  throw err;
11921
13337
  }
11922
13338
  try {
11923
- await mkdir3(resolve22(todosDir2, "archive"), { recursive: false });
13339
+ await mkdir3(resolve25(todosDir2, "archive"), { recursive: false });
11924
13340
  } catch (err) {
11925
13341
  const code = err.code;
11926
13342
  if (code === "EEXIST") return;
@@ -11936,7 +13352,7 @@ function notFound(res, slug) {
11936
13352
  res.status(404).json({ error: `Project "${slug}" not found` });
11937
13353
  }
11938
13354
  function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
11939
- const router = Router10({ mergeParams: true });
13355
+ const router = Router13({ mergeParams: true });
11940
13356
  function broadcastUpdate(projectSlug) {
11941
13357
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11942
13358
  }
@@ -12003,7 +13419,8 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
12003
13419
  updatedAt: now,
12004
13420
  planDir: null,
12005
13421
  linkedAssignmentId: null,
12006
- linkedAssignmentRef: null
13422
+ linkedAssignmentRef: null,
13423
+ bundleId: null
12007
13424
  };
12008
13425
  checklist.workspace = slug;
12009
13426
  checklist.items.push(newItem);
@@ -12108,7 +13525,7 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
12108
13525
  const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
12109
13526
  let archContent = "";
12110
13527
  if (await fileExists(archFile)) {
12111
- archContent = await readFile16(archFile, "utf-8");
13528
+ archContent = await readFile18(archFile, "utf-8");
12112
13529
  archContent = archContent.trimEnd() + "\n\n";
12113
13530
  } else {
12114
13531
  archContent = `---
@@ -12518,6 +13935,9 @@ workspace: ${slug}
12518
13935
  const item = checklist.items.find((i) => i.id === id);
12519
13936
  if (!item) return { error: `Todo "${id}" not found` };
12520
13937
  if (item.status === "completed") return { error: `Todo "${id}" is already completed` };
13938
+ if (item.bundleId !== null) {
13939
+ return { error: `Todo [t:${id}] is part of bundle b:${item.bundleId}; run \`syntaur todo bundle remove b:${item.bundleId} ${id}\` first.` };
13940
+ }
12521
13941
  items.push(item);
12522
13942
  }
12523
13943
  const scopeLabel = `project:${slug}`;
@@ -12552,17 +13972,17 @@ workspace: ${slug}
12552
13972
  if (tg.includes("/")) {
12553
13973
  const parts = tg.split("/");
12554
13974
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
12555
- assignmentDir = resolve22(projectsDir, parts[0], "assignments", parts[1]);
13975
+ assignmentDir = resolve25(projectsDir, parts[0], "assignments", parts[1]);
12556
13976
  assignmentRef = `${parts[0]}/${parts[1]}`;
12557
13977
  } 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)) {
12558
- assignmentDir = resolve22(assignmentsDirFn(), tg);
13978
+ assignmentDir = resolve25(assignmentsDirFn(), tg);
12559
13979
  assignmentRef = tg;
12560
13980
  } else {
12561
13981
  return { error: `Invalid target.assignment "${tg}"` };
12562
13982
  }
12563
- const assignmentMdPath = resolve22(assignmentDir, "assignment.md");
13983
+ const assignmentMdPath = resolve25(assignmentDir, "assignment.md");
12564
13984
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
12565
- let content = await readFile16(assignmentMdPath, "utf-8");
13985
+ let content = await readFile18(assignmentMdPath, "utf-8");
12566
13986
  content = appendTodosToAssignmentBody2(
12567
13987
  content,
12568
13988
  items.map((it) => ({
@@ -12607,6 +14027,10 @@ workspace: ${slug}
12607
14027
  broadcastUpdate(slug);
12608
14028
  res.json(result);
12609
14029
  } catch (error) {
14030
+ if (error instanceof BundlePromoteError) {
14031
+ res.status(400).json({ error: error.message });
14032
+ return;
14033
+ }
12610
14034
  if (error.code === "PROJECT_GONE") {
12611
14035
  notFound(res, getProjectIdParam(params(req).projectId));
12612
14036
  return;
@@ -12691,8 +14115,8 @@ workspace: ${slug}
12691
14115
  if (await fileExists(newPlanDir)) {
12692
14116
  return { status: 409, error: "plan dir already exists in target" };
12693
14117
  }
12694
- await mkdir3(dirname5(newPlanDir), { recursive: true });
12695
- await rename5(item.planDir, newPlanDir);
14118
+ await mkdir3(dirname6(newPlanDir), { recursive: true });
14119
+ await rename6(item.planDir, newPlanDir);
12696
14120
  item.planDir = newPlanDir;
12697
14121
  }
12698
14122
  sourceChecklist.items.splice(idx, 1);
@@ -12752,9 +14176,247 @@ workspace: ${slug}
12752
14176
  return router;
12753
14177
  }
12754
14178
 
14179
+ // src/dashboard/api-bundles.ts
14180
+ import { Router as Router14 } from "express";
14181
+ import { readdir as readdir12 } from "fs/promises";
14182
+
14183
+ // src/todos/bundle-parser.ts
14184
+ init_parser();
14185
+ init_fs();
14186
+ init_paths();
14187
+ init_parser2();
14188
+ import { randomBytes as randomBytes2 } from "crypto";
14189
+ import { readFile as readFile19 } from "fs/promises";
14190
+ var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
14191
+ var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
14192
+ var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
14193
+ var BUNDLE_LINE_REGEX = /^- b:([a-f0-9]{4})\s+<([^>]*)>\s*$/;
14194
+ function parseScopeToken(raw) {
14195
+ const idx = raw.indexOf(":");
14196
+ if (idx < 0) return null;
14197
+ const scopeRaw = raw.slice(0, idx);
14198
+ const scopeId = raw.slice(idx + 1);
14199
+ if (!SCOPE_VALUES.has(scopeRaw)) return null;
14200
+ if (!scopeId) return null;
14201
+ if (!SCOPE_ID_REGEX.test(scopeId)) return null;
14202
+ return { scope: scopeRaw, scopeId };
14203
+ }
14204
+ function parseTodosToken(raw) {
14205
+ if (!raw) return [];
14206
+ return raw.split(",").map((s) => s.trim()).filter((s) => BUNDLE_ID_REGEX.test(s));
14207
+ }
14208
+ function parseBundleLine(line) {
14209
+ const match = line.match(BUNDLE_LINE_REGEX);
14210
+ if (!match) return null;
14211
+ const id = match[1];
14212
+ const body = match[2];
14213
+ const fields = {};
14214
+ for (const pair of body.split(";")) {
14215
+ const trimmed = pair.trim();
14216
+ if (!trimmed) continue;
14217
+ const eq = trimmed.indexOf("=");
14218
+ if (eq < 0) continue;
14219
+ const key = trimmed.slice(0, eq).trim();
14220
+ const value = decodeMetaValue(trimmed.slice(eq + 1));
14221
+ fields[key] = value;
14222
+ }
14223
+ const scopeParsed = parseScopeToken(fields.scope ?? "");
14224
+ if (!scopeParsed) return null;
14225
+ const todoIds = parseTodosToken(fields.todos ?? "");
14226
+ const createdAt = fields.created ?? "";
14227
+ const updatedAt = fields.updated ?? "";
14228
+ if (!createdAt || !updatedAt) return null;
14229
+ return {
14230
+ id,
14231
+ slug: fields.slug ? fields.slug : null,
14232
+ scope: scopeParsed.scope,
14233
+ scopeId: scopeParsed.scopeId,
14234
+ todoIds,
14235
+ planDir: fields.plan ? fields.plan : null,
14236
+ branch: fields.branch ? fields.branch : null,
14237
+ worktreePath: fields.worktree ? fields.worktree : null,
14238
+ repository: fields.repository ? fields.repository : null,
14239
+ createdAt,
14240
+ updatedAt
14241
+ };
14242
+ }
14243
+ function parseBundles(content) {
14244
+ const [fm, body] = extractFrontmatter2(content);
14245
+ const version = getField(fm, "version") ?? "1";
14246
+ const bundles = [];
14247
+ for (const line of body.split("\n")) {
14248
+ const b = parseBundleLine(line);
14249
+ if (b) bundles.push(b);
14250
+ }
14251
+ return { version, bundles };
14252
+ }
14253
+ async function readBundles(todosDir2) {
14254
+ const path = bundlesPath(todosDir2);
14255
+ if (!await fileExists(path)) return [];
14256
+ const content = await readFile19(path, "utf-8");
14257
+ return parseBundles(content).bundles;
14258
+ }
14259
+
14260
+ // src/dashboard/api-bundles.ts
14261
+ init_parser2();
14262
+ init_fs();
14263
+ var WORKSPACE_REGEX3 = /^[a-z0-9_][a-z0-9-]*$/;
14264
+ function getWorkspaceParam2(value) {
14265
+ if (Array.isArray(value)) return value[0] ?? "";
14266
+ return value ?? "";
14267
+ }
14268
+ function deriveStatus(bundle, items) {
14269
+ const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
14270
+ const counts = { open: 0, in_progress: 0, blocked: 0, completed: 0, total: members.length };
14271
+ for (const m of members) counts[m.status]++;
14272
+ let status;
14273
+ if (counts.total === 0) status = "open";
14274
+ else if (counts.completed === counts.total) status = "completed";
14275
+ else if (counts.completed > 0 && counts.completed < counts.total) status = "mixed";
14276
+ else if (counts.in_progress > 0) status = "in_progress";
14277
+ else if (counts.blocked > 0) status = "blocked";
14278
+ else status = "open";
14279
+ return { status, counts };
14280
+ }
14281
+ function bundlesInScope(bundles, scope, scopeId) {
14282
+ return bundles.filter((b) => b.scope === scope && b.scopeId === scopeId);
14283
+ }
14284
+ function annotate(bundle, items) {
14285
+ const memberMap = new Map(items.map((i) => [i.id, i]));
14286
+ const members = bundle.todoIds.map((id) => memberMap.get(id)).filter((i) => i !== void 0);
14287
+ return { ...bundle, members, derivedStatus: deriveStatus(bundle, items) };
14288
+ }
14289
+ function createBundlesRouter(todosDir2, broadcast) {
14290
+ void broadcast;
14291
+ const router = Router14();
14292
+ function validateWorkspace(req, res, next) {
14293
+ const workspace = getWorkspaceParam2(req.params.workspace);
14294
+ if (workspace && !WORKSPACE_REGEX3.test(workspace)) {
14295
+ res.status(400).json({ error: `Invalid workspace name: "${workspace}"` });
14296
+ return;
14297
+ }
14298
+ next();
14299
+ }
14300
+ router.param("workspace", validateWorkspace);
14301
+ router.get("/", async (_req, res) => {
14302
+ try {
14303
+ await ensureDir(todosDir2);
14304
+ const bundles = await readBundles(todosDir2);
14305
+ const workspaceFiles = await readdir12(todosDir2).catch(() => []);
14306
+ const itemsByKey = /* @__PURE__ */ new Map();
14307
+ for (const f of workspaceFiles) {
14308
+ if (typeof f !== "string") continue;
14309
+ if (!f.endsWith(".md") || f.endsWith("-log.md")) continue;
14310
+ const workspace = f.replace(/\.md$/, "");
14311
+ const checklist = await readChecklist(todosDir2, workspace);
14312
+ itemsByKey.set(workspace, checklist.items);
14313
+ }
14314
+ if (!itemsByKey.has("_global")) itemsByKey.set("_global", (await readChecklist(todosDir2, "_global")).items);
14315
+ const scopes = [];
14316
+ const workspaceKeys = [...new Set(bundles.filter((b) => b.scope === "workspace").map((b) => b.scopeId))];
14317
+ for (const key of workspaceKeys) {
14318
+ const items = itemsByKey.get(key) ?? (await readChecklist(todosDir2, key)).items;
14319
+ scopes.push({
14320
+ scope: "workspace",
14321
+ scopeId: key,
14322
+ bundles: bundlesInScope(bundles, "workspace", key).map((b) => annotate(b, items))
14323
+ });
14324
+ }
14325
+ const globalItems = itemsByKey.get("_global") ?? [];
14326
+ scopes.push({
14327
+ scope: "global",
14328
+ scopeId: "_global",
14329
+ bundles: bundlesInScope(bundles, "global", "_global").map((b) => annotate(b, globalItems))
14330
+ });
14331
+ res.json({ scopes });
14332
+ } catch (error) {
14333
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list bundles" });
14334
+ }
14335
+ });
14336
+ router.get("/:workspace", async (req, res) => {
14337
+ try {
14338
+ const workspace = getWorkspaceParam2(req.params.workspace);
14339
+ await ensureDir(todosDir2);
14340
+ const checklist = await readChecklist(todosDir2, workspace);
14341
+ const bundles = await readBundles(todosDir2);
14342
+ const scope = workspace === "_global" ? "global" : "workspace";
14343
+ const scopeId = workspace;
14344
+ const filtered = bundlesInScope(bundles, scope, scopeId).map((b) => annotate(b, checklist.items));
14345
+ res.json({ scope, scopeId, bundles: filtered });
14346
+ } catch (error) {
14347
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list bundles" });
14348
+ }
14349
+ });
14350
+ return router;
14351
+ }
14352
+
14353
+ // src/dashboard/api-project-bundles.ts
14354
+ init_fs();
14355
+ init_paths();
14356
+ init_slug();
14357
+ import { Router as Router15 } from "express";
14358
+ import { resolve as resolve26 } from "path";
14359
+ init_parser2();
14360
+ function deriveStatus2(bundle, items) {
14361
+ const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
14362
+ const counts = { open: 0, in_progress: 0, blocked: 0, completed: 0, total: members.length };
14363
+ for (const m of members) counts[m.status]++;
14364
+ let status;
14365
+ if (counts.total === 0) status = "open";
14366
+ else if (counts.completed === counts.total) status = "completed";
14367
+ else if (counts.completed > 0 && counts.completed < counts.total) status = "mixed";
14368
+ else if (counts.in_progress > 0) status = "in_progress";
14369
+ else if (counts.blocked > 0) status = "blocked";
14370
+ else status = "open";
14371
+ return { status, counts };
14372
+ }
14373
+ function annotate2(bundle, items) {
14374
+ const memberMap = new Map(items.map((i) => [i.id, i]));
14375
+ const members = bundle.todoIds.map((id) => memberMap.get(id)).filter((i) => i !== void 0);
14376
+ return { ...bundle, members, derivedStatus: deriveStatus2(bundle, items) };
14377
+ }
14378
+ function getProjectIdParam2(value) {
14379
+ if (Array.isArray(value)) return value[0] ?? "";
14380
+ return value ?? "";
14381
+ }
14382
+ function notFound2(res, slug) {
14383
+ res.status(404).json({ error: `Project "${slug}" not found` });
14384
+ }
14385
+ function createProjectBundlesRouter(projectsDir, broadcast) {
14386
+ void broadcast;
14387
+ const router = Router15({ mergeParams: true });
14388
+ function validateProjectId(req, res, next) {
14389
+ const slug = getProjectIdParam2(req.params.projectId);
14390
+ if (!slug || !isValidSlug(slug)) {
14391
+ res.status(400).json({ error: `Invalid project slug: "${slug}"` });
14392
+ return;
14393
+ }
14394
+ next();
14395
+ }
14396
+ router.use(validateProjectId);
14397
+ router.get("/", async (req, res) => {
14398
+ try {
14399
+ const slug = getProjectIdParam2(req.params.projectId);
14400
+ const projectMd = resolve26(projectsDir, slug, "project.md");
14401
+ if (!await fileExists(projectMd)) {
14402
+ notFound2(res, slug);
14403
+ return;
14404
+ }
14405
+ const todosDir2 = projectTodosDir(projectsDir, slug);
14406
+ const bundles = await readBundles(todosDir2);
14407
+ const checklist = await readChecklist(todosDir2, slug);
14408
+ const filtered = bundles.filter((b) => b.scope === "project" && b.scopeId === slug).map((b) => annotate2(b, checklist.items));
14409
+ res.json({ scope: "project", scopeId: slug, bundles: filtered });
14410
+ } catch (error) {
14411
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to list bundles" });
14412
+ }
14413
+ });
14414
+ return router;
14415
+ }
14416
+
12755
14417
  // src/dashboard/api-backup.ts
12756
14418
  init_config2();
12757
- import { Router as Router11 } from "express";
14419
+ import { Router as Router16 } from "express";
12758
14420
 
12759
14421
  // src/utils/github-backup.ts
12760
14422
  init_paths();
@@ -12762,8 +14424,8 @@ init_fs();
12762
14424
  init_config2();
12763
14425
  import { execFile as execFile2 } from "child_process";
12764
14426
  import { promisify as promisify2 } from "util";
12765
- import { cp, mkdtemp, rm as rm2, readFile as readFile17, writeFile as writeFile4, unlink as unlink4, stat, open as open2, rename as rename6 } from "fs/promises";
12766
- import { resolve as resolve23, join as join2 } from "path";
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";
14428
+ import { resolve as resolve27, join as join2 } from "path";
12767
14429
  import { tmpdir } from "os";
12768
14430
  var exec2 = promisify2(execFile2);
12769
14431
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -12803,7 +14465,7 @@ async function resolveCategoryPath(category) {
12803
14465
  case "servers":
12804
14466
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
12805
14467
  case "config":
12806
- return { sourcePath: resolve23(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
14468
+ return { sourcePath: resolve27(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
12807
14469
  }
12808
14470
  }
12809
14471
  async function checkGitInstalled() {
@@ -12814,7 +14476,7 @@ async function checkGitInstalled() {
12814
14476
  }
12815
14477
  }
12816
14478
  async function acquireLock() {
12817
- const lockPath = resolve23(syntaurRoot(), LOCK_FILE_NAME);
14479
+ const lockPath = resolve27(syntaurRoot(), LOCK_FILE_NAME);
12818
14480
  await ensureDir(syntaurRoot());
12819
14481
  try {
12820
14482
  const handle = await open2(lockPath, "wx");
@@ -12823,7 +14485,7 @@ async function acquireLock() {
12823
14485
  return lockPath;
12824
14486
  } catch (err) {
12825
14487
  if (err.code === "EEXIST") {
12826
- const pid = await readFile17(lockPath, "utf-8").catch(() => "");
14488
+ const pid = await readFile20(lockPath, "utf-8").catch(() => "");
12827
14489
  throw new Error(
12828
14490
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
12829
14491
  );
@@ -12833,7 +14495,7 @@ async function acquireLock() {
12833
14495
  }
12834
14496
  async function releaseLock(lockPath) {
12835
14497
  try {
12836
- await unlink4(lockPath);
14498
+ await unlink5(lockPath);
12837
14499
  } catch {
12838
14500
  }
12839
14501
  }
@@ -12861,7 +14523,7 @@ async function copyRecursive(src, dest) {
12861
14523
  await ensureDir(dest);
12862
14524
  await cp(src, dest, { recursive: true, force: true });
12863
14525
  } else {
12864
- await ensureDir(resolve23(dest, ".."));
14526
+ await ensureDir(resolve27(dest, ".."));
12865
14527
  await cp(src, dest, { force: true });
12866
14528
  }
12867
14529
  }
@@ -12870,7 +14532,7 @@ function resolveCategoriesStrict(csv) {
12870
14532
  return parseCategoriesStrict(parts);
12871
14533
  }
12872
14534
  async function readSanitizedConfig(configPath) {
12873
- const content = await readFile17(configPath, "utf-8");
14535
+ const content = await readFile20(configPath, "utf-8");
12874
14536
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
12875
14537
  }
12876
14538
  async function backupToGithub(overrides) {
@@ -12899,9 +14561,9 @@ async function backupToGithub(overrides) {
12899
14561
  const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
12900
14562
  const destPath = join2(tmpDir, repoPath);
12901
14563
  if (isFile) {
12902
- await rm2(destPath, { force: true });
14564
+ await rm3(destPath, { force: true });
12903
14565
  } else {
12904
- await rm2(destPath, { recursive: true, force: true });
14566
+ await rm3(destPath, { recursive: true, force: true });
12905
14567
  }
12906
14568
  if (!await fileExists(sourcePath)) {
12907
14569
  console.warn(`Category "${category}": no local data at ${sourcePath}; backup will reflect deletion.`);
@@ -12909,8 +14571,8 @@ async function backupToGithub(overrides) {
12909
14571
  }
12910
14572
  if (category === "config") {
12911
14573
  const sanitized = await readSanitizedConfig(sourcePath);
12912
- await ensureDir(resolve23(destPath, ".."));
12913
- await writeFile4(destPath, sanitized, "utf-8");
14574
+ await ensureDir(resolve27(destPath, ".."));
14575
+ await writeFile5(destPath, sanitized, "utf-8");
12914
14576
  } else {
12915
14577
  await copyRecursive(sourcePath, destPath);
12916
14578
  }
@@ -12955,7 +14617,7 @@ async function backupToGithub(overrides) {
12955
14617
  };
12956
14618
  } finally {
12957
14619
  if (tmpDir) {
12958
- await rm2(tmpDir, { recursive: true, force: true }).catch(() => {
14620
+ await rm3(tmpDir, { recursive: true, force: true }).catch(() => {
12959
14621
  });
12960
14622
  }
12961
14623
  await releaseLock(lockPath);
@@ -12963,18 +14625,18 @@ async function backupToGithub(overrides) {
12963
14625
  }
12964
14626
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
12965
14627
  if (isFile) {
12966
- await ensureDir(resolve23(localPath, ".."));
14628
+ await ensureDir(resolve27(localPath, ".."));
12967
14629
  await cp(repoSrcPath, localPath, { force: true });
12968
14630
  return;
12969
14631
  }
12970
14632
  const stagingPath = `${localPath}.syntaur-restore-staging`;
12971
14633
  const backupPath = `${localPath}.syntaur-restore-backup`;
12972
- await rm2(stagingPath, { recursive: true, force: true });
14634
+ await rm3(stagingPath, { recursive: true, force: true });
12973
14635
  const backupExistsBefore = await fileExists(backupPath);
12974
14636
  const localExistsBefore = await fileExists(localPath);
12975
14637
  if (backupExistsBefore) {
12976
14638
  if (!localExistsBefore) {
12977
- await rename6(backupPath, localPath);
14639
+ await rename7(backupPath, localPath);
12978
14640
  } else {
12979
14641
  throw new Error(
12980
14642
  `Cannot restore "${localPath}": a stale crash-recovery backup exists at ${backupPath} while the current path also exists. Inspect both and remove the one you don't need, then retry.`
@@ -12986,18 +14648,18 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
12986
14648
  await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
12987
14649
  const localExists = await fileExists(localPath);
12988
14650
  if (localExists) {
12989
- await rename6(localPath, backupPath);
14651
+ await rename7(localPath, backupPath);
12990
14652
  localMovedAside = true;
12991
14653
  }
12992
- await rename6(stagingPath, localPath);
12993
- await rm2(backupPath, { recursive: true, force: true }).catch(() => {
14654
+ await rename7(stagingPath, localPath);
14655
+ await rm3(backupPath, { recursive: true, force: true }).catch(() => {
12994
14656
  });
12995
14657
  } catch (err) {
12996
14658
  if (localMovedAside && await fileExists(backupPath)) {
12997
- await rename6(backupPath, localPath).catch(() => {
14659
+ await rename7(backupPath, localPath).catch(() => {
12998
14660
  });
12999
14661
  }
13000
- await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
14662
+ await rm3(stagingPath, { recursive: true, force: true }).catch(() => {
13001
14663
  });
13002
14664
  throw err;
13003
14665
  }
@@ -13056,7 +14718,7 @@ async function restoreFromGithub(overrides) {
13056
14718
  };
13057
14719
  } finally {
13058
14720
  if (tmpDir) {
13059
- await rm2(tmpDir, { recursive: true, force: true }).catch(() => {
14721
+ await rm3(tmpDir, { recursive: true, force: true }).catch(() => {
13060
14722
  });
13061
14723
  }
13062
14724
  await releaseLock(lockPath);
@@ -13064,7 +14726,7 @@ async function restoreFromGithub(overrides) {
13064
14726
  }
13065
14727
  async function getBackupStatus() {
13066
14728
  const config = await readConfig();
13067
- const lockPath = resolve23(syntaurRoot(), LOCK_FILE_NAME);
14729
+ const lockPath = resolve27(syntaurRoot(), LOCK_FILE_NAME);
13068
14730
  const locked = await fileExists(lockPath);
13069
14731
  return {
13070
14732
  repo: config.backup?.repo ?? null,
@@ -13077,7 +14739,7 @@ async function getBackupStatus() {
13077
14739
 
13078
14740
  // src/dashboard/api-backup.ts
13079
14741
  function createBackupRouter() {
13080
- const router = Router11();
14742
+ const router = Router16();
13081
14743
  router.get("/", async (_req, res) => {
13082
14744
  try {
13083
14745
  const status = await getBackupStatus();
@@ -13402,10 +15064,11 @@ function createDashboardServer(options) {
13402
15064
  console.error("Session migration from markdown failed:", err);
13403
15065
  });
13404
15066
  initLeasesDb();
15067
+ initUsageDb();
13405
15068
  (async () => {
13406
15069
  try {
13407
15070
  const configResult = await migrateLegacyConfig(
13408
- resolve24(syntaurRoot(), "config.md")
15071
+ resolve28(syntaurRoot(), "config.md")
13409
15072
  );
13410
15073
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
13411
15074
  const summary = summarizeMigration(projectResult, configResult);
@@ -13440,55 +15103,19 @@ function createDashboardServer(options) {
13440
15103
  res.status(500).json({ error: "Failed to get help content" });
13441
15104
  }
13442
15105
  });
13443
- app.get("/api/config/statuses", async (_req, res) => {
13444
- try {
13445
- const config = await getStatusConfig();
13446
- res.json({
13447
- statuses: config.statuses,
13448
- order: config.order,
13449
- transitions: config.transitions,
13450
- custom: config.custom
13451
- });
13452
- } catch (error) {
13453
- console.error("Error getting status config:", error);
13454
- res.status(500).json({ error: "Failed to get status config" });
13455
- }
13456
- });
13457
- app.post("/api/config/statuses", async (req, res) => {
13458
- try {
13459
- const { statuses, order, transitions } = req.body;
13460
- if (!Array.isArray(statuses) || !Array.isArray(order) || !Array.isArray(transitions)) {
13461
- res.status(400).json({ error: "Request body must include statuses, order, and transitions arrays" });
13462
- return;
13463
- }
13464
- await writeStatusConfig({ statuses, order, transitions });
13465
- clearStatusConfigCache();
13466
- const config = await getStatusConfig();
13467
- res.json({
13468
- statuses: config.statuses,
13469
- order: config.order,
13470
- transitions: config.transitions,
13471
- custom: config.custom
13472
- });
13473
- } catch (error) {
13474
- console.error("Error saving status config:", error);
13475
- res.status(500).json({ error: "Failed to save status config" });
13476
- }
13477
- });
13478
- app.delete("/api/config/statuses", async (_req, res) => {
15106
+ app.use("/api/config/statuses", createStatusConfigRouter(projectsDir, assignmentsDir2));
15107
+ app.get("/api/config/types", async (_req, res) => {
13479
15108
  try {
13480
- await deleteStatusConfig();
13481
- clearStatusConfigCache();
13482
- const config = await getStatusConfig();
15109
+ const config = await readConfig();
15110
+ const types = getAssignmentTypes(config);
13483
15111
  res.json({
13484
- statuses: config.statuses,
13485
- order: config.order,
13486
- transitions: config.transitions,
13487
- custom: config.custom
15112
+ definitions: types.definitions,
15113
+ default: types.default,
15114
+ custom: config.types !== null
13488
15115
  });
13489
15116
  } catch (error) {
13490
- console.error("Error resetting status config:", error);
13491
- res.status(500).json({ error: "Failed to reset status config" });
15117
+ console.error("Error getting types config:", error);
15118
+ res.status(500).json({ error: "Failed to get types config" });
13492
15119
  }
13493
15120
  });
13494
15121
  const THEME_PRESET_SLUGS = ["default", "ocean", "forest", "sunset"];
@@ -13602,7 +15229,7 @@ function createDashboardServer(options) {
13602
15229
  }
13603
15230
  });
13604
15231
  const VIEW_PREFS_LOCK = "vp:global";
13605
- const FILTER_KEYS = /* @__PURE__ */ new Set(["status", "priority", "assignee", "project", "activity"]);
15232
+ const FILTER_KEYS = /* @__PURE__ */ new Set(["status", "type", "priority", "assignee", "project", "activity"]);
13606
15233
  const GLOBAL_KEYS = /* @__PURE__ */ new Set(["defaultView", "sortField", "sortDirection", "density", "grouping", "filters"]);
13607
15234
  const SCOPE_KEYS = /* @__PURE__ */ new Set(["defaultView", "sortField", "sortDirection", "grouping", "filters"]);
13608
15235
  const ROOT_KEYS = /* @__PURE__ */ new Set(["global", "projects"]);
@@ -13621,7 +15248,7 @@ function createDashboardServer(options) {
13621
15248
  const unknown = unknownKey(obj, FILTER_KEYS, "filters");
13622
15249
  if (unknown) return { ok: false, error: unknown };
13623
15250
  const out = {};
13624
- for (const key of ["status", "priority", "assignee", "project"]) {
15251
+ for (const key of ["status", "type", "priority", "assignee", "project"]) {
13625
15252
  if (obj[key] !== void 0) {
13626
15253
  if (!isFilterString(obj[key])) return { ok: false, error: `filters.${key} must be a non-empty string` };
13627
15254
  out[key] = obj[key];
@@ -13764,6 +15391,8 @@ function createDashboardServer(options) {
13764
15391
  res.status(500).json({ error: "Failed to reset view-prefs" });
13765
15392
  }
13766
15393
  });
15394
+ app.use("/api/saved-views", createSavedViewsRouter());
15395
+ app.use("/api/dashboard", createDashboardLayoutRouter());
13767
15396
  app.get("/api/projects", async (req, res) => {
13768
15397
  try {
13769
15398
  let projects = await listProjects(projectsDir);
@@ -13912,6 +15541,7 @@ function createDashboardServer(options) {
13912
15541
  app.use(createWriteRouter(projectsDir, assignmentsDir2, todosDir2));
13913
15542
  app.use("/api/servers", createServersRouter(serversDir2, projectsDir, assignmentsDir2));
13914
15543
  app.use("/api/leases", createLeasesRouter(broadcast));
15544
+ app.use("/api/usage", createUsageRouter());
13915
15545
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2));
13916
15546
  app.use("/api/config/agents", createAgentsRouter());
13917
15547
  app.use("/api/launch", createLaunchPreflightRouter());
@@ -13936,17 +15566,19 @@ function createDashboardServer(options) {
13936
15566
  });
13937
15567
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast, projectsDir));
13938
15568
  app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast, todosDir2));
15569
+ app.use("/api/bundles", createBundlesRouter(todosDir2, broadcast));
15570
+ app.use("/api/projects/:projectId/bundles", createProjectBundlesRouter(projectsDir, broadcast));
13939
15571
  app.use("/api/backup", createBackupRouter());
13940
15572
  if (serveStaticUi && dashboardDistPath) {
13941
15573
  const sendOpts = { dotfiles: "allow" };
13942
- app.use("/assets", express.static(resolve24(dashboardDistPath, "assets"), sendOpts));
15574
+ app.use("/assets", express.static(resolve28(dashboardDistPath, "assets"), sendOpts));
13943
15575
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
13944
15576
  app.get("{*path}", async (req, res) => {
13945
15577
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
13946
15578
  res.status(404).json({ error: "Not Found" });
13947
15579
  return;
13948
15580
  }
13949
- const indexPath = resolve24(dashboardDistPath, "index.html");
15581
+ const indexPath = resolve28(dashboardDistPath, "index.html");
13950
15582
  if (!await fileExists(indexPath)) {
13951
15583
  res.status(503).send(
13952
15584
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -13970,7 +15602,7 @@ function createDashboardServer(options) {
13970
15602
  serversDir: serversDir2,
13971
15603
  playbooksDir: playbooksDir2,
13972
15604
  todosDir: todosDir2,
13973
- dbPath: resolve24(syntaurRoot(), "syntaur.db"),
15605
+ dbPath: resolve28(syntaurRoot(), "syntaur.db"),
13974
15606
  onMessage: broadcast
13975
15607
  });
13976
15608
  startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
@@ -13985,8 +15617,8 @@ function createDashboardServer(options) {
13985
15617
  }
13986
15618
  });
13987
15619
  server.listen(port, () => {
13988
- const portFile = resolve24(syntaurRoot(), "dashboard-port");
13989
- writeFile5(portFile, String(port), "utf-8").catch(() => {
15620
+ const portFile = resolve28(syntaurRoot(), "dashboard-port");
15621
+ writeFile6(portFile, String(port), "utf-8").catch(() => {
13990
15622
  });
13991
15623
  resolvePromise();
13992
15624
  });
@@ -13999,12 +15631,13 @@ function createDashboardServer(options) {
13999
15631
  }
14000
15632
  closeSessionDb();
14001
15633
  closeLeasesDb();
15634
+ closeUsageDb();
14002
15635
  for (const client of clients) {
14003
15636
  client.terminate();
14004
15637
  }
14005
15638
  clients.clear();
14006
- const portFile = resolve24(syntaurRoot(), "dashboard-port");
14007
- await unlink5(portFile).catch(() => {
15639
+ const portFile = resolve28(syntaurRoot(), "dashboard-port");
15640
+ await unlink6(portFile).catch(() => {
14008
15641
  });
14009
15642
  server.closeAllConnections?.();
14010
15643
  return new Promise((resolvePromise) => {