syntaur 0.26.0 → 0.31.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 (80) hide show
  1. package/dashboard/dist/assets/{_basePickBy-jPItyrQO.js → _basePickBy-ku_fr_ud.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-pEwUwurC.js → _baseUniq-BfsrymUF.js} +1 -1
  3. package/dashboard/dist/assets/{arc-ZZtp507S.js → arc-CAIW_Q55.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BNUerPqd.js → architectureDiagram-2XIMDMQ5-BSe-tJ6X.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-CQyovXFv.js → blockDiagram-WCTKOSBZ-BUj6C0-C.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-wNQ6EHeF.js → c4Diagram-IC4MRINW-mV-acLl-.js} +1 -1
  7. package/dashboard/dist/assets/channel-DSNNpVLB.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-ZaueC30R.js → chunk-4BX2VUAB-TStVwY56.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-BjsRB0t8.js → chunk-55IACEB6-XPX1tCrH.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-BHuSr-Tl.js → chunk-FMBD7UC4-Dm-0meIo.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-SHNJA0es.js → chunk-JSJVCQXG-BVhtvJFZ.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-JXFPjeo4.js → chunk-KX2RTZJC-C3r5FnmX.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-BiJqWT0B.js → chunk-NQ4KR5QH-DYjOE2Lm.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-DoXWBqP2.js → chunk-QZHKN3VN-CWpi_905.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-Dqtf_5it.js → chunk-WL4C6EOR-BLePb5II.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-CCzPEXaq.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CCzPEXaq.js +1 -0
  18. package/dashboard/dist/assets/clone-D_RUHO8e.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-Cr6bkSKq.js → cose-bilkent-S5V4N54A-Bqz-BOZR.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-oXpXFuJQ.js → dagre-KLK3FWXG-ObXz616p.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-Bq_xdDbg.js → diagram-E7M64L7V-BmnMDa4P.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-N7Er4Dui.js → diagram-IFDJBPK2-DF97e8GZ.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-BU0Zm2Fn.js → diagram-P4PSJMXO-B89xgPXg.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BSgZb5me.js → erDiagram-INFDFZHY-DLsFlh-n.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-Bn7pEu0U.js → flowDiagram-PKNHOUZH-BTOf6WXa.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-B8Xq9tyM.js → ganttDiagram-A5KZAMGK-Bf3qSpfJ.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-BoLUjYDa.js → gitGraphDiagram-K3NZZRJ6-TXYSfb6n.js} +1 -1
  28. package/dashboard/dist/assets/{graph-Pde_ni_y.js → graph-CX9dAzVP.js} +1 -1
  29. package/dashboard/dist/assets/index-BohN_jjP.css +1 -0
  30. package/dashboard/dist/assets/index-jQ0-J3SI.js +556 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Brv2khjP.js → infoDiagram-LFFYTUFH-JqFsL_yV.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-D5hxQ0Ke.js → ishikawaDiagram-PHBUUO56-jA6-TFjI.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CUevv5jA.js → journeyDiagram-4ABVD52K-QvSViLCF.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Cf6XyrAC.js → kanban-definition-K7BYSVSG-CwD7Cohp.js} +1 -1
  35. package/dashboard/dist/assets/{layout-Bc8RP2w3.js → layout-upwYHmrE.js} +1 -1
  36. package/dashboard/dist/assets/{linear-Cd_XUbl7.js → linear-Caio4qd9.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-Bx8MuMEM.js → mermaid.core-GdFnPCS2.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-D_4Pl3Mu.js → mindmap-definition-YRQLILUH-gcJj2wQg.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-DRVbjwxO.js → pieDiagram-SKSYHLDU-D8HzHHsW.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BciLlBMH.js → quadrantDiagram-337W2JSQ-Dw7trxdd.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-Bprwe8Z2.js → requirementDiagram-Z7DCOOCP-4TeVfDt_.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DI0t8Uiu.js → sankeyDiagram-WA2Y5GQK-BoK6HCj7.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CpCLCs5J.js → sequenceDiagram-2WXFIKYE-Cfdz1JPT.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-V-1VCApT.js → stateDiagram-RAJIS63D-CTxzMaII.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BhvDlKAC.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DCAo6tA7.js → timeline-definition-YZTLITO2-BQ6k-eeg.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-CKlbZ6Y_.js → treemap-KZPCXAKY-Dj58PW0M.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-CJSijre_.js → vennDiagram-LZ73GAT5-C_nMFV9L.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DXd1BBmK.js → xychartDiagram-JWTSCODW-BcsMJT15.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +1061 -349
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +6894 -4678
  54. package/dist/index.js.map +1 -1
  55. package/dist/launch/index.js +1191 -1181
  56. package/dist/launch/index.js.map +1 -1
  57. package/package.json +2 -1
  58. package/platforms/README.md +21 -0
  59. package/platforms/claude-code/skills/clear-assignment/SKILL.md +2 -2
  60. package/platforms/claude-code/skills/log-progress/SKILL.md +29 -48
  61. package/platforms/claude-code/skills/plan-assignment/SKILL.md +20 -1
  62. package/platforms/claude-code/skills/save-session-summary/SKILL.md +28 -29
  63. package/platforms/claude-code/skills/set-workspace/SKILL.md +25 -41
  64. package/platforms/codex/skills/clear-assignment/SKILL.md +2 -2
  65. package/platforms/codex/skills/log-progress/SKILL.md +29 -48
  66. package/platforms/codex/skills/plan-assignment/SKILL.md +20 -1
  67. package/platforms/codex/skills/save-session-summary/SKILL.md +28 -29
  68. package/platforms/codex/skills/set-workspace/SKILL.md +25 -41
  69. package/skills/clear-assignment/SKILL.md +2 -2
  70. package/skills/log-progress/SKILL.md +29 -48
  71. package/skills/plan-assignment/SKILL.md +20 -1
  72. package/skills/save-session-summary/SKILL.md +28 -29
  73. package/skills/set-workspace/SKILL.md +25 -41
  74. package/dashboard/dist/assets/channel-BYnzdl2x.js +0 -1
  75. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BnPZbM4g.js +0 -1
  76. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BnPZbM4g.js +0 -1
  77. package/dashboard/dist/assets/clone-DYNFxLr3.js +0 -1
  78. package/dashboard/dist/assets/index-7rNWNKq7.css +0 -1
  79. package/dashboard/dist/assets/index-Nc9kfSW-.js +0 -550
  80. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-B6S2ctrX.js +0 -1
@@ -169,8 +169,8 @@ function extractFrontmatter(fileContent) {
169
169
  const body = fileContent.slice(match[0].length);
170
170
  return [frontmatterBlock, body];
171
171
  }
172
- function parseSimpleValue(raw) {
173
- const trimmed = raw.trim();
172
+ function parseSimpleValue(raw2) {
173
+ const trimmed = raw2.trim();
174
174
  if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
175
175
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
176
176
  return trimmed.slice(1, -1);
@@ -468,8 +468,8 @@ function extractFrontmatter2(fileContent) {
468
468
  const body = fileContent.slice(match[0].length).trim();
469
469
  return [frontmatterBlock, body];
470
470
  }
471
- function parseSimpleValue2(raw) {
472
- const trimmed = raw.trim();
471
+ function parseSimpleValue2(raw2) {
472
+ const trimmed = raw2.trim();
473
473
  if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
474
474
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
475
475
  return trimmed.slice(1, -1);
@@ -1500,8 +1500,8 @@ ${key}: ${formatted}${content.slice(closingIdx)}`;
1500
1500
  function readFrontmatterField(content, key) {
1501
1501
  const match = content.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
1502
1502
  if (!match) return null;
1503
- const raw = match[1].trim().replace(/^['"]|['"]$/g, "");
1504
- return raw === "" || raw === "null" ? null : raw;
1503
+ const raw2 = match[1].trim().replace(/^['"]|['"]$/g, "");
1504
+ return raw2 === "" || raw2 === "null" ? null : raw2;
1505
1505
  }
1506
1506
  async function migrateLegacyArchivedProjects(projectsDir) {
1507
1507
  const result = { reconciled: [] };
@@ -1771,9 +1771,9 @@ function normalizeHiddenList(input) {
1771
1771
  if (!Array.isArray(input)) return [];
1772
1772
  const seen = /* @__PURE__ */ new Set();
1773
1773
  const out = [];
1774
- for (const raw of input) {
1775
- if (typeof raw !== "string") continue;
1776
- const name = raw.trim();
1774
+ for (const raw2 of input) {
1775
+ if (typeof raw2 !== "string") continue;
1776
+ const name = raw2.trim();
1777
1777
  if (name.length === 0) continue;
1778
1778
  if (name.length > MAX_WORKSPACE_NAME_LENGTH) continue;
1779
1779
  if (/[\r\n]/.test(name)) continue;
@@ -1798,9 +1798,11 @@ __export(config_exports, {
1798
1798
  AgentConfigError: () => AgentConfigError,
1799
1799
  BUILTIN_AGENTS: () => BUILTIN_AGENTS,
1800
1800
  DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
1801
+ DEFAULT_STATUS_COLORS: () => DEFAULT_STATUS_COLORS,
1801
1802
  PROMPT_ARG_POSITIONS: () => PROMPT_ARG_POSITIONS,
1802
1803
  TERMINAL_CHOICES: () => TERMINAL_CHOICES,
1803
1804
  TerminalConfigError: () => TerminalConfigError,
1805
+ buildDefaultStatusConfig: () => buildDefaultStatusConfig,
1804
1806
  deleteAgentsConfig: () => deleteAgentsConfig,
1805
1807
  deleteHotkeyBindingsConfig: () => deleteHotkeyBindingsConfig,
1806
1808
  deleteStatusConfig: () => deleteStatusConfig,
@@ -1811,8 +1813,11 @@ __export(config_exports, {
1811
1813
  getAssignmentTypes: () => getAssignmentTypes,
1812
1814
  getTerminal: () => getTerminal,
1813
1815
  parseAgentCommand: () => parseAgentCommand,
1816
+ parseStatusConfig: () => parseStatusConfig,
1814
1817
  parseTerminalConfig: () => parseTerminalConfig,
1815
1818
  readConfig: () => readConfig,
1819
+ serializeStatusConfig: () => serializeStatusConfig,
1820
+ toTitleCase: () => toTitleCase,
1816
1821
  updateAgentsConfig: () => updateAgentsConfig,
1817
1822
  updateBackupConfig: () => updateBackupConfig,
1818
1823
  updateIntegrationConfig: () => updateIntegrationConfig,
@@ -2058,6 +2063,24 @@ function parseStatusConfig(content) {
2058
2063
  transitions
2059
2064
  };
2060
2065
  }
2066
+ function toTitleCase(s) {
2067
+ return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2068
+ }
2069
+ function buildDefaultStatusConfig() {
2070
+ return {
2071
+ statuses: DEFAULT_STATUSES.map((id) => ({
2072
+ id,
2073
+ label: toTitleCase(id),
2074
+ color: DEFAULT_STATUS_COLORS[id] ?? "gray",
2075
+ terminal: id === "completed" || id === "failed"
2076
+ })),
2077
+ order: [...DEFAULT_STATUSES],
2078
+ transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
2079
+ const [from, command] = key.split(":");
2080
+ return { from, command, to };
2081
+ })
2082
+ };
2083
+ }
2061
2084
  function serializeStatusConfig(statuses) {
2062
2085
  const lines = [];
2063
2086
  lines.push("statuses:");
@@ -2152,13 +2175,13 @@ function parsePlaybooksConfig(fmBlock) {
2152
2175
  continue;
2153
2176
  }
2154
2177
  if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
2155
- const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
2156
- if (raw.length === 0) continue;
2157
- if (/\s/.test(raw)) {
2158
- console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
2178
+ const raw2 = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
2179
+ if (raw2.length === 0) continue;
2180
+ if (/\s/.test(raw2)) {
2181
+ console.warn(`Warning: config.md playbooks.disabled entry "${raw2}" contains whitespace, ignoring`);
2159
2182
  continue;
2160
2183
  }
2161
- disabled.push(raw);
2184
+ disabled.push(raw2);
2162
2185
  continue;
2163
2186
  }
2164
2187
  }
@@ -2438,9 +2461,9 @@ function serializeHotkeyBindingsConfig(cfg) {
2438
2461
  async function writeHotkeyBindingsConfig(cfg) {
2439
2462
  const cleaned = {};
2440
2463
  for (const kind of BINDABLE_ACTION_KINDS) {
2441
- const raw = cfg.bindings[kind];
2442
- if (typeof raw !== "string" || raw.trim() === "") continue;
2443
- const canonical = canonicalizeCombo(raw);
2464
+ const raw2 = cfg.bindings[kind];
2465
+ if (typeof raw2 !== "string" || raw2.trim() === "") continue;
2466
+ const canonical = canonicalizeCombo(raw2);
2444
2467
  if (!canonical) continue;
2445
2468
  if (isReservedCombo(canonical)) continue;
2446
2469
  cleaned[kind] = canonical;
@@ -3127,7 +3150,7 @@ async function updateAgentsConfig(mutation, options = {}) {
3127
3150
  await writeAgentsConfig(next);
3128
3151
  return { previous, next, written: true };
3129
3152
  }
3130
- var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
3153
+ var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
3131
3154
  var init_config2 = __esm({
3132
3155
  "src/utils/config.ts"() {
3133
3156
  "use strict";
@@ -3135,6 +3158,7 @@ var init_config2 = __esm({
3135
3158
  init_fs();
3136
3159
  init_config();
3137
3160
  init_fs_migration();
3161
+ init_lifecycle();
3138
3162
  init_hotkeysCatalog();
3139
3163
  init_agents_schema();
3140
3164
  init_terminal_schema();
@@ -3182,6 +3206,14 @@ var init_config2 = __esm({
3182
3206
  AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
3183
3207
  AgentConfigError = class extends Error {
3184
3208
  };
3209
+ DEFAULT_STATUS_COLORS = {
3210
+ pending: "slate",
3211
+ in_progress: "teal",
3212
+ blocked: "amber",
3213
+ review: "violet",
3214
+ completed: "emerald",
3215
+ failed: "rose"
3216
+ };
3185
3217
  KNOWN_AGENT_SCALAR_FIELDS = /* @__PURE__ */ new Set([
3186
3218
  "id",
3187
3219
  "label",
@@ -3237,8 +3269,8 @@ async function resolvePlaybookSlug(playbooksDir2, slug) {
3237
3269
  for (const entry of entries) {
3238
3270
  if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
3239
3271
  const filePath = resolve7(playbooksDir2, entry.name);
3240
- const raw = await readFile6(filePath, "utf-8");
3241
- const parsed = parsePlaybook(raw);
3272
+ const raw2 = await readFile6(filePath, "utf-8");
3273
+ const parsed = parsePlaybook(raw2);
3242
3274
  const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
3243
3275
  if (canonical === slug) {
3244
3276
  return { filename: entry.name, slug: canonical, parsed };
@@ -3285,8 +3317,8 @@ async function rebuildPlaybookManifest(playbooksDir2) {
3285
3317
  const rows = [];
3286
3318
  for (const entry of entries) {
3287
3319
  if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
3288
- const raw = await readFile6(resolve7(playbooksDir2, entry.name), "utf-8");
3289
- const parsed = parsePlaybook(raw);
3320
+ const raw2 = await readFile6(resolve7(playbooksDir2, entry.name), "utf-8");
3321
+ const parsed = parsePlaybook(raw2);
3290
3322
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
3291
3323
  if (disabledSet.has(slug)) continue;
3292
3324
  rows.push({
@@ -3363,8 +3395,8 @@ async function renamePlaybook(playbooksDir2, oldSlug, newSlug) {
3363
3395
  );
3364
3396
  }
3365
3397
  }
3366
- const raw = await readFile6(oldPath, "utf-8");
3367
- let next = setFrontmatterField2(raw, "slug", newSlug);
3398
+ const raw2 = await readFile6(oldPath, "utf-8");
3399
+ let next = setFrontmatterField2(raw2, "slug", newSlug);
3368
3400
  next = setFrontmatterField2(next, "updated", `"${nowTimestamp()}"`);
3369
3401
  await writeFileForce(newPath, next);
3370
3402
  if (!renamedInPlace) {
@@ -4040,6 +4072,37 @@ function initSessionDb(dbPath) {
4040
4072
  UPDATE meta SET value = '4' WHERE key = 'schema_version';
4041
4073
  `);
4042
4074
  }
4075
+ const vBeforeV5 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
4076
+ if (vBeforeV5 === "4") {
4077
+ database.exec(`
4078
+ CREATE TABLE sessions_v5 (
4079
+ session_id TEXT PRIMARY KEY,
4080
+ project_slug TEXT,
4081
+ assignment_slug TEXT,
4082
+ agent TEXT NOT NULL,
4083
+ started TEXT NOT NULL,
4084
+ ended TEXT,
4085
+ status TEXT NOT NULL DEFAULT 'active',
4086
+ path TEXT,
4087
+ description TEXT,
4088
+ transcript_path TEXT,
4089
+ pid INTEGER,
4090
+ pid_started_at TEXT,
4091
+ original_head_sha TEXT,
4092
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4093
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4094
+ );
4095
+ INSERT INTO sessions_v5
4096
+ SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, description, transcript_path, pid, pid_started_at, NULL, created_at, updated_at
4097
+ FROM sessions;
4098
+ DROP TABLE sessions;
4099
+ ALTER TABLE sessions_v5 RENAME TO sessions;
4100
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
4101
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
4102
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
4103
+ UPDATE meta SET value = '5' WHERE key = 'schema_version';
4104
+ `);
4105
+ }
4043
4106
  });
4044
4107
  runMigrations.exclusive();
4045
4108
  db.exec(POST_MIGRATION_INDEXES_SQL);
@@ -4090,9 +4153,9 @@ async function migrateFromMarkdown(projectsDir) {
4090
4153
  }
4091
4154
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
4092
4155
  const { readFile: readFile21 } = await import("fs/promises");
4093
- const raw = await readFile21(filePath, "utf-8");
4156
+ const raw2 = await readFile21(filePath, "utf-8");
4094
4157
  const sessions = [];
4095
- const lines = raw.split("\n");
4158
+ const lines = raw2.split("\n");
4096
4159
  let inTable = false;
4097
4160
  let headerSeen = false;
4098
4161
  for (const line of lines) {
@@ -4131,7 +4194,7 @@ var init_session_db = __esm({
4131
4194
  init_paths();
4132
4195
  init_fs();
4133
4196
  db = null;
4134
- SCHEMA_VERSION = "4";
4197
+ SCHEMA_VERSION = "5";
4135
4198
  SCHEMA_SQL = `
4136
4199
  CREATE TABLE IF NOT EXISTS sessions (
4137
4200
  session_id TEXT PRIMARY KEY,
@@ -4146,6 +4209,7 @@ CREATE TABLE IF NOT EXISTS sessions (
4146
4209
  transcript_path TEXT,
4147
4210
  pid INTEGER,
4148
4211
  pid_started_at TEXT,
4212
+ original_head_sha TEXT,
4149
4213
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
4150
4214
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4151
4215
  );
@@ -4175,25 +4239,27 @@ function rowToSession(row) {
4175
4239
  description: row.description ?? null,
4176
4240
  transcriptPath: row.transcript_path ?? null,
4177
4241
  pid: row.pid ?? null,
4178
- pidStartedAt: row.pid_started_at ?? null
4242
+ pidStartedAt: row.pid_started_at ?? null,
4243
+ originalHeadSha: row.original_head_sha ?? null
4179
4244
  };
4180
4245
  }
4181
4246
  async function appendSession(_projectDir, session) {
4182
4247
  const db4 = getSessionDb();
4183
4248
  db4.prepare(`
4184
- INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at)
4185
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4249
+ INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
4250
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4186
4251
  ON CONFLICT(session_id) DO UPDATE SET
4187
- project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
4188
- assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
4189
- agent = excluded.agent,
4190
- status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
4191
- path = COALESCE(NULLIF(excluded.path, ''), path),
4192
- description = COALESCE(NULLIF(excluded.description, ''), description),
4193
- transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
4194
- pid = COALESCE(excluded.pid, pid),
4195
- pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),
4196
- updated_at = datetime('now')
4252
+ project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
4253
+ assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
4254
+ agent = excluded.agent,
4255
+ status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
4256
+ path = COALESCE(NULLIF(excluded.path, ''), path),
4257
+ description = COALESCE(NULLIF(excluded.description, ''), description),
4258
+ transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
4259
+ pid = COALESCE(excluded.pid, pid),
4260
+ pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),
4261
+ original_head_sha = COALESCE(NULLIF(original_head_sha, ''), NULLIF(excluded.original_head_sha, '')),
4262
+ updated_at = datetime('now')
4197
4263
  `).run(
4198
4264
  session.sessionId,
4199
4265
  session.projectSlug ?? null,
@@ -4205,7 +4271,8 @@ async function appendSession(_projectDir, session) {
4205
4271
  session.description ?? null,
4206
4272
  session.transcriptPath ?? null,
4207
4273
  session.pid ?? null,
4208
- session.pidStartedAt ?? null
4274
+ session.pidStartedAt ?? null,
4275
+ session.originalHeadSha ?? null
4209
4276
  );
4210
4277
  }
4211
4278
  async function updateSessionStatus(_projectDir, sessionId, status) {
@@ -4248,8 +4315,8 @@ async function deleteSessions(sessionIds) {
4248
4315
  }
4249
4316
  async function readAssignmentStatusFromPath(assignmentMdPath) {
4250
4317
  if (!await fileExists(assignmentMdPath)) return null;
4251
- const raw = await readFile8(assignmentMdPath, "utf-8");
4252
- const match = raw.match(/^status:\s*(.+)$/m);
4318
+ const raw2 = await readFile8(assignmentMdPath, "utf-8");
4319
+ const match = raw2.match(/^status:\s*(.+)$/m);
4253
4320
  return match ? match[1].trim() : null;
4254
4321
  }
4255
4322
  async function readAssignmentStatus(projectDir, assignmentSlug) {
@@ -4393,8 +4460,8 @@ async function listSessionFiles(dir) {
4393
4460
  async function readSessionFile(dir, name) {
4394
4461
  const filePath = resolve11(dir, `${sanitizeSessionName(name)}.md`);
4395
4462
  if (!await fileExists(filePath)) return null;
4396
- const raw = await readFile9(filePath, "utf-8");
4397
- const [frontmatter] = extractFrontmatter2(raw);
4463
+ const raw2 = await readFile9(filePath, "utf-8");
4464
+ const [frontmatter] = extractFrontmatter2(raw2);
4398
4465
  if (!frontmatter) return null;
4399
4466
  const session = getField(frontmatter, "session") ?? name;
4400
4467
  const registered = getField(frontmatter, "registered") ?? "";
@@ -4522,8 +4589,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
4522
4589
  return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
4523
4590
  }
4524
4591
  function delay(ms) {
4525
- return new Promise((resolve29) => {
4526
- const timer2 = setTimeout(resolve29, ms);
4592
+ return new Promise((resolve30) => {
4593
+ const timer2 = setTimeout(resolve30, ms);
4527
4594
  if (typeof timer2.unref === "function") {
4528
4595
  timer2.unref();
4529
4596
  }
@@ -5063,9 +5130,6 @@ async function computeStandaloneRecords(assignmentsDir2) {
5063
5130
  records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));
5064
5131
  return records;
5065
5132
  }
5066
- function toTitleCase(s) {
5067
- return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
5068
- }
5069
5133
  function getTransitionDefinitions(config) {
5070
5134
  if (!config.custom) return DEFAULT_TRANSITION_DEFINITIONS;
5071
5135
  const seen = /* @__PURE__ */ new Set();
@@ -5101,19 +5165,12 @@ async function getStatusConfig() {
5101
5165
  terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
5102
5166
  };
5103
5167
  } else {
5168
+ const def = buildDefaultStatusConfig();
5104
5169
  _cachedConfig = {
5105
5170
  custom: false,
5106
- statuses: DEFAULT_STATUSES.map((id) => ({
5107
- id,
5108
- label: toTitleCase(id),
5109
- color: DEFAULT_STATUS_COLORS[id] ?? "gray",
5110
- terminal: id === "completed" || id === "failed"
5111
- })),
5112
- order: [...DEFAULT_STATUSES],
5113
- transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
5114
- const [from, command] = key.split(":");
5115
- return { from, command, to };
5116
- }),
5171
+ statuses: def.statuses,
5172
+ order: def.order,
5173
+ transitions: def.transitions,
5117
5174
  transitionTable: DEFAULT_TRANSITION_TABLE,
5118
5175
  terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
5119
5176
  };
@@ -5130,8 +5187,8 @@ async function listProjects(projectsDir) {
5130
5187
  async function readWorkspaceRegistry(projectsDir) {
5131
5188
  const registryPath = resolve13(dirname2(projectsDir), "workspaces.json");
5132
5189
  try {
5133
- const raw = await readFile10(registryPath, "utf-8");
5134
- const parsed = JSON.parse(raw);
5190
+ const raw2 = await readFile10(registryPath, "utf-8");
5191
+ const parsed = JSON.parse(raw2);
5135
5192
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
5136
5193
  } catch {
5137
5194
  return [];
@@ -5219,8 +5276,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
5219
5276
  const timestamp = nowTimestamp();
5220
5277
  for (const slug of projectsReferencing) {
5221
5278
  const path = resolve13(projectsDir, slug, "project.md");
5222
- const raw = await readFile10(path, "utf-8");
5223
- let next = clearFrontmatterField(raw, "workspace");
5279
+ const raw2 = await readFile10(path, "utf-8");
5280
+ let next = clearFrontmatterField(raw2, "workspace");
5224
5281
  next = setUpdatedField(next, timestamp);
5225
5282
  await writeFileForce(path, next);
5226
5283
  rewroteFiles = true;
@@ -5228,8 +5285,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
5228
5285
  for (const id of standalonesReferencing) {
5229
5286
  if (!opts.assignmentsDir) break;
5230
5287
  const path = resolve13(opts.assignmentsDir, id, "assignment.md");
5231
- const raw = await readFile10(path, "utf-8");
5232
- let next = clearFrontmatterField(raw, "workspaceGroup");
5288
+ const raw2 = await readFile10(path, "utf-8");
5289
+ let next = clearFrontmatterField(raw2, "workspaceGroup");
5233
5290
  next = setUpdatedField(next, timestamp);
5234
5291
  await writeFileForce(path, next);
5235
5292
  rewroteFiles = true;
@@ -6679,8 +6736,8 @@ async function listPlaybooks(playbooksDir2) {
6679
6736
  for (const entry of entries) {
6680
6737
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
6681
6738
  const filePath = resolve13(playbooksDir2, entry.name);
6682
- const raw = await readFile10(filePath, "utf-8");
6683
- const parsed = parsePlaybook(raw);
6739
+ const raw2 = await readFile10(filePath, "utf-8");
6740
+ const parsed = parsePlaybook(raw2);
6684
6741
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
6685
6742
  playbooks.push({
6686
6743
  slug,
@@ -6713,7 +6770,7 @@ async function getPlaybookDetail(playbooksDir2, slug) {
6713
6770
  enabled
6714
6771
  };
6715
6772
  }
6716
- var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
6773
+ var WorkspaceBlockedError, STALE_ASSIGNMENT_MS, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, RECENT_SESSIONS_LIMIT, NEWEST_CREATED_LIMIT, SEGMENT_DISPLAY_CAP, STALE_LIMIT_DEFAULT, STALE_LIMIT_MAX, TERMINAL_STATUSES2, STATUS_TO_SEGMENT, HERO_PRIORITY, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
6717
6774
  var init_api = __esm({
6718
6775
  "src/dashboard/api.ts"() {
6719
6776
  "use strict";
@@ -6828,14 +6885,6 @@ var init_api = __esm({
6828
6885
  requiresReason: false
6829
6886
  }
6830
6887
  ];
6831
- DEFAULT_STATUS_COLORS = {
6832
- pending: "slate",
6833
- in_progress: "teal",
6834
- blocked: "amber",
6835
- review: "violet",
6836
- completed: "emerald",
6837
- failed: "rose"
6838
- };
6839
6888
  _cachedConfig = null;
6840
6889
  REFERENCED_BY_LIMIT = 50;
6841
6890
  migratedProjectsDirs = /* @__PURE__ */ new Set();
@@ -6904,8 +6953,8 @@ init_assignment_resolver();
6904
6953
  init_agent_sessions();
6905
6954
  import express from "express";
6906
6955
  import { createServer } from "http";
6907
- import { resolve as resolve28 } from "path";
6908
- import { writeFile as writeFile6, unlink as unlink6 } from "fs/promises";
6956
+ import { resolve as resolve29 } from "path";
6957
+ import { writeFile as writeFile7, unlink as unlink7 } from "fs/promises";
6909
6958
  import { WebSocketServer, WebSocket } from "ws";
6910
6959
 
6911
6960
  // src/dashboard/session-liveness.ts
@@ -6991,7 +7040,18 @@ function enrichSessions(sessions, agents, deps) {
6991
7040
 
6992
7041
  // src/dashboard/watcher.ts
6993
7042
  import { watch } from "chokidar";
6994
- import { basename as basename2, dirname as dirname3, relative, sep } from "path";
7043
+ import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, relative, sep } from "path";
7044
+ var defaultPathApi = { relative, isAbsolute: isAbsolute2 };
7045
+ function ignoreDotSegmentsBelow(root, pathApi = defaultPathApi) {
7046
+ return (p) => {
7047
+ const rel = pathApi.relative(root, p);
7048
+ if (!rel) return false;
7049
+ if (pathApi.isAbsolute(rel)) return false;
7050
+ const parts = rel.split(/[\\/]/);
7051
+ if (parts[0] === "..") return false;
7052
+ return parts.some((segment) => segment.startsWith("."));
7053
+ };
7054
+ }
6995
7055
  function createWatcher(options) {
6996
7056
  const { projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, dbPath, onMessage, debounceMs = 300 } = options;
6997
7057
  const pendingEvents = /* @__PURE__ */ new Map();
@@ -6999,7 +7059,7 @@ function createWatcher(options) {
6999
7059
  ignoreInitial: true,
7000
7060
  persistent: true,
7001
7061
  depth: 10,
7002
- ignored: /(^|[\/\\])\../
7062
+ ignored: ignoreDotSegmentsBelow(projectsDir)
7003
7063
  });
7004
7064
  function handleProjectChange(filePath) {
7005
7065
  const rel = relative(projectsDir, filePath);
@@ -7068,7 +7128,7 @@ function createWatcher(options) {
7068
7128
  ignoreInitial: true,
7069
7129
  persistent: true,
7070
7130
  depth: 5,
7071
- ignored: /(^|[\/\\])\../
7131
+ ignored: ignoreDotSegmentsBelow(assignmentsDir2)
7072
7132
  });
7073
7133
  standaloneWatcher.on("change", handleStandaloneChange2);
7074
7134
  standaloneWatcher.on("add", handleStandaloneChange2);
@@ -7097,7 +7157,7 @@ function createWatcher(options) {
7097
7157
  ignoreInitial: true,
7098
7158
  persistent: true,
7099
7159
  depth: 1,
7100
- ignored: /(^|[\/\\])\../
7160
+ ignored: ignoreDotSegmentsBelow(serversDir2)
7101
7161
  });
7102
7162
  serversWatcher.on("change", handleServerChange2);
7103
7163
  serversWatcher.on("add", handleServerChange2);
@@ -7126,7 +7186,7 @@ function createWatcher(options) {
7126
7186
  ignoreInitial: true,
7127
7187
  persistent: true,
7128
7188
  depth: 1,
7129
- ignored: /(^|[\/\\])\../
7189
+ ignored: ignoreDotSegmentsBelow(playbooksDir2)
7130
7190
  });
7131
7191
  playbooksWatcher.on("change", handlePlaybookChange2);
7132
7192
  playbooksWatcher.on("add", handlePlaybookChange2);
@@ -7155,7 +7215,7 @@ function createWatcher(options) {
7155
7215
  ignoreInitial: true,
7156
7216
  persistent: true,
7157
7217
  depth: 1,
7158
- ignored: /(^|[\/\\])\../
7218
+ ignored: ignoreDotSegmentsBelow(todosDir2)
7159
7219
  });
7160
7220
  todosWatcher.on("change", handleTodoChange2);
7161
7221
  todosWatcher.on("add", handleTodoChange2);
@@ -7187,7 +7247,7 @@ function createWatcher(options) {
7187
7247
  ignoreInitial: true,
7188
7248
  persistent: true,
7189
7249
  depth: 0,
7190
- ignored: /(^|[\/\\])\../
7250
+ ignored: ignoreDotSegmentsBelow(dbDir)
7191
7251
  });
7192
7252
  leasesDbWatcher.on("change", handleDbChange2);
7193
7253
  leasesDbWatcher.on("add", handleDbChange2);
@@ -7379,15 +7439,15 @@ async function readViewPrefsFile() {
7379
7439
  if (!await fileExists(path)) {
7380
7440
  return { ...DEFAULT_VIEW_PREFS_FILE };
7381
7441
  }
7382
- let raw;
7442
+ let raw2;
7383
7443
  try {
7384
- raw = await readFile11(path, "utf-8");
7444
+ raw2 = await readFile11(path, "utf-8");
7385
7445
  } catch {
7386
7446
  return { ...DEFAULT_VIEW_PREFS_FILE };
7387
7447
  }
7388
7448
  let parsed;
7389
7449
  try {
7390
- parsed = JSON.parse(raw);
7450
+ parsed = JSON.parse(raw2);
7391
7451
  } catch {
7392
7452
  await backupCorrupt(path);
7393
7453
  return { ...DEFAULT_VIEW_PREFS_FILE };
@@ -7592,15 +7652,15 @@ async function readSavedViewsFile() {
7592
7652
  if (!await fileExists(path)) {
7593
7653
  return cloneDefault();
7594
7654
  }
7595
- let raw;
7655
+ let raw2;
7596
7656
  try {
7597
- raw = await readFile12(path, "utf-8");
7657
+ raw2 = await readFile12(path, "utf-8");
7598
7658
  } catch {
7599
7659
  return cloneDefault();
7600
7660
  }
7601
7661
  let parsed;
7602
7662
  try {
7603
- parsed = JSON.parse(raw);
7663
+ parsed = JSON.parse(raw2);
7604
7664
  } catch {
7605
7665
  await backupCorrupt2(path);
7606
7666
  return cloneDefault();
@@ -7909,7 +7969,7 @@ function createDashboardLayoutRouter() {
7909
7969
  init_lifecycle();
7910
7970
  init_slug();
7911
7971
  import { Router as Router2 } from "express";
7912
- import { resolve as resolve18, basename as basename3, isAbsolute as isAbsolute2 } from "path";
7972
+ import { resolve as resolve18, basename as basename3, isAbsolute as isAbsolute4 } from "path";
7913
7973
  import { rm, readFile as readFile15, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
7914
7974
  import { spawnSync as spawnSync3 } from "child_process";
7915
7975
 
@@ -7928,6 +7988,20 @@ init_frontmatter();
7928
7988
  init_fs();
7929
7989
  import { spawn } from "child_process";
7930
7990
  import { readFile as readFile13 } from "fs/promises";
7991
+
7992
+ // src/launch/cwd.ts
7993
+ import { existsSync, statSync as statSync2 } from "fs";
7994
+ import { isAbsolute as isAbsolute3 } from "path";
7995
+ function isExistingDir(p) {
7996
+ if (!p || !isAbsolute3(p)) return false;
7997
+ try {
7998
+ return existsSync(p) && statSync2(p).isDirectory();
7999
+ } catch {
8000
+ return false;
8001
+ }
8002
+ }
8003
+
8004
+ // src/utils/git-worktree.ts
7931
8005
  function run(command, args, cwd) {
7932
8006
  return new Promise((resolvePromise) => {
7933
8007
  const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -7962,11 +8036,11 @@ async function createWorktree(opts) {
7962
8036
  );
7963
8037
  }
7964
8038
  }
7965
- async function removeWorktree(repository, worktreePath) {
7966
- const result = await run(
7967
- "git",
7968
- ["-C", repository, "worktree", "remove", "--force", worktreePath]
7969
- );
8039
+ async function removeWorktree(repository, worktreePath, opts = {}) {
8040
+ const args = ["-C", repository, "worktree", "remove"];
8041
+ if (opts.force) args.push("--force");
8042
+ args.push(worktreePath);
8043
+ const result = await run("git", args);
7970
8044
  return { ok: result.code === 0, stderr: result.stderr };
7971
8045
  }
7972
8046
  async function deleteBranch(repository, branch) {
@@ -8007,6 +8081,82 @@ async function detectDefaultBranch(repository) {
8007
8081
  }
8008
8082
  return branches[0] ?? null;
8009
8083
  }
8084
+ async function captureHeadSha(dir) {
8085
+ const result = await run("git", ["-C", dir, "rev-parse", "HEAD"]);
8086
+ if (result.code !== 0) return null;
8087
+ const sha = result.stdout.trim();
8088
+ return sha.length > 0 ? sha : null;
8089
+ }
8090
+ async function branchCheckedOutAt(repository, branch) {
8091
+ const result = await run("git", ["-C", repository, "worktree", "list", "--porcelain"]);
8092
+ if (result.code !== 0) return null;
8093
+ let currentPath = null;
8094
+ for (const line of result.stdout.split("\n")) {
8095
+ if (line.startsWith("worktree ")) {
8096
+ currentPath = line.slice("worktree ".length).trim();
8097
+ } else if (line.startsWith("branch ")) {
8098
+ const ref = line.slice("branch ".length).trim();
8099
+ if (ref === `refs/heads/${branch}` && currentPath) return currentPath;
8100
+ }
8101
+ }
8102
+ return null;
8103
+ }
8104
+ async function recreateWorktree(opts) {
8105
+ const { repository, worktreePath, branch } = opts;
8106
+ const originalHeadSha = opts.originalHeadSha ?? null;
8107
+ await run("git", ["-C", repository, "worktree", "prune"]);
8108
+ const add = async (args) => {
8109
+ const result = await run("git", ["-C", repository, "worktree", "add", ...args]);
8110
+ if (result.code !== 0) {
8111
+ throw new GitWorktreeError(
8112
+ `git worktree add failed (exit ${result.code}): ${result.stderr.trim() || "(no stderr)"}`,
8113
+ result.stderr
8114
+ );
8115
+ }
8116
+ };
8117
+ const refExists = async (ref) => {
8118
+ const result = await run("git", [
8119
+ "-C",
8120
+ repository,
8121
+ "rev-parse",
8122
+ "--verify",
8123
+ "--quiet",
8124
+ ref
8125
+ ]);
8126
+ return result.code === 0;
8127
+ };
8128
+ if (branch && (await listBranches(repository)).includes(branch)) {
8129
+ const checkedOutAt = await branchCheckedOutAt(repository, branch);
8130
+ if (!checkedOutAt || checkedOutAt === worktreePath || !isExistingDir(checkedOutAt)) {
8131
+ await add([worktreePath, branch]);
8132
+ return { baseUsed: branch, exact: true, branch };
8133
+ }
8134
+ const detachBase = originalHeadSha && await refExists(`${originalHeadSha}^{commit}`) ? originalHeadSha : `refs/heads/${branch}`;
8135
+ await add(["--detach", worktreePath, detachBase]);
8136
+ return { baseUsed: detachBase, exact: detachBase === originalHeadSha, branch: null };
8137
+ }
8138
+ let baseUsed = null;
8139
+ if (originalHeadSha && await refExists(`${originalHeadSha}^{commit}`)) {
8140
+ baseUsed = originalHeadSha;
8141
+ } else if (branch && await refExists(`refs/remotes/origin/${branch}`)) {
8142
+ baseUsed = `refs/remotes/origin/${branch}`;
8143
+ } else {
8144
+ baseUsed = await detectDefaultBranch(repository);
8145
+ }
8146
+ if (!baseUsed) {
8147
+ throw new GitWorktreeError(
8148
+ `recreateWorktree: no base ref to recreate ${worktreePath} (no original sha, no origin/${branch ?? "<none>"}, no default branch)`,
8149
+ ""
8150
+ );
8151
+ }
8152
+ const exact = baseUsed === originalHeadSha;
8153
+ if (branch) {
8154
+ await add(["-b", branch, worktreePath, baseUsed]);
8155
+ return { baseUsed, exact, branch };
8156
+ }
8157
+ await add(["--detach", worktreePath, baseUsed]);
8158
+ return { baseUsed, exact, branch: null };
8159
+ }
8010
8160
  async function createWorktreeAndRecord(opts) {
8011
8161
  const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
8012
8162
  await createWorktree({ repository, branch, worktreePath, parentBranch });
@@ -8020,7 +8170,7 @@ async function createWorktreeAndRecord(opts) {
8020
8170
  });
8021
8171
  await writeFileForce(assignmentPath, updated);
8022
8172
  } catch (writeErr) {
8023
- const cleanup = await removeWorktree(repository, worktreePath);
8173
+ const cleanup = await removeWorktree(repository, worktreePath, { force: true });
8024
8174
  const branchCleanup = await deleteBranch(repository, branch);
8025
8175
  const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);
8026
8176
  throw new Error(
@@ -8121,6 +8271,154 @@ function validateBranchName(name) {
8121
8271
  return null;
8122
8272
  }
8123
8273
 
8274
+ // src/dashboard/recreate-target.ts
8275
+ init_api();
8276
+ init_agent_sessions();
8277
+ async function resolveRecreateTarget(deps, target) {
8278
+ const { projectsDir, assignmentsDir: assignmentsDir2 } = deps;
8279
+ if (target.kind === "assignment") {
8280
+ const detail = "id" in target ? await getAssignmentDetailById(projectsDir, assignmentsDir2, target.id) : await getAssignmentDetail(
8281
+ projectsDir,
8282
+ target.projectSlug,
8283
+ target.assignmentSlug
8284
+ );
8285
+ if (!detail) return null;
8286
+ const worktreePath2 = detail.workspace.worktreePath ?? "";
8287
+ const repository2 = detail.workspace.repository ?? null;
8288
+ const branch2 = detail.workspace.branch ?? null;
8289
+ const missing2 = worktreePath2 !== "" && !isExistingDir(worktreePath2);
8290
+ return {
8291
+ kind: "assignment",
8292
+ id: detail.id,
8293
+ projectSlug: detail.projectSlug ?? null,
8294
+ assignmentSlug: detail.slug,
8295
+ worktreePath: worktreePath2,
8296
+ repository: repository2,
8297
+ branch: branch2,
8298
+ originalHeadSha: null,
8299
+ missing: missing2,
8300
+ recreatable: missing2 && isExistingDir(repository2)
8301
+ };
8302
+ }
8303
+ const session = getSessionById(target.id);
8304
+ if (!session) return null;
8305
+ let repository = null;
8306
+ let branch = null;
8307
+ let assignmentWorktreePath = "";
8308
+ if (session.projectSlug && session.assignmentSlug) {
8309
+ const detail = await getAssignmentDetail(
8310
+ projectsDir,
8311
+ session.projectSlug,
8312
+ session.assignmentSlug
8313
+ );
8314
+ if (detail) {
8315
+ repository = detail.workspace.repository ?? null;
8316
+ branch = detail.workspace.branch ?? null;
8317
+ assignmentWorktreePath = detail.workspace.worktreePath ?? "";
8318
+ }
8319
+ } else if (session.assignmentSlug) {
8320
+ const detail = await getAssignmentDetailById(
8321
+ projectsDir,
8322
+ assignmentsDir2,
8323
+ session.assignmentSlug
8324
+ );
8325
+ if (detail) {
8326
+ repository = detail.workspace.repository ?? null;
8327
+ branch = detail.workspace.branch ?? null;
8328
+ assignmentWorktreePath = detail.workspace.worktreePath ?? "";
8329
+ }
8330
+ }
8331
+ const worktreePath = session.path || assignmentWorktreePath;
8332
+ const missing = worktreePath !== "" && !isExistingDir(worktreePath);
8333
+ return {
8334
+ kind: "session",
8335
+ id: session.sessionId,
8336
+ projectSlug: session.projectSlug ?? null,
8337
+ assignmentSlug: session.assignmentSlug ?? null,
8338
+ worktreePath,
8339
+ repository,
8340
+ branch,
8341
+ originalHeadSha: session.originalHeadSha ?? null,
8342
+ missing,
8343
+ recreatable: missing && isExistingDir(repository)
8344
+ };
8345
+ }
8346
+
8347
+ // src/dashboard/worktree-recreate.ts
8348
+ async function recreateForTarget(deps, target) {
8349
+ const t = await resolveRecreateTarget(deps, target);
8350
+ if (!t) return { status: "not-found" };
8351
+ if (t.worktreePath === "") return { status: "no-path" };
8352
+ if (isExistingDir(t.worktreePath)) {
8353
+ return { status: "already-exists", branch: t.branch };
8354
+ }
8355
+ if (!t.repository) return { status: "no-repo" };
8356
+ const repoCheck = await assertRepoRoot(t.repository);
8357
+ if (!repoCheck.ok) {
8358
+ return { status: "bad-repo", httpStatus: repoCheck.status, error: repoCheck.error };
8359
+ }
8360
+ const key = `recreate:${t.worktreePath}`;
8361
+ if (worktreeInFlight.has(key)) return { status: "in-flight" };
8362
+ worktreeInFlight.add(key);
8363
+ try {
8364
+ const r = await recreateWorktree({
8365
+ repository: repoCheck.repo,
8366
+ worktreePath: t.worktreePath,
8367
+ branch: t.branch,
8368
+ originalHeadSha: t.originalHeadSha
8369
+ });
8370
+ return { status: "recreated", baseUsed: r.baseUsed, exact: r.exact, branch: r.branch };
8371
+ } finally {
8372
+ worktreeInFlight.delete(key);
8373
+ }
8374
+ }
8375
+ function recreateOutcomeToHttp(outcome) {
8376
+ switch (outcome.status) {
8377
+ case "not-found":
8378
+ return { httpStatus: 404, body: { error: "Target not found." } };
8379
+ case "no-path":
8380
+ return {
8381
+ httpStatus: 422,
8382
+ body: { error: "No recorded worktree path to recreate." }
8383
+ };
8384
+ case "no-repo":
8385
+ return {
8386
+ httpStatus: 422,
8387
+ body: {
8388
+ error: "Cannot recreate: no repository on record for this worktree."
8389
+ }
8390
+ };
8391
+ case "bad-repo":
8392
+ return { httpStatus: outcome.httpStatus, body: { error: outcome.error } };
8393
+ case "in-flight":
8394
+ return {
8395
+ httpStatus: 409,
8396
+ body: { error: "A recreate is already in progress for this worktree." }
8397
+ };
8398
+ case "already-exists":
8399
+ return {
8400
+ httpStatus: 200,
8401
+ body: {
8402
+ ok: true,
8403
+ alreadyExisted: true,
8404
+ baseUsed: outcome.branch ?? "HEAD",
8405
+ exact: true,
8406
+ branch: outcome.branch
8407
+ }
8408
+ };
8409
+ case "recreated":
8410
+ return {
8411
+ httpStatus: 200,
8412
+ body: {
8413
+ ok: true,
8414
+ baseUsed: outcome.baseUsed,
8415
+ exact: outcome.exact,
8416
+ branch: outcome.branch
8417
+ }
8418
+ };
8419
+ }
8420
+ }
8421
+
8124
8422
  // src/dashboard/repository-candidates.ts
8125
8423
  init_fs();
8126
8424
  init_parser();
@@ -8145,8 +8443,8 @@ async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
8145
8443
  const projectPath = resolve17(projectsDir, projectSlug, "project.md");
8146
8444
  if (await fileExists(projectPath)) {
8147
8445
  const project = parseProject(await readFile14(projectPath, "utf-8"));
8148
- for (const raw of project.repositories) {
8149
- const path = raw.trim();
8446
+ for (const raw2 of project.repositories) {
8447
+ const path = raw2.trim();
8150
8448
  if (!path) continue;
8151
8449
  const abs = resolve17(path);
8152
8450
  if (seen.has(abs)) continue;
@@ -8807,7 +9105,7 @@ async function assertRepoRoot(repoInput) {
8807
9105
  return { ok: false, status: 400, error: "`repository` is required." };
8808
9106
  }
8809
9107
  const repo = repoInput.trim();
8810
- if (!isAbsolute2(repo)) {
9108
+ if (!isAbsolute4(repo)) {
8811
9109
  return { ok: false, status: 400, error: "`repository` must be an absolute path." };
8812
9110
  }
8813
9111
  try {
@@ -10046,6 +10344,45 @@ ${entry}`;
10046
10344
  }
10047
10345
  }
10048
10346
  );
10347
+ router.post(
10348
+ "/api/projects/:slug/assignments/:aslug/worktree/recreate",
10349
+ async (req, res) => {
10350
+ try {
10351
+ const projectSlug = getParam(req.params.slug);
10352
+ const assignmentSlug = getParam(req.params.aslug);
10353
+ const outcome = await recreateForTarget(
10354
+ { projectsDir, assignmentsDir: assignmentsDir2 ?? "" },
10355
+ { kind: "assignment", projectSlug, assignmentSlug }
10356
+ );
10357
+ const { httpStatus, body } = recreateOutcomeToHttp(outcome);
10358
+ res.status(httpStatus).json(body);
10359
+ } catch (error) {
10360
+ console.error("Error recreating worktree:", error);
10361
+ res.status(500).json({ error: `Failed to recreate worktree: ${error.message}` });
10362
+ }
10363
+ }
10364
+ );
10365
+ router.post(
10366
+ "/api/assignments/:id/worktree/recreate",
10367
+ async (req, res) => {
10368
+ try {
10369
+ if (!assignmentsDir2) {
10370
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
10371
+ return;
10372
+ }
10373
+ const id = getParam(req.params.id);
10374
+ const outcome = await recreateForTarget(
10375
+ { projectsDir, assignmentsDir: assignmentsDir2 },
10376
+ { kind: "assignment", id }
10377
+ );
10378
+ const { httpStatus, body } = recreateOutcomeToHttp(outcome);
10379
+ res.status(httpStatus).json(body);
10380
+ } catch (error) {
10381
+ console.error("Error recreating worktree:", error);
10382
+ res.status(500).json({ error: `Failed to recreate worktree: ${error.message}` });
10383
+ }
10384
+ }
10385
+ );
10049
10386
  router.post("/api/projects/:slug/status-override", async (req, res) => {
10050
10387
  try {
10051
10388
  const projectSlug = getParam(req.params.slug);
@@ -11308,6 +11645,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
11308
11645
  const recordedPath = derivedPath ?? path ?? "";
11309
11646
  const pid = typeof rawPid === "number" && Number.isFinite(rawPid) && rawPid > 0 ? rawPid : null;
11310
11647
  const pidStartedAt = pid !== null ? captureProcessStartedAt(pid) : null;
11648
+ const originalHeadSha = isExistingDir(recordedPath) ? await captureHeadSha(recordedPath) : null;
11311
11649
  const session = {
11312
11650
  projectSlug: projectSlug || null,
11313
11651
  assignmentSlug: assignmentSlug || null,
@@ -11319,7 +11657,8 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
11319
11657
  description: description || null,
11320
11658
  transcriptPath: transcriptPath || null,
11321
11659
  pid,
11322
- pidStartedAt
11660
+ pidStartedAt,
11661
+ originalHeadSha
11323
11662
  };
11324
11663
  await appendSession("", session);
11325
11664
  broadcast?.({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -11328,6 +11667,21 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
11328
11667
  res.status(500).json({ error: error instanceof Error ? error.message : "Registration failed" });
11329
11668
  }
11330
11669
  });
11670
+ router.post("/:sessionId/worktree/recreate", async (req, res) => {
11671
+ try {
11672
+ const { sessionId } = req.params;
11673
+ const outcome = await recreateForTarget(
11674
+ { projectsDir, assignmentsDir: assignmentsDir2 ?? "" },
11675
+ { kind: "session", id: sessionId }
11676
+ );
11677
+ const { httpStatus, body } = recreateOutcomeToHttp(outcome);
11678
+ res.status(httpStatus).json(body);
11679
+ } catch (error) {
11680
+ res.status(500).json({
11681
+ error: error instanceof Error ? error.message : "Failed to recreate worktree"
11682
+ });
11683
+ }
11684
+ });
11331
11685
  router.patch("/:sessionId", async (req, res) => {
11332
11686
  try {
11333
11687
  const { sessionId } = req.params;
@@ -11459,8 +11813,8 @@ function mapAgentErrorToFieldErrors(err) {
11459
11813
  }
11460
11814
  return { error: message };
11461
11815
  }
11462
- function coerceAgentRow(raw, index) {
11463
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
11816
+ function coerceAgentRow(raw2, index) {
11817
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) {
11464
11818
  return {
11465
11819
  ok: false,
11466
11820
  status: 400,
@@ -11470,7 +11824,7 @@ function coerceAgentRow(raw, index) {
11470
11824
  }
11471
11825
  };
11472
11826
  }
11473
- const entry = raw;
11827
+ const entry = raw2;
11474
11828
  if (typeof entry.id !== "string" || entry.id.length === 0) {
11475
11829
  return {
11476
11830
  ok: false,
@@ -11579,14 +11933,14 @@ function createAgentsRouter() {
11579
11933
  });
11580
11934
  router.put("/", async (req, res) => {
11581
11935
  try {
11582
- const raw = req.body && typeof req.body === "object" ? req.body : {};
11583
- if (!Array.isArray(raw.agents)) {
11936
+ const raw2 = req.body && typeof req.body === "object" ? req.body : {};
11937
+ if (!Array.isArray(raw2.agents)) {
11584
11938
  res.status(400).json({ error: "agents must be an array" });
11585
11939
  return;
11586
11940
  }
11587
11941
  const cleaned = [];
11588
- for (let i = 0; i < raw.agents.length; i++) {
11589
- const result = coerceAgentRow(raw.agents[i], i);
11942
+ for (let i = 0; i < raw2.agents.length; i++) {
11943
+ const result = coerceAgentRow(raw2.agents[i], i);
11590
11944
  if (!result.ok) {
11591
11945
  res.status(result.status).json(result.body);
11592
11946
  return;
@@ -11628,7 +11982,7 @@ import { Router as Router6 } from "express";
11628
11982
 
11629
11983
  // src/utils/terminal-probe.ts
11630
11984
  import { spawnSync as spawnSync4 } from "child_process";
11631
- import { existsSync } from "fs";
11985
+ import { existsSync as existsSync2 } from "fs";
11632
11986
  import { homedir as homedir2 } from "os";
11633
11987
  import { join as join2 } from "path";
11634
11988
  var APP_BUNDLE_IDS = {
@@ -11650,12 +12004,12 @@ function defaultApplicationsDirs() {
11650
12004
  }
11651
12005
  function findAppBundle(terminal, dirs = defaultApplicationsDirs()) {
11652
12006
  const fixed = APP_FIXED_PATHS[terminal];
11653
- if (fixed && existsSync(fixed)) return fixed;
12007
+ if (fixed && existsSync2(fixed)) return fixed;
11654
12008
  const bundleName = APP_BUNDLE_NAMES[terminal];
11655
12009
  if (bundleName) {
11656
12010
  for (const dir of dirs) {
11657
12011
  const candidate = join2(dir, bundleName);
11658
- if (existsSync(candidate)) return candidate;
12012
+ if (existsSync2(candidate)) return candidate;
11659
12013
  }
11660
12014
  }
11661
12015
  return null;
@@ -11692,50 +12046,6 @@ function probeTerminalInstalled(terminal, opts = {}) {
11692
12046
  return { ok: false, reason: "no-probe-available" };
11693
12047
  }
11694
12048
 
11695
- // src/dashboard/api-launch-preflight.ts
11696
- init_api();
11697
-
11698
- // src/launch/cwd.ts
11699
- import { existsSync as existsSync2, statSync as statSync2 } from "fs";
11700
- import { isAbsolute as isAbsolute3 } from "path";
11701
- function isExistingDir(p) {
11702
- if (!p || !isAbsolute3(p)) return false;
11703
- try {
11704
- return existsSync2(p) && statSync2(p).isDirectory();
11705
- } catch {
11706
- return false;
11707
- }
11708
- }
11709
- function resolveWorkspaceCwd(input) {
11710
- const { worktreePath, repository, branch, assignmentSlug } = input;
11711
- if (isExistingDir(worktreePath)) {
11712
- return { cwd: worktreePath, fallbackWarning: null, invalidReason: null };
11713
- }
11714
- if (isExistingDir(repository)) {
11715
- const fallbackWarning = worktreePath ? `syntaur: workspace.worktreePath ${worktreePath} is not an existing directory for ${assignmentSlug} \u2014 launching in ${repository}` : formatFallbackCwdWarning({
11716
- assignmentSlug,
11717
- workspaceDir: repository,
11718
- worktreePath,
11719
- branch
11720
- });
11721
- return { cwd: repository, fallbackWarning, invalidReason: null };
11722
- }
11723
- const shown = (p) => p && p.trim().length > 0 ? p : "(unset)";
11724
- return {
11725
- cwd: null,
11726
- fallbackWarning: null,
11727
- invalidReason: `workspace path invalid for ${assignmentSlug}: tried worktreePath ${shown(worktreePath)} and repository ${shown(repository)} \u2014 neither is an existing directory`
11728
- };
11729
- }
11730
- function formatFallbackCwdWarning(opts) {
11731
- const missing = [];
11732
- if (!opts.worktreePath) missing.push("worktreePath");
11733
- if (!opts.branch) missing.push("branch");
11734
- if (missing.length === 0) return null;
11735
- const fields = missing.map((m) => `workspace.${m}`).join(" and ");
11736
- return `syntaur: ${fields} not set for ${opts.assignmentSlug} \u2014 launching in ${opts.workspaceDir}`;
11737
- }
11738
-
11739
12049
  // src/dashboard/api-launch-preflight.ts
11740
12050
  function createLaunchPreflightRouter(projectsDir, assignmentsDir2) {
11741
12051
  const router = Router6();
@@ -11763,29 +12073,44 @@ function createLaunchPreflightRouter(projectsDir, assignmentsDir2) {
11763
12073
  return;
11764
12074
  }
11765
12075
  const target = body.target;
11766
- if (target && target.kind === "assignment" && typeof target.id === "string") {
11767
- const detail = await getAssignmentDetailById(
11768
- projectsDir,
11769
- assignmentsDir2,
11770
- target.id
12076
+ if (target && (target.kind === "assignment" || target.kind === "session") && typeof target.id === "string") {
12077
+ const resolved = await resolveRecreateTarget(
12078
+ { projectsDir, assignmentsDir: assignmentsDir2 },
12079
+ target.kind === "assignment" ? { kind: "assignment", id: target.id } : { kind: "session", id: target.id }
11771
12080
  );
11772
- if (detail) {
11773
- const picked = resolveWorkspaceCwd({
11774
- worktreePath: detail.workspace.worktreePath,
11775
- repository: detail.workspace.repository,
11776
- branch: detail.workspace.branch,
11777
- assignmentSlug: detail.slug
11778
- });
11779
- if (picked.cwd === null) {
11780
- const response2 = {
11781
- ok: false,
11782
- terminal,
11783
- reason: "workspace-path-invalid",
11784
- message: picked.invalidReason ?? "This assignment has no valid workspace directory."
11785
- };
11786
- res.json(response2);
11787
- return;
11788
- }
12081
+ if (resolved && resolved.missing) {
12082
+ const response2 = resolved.recreatable ? {
12083
+ ok: false,
12084
+ terminal,
12085
+ reason: "workspace-path-invalid",
12086
+ message: `Worktree ${resolved.worktreePath} was deleted.`,
12087
+ recreate: {
12088
+ kind: resolved.kind,
12089
+ id: resolved.id,
12090
+ projectSlug: resolved.projectSlug,
12091
+ assignmentSlug: resolved.assignmentSlug,
12092
+ deletedPath: resolved.worktreePath,
12093
+ repository: resolved.repository,
12094
+ branch: resolved.branch
12095
+ }
12096
+ } : {
12097
+ ok: false,
12098
+ terminal,
12099
+ reason: "workspace-path-invalid",
12100
+ message: `Worktree ${resolved.worktreePath} was deleted and can't be auto-recreated (no repository on record). Set a valid workspace for this assignment, then try again.`
12101
+ };
12102
+ res.json(response2);
12103
+ return;
12104
+ }
12105
+ if (resolved && resolved.kind === "assignment" && resolved.worktreePath === "" && !isExistingDir(resolved.repository)) {
12106
+ const response2 = {
12107
+ ok: false,
12108
+ terminal,
12109
+ reason: "workspace-path-invalid",
12110
+ message: "This assignment has no valid workspace directory. Set a repository or worktree, then try again."
12111
+ };
12112
+ res.json(response2);
12113
+ return;
11789
12114
  }
11790
12115
  }
11791
12116
  const response = { ok: true, terminal };
@@ -12186,18 +12511,18 @@ function buildAffectedResponse(id, list) {
12186
12511
  function isString(x) {
12187
12512
  return typeof x === "string" && x.length > 0;
12188
12513
  }
12189
- function parseResolutions(raw) {
12190
- if (raw === void 0) {
12514
+ function parseResolutions(raw2) {
12515
+ if (raw2 === void 0) {
12191
12516
  return { resolutions: [], malformed: null, duplicateIds: null };
12192
12517
  }
12193
- if (!Array.isArray(raw)) {
12518
+ if (!Array.isArray(raw2)) {
12194
12519
  return { resolutions: [], malformed: "resolutions must be an array", duplicateIds: null };
12195
12520
  }
12196
12521
  const out = [];
12197
12522
  const seen = /* @__PURE__ */ new Set();
12198
12523
  const dups = /* @__PURE__ */ new Set();
12199
- for (let i = 0; i < raw.length; i++) {
12200
- const r = raw[i];
12524
+ for (let i = 0; i < raw2.length; i++) {
12525
+ const r = raw2[i];
12201
12526
  if (!r || typeof r !== "object") {
12202
12527
  return { resolutions: [], malformed: `resolutions[${i}] must be an object`, duplicateIds: null };
12203
12528
  }
@@ -13173,10 +13498,10 @@ init_fs_migration();
13173
13498
  init_parser2();
13174
13499
  init_fs();
13175
13500
  init_paths();
13176
- import { Router as Router13 } from "express";
13177
- import { readdir as readdir10 } from "fs/promises";
13178
- import { resolve as resolvePath, dirname as dirname5 } from "path";
13179
- import { rename as rename5, mkdir as mkdir2 } from "fs/promises";
13501
+ import { Router as Router14 } from "express";
13502
+ import { readdir as readdir11 } from "fs/promises";
13503
+ import { resolve as resolvePath, dirname as dirname6 } from "path";
13504
+ import { rename as rename6, mkdir as mkdir3 } from "fs/promises";
13180
13505
  init_slug();
13181
13506
 
13182
13507
  // src/utils/promote-todos.ts
@@ -13515,6 +13840,331 @@ async function promoteTodosToNewAssignment(groups, options) {
13515
13840
 
13516
13841
  // src/dashboard/api-todos.ts
13517
13842
  init_api();
13843
+
13844
+ // src/dashboard/todo-attachments-routes.ts
13845
+ import { raw } from "express";
13846
+
13847
+ // src/todos/attachments.ts
13848
+ import { mkdir as mkdir2, readdir as readdir10, stat, rename as rename5, rm as rm3, unlink as unlink5, writeFile as writeFile5, cp } from "fs/promises";
13849
+ import { resolve as resolve25, basename as basename4, dirname as dirname5, extname } from "path";
13850
+
13851
+ // src/utils/proof-artifact-id.ts
13852
+ import { randomBytes as randomBytes2 } from "crypto";
13853
+ function generateArtifactId() {
13854
+ const ts = Date.now().toString(36);
13855
+ const rand = randomBytes2(2).toString("hex");
13856
+ return `${ts}-${rand}`;
13857
+ }
13858
+
13859
+ // src/todos/attachments.ts
13860
+ var SCOPE_RE = /^[a-z0-9_][a-z0-9-]*$/;
13861
+ var TODO_ID_RE = /^[a-f0-9]{4}$/;
13862
+ var ATTACHMENT_ID_RE = /^[a-z0-9]+-[0-9a-f]{4}$/;
13863
+ var AttachmentValidationError = class extends Error {
13864
+ constructor(message) {
13865
+ super(message);
13866
+ this.name = "AttachmentValidationError";
13867
+ }
13868
+ };
13869
+ function assertScope(scopeId) {
13870
+ if (!SCOPE_RE.test(scopeId)) throw new AttachmentValidationError(`Invalid scope id: "${scopeId}"`);
13871
+ }
13872
+ function assertTodoId(todoId) {
13873
+ if (!TODO_ID_RE.test(todoId)) throw new AttachmentValidationError(`Invalid todo id: "${todoId}"`);
13874
+ }
13875
+ function assertAttachmentId(attachmentId) {
13876
+ if (!ATTACHMENT_ID_RE.test(attachmentId)) {
13877
+ throw new AttachmentValidationError(`Invalid attachment id: "${attachmentId}"`);
13878
+ }
13879
+ }
13880
+ var EXT_MIME = {
13881
+ png: "image/png",
13882
+ jpg: "image/jpeg",
13883
+ jpeg: "image/jpeg",
13884
+ gif: "image/gif",
13885
+ webp: "image/webp",
13886
+ bmp: "image/bmp",
13887
+ ico: "image/x-icon",
13888
+ svg: "image/svg+xml",
13889
+ pdf: "application/pdf",
13890
+ txt: "text/plain",
13891
+ log: "text/plain",
13892
+ md: "text/markdown",
13893
+ json: "application/json",
13894
+ csv: "text/csv",
13895
+ html: "text/html",
13896
+ htm: "text/html",
13897
+ xml: "application/xml",
13898
+ mp4: "video/mp4",
13899
+ mov: "video/quicktime",
13900
+ webm: "video/webm",
13901
+ mp3: "audio/mpeg",
13902
+ wav: "audio/wav",
13903
+ m4a: "audio/mp4",
13904
+ zip: "application/zip"
13905
+ };
13906
+ function mimeForName(name) {
13907
+ const ext = extname(name).slice(1).toLowerCase();
13908
+ return EXT_MIME[ext] ?? "application/octet-stream";
13909
+ }
13910
+ var SAFE_INLINE_MIME = /* @__PURE__ */ new Set([
13911
+ "image/png",
13912
+ "image/jpeg",
13913
+ "image/gif",
13914
+ "image/webp",
13915
+ "application/pdf",
13916
+ "text/plain"
13917
+ ]);
13918
+ function isSafeInlineMime(mime) {
13919
+ return SAFE_INLINE_MIME.has(mime);
13920
+ }
13921
+ function sanitizeAttachmentName(name) {
13922
+ let n = basename4(name || "").replace(/["'\\/]/g, "_");
13923
+ n = Array.from(n, (ch) => {
13924
+ const code = ch.charCodeAt(0);
13925
+ return code < 32 || code === 127 ? "_" : ch;
13926
+ }).join("");
13927
+ n = n.trim();
13928
+ if (!n || n === "." || n === "..") n = "file";
13929
+ if (n.length > 120) {
13930
+ const ext = extname(n);
13931
+ n = n.slice(0, Math.max(1, 120 - ext.length)) + ext;
13932
+ }
13933
+ return n;
13934
+ }
13935
+ function attachmentsRootDir(todosDir2) {
13936
+ return resolve25(todosDir2, "attachments");
13937
+ }
13938
+ function attachmentDirFor(todosDir2, scopeId, todoId) {
13939
+ assertScope(scopeId);
13940
+ assertTodoId(todoId);
13941
+ return resolve25(attachmentsRootDir(todosDir2), scopeId, todoId);
13942
+ }
13943
+ async function dirExists(p) {
13944
+ try {
13945
+ return (await stat(p)).isDirectory();
13946
+ } catch {
13947
+ return false;
13948
+ }
13949
+ }
13950
+ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes) {
13951
+ const dir = attachmentDirFor(todosDir2, scopeId, todoId);
13952
+ await mkdir2(dir, { recursive: true });
13953
+ const id = generateArtifactId();
13954
+ const filename = sanitizeAttachmentName(originalName);
13955
+ await writeFile5(resolve25(dir, `${id}__${filename}`), bytes);
13956
+ return {
13957
+ id,
13958
+ filename,
13959
+ mime: mimeForName(filename),
13960
+ size: bytes.length,
13961
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
13962
+ };
13963
+ }
13964
+ async function listAttachments(todosDir2, scopeId, todoId) {
13965
+ const dir = attachmentDirFor(todosDir2, scopeId, todoId);
13966
+ let names;
13967
+ try {
13968
+ names = await readdir10(dir);
13969
+ } catch {
13970
+ return [];
13971
+ }
13972
+ const out = [];
13973
+ for (const stored of names) {
13974
+ const sep2 = stored.indexOf("__");
13975
+ if (sep2 <= 0) continue;
13976
+ const id = stored.slice(0, sep2);
13977
+ if (!ATTACHMENT_ID_RE.test(id)) continue;
13978
+ const filename = stored.slice(sep2 + 2);
13979
+ try {
13980
+ const st = await stat(resolve25(dir, stored));
13981
+ if (!st.isFile()) continue;
13982
+ out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
13983
+ } catch {
13984
+ }
13985
+ }
13986
+ out.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
13987
+ return out;
13988
+ }
13989
+ async function readScopeAttachments(todosDir2, scopeId) {
13990
+ assertScope(scopeId);
13991
+ const scopeDir = resolve25(attachmentsRootDir(todosDir2), scopeId);
13992
+ let todoIds;
13993
+ try {
13994
+ todoIds = await readdir10(scopeDir);
13995
+ } catch {
13996
+ return {};
13997
+ }
13998
+ const result = {};
13999
+ for (const todoId of todoIds) {
14000
+ if (!TODO_ID_RE.test(todoId)) continue;
14001
+ const list = await listAttachments(todosDir2, scopeId, todoId);
14002
+ if (list.length) result[todoId] = list;
14003
+ }
14004
+ return result;
14005
+ }
14006
+ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
14007
+ assertAttachmentId(attachmentId);
14008
+ const dir = attachmentDirFor(todosDir2, scopeId, todoId);
14009
+ let names;
14010
+ try {
14011
+ names = await readdir10(dir);
14012
+ } catch {
14013
+ return null;
14014
+ }
14015
+ const prefix = `${attachmentId}__`;
14016
+ const stored = names.find((n) => n.startsWith(prefix));
14017
+ if (!stored) return null;
14018
+ const filename = stored.slice(prefix.length);
14019
+ return { path: resolve25(dir, stored), filename, mime: mimeForName(filename) };
14020
+ }
14021
+ async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
14022
+ const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
14023
+ if (!resolved) return false;
14024
+ await unlink5(resolved.path);
14025
+ return true;
14026
+ }
14027
+ async function deleteAllAttachments(todosDir2, scopeId, todoId) {
14028
+ await rm3(attachmentDirFor(todosDir2, scopeId, todoId), { recursive: true, force: true });
14029
+ }
14030
+ async function attachmentMoveConflict(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId, todoId) {
14031
+ const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
14032
+ const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
14033
+ return await dirExists(src) && await dirExists(dst);
14034
+ }
14035
+ async function moveAttachments(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId, todoId) {
14036
+ const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
14037
+ if (!await dirExists(src)) return;
14038
+ const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
14039
+ await mkdir2(dirname5(dst), { recursive: true });
14040
+ try {
14041
+ await rename5(src, dst);
14042
+ } catch (err) {
14043
+ if (err?.code === "EXDEV") {
14044
+ await cp(src, dst, { recursive: true });
14045
+ await rm3(src, { recursive: true, force: true });
14046
+ } else {
14047
+ throw err;
14048
+ }
14049
+ }
14050
+ }
14051
+
14052
+ // src/dashboard/todo-attachments-routes.ts
14053
+ var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
14054
+ function headerValue(req, name) {
14055
+ const v = req.headers[name];
14056
+ return Array.isArray(v) ? v[0] : v;
14057
+ }
14058
+ function paramStr(v) {
14059
+ return Array.isArray(v) ? v[0] ?? "" : v ?? "";
14060
+ }
14061
+ function sendError(res, err) {
14062
+ if (err instanceof AttachmentValidationError) {
14063
+ res.status(400).json({ error: err.message });
14064
+ return;
14065
+ }
14066
+ res.status(500).json({ error: err instanceof Error ? err.message : "Attachment operation failed" });
14067
+ }
14068
+ function contentDisposition(filename, inline) {
14069
+ const disp = inline ? "inline" : "attachment";
14070
+ const asciiFallback = Array.from(filename, (ch) => {
14071
+ const code = ch.charCodeAt(0);
14072
+ return code >= 32 && code <= 126 && ch !== '"' && ch !== "\\" ? ch : "_";
14073
+ }).join("");
14074
+ return `${disp}; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
14075
+ }
14076
+ function installTodoAttachmentRoutes(router, prefix, opts) {
14077
+ router.post(
14078
+ `${prefix}/attachments`,
14079
+ raw({ type: () => true, limit: MAX_UPLOAD_BYTES }),
14080
+ async (req, res) => {
14081
+ try {
14082
+ const rawName = headerValue(req, "x-attachment-filename");
14083
+ let filename = "file";
14084
+ if (rawName) {
14085
+ try {
14086
+ filename = decodeURIComponent(rawName);
14087
+ } catch {
14088
+ res.status(400).json({ error: "Invalid x-attachment-filename header" });
14089
+ return;
14090
+ }
14091
+ }
14092
+ const body = req.body;
14093
+ if (!Buffer.isBuffer(body) || body.length === 0) {
14094
+ res.status(400).json({ error: "Empty upload body" });
14095
+ return;
14096
+ }
14097
+ const scope = opts.resolveScope(req);
14098
+ const result = await opts.withScopeLock(req, async () => {
14099
+ if (!await opts.todoExists(scope)) return null;
14100
+ return writeAttachment(scope.todosDir, scope.scopeId, scope.todoId, filename, body);
14101
+ });
14102
+ if (!result) {
14103
+ res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
14104
+ return;
14105
+ }
14106
+ opts.onChange(req);
14107
+ res.status(201).json(result);
14108
+ } catch (err) {
14109
+ sendError(res, err);
14110
+ }
14111
+ }
14112
+ );
14113
+ router.get(`${prefix}/attachments/:attachmentId`, async (req, res) => {
14114
+ try {
14115
+ const scope = opts.resolveScope(req);
14116
+ const attachmentId = paramStr(req.params.attachmentId);
14117
+ const resolved = await opts.withScopeLock(req, async () => {
14118
+ if (!await opts.todoExists(scope)) return { notFound: true };
14119
+ return {
14120
+ file: await resolveAttachmentFile(scope.todosDir, scope.scopeId, scope.todoId, attachmentId)
14121
+ };
14122
+ });
14123
+ if ("notFound" in resolved) {
14124
+ res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
14125
+ return;
14126
+ }
14127
+ if (!resolved.file) {
14128
+ res.status(404).json({ error: `Attachment "${attachmentId}" not found` });
14129
+ return;
14130
+ }
14131
+ const { path, filename, mime } = resolved.file;
14132
+ const inline = isSafeInlineMime(mime);
14133
+ res.setHeader("X-Content-Type-Options", "nosniff");
14134
+ res.setHeader("Content-Type", inline ? mime : "application/octet-stream");
14135
+ res.setHeader("Content-Disposition", contentDisposition(filename, inline));
14136
+ res.sendFile(path, (err) => {
14137
+ if (err && !res.headersSent) res.status(500).end();
14138
+ });
14139
+ } catch (err) {
14140
+ sendError(res, err);
14141
+ }
14142
+ });
14143
+ router.delete(`${prefix}/attachments/:attachmentId`, async (req, res) => {
14144
+ try {
14145
+ const scope = opts.resolveScope(req);
14146
+ const attachmentId = paramStr(req.params.attachmentId);
14147
+ const result = await opts.withScopeLock(req, async () => {
14148
+ if (!await opts.todoExists(scope)) return { notFound: true };
14149
+ return { deleted: await deleteAttachment(scope.todosDir, scope.scopeId, scope.todoId, attachmentId) };
14150
+ });
14151
+ if ("notFound" in result) {
14152
+ res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
14153
+ return;
14154
+ }
14155
+ if (!result.deleted) {
14156
+ res.status(404).json({ error: `Attachment "${attachmentId}" not found` });
14157
+ return;
14158
+ }
14159
+ opts.onChange(req);
14160
+ res.json({ deleted: attachmentId });
14161
+ } catch (err) {
14162
+ sendError(res, err);
14163
+ }
14164
+ });
14165
+ }
14166
+
14167
+ // src/dashboard/api-todos.ts
13518
14168
  var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
13519
14169
  function getWorkspaceParam(value) {
13520
14170
  if (Array.isArray(value)) {
@@ -13528,7 +14178,7 @@ function touchItem3(item) {
13528
14178
  item.updatedAt = now;
13529
14179
  }
13530
14180
  function createTodosRouter(todosDir2, broadcast, projectsDir) {
13531
- const router = Router13();
14181
+ const router = Router14();
13532
14182
  installRecordsInvalidation(router);
13533
14183
  function broadcastUpdate() {
13534
14184
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -13545,6 +14195,19 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
13545
14195
  next();
13546
14196
  }
13547
14197
  router.param("workspace", validateWorkspace);
14198
+ installTodoAttachmentRoutes(router, "/:workspace/:id", {
14199
+ resolveScope: (req) => ({
14200
+ todosDir: todosDir2,
14201
+ scopeId: getWorkspaceParam(req.params.workspace),
14202
+ todoId: getWorkspaceParam(req.params.id)
14203
+ }),
14204
+ withScopeLock: (req, fn) => wsLock(getWorkspaceParam(req.params.workspace), fn),
14205
+ todoExists: async (scope) => {
14206
+ const checklist = await readChecklist(scope.todosDir, scope.scopeId);
14207
+ return checklist.items.some((i) => i.id === scope.todoId);
14208
+ },
14209
+ onChange: () => broadcastUpdate()
14210
+ });
13548
14211
  router.post("/promote-bulk", async (req, res) => {
13549
14212
  try {
13550
14213
  const { groups, mode, target, title, type, priority, keepSource } = req.body ?? {};
@@ -13649,17 +14312,18 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
13649
14312
  router.get("/", async (_req, res) => {
13650
14313
  try {
13651
14314
  await ensureDir(todosDir2);
13652
- const files = await readdir10(todosDir2).catch(() => []);
14315
+ const files = await readdir11(todosDir2).catch(() => []);
13653
14316
  const workspaces = [];
13654
14317
  for (const file of files) {
13655
14318
  if (typeof file !== "string") continue;
13656
14319
  if (!file.endsWith(".md") || file.endsWith("-log.md")) continue;
13657
14320
  const workspace = file.replace(".md", "");
13658
14321
  const checklist = await readChecklist(todosDir2, workspace);
14322
+ const attachmentsByTodo = await readScopeAttachments(todosDir2, checklist.workspace);
13659
14323
  workspaces.push({
13660
14324
  workspace: checklist.workspace,
13661
14325
  archiveInterval: checklist.archiveInterval,
13662
- items: checklist.items,
14326
+ items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
13663
14327
  counts: computeCounts(checklist.items)
13664
14328
  });
13665
14329
  }
@@ -13672,10 +14336,11 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
13672
14336
  try {
13673
14337
  const workspace = getWorkspaceParam(req.params.workspace);
13674
14338
  const checklist = await readChecklist(todosDir2, workspace);
14339
+ const attachmentsByTodo = await readScopeAttachments(todosDir2, workspace);
13675
14340
  res.json({
13676
14341
  workspace: checklist.workspace,
13677
14342
  archiveInterval: checklist.archiveInterval,
13678
- items: checklist.items,
14343
+ items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
13679
14344
  counts: computeCounts(checklist.items)
13680
14345
  });
13681
14346
  } catch (error) {
@@ -13763,63 +14428,68 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
13763
14428
  router.post("/:workspace/archive", async (req, res) => {
13764
14429
  try {
13765
14430
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
13766
- const { resolve: resolve29 } = await import("path");
14431
+ const { resolve: resolve30 } = await import("path");
13767
14432
  const { readFile: readFile21 } = await import("fs/promises");
13768
14433
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
13769
14434
  const workspace = getWorkspaceParam(req.params.workspace);
13770
- const checklist = await readChecklist(todosDir2, workspace);
13771
- const log = await readLog(todosDir2, workspace);
13772
- const completedIds = new Set(
13773
- checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
13774
- );
13775
- if (completedIds.size === 0) {
13776
- res.json({ archived: 0, message: "No completed items to archive" });
13777
- return;
13778
- }
13779
- const toArchive = log.entries.filter(
13780
- (e) => e.itemIds.every((id) => completedIds.has(id))
13781
- );
13782
- const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
13783
- await ensureDir(resolve29(todosDir2, "archive"));
13784
- let archContent = "";
13785
- if (await fileExists(archFile)) {
13786
- archContent = await readFile21(archFile, "utf-8");
13787
- archContent = archContent.trimEnd() + "\n\n";
13788
- } else {
13789
- archContent = `---
14435
+ const outcome = await wsLock(workspace, async () => {
14436
+ const checklist = await readChecklist(todosDir2, workspace);
14437
+ const log = await readLog(todosDir2, workspace);
14438
+ const completedIds = new Set(
14439
+ checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
14440
+ );
14441
+ if (completedIds.size === 0) {
14442
+ return { archived: 0, message: "No completed items to archive" };
14443
+ }
14444
+ const toArchive = log.entries.filter(
14445
+ (e) => e.itemIds.every((id) => completedIds.has(id))
14446
+ );
14447
+ const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
14448
+ await ensureDir(resolve30(todosDir2, "archive"));
14449
+ let archContent = "";
14450
+ if (await fileExists(archFile)) {
14451
+ archContent = await readFile21(archFile, "utf-8");
14452
+ archContent = archContent.trimEnd() + "\n\n";
14453
+ } else {
14454
+ archContent = `---
13790
14455
  workspace: ${workspace}
13791
14456
  ---
13792
14457
 
13793
14458
  # Archive
13794
14459
 
13795
14460
  `;
13796
- }
13797
- const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
13798
- for (const item of completedItems) {
13799
- archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
14461
+ }
14462
+ const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
14463
+ for (const item of completedItems) {
14464
+ archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
13800
14465
  `;
13801
- }
13802
- archContent += "\n";
13803
- for (const entry of toArchive) {
13804
- archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
14466
+ }
14467
+ archContent += "\n";
14468
+ for (const entry of toArchive) {
14469
+ archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
13805
14470
  `;
13806
- if (entry.items) archContent += `**Items:** ${entry.items}
14471
+ if (entry.items) archContent += `**Items:** ${entry.items}
13807
14472
  `;
13808
- if (entry.session) archContent += `**Session:** ${entry.session}
14473
+ if (entry.session) archContent += `**Session:** ${entry.session}
13809
14474
  `;
13810
- if (entry.branch) archContent += `**Branch:** ${entry.branch}
14475
+ if (entry.branch) archContent += `**Branch:** ${entry.branch}
13811
14476
  `;
13812
- if (entry.summary) archContent += `**Summary:** ${entry.summary}
14477
+ if (entry.summary) archContent += `**Summary:** ${entry.summary}
13813
14478
  `;
13814
- if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
14479
+ if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
13815
14480
  `;
13816
- archContent += "\n";
13817
- }
13818
- await writeFileForce2(archFile, archContent);
13819
- checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
13820
- await writeChecklist(todosDir2, checklist);
13821
- broadcastUpdate();
13822
- res.json({ archived: completedIds.size, logEntries: toArchive.length });
14481
+ archContent += "\n";
14482
+ }
14483
+ await writeFileForce2(archFile, archContent);
14484
+ checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
14485
+ await writeChecklist(todosDir2, checklist);
14486
+ for (const id of completedIds) {
14487
+ await deleteAllAttachments(todosDir2, workspace, id);
14488
+ }
14489
+ return { archived: completedIds.size, logEntries: toArchive.length };
14490
+ });
14491
+ if (outcome.archived > 0) broadcastUpdate();
14492
+ res.json(outcome);
13823
14493
  } catch (error) {
13824
14494
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to archive" });
13825
14495
  }
@@ -13844,7 +14514,8 @@ workspace: ${workspace}
13844
14514
  }
13845
14515
  const log = await readLog(todosDir2, workspace);
13846
14516
  const logEntries = log.entries.filter((e) => e.itemIds.includes(req.params.id));
13847
- res.json({ ...item, log: logEntries });
14517
+ const attachments = await listAttachments(todosDir2, workspace, item.id);
14518
+ res.json({ ...item, attachments, log: logEntries });
13848
14519
  } catch (error) {
13849
14520
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
13850
14521
  }
@@ -13881,6 +14552,7 @@ workspace: ${workspace}
13881
14552
  if (idx === -1) return false;
13882
14553
  checklist.items.splice(idx, 1);
13883
14554
  await writeChecklist(todosDir2, checklist);
14555
+ await deleteAllAttachments(todosDir2, workspace, req.params.id);
13884
14556
  return true;
13885
14557
  });
13886
14558
  if (!deleted) {
@@ -14192,15 +14864,22 @@ workspace: ${workspace}
14192
14864
  return { status: 409, error: "id already exists in target" };
14193
14865
  }
14194
14866
  const item = sourceChecklist.items[idx];
14867
+ let newPlanDir = null;
14195
14868
  if (item.planDir) {
14196
- const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
14869
+ newPlanDir = todoPlanDir(target.todosPath, target.id, id);
14197
14870
  if (await fileExists(newPlanDir)) {
14198
14871
  return { status: 409, error: "plan dir already exists in target" };
14199
14872
  }
14200
- await mkdir2(dirname5(newPlanDir), { recursive: true });
14201
- await rename5(item.planDir, newPlanDir);
14873
+ }
14874
+ if (await attachmentMoveConflict(todosDir2, sourceWs, target.todosPath, target.id, id)) {
14875
+ return { status: 409, error: "attachments already exist in target" };
14876
+ }
14877
+ if (item.planDir && newPlanDir) {
14878
+ await mkdir3(dirname6(newPlanDir), { recursive: true });
14879
+ await rename6(item.planDir, newPlanDir);
14202
14880
  item.planDir = newPlanDir;
14203
14881
  }
14882
+ await moveAttachments(todosDir2, sourceWs, target.todosPath, target.id, id);
14204
14883
  sourceChecklist.items.splice(idx, 1);
14205
14884
  targetChecklist.items.push(item);
14206
14885
  await writeChecklist(todosDir2, sourceChecklist);
@@ -14273,9 +14952,9 @@ init_parser2();
14273
14952
  init_fs();
14274
14953
  init_paths();
14275
14954
  init_slug();
14276
- import { Router as Router14 } from "express";
14277
- import { mkdir as mkdir3, readFile as readFile18, rename as rename6 } from "fs/promises";
14278
- import { resolve as resolve25, dirname as dirname6 } from "path";
14955
+ import { Router as Router15 } from "express";
14956
+ import { mkdir as mkdir4, readFile as readFile18, rename as rename7 } from "fs/promises";
14957
+ import { resolve as resolve26, dirname as dirname7 } from "path";
14279
14958
  init_api();
14280
14959
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
14281
14960
  function touchItem4(item) {
@@ -14291,12 +14970,12 @@ function params(req) {
14291
14970
  return req.params;
14292
14971
  }
14293
14972
  async function projectExists(projectsDir, slug) {
14294
- return fileExists(resolve25(projectsDir, slug, "project.md"));
14973
+ return fileExists(resolve26(projectsDir, slug, "project.md"));
14295
14974
  }
14296
14975
  async function ensureProjectTodosDir(projectsDir, slug) {
14297
14976
  const todosDir2 = projectTodosDir(projectsDir, slug);
14298
14977
  try {
14299
- await mkdir3(todosDir2, { recursive: false });
14978
+ await mkdir4(todosDir2, { recursive: false });
14300
14979
  } catch (err) {
14301
14980
  const code = err.code;
14302
14981
  if (code === "EEXIST") return;
@@ -14308,7 +14987,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
14308
14987
  throw err;
14309
14988
  }
14310
14989
  try {
14311
- await mkdir3(resolve25(todosDir2, "archive"), { recursive: false });
14990
+ await mkdir4(resolve26(todosDir2, "archive"), { recursive: false });
14312
14991
  } catch (err) {
14313
14992
  const code = err.code;
14314
14993
  if (code === "EEXIST") return;
@@ -14324,7 +15003,7 @@ function notFound(res, slug) {
14324
15003
  res.status(404).json({ error: `Project "${slug}" not found` });
14325
15004
  }
14326
15005
  function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
14327
- const router = Router14({ mergeParams: true });
15006
+ const router = Router15({ mergeParams: true });
14328
15007
  installRecordsInvalidation(router);
14329
15008
  function broadcastUpdate(projectSlug) {
14330
15009
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -14341,6 +15020,18 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
14341
15020
  next();
14342
15021
  }
14343
15022
  router.use(validateProjectId);
15023
+ installTodoAttachmentRoutes(router, "/:id", {
15024
+ resolveScope: (req) => {
15025
+ const slug = getProjectIdParam(params(req).projectId);
15026
+ return { todosDir: projectTodosDir(projectsDir, slug), scopeId: slug, todoId: params(req).id ?? "" };
15027
+ },
15028
+ withScopeLock: (req, fn) => projLock(getProjectIdParam(params(req).projectId), fn),
15029
+ todoExists: async (scope) => {
15030
+ const checklist = await readChecklist(scope.todosDir, scope.scopeId);
15031
+ return checklist.items.some((i) => i.id === scope.todoId);
15032
+ },
15033
+ onChange: (req) => broadcastUpdate(getProjectIdParam(params(req).projectId))
15034
+ });
14344
15035
  router.get("/", async (req, res) => {
14345
15036
  try {
14346
15037
  const slug = getProjectIdParam(params(req).projectId);
@@ -14350,10 +15041,11 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
14350
15041
  }
14351
15042
  const todosDir2 = projectTodosDir(projectsDir, slug);
14352
15043
  const checklist = await readChecklist(todosDir2, slug);
15044
+ const attachmentsByTodo = await readScopeAttachments(todosDir2, slug);
14353
15045
  res.json({
14354
15046
  workspace: checklist.workspace,
14355
15047
  archiveInterval: checklist.archiveInterval,
14356
- items: checklist.items,
15048
+ items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
14357
15049
  counts: computeCounts(checklist.items)
14358
15050
  });
14359
15051
  } catch (error) {
@@ -14481,61 +15173,71 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
14481
15173
  notFound(res, slug);
14482
15174
  return;
14483
15175
  }
14484
- const todosDir2 = projectTodosDir(projectsDir, slug);
14485
- await ensureProjectTodosDir(projectsDir, slug);
14486
- const checklist = await readChecklist(todosDir2, slug);
14487
- const log = await readLog(todosDir2, slug);
14488
- const completedIds = new Set(
14489
- checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
14490
- );
14491
- if (completedIds.size === 0) {
14492
- res.json({ archived: 0, message: "No completed items to archive" });
14493
- return;
14494
- }
14495
- const toArchive = log.entries.filter(
14496
- (e) => e.itemIds.every((id) => completedIds.has(id))
14497
- );
14498
- const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
14499
- let archContent = "";
14500
- if (await fileExists(archFile)) {
14501
- archContent = await readFile18(archFile, "utf-8");
14502
- archContent = archContent.trimEnd() + "\n\n";
14503
- } else {
14504
- archContent = `---
15176
+ const outcome = await projLock(slug, async () => {
15177
+ if (!await projectExists(projectsDir, slug)) return "gone";
15178
+ await ensureProjectTodosDir(projectsDir, slug);
15179
+ const todosDir2 = projectTodosDir(projectsDir, slug);
15180
+ const checklist = await readChecklist(todosDir2, slug);
15181
+ const log = await readLog(todosDir2, slug);
15182
+ const completedIds = new Set(
15183
+ checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
15184
+ );
15185
+ if (completedIds.size === 0) {
15186
+ return { archived: 0, message: "No completed items to archive" };
15187
+ }
15188
+ const toArchive = log.entries.filter(
15189
+ (e) => e.itemIds.every((id) => completedIds.has(id))
15190
+ );
15191
+ const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
15192
+ let archContent = "";
15193
+ if (await fileExists(archFile)) {
15194
+ archContent = await readFile18(archFile, "utf-8");
15195
+ archContent = archContent.trimEnd() + "\n\n";
15196
+ } else {
15197
+ archContent = `---
14505
15198
  workspace: ${slug}
14506
15199
  ---
14507
15200
 
14508
15201
  # Archive
14509
15202
 
14510
15203
  `;
14511
- }
14512
- const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
14513
- for (const item of completedItems) {
14514
- archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
15204
+ }
15205
+ const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
15206
+ for (const item of completedItems) {
15207
+ archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
14515
15208
  `;
14516
- }
14517
- archContent += "\n";
14518
- for (const entry of toArchive) {
14519
- archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
15209
+ }
15210
+ archContent += "\n";
15211
+ for (const entry of toArchive) {
15212
+ archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
14520
15213
  `;
14521
- if (entry.items) archContent += `**Items:** ${entry.items}
15214
+ if (entry.items) archContent += `**Items:** ${entry.items}
14522
15215
  `;
14523
- if (entry.session) archContent += `**Session:** ${entry.session}
15216
+ if (entry.session) archContent += `**Session:** ${entry.session}
14524
15217
  `;
14525
- if (entry.branch) archContent += `**Branch:** ${entry.branch}
15218
+ if (entry.branch) archContent += `**Branch:** ${entry.branch}
14526
15219
  `;
14527
- if (entry.summary) archContent += `**Summary:** ${entry.summary}
15220
+ if (entry.summary) archContent += `**Summary:** ${entry.summary}
14528
15221
  `;
14529
- if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
15222
+ if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
14530
15223
  `;
14531
- archContent += "\n";
15224
+ archContent += "\n";
15225
+ }
15226
+ await writeFileForce(archFile, archContent);
15227
+ checklist.workspace = slug;
15228
+ checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
15229
+ await writeChecklist(todosDir2, checklist);
15230
+ for (const id of completedIds) {
15231
+ await deleteAllAttachments(todosDir2, slug, id);
15232
+ }
15233
+ return { archived: completedIds.size, logEntries: toArchive.length };
15234
+ });
15235
+ if (outcome === "gone") {
15236
+ notFound(res, slug);
15237
+ return;
14532
15238
  }
14533
- await writeFileForce(archFile, archContent);
14534
- checklist.workspace = slug;
14535
- checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
14536
- await writeChecklist(todosDir2, checklist);
14537
- broadcastUpdate(slug);
14538
- res.json({ archived: completedIds.size, logEntries: toArchive.length });
15239
+ if (outcome.archived > 0) broadcastUpdate(slug);
15240
+ res.json(outcome);
14539
15241
  } catch (error) {
14540
15242
  if (error.code === "PROJECT_GONE") {
14541
15243
  notFound(res, getProjectIdParam(params(req).projectId));
@@ -14575,7 +15277,8 @@ workspace: ${slug}
14575
15277
  }
14576
15278
  const log = await readLog(todosDir2, slug);
14577
15279
  const logEntries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
14578
- res.json({ ...item, log: logEntries });
15280
+ const attachments = await listAttachments(todosDir2, slug, item.id);
15281
+ res.json({ ...item, attachments, log: logEntries });
14579
15282
  } catch (error) {
14580
15283
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
14581
15284
  }
@@ -14631,11 +15334,13 @@ workspace: ${slug}
14631
15334
  await ensureProjectTodosDir(projectsDir, slug);
14632
15335
  const todosDir2 = projectTodosDir(projectsDir, slug);
14633
15336
  const checklist = await readChecklist(todosDir2, slug);
14634
- const idx = checklist.items.findIndex((i) => i.id === (params(req).id ?? ""));
15337
+ const targetId = params(req).id ?? "";
15338
+ const idx = checklist.items.findIndex((i) => i.id === targetId);
14635
15339
  if (idx === -1) return false;
14636
15340
  checklist.items.splice(idx, 1);
14637
15341
  checklist.workspace = slug;
14638
15342
  await writeChecklist(todosDir2, checklist);
15343
+ await deleteAllAttachments(todosDir2, slug, targetId);
14639
15344
  return true;
14640
15345
  });
14641
15346
  if (deleted === "gone") {
@@ -14945,15 +15650,15 @@ workspace: ${slug}
14945
15650
  if (tg.includes("/")) {
14946
15651
  const parts = tg.split("/");
14947
15652
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
14948
- assignmentDir = resolve25(projectsDir, parts[0], "assignments", parts[1]);
15653
+ assignmentDir = resolve26(projectsDir, parts[0], "assignments", parts[1]);
14949
15654
  assignmentRef = `${parts[0]}/${parts[1]}`;
14950
15655
  } 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)) {
14951
- assignmentDir = resolve25(assignmentsDirFn(), tg);
15656
+ assignmentDir = resolve26(assignmentsDirFn(), tg);
14952
15657
  assignmentRef = tg;
14953
15658
  } else {
14954
15659
  return { error: `Invalid target.assignment "${tg}"` };
14955
15660
  }
14956
- const assignmentMdPath = resolve25(assignmentDir, "assignment.md");
15661
+ const assignmentMdPath = resolve26(assignmentDir, "assignment.md");
14957
15662
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
14958
15663
  let content = await readFile18(assignmentMdPath, "utf-8");
14959
15664
  content = appendTodosToAssignmentBody2(
@@ -15083,15 +15788,22 @@ workspace: ${slug}
15083
15788
  return { status: 409, error: "id already exists in target" };
15084
15789
  }
15085
15790
  const item = sourceChecklist.items[idx];
15791
+ let newPlanDir = null;
15086
15792
  if (item.planDir) {
15087
- const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
15793
+ newPlanDir = todoPlanDir(target.todosPath, target.id, id);
15088
15794
  if (await fileExists(newPlanDir)) {
15089
15795
  return { status: 409, error: "plan dir already exists in target" };
15090
15796
  }
15091
- await mkdir3(dirname6(newPlanDir), { recursive: true });
15092
- await rename6(item.planDir, newPlanDir);
15797
+ }
15798
+ if (await attachmentMoveConflict(sourceTodosDir, sourceSlug, target.todosPath, target.id, id)) {
15799
+ return { status: 409, error: "attachments already exist in target" };
15800
+ }
15801
+ if (item.planDir && newPlanDir) {
15802
+ await mkdir4(dirname7(newPlanDir), { recursive: true });
15803
+ await rename7(item.planDir, newPlanDir);
15093
15804
  item.planDir = newPlanDir;
15094
15805
  }
15806
+ await moveAttachments(sourceTodosDir, sourceSlug, target.todosPath, target.id, id);
15095
15807
  sourceChecklist.items.splice(idx, 1);
15096
15808
  targetChecklist.items.push(item);
15097
15809
  sourceChecklist.workspace = sourceSlug;
@@ -15150,33 +15862,33 @@ workspace: ${slug}
15150
15862
  }
15151
15863
 
15152
15864
  // src/dashboard/api-bundles.ts
15153
- import { Router as Router15 } from "express";
15154
- import { readdir as readdir11 } from "fs/promises";
15865
+ import { Router as Router16 } from "express";
15866
+ import { readdir as readdir12 } from "fs/promises";
15155
15867
 
15156
15868
  // src/todos/bundle-parser.ts
15157
15869
  init_parser();
15158
15870
  init_fs();
15159
15871
  init_paths();
15160
15872
  init_parser2();
15161
- import { randomBytes as randomBytes2 } from "crypto";
15873
+ import { randomBytes as randomBytes3 } from "crypto";
15162
15874
  import { readFile as readFile19 } from "fs/promises";
15163
15875
  var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
15164
15876
  var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
15165
15877
  var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
15166
15878
  var BUNDLE_LINE_REGEX = /^- b:([a-f0-9]{4})\s+<([^>]*)>\s*$/;
15167
- function parseScopeToken(raw) {
15168
- const idx = raw.indexOf(":");
15879
+ function parseScopeToken(raw2) {
15880
+ const idx = raw2.indexOf(":");
15169
15881
  if (idx < 0) return null;
15170
- const scopeRaw = raw.slice(0, idx);
15171
- const scopeId = raw.slice(idx + 1);
15882
+ const scopeRaw = raw2.slice(0, idx);
15883
+ const scopeId = raw2.slice(idx + 1);
15172
15884
  if (!SCOPE_VALUES.has(scopeRaw)) return null;
15173
15885
  if (!scopeId) return null;
15174
15886
  if (!SCOPE_ID_REGEX.test(scopeId)) return null;
15175
15887
  return { scope: scopeRaw, scopeId };
15176
15888
  }
15177
- function parseTodosToken(raw) {
15178
- if (!raw) return [];
15179
- return raw.split(",").map((s) => s.trim()).filter((s) => BUNDLE_ID_REGEX.test(s));
15889
+ function parseTodosToken(raw2) {
15890
+ if (!raw2) return [];
15891
+ return raw2.split(",").map((s) => s.trim()).filter((s) => BUNDLE_ID_REGEX.test(s));
15180
15892
  }
15181
15893
  function parseBundleLine(line) {
15182
15894
  const match = line.match(BUNDLE_LINE_REGEX);
@@ -15261,7 +15973,7 @@ function annotate(bundle, items) {
15261
15973
  }
15262
15974
  function createBundlesRouter(todosDir2, broadcast) {
15263
15975
  void broadcast;
15264
- const router = Router15();
15976
+ const router = Router16();
15265
15977
  function validateWorkspace(req, res, next) {
15266
15978
  const workspace = getWorkspaceParam2(req.params.workspace);
15267
15979
  if (workspace && !WORKSPACE_REGEX3.test(workspace)) {
@@ -15275,7 +15987,7 @@ function createBundlesRouter(todosDir2, broadcast) {
15275
15987
  try {
15276
15988
  await ensureDir(todosDir2);
15277
15989
  const bundles = await readBundles(todosDir2);
15278
- const workspaceFiles = await readdir11(todosDir2).catch(() => []);
15990
+ const workspaceFiles = await readdir12(todosDir2).catch(() => []);
15279
15991
  const itemsByKey = /* @__PURE__ */ new Map();
15280
15992
  for (const f of workspaceFiles) {
15281
15993
  if (typeof f !== "string") continue;
@@ -15327,8 +16039,8 @@ function createBundlesRouter(todosDir2, broadcast) {
15327
16039
  init_fs();
15328
16040
  init_paths();
15329
16041
  init_slug();
15330
- import { Router as Router16 } from "express";
15331
- import { resolve as resolve26 } from "path";
16042
+ import { Router as Router17 } from "express";
16043
+ import { resolve as resolve27 } from "path";
15332
16044
  init_parser2();
15333
16045
  function deriveStatus2(bundle, items) {
15334
16046
  const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
@@ -15357,7 +16069,7 @@ function notFound2(res, slug) {
15357
16069
  }
15358
16070
  function createProjectBundlesRouter(projectsDir, broadcast) {
15359
16071
  void broadcast;
15360
- const router = Router16({ mergeParams: true });
16072
+ const router = Router17({ mergeParams: true });
15361
16073
  function validateProjectId(req, res, next) {
15362
16074
  const slug = getProjectIdParam2(req.params.projectId);
15363
16075
  if (!slug || !isValidSlug(slug)) {
@@ -15370,7 +16082,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
15370
16082
  router.get("/", async (req, res) => {
15371
16083
  try {
15372
16084
  const slug = getProjectIdParam2(req.params.projectId);
15373
- const projectMd = resolve26(projectsDir, slug, "project.md");
16085
+ const projectMd = resolve27(projectsDir, slug, "project.md");
15374
16086
  if (!await fileExists(projectMd)) {
15375
16087
  notFound2(res, slug);
15376
16088
  return;
@@ -15391,7 +16103,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
15391
16103
  init_config2();
15392
16104
  init_api();
15393
16105
  init_scanner();
15394
- import { Router as Router17 } from "express";
16106
+ import { Router as Router18 } from "express";
15395
16107
 
15396
16108
  // src/utils/github-backup.ts
15397
16109
  init_paths();
@@ -15399,8 +16111,8 @@ init_fs();
15399
16111
  init_config2();
15400
16112
  import { execFile as execFile2 } from "child_process";
15401
16113
  import { promisify as promisify2 } from "util";
15402
- 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";
15403
- import { resolve as resolve27, join as join3 } from "path";
16114
+ import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile20, writeFile as writeFile6, unlink as unlink6, stat as stat2, open as open2, rename as rename8 } from "fs/promises";
16115
+ import { resolve as resolve28, join as join3 } from "path";
15404
16116
  import { tmpdir } from "os";
15405
16117
  var exec2 = promisify2(execFile2);
15406
16118
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -15440,7 +16152,7 @@ async function resolveCategoryPath(category) {
15440
16152
  case "servers":
15441
16153
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
15442
16154
  case "config":
15443
- return { sourcePath: resolve27(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
16155
+ return { sourcePath: resolve28(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
15444
16156
  }
15445
16157
  }
15446
16158
  async function checkGitInstalled() {
@@ -15451,7 +16163,7 @@ async function checkGitInstalled() {
15451
16163
  }
15452
16164
  }
15453
16165
  async function acquireLock() {
15454
- const lockPath = resolve27(syntaurRoot(), LOCK_FILE_NAME);
16166
+ const lockPath = resolve28(syntaurRoot(), LOCK_FILE_NAME);
15455
16167
  await ensureDir(syntaurRoot());
15456
16168
  try {
15457
16169
  const handle = await open2(lockPath, "wx");
@@ -15470,7 +16182,7 @@ async function acquireLock() {
15470
16182
  }
15471
16183
  async function releaseLock(lockPath) {
15472
16184
  try {
15473
- await unlink5(lockPath);
16185
+ await unlink6(lockPath);
15474
16186
  } catch {
15475
16187
  }
15476
16188
  }
@@ -15493,13 +16205,13 @@ async function cloneOrInit(repoUrl, destDir) {
15493
16205
  }
15494
16206
  async function copyRecursive(src, dest) {
15495
16207
  if (!await fileExists(src)) return;
15496
- const s = await stat(src);
16208
+ const s = await stat2(src);
15497
16209
  if (s.isDirectory()) {
15498
16210
  await ensureDir(dest);
15499
- await cp(src, dest, { recursive: true, force: true });
16211
+ await cp2(src, dest, { recursive: true, force: true });
15500
16212
  } else {
15501
- await ensureDir(resolve27(dest, ".."));
15502
- await cp(src, dest, { force: true });
16213
+ await ensureDir(resolve28(dest, ".."));
16214
+ await cp2(src, dest, { force: true });
15503
16215
  }
15504
16216
  }
15505
16217
  function resolveCategoriesStrict(csv) {
@@ -15536,9 +16248,9 @@ async function backupToGithub(overrides) {
15536
16248
  const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
15537
16249
  const destPath = join3(tmpDir, repoPath);
15538
16250
  if (isFile) {
15539
- await rm3(destPath, { force: true });
16251
+ await rm4(destPath, { force: true });
15540
16252
  } else {
15541
- await rm3(destPath, { recursive: true, force: true });
16253
+ await rm4(destPath, { recursive: true, force: true });
15542
16254
  }
15543
16255
  if (!await fileExists(sourcePath)) {
15544
16256
  console.warn(`Category "${category}": no local data at ${sourcePath}; backup will reflect deletion.`);
@@ -15546,8 +16258,8 @@ async function backupToGithub(overrides) {
15546
16258
  }
15547
16259
  if (category === "config") {
15548
16260
  const sanitized = await readSanitizedConfig(sourcePath);
15549
- await ensureDir(resolve27(destPath, ".."));
15550
- await writeFile5(destPath, sanitized, "utf-8");
16261
+ await ensureDir(resolve28(destPath, ".."));
16262
+ await writeFile6(destPath, sanitized, "utf-8");
15551
16263
  } else {
15552
16264
  await copyRecursive(sourcePath, destPath);
15553
16265
  }
@@ -15592,7 +16304,7 @@ async function backupToGithub(overrides) {
15592
16304
  };
15593
16305
  } finally {
15594
16306
  if (tmpDir) {
15595
- await rm3(tmpDir, { recursive: true, force: true }).catch(() => {
16307
+ await rm4(tmpDir, { recursive: true, force: true }).catch(() => {
15596
16308
  });
15597
16309
  }
15598
16310
  await releaseLock(lockPath);
@@ -15600,18 +16312,18 @@ async function backupToGithub(overrides) {
15600
16312
  }
15601
16313
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
15602
16314
  if (isFile) {
15603
- await ensureDir(resolve27(localPath, ".."));
15604
- await cp(repoSrcPath, localPath, { force: true });
16315
+ await ensureDir(resolve28(localPath, ".."));
16316
+ await cp2(repoSrcPath, localPath, { force: true });
15605
16317
  return;
15606
16318
  }
15607
16319
  const stagingPath = `${localPath}.syntaur-restore-staging`;
15608
16320
  const backupPath = `${localPath}.syntaur-restore-backup`;
15609
- await rm3(stagingPath, { recursive: true, force: true });
16321
+ await rm4(stagingPath, { recursive: true, force: true });
15610
16322
  const backupExistsBefore = await fileExists(backupPath);
15611
16323
  const localExistsBefore = await fileExists(localPath);
15612
16324
  if (backupExistsBefore) {
15613
16325
  if (!localExistsBefore) {
15614
- await rename7(backupPath, localPath);
16326
+ await rename8(backupPath, localPath);
15615
16327
  } else {
15616
16328
  throw new Error(
15617
16329
  `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.`
@@ -15620,21 +16332,21 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
15620
16332
  }
15621
16333
  let localMovedAside = false;
15622
16334
  try {
15623
- await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
16335
+ await cp2(repoSrcPath, stagingPath, { recursive: true, force: true });
15624
16336
  const localExists = await fileExists(localPath);
15625
16337
  if (localExists) {
15626
- await rename7(localPath, backupPath);
16338
+ await rename8(localPath, backupPath);
15627
16339
  localMovedAside = true;
15628
16340
  }
15629
- await rename7(stagingPath, localPath);
15630
- await rm3(backupPath, { recursive: true, force: true }).catch(() => {
16341
+ await rename8(stagingPath, localPath);
16342
+ await rm4(backupPath, { recursive: true, force: true }).catch(() => {
15631
16343
  });
15632
16344
  } catch (err) {
15633
16345
  if (localMovedAside && await fileExists(backupPath)) {
15634
- await rename7(backupPath, localPath).catch(() => {
16346
+ await rename8(backupPath, localPath).catch(() => {
15635
16347
  });
15636
16348
  }
15637
- await rm3(stagingPath, { recursive: true, force: true }).catch(() => {
16349
+ await rm4(stagingPath, { recursive: true, force: true }).catch(() => {
15638
16350
  });
15639
16351
  throw err;
15640
16352
  }
@@ -15693,7 +16405,7 @@ async function restoreFromGithub(overrides) {
15693
16405
  };
15694
16406
  } finally {
15695
16407
  if (tmpDir) {
15696
- await rm3(tmpDir, { recursive: true, force: true }).catch(() => {
16408
+ await rm4(tmpDir, { recursive: true, force: true }).catch(() => {
15697
16409
  });
15698
16410
  }
15699
16411
  await releaseLock(lockPath);
@@ -15701,7 +16413,7 @@ async function restoreFromGithub(overrides) {
15701
16413
  }
15702
16414
  async function getBackupStatus() {
15703
16415
  const config = await readConfig();
15704
- const lockPath = resolve27(syntaurRoot(), LOCK_FILE_NAME);
16416
+ const lockPath = resolve28(syntaurRoot(), LOCK_FILE_NAME);
15705
16417
  const locked = await fileExists(lockPath);
15706
16418
  return {
15707
16419
  repo: config.backup?.repo ?? null,
@@ -15714,7 +16426,7 @@ async function getBackupStatus() {
15714
16426
 
15715
16427
  // src/dashboard/api-backup.ts
15716
16428
  function createBackupRouter() {
15717
- const router = Router17();
16429
+ const router = Router18();
15718
16430
  router.get("/", async (_req, res) => {
15719
16431
  try {
15720
16432
  const status = await getBackupStatus();
@@ -16049,7 +16761,7 @@ function createDashboardServer(options) {
16049
16761
  (async () => {
16050
16762
  try {
16051
16763
  const configResult = await migrateLegacyConfig(
16052
- resolve28(syntaurRoot(), "config.md")
16764
+ resolve29(syntaurRoot(), "config.md")
16053
16765
  );
16054
16766
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
16055
16767
  const summary = summarizeMigration(projectResult, configResult);
@@ -16150,8 +16862,8 @@ function createDashboardServer(options) {
16150
16862
  });
16151
16863
  app.put("/api/config/hotkeys", async (req, res) => {
16152
16864
  try {
16153
- const raw = req.body && typeof req.body === "object" ? req.body : {};
16154
- const incoming = raw.bindings;
16865
+ const raw2 = req.body && typeof req.body === "object" ? req.body : {};
16866
+ const incoming = raw2.bindings;
16155
16867
  if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) {
16156
16868
  res.status(400).json({ error: "bindings must be an object keyed by action kind" });
16157
16869
  return;
@@ -16567,14 +17279,14 @@ function createDashboardServer(options) {
16567
17279
  app.use("/api/backup", createBackupRouter());
16568
17280
  if (serveStaticUi && dashboardDistPath) {
16569
17281
  const sendOpts = { dotfiles: "allow" };
16570
- app.use("/assets", express.static(resolve28(dashboardDistPath, "assets"), sendOpts));
17282
+ app.use("/assets", express.static(resolve29(dashboardDistPath, "assets"), sendOpts));
16571
17283
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
16572
17284
  app.get("{*path}", async (req, res) => {
16573
17285
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
16574
17286
  res.status(404).json({ error: "Not Found" });
16575
17287
  return;
16576
17288
  }
16577
- const indexPath = resolve28(dashboardDistPath, "index.html");
17289
+ const indexPath = resolve29(dashboardDistPath, "index.html");
16578
17290
  if (!await fileExists(indexPath)) {
16579
17291
  res.status(503).send(
16580
17292
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -16598,7 +17310,7 @@ function createDashboardServer(options) {
16598
17310
  serversDir: serversDir2,
16599
17311
  playbooksDir: playbooksDir2,
16600
17312
  todosDir: todosDir2,
16601
- dbPath: resolve28(syntaurRoot(), "syntaur.db"),
17313
+ dbPath: resolve29(syntaurRoot(), "syntaur.db"),
16602
17314
  onMessage: broadcast
16603
17315
  });
16604
17316
  startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
@@ -16613,8 +17325,8 @@ function createDashboardServer(options) {
16613
17325
  }
16614
17326
  });
16615
17327
  server.listen(port, () => {
16616
- const portFile = resolve28(syntaurRoot(), "dashboard-port");
16617
- writeFile6(portFile, String(port), "utf-8").catch(() => {
17328
+ const portFile = resolve29(syntaurRoot(), "dashboard-port");
17329
+ writeFile7(portFile, String(port), "utf-8").catch(() => {
16618
17330
  });
16619
17331
  resolvePromise();
16620
17332
  });
@@ -16632,8 +17344,8 @@ function createDashboardServer(options) {
16632
17344
  client.terminate();
16633
17345
  }
16634
17346
  clients.clear();
16635
- const portFile = resolve28(syntaurRoot(), "dashboard-port");
16636
- await unlink6(portFile).catch(() => {
17347
+ const portFile = resolve29(syntaurRoot(), "dashboard-port");
17348
+ await unlink7(portFile).catch(() => {
16637
17349
  });
16638
17350
  server.closeAllConnections?.();
16639
17351
  return new Promise((resolvePromise) => {