syntaur 0.6.0 → 0.6.1

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 (62) hide show
  1. package/dashboard/dist/assets/{_basePickBy-BQIP1Ca7.js → _basePickBy-DTYUlCEg.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-BnBWRwT7.js → _baseUniq-C0Y4HRd5.js} +1 -1
  3. package/dashboard/dist/assets/{arc-BYWL4eq0.js → arc-BFx2eqN9.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CD_SWPSa.js → architectureDiagram-2XIMDMQ5-Erol1JD6.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BS1ZbFBU.js → blockDiagram-WCTKOSBZ-kSkh6VkS.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-D99yg-l2.js → c4Diagram-IC4MRINW-C04oKzvX.js} +1 -1
  7. package/dashboard/dist/assets/channel-C82tBKZ7.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-BkN9IORC.js → chunk-4BX2VUAB-C3t0tXt-.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-BQPHWefV.js → chunk-55IACEB6-2cnyEL0b.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-CNcExMdx.js → chunk-FMBD7UC4-DIY9MTNi.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-LXBmftkC.js → chunk-JSJVCQXG-Cw8fpqpE.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-Tqi7zNqq.js → chunk-KX2RTZJC-BAhd66XV.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DkMbx-rW.js → chunk-NQ4KR5QH-RzXwoxk3.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-BlrRCfkJ.js → chunk-QZHKN3VN-Dgri4sGz.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-of3XBzMu.js → chunk-WL4C6EOR-DYLj9JRa.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-STOZ51tg.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-STOZ51tg.js +1 -0
  18. package/dashboard/dist/assets/clone-TzhWk-Bj.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-BlIiyO76.js → cose-bilkent-S5V4N54A-DfY_Fnfu.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-CYQjSI9N.js → dagre-KLK3FWXG-CyTKIVSK.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-BZHzTKct.js → diagram-E7M64L7V-Krub7Xxo.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-kMP3WqBV.js → diagram-IFDJBPK2-giUl9uHz.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-BWSHyFOv.js → diagram-P4PSJMXO-oAtnO3C9.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-B5HrvsPP.js → erDiagram-INFDFZHY-eYaVjXqo.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-Dm4ewP7w.js → flowDiagram-PKNHOUZH-or5S0_Sb.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DB3k27zu.js → ganttDiagram-A5KZAMGK-C9R1lsme.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-G7y6Ey-m.js → gitGraphDiagram-K3NZZRJ6-BQwsDzvp.js} +1 -1
  28. package/dashboard/dist/assets/{graph-CaM4i6vq.js → graph-EQOX1wg8.js} +1 -1
  29. package/dashboard/dist/assets/index-Cy7yjuqO.js +500 -0
  30. package/dashboard/dist/assets/index-u80fISp0.css +1 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-JNTUbTjg.js → infoDiagram-LFFYTUFH-BjLlQWxk.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-BZJt1ht8.js → ishikawaDiagram-PHBUUO56-BNjydh4j.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DPcqvl9A.js → journeyDiagram-4ABVD52K-DNzE7TgQ.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-D1D7AuOV.js → kanban-definition-K7BYSVSG-FbNvGx6i.js} +1 -1
  35. package/dashboard/dist/assets/{layout-BTOh3EDT.js → layout-B2yZvlWs.js} +1 -1
  36. package/dashboard/dist/assets/{linear-MbCpC_Cg.js → linear-p68yY_14.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-CYbhqlNy.js → mermaid.core-D558akcW.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-CwYCISFH.js → mindmap-definition-YRQLILUH-CZPgesSK.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-5qfZ73SG.js → pieDiagram-SKSYHLDU-CdXMWspp.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-WI8y1sQ_.js → quadrantDiagram-337W2JSQ-D7tq22ZY.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BFlD0ZTS.js → requirementDiagram-Z7DCOOCP-ByZxUSmd.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-Bdckv1Se.js → sankeyDiagram-WA2Y5GQK-CZon9rRY.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DgzxKAlZ.js → sequenceDiagram-2WXFIKYE-nbELB6rb.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DO4OXahC.js → stateDiagram-RAJIS63D-D_OPKr5B.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-4pLM5B3m.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BBB01JWw.js → timeline-definition-YZTLITO2-Cxuvk1D2.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-Dr0jb8op.js → treemap-KZPCXAKY-C0CRpL92.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-D40KFl2o.js → vennDiagram-LZ73GAT5-DijCj6M3.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DBUmWQfT.js → xychartDiagram-JWTSCODW-CdpE0oRi.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +419 -38
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +577 -88
  54. package/dist/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/dashboard/dist/assets/channel-Df6VrFK5.js +0 -1
  57. package/dashboard/dist/assets/classDiagram-VBA2DB6C-CyfzumTY.js +0 -1
  58. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CyfzumTY.js +0 -1
  59. package/dashboard/dist/assets/clone-CMs4Aqrx.js +0 -1
  60. package/dashboard/dist/assets/index-B4QMu-Oq.css +0 -1
  61. package/dashboard/dist/assets/index-BBWZjPBC.js +0 -495
  62. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-o8bgX-J3.js +0 -1
@@ -8172,37 +8172,62 @@ init_fs_migration();
8172
8172
  // src/dashboard/api-todos.ts
8173
8173
  init_parser2();
8174
8174
  init_fs();
8175
+ init_paths();
8175
8176
  import { Router as Router5 } from "express";
8176
8177
  import { readdir as readdir8 } from "fs/promises";
8177
- var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
8178
- function getWorkspaceParam(value) {
8179
- if (Array.isArray(value)) {
8180
- return value[0] ?? "";
8181
- }
8182
- return value ?? "";
8183
- }
8178
+ import { resolve as resolvePath, dirname as dirname3 } from "path";
8179
+ import { rename as rename3, mkdir as mkdir2 } from "fs/promises";
8180
+
8181
+ // src/dashboard/todos-locks.ts
8184
8182
  var writeLocks = /* @__PURE__ */ new Map();
8185
8183
  function withLock(lockKey, fn) {
8186
8184
  const prev = writeLocks.get(lockKey) ?? Promise.resolve();
8187
8185
  const next = prev.then(fn);
8188
- writeLocks.set(lockKey, next.then(() => {
8189
- }, () => {
8190
- }));
8186
+ writeLocks.set(
8187
+ lockKey,
8188
+ next.then(
8189
+ () => {
8190
+ },
8191
+ () => {
8192
+ }
8193
+ )
8194
+ );
8191
8195
  return next;
8192
8196
  }
8193
8197
  function wsLock(workspace, fn) {
8194
8198
  return withLock(`ws:${workspace}`, fn);
8195
8199
  }
8200
+ function projLock(slug, fn) {
8201
+ return withLock(`proj:${slug}`, fn);
8202
+ }
8203
+ function withTwoLocks(keyA, keyB, fn) {
8204
+ if (keyA === keyB) return withLock(keyA, fn);
8205
+ const [first, second] = keyA < keyB ? [keyA, keyB] : [keyB, keyA];
8206
+ return withLock(first, () => withLock(second, fn));
8207
+ }
8208
+
8209
+ // src/dashboard/api-todos.ts
8210
+ init_slug();
8211
+ var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
8212
+ function getWorkspaceParam(value) {
8213
+ if (Array.isArray(value)) {
8214
+ return value[0] ?? "";
8215
+ }
8216
+ return value ?? "";
8217
+ }
8196
8218
  function touchItem(item) {
8197
8219
  const now = (/* @__PURE__ */ new Date()).toISOString();
8198
8220
  if (item.createdAt === null) item.createdAt = now;
8199
8221
  item.updatedAt = now;
8200
8222
  }
8201
- function createTodosRouter(todosDir2, broadcast) {
8223
+ function createTodosRouter(todosDir2, broadcast, projectsDir) {
8202
8224
  const router = Router5();
8203
8225
  function broadcastUpdate() {
8204
8226
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8205
8227
  }
8228
+ function broadcastProject(slug) {
8229
+ broadcast({ type: "todos-updated", projectSlug: slug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8230
+ }
8206
8231
  function validateWorkspace(req, res, next) {
8207
8232
  const workspace = getWorkspaceParam(req.params.workspace);
8208
8233
  if (workspace && !WORKSPACE_REGEX.test(workspace)) {
@@ -8601,7 +8626,7 @@ workspace: ${workspace}
8601
8626
  items.push(item);
8602
8627
  }
8603
8628
  const scopeLabel = workspace === "_global" ? "_global" : `workspace:${workspace}`;
8604
- const { resolve: resolvePath } = await import("path");
8629
+ const { resolve: resolvePath2 } = await import("path");
8605
8630
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8606
8631
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
8607
8632
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
@@ -8631,18 +8656,18 @@ workspace: ${workspace}
8631
8656
  const parts = tg.split("/");
8632
8657
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
8633
8658
  const config = await readConfig2();
8634
- assignmentDir = resolvePath(config.defaultProjectDir, parts[0], "assignments", parts[1]);
8659
+ assignmentDir = resolvePath2(config.defaultProjectDir, parts[0], "assignments", parts[1]);
8635
8660
  assignmentRef = `${parts[0]}/${parts[1]}`;
8636
8661
  } 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)) {
8637
- assignmentDir = resolvePath(assignmentsDirFn(), tg);
8662
+ assignmentDir = resolvePath2(assignmentsDirFn(), tg);
8638
8663
  assignmentRef = tg;
8639
8664
  } else {
8640
8665
  return { error: `Invalid target.assignment "${tg}"` };
8641
8666
  }
8642
- const assignmentMdPath2 = resolvePath(assignmentDir, "assignment.md");
8667
+ const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
8643
8668
  if (!await fileExists2(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
8644
8669
  }
8645
- const assignmentMdPath = resolvePath(assignmentDir, "assignment.md");
8670
+ const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
8646
8671
  let content = await readFile15(assignmentMdPath, "utf-8");
8647
8672
  content = appendTodosToAssignmentBody2(
8648
8673
  content,
@@ -8686,6 +8711,115 @@ workspace: ${workspace}
8686
8711
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
8687
8712
  }
8688
8713
  });
8714
+ router.post("/:workspace/:id/move", async (req, res) => {
8715
+ try {
8716
+ const sourceWs = getWorkspaceParam(req.params.workspace);
8717
+ const id = req.params.id;
8718
+ const to = req.body?.to;
8719
+ if (!to || typeof to !== "object") {
8720
+ res.status(400).json({ error: "body.to is required" });
8721
+ return;
8722
+ }
8723
+ const targetCount = [Boolean(to.workspace), Boolean(to.project), Boolean(to.global)].filter(Boolean).length;
8724
+ if (targetCount !== 1) {
8725
+ res.status(400).json({ error: "body.to must specify exactly one of workspace, project, or global" });
8726
+ return;
8727
+ }
8728
+ if (to.project && !isValidSlug(to.project)) {
8729
+ res.status(400).json({ error: `Invalid target project slug: "${to.project}"` });
8730
+ return;
8731
+ }
8732
+ if (to.workspace && !WORKSPACE_REGEX.test(to.workspace)) {
8733
+ res.status(400).json({ error: `Invalid target workspace name: "${to.workspace}"` });
8734
+ return;
8735
+ }
8736
+ let target;
8737
+ if (to.global) {
8738
+ target = { kind: "workspace", id: "_global", todosPath: todosDir2, lockKey: "ws:_global" };
8739
+ } else if (to.workspace) {
8740
+ target = { kind: "workspace", id: to.workspace, todosPath: todosDir2, lockKey: `ws:${to.workspace}` };
8741
+ } else {
8742
+ if (!projectsDir) {
8743
+ res.status(500).json({ error: "Server not configured with projectsDir; cannot move to project scope" });
8744
+ return;
8745
+ }
8746
+ const slug = to.project;
8747
+ const projectMd = resolvePath(projectsDir, slug, "project.md");
8748
+ if (!await fileExists(projectMd)) {
8749
+ res.status(404).json({ error: `Target project "${slug}" not found` });
8750
+ return;
8751
+ }
8752
+ target = {
8753
+ kind: "project",
8754
+ id: slug,
8755
+ todosPath: projectTodosDir(projectsDir, slug),
8756
+ lockKey: `proj:${slug}`
8757
+ };
8758
+ }
8759
+ const sourceLockKey = `ws:${sourceWs}`;
8760
+ if (sourceLockKey === target.lockKey) {
8761
+ res.status(400).json({ error: "cannot move to the same scope" });
8762
+ return;
8763
+ }
8764
+ const result = await withTwoLocks(sourceLockKey, target.lockKey, async () => {
8765
+ const sourceChecklist = await readChecklist(todosDir2, sourceWs);
8766
+ const targetChecklist = await readChecklist(target.todosPath, target.id);
8767
+ const idx = sourceChecklist.items.findIndex((i) => i.id === id);
8768
+ if (idx === -1) return { status: 404, error: `Todo "${id}" not found` };
8769
+ if (targetChecklist.items.some((i) => i.id === id)) {
8770
+ return { status: 409, error: "id already exists in target" };
8771
+ }
8772
+ const item = sourceChecklist.items[idx];
8773
+ if (item.planDir) {
8774
+ const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
8775
+ if (await fileExists(newPlanDir)) {
8776
+ return { status: 409, error: "plan dir already exists in target" };
8777
+ }
8778
+ await mkdir2(dirname3(newPlanDir), { recursive: true });
8779
+ await rename3(item.planDir, newPlanDir);
8780
+ item.planDir = newPlanDir;
8781
+ }
8782
+ sourceChecklist.items.splice(idx, 1);
8783
+ targetChecklist.items.push(item);
8784
+ await writeChecklist(todosDir2, sourceChecklist);
8785
+ await writeChecklist(target.todosPath, targetChecklist);
8786
+ const sourceLabel = sourceWs === "_global" ? "_global" : `workspace:${sourceWs}`;
8787
+ const targetLabel = target.kind === "project" ? `project:${target.id}` : target.id === "_global" ? "_global" : `workspace:${target.id}`;
8788
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
8789
+ await appendLogEntry2(todosDir2, sourceWs, {
8790
+ timestamp: ts,
8791
+ itemIds: [id],
8792
+ items: item.description,
8793
+ session: null,
8794
+ branch: item.branch || null,
8795
+ summary: `Moved to ${targetLabel}`,
8796
+ blockers: null,
8797
+ status: null
8798
+ });
8799
+ await appendLogEntry2(target.todosPath, target.id, {
8800
+ timestamp: ts,
8801
+ itemIds: [id],
8802
+ items: item.description,
8803
+ session: null,
8804
+ branch: item.branch || null,
8805
+ summary: `Moved from ${sourceLabel}`,
8806
+ blockers: null,
8807
+ status: null
8808
+ });
8809
+ return { status: 200, item };
8810
+ });
8811
+ if (result.status !== 200) {
8812
+ res.status(result.status).json({ error: result.error });
8813
+ return;
8814
+ }
8815
+ broadcastUpdate();
8816
+ if (target.kind === "project") broadcastProject(target.id);
8817
+ else broadcastUpdate();
8818
+ res.json({ moved: id, to: target });
8819
+ } catch (error) {
8820
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to move todo" });
8821
+ }
8822
+ });
8689
8823
  router.post("/:workspace/:id/unblock", async (req, res) => {
8690
8824
  try {
8691
8825
  const workspace = getWorkspaceParam(req.params.workspace);
@@ -8718,18 +8852,9 @@ init_fs();
8718
8852
  init_paths();
8719
8853
  init_slug();
8720
8854
  import { Router as Router6 } from "express";
8721
- import { mkdir as mkdir2, readFile as readFile13 } from "fs/promises";
8722
- import { resolve as resolve17 } from "path";
8723
- var writeLocks2 = /* @__PURE__ */ new Map();
8724
- function projLock(slug, fn) {
8725
- const key = `proj:${slug}`;
8726
- const prev = writeLocks2.get(key) ?? Promise.resolve();
8727
- const next = prev.then(fn);
8728
- writeLocks2.set(key, next.then(() => {
8729
- }, () => {
8730
- }));
8731
- return next;
8732
- }
8855
+ import { mkdir as mkdir3, readFile as readFile13, rename as rename4 } from "fs/promises";
8856
+ import { resolve as resolve17, dirname as dirname4 } from "path";
8857
+ var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
8733
8858
  function touchItem2(item) {
8734
8859
  const now = (/* @__PURE__ */ new Date()).toISOString();
8735
8860
  if (item.createdAt === null) item.createdAt = now;
@@ -8748,7 +8873,7 @@ async function projectExists(projectsDir, slug) {
8748
8873
  async function ensureProjectTodosDir(projectsDir, slug) {
8749
8874
  const todosDir2 = projectTodosDir(projectsDir, slug);
8750
8875
  try {
8751
- await mkdir2(todosDir2, { recursive: false });
8876
+ await mkdir3(todosDir2, { recursive: false });
8752
8877
  } catch (err) {
8753
8878
  const code = err.code;
8754
8879
  if (code === "EEXIST") return;
@@ -8760,7 +8885,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
8760
8885
  throw err;
8761
8886
  }
8762
8887
  try {
8763
- await mkdir2(resolve17(todosDir2, "archive"), { recursive: false });
8888
+ await mkdir3(resolve17(todosDir2, "archive"), { recursive: false });
8764
8889
  } catch (err) {
8765
8890
  const code = err.code;
8766
8891
  if (code === "EEXIST") return;
@@ -8775,11 +8900,14 @@ async function ensureProjectTodosDir(projectsDir, slug) {
8775
8900
  function notFound(res, slug) {
8776
8901
  res.status(404).json({ error: `Project "${slug}" not found` });
8777
8902
  }
8778
- function createProjectTodosRouter(projectsDir, broadcast) {
8903
+ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
8779
8904
  const router = Router6({ mergeParams: true });
8780
8905
  function broadcastUpdate(projectSlug) {
8781
8906
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8782
8907
  }
8908
+ function broadcastWorkspace() {
8909
+ broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8910
+ }
8783
8911
  function validateProjectId(req, res, next) {
8784
8912
  const slug = getProjectIdParam(params(req).projectId);
8785
8913
  if (!slug || !isValidSlug(slug)) {
@@ -9327,6 +9455,259 @@ workspace: ${slug}
9327
9455
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to unblock todo" });
9328
9456
  }
9329
9457
  });
9458
+ router.post("/promote", async (req, res) => {
9459
+ try {
9460
+ const slug = getProjectIdParam(params(req).projectId);
9461
+ if (!await projectExists(projectsDir, slug)) {
9462
+ notFound(res, slug);
9463
+ return;
9464
+ }
9465
+ const { todoIds, mode, target, title, type, priority, keepSource } = req.body ?? {};
9466
+ if (!Array.isArray(todoIds) || todoIds.length === 0) {
9467
+ res.status(400).json({ error: "todoIds (non-empty array of strings) is required" });
9468
+ return;
9469
+ }
9470
+ if (mode !== "new-assignment" && mode !== "to-assignment") {
9471
+ res.status(400).json({ error: 'mode must be "new-assignment" or "to-assignment"' });
9472
+ return;
9473
+ }
9474
+ const result = await projLock(slug, async () => {
9475
+ if (!await projectExists(projectsDir, slug)) return { gone: true };
9476
+ await ensureProjectTodosDir(projectsDir, slug);
9477
+ const todosDir2 = projectTodosDir(projectsDir, slug);
9478
+ const checklist = await readChecklist(todosDir2, slug);
9479
+ const items = [];
9480
+ for (const id of todoIds) {
9481
+ const item = checklist.items.find((i) => i.id === id);
9482
+ if (!item) return { error: `Todo "${id}" not found` };
9483
+ if (item.status === "completed") return { error: `Todo "${id}" is already completed` };
9484
+ items.push(item);
9485
+ }
9486
+ const scopeLabel = `project:${slug}`;
9487
+ const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
9488
+ const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
9489
+ const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
9490
+ let assignmentRef;
9491
+ let assignmentDir;
9492
+ if (mode === "new-assignment") {
9493
+ const targetProject = target?.project ?? slug;
9494
+ if (!targetProject) return { error: "target.project is required for new-assignment mode" };
9495
+ if (items.length > 1 && !title) return { error: "title is required when promoting multiple todos" };
9496
+ const { createAssignmentCommand: createAssignmentCommand2 } = await Promise.resolve().then(() => (init_create_assignment(), create_assignment_exports));
9497
+ const created = await createAssignmentCommand2(title || items[0].description, {
9498
+ project: targetProject,
9499
+ type,
9500
+ priority,
9501
+ withTodos: true,
9502
+ silent: true
9503
+ });
9504
+ assignmentDir = created.assignmentDir;
9505
+ assignmentRef = `${created.projectSlug}/${created.slug}`;
9506
+ } else {
9507
+ const tg = target?.assignment || "";
9508
+ if (!tg) return { error: "target.assignment is required for to-assignment mode" };
9509
+ if (tg.includes("/")) {
9510
+ const parts = tg.split("/");
9511
+ if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
9512
+ assignmentDir = resolve17(projectsDir, parts[0], "assignments", parts[1]);
9513
+ assignmentRef = `${parts[0]}/${parts[1]}`;
9514
+ } 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)) {
9515
+ assignmentDir = resolve17(assignmentsDirFn(), tg);
9516
+ assignmentRef = tg;
9517
+ } else {
9518
+ return { error: `Invalid target.assignment "${tg}"` };
9519
+ }
9520
+ const assignmentMdPath2 = resolve17(assignmentDir, "assignment.md");
9521
+ if (!await fileExists(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
9522
+ }
9523
+ const assignmentMdPath = resolve17(assignmentDir, "assignment.md");
9524
+ let content = await readFile13(assignmentMdPath, "utf-8");
9525
+ content = appendTodosToAssignmentBody2(
9526
+ content,
9527
+ items.map((it) => ({
9528
+ description: it.description,
9529
+ trace: `promoted from t:${it.id} in ${scopeLabel}`
9530
+ }))
9531
+ );
9532
+ content = touchAssignmentUpdated2(content, nowTimestamp3());
9533
+ await writeFileForce(assignmentMdPath, content);
9534
+ if (!keepSource) {
9535
+ for (const item of items) {
9536
+ item.status = "completed";
9537
+ item.session = null;
9538
+ touchItem2(item);
9539
+ }
9540
+ checklist.workspace = slug;
9541
+ await writeChecklist(todosDir2, checklist);
9542
+ for (const item of items) {
9543
+ const entry = {
9544
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9545
+ itemIds: [item.id],
9546
+ items: item.description,
9547
+ session: null,
9548
+ branch: item.branch || null,
9549
+ summary: `Promoted to assignment ${assignmentRef}`,
9550
+ blockers: null,
9551
+ status: null
9552
+ };
9553
+ await appendLogEntry2(todosDir2, slug, entry);
9554
+ }
9555
+ }
9556
+ return { assignmentRef, assignmentDir, promoted: items.map((i) => i.id) };
9557
+ });
9558
+ if ("gone" in result) {
9559
+ notFound(res, slug);
9560
+ return;
9561
+ }
9562
+ if ("error" in result) {
9563
+ res.status(400).json({ error: result.error });
9564
+ return;
9565
+ }
9566
+ broadcastUpdate(slug);
9567
+ res.json(result);
9568
+ } catch (error) {
9569
+ if (error.code === "PROJECT_GONE") {
9570
+ notFound(res, getProjectIdParam(params(req).projectId));
9571
+ return;
9572
+ }
9573
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
9574
+ }
9575
+ });
9576
+ router.post("/:id/move", async (req, res) => {
9577
+ try {
9578
+ const sourceSlug = getProjectIdParam(params(req).projectId);
9579
+ const id = params(req).id ?? "";
9580
+ if (!await projectExists(projectsDir, sourceSlug)) {
9581
+ notFound(res, sourceSlug);
9582
+ return;
9583
+ }
9584
+ const to = req.body?.to;
9585
+ if (!to || typeof to !== "object") {
9586
+ res.status(400).json({ error: "body.to is required" });
9587
+ return;
9588
+ }
9589
+ const targetCount = [Boolean(to.workspace), Boolean(to.project), Boolean(to.global)].filter(Boolean).length;
9590
+ if (targetCount !== 1) {
9591
+ res.status(400).json({ error: "body.to must specify exactly one of workspace, project, or global" });
9592
+ return;
9593
+ }
9594
+ if (to.project && !isValidSlug(to.project)) {
9595
+ res.status(400).json({ error: `Invalid target project slug: "${to.project}"` });
9596
+ return;
9597
+ }
9598
+ if (to.workspace && !WORKSPACE_REGEX2.test(to.workspace)) {
9599
+ res.status(400).json({ error: `Invalid target workspace name: "${to.workspace}"` });
9600
+ return;
9601
+ }
9602
+ let target;
9603
+ if (to.global) {
9604
+ if (!workspaceTodosDir) {
9605
+ res.status(500).json({ error: "Server not configured with workspaceTodosDir; cannot move to global scope" });
9606
+ return;
9607
+ }
9608
+ target = { kind: "workspace", id: "_global", todosPath: workspaceTodosDir, lockKey: "ws:_global" };
9609
+ } else if (to.workspace) {
9610
+ if (!workspaceTodosDir) {
9611
+ res.status(500).json({ error: "Server not configured with workspaceTodosDir; cannot move to workspace scope" });
9612
+ return;
9613
+ }
9614
+ target = { kind: "workspace", id: to.workspace, todosPath: workspaceTodosDir, lockKey: `ws:${to.workspace}` };
9615
+ } else {
9616
+ const tslug = to.project;
9617
+ if (!await projectExists(projectsDir, tslug)) {
9618
+ res.status(404).json({ error: `Target project "${tslug}" not found` });
9619
+ return;
9620
+ }
9621
+ target = {
9622
+ kind: "project",
9623
+ id: tslug,
9624
+ todosPath: projectTodosDir(projectsDir, tslug),
9625
+ lockKey: `proj:${tslug}`
9626
+ };
9627
+ }
9628
+ const sourceLockKey = `proj:${sourceSlug}`;
9629
+ if (sourceLockKey === target.lockKey) {
9630
+ res.status(400).json({ error: "cannot move to the same scope" });
9631
+ return;
9632
+ }
9633
+ const result = await withTwoLocks(sourceLockKey, target.lockKey, async () => {
9634
+ if (!await projectExists(projectsDir, sourceSlug)) return { status: "gone" };
9635
+ if (target.kind === "project" && !await projectExists(projectsDir, target.id)) {
9636
+ return { status: "targetGone" };
9637
+ }
9638
+ await ensureProjectTodosDir(projectsDir, sourceSlug);
9639
+ const sourceTodosDir = projectTodosDir(projectsDir, sourceSlug);
9640
+ const sourceChecklist = await readChecklist(sourceTodosDir, sourceSlug);
9641
+ const targetChecklist = await readChecklist(target.todosPath, target.id);
9642
+ const idx = sourceChecklist.items.findIndex((i) => i.id === id);
9643
+ if (idx === -1) return { status: 404, error: `Todo "${id}" not found` };
9644
+ if (targetChecklist.items.some((i) => i.id === id)) {
9645
+ return { status: 409, error: "id already exists in target" };
9646
+ }
9647
+ const item = sourceChecklist.items[idx];
9648
+ if (item.planDir) {
9649
+ const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
9650
+ if (await fileExists(newPlanDir)) {
9651
+ return { status: 409, error: "plan dir already exists in target" };
9652
+ }
9653
+ await mkdir3(dirname4(newPlanDir), { recursive: true });
9654
+ await rename4(item.planDir, newPlanDir);
9655
+ item.planDir = newPlanDir;
9656
+ }
9657
+ sourceChecklist.items.splice(idx, 1);
9658
+ targetChecklist.items.push(item);
9659
+ sourceChecklist.workspace = sourceSlug;
9660
+ await writeChecklist(sourceTodosDir, sourceChecklist);
9661
+ await writeChecklist(target.todosPath, targetChecklist);
9662
+ const sourceLabel = `project:${sourceSlug}`;
9663
+ const targetLabel = target.kind === "project" ? `project:${target.id}` : target.id === "_global" ? "_global" : `workspace:${target.id}`;
9664
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
9665
+ await appendLogEntry2(sourceTodosDir, sourceSlug, {
9666
+ timestamp: ts,
9667
+ itemIds: [id],
9668
+ items: item.description,
9669
+ session: null,
9670
+ branch: item.branch || null,
9671
+ summary: `Moved to ${targetLabel}`,
9672
+ blockers: null,
9673
+ status: null
9674
+ });
9675
+ await appendLogEntry2(target.todosPath, target.id, {
9676
+ timestamp: ts,
9677
+ itemIds: [id],
9678
+ items: item.description,
9679
+ session: null,
9680
+ branch: item.branch || null,
9681
+ summary: `Moved from ${sourceLabel}`,
9682
+ blockers: null,
9683
+ status: null
9684
+ });
9685
+ return { status: 200, item };
9686
+ });
9687
+ if (result.status === "gone") {
9688
+ notFound(res, sourceSlug);
9689
+ return;
9690
+ }
9691
+ if (result.status === "targetGone") {
9692
+ res.status(404).json({ error: `Target project "${target.id}" not found` });
9693
+ return;
9694
+ }
9695
+ if (result.status !== 200) {
9696
+ res.status(result.status).json({ error: result.error });
9697
+ return;
9698
+ }
9699
+ broadcastUpdate(sourceSlug);
9700
+ if (target.kind === "project") broadcastUpdate(target.id);
9701
+ else broadcastWorkspace();
9702
+ res.json({ moved: id, to: target });
9703
+ } catch (error) {
9704
+ if (error.code === "PROJECT_GONE") {
9705
+ notFound(res, getProjectIdParam(params(req).projectId));
9706
+ return;
9707
+ }
9708
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to move todo" });
9709
+ }
9710
+ });
9330
9711
  return router;
9331
9712
  }
9332
9713
 
@@ -9340,7 +9721,7 @@ init_fs();
9340
9721
  init_config2();
9341
9722
  import { execFile as execFile2 } from "child_process";
9342
9723
  import { promisify as promisify2 } from "util";
9343
- import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
9724
+ import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename5 } from "fs/promises";
9344
9725
  import { resolve as resolve18, join as join2 } from "path";
9345
9726
  import { tmpdir } from "os";
9346
9727
  var exec2 = promisify2(execFile2);
@@ -9552,7 +9933,7 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
9552
9933
  const localExistsBefore = await fileExists(localPath);
9553
9934
  if (backupExistsBefore) {
9554
9935
  if (!localExistsBefore) {
9555
- await rename3(backupPath, localPath);
9936
+ await rename5(backupPath, localPath);
9556
9937
  } else {
9557
9938
  throw new Error(
9558
9939
  `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.`
@@ -9564,15 +9945,15 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
9564
9945
  await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
9565
9946
  const localExists = await fileExists(localPath);
9566
9947
  if (localExists) {
9567
- await rename3(localPath, backupPath);
9948
+ await rename5(localPath, backupPath);
9568
9949
  localMovedAside = true;
9569
9950
  }
9570
- await rename3(stagingPath, localPath);
9951
+ await rename5(stagingPath, localPath);
9571
9952
  await rm2(backupPath, { recursive: true, force: true }).catch(() => {
9572
9953
  });
9573
9954
  } catch (err) {
9574
9955
  if (localMovedAside && await fileExists(backupPath)) {
9575
- await rename3(backupPath, localPath).catch(() => {
9956
+ await rename5(backupPath, localPath).catch(() => {
9576
9957
  });
9577
9958
  }
9578
9959
  await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
@@ -10311,8 +10692,8 @@ function createDashboardServer(options) {
10311
10692
  app.use("/api/servers", createServersRouter(serversDir2, projectsDir, assignmentsDir2));
10312
10693
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2));
10313
10694
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
10314
- app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
10315
- app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast));
10695
+ app.use("/api/todos", createTodosRouter(todosDir2, broadcast, projectsDir));
10696
+ app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast, todosDir2));
10316
10697
  app.use("/api/backup", createBackupRouter());
10317
10698
  if (serveStaticUi && dashboardDistPath) {
10318
10699
  const sendOpts = { dotfiles: "allow" };