syntaur 0.6.0 → 0.7.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 (83) hide show
  1. package/README.md +2 -2
  2. package/dashboard/dist/assets/{_basePickBy-BQIP1Ca7.js → _basePickBy-DTYUlCEg.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-BnBWRwT7.js → _baseUniq-C0Y4HRd5.js} +1 -1
  4. package/dashboard/dist/assets/{arc-BYWL4eq0.js → arc-BFx2eqN9.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CD_SWPSa.js → architectureDiagram-2XIMDMQ5-Erol1JD6.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BS1ZbFBU.js → blockDiagram-WCTKOSBZ-kSkh6VkS.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-D99yg-l2.js → c4Diagram-IC4MRINW-C04oKzvX.js} +1 -1
  8. package/dashboard/dist/assets/channel-C82tBKZ7.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-BkN9IORC.js → chunk-4BX2VUAB-C3t0tXt-.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-BQPHWefV.js → chunk-55IACEB6-2cnyEL0b.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-CNcExMdx.js → chunk-FMBD7UC4-DIY9MTNi.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-LXBmftkC.js → chunk-JSJVCQXG-Cw8fpqpE.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-Tqi7zNqq.js → chunk-KX2RTZJC-BAhd66XV.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DkMbx-rW.js → chunk-NQ4KR5QH-RzXwoxk3.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-BlrRCfkJ.js → chunk-QZHKN3VN-Dgri4sGz.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-of3XBzMu.js → chunk-WL4C6EOR-DYLj9JRa.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-STOZ51tg.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-STOZ51tg.js +1 -0
  19. package/dashboard/dist/assets/clone-TzhWk-Bj.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-BlIiyO76.js → cose-bilkent-S5V4N54A-DfY_Fnfu.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CYQjSI9N.js → dagre-KLK3FWXG-CyTKIVSK.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-BZHzTKct.js → diagram-E7M64L7V-Krub7Xxo.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-kMP3WqBV.js → diagram-IFDJBPK2-giUl9uHz.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-BWSHyFOv.js → diagram-P4PSJMXO-oAtnO3C9.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-B5HrvsPP.js → erDiagram-INFDFZHY-eYaVjXqo.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-Dm4ewP7w.js → flowDiagram-PKNHOUZH-or5S0_Sb.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DB3k27zu.js → ganttDiagram-A5KZAMGK-C9R1lsme.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-G7y6Ey-m.js → gitGraphDiagram-K3NZZRJ6-BQwsDzvp.js} +1 -1
  29. package/dashboard/dist/assets/{graph-CaM4i6vq.js → graph-EQOX1wg8.js} +1 -1
  30. package/dashboard/dist/assets/index-Cy7yjuqO.js +500 -0
  31. package/dashboard/dist/assets/index-u80fISp0.css +1 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-JNTUbTjg.js → infoDiagram-LFFYTUFH-BjLlQWxk.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-BZJt1ht8.js → ishikawaDiagram-PHBUUO56-BNjydh4j.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DPcqvl9A.js → journeyDiagram-4ABVD52K-DNzE7TgQ.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-D1D7AuOV.js → kanban-definition-K7BYSVSG-FbNvGx6i.js} +1 -1
  36. package/dashboard/dist/assets/{layout-BTOh3EDT.js → layout-B2yZvlWs.js} +1 -1
  37. package/dashboard/dist/assets/{linear-MbCpC_Cg.js → linear-p68yY_14.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-CYbhqlNy.js → mermaid.core-D558akcW.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-CwYCISFH.js → mindmap-definition-YRQLILUH-CZPgesSK.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-5qfZ73SG.js → pieDiagram-SKSYHLDU-CdXMWspp.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-WI8y1sQ_.js → quadrantDiagram-337W2JSQ-D7tq22ZY.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BFlD0ZTS.js → requirementDiagram-Z7DCOOCP-ByZxUSmd.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-Bdckv1Se.js → sankeyDiagram-WA2Y5GQK-CZon9rRY.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DgzxKAlZ.js → sequenceDiagram-2WXFIKYE-nbELB6rb.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DO4OXahC.js → stateDiagram-RAJIS63D-D_OPKr5B.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-4pLM5B3m.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BBB01JWw.js → timeline-definition-YZTLITO2-Cxuvk1D2.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-Dr0jb8op.js → treemap-KZPCXAKY-C0CRpL92.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-D40KFl2o.js → vennDiagram-LZ73GAT5-DijCj6M3.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DBUmWQfT.js → xychartDiagram-JWTSCODW-CdpE0oRi.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +429 -39
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +658 -134
  55. package/dist/index.js.map +1 -1
  56. package/package.json +1 -1
  57. package/platforms/claude-code/README.md +3 -3
  58. package/platforms/claude-code/agents/syntaur-expert.md +12 -4
  59. package/platforms/claude-code/commands/save-session-summary/save-session-summary.md +24 -0
  60. package/platforms/claude-code/hooks/hooks.json +10 -0
  61. package/platforms/claude-code/hooks/session-start.sh +26 -1
  62. package/platforms/claude-code/references/file-ownership.md +2 -1
  63. package/platforms/claude-code/references/protocol-summary.md +6 -1
  64. package/platforms/claude-code/skills/track-session/SKILL.md +86 -0
  65. package/platforms/codex/README.md +2 -2
  66. package/platforms/codex/agents/syntaur-operator.md +6 -4
  67. package/platforms/codex/commands/save-session-summary.md +23 -0
  68. package/platforms/codex/references/file-ownership.md +2 -1
  69. package/platforms/codex/references/protocol-summary.md +6 -1
  70. package/vendor/syntaur-skills/skills/complete-assignment/SKILL.md +2 -0
  71. package/vendor/syntaur-skills/skills/grab-assignment/SKILL.md +7 -2
  72. package/vendor/syntaur-skills/skills/plan-assignment/SKILL.md +3 -1
  73. package/vendor/syntaur-skills/skills/save-session-summary/SKILL.md +113 -0
  74. package/vendor/syntaur-skills/skills/syntaur-protocol/SKILL.md +23 -4
  75. package/vendor/syntaur-skills/skills/syntaur-protocol/references/file-ownership.md +2 -1
  76. package/vendor/syntaur-skills/skills/syntaur-protocol/references/protocol-summary.md +6 -1
  77. package/dashboard/dist/assets/channel-Df6VrFK5.js +0 -1
  78. package/dashboard/dist/assets/classDiagram-VBA2DB6C-CyfzumTY.js +0 -1
  79. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CyfzumTY.js +0 -1
  80. package/dashboard/dist/assets/clone-CMs4Aqrx.js +0 -1
  81. package/dashboard/dist/assets/index-B4QMu-Oq.css +0 -1
  82. package/dashboard/dist/assets/index-BBWZjPBC.js +0 -495
  83. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-o8bgX-J3.js +0 -1
@@ -4771,8 +4771,9 @@ ${todosSection}## Context
4771
4771
  - [Progress](./progress.md)
4772
4772
  - [Comments](./comments.md)
4773
4773
  - [Scratchpad](./scratchpad.md)
4774
- - [Handoff](./handoff.md)
4774
+ - [Handoff](./handoff.md) \u2014 cross-ticket outbound
4775
4775
  - [Decision Record](./decision-record.md)
4776
+ - [Sessions](./sessions/) \u2014 per-session continuity summaries (one \`<session-id>/summary.md\` per session)
4776
4777
  `;
4777
4778
  }
4778
4779
  var init_assignment = __esm({
@@ -4826,6 +4827,13 @@ var init_handoff = __esm({
4826
4827
  }
4827
4828
  });
4828
4829
 
4830
+ // src/templates/session-summary.ts
4831
+ var init_session_summary = __esm({
4832
+ "src/templates/session-summary.ts"() {
4833
+ "use strict";
4834
+ }
4835
+ });
4836
+
4829
4837
  // src/templates/progress.ts
4830
4838
  function renderProgress(params2) {
4831
4839
  return `---
@@ -5076,6 +5084,7 @@ var init_templates = __esm({
5076
5084
  init_plan();
5077
5085
  init_scratchpad();
5078
5086
  init_handoff();
5087
+ init_session_summary();
5079
5088
  init_progress();
5080
5089
  init_comments();
5081
5090
  init_decision_record();
@@ -8172,37 +8181,62 @@ init_fs_migration();
8172
8181
  // src/dashboard/api-todos.ts
8173
8182
  init_parser2();
8174
8183
  init_fs();
8184
+ init_paths();
8175
8185
  import { Router as Router5 } from "express";
8176
8186
  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
- }
8187
+ import { resolve as resolvePath, dirname as dirname3 } from "path";
8188
+ import { rename as rename3, mkdir as mkdir2 } from "fs/promises";
8189
+
8190
+ // src/dashboard/todos-locks.ts
8184
8191
  var writeLocks = /* @__PURE__ */ new Map();
8185
8192
  function withLock(lockKey, fn) {
8186
8193
  const prev = writeLocks.get(lockKey) ?? Promise.resolve();
8187
8194
  const next = prev.then(fn);
8188
- writeLocks.set(lockKey, next.then(() => {
8189
- }, () => {
8190
- }));
8195
+ writeLocks.set(
8196
+ lockKey,
8197
+ next.then(
8198
+ () => {
8199
+ },
8200
+ () => {
8201
+ }
8202
+ )
8203
+ );
8191
8204
  return next;
8192
8205
  }
8193
8206
  function wsLock(workspace, fn) {
8194
8207
  return withLock(`ws:${workspace}`, fn);
8195
8208
  }
8209
+ function projLock(slug, fn) {
8210
+ return withLock(`proj:${slug}`, fn);
8211
+ }
8212
+ function withTwoLocks(keyA, keyB, fn) {
8213
+ if (keyA === keyB) return withLock(keyA, fn);
8214
+ const [first, second] = keyA < keyB ? [keyA, keyB] : [keyB, keyA];
8215
+ return withLock(first, () => withLock(second, fn));
8216
+ }
8217
+
8218
+ // src/dashboard/api-todos.ts
8219
+ init_slug();
8220
+ var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
8221
+ function getWorkspaceParam(value) {
8222
+ if (Array.isArray(value)) {
8223
+ return value[0] ?? "";
8224
+ }
8225
+ return value ?? "";
8226
+ }
8196
8227
  function touchItem(item) {
8197
8228
  const now = (/* @__PURE__ */ new Date()).toISOString();
8198
8229
  if (item.createdAt === null) item.createdAt = now;
8199
8230
  item.updatedAt = now;
8200
8231
  }
8201
- function createTodosRouter(todosDir2, broadcast) {
8232
+ function createTodosRouter(todosDir2, broadcast, projectsDir) {
8202
8233
  const router = Router5();
8203
8234
  function broadcastUpdate() {
8204
8235
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8205
8236
  }
8237
+ function broadcastProject(slug) {
8238
+ broadcast({ type: "todos-updated", projectSlug: slug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8239
+ }
8206
8240
  function validateWorkspace(req, res, next) {
8207
8241
  const workspace = getWorkspaceParam(req.params.workspace);
8208
8242
  if (workspace && !WORKSPACE_REGEX.test(workspace)) {
@@ -8601,7 +8635,7 @@ workspace: ${workspace}
8601
8635
  items.push(item);
8602
8636
  }
8603
8637
  const scopeLabel = workspace === "_global" ? "_global" : `workspace:${workspace}`;
8604
- const { resolve: resolvePath } = await import("path");
8638
+ const { resolve: resolvePath2 } = await import("path");
8605
8639
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8606
8640
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
8607
8641
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
@@ -8631,18 +8665,18 @@ workspace: ${workspace}
8631
8665
  const parts = tg.split("/");
8632
8666
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
8633
8667
  const config = await readConfig2();
8634
- assignmentDir = resolvePath(config.defaultProjectDir, parts[0], "assignments", parts[1]);
8668
+ assignmentDir = resolvePath2(config.defaultProjectDir, parts[0], "assignments", parts[1]);
8635
8669
  assignmentRef = `${parts[0]}/${parts[1]}`;
8636
8670
  } 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);
8671
+ assignmentDir = resolvePath2(assignmentsDirFn(), tg);
8638
8672
  assignmentRef = tg;
8639
8673
  } else {
8640
8674
  return { error: `Invalid target.assignment "${tg}"` };
8641
8675
  }
8642
- const assignmentMdPath2 = resolvePath(assignmentDir, "assignment.md");
8676
+ const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
8643
8677
  if (!await fileExists2(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
8644
8678
  }
8645
- const assignmentMdPath = resolvePath(assignmentDir, "assignment.md");
8679
+ const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
8646
8680
  let content = await readFile15(assignmentMdPath, "utf-8");
8647
8681
  content = appendTodosToAssignmentBody2(
8648
8682
  content,
@@ -8686,6 +8720,115 @@ workspace: ${workspace}
8686
8720
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
8687
8721
  }
8688
8722
  });
8723
+ router.post("/:workspace/:id/move", async (req, res) => {
8724
+ try {
8725
+ const sourceWs = getWorkspaceParam(req.params.workspace);
8726
+ const id = req.params.id;
8727
+ const to = req.body?.to;
8728
+ if (!to || typeof to !== "object") {
8729
+ res.status(400).json({ error: "body.to is required" });
8730
+ return;
8731
+ }
8732
+ const targetCount = [Boolean(to.workspace), Boolean(to.project), Boolean(to.global)].filter(Boolean).length;
8733
+ if (targetCount !== 1) {
8734
+ res.status(400).json({ error: "body.to must specify exactly one of workspace, project, or global" });
8735
+ return;
8736
+ }
8737
+ if (to.project && !isValidSlug(to.project)) {
8738
+ res.status(400).json({ error: `Invalid target project slug: "${to.project}"` });
8739
+ return;
8740
+ }
8741
+ if (to.workspace && !WORKSPACE_REGEX.test(to.workspace)) {
8742
+ res.status(400).json({ error: `Invalid target workspace name: "${to.workspace}"` });
8743
+ return;
8744
+ }
8745
+ let target;
8746
+ if (to.global) {
8747
+ target = { kind: "workspace", id: "_global", todosPath: todosDir2, lockKey: "ws:_global" };
8748
+ } else if (to.workspace) {
8749
+ target = { kind: "workspace", id: to.workspace, todosPath: todosDir2, lockKey: `ws:${to.workspace}` };
8750
+ } else {
8751
+ if (!projectsDir) {
8752
+ res.status(500).json({ error: "Server not configured with projectsDir; cannot move to project scope" });
8753
+ return;
8754
+ }
8755
+ const slug = to.project;
8756
+ const projectMd = resolvePath(projectsDir, slug, "project.md");
8757
+ if (!await fileExists(projectMd)) {
8758
+ res.status(404).json({ error: `Target project "${slug}" not found` });
8759
+ return;
8760
+ }
8761
+ target = {
8762
+ kind: "project",
8763
+ id: slug,
8764
+ todosPath: projectTodosDir(projectsDir, slug),
8765
+ lockKey: `proj:${slug}`
8766
+ };
8767
+ }
8768
+ const sourceLockKey = `ws:${sourceWs}`;
8769
+ if (sourceLockKey === target.lockKey) {
8770
+ res.status(400).json({ error: "cannot move to the same scope" });
8771
+ return;
8772
+ }
8773
+ const result = await withTwoLocks(sourceLockKey, target.lockKey, async () => {
8774
+ const sourceChecklist = await readChecklist(todosDir2, sourceWs);
8775
+ const targetChecklist = await readChecklist(target.todosPath, target.id);
8776
+ const idx = sourceChecklist.items.findIndex((i) => i.id === id);
8777
+ if (idx === -1) return { status: 404, error: `Todo "${id}" not found` };
8778
+ if (targetChecklist.items.some((i) => i.id === id)) {
8779
+ return { status: 409, error: "id already exists in target" };
8780
+ }
8781
+ const item = sourceChecklist.items[idx];
8782
+ if (item.planDir) {
8783
+ const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
8784
+ if (await fileExists(newPlanDir)) {
8785
+ return { status: 409, error: "plan dir already exists in target" };
8786
+ }
8787
+ await mkdir2(dirname3(newPlanDir), { recursive: true });
8788
+ await rename3(item.planDir, newPlanDir);
8789
+ item.planDir = newPlanDir;
8790
+ }
8791
+ sourceChecklist.items.splice(idx, 1);
8792
+ targetChecklist.items.push(item);
8793
+ await writeChecklist(todosDir2, sourceChecklist);
8794
+ await writeChecklist(target.todosPath, targetChecklist);
8795
+ const sourceLabel = sourceWs === "_global" ? "_global" : `workspace:${sourceWs}`;
8796
+ const targetLabel = target.kind === "project" ? `project:${target.id}` : target.id === "_global" ? "_global" : `workspace:${target.id}`;
8797
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
8798
+ await appendLogEntry2(todosDir2, sourceWs, {
8799
+ timestamp: ts,
8800
+ itemIds: [id],
8801
+ items: item.description,
8802
+ session: null,
8803
+ branch: item.branch || null,
8804
+ summary: `Moved to ${targetLabel}`,
8805
+ blockers: null,
8806
+ status: null
8807
+ });
8808
+ await appendLogEntry2(target.todosPath, target.id, {
8809
+ timestamp: ts,
8810
+ itemIds: [id],
8811
+ items: item.description,
8812
+ session: null,
8813
+ branch: item.branch || null,
8814
+ summary: `Moved from ${sourceLabel}`,
8815
+ blockers: null,
8816
+ status: null
8817
+ });
8818
+ return { status: 200, item };
8819
+ });
8820
+ if (result.status !== 200) {
8821
+ res.status(result.status).json({ error: result.error });
8822
+ return;
8823
+ }
8824
+ broadcastUpdate();
8825
+ if (target.kind === "project") broadcastProject(target.id);
8826
+ else broadcastUpdate();
8827
+ res.json({ moved: id, to: target });
8828
+ } catch (error) {
8829
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to move todo" });
8830
+ }
8831
+ });
8689
8832
  router.post("/:workspace/:id/unblock", async (req, res) => {
8690
8833
  try {
8691
8834
  const workspace = getWorkspaceParam(req.params.workspace);
@@ -8718,18 +8861,9 @@ init_fs();
8718
8861
  init_paths();
8719
8862
  init_slug();
8720
8863
  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
- }
8864
+ import { mkdir as mkdir3, readFile as readFile13, rename as rename4 } from "fs/promises";
8865
+ import { resolve as resolve17, dirname as dirname4 } from "path";
8866
+ var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
8733
8867
  function touchItem2(item) {
8734
8868
  const now = (/* @__PURE__ */ new Date()).toISOString();
8735
8869
  if (item.createdAt === null) item.createdAt = now;
@@ -8748,7 +8882,7 @@ async function projectExists(projectsDir, slug) {
8748
8882
  async function ensureProjectTodosDir(projectsDir, slug) {
8749
8883
  const todosDir2 = projectTodosDir(projectsDir, slug);
8750
8884
  try {
8751
- await mkdir2(todosDir2, { recursive: false });
8885
+ await mkdir3(todosDir2, { recursive: false });
8752
8886
  } catch (err) {
8753
8887
  const code = err.code;
8754
8888
  if (code === "EEXIST") return;
@@ -8760,7 +8894,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
8760
8894
  throw err;
8761
8895
  }
8762
8896
  try {
8763
- await mkdir2(resolve17(todosDir2, "archive"), { recursive: false });
8897
+ await mkdir3(resolve17(todosDir2, "archive"), { recursive: false });
8764
8898
  } catch (err) {
8765
8899
  const code = err.code;
8766
8900
  if (code === "EEXIST") return;
@@ -8775,11 +8909,14 @@ async function ensureProjectTodosDir(projectsDir, slug) {
8775
8909
  function notFound(res, slug) {
8776
8910
  res.status(404).json({ error: `Project "${slug}" not found` });
8777
8911
  }
8778
- function createProjectTodosRouter(projectsDir, broadcast) {
8912
+ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
8779
8913
  const router = Router6({ mergeParams: true });
8780
8914
  function broadcastUpdate(projectSlug) {
8781
8915
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8782
8916
  }
8917
+ function broadcastWorkspace() {
8918
+ broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8919
+ }
8783
8920
  function validateProjectId(req, res, next) {
8784
8921
  const slug = getProjectIdParam(params(req).projectId);
8785
8922
  if (!slug || !isValidSlug(slug)) {
@@ -9327,6 +9464,259 @@ workspace: ${slug}
9327
9464
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to unblock todo" });
9328
9465
  }
9329
9466
  });
9467
+ router.post("/promote", async (req, res) => {
9468
+ try {
9469
+ const slug = getProjectIdParam(params(req).projectId);
9470
+ if (!await projectExists(projectsDir, slug)) {
9471
+ notFound(res, slug);
9472
+ return;
9473
+ }
9474
+ const { todoIds, mode, target, title, type, priority, keepSource } = req.body ?? {};
9475
+ if (!Array.isArray(todoIds) || todoIds.length === 0) {
9476
+ res.status(400).json({ error: "todoIds (non-empty array of strings) is required" });
9477
+ return;
9478
+ }
9479
+ if (mode !== "new-assignment" && mode !== "to-assignment") {
9480
+ res.status(400).json({ error: 'mode must be "new-assignment" or "to-assignment"' });
9481
+ return;
9482
+ }
9483
+ const result = await projLock(slug, async () => {
9484
+ if (!await projectExists(projectsDir, slug)) return { gone: true };
9485
+ await ensureProjectTodosDir(projectsDir, slug);
9486
+ const todosDir2 = projectTodosDir(projectsDir, slug);
9487
+ const checklist = await readChecklist(todosDir2, slug);
9488
+ const items = [];
9489
+ for (const id of todoIds) {
9490
+ const item = checklist.items.find((i) => i.id === id);
9491
+ if (!item) return { error: `Todo "${id}" not found` };
9492
+ if (item.status === "completed") return { error: `Todo "${id}" is already completed` };
9493
+ items.push(item);
9494
+ }
9495
+ const scopeLabel = `project:${slug}`;
9496
+ const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
9497
+ const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
9498
+ const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
9499
+ let assignmentRef;
9500
+ let assignmentDir;
9501
+ if (mode === "new-assignment") {
9502
+ const targetProject = target?.project ?? slug;
9503
+ if (!targetProject) return { error: "target.project is required for new-assignment mode" };
9504
+ if (items.length > 1 && !title) return { error: "title is required when promoting multiple todos" };
9505
+ const { createAssignmentCommand: createAssignmentCommand2 } = await Promise.resolve().then(() => (init_create_assignment(), create_assignment_exports));
9506
+ const created = await createAssignmentCommand2(title || items[0].description, {
9507
+ project: targetProject,
9508
+ type,
9509
+ priority,
9510
+ withTodos: true,
9511
+ silent: true
9512
+ });
9513
+ assignmentDir = created.assignmentDir;
9514
+ assignmentRef = `${created.projectSlug}/${created.slug}`;
9515
+ } else {
9516
+ const tg = target?.assignment || "";
9517
+ if (!tg) return { error: "target.assignment is required for to-assignment mode" };
9518
+ if (tg.includes("/")) {
9519
+ const parts = tg.split("/");
9520
+ if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
9521
+ assignmentDir = resolve17(projectsDir, parts[0], "assignments", parts[1]);
9522
+ assignmentRef = `${parts[0]}/${parts[1]}`;
9523
+ } 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)) {
9524
+ assignmentDir = resolve17(assignmentsDirFn(), tg);
9525
+ assignmentRef = tg;
9526
+ } else {
9527
+ return { error: `Invalid target.assignment "${tg}"` };
9528
+ }
9529
+ const assignmentMdPath2 = resolve17(assignmentDir, "assignment.md");
9530
+ if (!await fileExists(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
9531
+ }
9532
+ const assignmentMdPath = resolve17(assignmentDir, "assignment.md");
9533
+ let content = await readFile13(assignmentMdPath, "utf-8");
9534
+ content = appendTodosToAssignmentBody2(
9535
+ content,
9536
+ items.map((it) => ({
9537
+ description: it.description,
9538
+ trace: `promoted from t:${it.id} in ${scopeLabel}`
9539
+ }))
9540
+ );
9541
+ content = touchAssignmentUpdated2(content, nowTimestamp3());
9542
+ await writeFileForce(assignmentMdPath, content);
9543
+ if (!keepSource) {
9544
+ for (const item of items) {
9545
+ item.status = "completed";
9546
+ item.session = null;
9547
+ touchItem2(item);
9548
+ }
9549
+ checklist.workspace = slug;
9550
+ await writeChecklist(todosDir2, checklist);
9551
+ for (const item of items) {
9552
+ const entry = {
9553
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9554
+ itemIds: [item.id],
9555
+ items: item.description,
9556
+ session: null,
9557
+ branch: item.branch || null,
9558
+ summary: `Promoted to assignment ${assignmentRef}`,
9559
+ blockers: null,
9560
+ status: null
9561
+ };
9562
+ await appendLogEntry2(todosDir2, slug, entry);
9563
+ }
9564
+ }
9565
+ return { assignmentRef, assignmentDir, promoted: items.map((i) => i.id) };
9566
+ });
9567
+ if ("gone" in result) {
9568
+ notFound(res, slug);
9569
+ return;
9570
+ }
9571
+ if ("error" in result) {
9572
+ res.status(400).json({ error: result.error });
9573
+ return;
9574
+ }
9575
+ broadcastUpdate(slug);
9576
+ res.json(result);
9577
+ } catch (error) {
9578
+ if (error.code === "PROJECT_GONE") {
9579
+ notFound(res, getProjectIdParam(params(req).projectId));
9580
+ return;
9581
+ }
9582
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
9583
+ }
9584
+ });
9585
+ router.post("/:id/move", async (req, res) => {
9586
+ try {
9587
+ const sourceSlug = getProjectIdParam(params(req).projectId);
9588
+ const id = params(req).id ?? "";
9589
+ if (!await projectExists(projectsDir, sourceSlug)) {
9590
+ notFound(res, sourceSlug);
9591
+ return;
9592
+ }
9593
+ const to = req.body?.to;
9594
+ if (!to || typeof to !== "object") {
9595
+ res.status(400).json({ error: "body.to is required" });
9596
+ return;
9597
+ }
9598
+ const targetCount = [Boolean(to.workspace), Boolean(to.project), Boolean(to.global)].filter(Boolean).length;
9599
+ if (targetCount !== 1) {
9600
+ res.status(400).json({ error: "body.to must specify exactly one of workspace, project, or global" });
9601
+ return;
9602
+ }
9603
+ if (to.project && !isValidSlug(to.project)) {
9604
+ res.status(400).json({ error: `Invalid target project slug: "${to.project}"` });
9605
+ return;
9606
+ }
9607
+ if (to.workspace && !WORKSPACE_REGEX2.test(to.workspace)) {
9608
+ res.status(400).json({ error: `Invalid target workspace name: "${to.workspace}"` });
9609
+ return;
9610
+ }
9611
+ let target;
9612
+ if (to.global) {
9613
+ if (!workspaceTodosDir) {
9614
+ res.status(500).json({ error: "Server not configured with workspaceTodosDir; cannot move to global scope" });
9615
+ return;
9616
+ }
9617
+ target = { kind: "workspace", id: "_global", todosPath: workspaceTodosDir, lockKey: "ws:_global" };
9618
+ } else if (to.workspace) {
9619
+ if (!workspaceTodosDir) {
9620
+ res.status(500).json({ error: "Server not configured with workspaceTodosDir; cannot move to workspace scope" });
9621
+ return;
9622
+ }
9623
+ target = { kind: "workspace", id: to.workspace, todosPath: workspaceTodosDir, lockKey: `ws:${to.workspace}` };
9624
+ } else {
9625
+ const tslug = to.project;
9626
+ if (!await projectExists(projectsDir, tslug)) {
9627
+ res.status(404).json({ error: `Target project "${tslug}" not found` });
9628
+ return;
9629
+ }
9630
+ target = {
9631
+ kind: "project",
9632
+ id: tslug,
9633
+ todosPath: projectTodosDir(projectsDir, tslug),
9634
+ lockKey: `proj:${tslug}`
9635
+ };
9636
+ }
9637
+ const sourceLockKey = `proj:${sourceSlug}`;
9638
+ if (sourceLockKey === target.lockKey) {
9639
+ res.status(400).json({ error: "cannot move to the same scope" });
9640
+ return;
9641
+ }
9642
+ const result = await withTwoLocks(sourceLockKey, target.lockKey, async () => {
9643
+ if (!await projectExists(projectsDir, sourceSlug)) return { status: "gone" };
9644
+ if (target.kind === "project" && !await projectExists(projectsDir, target.id)) {
9645
+ return { status: "targetGone" };
9646
+ }
9647
+ await ensureProjectTodosDir(projectsDir, sourceSlug);
9648
+ const sourceTodosDir = projectTodosDir(projectsDir, sourceSlug);
9649
+ const sourceChecklist = await readChecklist(sourceTodosDir, sourceSlug);
9650
+ const targetChecklist = await readChecklist(target.todosPath, target.id);
9651
+ const idx = sourceChecklist.items.findIndex((i) => i.id === id);
9652
+ if (idx === -1) return { status: 404, error: `Todo "${id}" not found` };
9653
+ if (targetChecklist.items.some((i) => i.id === id)) {
9654
+ return { status: 409, error: "id already exists in target" };
9655
+ }
9656
+ const item = sourceChecklist.items[idx];
9657
+ if (item.planDir) {
9658
+ const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
9659
+ if (await fileExists(newPlanDir)) {
9660
+ return { status: 409, error: "plan dir already exists in target" };
9661
+ }
9662
+ await mkdir3(dirname4(newPlanDir), { recursive: true });
9663
+ await rename4(item.planDir, newPlanDir);
9664
+ item.planDir = newPlanDir;
9665
+ }
9666
+ sourceChecklist.items.splice(idx, 1);
9667
+ targetChecklist.items.push(item);
9668
+ sourceChecklist.workspace = sourceSlug;
9669
+ await writeChecklist(sourceTodosDir, sourceChecklist);
9670
+ await writeChecklist(target.todosPath, targetChecklist);
9671
+ const sourceLabel = `project:${sourceSlug}`;
9672
+ const targetLabel = target.kind === "project" ? `project:${target.id}` : target.id === "_global" ? "_global" : `workspace:${target.id}`;
9673
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
9674
+ await appendLogEntry2(sourceTodosDir, sourceSlug, {
9675
+ timestamp: ts,
9676
+ itemIds: [id],
9677
+ items: item.description,
9678
+ session: null,
9679
+ branch: item.branch || null,
9680
+ summary: `Moved to ${targetLabel}`,
9681
+ blockers: null,
9682
+ status: null
9683
+ });
9684
+ await appendLogEntry2(target.todosPath, target.id, {
9685
+ timestamp: ts,
9686
+ itemIds: [id],
9687
+ items: item.description,
9688
+ session: null,
9689
+ branch: item.branch || null,
9690
+ summary: `Moved from ${sourceLabel}`,
9691
+ blockers: null,
9692
+ status: null
9693
+ });
9694
+ return { status: 200, item };
9695
+ });
9696
+ if (result.status === "gone") {
9697
+ notFound(res, sourceSlug);
9698
+ return;
9699
+ }
9700
+ if (result.status === "targetGone") {
9701
+ res.status(404).json({ error: `Target project "${target.id}" not found` });
9702
+ return;
9703
+ }
9704
+ if (result.status !== 200) {
9705
+ res.status(result.status).json({ error: result.error });
9706
+ return;
9707
+ }
9708
+ broadcastUpdate(sourceSlug);
9709
+ if (target.kind === "project") broadcastUpdate(target.id);
9710
+ else broadcastWorkspace();
9711
+ res.json({ moved: id, to: target });
9712
+ } catch (error) {
9713
+ if (error.code === "PROJECT_GONE") {
9714
+ notFound(res, getProjectIdParam(params(req).projectId));
9715
+ return;
9716
+ }
9717
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to move todo" });
9718
+ }
9719
+ });
9330
9720
  return router;
9331
9721
  }
9332
9722
 
@@ -9340,7 +9730,7 @@ init_fs();
9340
9730
  init_config2();
9341
9731
  import { execFile as execFile2 } from "child_process";
9342
9732
  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";
9733
+ import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename5 } from "fs/promises";
9344
9734
  import { resolve as resolve18, join as join2 } from "path";
9345
9735
  import { tmpdir } from "os";
9346
9736
  var exec2 = promisify2(execFile2);
@@ -9552,7 +9942,7 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
9552
9942
  const localExistsBefore = await fileExists(localPath);
9553
9943
  if (backupExistsBefore) {
9554
9944
  if (!localExistsBefore) {
9555
- await rename3(backupPath, localPath);
9945
+ await rename5(backupPath, localPath);
9556
9946
  } else {
9557
9947
  throw new Error(
9558
9948
  `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 +9954,15 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
9564
9954
  await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
9565
9955
  const localExists = await fileExists(localPath);
9566
9956
  if (localExists) {
9567
- await rename3(localPath, backupPath);
9957
+ await rename5(localPath, backupPath);
9568
9958
  localMovedAside = true;
9569
9959
  }
9570
- await rename3(stagingPath, localPath);
9960
+ await rename5(stagingPath, localPath);
9571
9961
  await rm2(backupPath, { recursive: true, force: true }).catch(() => {
9572
9962
  });
9573
9963
  } catch (err) {
9574
9964
  if (localMovedAside && await fileExists(backupPath)) {
9575
- await rename3(backupPath, localPath).catch(() => {
9965
+ await rename5(backupPath, localPath).catch(() => {
9576
9966
  });
9577
9967
  }
9578
9968
  await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
@@ -10311,8 +10701,8 @@ function createDashboardServer(options) {
10311
10701
  app.use("/api/servers", createServersRouter(serversDir2, projectsDir, assignmentsDir2));
10312
10702
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2));
10313
10703
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
10314
- app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
10315
- app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast));
10704
+ app.use("/api/todos", createTodosRouter(todosDir2, broadcast, projectsDir));
10705
+ app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast, todosDir2));
10316
10706
  app.use("/api/backup", createBackupRouter());
10317
10707
  if (serveStaticUi && dashboardDistPath) {
10318
10708
  const sendOpts = { dotfiles: "allow" };