syntaur 0.16.0 → 0.16.2

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 (65) hide show
  1. package/dashboard/dist/assets/{_basePickBy-B1S22H85.js → _basePickBy-DTuHiZCP.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-CJIRCCsh.js → _baseUniq-BjbISCNN.js} +1 -1
  3. package/dashboard/dist/assets/{arc-CdlNYTcd.js → arc-D9mCa8If.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-KVI0HWOy.js → architectureDiagram-2XIMDMQ5-CLWheiZu.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-ClnG7gaq.js → blockDiagram-WCTKOSBZ-BBxrVTWB.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BD9lkttL.js → c4Diagram-IC4MRINW-DnhDZ2W3.js} +1 -1
  7. package/dashboard/dist/assets/channel-CN5VmjNa.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-BM3DXZMG.js → chunk-4BX2VUAB-DeAfVeDa.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-DUIbFJ3r.js → chunk-55IACEB6-cKEeGDNI.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-B_cxCRkA.js → chunk-FMBD7UC4-7RuZ6HEq.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-2mLf1WoM.js → chunk-JSJVCQXG-B5dwG0bv.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-BMz3ND2R.js → chunk-KX2RTZJC-DeyQYDVj.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DiCxpw3L.js → chunk-NQ4KR5QH-NYo4hSqA.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-BcaWPzd5.js → chunk-QZHKN3VN-CtJFICF8.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-BbsfhS6J.js → chunk-WL4C6EOR-DZ3WYdab.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-CqLb9GuU.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CqLb9GuU.js +1 -0
  18. package/dashboard/dist/assets/clone-DwvrjCQs.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-iuQzyyrA.js → cose-bilkent-S5V4N54A-DaYOsf-M.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-C0TB1zg5.js → dagre-KLK3FWXG-DzLc7ykF.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-Ck8tvxP2.js → diagram-E7M64L7V-kpjEdOqb.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-rQKIi-sc.js → diagram-IFDJBPK2-D3EHoh6j.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-DS1RGSgx.js → diagram-P4PSJMXO-Cp6Un2ys.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BqWCybWW.js → erDiagram-INFDFZHY-DDjOUPWk.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BCoQtyQ5.js → flowDiagram-PKNHOUZH-CfO51xge.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CD1BmicP.js → ganttDiagram-A5KZAMGK-BDzAtkcp.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-tGDYOZuO.js → gitGraphDiagram-K3NZZRJ6-JPMFN2zF.js} +1 -1
  28. package/dashboard/dist/assets/{graph-BT0s-094.js → graph-DXDryilu.js} +1 -1
  29. package/dashboard/dist/assets/index-D7UtkCYM.js +515 -0
  30. package/dashboard/dist/assets/{index-D2Yoi6DJ.css → index-DTXSWSzH.css} +1 -1
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-E4dVb8BD.js → infoDiagram-LFFYTUFH-CYi5kkvz.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CubuM_ev.js → ishikawaDiagram-PHBUUO56-DQl_IUe8.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DfyJbgMF.js → journeyDiagram-4ABVD52K-BXXGPcrS.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-BXamajyZ.js → kanban-definition-K7BYSVSG-BqJestUY.js} +1 -1
  35. package/dashboard/dist/assets/{layout-6b_SlWD_.js → layout-D5VdYmWn.js} +1 -1
  36. package/dashboard/dist/assets/{linear-BE0O-66A.js → linear-dZA_O_GN.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-DM3qWr07.js → mermaid.core-B0Ixd1yP.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-puOtERrG.js → mindmap-definition-YRQLILUH-CSJYdSMG.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-cE6l8UBX.js → pieDiagram-SKSYHLDU-DmYrRZHN.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-CgDAyZQP.js → quadrantDiagram-337W2JSQ-La3ce5kE.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CJrai_8U.js → requirementDiagram-Z7DCOOCP-DPitIZQl.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-Bf72l7UW.js → sankeyDiagram-WA2Y5GQK-CAmCqGkr.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-0D8RN7ds.js → sequenceDiagram-2WXFIKYE-6lEzNTWG.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-B8LucUIt.js → stateDiagram-RAJIS63D-DyGKCS2C.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BOmRICoY.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DL3Hq8ii.js → timeline-definition-YZTLITO2-D9RxQE3j.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-wF1X4_uI.js → treemap-KZPCXAKY-TuXbGNN4.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-DDAjoGTZ.js → vennDiagram-LZ73GAT5-Bly5knr-.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-T-YmzRW8.js → xychartDiagram-JWTSCODW-CA-5z7-4.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +820 -166
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +1244 -774
  54. package/dist/index.js.map +1 -1
  55. package/dist/launch/index.d.ts +20 -2
  56. package/dist/launch/index.js +67 -20
  57. package/dist/launch/index.js.map +1 -1
  58. package/package.json +1 -1
  59. package/scripts/install-macos-url-handler.mjs +40 -7
  60. package/dashboard/dist/assets/channel-Cm_FzKJq.js +0 -1
  61. package/dashboard/dist/assets/classDiagram-VBA2DB6C-Bdan2Dp5.js +0 -1
  62. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Bdan2Dp5.js +0 -1
  63. package/dashboard/dist/assets/clone-9PoPEM_s.js +0 -1
  64. package/dashboard/dist/assets/index-BdSkANCC.js +0 -510
  65. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-D0dc7wxQ.js +0 -1
@@ -294,6 +294,62 @@ function updateAssignmentFile(fileContent, updates) {
294
294
  }
295
295
  return result;
296
296
  }
297
+ function findWorkspaceBlock(fmBlock) {
298
+ const headerMatch = fmBlock.match(/^workspace:\s*$/m);
299
+ if (!headerMatch) return null;
300
+ const headerStart = fmBlock.indexOf(headerMatch[0]);
301
+ const bodyStart = headerStart + headerMatch[0].length + 1;
302
+ const after = fmBlock.slice(bodyStart);
303
+ const lines = after.split("\n");
304
+ let consumed = 0;
305
+ for (let i = 0; i < lines.length; i++) {
306
+ const line = lines[i];
307
+ if (line.length === 0) {
308
+ consumed += line.length + 1;
309
+ continue;
310
+ }
311
+ if (line[0] !== " ") break;
312
+ consumed += line.length + 1;
313
+ }
314
+ const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);
315
+ return { headerStart, bodyStart, bodyEnd };
316
+ }
317
+ function updateAssignmentWorkspace(fileContent, partial) {
318
+ const fmMatch = fileContent.match(/^(---\n)([\s\S]*?)(\n---)/);
319
+ if (!fmMatch) {
320
+ throw new Error("No frontmatter found in assignment file. Expected --- delimiters.");
321
+ }
322
+ const fmBlock = fmMatch[2];
323
+ const fields = ["repository", "worktreePath", "branch", "parentBranch"];
324
+ const block = findWorkspaceBlock(fmBlock);
325
+ let newFm = fmBlock;
326
+ if (block) {
327
+ let body = fmBlock.slice(block.bodyStart, block.bodyEnd);
328
+ for (const field of fields) {
329
+ if (!(field in partial)) continue;
330
+ const value = partial[field] ?? null;
331
+ const formatted = formatYamlValue(value);
332
+ const lineRegex = new RegExp(`^(\\s+${field}:)\\s*.*$`, "m");
333
+ if (lineRegex.test(body)) {
334
+ body = body.replace(lineRegex, `$1 ${formatted}`);
335
+ } else {
336
+ const trimmed = body.replace(/\n+$/, "");
337
+ body = `${trimmed}${trimmed.length > 0 ? "\n" : ""} ${field}: ${formatted}
338
+ `;
339
+ }
340
+ }
341
+ newFm = fmBlock.slice(0, block.bodyStart) + body + fmBlock.slice(block.bodyEnd);
342
+ } else {
343
+ const lines = ["workspace:"];
344
+ for (const field of fields) {
345
+ const value = field in partial ? partial[field] ?? null : null;
346
+ lines.push(` ${field}: ${formatYamlValue(value)}`);
347
+ }
348
+ newFm = `${fmBlock.replace(/\n+$/, "")}
349
+ ${lines.join("\n")}`;
350
+ }
351
+ return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;
352
+ }
297
353
  var init_frontmatter = __esm({
298
354
  "src/lifecycle/frontmatter.ts"() {
299
355
  "use strict";
@@ -407,6 +463,12 @@ function parseListField(frontmatter, fieldName) {
407
463
  }
408
464
  return results;
409
465
  }
466
+ function unquoteYamlString(value) {
467
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
468
+ return value.slice(1, -1);
469
+ }
470
+ return value;
471
+ }
410
472
  function parseProject(fileContent) {
411
473
  const [fm, body] = extractFrontmatter2(fileContent);
412
474
  const slug = getField(fm, "slug") ?? getField(fm, "mission") ?? "";
@@ -422,6 +484,7 @@ function parseProject(fileContent) {
422
484
  updated: getField(fm, "updated") ?? "",
423
485
  tags: parseListField(fm, "tags"),
424
486
  workspace: getField(fm, "workspace"),
487
+ repositories: parseListField(fm, "repositories").map(unquoteYamlString),
425
488
  externalIds: parseExternalIds2(fm),
426
489
  body
427
490
  };
@@ -1319,7 +1382,6 @@ backup:
1319
1382
  categories: projects, playbooks, todos, servers, config
1320
1383
  lastBackup: null
1321
1384
  lastRestore: null
1322
- terminal: terminal-app
1323
1385
  ---
1324
1386
 
1325
1387
  # Syntaur Configuration
@@ -1588,6 +1650,22 @@ var init_agents_schema = __esm({
1588
1650
  }
1589
1651
  });
1590
1652
 
1653
+ // src/utils/terminal-schema.ts
1654
+ var TERMINAL_CHOICES;
1655
+ var init_terminal_schema = __esm({
1656
+ "src/utils/terminal-schema.ts"() {
1657
+ "use strict";
1658
+ TERMINAL_CHOICES = [
1659
+ "terminal-app",
1660
+ "iterm",
1661
+ "ghostty",
1662
+ "alacritty",
1663
+ "warp",
1664
+ "kitty"
1665
+ ];
1666
+ }
1667
+ });
1668
+
1591
1669
  // src/utils/config.ts
1592
1670
  var config_exports = {};
1593
1671
  __export(config_exports, {
@@ -1601,6 +1679,7 @@ __export(config_exports, {
1601
1679
  deleteAgentsConfig: () => deleteAgentsConfig,
1602
1680
  deleteHotkeyBindingsConfig: () => deleteHotkeyBindingsConfig,
1603
1681
  deleteStatusConfig: () => deleteStatusConfig,
1682
+ deleteTerminalConfig: () => deleteTerminalConfig,
1604
1683
  deleteThemeConfig: () => deleteThemeConfig,
1605
1684
  getAgents: () => getAgents,
1606
1685
  getAssignmentTypes: () => getAssignmentTypes,
@@ -1617,9 +1696,11 @@ __export(config_exports, {
1617
1696
  writeAgentsConfig: () => writeAgentsConfig,
1618
1697
  writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
1619
1698
  writeStatusConfig: () => writeStatusConfig,
1699
+ writeTerminalConfig: () => writeTerminalConfig,
1620
1700
  writeThemeConfig: () => writeThemeConfig
1621
1701
  });
1622
1702
  import { readFile as readFile4 } from "fs/promises";
1703
+ import { spawnSync } from "child_process";
1623
1704
  import { resolve as resolve6, isAbsolute } from "path";
1624
1705
  function parseAgentCommand(value, agentId) {
1625
1706
  if (typeof value !== "string" || value.trim() === "") {
@@ -2032,6 +2113,52 @@ ${cleanedFm}
2032
2113
  ---${afterFrontmatter}`;
2033
2114
  await writeFileForce(configPath, newContent);
2034
2115
  }
2116
+ function stripTopLevelScalar(fmBlock, key) {
2117
+ const lines = fmBlock.split("\n");
2118
+ const keyRegex = new RegExp(`^${key}:\\s*\\S`);
2119
+ const filtered = lines.filter((line) => !keyRegex.test(line));
2120
+ return filtered.join("\n").replace(/\n+$/, "");
2121
+ }
2122
+ async function writeTerminalConfig(terminal) {
2123
+ const configPath = resolve6(syntaurRoot(), "config.md");
2124
+ const terminalLine = `terminal: ${terminal}`;
2125
+ const existing = await fileExists(configPath) ? await readFile4(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2126
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2127
+ if (!fmMatch) {
2128
+ const content = `---
2129
+ version: "2.0"
2130
+ defaultProjectDir: ${defaultProjectDir()}
2131
+ ${terminalLine}
2132
+ ---
2133
+ ${existing}`;
2134
+ await writeFileForce(configPath, content);
2135
+ return;
2136
+ }
2137
+ const fmBlock = fmMatch[2];
2138
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2139
+ const cleanedFm = stripTopLevelScalar(fmBlock, "terminal");
2140
+ const newFm = `${cleanedFm}
2141
+ ${terminalLine}`.replace(/^\n+/, "");
2142
+ const normalizedFm = newFm.replace(/\n+$/, "");
2143
+ const newContent = `---
2144
+ ${normalizedFm}
2145
+ ---${afterFrontmatter}`;
2146
+ await writeFileForce(configPath, newContent);
2147
+ }
2148
+ async function deleteTerminalConfig() {
2149
+ const configPath = resolve6(syntaurRoot(), "config.md");
2150
+ if (!await fileExists(configPath)) return;
2151
+ const existing = await readFile4(configPath, "utf-8");
2152
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2153
+ if (!fmMatch) return;
2154
+ const fmBlock = fmMatch[2];
2155
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2156
+ const cleanedFm = stripTopLevelScalar(fmBlock, "terminal");
2157
+ const newContent = `---
2158
+ ${cleanedFm}
2159
+ ---${afterFrontmatter}`;
2160
+ await writeFileForce(configPath, newContent);
2161
+ }
2035
2162
  function parseHotkeyBindingsConfig(content) {
2036
2163
  const match = content.match(/^---\n([\s\S]*?)\n---/);
2037
2164
  if (!match) return null;
@@ -2727,7 +2854,18 @@ function parseTerminalConfig(value) {
2727
2854
  return trimmed;
2728
2855
  }
2729
2856
  function getTerminal(config) {
2730
- return config.terminal ?? "terminal-app";
2857
+ if (config.terminal) return config.terminal;
2858
+ if (process.platform === "darwin") return "terminal-app";
2859
+ if (process.platform === "linux") {
2860
+ const order = ["kitty", "alacritty", "warp"];
2861
+ for (const candidate of order) {
2862
+ const result = spawnSync("which", [candidate], { encoding: "utf-8" });
2863
+ if (result.status === 0 && result.stdout.trim().length > 0) {
2864
+ return candidate;
2865
+ }
2866
+ }
2867
+ }
2868
+ return "terminal-app";
2731
2869
  }
2732
2870
  async function updateAgentsConfig(mutation, options = {}) {
2733
2871
  const config = await readConfig();
@@ -2740,7 +2878,7 @@ async function updateAgentsConfig(mutation, options = {}) {
2740
2878
  await writeAgentsConfig(next);
2741
2879
  return { previous, next, written: true };
2742
2880
  }
2743
- var DEFAULT_ASSIGNMENT_TYPES, TERMINAL_CHOICES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
2881
+ var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
2744
2882
  var init_config2 = __esm({
2745
2883
  "src/utils/config.ts"() {
2746
2884
  "use strict";
@@ -2750,6 +2888,7 @@ var init_config2 = __esm({
2750
2888
  init_fs_migration();
2751
2889
  init_hotkeysCatalog();
2752
2890
  init_agents_schema();
2891
+ init_terminal_schema();
2753
2892
  DEFAULT_ASSIGNMENT_TYPES = {
2754
2893
  definitions: [
2755
2894
  { id: "feature", label: "Feature" },
@@ -2760,14 +2899,6 @@ var init_config2 = __esm({
2760
2899
  ],
2761
2900
  default: "feature"
2762
2901
  };
2763
- TERMINAL_CHOICES = [
2764
- "terminal-app",
2765
- "iterm",
2766
- "ghostty",
2767
- "alacritty",
2768
- "warp",
2769
- "kitty"
2770
- ];
2771
2902
  DEFAULT_CONFIG = {
2772
2903
  version: "2.0",
2773
2904
  defaultProjectDir: defaultProjectDir(),
@@ -3705,8 +3836,8 @@ async function migrateFromMarkdown(projectsDir) {
3705
3836
  return allSessions.length;
3706
3837
  }
3707
3838
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
3708
- const { readFile: readFile16 } = await import("fs/promises");
3709
- const raw = await readFile16(filePath, "utf-8");
3839
+ const { readFile: readFile18 } = await import("fs/promises");
3840
+ const raw = await readFile18(filePath, "utf-8");
3710
3841
  const sessions = [];
3711
3842
  const lines = raw.split("\n");
3712
3843
  let inTable = false;
@@ -4895,7 +5026,8 @@ async function getProjectDetail(projectsDir, slug) {
4895
5026
  resources,
4896
5027
  memories,
4897
5028
  dependencyGraph,
4898
- workspace: project.workspace
5029
+ workspace: project.workspace,
5030
+ repositories: project.repositories
4899
5031
  };
4900
5032
  }
4901
5033
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
@@ -6175,7 +6307,7 @@ init_assignment_resolver();
6175
6307
  init_agent_sessions();
6176
6308
  import express from "express";
6177
6309
  import { createServer } from "http";
6178
- import { resolve as resolve22 } from "path";
6310
+ import { resolve as resolve24 } from "path";
6179
6311
  import { writeFile as writeFile5, unlink as unlink5 } from "fs/promises";
6180
6312
  import { WebSocketServer, WebSocket } from "ws";
6181
6313
 
@@ -6708,8 +6840,9 @@ function withTwoLocks(keyA, keyB, fn) {
6708
6840
  init_lifecycle();
6709
6841
  init_slug();
6710
6842
  import { Router } from "express";
6711
- import { resolve as resolve15, basename as basename3 } from "path";
6712
- import { rm, readFile as readFile12, open as fsOpen } from "fs/promises";
6843
+ import { resolve as resolve17, basename as basename3, isAbsolute as isAbsolute2 } from "path";
6844
+ import { rm, readFile as readFile14, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
6845
+ import { spawnSync as spawnSync3 } from "child_process";
6713
6846
 
6714
6847
  // src/utils/uuid.ts
6715
6848
  import { randomUUID } from "crypto";
@@ -6720,6 +6853,203 @@ function generateId() {
6720
6853
  // src/dashboard/api-write.ts
6721
6854
  init_timestamp();
6722
6855
  init_fs();
6856
+
6857
+ // src/utils/git-worktree.ts
6858
+ init_frontmatter();
6859
+ init_fs();
6860
+ import { spawn } from "child_process";
6861
+ import { readFile as readFile12 } from "fs/promises";
6862
+ function run(command, args, cwd) {
6863
+ return new Promise((resolvePromise) => {
6864
+ const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
6865
+ let stdout = "";
6866
+ let stderr = "";
6867
+ child.stdout.on("data", (chunk) => stdout += chunk.toString());
6868
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
6869
+ child.on("error", (err) => {
6870
+ resolvePromise({ code: -1, stdout, stderr: stderr + String(err) });
6871
+ });
6872
+ child.on("close", (code) => {
6873
+ resolvePromise({ code: code ?? -1, stdout, stderr });
6874
+ });
6875
+ });
6876
+ }
6877
+ var GitWorktreeError = class extends Error {
6878
+ constructor(message, stderr) {
6879
+ super(message);
6880
+ this.stderr = stderr;
6881
+ }
6882
+ };
6883
+ async function createWorktree(opts) {
6884
+ const { repository, branch, worktreePath, parentBranch } = opts;
6885
+ const result = await run(
6886
+ "git",
6887
+ ["-C", repository, "worktree", "add", "-b", branch, worktreePath, parentBranch]
6888
+ );
6889
+ if (result.code !== 0) {
6890
+ throw new GitWorktreeError(
6891
+ `git worktree add failed (exit ${result.code}): ${result.stderr.trim() || "(no stderr)"}`,
6892
+ result.stderr
6893
+ );
6894
+ }
6895
+ }
6896
+ async function removeWorktree(repository, worktreePath) {
6897
+ const result = await run(
6898
+ "git",
6899
+ ["-C", repository, "worktree", "remove", "--force", worktreePath]
6900
+ );
6901
+ return { ok: result.code === 0, stderr: result.stderr };
6902
+ }
6903
+ async function deleteBranch(repository, branch) {
6904
+ const result = await run("git", ["-C", repository, "branch", "-D", branch]);
6905
+ return { ok: result.code === 0, stderr: result.stderr };
6906
+ }
6907
+ async function createWorktreeAndRecord(opts) {
6908
+ const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
6909
+ await createWorktree({ repository, branch, worktreePath, parentBranch });
6910
+ try {
6911
+ const content = await readFile12(assignmentPath, "utf-8");
6912
+ const updated = updateAssignmentWorkspace(content, {
6913
+ repository,
6914
+ worktreePath,
6915
+ branch,
6916
+ parentBranch
6917
+ });
6918
+ await writeFileForce(assignmentPath, updated);
6919
+ } catch (writeErr) {
6920
+ const cleanup = await removeWorktree(repository, worktreePath);
6921
+ const branchCleanup = await deleteBranch(repository, branch);
6922
+ const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);
6923
+ throw new Error(
6924
+ formatRollbackError({
6925
+ writeMsg,
6926
+ worktreePath,
6927
+ branch,
6928
+ worktreeCleanup: cleanup,
6929
+ branchCleanup
6930
+ })
6931
+ );
6932
+ }
6933
+ }
6934
+ function formatRollbackError(opts) {
6935
+ const { writeMsg, worktreePath, branch, worktreeCleanup, branchCleanup } = opts;
6936
+ const wtMsg = worktreeCleanup.stderr.trim() || "(no stderr)";
6937
+ const brMsg = branchCleanup.stderr.trim() || "(no stderr)";
6938
+ if (!worktreeCleanup.ok && !branchCleanup.ok) {
6939
+ return `Failed to update assignment frontmatter AND failed to clean up both worktree and branch. Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Branch cleanup error: ${brMsg}. Orphan worktree at ${worktreePath} and orphan branch "${branch}" \u2014 remove them manually.`;
6940
+ }
6941
+ if (!worktreeCleanup.ok) {
6942
+ return `Failed to update assignment frontmatter AND failed to clean up worktree. Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Orphan worktree at ${worktreePath} \u2014 remove it manually.`;
6943
+ }
6944
+ if (!branchCleanup.ok) {
6945
+ return `Failed to update assignment frontmatter: ${writeMsg}. Rolled back git worktree at ${worktreePath}, but could not delete branch "${branch}": ${brMsg}. Remove the branch manually.`;
6946
+ }
6947
+ return `Failed to update assignment frontmatter: ${writeMsg}. Rolled back git worktree at ${worktreePath} and branch "${branch}".`;
6948
+ }
6949
+
6950
+ // src/utils/worktree-defaults.ts
6951
+ init_paths();
6952
+ import { spawnSync as spawnSync2 } from "child_process";
6953
+ import { resolve as resolve15 } from "path";
6954
+ function computeWorktreeDefaults(opts) {
6955
+ const repository = opts.existing.repository ?? detectCurrentGitRoot(opts.cwd);
6956
+ const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
6957
+ const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch(opts.cwd) ?? "main";
6958
+ const worktreeBase = repository ? resolve15(repository, ".worktrees", branch) : resolve15(
6959
+ syntaurRoot(),
6960
+ "worktrees",
6961
+ opts.projectSlug || "standalone",
6962
+ opts.assignmentSlug
6963
+ );
6964
+ return {
6965
+ ...repository ? { repository } : {},
6966
+ branch,
6967
+ parentBranch,
6968
+ worktreePath: worktreeBase
6969
+ };
6970
+ }
6971
+ function detectCurrentGitRoot(cwd) {
6972
+ const result = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
6973
+ cwd,
6974
+ encoding: "utf-8"
6975
+ });
6976
+ if (result.status !== 0) return void 0;
6977
+ const out = result.stdout.trim();
6978
+ return out.length > 0 ? out : void 0;
6979
+ }
6980
+ function detectCurrentBranch(cwd) {
6981
+ const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
6982
+ cwd,
6983
+ encoding: "utf-8"
6984
+ });
6985
+ if (result.status !== 0) return void 0;
6986
+ const out = result.stdout.trim();
6987
+ if (!out || out === "HEAD") return void 0;
6988
+ return out;
6989
+ }
6990
+
6991
+ // src/dashboard/repository-candidates.ts
6992
+ init_fs();
6993
+ init_parser();
6994
+ import { readdir as readdir9, readFile as readFile13 } from "fs/promises";
6995
+ import { resolve as resolve16 } from "path";
6996
+ async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
6997
+ const seen = /* @__PURE__ */ new Set();
6998
+ const out = [];
6999
+ const projectPath = resolve16(projectsDir, projectSlug, "project.md");
7000
+ if (await fileExists(projectPath)) {
7001
+ const project = parseProject(await readFile13(projectPath, "utf-8"));
7002
+ for (const raw of project.repositories) {
7003
+ const path = raw.trim();
7004
+ if (!path) continue;
7005
+ const abs = resolve16(path);
7006
+ if (seen.has(abs)) continue;
7007
+ seen.add(abs);
7008
+ out.push({ path: abs, source: "project", sourceAssignmentSlug: null });
7009
+ }
7010
+ }
7011
+ const assignmentsDir2 = resolve16(projectsDir, projectSlug, "assignments");
7012
+ if (await fileExists(assignmentsDir2)) {
7013
+ const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
7014
+ for (const entry of entries) {
7015
+ if (!entry.isDirectory()) continue;
7016
+ const assignmentMd = resolve16(assignmentsDir2, entry.name, "assignment.md");
7017
+ if (!await fileExists(assignmentMd)) continue;
7018
+ const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
7019
+ const repo = parsed.workspace.repository?.trim();
7020
+ if (!repo) continue;
7021
+ const abs = resolve16(repo);
7022
+ if (seen.has(abs)) continue;
7023
+ seen.add(abs);
7024
+ out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
7025
+ }
7026
+ }
7027
+ return out;
7028
+ }
7029
+ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignmentId) {
7030
+ if (!await fileExists(assignmentsDir2)) {
7031
+ return [];
7032
+ }
7033
+ const seen = /* @__PURE__ */ new Set();
7034
+ const out = [];
7035
+ const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
7036
+ for (const entry of entries) {
7037
+ if (!entry.isDirectory()) continue;
7038
+ if (entry.name === excludeAssignmentId) continue;
7039
+ const assignmentMd = resolve16(assignmentsDir2, entry.name, "assignment.md");
7040
+ if (!await fileExists(assignmentMd)) continue;
7041
+ const parsed = parseAssignmentFull(await readFile13(assignmentMd, "utf-8"));
7042
+ const repo = parsed.workspace.repository?.trim();
7043
+ if (!repo) continue;
7044
+ const abs = resolve16(repo);
7045
+ if (seen.has(abs)) continue;
7046
+ seen.add(abs);
7047
+ out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
7048
+ }
7049
+ return out;
7050
+ }
7051
+
7052
+ // src/dashboard/api-write.ts
6723
7053
  init_parser();
6724
7054
 
6725
7055
  // src/dashboard/acceptance-criteria.ts
@@ -6809,10 +7139,17 @@ function escapeYamlString(value) {
6809
7139
  }
6810
7140
 
6811
7141
  // src/templates/project.ts
7142
+ function renderRepositoriesBlock(repos) {
7143
+ if (!repos || repos.length === 0) {
7144
+ return "repositories: []";
7145
+ }
7146
+ return ["repositories:", ...repos.map((p) => ` - ${escapeYamlString(p)}`)].join("\n");
7147
+ }
6812
7148
  function renderProject(params2) {
6813
7149
  const safeTitle = escapeYamlString(params2.title);
6814
7150
  const workspaceLine = params2.workspace ? `
6815
7151
  workspace: ${params2.workspace}` : "";
7152
+ const repositoriesBlock = renderRepositoriesBlock(params2.repositories);
6816
7153
  return `---
6817
7154
  id: ${params2.id}
6818
7155
  slug: ${params2.slug}
@@ -6823,7 +7160,8 @@ archivedReason: null
6823
7160
  created: "${params2.timestamp}"
6824
7161
  updated: "${params2.timestamp}"
6825
7162
  externalIds: []
6826
- tags: []${workspaceLine}
7163
+ tags: []
7164
+ ${repositoriesBlock}${workspaceLine}
6827
7165
  ---
6828
7166
 
6829
7167
  # ${params2.title}
@@ -7266,7 +7604,101 @@ async function readCurrentDocument(filePath) {
7266
7604
  if (!await fileExists(filePath)) {
7267
7605
  return null;
7268
7606
  }
7269
- return readFile12(filePath, "utf-8");
7607
+ return readFile14(filePath, "utf-8");
7608
+ }
7609
+ async function handleWorktreeCreate(req, res, ctx) {
7610
+ if (!await fileExists(ctx.assignmentPath)) {
7611
+ res.status(404).json({ error: "Assignment not found" });
7612
+ return;
7613
+ }
7614
+ const parsed = parseAssignmentFull(await readFile14(ctx.assignmentPath, "utf-8"));
7615
+ if (parsed.workspace.worktreePath) {
7616
+ res.status(409).json({ error: "Worktree already configured for this assignment" });
7617
+ return;
7618
+ }
7619
+ const { repository, branch: bodyBranch, parentBranch: bodyParent } = req.body ?? {};
7620
+ if (typeof repository !== "string" || !repository.trim()) {
7621
+ res.status(400).json({ error: "`repository` is required." });
7622
+ return;
7623
+ }
7624
+ if (!isAbsolute2(repository)) {
7625
+ res.status(400).json({ error: "`repository` must be an absolute path." });
7626
+ return;
7627
+ }
7628
+ try {
7629
+ const st = await fsStat(repository);
7630
+ if (!st.isDirectory()) {
7631
+ res.status(400).json({ error: `Repository path is not a directory: ${repository}` });
7632
+ return;
7633
+ }
7634
+ } catch {
7635
+ res.status(400).json({ error: `Repository path does not exist: ${repository}` });
7636
+ return;
7637
+ }
7638
+ const topLevel = spawnSync3("git", ["-C", repository, "rev-parse", "--show-toplevel"], {
7639
+ encoding: "utf-8"
7640
+ });
7641
+ const topLevelOut = topLevel.stdout.trim();
7642
+ if (topLevel.status !== 0 || !topLevelOut) {
7643
+ res.status(400).json({ error: `Repository path is not a git working tree: ${repository}` });
7644
+ return;
7645
+ }
7646
+ const [requestReal, topLevelReal] = await Promise.all([
7647
+ fsRealpath(repository),
7648
+ fsRealpath(topLevelOut)
7649
+ ]);
7650
+ if (requestReal !== topLevelReal) {
7651
+ res.status(400).json({
7652
+ error: `Repository path must be the git working-tree root. Got ${repository}; the enclosing repo root is ${topLevelOut}.`
7653
+ });
7654
+ return;
7655
+ }
7656
+ const defaults = computeWorktreeDefaults({
7657
+ projectSlug: ctx.projectSlug,
7658
+ assignmentSlug: ctx.assignmentSlug,
7659
+ existing: parsed.workspace,
7660
+ cwd: repository
7661
+ });
7662
+ const branch = typeof bodyBranch === "string" && bodyBranch.trim() ? bodyBranch.trim() : defaults.branch;
7663
+ const parentBranch = typeof bodyParent === "string" && bodyParent.trim() ? bodyParent.trim() : defaults.parentBranch;
7664
+ const worktreePath = resolve17(repository, ".worktrees", branch);
7665
+ try {
7666
+ await fsStat(worktreePath);
7667
+ res.status(409).json({
7668
+ error: `A file or directory already exists at ${worktreePath}. Remove it or choose a different branch.`
7669
+ });
7670
+ return;
7671
+ } catch {
7672
+ }
7673
+ const parentCheck = spawnSync3(
7674
+ "git",
7675
+ ["-C", repository, "rev-parse", "--verify", "--quiet", parentBranch],
7676
+ { encoding: "utf-8" }
7677
+ );
7678
+ if (parentCheck.status !== 0) {
7679
+ res.status(400).json({
7680
+ error: `Parent branch "${parentBranch}" does not exist in ${repository}.`
7681
+ });
7682
+ return;
7683
+ }
7684
+ try {
7685
+ await createWorktreeAndRecord({
7686
+ assignmentPath: ctx.assignmentPath,
7687
+ repository,
7688
+ branch,
7689
+ worktreePath,
7690
+ parentBranch
7691
+ });
7692
+ } catch (error) {
7693
+ if (error instanceof GitWorktreeError) {
7694
+ res.status(400).json({ error: error.message, stderr: error.stderr });
7695
+ return;
7696
+ }
7697
+ res.status(500).json({ error: error.message });
7698
+ return;
7699
+ }
7700
+ const assignment = await ctx.reload();
7701
+ res.json({ assignment });
7270
7702
  }
7271
7703
  function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
7272
7704
  const linkedTodosLookup = todosDir2 ? { todosDir: todosDir2, projectsDir } : void 0;
@@ -7495,9 +7927,9 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
7495
7927
  });
7496
7928
  return;
7497
7929
  }
7498
- const folderPath = resolve15(projectDir, folder);
7930
+ const folderPath = resolve17(projectDir, folder);
7499
7931
  await ensureDir(folderPath);
7500
- const filePath = resolve15(folderPath, `${requestedSlug}.md`);
7932
+ const filePath = resolve17(folderPath, `${requestedSlug}.md`);
7501
7933
  const timestamp = nowTimestamp();
7502
7934
  let content = renderItemStub(kind, {
7503
7935
  slug: requestedSlug,
@@ -7541,14 +7973,14 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
7541
7973
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
7542
7974
  return;
7543
7975
  }
7544
- const filePath = resolve15(projectDir, folder, `${itemSlug}.md`);
7976
+ const filePath = resolve17(projectDir, folder, `${itemSlug}.md`);
7545
7977
  if (!await fileExists(filePath)) {
7546
7978
  res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
7547
7979
  return;
7548
7980
  }
7549
7981
  const nextContentRaw = requireContent(req, res);
7550
7982
  if (!nextContentRaw) return;
7551
- const currentContent = await readFile12(filePath, "utf-8");
7983
+ const currentContent = await readFile14(filePath, "utf-8");
7552
7984
  const frontmatterBlock = extractFrontmatterBlock(currentContent);
7553
7985
  if (!frontmatterBlock) {
7554
7986
  res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
@@ -7577,7 +8009,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7577
8009
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
7578
8010
  return;
7579
8011
  }
7580
- const filePath = resolve15(projectDir, folder, `${itemSlug}.md`);
8012
+ const filePath = resolve17(projectDir, folder, `${itemSlug}.md`);
7581
8013
  if (!await fileExists(filePath)) {
7582
8014
  res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
7583
8015
  return;
@@ -7611,26 +8043,26 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7611
8043
  res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
7612
8044
  return;
7613
8045
  }
7614
- const projectDir = resolve15(projectsDir, slug);
8046
+ const projectDir = resolve17(projectsDir, slug);
7615
8047
  if (await fileExists(projectDir)) {
7616
8048
  res.status(409).json({ error: `Project "${slug}" already exists` });
7617
8049
  return;
7618
8050
  }
7619
8051
  const title = fields.title;
7620
8052
  const timestamp = fields.created || nowTimestamp();
7621
- await ensureDir(resolve15(projectDir, "assignments"));
7622
- await ensureDir(resolve15(projectDir, "resources"));
7623
- await ensureDir(resolve15(projectDir, "memories"));
7624
- await writeFileForce(resolve15(projectDir, "project.md"), content);
8053
+ await ensureDir(resolve17(projectDir, "assignments"));
8054
+ await ensureDir(resolve17(projectDir, "resources"));
8055
+ await ensureDir(resolve17(projectDir, "memories"));
8056
+ await writeFileForce(resolve17(projectDir, "project.md"), content);
7625
8057
  try {
7626
8058
  const companions = [
7627
- [resolve15(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
7628
- [resolve15(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
7629
- [resolve15(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
7630
- [resolve15(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
7631
- [resolve15(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
7632
- [resolve15(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
7633
- [resolve15(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
8059
+ [resolve17(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
8060
+ [resolve17(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
8061
+ [resolve17(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
8062
+ [resolve17(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
8063
+ [resolve17(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
8064
+ [resolve17(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
8065
+ [resolve17(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
7634
8066
  ];
7635
8067
  for (const [filePath, fileContent] of companions) {
7636
8068
  await writeFileForce(filePath, fileContent);
@@ -7651,8 +8083,8 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7651
8083
  router.post("/api/projects/:slug/assignments", async (req, res) => {
7652
8084
  try {
7653
8085
  const projectSlug = getParam(req.params.slug);
7654
- const projectDir = resolve15(projectsDir, projectSlug);
7655
- const projectMdPath = resolve15(projectDir, "project.md");
8086
+ const projectDir = resolve17(projectsDir, projectSlug);
8087
+ const projectMdPath = resolve17(projectDir, "project.md");
7656
8088
  if (!await fileExists(projectMdPath)) {
7657
8089
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
7658
8090
  return;
@@ -7682,7 +8114,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7682
8114
  res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
7683
8115
  return;
7684
8116
  }
7685
- const assignmentDir = resolve15(projectDir, "assignments", assignmentSlug);
8117
+ const assignmentDir = resolve17(projectDir, "assignments", assignmentSlug);
7686
8118
  if (await fileExists(assignmentDir)) {
7687
8119
  res.status(409).json({
7688
8120
  error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
@@ -7691,12 +8123,12 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7691
8123
  }
7692
8124
  const timestamp = fields.created || nowTimestamp();
7693
8125
  await ensureDir(assignmentDir);
7694
- await writeFileForce(resolve15(assignmentDir, "assignment.md"), content);
8126
+ await writeFileForce(resolve17(assignmentDir, "assignment.md"), content);
7695
8127
  try {
7696
8128
  const companions = [
7697
- [resolve15(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
7698
- [resolve15(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
7699
- [resolve15(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
8129
+ [resolve17(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
8130
+ [resolve17(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
8131
+ [resolve17(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
7700
8132
  ];
7701
8133
  for (const [filePath, fileContent] of companions) {
7702
8134
  await writeFileForce(filePath, fileContent);
@@ -7717,7 +8149,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7717
8149
  router.patch("/api/projects/:slug", async (req, res) => {
7718
8150
  try {
7719
8151
  const projectSlug = getParam(req.params.slug);
7720
- const projectPath = resolve15(projectsDir, projectSlug, "project.md");
8152
+ const projectPath = resolve17(projectsDir, projectSlug, "project.md");
7721
8153
  const currentContent = await readCurrentDocument(projectPath);
7722
8154
  if (!currentContent) {
7723
8155
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
@@ -7750,7 +8182,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7750
8182
  try {
7751
8183
  const projectSlug = getParam(req.params.slug);
7752
8184
  const assignmentSlug = getParam(req.params.aslug);
7753
- const assignmentPath = resolve15(
8185
+ const assignmentPath = resolve17(
7754
8186
  projectsDir,
7755
8187
  projectSlug,
7756
8188
  "assignments",
@@ -7793,7 +8225,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7793
8225
  try {
7794
8226
  const projectSlug = getParam(req.params.slug);
7795
8227
  const assignmentSlug = getParam(req.params.aslug);
7796
- const assignmentPath = resolve15(
8228
+ const assignmentPath = resolve17(
7797
8229
  projectsDir,
7798
8230
  projectSlug,
7799
8231
  "assignments",
@@ -7829,7 +8261,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7829
8261
  try {
7830
8262
  const projectSlug = getParam(req.params.slug);
7831
8263
  const assignmentSlug = getParam(req.params.aslug);
7832
- const planPath = resolve15(
8264
+ const planPath = resolve17(
7833
8265
  projectsDir,
7834
8266
  projectSlug,
7835
8267
  "assignments",
@@ -7867,7 +8299,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7867
8299
  try {
7868
8300
  const projectSlug = getParam(req.params.slug);
7869
8301
  const assignmentSlug = getParam(req.params.aslug);
7870
- const scratchpadPath = resolve15(
8302
+ const scratchpadPath = resolve17(
7871
8303
  projectsDir,
7872
8304
  projectSlug,
7873
8305
  "assignments",
@@ -7905,7 +8337,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7905
8337
  try {
7906
8338
  const projectSlug = getParam(req.params.slug);
7907
8339
  const assignmentSlug = getParam(req.params.aslug);
7908
- const handoffPath = resolve15(
8340
+ const handoffPath = resolve17(
7909
8341
  projectsDir,
7910
8342
  projectSlug,
7911
8343
  "assignments",
@@ -7943,7 +8375,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7943
8375
  try {
7944
8376
  const projectSlug = getParam(req.params.slug);
7945
8377
  const assignmentSlug = getParam(req.params.aslug);
7946
- const decisionPath = resolve15(
8378
+ const decisionPath = resolve17(
7947
8379
  projectsDir,
7948
8380
  projectSlug,
7949
8381
  "assignments",
@@ -7981,7 +8413,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7981
8413
  try {
7982
8414
  const projectSlug = getParam(req.params.slug);
7983
8415
  const assignmentSlug = getParam(req.params.aslug);
7984
- const commentsPath = resolve15(
8416
+ const commentsPath = resolve17(
7985
8417
  projectsDir,
7986
8418
  projectSlug,
7987
8419
  "assignments",
@@ -7999,7 +8431,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
7999
8431
  let currentContent;
8000
8432
  let currentCount = 0;
8001
8433
  if (await fileExists(commentsPath)) {
8002
- currentContent = await readFile12(commentsPath, "utf-8");
8434
+ currentContent = await readFile14(commentsPath, "utf-8");
8003
8435
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
8004
8436
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
8005
8437
  } else {
@@ -8040,7 +8472,7 @@ ${entry}`;
8040
8472
  const projectSlug = getParam(req.params.slug);
8041
8473
  const assignmentSlug = getParam(req.params.aslug);
8042
8474
  const commentId = getParam(req.params.commentId);
8043
- const commentsPath = resolve15(
8475
+ const commentsPath = resolve17(
8044
8476
  projectsDir,
8045
8477
  projectSlug,
8046
8478
  "assignments",
@@ -8056,7 +8488,7 @@ ${entry}`;
8056
8488
  res.status(400).json({ error: "resolved (boolean) is required" });
8057
8489
  return;
8058
8490
  }
8059
- const content = await readFile12(commentsPath, "utf-8");
8491
+ const content = await readFile14(commentsPath, "utf-8");
8060
8492
  const parsed = parseComments(content);
8061
8493
  const target = parsed.entries.find((e) => e.id === commentId);
8062
8494
  if (!target) {
@@ -8091,7 +8523,7 @@ ${entry}`;
8091
8523
  router.post("/api/projects/:slug/move-workspace", async (req, res) => {
8092
8524
  try {
8093
8525
  const projectSlug = getParam(req.params.slug);
8094
- const projectPath = resolve15(projectsDir, projectSlug, "project.md");
8526
+ const projectPath = resolve17(projectsDir, projectSlug, "project.md");
8095
8527
  if (!await fileExists(projectPath)) {
8096
8528
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
8097
8529
  return;
@@ -8103,7 +8535,7 @@ ${entry}`;
8103
8535
  });
8104
8536
  return;
8105
8537
  }
8106
- let content = await readFile12(projectPath, "utf-8");
8538
+ let content = await readFile14(projectPath, "utf-8");
8107
8539
  content = setTopLevelField(content, "workspace", workspace ?? null);
8108
8540
  content = setTopLevelField(content, "updated", nowTimestamp());
8109
8541
  await writeFileForce(projectPath, content);
@@ -8139,8 +8571,8 @@ ${entry}`;
8139
8571
  });
8140
8572
  return;
8141
8573
  }
8142
- const assignmentPath = resolve15(resolved.assignmentDir, "assignment.md");
8143
- let content = await readFile12(assignmentPath, "utf-8");
8574
+ const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8575
+ let content = await readFile14(assignmentPath, "utf-8");
8144
8576
  content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
8145
8577
  content = setTopLevelField(content, "updated", nowTimestamp());
8146
8578
  await writeFileForce(assignmentPath, content);
@@ -8151,10 +8583,108 @@ ${entry}`;
8151
8583
  res.status(500).json({ error: `Failed to move workspace: ${error.message}` });
8152
8584
  }
8153
8585
  });
8586
+ router.get(
8587
+ "/api/projects/:slug/repository-candidates",
8588
+ async (req, res) => {
8589
+ try {
8590
+ const projectSlug = getParam(req.params.slug);
8591
+ const projectPath = resolve17(projectsDir, projectSlug, "project.md");
8592
+ if (!await fileExists(projectPath)) {
8593
+ res.status(404).json({ error: `Project "${projectSlug}" not found` });
8594
+ return;
8595
+ }
8596
+ const candidates = await getProjectRepositoryCandidates(projectsDir, projectSlug);
8597
+ res.json({ candidates });
8598
+ } catch (error) {
8599
+ console.error("Error listing repository candidates:", error);
8600
+ res.status(500).json({
8601
+ error: `Failed to list repository candidates: ${error.message}`
8602
+ });
8603
+ }
8604
+ }
8605
+ );
8606
+ router.get(
8607
+ "/api/assignments/:id/repository-candidates",
8608
+ async (req, res) => {
8609
+ try {
8610
+ if (!assignmentsDir2) {
8611
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
8612
+ return;
8613
+ }
8614
+ const id = getParam(req.params.id);
8615
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
8616
+ if (!resolved) {
8617
+ res.status(404).json({ error: `Assignment "${id}" not found` });
8618
+ return;
8619
+ }
8620
+ const candidates = resolved.standalone ? await getStandaloneRepositoryCandidates(assignmentsDir2, id) : await getProjectRepositoryCandidates(projectsDir, resolved.projectSlug);
8621
+ res.json({ candidates });
8622
+ } catch (error) {
8623
+ console.error("Error listing repository candidates:", error);
8624
+ res.status(500).json({
8625
+ error: `Failed to list repository candidates: ${error.message}`
8626
+ });
8627
+ }
8628
+ }
8629
+ );
8630
+ router.post(
8631
+ "/api/projects/:slug/assignments/:aslug/worktree",
8632
+ async (req, res) => {
8633
+ try {
8634
+ const projectSlug = getParam(req.params.slug);
8635
+ const assignmentSlug = getParam(req.params.aslug);
8636
+ const assignmentPath = resolve17(
8637
+ projectsDir,
8638
+ projectSlug,
8639
+ "assignments",
8640
+ assignmentSlug,
8641
+ "assignment.md"
8642
+ );
8643
+ await handleWorktreeCreate(req, res, {
8644
+ assignmentPath,
8645
+ projectSlug,
8646
+ assignmentSlug,
8647
+ reload: () => getAssignmentDetail(projectsDir, projectSlug, assignmentSlug)
8648
+ });
8649
+ } catch (error) {
8650
+ console.error("Error creating worktree:", error);
8651
+ res.status(500).json({ error: `Failed to create worktree: ${error.message}` });
8652
+ }
8653
+ }
8654
+ );
8655
+ router.post(
8656
+ "/api/assignments/:id/worktree",
8657
+ async (req, res) => {
8658
+ try {
8659
+ if (!assignmentsDir2) {
8660
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
8661
+ return;
8662
+ }
8663
+ const id = getParam(req.params.id);
8664
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
8665
+ if (!resolved) {
8666
+ res.status(404).json({ error: `Assignment "${id}" not found` });
8667
+ return;
8668
+ }
8669
+ const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8670
+ const parsedForSlug = parseAssignmentFull(await readFile14(assignmentPath, "utf-8"));
8671
+ const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
8672
+ await handleWorktreeCreate(req, res, {
8673
+ assignmentPath,
8674
+ projectSlug: resolved.projectSlug ?? "",
8675
+ assignmentSlug: assignmentSlugForBranch,
8676
+ reload: () => getAssignmentDetailById(projectsDir, assignmentsDir2, id)
8677
+ });
8678
+ } catch (error) {
8679
+ console.error("Error creating worktree:", error);
8680
+ res.status(500).json({ error: `Failed to create worktree: ${error.message}` });
8681
+ }
8682
+ }
8683
+ );
8154
8684
  router.post("/api/projects/:slug/status-override", async (req, res) => {
8155
8685
  try {
8156
8686
  const projectSlug = getParam(req.params.slug);
8157
- const projectPath = resolve15(projectsDir, projectSlug, "project.md");
8687
+ const projectPath = resolve17(projectsDir, projectSlug, "project.md");
8158
8688
  if (!await fileExists(projectPath)) {
8159
8689
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
8160
8690
  return;
@@ -8166,7 +8696,7 @@ ${entry}`;
8166
8696
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
8167
8697
  return;
8168
8698
  }
8169
- let content = await readFile12(projectPath, "utf-8");
8699
+ let content = await readFile14(projectPath, "utf-8");
8170
8700
  content = setTopLevelField(content, "statusOverride", status ?? null);
8171
8701
  content = setTopLevelField(content, "updated", nowTimestamp());
8172
8702
  await writeFileForce(projectPath, content);
@@ -8181,7 +8711,7 @@ ${entry}`;
8181
8711
  try {
8182
8712
  const projectSlug = getParam(req.params.slug);
8183
8713
  const assignmentSlug = getParam(req.params.aslug);
8184
- const assignmentPath = resolve15(
8714
+ const assignmentPath = resolve17(
8185
8715
  projectsDir,
8186
8716
  projectSlug,
8187
8717
  "assignments",
@@ -8199,7 +8729,7 @@ ${entry}`;
8199
8729
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
8200
8730
  return;
8201
8731
  }
8202
- let content = await readFile12(assignmentPath, "utf-8");
8732
+ let content = await readFile14(assignmentPath, "utf-8");
8203
8733
  content = setTopLevelField(content, "status", status);
8204
8734
  content = setTopLevelField(content, "updated", nowTimestamp());
8205
8735
  if (status !== "blocked") {
@@ -8217,7 +8747,7 @@ ${entry}`;
8217
8747
  try {
8218
8748
  const projectSlug = getParam(req.params.slug);
8219
8749
  const assignmentSlug = getParam(req.params.aslug);
8220
- const assignmentPath = resolve15(
8750
+ const assignmentPath = resolve17(
8221
8751
  projectsDir,
8222
8752
  projectSlug,
8223
8753
  "assignments",
@@ -8233,7 +8763,7 @@ ${entry}`;
8233
8763
  res.status(400).json({ error: validation.error });
8234
8764
  return;
8235
8765
  }
8236
- let content = await readFile12(assignmentPath, "utf-8");
8766
+ let content = await readFile14(assignmentPath, "utf-8");
8237
8767
  content = setTopLevelField(content, "assignee", validation.value);
8238
8768
  content = setTopLevelField(content, "updated", nowTimestamp());
8239
8769
  await writeFileForce(assignmentPath, content);
@@ -8255,8 +8785,8 @@ ${entry}`;
8255
8785
  res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
8256
8786
  return;
8257
8787
  }
8258
- const projectDir = resolve15(projectsDir, projectSlug);
8259
- const assignmentPath = resolve15(projectDir, "assignments", assignmentSlug, "assignment.md");
8788
+ const projectDir = resolve17(projectsDir, projectSlug);
8789
+ const assignmentPath = resolve17(projectDir, "assignments", assignmentSlug, "assignment.md");
8260
8790
  if (!await fileExists(assignmentPath)) {
8261
8791
  res.status(404).json({ error: "Assignment not found" });
8262
8792
  return;
@@ -8283,8 +8813,8 @@ ${entry}`;
8283
8813
  try {
8284
8814
  const projectSlug = getParam(req.params.slug);
8285
8815
  const assignmentSlug = getParam(req.params.aslug);
8286
- const assignmentDir = resolve15(projectsDir, projectSlug, "assignments", assignmentSlug);
8287
- const assignmentPath = resolve15(assignmentDir, "assignment.md");
8816
+ const assignmentDir = resolve17(projectsDir, projectSlug, "assignments", assignmentSlug);
8817
+ const assignmentPath = resolve17(assignmentDir, "assignment.md");
8288
8818
  if (!await fileExists(assignmentPath)) {
8289
8819
  res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
8290
8820
  return;
@@ -8339,7 +8869,7 @@ ${entry}`;
8339
8869
  return;
8340
8870
  }
8341
8871
  const id2 = generateId();
8342
- const assignmentDir2 = resolve15(assignmentsDir2, id2);
8872
+ const assignmentDir2 = resolve17(assignmentsDir2, id2);
8343
8873
  if (await fileExists(assignmentDir2)) {
8344
8874
  res.status(500).json({ error: "UUID collision \u2014 try again" });
8345
8875
  return;
@@ -8347,25 +8877,25 @@ ${entry}`;
8347
8877
  const timestamp2 = fields.created || nowTimestamp();
8348
8878
  await ensureDir(assignmentDir2);
8349
8879
  const normalizedContent = setTopLevelField(rawContent, "id", id2);
8350
- await writeFileForce(resolve15(assignmentDir2, "assignment.md"), normalizedContent);
8880
+ await writeFileForce(resolve17(assignmentDir2, "assignment.md"), normalizedContent);
8351
8881
  await writeFileForce(
8352
- resolve15(assignmentDir2, "scratchpad.md"),
8882
+ resolve17(assignmentDir2, "scratchpad.md"),
8353
8883
  renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
8354
8884
  );
8355
8885
  await writeFileForce(
8356
- resolve15(assignmentDir2, "handoff.md"),
8886
+ resolve17(assignmentDir2, "handoff.md"),
8357
8887
  renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
8358
8888
  );
8359
8889
  await writeFileForce(
8360
- resolve15(assignmentDir2, "decision-record.md"),
8890
+ resolve17(assignmentDir2, "decision-record.md"),
8361
8891
  renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
8362
8892
  );
8363
8893
  await writeFileForce(
8364
- resolve15(assignmentDir2, "progress.md"),
8894
+ resolve17(assignmentDir2, "progress.md"),
8365
8895
  renderProgress({ assignment: id2, timestamp: timestamp2 })
8366
8896
  );
8367
8897
  await writeFileForce(
8368
- resolve15(assignmentDir2, "comments.md"),
8898
+ resolve17(assignmentDir2, "comments.md"),
8369
8899
  renderComments({ assignment: id2, timestamp: timestamp2 })
8370
8900
  );
8371
8901
  const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir2, id2);
@@ -8383,7 +8913,7 @@ ${entry}`;
8383
8913
  return;
8384
8914
  }
8385
8915
  const id = generateId();
8386
- const assignmentDir = resolve15(assignmentsDir2, id);
8916
+ const assignmentDir = resolve17(assignmentsDir2, id);
8387
8917
  if (await fileExists(assignmentDir)) {
8388
8918
  res.status(500).json({ error: "UUID collision \u2014 try again" });
8389
8919
  return;
@@ -8403,25 +8933,25 @@ ${entry}`;
8403
8933
  project: null,
8404
8934
  type: typeof type === "string" ? type : void 0
8405
8935
  });
8406
- await writeFileForce(resolve15(assignmentDir, "assignment.md"), assignmentContent);
8936
+ await writeFileForce(resolve17(assignmentDir, "assignment.md"), assignmentContent);
8407
8937
  await writeFileForce(
8408
- resolve15(assignmentDir, "scratchpad.md"),
8938
+ resolve17(assignmentDir, "scratchpad.md"),
8409
8939
  renderScratchpad({ assignmentSlug: id, timestamp })
8410
8940
  );
8411
8941
  await writeFileForce(
8412
- resolve15(assignmentDir, "handoff.md"),
8942
+ resolve17(assignmentDir, "handoff.md"),
8413
8943
  renderHandoff({ assignmentSlug: id, timestamp })
8414
8944
  );
8415
8945
  await writeFileForce(
8416
- resolve15(assignmentDir, "decision-record.md"),
8946
+ resolve17(assignmentDir, "decision-record.md"),
8417
8947
  renderDecisionRecord({ assignmentSlug: id, timestamp })
8418
8948
  );
8419
8949
  await writeFileForce(
8420
- resolve15(assignmentDir, "progress.md"),
8950
+ resolve17(assignmentDir, "progress.md"),
8421
8951
  renderProgress({ assignment: id, timestamp })
8422
8952
  );
8423
8953
  await writeFileForce(
8424
- resolve15(assignmentDir, "comments.md"),
8954
+ resolve17(assignmentDir, "comments.md"),
8425
8955
  renderComments({ assignment: id, timestamp })
8426
8956
  );
8427
8957
  const detail = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
@@ -8549,7 +9079,7 @@ ${entry}`;
8549
9079
  res.status(404).json({ error: `Assignment "${id}" not found` });
8550
9080
  return;
8551
9081
  }
8552
- const assignmentPath = resolve15(resolved.assignmentDir, "assignment.md");
9082
+ const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8553
9083
  const currentContent = await readCurrentDocument(assignmentPath);
8554
9084
  if (!currentContent) {
8555
9085
  res.status(404).json({ error: "Assignment not found" });
@@ -8591,7 +9121,7 @@ ${entry}`;
8591
9121
  res.status(404).json({ error: `Assignment "${id}" not found` });
8592
9122
  return;
8593
9123
  }
8594
- const planPath = resolve15(resolved.assignmentDir, "plan.md");
9124
+ const planPath = resolve17(resolved.assignmentDir, "plan.md");
8595
9125
  const currentContent = await readCurrentDocument(planPath);
8596
9126
  if (!currentContent) {
8597
9127
  res.status(404).json({ error: "Plan not found" });
@@ -8625,7 +9155,7 @@ ${entry}`;
8625
9155
  res.status(404).json({ error: `Assignment "${id}" not found` });
8626
9156
  return;
8627
9157
  }
8628
- const scratchpadPath = resolve15(resolved.assignmentDir, "scratchpad.md");
9158
+ const scratchpadPath = resolve17(resolved.assignmentDir, "scratchpad.md");
8629
9159
  const currentContent = await readCurrentDocument(scratchpadPath);
8630
9160
  if (!currentContent) {
8631
9161
  res.status(404).json({ error: "Scratchpad not found" });
@@ -8659,7 +9189,7 @@ ${entry}`;
8659
9189
  res.status(404).json({ error: `Assignment "${id}" not found` });
8660
9190
  return;
8661
9191
  }
8662
- const handoffPath = resolve15(resolved.assignmentDir, "handoff.md");
9192
+ const handoffPath = resolve17(resolved.assignmentDir, "handoff.md");
8663
9193
  const currentContent = await readCurrentDocument(handoffPath);
8664
9194
  if (!currentContent) {
8665
9195
  res.status(404).json({ error: "Handoff log not found" });
@@ -8699,7 +9229,7 @@ ${entry}`;
8699
9229
  res.status(404).json({ error: `Assignment "${id}" not found` });
8700
9230
  return;
8701
9231
  }
8702
- const decisionPath = resolve15(resolved.assignmentDir, "decision-record.md");
9232
+ const decisionPath = resolve17(resolved.assignmentDir, "decision-record.md");
8703
9233
  const currentContent = await readCurrentDocument(decisionPath);
8704
9234
  if (!currentContent) {
8705
9235
  res.status(404).json({ error: "Decision record not found" });
@@ -8739,7 +9269,7 @@ ${entry}`;
8739
9269
  res.status(404).json({ error: `Assignment "${id}" not found` });
8740
9270
  return;
8741
9271
  }
8742
- const assignmentPath = resolve15(resolved.assignmentDir, "assignment.md");
9272
+ const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8743
9273
  if (!await fileExists(assignmentPath)) {
8744
9274
  res.status(404).json({ error: "Assignment not found" });
8745
9275
  return;
@@ -8751,7 +9281,7 @@ ${entry}`;
8751
9281
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
8752
9282
  return;
8753
9283
  }
8754
- let content = await readFile12(assignmentPath, "utf-8");
9284
+ let content = await readFile14(assignmentPath, "utf-8");
8755
9285
  content = setTopLevelField(content, "status", status);
8756
9286
  content = setTopLevelField(content, "updated", nowTimestamp());
8757
9287
  if (status !== "blocked") {
@@ -8777,7 +9307,7 @@ ${entry}`;
8777
9307
  res.status(404).json({ error: `Assignment "${id}" not found` });
8778
9308
  return;
8779
9309
  }
8780
- const assignmentPath = resolve15(resolved.assignmentDir, "assignment.md");
9310
+ const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8781
9311
  if (!await fileExists(assignmentPath)) {
8782
9312
  res.status(404).json({ error: "Assignment not found" });
8783
9313
  return;
@@ -8787,7 +9317,7 @@ ${entry}`;
8787
9317
  res.status(400).json({ error: validation.error });
8788
9318
  return;
8789
9319
  }
8790
- let content = await readFile12(assignmentPath, "utf-8");
9320
+ let content = await readFile14(assignmentPath, "utf-8");
8791
9321
  content = setTopLevelField(content, "assignee", validation.value);
8792
9322
  content = setTopLevelField(content, "updated", nowTimestamp());
8793
9323
  await writeFileForce(assignmentPath, content);
@@ -8841,9 +9371,9 @@ ${entry}`;
8841
9371
  if (!resolved) {
8842
9372
  throw new Error(`assignment "${item.id}" not found`);
8843
9373
  }
8844
- assignmentPath = resolve15(resolved.assignmentDir, "assignment.md");
9374
+ assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8845
9375
  } else if (typeof item.projectSlug === "string" && typeof item.assignmentSlug === "string" && item.projectSlug && item.assignmentSlug) {
8846
- assignmentPath = resolve15(
9376
+ assignmentPath = resolve17(
8847
9377
  projectsDir,
8848
9378
  item.projectSlug,
8849
9379
  "assignments",
@@ -8856,7 +9386,7 @@ ${entry}`;
8856
9386
  if (!await fileExists(assignmentPath)) {
8857
9387
  throw new Error("assignment file not found");
8858
9388
  }
8859
- let content = await readFile12(assignmentPath, "utf-8");
9389
+ let content = await readFile14(assignmentPath, "utf-8");
8860
9390
  content = setTopLevelField(content, "status", status);
8861
9391
  content = setTopLevelField(content, "updated", timestamp);
8862
9392
  if (status !== "blocked") {
@@ -8888,7 +9418,7 @@ ${entry}`;
8888
9418
  res.status(404).json({ error: `Assignment "${id}" not found` });
8889
9419
  return;
8890
9420
  }
8891
- const assignmentPath = resolve15(resolved.assignmentDir, "assignment.md");
9421
+ const assignmentPath = resolve17(resolved.assignmentDir, "assignment.md");
8892
9422
  const currentContent = await readCurrentDocument(assignmentPath);
8893
9423
  if (!currentContent) {
8894
9424
  res.status(404).json({ error: "Assignment not found" });
@@ -8980,7 +9510,7 @@ function buildBulkItemKey(raw, index) {
8980
9510
  return `#${index}`;
8981
9511
  }
8982
9512
  async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
8983
- const commentsPath = resolve15(assignmentDir, "comments.md");
9513
+ const commentsPath = resolve17(assignmentDir, "comments.md");
8984
9514
  const { body, author, type, replyTo } = req.body || {};
8985
9515
  if (!body || typeof body !== "string" || !body.trim()) {
8986
9516
  res.status(400).json({ error: "body is required" });
@@ -8992,7 +9522,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
8992
9522
  let currentContent;
8993
9523
  let currentCount = 0;
8994
9524
  if (await fileExists(commentsPath)) {
8995
- currentContent = await readFile12(commentsPath, "utf-8");
9525
+ currentContent = await readFile14(commentsPath, "utf-8");
8996
9526
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
8997
9527
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
8998
9528
  } else {
@@ -9022,7 +9552,7 @@ ${entry}`;
9022
9552
  res.status(201).json({ assignment, comment: { id: comment.id } });
9023
9553
  }
9024
9554
  async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
9025
- const commentsPath = resolve15(assignmentDir, "comments.md");
9555
+ const commentsPath = resolve17(assignmentDir, "comments.md");
9026
9556
  if (!await fileExists(commentsPath)) {
9027
9557
  res.status(404).json({ error: "Comments file not found" });
9028
9558
  return;
@@ -9032,7 +9562,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
9032
9562
  res.status(400).json({ error: "resolved (boolean) is required" });
9033
9563
  return;
9034
9564
  }
9035
- const content = await readFile12(commentsPath, "utf-8");
9565
+ const content = await readFile14(commentsPath, "utf-8");
9036
9566
  const parsed = parseComments(content);
9037
9567
  const target = parsed.entries.find((e) => e.id === commentId);
9038
9568
  if (!target) {
@@ -9179,7 +9709,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
9179
9709
  init_agent_sessions();
9180
9710
  init_fs();
9181
9711
  import { Router as Router3 } from "express";
9182
- import { resolve as resolve16 } from "path";
9712
+ import { resolve as resolve18 } from "path";
9183
9713
 
9184
9714
  // src/utils/transcript.ts
9185
9715
  import { open } from "fs/promises";
@@ -9277,7 +9807,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
9277
9807
  try {
9278
9808
  const { projectSlug } = req.params;
9279
9809
  const assignment = req.query.assignment;
9280
- const projectDir = resolve16(projectsDir, projectSlug);
9810
+ const projectDir = resolve18(projectsDir, projectSlug);
9281
9811
  if (!await fileExists(projectDir)) {
9282
9812
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
9283
9813
  return;
@@ -9307,7 +9837,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
9307
9837
  return;
9308
9838
  }
9309
9839
  if (projectSlug) {
9310
- const projectDir = resolve16(projectsDir, projectSlug);
9840
+ const projectDir = resolve18(projectsDir, projectSlug);
9311
9841
  if (!await fileExists(projectDir)) {
9312
9842
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
9313
9843
  return;
@@ -9631,14 +10161,136 @@ function createAgentsRouter() {
9631
10161
  return router;
9632
10162
  }
9633
10163
 
9634
- // src/dashboard/api-leases.ts
10164
+ // src/dashboard/api-launch-preflight.ts
10165
+ init_config2();
9635
10166
  import { Router as Router5 } from "express";
9636
10167
 
10168
+ // src/utils/terminal-probe.ts
10169
+ import { spawnSync as spawnSync4 } from "child_process";
10170
+ var APP_BUNDLE_IDS = {
10171
+ "terminal-app": "com.apple.Terminal",
10172
+ iterm: "com.googlecode.iterm2",
10173
+ ghostty: "com.mitchellh.ghostty",
10174
+ warp: "dev.warp.Warp-Stable"
10175
+ };
10176
+ var CLI_NAMES = {
10177
+ alacritty: "alacritty",
10178
+ kitty: "kitty"
10179
+ };
10180
+ function probeTerminalInstalled(terminal) {
10181
+ const bundleId = APP_BUNDLE_IDS[terminal];
10182
+ if (bundleId) {
10183
+ const result = spawnSync4(
10184
+ "mdfind",
10185
+ [`kMDItemCFBundleIdentifier == '${bundleId}'`],
10186
+ { encoding: "utf-8" }
10187
+ );
10188
+ if (result.status === 0 && result.stdout.trim().length > 0) {
10189
+ return { ok: true, foundPath: result.stdout.trim().split("\n")[0] };
10190
+ }
10191
+ return { ok: false, reason: "not-installed" };
10192
+ }
10193
+ const cliName = CLI_NAMES[terminal];
10194
+ if (cliName) {
10195
+ const result = spawnSync4("which", [cliName], { encoding: "utf-8" });
10196
+ if (result.status === 0 && result.stdout.trim().length > 0) {
10197
+ return { ok: true, foundPath: result.stdout.trim() };
10198
+ }
10199
+ return { ok: false, reason: "not-installed" };
10200
+ }
10201
+ return { ok: false, reason: "no-probe-available" };
10202
+ }
10203
+
10204
+ // src/dashboard/api-launch-preflight.ts
10205
+ function createLaunchPreflightRouter() {
10206
+ const router = Router5();
10207
+ router.post("/preflight", async (req, res) => {
10208
+ try {
10209
+ const body = req.body ?? {};
10210
+ if (body.terminal !== void 0 && (typeof body.terminal !== "string" || !TERMINAL_CHOICES.includes(body.terminal))) {
10211
+ res.status(400).json({
10212
+ error: `terminal must be one of: ${TERMINAL_CHOICES.join(", ")}`
10213
+ });
10214
+ return;
10215
+ }
10216
+ const config = await readConfig();
10217
+ const terminal = body.terminal ?? getTerminal(config);
10218
+ const probe = probeTerminalInstalled(terminal);
10219
+ if (probe.ok) {
10220
+ const response2 = { ok: true, terminal };
10221
+ res.json(response2);
10222
+ return;
10223
+ }
10224
+ const suggestedFallback = getTerminal({ ...config, terminal: null });
10225
+ const response = {
10226
+ ok: false,
10227
+ terminal,
10228
+ reason: "not-installed",
10229
+ suggestedFallback
10230
+ };
10231
+ res.json(response);
10232
+ } catch (error) {
10233
+ console.error("Error in launch preflight:", error);
10234
+ res.status(500).json({ error: "preflight failed" });
10235
+ }
10236
+ });
10237
+ return router;
10238
+ }
10239
+
10240
+ // src/dashboard/api-terminal-config.ts
10241
+ init_config2();
10242
+ import { Router as Router6 } from "express";
10243
+ function createTerminalConfigRouter() {
10244
+ const router = Router6();
10245
+ router.get("/", async (_req, res) => {
10246
+ try {
10247
+ const config = await readConfig();
10248
+ res.json({
10249
+ terminal: getTerminal(config),
10250
+ custom: config.terminal !== null
10251
+ });
10252
+ } catch (error) {
10253
+ console.error("Error getting terminal config:", error);
10254
+ res.status(500).json({ error: "Failed to get terminal config" });
10255
+ }
10256
+ });
10257
+ router.post("/", async (req, res) => {
10258
+ try {
10259
+ const { terminal } = req.body ?? {};
10260
+ if (typeof terminal !== "string" || !TERMINAL_CHOICES.includes(terminal)) {
10261
+ res.status(400).json({
10262
+ error: `terminal must be one of: ${TERMINAL_CHOICES.join(", ")}`
10263
+ });
10264
+ return;
10265
+ }
10266
+ await writeTerminalConfig(terminal);
10267
+ res.json({ terminal, custom: true });
10268
+ } catch (error) {
10269
+ console.error("Error saving terminal config:", error);
10270
+ res.status(500).json({ error: "Failed to save terminal config" });
10271
+ }
10272
+ });
10273
+ router.delete("/", async (_req, res) => {
10274
+ try {
10275
+ await deleteTerminalConfig();
10276
+ const config = await readConfig();
10277
+ res.json({ terminal: getTerminal(config), custom: false });
10278
+ } catch (error) {
10279
+ console.error("Error resetting terminal config:", error);
10280
+ res.status(500).json({ error: "Failed to reset terminal config" });
10281
+ }
10282
+ });
10283
+ return router;
10284
+ }
10285
+
10286
+ // src/dashboard/api-leases.ts
10287
+ import { Router as Router7 } from "express";
10288
+
9637
10289
  // src/db/leases-db.ts
9638
10290
  init_paths();
9639
10291
  import Database2 from "better-sqlite3";
9640
10292
  import { randomUUID as randomUUID2 } from "crypto";
9641
- import { resolve as resolve17 } from "path";
10293
+ import { resolve as resolve19 } from "path";
9642
10294
  var db2 = null;
9643
10295
  var LEASE_SCHEMA_VERSION = "1";
9644
10296
  var SCHEMA_SQL2 = `
@@ -9725,7 +10377,7 @@ function isBusyError(err) {
9725
10377
  }
9726
10378
  function initLeasesDb(dbPath) {
9727
10379
  if (db2) return db2;
9728
- const finalPath = dbPath ?? resolve17(syntaurRoot(), "syntaur.db");
10380
+ const finalPath = dbPath ?? resolve19(syntaurRoot(), "syntaur.db");
9729
10381
  db2 = new Database2(finalPath);
9730
10382
  db2.pragma("journal_mode = WAL");
9731
10383
  db2.pragma("busy_timeout = 5000");
@@ -9814,7 +10466,7 @@ function forceReleaseLease(lease_id) {
9814
10466
 
9815
10467
  // src/dashboard/api-leases.ts
9816
10468
  function createLeasesRouter(broadcast) {
9817
- const router = Router5();
10469
+ const router = Router7();
9818
10470
  router.get("/", (_req, res) => {
9819
10471
  try {
9820
10472
  initLeasesDb();
@@ -9874,9 +10526,9 @@ init_parser();
9874
10526
  init_slug();
9875
10527
  init_timestamp();
9876
10528
  init_fs();
9877
- import { Router as Router6 } from "express";
9878
- import { resolve as resolve18 } from "path";
9879
- import { readFile as readFile13 } from "fs/promises";
10529
+ import { Router as Router8 } from "express";
10530
+ import { resolve as resolve20 } from "path";
10531
+ import { readFile as readFile15 } from "fs/promises";
9880
10532
  init_playbooks();
9881
10533
  function statusForPlaybookError(code) {
9882
10534
  switch (code) {
@@ -9891,7 +10543,7 @@ function statusForPlaybookError(code) {
9891
10543
  }
9892
10544
  }
9893
10545
  function createPlaybooksRouter(playbooksDir2) {
9894
- const router = Router6();
10546
+ const router = Router8();
9895
10547
  router.get("/", async (_req, res) => {
9896
10548
  try {
9897
10549
  const playbooks = await listPlaybooks(playbooksDir2);
@@ -9958,8 +10610,8 @@ function createPlaybooksRouter(playbooksDir2) {
9958
10610
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
9959
10611
  return;
9960
10612
  }
9961
- const filePath = resolve18(playbooksDir2, resolved.filename);
9962
- const content = await readFile13(filePath, "utf-8");
10613
+ const filePath = resolve20(playbooksDir2, resolved.filename);
10614
+ const content = await readFile15(filePath, "utf-8");
9963
10615
  res.json({
9964
10616
  documentType: "playbook",
9965
10617
  title: `Edit Playbook: ${resolved.slug}`,
@@ -9984,7 +10636,7 @@ function createPlaybooksRouter(playbooksDir2) {
9984
10636
  return;
9985
10637
  }
9986
10638
  await ensureDir(playbooksDir2);
9987
- const filePath = resolve18(playbooksDir2, `${slug}.md`);
10639
+ const filePath = resolve20(playbooksDir2, `${slug}.md`);
9988
10640
  if (await fileExists(filePath)) {
9989
10641
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
9990
10642
  return;
@@ -10008,7 +10660,7 @@ function createPlaybooksRouter(playbooksDir2) {
10008
10660
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
10009
10661
  return;
10010
10662
  }
10011
- const filePath = resolve18(playbooksDir2, resolved.filename);
10663
+ const filePath = resolve20(playbooksDir2, resolved.filename);
10012
10664
  await writeFileForce(filePath, content);
10013
10665
  await rebuildPlaybookManifest(playbooksDir2);
10014
10666
  res.json({ slug: resolved.slug, path: filePath });
@@ -10055,8 +10707,8 @@ init_fs_migration();
10055
10707
  init_parser2();
10056
10708
  init_fs();
10057
10709
  init_paths();
10058
- import { Router as Router7 } from "express";
10059
- import { readdir as readdir9 } from "fs/promises";
10710
+ import { Router as Router9 } from "express";
10711
+ import { readdir as readdir10 } from "fs/promises";
10060
10712
  import { resolve as resolvePath, dirname as dirname4 } from "path";
10061
10713
  import { rename as rename4, mkdir as mkdir2 } from "fs/promises";
10062
10714
  init_slug();
@@ -10067,7 +10719,7 @@ init_parser2();
10067
10719
  // src/commands/create-assignment.ts
10068
10720
  init_slug();
10069
10721
  init_timestamp();
10070
- import { resolve as resolve19 } from "path";
10722
+ import { resolve as resolve21 } from "path";
10071
10723
  init_paths();
10072
10724
  init_fs();
10073
10725
  init_config2();
@@ -10145,14 +10797,14 @@ async function createAssignmentCommand(title, options) {
10145
10797
  if (options.oneOff) {
10146
10798
  const standaloneRoot = assignmentsDir();
10147
10799
  folderName = id;
10148
- assignmentDir = resolve19(standaloneRoot, folderName);
10800
+ assignmentDir = resolve21(standaloneRoot, folderName);
10149
10801
  projectSlug = null;
10150
10802
  await ensureDir(standaloneRoot);
10151
10803
  } else {
10152
10804
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
10153
10805
  projectSlug = options.project;
10154
- const projectDir = resolve19(baseDir, projectSlug);
10155
- const projectMdPath = resolve19(projectDir, "project.md");
10806
+ const projectDir = resolve21(baseDir, projectSlug);
10807
+ const projectMdPath = resolve21(projectDir, "project.md");
10156
10808
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
10157
10809
  throw new Error(
10158
10810
  `Project "${projectSlug}" not found at ${projectDir}.
@@ -10160,9 +10812,9 @@ Run 'syntaur create-project' first or use --one-off.`
10160
10812
  );
10161
10813
  }
10162
10814
  if (dependsOn.length > 0) {
10163
- const depDirBase = resolve19(projectDir, "assignments");
10815
+ const depDirBase = resolve21(projectDir, "assignments");
10164
10816
  for (const dep of dependsOn) {
10165
- const depDir = resolve19(depDirBase, dep);
10817
+ const depDir = resolve21(depDirBase, dep);
10166
10818
  if (!await fileExists(depDir)) {
10167
10819
  console.warn(
10168
10820
  `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
@@ -10171,7 +10823,7 @@ Run 'syntaur create-project' first or use --one-off.`
10171
10823
  }
10172
10824
  }
10173
10825
  folderName = assignmentSlug;
10174
- assignmentDir = resolve19(projectDir, "assignments", folderName);
10826
+ assignmentDir = resolve21(projectDir, "assignments", folderName);
10175
10827
  }
10176
10828
  if (await fileExists(assignmentDir)) {
10177
10829
  throw new Error(
@@ -10183,7 +10835,7 @@ Use --slug to specify a different slug.`
10183
10835
  const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
10184
10836
  const files = [
10185
10837
  [
10186
- resolve19(assignmentDir, "assignment.md"),
10838
+ resolve21(assignmentDir, "assignment.md"),
10187
10839
  renderAssignment({
10188
10840
  id,
10189
10841
  slug: assignmentSlug,
@@ -10201,35 +10853,35 @@ Use --slug to specify a different slug.`
10201
10853
  })
10202
10854
  ],
10203
10855
  [
10204
- resolve19(assignmentDir, "scratchpad.md"),
10856
+ resolve21(assignmentDir, "scratchpad.md"),
10205
10857
  renderScratchpad({
10206
10858
  assignmentSlug: companionAssignmentRef,
10207
10859
  timestamp
10208
10860
  })
10209
10861
  ],
10210
10862
  [
10211
- resolve19(assignmentDir, "handoff.md"),
10863
+ resolve21(assignmentDir, "handoff.md"),
10212
10864
  renderHandoff({
10213
10865
  assignmentSlug: companionAssignmentRef,
10214
10866
  timestamp
10215
10867
  })
10216
10868
  ],
10217
10869
  [
10218
- resolve19(assignmentDir, "decision-record.md"),
10870
+ resolve21(assignmentDir, "decision-record.md"),
10219
10871
  renderDecisionRecord({
10220
10872
  assignmentSlug: companionAssignmentRef,
10221
10873
  timestamp
10222
10874
  })
10223
10875
  ],
10224
10876
  [
10225
- resolve19(assignmentDir, "progress.md"),
10877
+ resolve21(assignmentDir, "progress.md"),
10226
10878
  renderProgress({
10227
10879
  assignment: companionAssignmentRef,
10228
10880
  timestamp
10229
10881
  })
10230
10882
  ],
10231
10883
  [
10232
- resolve19(assignmentDir, "comments.md"),
10884
+ resolve21(assignmentDir, "comments.md"),
10233
10885
  renderComments({
10234
10886
  assignment: companionAssignmentRef,
10235
10887
  timestamp
@@ -10394,7 +11046,7 @@ function touchItem3(item) {
10394
11046
  item.updatedAt = now;
10395
11047
  }
10396
11048
  function createTodosRouter(todosDir2, broadcast, projectsDir) {
10397
- const router = Router7();
11049
+ const router = Router9();
10398
11050
  function broadcastUpdate() {
10399
11051
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
10400
11052
  }
@@ -10510,7 +11162,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
10510
11162
  router.get("/", async (_req, res) => {
10511
11163
  try {
10512
11164
  await ensureDir(todosDir2);
10513
- const files = await readdir9(todosDir2).catch(() => []);
11165
+ const files = await readdir10(todosDir2).catch(() => []);
10514
11166
  const workspaces = [];
10515
11167
  for (const file of files) {
10516
11168
  if (typeof file !== "string") continue;
@@ -10623,8 +11275,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
10623
11275
  router.post("/:workspace/archive", async (req, res) => {
10624
11276
  try {
10625
11277
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
10626
- const { resolve: resolve23 } = await import("path");
10627
- const { readFile: readFile16 } = await import("fs/promises");
11278
+ const { resolve: resolve25 } = await import("path");
11279
+ const { readFile: readFile18 } = await import("fs/promises");
10628
11280
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
10629
11281
  const workspace = getWorkspaceParam(req.params.workspace);
10630
11282
  const checklist = await readChecklist(todosDir2, workspace);
@@ -10640,10 +11292,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
10640
11292
  (e) => e.itemIds.every((id) => completedIds.has(id))
10641
11293
  );
10642
11294
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
10643
- await ensureDir(resolve23(todosDir2, "archive"));
11295
+ await ensureDir(resolve25(todosDir2, "archive"));
10644
11296
  let archContent = "";
10645
11297
  if (await fileExists(archFile)) {
10646
- archContent = await readFile16(archFile, "utf-8");
11298
+ archContent = await readFile18(archFile, "utf-8");
10647
11299
  archContent = archContent.trimEnd() + "\n\n";
10648
11300
  } else {
10649
11301
  archContent = `---
@@ -10922,7 +11574,7 @@ workspace: ${workspace}
10922
11574
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
10923
11575
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
10924
11576
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
10925
- const { readFile: readFile16 } = await import("fs/promises");
11577
+ const { readFile: readFile18 } = await import("fs/promises");
10926
11578
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
10927
11579
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
10928
11580
  let assignmentRef;
@@ -10943,7 +11595,7 @@ workspace: ${workspace}
10943
11595
  }
10944
11596
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
10945
11597
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
10946
- let content = await readFile16(assignmentMdPath, "utf-8");
11598
+ let content = await readFile18(assignmentMdPath, "utf-8");
10947
11599
  content = appendTodosToAssignmentBody2(
10948
11600
  content,
10949
11601
  items.map((it) => ({
@@ -11126,9 +11778,9 @@ init_parser2();
11126
11778
  init_fs();
11127
11779
  init_paths();
11128
11780
  init_slug();
11129
- import { Router as Router8 } from "express";
11130
- import { mkdir as mkdir3, readFile as readFile14, rename as rename5 } from "fs/promises";
11131
- import { resolve as resolve20, dirname as dirname5 } from "path";
11781
+ import { Router as Router10 } from "express";
11782
+ import { mkdir as mkdir3, readFile as readFile16, rename as rename5 } from "fs/promises";
11783
+ import { resolve as resolve22, dirname as dirname5 } from "path";
11132
11784
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
11133
11785
  function touchItem4(item) {
11134
11786
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -11143,7 +11795,7 @@ function params(req) {
11143
11795
  return req.params;
11144
11796
  }
11145
11797
  async function projectExists(projectsDir, slug) {
11146
- return fileExists(resolve20(projectsDir, slug, "project.md"));
11798
+ return fileExists(resolve22(projectsDir, slug, "project.md"));
11147
11799
  }
11148
11800
  async function ensureProjectTodosDir(projectsDir, slug) {
11149
11801
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -11160,7 +11812,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
11160
11812
  throw err;
11161
11813
  }
11162
11814
  try {
11163
- await mkdir3(resolve20(todosDir2, "archive"), { recursive: false });
11815
+ await mkdir3(resolve22(todosDir2, "archive"), { recursive: false });
11164
11816
  } catch (err) {
11165
11817
  const code = err.code;
11166
11818
  if (code === "EEXIST") return;
@@ -11176,7 +11828,7 @@ function notFound(res, slug) {
11176
11828
  res.status(404).json({ error: `Project "${slug}" not found` });
11177
11829
  }
11178
11830
  function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
11179
- const router = Router8({ mergeParams: true });
11831
+ const router = Router10({ mergeParams: true });
11180
11832
  function broadcastUpdate(projectSlug) {
11181
11833
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11182
11834
  }
@@ -11348,7 +12000,7 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
11348
12000
  const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
11349
12001
  let archContent = "";
11350
12002
  if (await fileExists(archFile)) {
11351
- archContent = await readFile14(archFile, "utf-8");
12003
+ archContent = await readFile16(archFile, "utf-8");
11352
12004
  archContent = archContent.trimEnd() + "\n\n";
11353
12005
  } else {
11354
12006
  archContent = `---
@@ -11792,17 +12444,17 @@ workspace: ${slug}
11792
12444
  if (tg.includes("/")) {
11793
12445
  const parts = tg.split("/");
11794
12446
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
11795
- assignmentDir = resolve20(projectsDir, parts[0], "assignments", parts[1]);
12447
+ assignmentDir = resolve22(projectsDir, parts[0], "assignments", parts[1]);
11796
12448
  assignmentRef = `${parts[0]}/${parts[1]}`;
11797
12449
  } 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)) {
11798
- assignmentDir = resolve20(assignmentsDirFn(), tg);
12450
+ assignmentDir = resolve22(assignmentsDirFn(), tg);
11799
12451
  assignmentRef = tg;
11800
12452
  } else {
11801
12453
  return { error: `Invalid target.assignment "${tg}"` };
11802
12454
  }
11803
- const assignmentMdPath = resolve20(assignmentDir, "assignment.md");
12455
+ const assignmentMdPath = resolve22(assignmentDir, "assignment.md");
11804
12456
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
11805
- let content = await readFile14(assignmentMdPath, "utf-8");
12457
+ let content = await readFile16(assignmentMdPath, "utf-8");
11806
12458
  content = appendTodosToAssignmentBody2(
11807
12459
  content,
11808
12460
  items.map((it) => ({
@@ -11994,7 +12646,7 @@ workspace: ${slug}
11994
12646
 
11995
12647
  // src/dashboard/api-backup.ts
11996
12648
  init_config2();
11997
- import { Router as Router9 } from "express";
12649
+ import { Router as Router11 } from "express";
11998
12650
 
11999
12651
  // src/utils/github-backup.ts
12000
12652
  init_paths();
@@ -12002,8 +12654,8 @@ init_fs();
12002
12654
  init_config2();
12003
12655
  import { execFile as execFile2 } from "child_process";
12004
12656
  import { promisify as promisify2 } from "util";
12005
- import { cp, mkdtemp, rm as rm2, readFile as readFile15, writeFile as writeFile4, unlink as unlink4, stat, open as open2, rename as rename6 } from "fs/promises";
12006
- import { resolve as resolve21, join as join2 } from "path";
12657
+ import { cp, mkdtemp, rm as rm2, readFile as readFile17, writeFile as writeFile4, unlink as unlink4, stat, open as open2, rename as rename6 } from "fs/promises";
12658
+ import { resolve as resolve23, join as join2 } from "path";
12007
12659
  import { tmpdir } from "os";
12008
12660
  var exec2 = promisify2(execFile2);
12009
12661
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -12043,7 +12695,7 @@ async function resolveCategoryPath(category) {
12043
12695
  case "servers":
12044
12696
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
12045
12697
  case "config":
12046
- return { sourcePath: resolve21(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
12698
+ return { sourcePath: resolve23(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
12047
12699
  }
12048
12700
  }
12049
12701
  async function checkGitInstalled() {
@@ -12054,7 +12706,7 @@ async function checkGitInstalled() {
12054
12706
  }
12055
12707
  }
12056
12708
  async function acquireLock() {
12057
- const lockPath = resolve21(syntaurRoot(), LOCK_FILE_NAME);
12709
+ const lockPath = resolve23(syntaurRoot(), LOCK_FILE_NAME);
12058
12710
  await ensureDir(syntaurRoot());
12059
12711
  try {
12060
12712
  const handle = await open2(lockPath, "wx");
@@ -12063,7 +12715,7 @@ async function acquireLock() {
12063
12715
  return lockPath;
12064
12716
  } catch (err) {
12065
12717
  if (err.code === "EEXIST") {
12066
- const pid = await readFile15(lockPath, "utf-8").catch(() => "");
12718
+ const pid = await readFile17(lockPath, "utf-8").catch(() => "");
12067
12719
  throw new Error(
12068
12720
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
12069
12721
  );
@@ -12101,7 +12753,7 @@ async function copyRecursive(src, dest) {
12101
12753
  await ensureDir(dest);
12102
12754
  await cp(src, dest, { recursive: true, force: true });
12103
12755
  } else {
12104
- await ensureDir(resolve21(dest, ".."));
12756
+ await ensureDir(resolve23(dest, ".."));
12105
12757
  await cp(src, dest, { force: true });
12106
12758
  }
12107
12759
  }
@@ -12110,7 +12762,7 @@ function resolveCategoriesStrict(csv) {
12110
12762
  return parseCategoriesStrict(parts);
12111
12763
  }
12112
12764
  async function readSanitizedConfig(configPath) {
12113
- const content = await readFile15(configPath, "utf-8");
12765
+ const content = await readFile17(configPath, "utf-8");
12114
12766
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
12115
12767
  }
12116
12768
  async function backupToGithub(overrides) {
@@ -12149,7 +12801,7 @@ async function backupToGithub(overrides) {
12149
12801
  }
12150
12802
  if (category === "config") {
12151
12803
  const sanitized = await readSanitizedConfig(sourcePath);
12152
- await ensureDir(resolve21(destPath, ".."));
12804
+ await ensureDir(resolve23(destPath, ".."));
12153
12805
  await writeFile4(destPath, sanitized, "utf-8");
12154
12806
  } else {
12155
12807
  await copyRecursive(sourcePath, destPath);
@@ -12203,7 +12855,7 @@ async function backupToGithub(overrides) {
12203
12855
  }
12204
12856
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
12205
12857
  if (isFile) {
12206
- await ensureDir(resolve21(localPath, ".."));
12858
+ await ensureDir(resolve23(localPath, ".."));
12207
12859
  await cp(repoSrcPath, localPath, { force: true });
12208
12860
  return;
12209
12861
  }
@@ -12304,7 +12956,7 @@ async function restoreFromGithub(overrides) {
12304
12956
  }
12305
12957
  async function getBackupStatus() {
12306
12958
  const config = await readConfig();
12307
- const lockPath = resolve21(syntaurRoot(), LOCK_FILE_NAME);
12959
+ const lockPath = resolve23(syntaurRoot(), LOCK_FILE_NAME);
12308
12960
  const locked = await fileExists(lockPath);
12309
12961
  return {
12310
12962
  repo: config.backup?.repo ?? null,
@@ -12317,7 +12969,7 @@ async function getBackupStatus() {
12317
12969
 
12318
12970
  // src/dashboard/api-backup.ts
12319
12971
  function createBackupRouter() {
12320
- const router = Router9();
12972
+ const router = Router11();
12321
12973
  router.get("/", async (_req, res) => {
12322
12974
  try {
12323
12975
  const status = await getBackupStatus();
@@ -12645,7 +13297,7 @@ function createDashboardServer(options) {
12645
13297
  (async () => {
12646
13298
  try {
12647
13299
  const configResult = await migrateLegacyConfig(
12648
- resolve22(syntaurRoot(), "config.md")
13300
+ resolve24(syntaurRoot(), "config.md")
12649
13301
  );
12650
13302
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
12651
13303
  const summary = summarizeMigration(projectResult, configResult);
@@ -12768,6 +13420,7 @@ function createDashboardServer(options) {
12768
13420
  res.status(500).json({ error: "Failed to reset theme config" });
12769
13421
  }
12770
13422
  });
13423
+ app.use("/api/config/terminal", createTerminalConfigRouter());
12771
13424
  app.get("/api/config/hotkeys", async (_req, res) => {
12772
13425
  try {
12773
13426
  const config = await readConfig();
@@ -13153,6 +13806,7 @@ function createDashboardServer(options) {
13153
13806
  app.use("/api/leases", createLeasesRouter(broadcast));
13154
13807
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2));
13155
13808
  app.use("/api/config/agents", createAgentsRouter());
13809
+ app.use("/api/launch", createLaunchPreflightRouter());
13156
13810
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
13157
13811
  app.get("/api/memories", async (_req, res) => {
13158
13812
  try {
@@ -13177,14 +13831,14 @@ function createDashboardServer(options) {
13177
13831
  app.use("/api/backup", createBackupRouter());
13178
13832
  if (serveStaticUi && dashboardDistPath) {
13179
13833
  const sendOpts = { dotfiles: "allow" };
13180
- app.use("/assets", express.static(resolve22(dashboardDistPath, "assets"), sendOpts));
13834
+ app.use("/assets", express.static(resolve24(dashboardDistPath, "assets"), sendOpts));
13181
13835
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
13182
13836
  app.get("{*path}", async (req, res) => {
13183
13837
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
13184
13838
  res.status(404).json({ error: "Not Found" });
13185
13839
  return;
13186
13840
  }
13187
- const indexPath = resolve22(dashboardDistPath, "index.html");
13841
+ const indexPath = resolve24(dashboardDistPath, "index.html");
13188
13842
  if (!await fileExists(indexPath)) {
13189
13843
  res.status(503).send(
13190
13844
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -13208,7 +13862,7 @@ function createDashboardServer(options) {
13208
13862
  serversDir: serversDir2,
13209
13863
  playbooksDir: playbooksDir2,
13210
13864
  todosDir: todosDir2,
13211
- dbPath: resolve22(syntaurRoot(), "syntaur.db"),
13865
+ dbPath: resolve24(syntaurRoot(), "syntaur.db"),
13212
13866
  onMessage: broadcast
13213
13867
  });
13214
13868
  startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
@@ -13223,7 +13877,7 @@ function createDashboardServer(options) {
13223
13877
  }
13224
13878
  });
13225
13879
  server.listen(port, () => {
13226
- const portFile = resolve22(syntaurRoot(), "dashboard-port");
13880
+ const portFile = resolve24(syntaurRoot(), "dashboard-port");
13227
13881
  writeFile5(portFile, String(port), "utf-8").catch(() => {
13228
13882
  });
13229
13883
  resolvePromise();
@@ -13241,7 +13895,7 @@ function createDashboardServer(options) {
13241
13895
  client.terminate();
13242
13896
  }
13243
13897
  clients.clear();
13244
- const portFile = resolve22(syntaurRoot(), "dashboard-port");
13898
+ const portFile = resolve24(syntaurRoot(), "dashboard-port");
13245
13899
  await unlink5(portFile).catch(() => {
13246
13900
  });
13247
13901
  server.closeAllConnections?.();