syntaur 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dashboard/dist/assets/{_basePickBy-ij-Ukp6s.js → _basePickBy-BQIP1Ca7.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-CZKk9gZR.js → _baseUniq-BnBWRwT7.js} +1 -1
  3. package/dashboard/dist/assets/{arc-C30UbJZB.js → arc-BYWL4eq0.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BDVieGIr.js → architectureDiagram-2XIMDMQ5-CD_SWPSa.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DZYY4t9w.js → blockDiagram-WCTKOSBZ-BS1ZbFBU.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-B019MXol.js → c4Diagram-IC4MRINW-D99yg-l2.js} +1 -1
  7. package/dashboard/dist/assets/channel-Df6VrFK5.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-DrkTwY15.js → chunk-4BX2VUAB-BkN9IORC.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-mFTAE8DD.js → chunk-55IACEB6-BQPHWefV.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-VgDZaNoS.js → chunk-FMBD7UC4-CNcExMdx.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-C_KXaq-c.js → chunk-JSJVCQXG-LXBmftkC.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-DI-P_pPL.js → chunk-KX2RTZJC-Tqi7zNqq.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-TgYAsxTk.js → chunk-NQ4KR5QH-DkMbx-rW.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-Drfv_VpM.js → chunk-QZHKN3VN-BlrRCfkJ.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-CpLwvo_U.js → chunk-WL4C6EOR-of3XBzMu.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-CyfzumTY.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CyfzumTY.js +1 -0
  18. package/dashboard/dist/assets/clone-CMs4Aqrx.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CkKtF37m.js → cose-bilkent-S5V4N54A-BlIiyO76.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-BBlY_FL3.js → dagre-KLK3FWXG-CYQjSI9N.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-DLsFDLHm.js → diagram-E7M64L7V-BZHzTKct.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2--sb7diMG.js → diagram-IFDJBPK2-kMP3WqBV.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-D2LuEWVt.js → diagram-P4PSJMXO-BWSHyFOv.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-C1BEeili.js → erDiagram-INFDFZHY-B5HrvsPP.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BpbapQbU.js → flowDiagram-PKNHOUZH-Dm4ewP7w.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-Io60qUuG.js → ganttDiagram-A5KZAMGK-DB3k27zu.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-oemlGgRh.js → gitGraphDiagram-K3NZZRJ6-G7y6Ey-m.js} +1 -1
  28. package/dashboard/dist/assets/{graph-BZb-lGfH.js → graph-CaM4i6vq.js} +1 -1
  29. package/dashboard/dist/assets/index-B4QMu-Oq.css +1 -0
  30. package/dashboard/dist/assets/index-BBWZjPBC.js +495 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Ca4mwnZF.js → infoDiagram-LFFYTUFH-JNTUbTjg.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-9zuQ8y8W.js → ishikawaDiagram-PHBUUO56-BZJt1ht8.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-OdeeOdMx.js → journeyDiagram-4ABVD52K-DPcqvl9A.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Cie4JtFn.js → kanban-definition-K7BYSVSG-D1D7AuOV.js} +1 -1
  35. package/dashboard/dist/assets/{layout-Bmx2mvFv.js → layout-BTOh3EDT.js} +1 -1
  36. package/dashboard/dist/assets/{linear-CW6K_-MX.js → linear-MbCpC_Cg.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-DmfO6BgK.js → mermaid.core-CYbhqlNy.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-L6b3vG79.js → mindmap-definition-YRQLILUH-CwYCISFH.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CkHTCIWg.js → pieDiagram-SKSYHLDU-5qfZ73SG.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-B9MqhhIC.js → quadrantDiagram-337W2JSQ-WI8y1sQ_.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CyHAfXCK.js → requirementDiagram-Z7DCOOCP-BFlD0ZTS.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DHNzGGyE.js → sankeyDiagram-WA2Y5GQK-Bdckv1Se.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BVvcJkrx.js → sequenceDiagram-2WXFIKYE-DgzxKAlZ.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-CZ2cknh7.js → stateDiagram-RAJIS63D-DO4OXahC.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-o8bgX-J3.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BXUtlVyd.js → timeline-definition-YZTLITO2-BBB01JWw.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-Dgi-hMKM.js → treemap-KZPCXAKY-Dr0jb8op.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-C9zGrrUQ.js → vennDiagram-LZ73GAT5-D40KFl2o.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-Dq71BUtc.js → xychartDiagram-JWTSCODW-DBUmWQfT.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +2380 -1263
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +5601 -4684
  54. package/dist/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/vendor/syntaur-skills/README.md +2 -0
  57. package/vendor/syntaur-skills/skills/clear-assignment/SKILL.md +111 -0
  58. package/vendor/syntaur-skills/skills/manage-statuses/SKILL.md +72 -0
  59. package/dashboard/dist/assets/channel-DH4gshIt.js +0 -1
  60. package/dashboard/dist/assets/classDiagram-VBA2DB6C-emsfh8H4.js +0 -1
  61. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-emsfh8H4.js +0 -1
  62. package/dashboard/dist/assets/clone-gdeRwgBN.js +0 -1
  63. package/dashboard/dist/assets/index-BSVCsfvM.css +0 -1
  64. package/dashboard/dist/assets/index-CXWVuGs-.js +0 -481
  65. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-C4CPervD.js +0 -1
@@ -9,6 +9,18 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/utils/paths.ts
12
+ var paths_exports = {};
13
+ __export(paths_exports, {
14
+ assignmentsDir: () => assignmentsDir,
15
+ defaultProjectDir: () => defaultProjectDir,
16
+ expandHome: () => expandHome,
17
+ playbooksDir: () => playbooksDir,
18
+ projectTodosDir: () => projectTodosDir,
19
+ serversDir: () => serversDir,
20
+ syntaurRoot: () => syntaurRoot,
21
+ todoPlanDir: () => todoPlanDir,
22
+ todosDir: () => todosDir
23
+ });
12
24
  import { homedir } from "os";
13
25
  import { resolve } from "path";
14
26
  function expandHome(p) {
@@ -27,6 +39,9 @@ function syntaurRoot() {
27
39
  function defaultProjectDir() {
28
40
  return resolve(syntaurRoot(), "projects");
29
41
  }
42
+ function assignmentsDir() {
43
+ return resolve(syntaurRoot(), "assignments");
44
+ }
30
45
  function serversDir() {
31
46
  return resolve(syntaurRoot(), "servers");
32
47
  }
@@ -39,6 +54,9 @@ function todosDir() {
39
54
  function projectTodosDir(projectsDir, projectSlug) {
40
55
  return resolve(projectsDir, projectSlug, "todos");
41
56
  }
57
+ function todoPlanDir(todosDir2, workspaceOrProject, todoId) {
58
+ return resolve(todosDir2, "plans", workspaceOrProject, todoId);
59
+ }
42
60
  var init_paths = __esm({
43
61
  "src/utils/paths.ts"() {
44
62
  "use strict";
@@ -311,6 +329,10 @@ var init_fs = __esm({
311
329
  });
312
330
 
313
331
  // src/utils/timestamp.ts
332
+ var timestamp_exports = {};
333
+ __export(timestamp_exports, {
334
+ nowTimestamp: () => nowTimestamp
335
+ });
314
336
  function nowTimestamp() {
315
337
  return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
316
338
  }
@@ -622,7 +644,124 @@ var init_fs_migration = __esm({
622
644
  }
623
645
  });
624
646
 
647
+ // src/utils/hotkeysCatalog.ts
648
+ function isBindableActionKind(value) {
649
+ return typeof value === "string" && BINDABLE_ACTION_KINDS.includes(value);
650
+ }
651
+ function canonicalizeCombo(input) {
652
+ if (typeof input !== "string") return "";
653
+ const trimmed = input.trim();
654
+ if (!trimmed) return "";
655
+ if (/\s/.test(trimmed) && !trimmed.includes("+")) {
656
+ return trimmed.split(/\s+/).map(canonicalizeCombo).filter((part) => part.length > 0).join(" ");
657
+ }
658
+ const parts = trimmed.split("+").map((p) => p.trim()).filter((p) => p.length > 0);
659
+ if (parts.length === 0) return "";
660
+ if (parts.length === 1) {
661
+ return parts[0].toLowerCase();
662
+ }
663
+ const key = parts[parts.length - 1].toLowerCase();
664
+ const mods = parts.slice(0, -1).map((m) => m.toLowerCase());
665
+ const seen = /* @__PURE__ */ new Set();
666
+ const ordered = [];
667
+ for (const m of MODIFIER_ORDER) {
668
+ if (mods.includes(m) && !seen.has(m)) {
669
+ ordered.push(m);
670
+ seen.add(m);
671
+ }
672
+ }
673
+ for (const m of mods) {
674
+ if (!seen.has(m)) {
675
+ ordered.push(m);
676
+ seen.add(m);
677
+ }
678
+ }
679
+ return [...ordered, key].join("+");
680
+ }
681
+ function isReservedCombo(combo) {
682
+ const c = canonicalizeCombo(combo);
683
+ if (!c) return false;
684
+ return BUILTIN_RESERVED_COMBOS.includes(c);
685
+ }
686
+ var BINDABLE_ACTION_KINDS, BUILTIN_RESERVED_COMBOS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
687
+ var init_hotkeysCatalog = __esm({
688
+ "src/utils/hotkeysCatalog.ts"() {
689
+ "use strict";
690
+ BINDABLE_ACTION_KINDS = [
691
+ "new-workspace",
692
+ "new-project",
693
+ "new-todo",
694
+ "new-assignment"
695
+ ];
696
+ BUILTIN_RESERVED_COMBOS = [
697
+ "mod+k",
698
+ "mod+shift+k",
699
+ "?",
700
+ "escape",
701
+ "enter",
702
+ "shift+t",
703
+ // g-chord starter + suffixes
704
+ "g",
705
+ "g o",
706
+ "g m",
707
+ "g a",
708
+ "g t",
709
+ "g s",
710
+ "g !",
711
+ "g ,",
712
+ // list-scope navigation
713
+ "j",
714
+ "k",
715
+ "o",
716
+ // ProjectDetail page
717
+ "a",
718
+ "e",
719
+ // AssignmentsPage board
720
+ "/",
721
+ "r",
722
+ // AssignmentDetail page
723
+ "p",
724
+ "h",
725
+ "d",
726
+ "s",
727
+ "[",
728
+ "]"
729
+ ];
730
+ MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"];
731
+ DEFAULT_BINDABLE_HOTKEYS = {
732
+ "new-workspace": canonicalizeCombo("Mod+Shift+Alt+w"),
733
+ "new-project": canonicalizeCombo("Mod+Shift+Alt+p"),
734
+ "new-todo": canonicalizeCombo("Mod+Shift+Alt+t"),
735
+ "new-assignment": canonicalizeCombo("Mod+Shift+Alt+a")
736
+ };
737
+ }
738
+ });
739
+
625
740
  // src/utils/config.ts
741
+ var config_exports = {};
742
+ __export(config_exports, {
743
+ AgentConfigError: () => AgentConfigError,
744
+ BUILTIN_AGENTS: () => BUILTIN_AGENTS,
745
+ DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
746
+ deleteAgentsConfig: () => deleteAgentsConfig,
747
+ deleteHotkeyBindingsConfig: () => deleteHotkeyBindingsConfig,
748
+ deleteStatusConfig: () => deleteStatusConfig,
749
+ deleteThemeConfig: () => deleteThemeConfig,
750
+ getAgents: () => getAgents,
751
+ getAssignmentTypes: () => getAssignmentTypes,
752
+ parseAgentCommand: () => parseAgentCommand,
753
+ readConfig: () => readConfig,
754
+ updateAgentsConfig: () => updateAgentsConfig,
755
+ updateBackupConfig: () => updateBackupConfig,
756
+ updateIntegrationConfig: () => updateIntegrationConfig,
757
+ updateOnboardingConfig: () => updateOnboardingConfig,
758
+ updatePlaybooksConfig: () => updatePlaybooksConfig,
759
+ validateAgentList: () => validateAgentList,
760
+ writeAgentsConfig: () => writeAgentsConfig,
761
+ writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
762
+ writeStatusConfig: () => writeStatusConfig,
763
+ writeThemeConfig: () => writeThemeConfig
764
+ });
626
765
  import { readFile as readFile3 } from "fs/promises";
627
766
  import { resolve as resolve4, isAbsolute } from "path";
628
767
  function parseAgentCommand(value, agentId) {
@@ -695,7 +834,8 @@ function cloneDefaultConfig() {
695
834
  playbooks: {
696
835
  disabled: [...DEFAULT_CONFIG.playbooks.disabled]
697
836
  },
698
- theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null
837
+ theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null,
838
+ hotkeys: DEFAULT_CONFIG.hotkeys ? { bindings: { ...DEFAULT_CONFIG.hotkeys.bindings } } : null
699
839
  };
700
840
  }
701
841
  function parseFrontmatter(content) {
@@ -842,6 +982,25 @@ function serializeStatusConfig(statuses) {
842
982
  }
843
983
  return lines.join("\n");
844
984
  }
985
+ function serializeIntegrationConfig(integrations) {
986
+ const lines = [];
987
+ if (integrations.claudePluginDir) {
988
+ lines.push(` claudePluginDir: ${integrations.claudePluginDir}`);
989
+ }
990
+ if (integrations.codexPluginDir) {
991
+ lines.push(` codexPluginDir: ${integrations.codexPluginDir}`);
992
+ }
993
+ if (integrations.codexMarketplacePath) {
994
+ lines.push(` codexMarketplacePath: ${integrations.codexMarketplacePath}`);
995
+ }
996
+ if (lines.length === 0) {
997
+ return null;
998
+ }
999
+ return ["integrations:", ...lines].join("\n");
1000
+ }
1001
+ function serializeOnboardingConfig(onboarding) {
1002
+ return ["onboarding:", ` completed: ${onboarding.completed ? "true" : "false"}`].join("\n");
1003
+ }
845
1004
  function serializeBackupConfig(backup) {
846
1005
  const lines = ["backup:"];
847
1006
  lines.push(` repo: ${backup.repo ?? "null"}`);
@@ -991,6 +1150,101 @@ ${cleanedFm}
991
1150
  ---${afterFrontmatter}`;
992
1151
  await writeFileForce(configPath, newContent);
993
1152
  }
1153
+ function parseHotkeyBindingsConfig(content) {
1154
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1155
+ if (!match) return null;
1156
+ const fmBlock = match[1];
1157
+ const blockStart = fmBlock.match(/^hotkeys:\s*$/m);
1158
+ if (!blockStart) return null;
1159
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
1160
+ const remaining = fmBlock.slice(startIdx).split("\n");
1161
+ const bindings = {};
1162
+ let inBindings = false;
1163
+ for (const line of remaining) {
1164
+ const trimmed = line.trimStart();
1165
+ const indent = line.length - trimmed.length;
1166
+ if (indent === 0 && trimmed.length > 0) break;
1167
+ if (trimmed === "") continue;
1168
+ if (indent === 2 && trimmed === "bindings:") {
1169
+ inBindings = true;
1170
+ continue;
1171
+ }
1172
+ if (inBindings && indent === 4) {
1173
+ const colonIdx = trimmed.indexOf(":");
1174
+ if (colonIdx <= 0) continue;
1175
+ const rawKind = trimmed.slice(0, colonIdx).trim();
1176
+ const rawValue = trimmed.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
1177
+ if (!isBindableActionKind(rawKind)) continue;
1178
+ if (rawValue.length === 0) continue;
1179
+ bindings[rawKind] = canonicalizeCombo(rawValue);
1180
+ }
1181
+ }
1182
+ if (Object.keys(bindings).length === 0) return null;
1183
+ return { bindings };
1184
+ }
1185
+ function serializeHotkeyBindingsConfig(cfg) {
1186
+ const lines = ["hotkeys:", " bindings:"];
1187
+ for (const kind of BINDABLE_ACTION_KINDS) {
1188
+ const value = cfg.bindings[kind];
1189
+ if (!value) continue;
1190
+ lines.push(` ${kind}: "${canonicalizeCombo(value)}"`);
1191
+ }
1192
+ if (lines.length === 2) return "";
1193
+ return lines.join("\n");
1194
+ }
1195
+ async function writeHotkeyBindingsConfig(cfg) {
1196
+ const cleaned = {};
1197
+ for (const kind of BINDABLE_ACTION_KINDS) {
1198
+ const raw = cfg.bindings[kind];
1199
+ if (typeof raw !== "string" || raw.trim() === "") continue;
1200
+ const canonical = canonicalizeCombo(raw);
1201
+ if (!canonical) continue;
1202
+ if (isReservedCombo(canonical)) continue;
1203
+ cleaned[kind] = canonical;
1204
+ }
1205
+ if (Object.keys(cleaned).length === 0) {
1206
+ await deleteHotkeyBindingsConfig();
1207
+ return;
1208
+ }
1209
+ const configPath = resolve4(syntaurRoot(), "config.md");
1210
+ const block = serializeHotkeyBindingsConfig({ bindings: cleaned });
1211
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1212
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1213
+ if (!fmMatch) {
1214
+ const content = `---
1215
+ version: "2.0"
1216
+ defaultProjectDir: ${defaultProjectDir()}
1217
+ ${block}
1218
+ ---
1219
+ ${existing}`;
1220
+ await writeFileForce(configPath, content);
1221
+ return;
1222
+ }
1223
+ const fmBlock = fmMatch[2];
1224
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1225
+ const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
1226
+ const newFm = `${cleanedFm}
1227
+ ${block}`.replace(/^\n+/, "");
1228
+ const normalizedFm = newFm.replace(/\n+$/, "");
1229
+ const newContent = `---
1230
+ ${normalizedFm}
1231
+ ---${afterFrontmatter}`;
1232
+ await writeFileForce(configPath, newContent);
1233
+ }
1234
+ async function deleteHotkeyBindingsConfig() {
1235
+ const configPath = resolve4(syntaurRoot(), "config.md");
1236
+ if (!await fileExists(configPath)) return;
1237
+ const existing = await readFile3(configPath, "utf-8");
1238
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1239
+ if (!fmMatch) return;
1240
+ const fmBlock = fmMatch[2];
1241
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1242
+ const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
1243
+ const newContent = `---
1244
+ ${cleanedFm}
1245
+ ---${afterFrontmatter}`;
1246
+ await writeFileForce(configPath, newContent);
1247
+ }
994
1248
  function stripTopLevelBlock(fmBlock, key) {
995
1249
  const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
996
1250
  if (!blockStart) {
@@ -1185,6 +1439,82 @@ function assignAgentField(target, key, rawValue) {
1185
1439
  break;
1186
1440
  }
1187
1441
  }
1442
+ function yamlQuoteScalar(value) {
1443
+ if (/[\r\n]/.test(value)) {
1444
+ throw new AgentConfigError(
1445
+ `value contains newlines, which the agents config serializer does not support: ${JSON.stringify(value)}`
1446
+ );
1447
+ }
1448
+ if (value === "" || /[:#{}[\],&*?|>!%@`"'\\\t]/.test(value) || /^\s|\s$/.test(value)) {
1449
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t");
1450
+ return `"${escaped}"`;
1451
+ }
1452
+ return value;
1453
+ }
1454
+ function serializeAgentsConfig(agents) {
1455
+ const lines = ["agents:"];
1456
+ for (const a of agents) {
1457
+ lines.push(` - id: ${yamlQuoteScalar(a.id)}`);
1458
+ lines.push(` label: ${yamlQuoteScalar(a.label)}`);
1459
+ lines.push(` command: ${yamlQuoteScalar(a.command)}`);
1460
+ if (a.args && a.args.length > 0) {
1461
+ lines.push(` args:`);
1462
+ for (const arg of a.args) {
1463
+ lines.push(` - ${yamlQuoteScalar(arg)}`);
1464
+ }
1465
+ }
1466
+ if (a.promptArgPosition && a.promptArgPosition !== "first") {
1467
+ lines.push(` promptArgPosition: ${a.promptArgPosition}`);
1468
+ }
1469
+ if (a.default) {
1470
+ lines.push(` default: true`);
1471
+ }
1472
+ if (a.resolveFromShellAliases) {
1473
+ lines.push(` resolveFromShellAliases: true`);
1474
+ }
1475
+ }
1476
+ return lines.join("\n");
1477
+ }
1478
+ async function writeAgentsConfig(agents) {
1479
+ validateAgentList(agents);
1480
+ const configPath = resolve4(syntaurRoot(), "config.md");
1481
+ const agentsBlock = serializeAgentsConfig(agents);
1482
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1483
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1484
+ if (!fmMatch) {
1485
+ const content = `---
1486
+ version: "2.0"
1487
+ defaultProjectDir: ${defaultProjectDir()}
1488
+ ${agentsBlock}
1489
+ ---
1490
+ ${existing}`;
1491
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
1492
+ return;
1493
+ }
1494
+ const fmBlock = fmMatch[2];
1495
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1496
+ const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
1497
+ const newFm = `${cleanedFm}
1498
+ ${agentsBlock}`.replace(/^\n+/, "").replace(/\n+$/, "");
1499
+ const newContent = `---
1500
+ ${newFm}
1501
+ ---${afterFrontmatter}`;
1502
+ await writeFileForce(configPath, newContent);
1503
+ }
1504
+ async function deleteAgentsConfig() {
1505
+ const configPath = resolve4(syntaurRoot(), "config.md");
1506
+ if (!await fileExists(configPath)) return;
1507
+ const existing = await readFile3(configPath, "utf-8");
1508
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1509
+ if (!fmMatch) return;
1510
+ const fmBlock = fmMatch[2];
1511
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1512
+ const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
1513
+ const newContent = `---
1514
+ ${cleanedFm}
1515
+ ---${afterFrontmatter}`;
1516
+ await writeFileForce(configPath, newContent);
1517
+ }
1188
1518
  async function writeStatusConfig(statuses) {
1189
1519
  const configPath = resolve4(syntaurRoot(), "config.md");
1190
1520
  const statusBlock = serializeStatusConfig(statuses);
@@ -1253,6 +1583,66 @@ ${cleanedFm}
1253
1583
  ---${afterFrontmatter}`;
1254
1584
  await writeFileForce(configPath, newContent);
1255
1585
  }
1586
+ async function updateIntegrationConfig(integrations) {
1587
+ const configPath = resolve4(syntaurRoot(), "config.md");
1588
+ const nextIntegrations = {
1589
+ ...(await readConfig()).integrations,
1590
+ ...integrations
1591
+ };
1592
+ const integrationBlock = serializeIntegrationConfig(nextIntegrations);
1593
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1594
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1595
+ if (!fmMatch) {
1596
+ const content = `---
1597
+ version: "2.0"
1598
+ defaultProjectDir: ${defaultProjectDir()}
1599
+ ${integrationBlock ?? ""}
1600
+ ---
1601
+ ${existing}`;
1602
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
1603
+ return;
1604
+ }
1605
+ const fmBlock = fmMatch[2];
1606
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1607
+ const cleanedFm = stripTopLevelBlock(fmBlock, "integrations");
1608
+ const newFm = integrationBlock ? `${cleanedFm}
1609
+ ${integrationBlock}`.replace(/^\n+/, "") : cleanedFm;
1610
+ const normalizedFm = newFm.replace(/\n+$/, "");
1611
+ const newContent = `---
1612
+ ${normalizedFm}
1613
+ ---${afterFrontmatter}`;
1614
+ await writeFileForce(configPath, newContent);
1615
+ }
1616
+ async function updateOnboardingConfig(onboarding) {
1617
+ const configPath = resolve4(syntaurRoot(), "config.md");
1618
+ const nextOnboarding = {
1619
+ ...(await readConfig()).onboarding,
1620
+ ...onboarding
1621
+ };
1622
+ const onboardingBlock = serializeOnboardingConfig(nextOnboarding);
1623
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1624
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1625
+ if (!fmMatch) {
1626
+ const content = `---
1627
+ version: "2.0"
1628
+ defaultProjectDir: ${defaultProjectDir()}
1629
+ ${onboardingBlock}
1630
+ ---
1631
+ ${existing}`;
1632
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
1633
+ return;
1634
+ }
1635
+ const fmBlock = fmMatch[2];
1636
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1637
+ const cleanedFm = stripTopLevelBlock(fmBlock, "onboarding");
1638
+ const newFm = `${cleanedFm}
1639
+ ${onboardingBlock}`.replace(/^\n+/, "");
1640
+ const normalizedFm = newFm.replace(/\n+$/, "");
1641
+ const newContent = `---
1642
+ ${normalizedFm}
1643
+ ---${afterFrontmatter}`;
1644
+ await writeFileForce(configPath, newContent);
1645
+ }
1256
1646
  async function updateBackupConfig(backup) {
1257
1647
  const configPath = resolve4(syntaurRoot(), "config.md");
1258
1648
  const current = (await readConfig()).backup;
@@ -1347,10 +1737,28 @@ async function readConfig() {
1347
1737
  types: null,
1348
1738
  agents: normalizeAgentsFromConfig(parseAgentsConfig(content)),
1349
1739
  playbooks: parsePlaybooksConfig(fmBlock),
1350
- theme: parseThemeConfig(content)
1740
+ theme: parseThemeConfig(content),
1741
+ hotkeys: parseHotkeyBindingsConfig(content)
1351
1742
  };
1352
1743
  }
1353
- var DEFAULT_CONFIG, AGENT_ID_PATTERN, PROMPT_ARG_POSITIONS, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, migratedConfigPaths;
1744
+ function getAssignmentTypes(config) {
1745
+ return config.types ?? DEFAULT_ASSIGNMENT_TYPES;
1746
+ }
1747
+ function getAgents(config) {
1748
+ return config.agents ?? BUILTIN_AGENTS;
1749
+ }
1750
+ async function updateAgentsConfig(mutation, options = {}) {
1751
+ const config = await readConfig();
1752
+ const previous = config.agents ?? [...BUILTIN_AGENTS];
1753
+ const next = mutation.apply(previous);
1754
+ validateAgentList(next);
1755
+ if (options.dryRun) {
1756
+ return { previous, next, written: false };
1757
+ }
1758
+ await writeAgentsConfig(next);
1759
+ return { previous, next, written: true };
1760
+ }
1761
+ var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, BUILTIN_AGENTS, AGENT_ID_PATTERN, PROMPT_ARG_POSITIONS, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, migratedConfigPaths;
1354
1762
  var init_config2 = __esm({
1355
1763
  "src/utils/config.ts"() {
1356
1764
  "use strict";
@@ -1358,6 +1766,17 @@ var init_config2 = __esm({
1358
1766
  init_fs();
1359
1767
  init_config();
1360
1768
  init_fs_migration();
1769
+ init_hotkeysCatalog();
1770
+ DEFAULT_ASSIGNMENT_TYPES = {
1771
+ definitions: [
1772
+ { id: "feature", label: "Feature" },
1773
+ { id: "bug", label: "Bug" },
1774
+ { id: "refactor", label: "Refactor" },
1775
+ { id: "research", label: "Research" },
1776
+ { id: "chore", label: "Chore" }
1777
+ ],
1778
+ default: "feature"
1779
+ };
1361
1780
  DEFAULT_CONFIG = {
1362
1781
  version: "2.0",
1363
1782
  defaultProjectDir: defaultProjectDir(),
@@ -1381,8 +1800,13 @@ var init_config2 = __esm({
1381
1800
  playbooks: {
1382
1801
  disabled: []
1383
1802
  },
1384
- theme: null
1803
+ theme: null,
1804
+ hotkeys: null
1385
1805
  };
1806
+ BUILTIN_AGENTS = [
1807
+ { id: "claude", label: "Claude", command: "claude", default: true },
1808
+ { id: "codex", label: "Codex", command: "codex" }
1809
+ ];
1386
1810
  AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
1387
1811
  PROMPT_ARG_POSITIONS = ["first", "last", "none"];
1388
1812
  AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
@@ -1785,10 +2209,10 @@ var init_playbooks = __esm({
1785
2209
  // src/utils/assignment-resolver.ts
1786
2210
  import { resolve as resolve6 } from "path";
1787
2211
  import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
1788
- async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
2212
+ async function resolveAssignmentById(projectsDir, assignmentsDir2, id) {
1789
2213
  let standaloneMatch = null;
1790
2214
  let projectMatch = null;
1791
- const standaloneDir = resolve6(assignmentsDir, id);
2215
+ const standaloneDir = resolve6(assignmentsDir2, id);
1792
2216
  const standalonePath = resolve6(standaloneDir, "assignment.md");
1793
2217
  if (await fileExists(standalonePath)) {
1794
2218
  let workspaceGroup = null;
@@ -2560,7 +2984,7 @@ async function getGitInfo(cwd) {
2560
2984
  }
2561
2985
  return { branch: branch || null, worktree: isWorktree };
2562
2986
  }
2563
- async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
2987
+ async function loadWorkspaceRecords(projectsDir, assignmentsDir2) {
2564
2988
  const records = [];
2565
2989
  try {
2566
2990
  const projects = await listProjects(projectsDir);
@@ -2592,12 +3016,12 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
2592
3016
  }
2593
3017
  } catch {
2594
3018
  }
2595
- if (assignmentsDir) {
3019
+ if (assignmentsDir2) {
2596
3020
  try {
2597
- const entries = await readdir5(assignmentsDir);
3021
+ const entries = await readdir5(assignmentsDir2);
2598
3022
  for (const id of entries) {
2599
3023
  if (id.startsWith(".") || id.startsWith("_")) continue;
2600
- const aFile = resolve8(assignmentsDir, id, "assignment.md");
3024
+ const aFile = resolve8(assignmentsDir2, id, "assignment.md");
2601
3025
  try {
2602
3026
  const raw = await readFile7(aFile, "utf-8");
2603
3027
  const [fm] = extractFrontmatter2(raw);
@@ -2849,14 +3273,14 @@ var init_scanner = __esm({
2849
3273
  // src/dashboard/api.ts
2850
3274
  import { readdir as readdir6, readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
2851
3275
  import { resolve as resolve9, dirname as dirname2 } from "path";
2852
- async function listStandaloneRecords(assignmentsDir) {
2853
- if (!assignmentsDir) return [];
2854
- if (!await fileExists(assignmentsDir)) return [];
2855
- const entries = await readdir6(assignmentsDir, { withFileTypes: true });
3276
+ async function listStandaloneRecords(assignmentsDir2) {
3277
+ if (!assignmentsDir2) return [];
3278
+ if (!await fileExists(assignmentsDir2)) return [];
3279
+ const entries = await readdir6(assignmentsDir2, { withFileTypes: true });
2856
3280
  const records = [];
2857
3281
  for (const entry of entries) {
2858
3282
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
2859
- const assignmentDir = resolve9(assignmentsDir, entry.name);
3283
+ const assignmentDir = resolve9(assignmentsDir2, entry.name);
2860
3284
  const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
2861
3285
  if (!await fileExists(assignmentMdPath)) continue;
2862
3286
  try {
@@ -2942,11 +3366,11 @@ async function writeWorkspaceRegistry(projectsDir, workspaces) {
2942
3366
  const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
2943
3367
  await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
2944
3368
  }
2945
- async function listWorkspaces(projectsDir, assignmentsDir) {
3369
+ async function listWorkspaces(projectsDir, assignmentsDir2) {
2946
3370
  const [projectRecords, registered, standaloneRecords] = await Promise.all([
2947
3371
  listProjectRecords(projectsDir),
2948
3372
  readWorkspaceRegistry(projectsDir),
2949
- listStandaloneRecords(assignmentsDir)
3373
+ listStandaloneRecords(assignmentsDir2)
2950
3374
  ]);
2951
3375
  const workspaceSet = new Set(registered);
2952
3376
  let hasUngrouped = false;
@@ -2980,16 +3404,16 @@ async function deleteWorkspace(projectsDir, name) {
2980
3404
  const filtered = registered.filter((w) => w !== name);
2981
3405
  await writeWorkspaceRegistry(projectsDir, filtered);
2982
3406
  }
2983
- async function getOverview(projectsDir, serversDir2, assignmentsDir) {
3407
+ async function getOverview(projectsDir, serversDir2, assignmentsDir2) {
2984
3408
  const projectRecords = await listProjectRecords(projectsDir);
2985
- const standaloneRecords = await listStandaloneRecords(assignmentsDir);
3409
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
2986
3410
  const attention = buildAttentionItems(projectRecords, standaloneRecords);
2987
3411
  const recentActivity = buildRecentActivity(projectRecords, standaloneRecords);
2988
3412
  let serverStats;
2989
3413
  if (serversDir2) {
2990
3414
  try {
2991
3415
  const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
2992
- const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir });
3416
+ const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 });
2993
3417
  if (servers.tmuxAvailable) {
2994
3418
  const alive = servers.sessions.filter((s) => s.alive).length;
2995
3419
  const totalPorts = servers.sessions.reduce((sum, s) => sum + s.windows.reduce((ws, w) => ws + w.panes.reduce((ps, p) => ps + p.ports.length, 0), 0), 0);
@@ -3035,14 +3459,14 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir) {
3035
3459
  serverStats
3036
3460
  };
3037
3461
  }
3038
- async function getAttention(projectsDir, serversDir2, assignmentsDir) {
3462
+ async function getAttention(projectsDir, serversDir2, assignmentsDir2) {
3039
3463
  const projectRecords = await listProjectRecords(projectsDir);
3040
- const standaloneRecords = await listStandaloneRecords(assignmentsDir);
3464
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
3041
3465
  const items = buildAttentionItems(projectRecords, standaloneRecords);
3042
3466
  if (serversDir2) {
3043
3467
  try {
3044
3468
  const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
3045
- const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir });
3469
+ const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 });
3046
3470
  for (const session of servers.sessions) {
3047
3471
  if (!session.alive) {
3048
3472
  items.push({
@@ -3086,7 +3510,7 @@ async function getAttention(projectsDir, serversDir2, assignmentsDir) {
3086
3510
  items: pagedItems
3087
3511
  };
3088
3512
  }
3089
- async function listAssignmentsBoard(projectsDir, assignmentsDir) {
3513
+ async function listAssignmentsBoard(projectsDir, assignmentsDir2) {
3090
3514
  const projectRecords = await listProjectRecords(projectsDir);
3091
3515
  const projectItems = await Promise.all(
3092
3516
  projectRecords.flatMap(
@@ -3097,7 +3521,7 @@ async function listAssignmentsBoard(projectsDir, assignmentsDir) {
3097
3521
  )
3098
3522
  )
3099
3523
  );
3100
- const standaloneRecords = await listStandaloneRecords(assignmentsDir);
3524
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
3101
3525
  const standaloneItems = await Promise.all(
3102
3526
  standaloneRecords.map(async (sr) => toStandaloneBoardItem(sr))
3103
3527
  );
@@ -3158,8 +3582,8 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
3158
3582
  appendOnly: documentType === "handoff" || documentType === "decision-record"
3159
3583
  };
3160
3584
  }
3161
- async function getEditableDocumentById(projectsDir, assignmentsDir, documentType, id) {
3162
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
3585
+ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentType, id) {
3586
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
3163
3587
  if (!resolved) return null;
3164
3588
  if (!resolved.standalone && resolved.projectSlug) {
3165
3589
  return getEditableDocument(
@@ -3390,7 +3814,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
3390
3814
  );
3391
3815
  return detail;
3392
3816
  }
3393
- async function computeReferencedBy(target, projectsDir, assignmentsDir) {
3817
+ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
3394
3818
  const sources = [];
3395
3819
  const projectRecords = await listProjectRecords(projectsDir);
3396
3820
  for (const rec of projectRecords) {
@@ -3404,7 +3828,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
3404
3828
  });
3405
3829
  }
3406
3830
  }
3407
- const standaloneRecords = await listStandaloneRecords(assignmentsDir);
3831
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
3408
3832
  for (const sr of standaloneRecords) {
3409
3833
  sources.push({
3410
3834
  id: sr.id,
@@ -3477,8 +3901,8 @@ function buildLinkPatternsForTarget(target) {
3477
3901
  function escapeRegExpLocal(value) {
3478
3902
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3479
3903
  }
3480
- async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
3481
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
3904
+ async function getAssignmentDetailById(projectsDir, assignmentsDir2, id) {
3905
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
3482
3906
  if (!resolved) return null;
3483
3907
  if (!resolved.standalone && resolved.projectSlug) {
3484
3908
  const detail = await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
@@ -3486,7 +3910,7 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
3486
3910
  detail.referencedBy = await computeReferencedBy(
3487
3911
  { id: detail.id, projectSlug: detail.projectSlug, slug: detail.slug },
3488
3912
  projectsDir,
3489
- assignmentsDir
3913
+ assignmentsDir2
3490
3914
  );
3491
3915
  return detail;
3492
3916
  }
@@ -3495,7 +3919,7 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
3495
3919
  standaloneDetail.referencedBy = await computeReferencedBy(
3496
3920
  { id: standaloneDetail.id, projectSlug: null, slug: standaloneDetail.slug },
3497
3921
  projectsDir,
3498
- assignmentsDir
3922
+ assignmentsDir2
3499
3923
  );
3500
3924
  return standaloneDetail;
3501
3925
  }
@@ -3620,17 +4044,17 @@ async function listProjectRecords(projectsDir) {
3620
4044
  return records;
3621
4045
  }
3622
4046
  async function listAssignmentRecords(projectPath) {
3623
- const assignmentsDir = resolve9(projectPath, "assignments");
3624
- if (!await fileExists(assignmentsDir)) {
4047
+ const assignmentsDir2 = resolve9(projectPath, "assignments");
4048
+ if (!await fileExists(assignmentsDir2)) {
3625
4049
  return [];
3626
4050
  }
3627
- const entries = await readdir6(assignmentsDir, { withFileTypes: true });
4051
+ const entries = await readdir6(assignmentsDir2, { withFileTypes: true });
3628
4052
  const records = [];
3629
4053
  for (const entry of entries) {
3630
4054
  if (!entry.isDirectory()) {
3631
4055
  continue;
3632
4056
  }
3633
- const assignmentMd = resolve9(assignmentsDir, entry.name, "assignment.md");
4057
+ const assignmentMd = resolve9(assignmentsDir2, entry.name, "assignment.md");
3634
4058
  if (!await fileExists(assignmentMd)) {
3635
4059
  continue;
3636
4060
  }
@@ -4177,1236 +4601,1707 @@ var init_api = __esm({
4177
4601
  }
4178
4602
  });
4179
4603
 
4180
- // src/todos/parser.ts
4181
- var parser_exports = {};
4182
- __export(parser_exports, {
4183
- appendLogEntry: () => appendLogEntry2,
4184
- archivePath: () => archivePath,
4185
- checklistPath: () => checklistPath,
4186
- computeCounts: () => computeCounts,
4187
- generateShortId: () => generateShortId,
4188
- generateUniqueId: () => generateUniqueId,
4189
- logPath: () => logPath,
4190
- parseChecklist: () => parseChecklist,
4191
- parseChecklistItem: () => parseChecklistItem,
4192
- parseLog: () => parseLog,
4193
- readChecklist: () => readChecklist,
4194
- readLog: () => readLog,
4195
- serializeChecklist: () => serializeChecklist,
4196
- serializeChecklistItem: () => serializeChecklistItem,
4197
- serializeLogEntry: () => serializeLogEntry,
4198
- writeChecklist: () => writeChecklist
4199
- });
4200
- import { randomBytes } from "crypto";
4201
- import { readFile as readFile12 } from "fs/promises";
4202
- import { resolve as resolve15 } from "path";
4203
- function generateShortId() {
4204
- return randomBytes(2).toString("hex");
4205
- }
4206
- function generateUniqueId(existingIds) {
4207
- let id = generateShortId();
4208
- let attempts = 0;
4209
- while (existingIds.has(id) && attempts < 100) {
4210
- id = generateShortId();
4211
- attempts++;
4212
- }
4213
- return id;
4214
- }
4215
- function parseStatus2(marker) {
4216
- if (marker === " ") return { status: "open", session: null };
4217
- if (marker === "x") return { status: "completed", session: null };
4218
- if (marker === "!") return { status: "blocked", session: null };
4219
- if (marker.startsWith(">:")) return { status: "in_progress", session: marker.slice(2) };
4220
- if (marker === ">") return { status: "in_progress", session: null };
4221
- return { status: "open", session: null };
4604
+ // src/utils/slug.ts
4605
+ function slugify(title) {
4606
+ return title.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4222
4607
  }
4223
- function sanitizeSession(session) {
4224
- return session.replace(/[\[\]]/g, "");
4608
+ function isValidSlug(slug) {
4609
+ return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug);
4225
4610
  }
4226
- function statusToMarker(item) {
4227
- switch (item.status) {
4228
- case "open":
4229
- return " ";
4230
- case "completed":
4231
- return "x";
4232
- case "blocked":
4233
- return "!";
4234
- case "in_progress":
4235
- return item.session ? `>:${sanitizeSession(item.session)}` : ">";
4611
+ var init_slug = __esm({
4612
+ "src/utils/slug.ts"() {
4613
+ "use strict";
4236
4614
  }
4615
+ });
4616
+
4617
+ // src/utils/uuid.ts
4618
+ import { randomUUID } from "crypto";
4619
+ function generateId() {
4620
+ return randomUUID();
4237
4621
  }
4238
- function parseChecklistItem(line) {
4239
- const match = line.match(ITEM_REGEX);
4240
- if (!match) return null;
4241
- const marker = match[1];
4242
- const rest = match[2];
4243
- const { status, session } = parseStatus2(marker);
4244
- const idMatch = rest.match(ID_REGEX);
4245
- const id = idMatch ? idMatch[1] : "";
4246
- const tags = [];
4247
- let tagMatch;
4248
- const tagRegex = new RegExp(TAG_REGEX.source, "g");
4249
- while ((tagMatch = tagRegex.exec(rest)) !== null) {
4250
- tags.push(tagMatch[1]);
4251
- }
4252
- let description = rest;
4253
- const firstTagIdx = rest.search(/#[a-zA-Z0-9_-]/);
4254
- const firstIdIdx = rest.search(/\[t:[a-f0-9]{4}\]/);
4255
- const cutPoints = [firstTagIdx, firstIdIdx].filter((i) => i >= 0);
4256
- if (cutPoints.length > 0) {
4257
- description = rest.slice(0, Math.min(...cutPoints)).trim();
4258
- }
4259
- return { id, description, status, tags, session };
4260
- }
4261
- function serializeChecklistItem(item) {
4262
- const marker = statusToMarker(item);
4263
- const tagStr = item.tags.map((t) => `#${t}`).join(" ");
4264
- const parts = [`- [${marker}] ${item.description}`];
4265
- if (tagStr) parts.push(tagStr);
4266
- parts.push(`[t:${item.id}]`);
4267
- return parts.join(" ");
4268
- }
4269
- function parseChecklist(content) {
4270
- const [fm, body] = extractFrontmatter2(content);
4271
- const workspace = getField(fm, "workspace") || "_global";
4272
- const archiveIntervalRaw = getField(fm, "archiveInterval") || "weekly";
4273
- const archiveInterval = ["daily", "weekly", "monthly", "never"].includes(archiveIntervalRaw) ? archiveIntervalRaw : "weekly";
4274
- const items = [];
4275
- for (const line of body.split("\n")) {
4276
- const item = parseChecklistItem(line);
4277
- if (item) items.push(item);
4622
+ var init_uuid = __esm({
4623
+ "src/utils/uuid.ts"() {
4624
+ "use strict";
4278
4625
  }
4279
- return { workspace, archiveInterval, items };
4280
- }
4281
- function serializeChecklist(checklist) {
4282
- const fm = [
4283
- "---",
4284
- `workspace: ${checklist.workspace}`,
4285
- `archiveInterval: ${checklist.archiveInterval}`,
4286
- "---"
4287
- ].join("\n");
4288
- const header = "# Quick Todos";
4289
- const items = checklist.items.map(serializeChecklistItem).join("\n");
4290
- return `${fm}
4626
+ });
4291
4627
 
4292
- ${header}
4628
+ // src/templates/manifest.ts
4629
+ function renderManifest(params2) {
4630
+ return `---
4631
+ version: "2.0"
4632
+ project: ${params2.slug}
4633
+ generated: "${params2.timestamp}"
4634
+ ---
4293
4635
 
4294
- ${items}
4636
+ # Project: ${params2.slug}
4637
+
4638
+ ## Overview
4639
+ - [Project Overview](./project.md)
4640
+
4641
+ ## Indexes
4642
+ - [Assignments](./_index-assignments.md)
4643
+ - [Plans](./_index-plans.md)
4644
+ - [Decision Records](./_index-decisions.md)
4645
+ - [Status](./_status.md)
4646
+ - [Resources](./resources/_index.md)
4647
+ - [Memories](./memories/_index.md)
4295
4648
  `;
4296
4649
  }
4297
- function parseLog(content) {
4298
- const [fm, body] = extractFrontmatter2(content);
4299
- const workspace = getField(fm, "workspace") || "_global";
4300
- const entries = [];
4301
- const sections = body.split(/^### /m).filter((s) => s.match(/^\d{4}-/));
4302
- for (const section of sections) {
4303
- const lines = section.split("\n");
4304
- const heading = lines[0]?.trim() || "";
4305
- const headingMatch = heading.match(/^(\S+)\s*—?\s*(.*)/);
4306
- if (!headingMatch) continue;
4307
- const timestamp = headingMatch[1];
4308
- const idsPart = headingMatch[2] || "";
4309
- const itemIds = [...idsPart.matchAll(/t:([a-f0-9]{4})/g)].map((m) => m[1]);
4310
- const entry = {
4311
- timestamp,
4312
- itemIds,
4313
- items: "",
4314
- session: null,
4315
- branch: null,
4316
- summary: "",
4317
- blockers: null,
4318
- status: null
4319
- };
4320
- for (const line of lines.slice(1)) {
4321
- const fieldMatch = line.match(/^\*\*(\w+):\*\*\s*(.*)/);
4322
- if (!fieldMatch) continue;
4323
- const key = fieldMatch[1].toLowerCase();
4324
- const value = fieldMatch[2].trim();
4325
- switch (key) {
4326
- case "items":
4327
- entry.items = value;
4328
- break;
4329
- case "session":
4330
- entry.session = value;
4331
- break;
4332
- case "branch":
4333
- entry.branch = value;
4334
- break;
4335
- case "summary":
4336
- entry.summary = value;
4337
- break;
4338
- case "blockers":
4339
- entry.blockers = value;
4340
- break;
4341
- case "status":
4342
- entry.status = value;
4343
- break;
4344
- }
4345
- }
4346
- entries.push(entry);
4347
- }
4348
- return { workspace, entries };
4349
- }
4350
- function serializeLogEntry(entry) {
4351
- const idStr = entry.itemIds.map((id) => `t:${id}`).join(", ");
4352
- const lines = [`### ${entry.timestamp} \u2014 ${idStr}`];
4353
- if (entry.items) lines.push(`**Items:** ${entry.items}`);
4354
- if (entry.session) lines.push(`**Session:** ${entry.session}`);
4355
- if (entry.branch) lines.push(`**Branch:** ${entry.branch}`);
4356
- if (entry.summary) lines.push(`**Summary:** ${entry.summary}`);
4357
- if (entry.blockers) lines.push(`**Blockers:** ${entry.blockers}`);
4358
- if (entry.status) lines.push(`**Status:** ${entry.status}`);
4359
- return lines.join("\n");
4360
- }
4361
- function checklistPath(todosDir2, workspace) {
4362
- return resolve15(todosDir2, `${workspace}.md`);
4363
- }
4364
- function logPath(todosDir2, workspace) {
4365
- return resolve15(todosDir2, `${workspace}-log.md`);
4366
- }
4367
- function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
4368
- const year = now.getFullYear();
4369
- const month = String(now.getMonth() + 1).padStart(2, "0");
4370
- const day = String(now.getDate()).padStart(2, "0");
4371
- let suffix;
4372
- switch (interval) {
4373
- case "daily":
4374
- suffix = `${year}-${month}-${day}`;
4375
- break;
4376
- case "weekly": {
4377
- const jan1 = new Date(year, 0, 1);
4378
- const days = Math.floor((now.getTime() - jan1.getTime()) / 864e5);
4379
- const week = String(Math.ceil((days + jan1.getDay() + 1) / 7)).padStart(2, "0");
4380
- suffix = `${year}-W${week}`;
4381
- break;
4382
- }
4383
- case "monthly":
4384
- suffix = `${year}-${month}`;
4385
- break;
4386
- default:
4387
- suffix = `${year}-${month}-${day}`;
4650
+ var init_manifest = __esm({
4651
+ "src/templates/manifest.ts"() {
4652
+ "use strict";
4388
4653
  }
4389
- return resolve15(todosDir2, "archive", `${workspace}-${suffix}.md`);
4390
- }
4391
- async function readChecklist(todosDir2, workspace) {
4392
- const path = checklistPath(todosDir2, workspace);
4393
- if (!await fileExists(path)) {
4394
- return { workspace, archiveInterval: "weekly", items: [] };
4654
+ });
4655
+
4656
+ // src/utils/yaml.ts
4657
+ function escapeYamlString(value) {
4658
+ if (value.includes("\n") || value.includes("\r")) {
4659
+ throw new Error(
4660
+ `YAML string values must be single-line. Got: "${value.slice(0, 50)}..."`
4661
+ );
4395
4662
  }
4396
- const content = await readFile12(path, "utf-8");
4397
- return parseChecklist(content);
4398
- }
4399
- async function writeChecklist(todosDir2, checklist) {
4400
- await ensureDir(todosDir2);
4401
- const path = checklistPath(todosDir2, checklist.workspace);
4402
- await writeFileForce(path, serializeChecklist(checklist));
4663
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4664
+ return `"${escaped}"`;
4403
4665
  }
4404
- async function readLog(todosDir2, workspace) {
4405
- const path = logPath(todosDir2, workspace);
4406
- if (!await fileExists(path)) {
4407
- return { workspace, entries: [] };
4666
+ var init_yaml = __esm({
4667
+ "src/utils/yaml.ts"() {
4668
+ "use strict";
4408
4669
  }
4409
- const content = await readFile12(path, "utf-8");
4410
- return parseLog(content);
4411
- }
4412
- async function appendLogEntry2(todosDir2, workspace, entry) {
4413
- await ensureDir(todosDir2);
4414
- const path = logPath(todosDir2, workspace);
4415
- let content;
4416
- if (await fileExists(path)) {
4417
- content = await readFile12(path, "utf-8");
4418
- content = content.trimEnd() + "\n\n" + serializeLogEntry(entry) + "\n";
4419
- } else {
4420
- const fm = `---
4421
- workspace: ${workspace}
4670
+ });
4671
+
4672
+ // src/templates/project.ts
4673
+ function renderProject(params2) {
4674
+ const safeTitle = escapeYamlString(params2.title);
4675
+ const workspaceLine = params2.workspace ? `
4676
+ workspace: ${params2.workspace}` : "";
4677
+ return `---
4678
+ id: ${params2.id}
4679
+ slug: ${params2.slug}
4680
+ title: ${safeTitle}
4681
+ archived: false
4682
+ archivedAt: null
4683
+ archivedReason: null
4684
+ created: "${params2.timestamp}"
4685
+ updated: "${params2.timestamp}"
4686
+ externalIds: []
4687
+ tags: []${workspaceLine}
4422
4688
  ---
4423
4689
 
4424
- # Todo Log
4690
+ # ${params2.title}
4691
+
4692
+ ## Overview
4425
4693
 
4694
+ <!-- Describe the project goal, context, and success criteria here. -->
4695
+
4696
+ ## Notes
4697
+
4698
+ <!-- Optional human notes, updates, or context. -->
4426
4699
  `;
4427
- content = fm + serializeLogEntry(entry) + "\n";
4428
- }
4429
- await writeFileForce(path, content);
4430
4700
  }
4431
- function computeCounts(items) {
4432
- const counts = { open: 0, in_progress: 0, completed: 0, blocked: 0, total: items.length };
4433
- for (const item of items) {
4434
- counts[item.status]++;
4435
- }
4436
- return counts;
4437
- }
4438
- var ITEM_REGEX, ID_REGEX, TAG_REGEX;
4439
- var init_parser2 = __esm({
4440
- "src/todos/parser.ts"() {
4701
+ var init_project = __esm({
4702
+ "src/templates/project.ts"() {
4441
4703
  "use strict";
4442
- init_parser();
4443
- init_fs();
4444
- ITEM_REGEX = /^- \[([ x!]|>[^\]]*)\]\s+(.+)$/;
4445
- ID_REGEX = /\[t:([a-f0-9]{4})\]/;
4446
- TAG_REGEX = /#([a-zA-Z0-9_-]+)/g;
4704
+ init_yaml();
4447
4705
  }
4448
4706
  });
4449
4707
 
4450
- // src/dashboard/server.ts
4451
- init_paths();
4452
- init_api();
4453
- init_assignment_resolver();
4454
- import express from "express";
4455
- import { createServer } from "http";
4456
- import { resolve as resolve18 } from "path";
4457
- import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
4458
- import { WebSocketServer, WebSocket } from "ws";
4459
-
4460
- // src/dashboard/agent-sessions.ts
4461
- init_fs();
4462
- import { readFile as readFile9 } from "fs/promises";
4463
- import { resolve as resolve11 } from "path";
4464
-
4465
- // src/dashboard/session-db.ts
4466
- init_paths();
4467
- init_fs();
4468
- import Database from "better-sqlite3";
4469
- import { resolve as resolve10 } from "path";
4470
- import { readdir as readdir7 } from "fs/promises";
4471
- var db = null;
4472
- var SCHEMA_VERSION = "3";
4473
- var SCHEMA_SQL = `
4474
- CREATE TABLE IF NOT EXISTS sessions (
4475
- session_id TEXT PRIMARY KEY,
4476
- project_slug TEXT,
4477
- assignment_slug TEXT,
4478
- agent TEXT NOT NULL,
4479
- started TEXT NOT NULL,
4480
- ended TEXT,
4481
- status TEXT NOT NULL DEFAULT 'active',
4482
- path TEXT,
4483
- description TEXT,
4484
- transcript_path TEXT,
4485
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
4486
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4487
- );
4488
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
4489
- CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
4708
+ // src/templates/assignment.ts
4709
+ function renderAssignment(params2) {
4710
+ const safeTitle = escapeYamlString(params2.title);
4711
+ const dependsOnYaml = params2.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
4712
+ - ${params2.dependsOn.join("\n - ")}`;
4713
+ const linksYaml = params2.links.length === 0 ? "links: []" : `links:
4714
+ - ${params2.links.join("\n - ")}`;
4715
+ const projectYaml = `project: ${params2.project == null ? "null" : params2.project}`;
4716
+ const workspaceGroupLine = params2.workspaceGroup ? `
4717
+ workspaceGroup: ${params2.workspaceGroup}` : "";
4718
+ const typeYaml = `type: ${params2.type ?? "feature"}`;
4719
+ const todosSection = params2.includeTodos ? `## Todos
4720
+
4721
+ <!--
4722
+ Checklist of work items for this assignment. Items may be simple tasks
4723
+ or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
4724
+ When a plan is superseded by a new one, mark the old todo as:
4725
+ - [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
4726
+ Never delete superseded todos \u2014 preserve the history.
4727
+ -->
4728
+
4729
+ ` : "";
4730
+ return `---
4731
+ id: ${params2.id}
4732
+ slug: ${params2.slug}
4733
+ title: ${safeTitle}
4734
+ ${projectYaml}${workspaceGroupLine}
4735
+ ${typeYaml}
4736
+ status: pending
4737
+ priority: ${params2.priority}
4738
+ created: "${params2.timestamp}"
4739
+ updated: "${params2.timestamp}"
4740
+ assignee: null
4741
+ externalIds: []
4742
+ ${dependsOnYaml}
4743
+ ${linksYaml}
4744
+ blockedReason: null
4745
+ workspace:
4746
+ repository: null
4747
+ worktreePath: null
4748
+ branch: null
4749
+ parentBranch: null
4750
+ tags: []
4751
+ ---
4752
+
4753
+ # ${params2.title}
4754
+
4755
+ ## Objective
4756
+
4757
+ <!-- Clear description of what needs to be done and why. -->
4758
+
4759
+ ## Acceptance Criteria
4760
+
4761
+ - [ ] <!-- criterion 1 -->
4762
+ - [ ] <!-- criterion 2 -->
4763
+ - [ ] <!-- criterion 3 -->
4764
+
4765
+ ${todosSection}## Context
4766
+
4767
+ <!-- Links to relevant docs, code, or other assignments. -->
4768
+
4769
+ ## Links
4770
+
4771
+ - [Progress](./progress.md)
4772
+ - [Comments](./comments.md)
4773
+ - [Scratchpad](./scratchpad.md)
4774
+ - [Handoff](./handoff.md)
4775
+ - [Decision Record](./decision-record.md)
4490
4776
  `;
4491
- var POST_MIGRATION_INDEXES_SQL = `
4492
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
4493
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
4777
+ }
4778
+ var init_assignment = __esm({
4779
+ "src/templates/assignment.ts"() {
4780
+ "use strict";
4781
+ init_yaml();
4782
+ }
4783
+ });
4784
+
4785
+ // src/templates/plan.ts
4786
+ var init_plan = __esm({
4787
+ "src/templates/plan.ts"() {
4788
+ "use strict";
4789
+ }
4790
+ });
4791
+
4792
+ // src/templates/scratchpad.ts
4793
+ function renderScratchpad(params2) {
4794
+ return `---
4795
+ assignment: ${params2.assignmentSlug}
4796
+ updated: "${params2.timestamp}"
4797
+ ---
4798
+
4799
+ # Scratchpad
4800
+
4801
+ No working notes yet.
4494
4802
  `;
4495
- function initSessionDb(dbPath) {
4496
- if (db) return db;
4497
- const finalPath = dbPath ?? resolve10(syntaurRoot(), "syntaur.db");
4498
- db = new Database(finalPath);
4499
- db.pragma("journal_mode = WAL");
4500
- db.exec(SCHEMA_SQL);
4501
- db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
4502
- "schema_version",
4503
- SCHEMA_VERSION
4504
- );
4505
- const database = db;
4506
- const runMigrations = database.transaction(() => {
4507
- const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
4508
- if (vBeforeV2 === "1") {
4509
- database.exec(`
4510
- CREATE TABLE sessions_v2 (
4511
- session_id TEXT PRIMARY KEY,
4512
- project_slug TEXT,
4513
- assignment_slug TEXT,
4514
- agent TEXT NOT NULL,
4515
- started TEXT NOT NULL,
4516
- ended TEXT,
4517
- status TEXT NOT NULL DEFAULT 'active',
4518
- path TEXT,
4519
- description TEXT,
4520
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
4521
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4522
- );
4523
- INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
4524
- DROP TABLE sessions;
4525
- ALTER TABLE sessions_v2 RENAME TO sessions;
4526
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
4527
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
4528
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
4529
- UPDATE meta SET value = '2' WHERE key = 'schema_version';
4530
- `);
4531
- }
4532
- const vBeforeV3 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
4533
- if (vBeforeV3 === "2") {
4534
- const v2Columns = database.prepare("PRAGMA table_info(sessions)").all();
4535
- const v2ColNames = v2Columns.map((c) => c.name);
4536
- const hasProject = v2ColNames.includes("project_slug");
4537
- const hasMission = v2ColNames.includes("mission_slug");
4538
- const projectSlugExpr = hasProject && hasMission ? "COALESCE(project_slug, mission_slug)" : hasProject ? "project_slug" : hasMission ? "mission_slug" : null;
4539
- if (!projectSlugExpr) {
4540
- throw new Error(
4541
- "sessions table has neither project_slug nor mission_slug; cannot migrate from v2 to v3"
4542
- );
4543
- }
4544
- database.exec(`
4545
- CREATE TABLE sessions_v3 (
4546
- session_id TEXT PRIMARY KEY,
4547
- project_slug TEXT,
4548
- assignment_slug TEXT,
4549
- agent TEXT NOT NULL,
4550
- started TEXT NOT NULL,
4551
- ended TEXT,
4552
- status TEXT NOT NULL DEFAULT 'active',
4553
- path TEXT,
4554
- description TEXT,
4555
- transcript_path TEXT,
4556
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
4557
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4558
- );
4559
- INSERT INTO sessions_v3
4560
- SELECT session_id, ${projectSlugExpr}, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at
4561
- FROM sessions;
4562
- DROP TABLE sessions;
4563
- ALTER TABLE sessions_v3 RENAME TO sessions;
4564
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
4565
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
4566
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
4567
- UPDATE meta SET value = '3' WHERE key = 'schema_version';
4568
- `);
4569
- }
4570
- });
4571
- runMigrations.exclusive();
4572
- db.exec(POST_MIGRATION_INDEXES_SQL);
4573
- return db;
4574
4803
  }
4575
- function getSessionDb() {
4576
- if (!db) {
4577
- throw new Error(
4578
- "Session database not initialized. Call initSessionDb() first."
4579
- );
4804
+ var init_scratchpad = __esm({
4805
+ "src/templates/scratchpad.ts"() {
4806
+ "use strict";
4580
4807
  }
4581
- return db;
4808
+ });
4809
+
4810
+ // src/templates/handoff.ts
4811
+ function renderHandoff(params2) {
4812
+ return `---
4813
+ assignment: ${params2.assignmentSlug}
4814
+ updated: "${params2.timestamp}"
4815
+ handoffCount: 0
4816
+ ---
4817
+
4818
+ # Handoff Log
4819
+
4820
+ No handoffs recorded yet.
4821
+ `;
4582
4822
  }
4583
- function closeSessionDb() {
4584
- if (db) {
4585
- db.close();
4586
- db = null;
4823
+ var init_handoff = __esm({
4824
+ "src/templates/handoff.ts"() {
4825
+ "use strict";
4587
4826
  }
4827
+ });
4828
+
4829
+ // src/templates/progress.ts
4830
+ function renderProgress(params2) {
4831
+ return `---
4832
+ assignment: ${params2.assignment}
4833
+ entryCount: 0
4834
+ generated: "${params2.timestamp}"
4835
+ updated: "${params2.timestamp}"
4836
+ ---
4837
+
4838
+ # Progress
4839
+
4840
+ No progress yet.
4841
+ `;
4588
4842
  }
4589
- async function migrateFromMarkdown(projectsDir) {
4590
- const database = getSessionDb();
4591
- const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
4592
- if (count.count > 0) return 0;
4593
- if (!await fileExists(projectsDir)) return 0;
4594
- const entries = await readdir7(projectsDir, { withFileTypes: true });
4595
- const allSessions = [];
4596
- for (const entry of entries) {
4597
- if (!entry.isDirectory()) continue;
4598
- const projectDir = resolve10(projectsDir, entry.name);
4599
- const indexPath = resolve10(projectDir, "_index-sessions.md");
4600
- if (!await fileExists(indexPath)) continue;
4601
- const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
4602
- allSessions.push(...sessions);
4843
+ var init_progress = __esm({
4844
+ "src/templates/progress.ts"() {
4845
+ "use strict";
4603
4846
  }
4604
- if (allSessions.length === 0) return 0;
4605
- const insert = database.prepare(`
4606
- INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
4607
- VALUES (?, ?, ?, ?, ?, ?, ?)
4608
- `);
4609
- const insertAll = database.transaction((sessions) => {
4610
- for (const s of sessions) {
4611
- insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
4612
- }
4613
- });
4614
- insertAll(allSessions);
4615
- console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);
4616
- return allSessions.length;
4847
+ });
4848
+
4849
+ // src/templates/comments.ts
4850
+ function renderComments(params2) {
4851
+ return `---
4852
+ assignment: ${params2.assignment}
4853
+ entryCount: 0
4854
+ generated: "${params2.timestamp}"
4855
+ updated: "${params2.timestamp}"
4856
+ ---
4857
+
4858
+ # Comments
4859
+
4860
+ No comments yet.
4861
+ `;
4617
4862
  }
4618
- async function parseMarkdownSessionsIndex(filePath, projectSlug) {
4619
- const { readFile: readFile15 } = await import("fs/promises");
4620
- const raw = await readFile15(filePath, "utf-8");
4621
- const sessions = [];
4622
- const lines = raw.split("\n");
4623
- let inTable = false;
4624
- let headerSeen = false;
4625
- for (const line of lines) {
4626
- const trimmed = line.trim();
4627
- if (!trimmed) continue;
4628
- if (trimmed.startsWith("| Assignment") || trimmed.startsWith("|Assignment")) {
4629
- inTable = true;
4630
- headerSeen = false;
4631
- continue;
4632
- }
4633
- if (inTable && !headerSeen && trimmed.match(/^\|[-\s|]+\|$/)) {
4634
- headerSeen = true;
4635
- continue;
4636
- }
4637
- if (inTable && headerSeen && trimmed.startsWith("|")) {
4638
- const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
4639
- if (cells.length >= 6) {
4640
- sessions.push({
4641
- assignmentSlug: cells[0],
4642
- agent: cells[1],
4643
- sessionId: cells[2],
4644
- started: cells[3],
4645
- status: cells[4] || "active",
4646
- path: cells[5],
4647
- projectSlug
4648
- });
4649
- }
4650
- }
4863
+ function formatCommentEntry(comment) {
4864
+ const lines = [];
4865
+ lines.push(`## ${comment.id}`);
4866
+ lines.push("");
4867
+ lines.push(`**Recorded:** ${comment.timestamp}`);
4868
+ lines.push(`**Author:** ${comment.author}`);
4869
+ lines.push(`**Type:** ${comment.type}`);
4870
+ if (comment.replyTo) {
4871
+ lines.push(`**Reply to:** ${comment.replyTo}`);
4651
4872
  }
4652
- return sessions;
4873
+ if (comment.type === "question") {
4874
+ lines.push(`**Resolved:** ${comment.resolved ? "true" : "false"}`);
4875
+ }
4876
+ lines.push("");
4877
+ lines.push(comment.body.trim());
4878
+ lines.push("");
4879
+ return lines.join("\n");
4653
4880
  }
4881
+ var init_comments = __esm({
4882
+ "src/templates/comments.ts"() {
4883
+ "use strict";
4884
+ }
4885
+ });
4654
4886
 
4655
- // src/dashboard/agent-sessions.ts
4656
- function rowToSession(row) {
4657
- return {
4658
- sessionId: row.session_id,
4659
- projectSlug: row.project_slug ?? null,
4660
- assignmentSlug: row.assignment_slug ?? null,
4661
- agent: row.agent,
4662
- started: row.started,
4663
- ended: row.ended ?? null,
4664
- status: row.status,
4665
- path: row.path ?? "",
4666
- description: row.description ?? null,
4667
- transcriptPath: row.transcript_path ?? null
4668
- };
4669
- }
4670
- async function appendSession(_projectDir, session) {
4671
- const db2 = getSessionDb();
4672
- db2.prepare(`
4673
- INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
4674
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
4675
- ON CONFLICT(session_id) DO UPDATE SET
4676
- project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
4677
- assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
4678
- agent = excluded.agent,
4679
- status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
4680
- path = COALESCE(NULLIF(excluded.path, ''), path),
4681
- description = COALESCE(NULLIF(excluded.description, ''), description),
4682
- transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
4683
- updated_at = datetime('now')
4684
- `).run(
4685
- session.sessionId,
4686
- session.projectSlug ?? null,
4687
- session.assignmentSlug ?? null,
4688
- session.agent,
4689
- session.started,
4690
- session.status,
4691
- session.path,
4692
- session.description ?? null,
4693
- session.transcriptPath ?? null
4694
- );
4695
- }
4696
- async function updateSessionStatus(_projectDir, sessionId, status) {
4697
- const db2 = getSessionDb();
4698
- const isTerminal = status === "completed" || status === "stopped";
4699
- const result = isTerminal ? db2.prepare(
4700
- "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
4701
- ).run(status, sessionId) : db2.prepare(
4702
- "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
4703
- ).run(status, sessionId);
4704
- return result.changes > 0;
4705
- }
4706
- async function listAllSessions(_projectsDir) {
4707
- const db2 = getSessionDb();
4708
- const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
4709
- return rows.map(rowToSession);
4887
+ // src/templates/decision-record.ts
4888
+ function renderDecisionRecord(params2) {
4889
+ return `---
4890
+ assignment: ${params2.assignmentSlug}
4891
+ updated: "${params2.timestamp}"
4892
+ decisionCount: 0
4893
+ ---
4894
+
4895
+ # Decision Record
4896
+
4897
+ No decisions recorded yet.
4898
+ `;
4710
4899
  }
4711
- async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
4712
- const db2 = getSessionDb();
4713
- if (assignmentSlug) {
4714
- const rows2 = db2.prepare(
4715
- "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
4716
- ).all(projectSlug, assignmentSlug);
4717
- return rows2.map(rowToSession);
4900
+ var init_decision_record = __esm({
4901
+ "src/templates/decision-record.ts"() {
4902
+ "use strict";
4718
4903
  }
4719
- const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
4720
- return rows.map(rowToSession);
4904
+ });
4905
+
4906
+ // src/templates/index-stubs.ts
4907
+ function renderIndexAssignments(params2) {
4908
+ return `---
4909
+ project: ${params2.slug}
4910
+ generated: "${params2.timestamp}"
4911
+ total: 0
4912
+ by_status:
4913
+ pending: 0
4914
+ in_progress: 0
4915
+ blocked: 0
4916
+ review: 0
4917
+ completed: 0
4918
+ failed: 0
4919
+ ---
4920
+
4921
+ # Assignments
4922
+
4923
+ | Slug | Title | Status | Priority | Assignee | Dependencies | Updated |
4924
+ |------|-------|--------|----------|----------|--------------|---------|
4925
+ `;
4721
4926
  }
4722
- async function deleteSessions(sessionIds) {
4723
- if (sessionIds.length === 0) return 0;
4724
- const db2 = getSessionDb();
4725
- const placeholders = sessionIds.map(() => "?").join(", ");
4726
- const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
4727
- return result.changes;
4927
+ function renderIndexPlans(params2) {
4928
+ return `---
4929
+ project: ${params2.slug}
4930
+ generated: "${params2.timestamp}"
4931
+ ---
4932
+
4933
+ # Plans
4934
+
4935
+ | Assignment | Plan Status | Updated |
4936
+ |------------|-------------|---------|
4937
+ `;
4728
4938
  }
4729
- var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
4730
- async function readAssignmentStatusFromPath(assignmentMdPath) {
4731
- if (!await fileExists(assignmentMdPath)) return null;
4732
- const raw = await readFile9(assignmentMdPath, "utf-8");
4733
- const match = raw.match(/^status:\s*(.+)$/m);
4734
- return match ? match[1].trim() : null;
4939
+ function renderIndexDecisions(params2) {
4940
+ return `---
4941
+ project: ${params2.slug}
4942
+ generated: "${params2.timestamp}"
4943
+ ---
4944
+
4945
+ # Decision Records
4946
+
4947
+ | Assignment | Count | Latest Decision | Latest Status | Updated |
4948
+ |------------|-------|-----------------|---------------|---------|
4949
+ `;
4735
4950
  }
4736
- async function readAssignmentStatus(projectDir, assignmentSlug) {
4737
- return readAssignmentStatusFromPath(
4738
- resolve11(projectDir, "assignments", assignmentSlug, "assignment.md")
4739
- );
4951
+ function renderStatus(params2) {
4952
+ return `---
4953
+ project: ${params2.slug}
4954
+ generated: "${params2.timestamp}"
4955
+ status: pending
4956
+ progress:
4957
+ total: 0
4958
+ completed: 0
4959
+ in_progress: 0
4960
+ blocked: 0
4961
+ pending: 0
4962
+ review: 0
4963
+ failed: 0
4964
+ needsAttention:
4965
+ blockedCount: 0
4966
+ failedCount: 0
4967
+ openQuestions: 0
4968
+ ---
4969
+
4970
+ # Project Status: ${params2.title}
4971
+
4972
+ **Status:** pending
4973
+ **Progress:** 0/0 assignments complete
4974
+
4975
+ ## Assignments
4976
+
4977
+ No assignments yet.
4978
+
4979
+ ## Dependency Graph
4980
+
4981
+ No dependencies yet.
4982
+
4983
+ ## Needs Attention
4984
+
4985
+ - **0 blocked** assignments
4986
+ - **0 failed** assignments
4987
+ - **0 unanswered** questions
4988
+ `;
4740
4989
  }
4741
- async function reconcileActiveSessions(projectsDir, assignmentsDir) {
4742
- const db2 = getSessionDb();
4743
- const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
4744
- if (activeSessions.length === 0) return 0;
4745
- const assignmentStatuses = /* @__PURE__ */ new Map();
4746
- const seen = /* @__PURE__ */ new Set();
4747
- for (const session of activeSessions) {
4748
- const aslug = session.assignment_slug;
4749
- if (!aslug) continue;
4750
- const projectKey = session.project_slug ?? "__standalone__";
4751
- const key = `${projectKey}/${aslug}`;
4752
- if (seen.has(key)) continue;
4753
- seen.add(key);
4754
- if (session.project_slug) {
4755
- const status = await readAssignmentStatus(
4756
- resolve11(projectsDir, session.project_slug),
4757
- aslug
4758
- );
4759
- if (status) assignmentStatuses.set(key, status);
4760
- } else if (assignmentsDir) {
4761
- const status = await readAssignmentStatusFromPath(
4762
- resolve11(assignmentsDir, aslug, "assignment.md")
4763
- );
4764
- if (status) assignmentStatuses.set(key, status);
4765
- }
4766
- }
4767
- let totalUpdated = 0;
4768
- for (const session of activeSessions) {
4769
- const projectKey = session.project_slug ?? "__standalone__";
4770
- const key = `${projectKey}/${session.assignment_slug}`;
4771
- const assignmentStatus = assignmentStatuses.get(key);
4772
- if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
4773
- const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
4774
- await updateSessionStatus("", session.session_id, newStatus);
4775
- totalUpdated++;
4776
- }
4777
- return totalUpdated;
4990
+ function renderResourcesIndex(params2) {
4991
+ return `---
4992
+ project: ${params2.slug}
4993
+ generated: "${params2.timestamp}"
4994
+ total: 0
4995
+ ---
4996
+
4997
+ # Resources
4998
+
4999
+ | Name | Category | Source | Related Assignments | Updated |
5000
+ |------|----------|--------|---------------------|---------|
5001
+ `;
4778
5002
  }
4779
- async function listSessionsByAssignment(projectSlug, assignmentSlug) {
4780
- const db2 = getSessionDb();
4781
- const rows = projectSlug === null ? db2.prepare(
4782
- "SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
4783
- ).all(assignmentSlug) : db2.prepare(
4784
- "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
4785
- ).all(projectSlug, assignmentSlug);
4786
- return rows.map(rowToSession);
5003
+ function renderMemoriesIndex(params2) {
5004
+ return `---
5005
+ project: ${params2.slug}
5006
+ generated: "${params2.timestamp}"
5007
+ total: 0
5008
+ ---
5009
+
5010
+ # Memories
5011
+
5012
+ | Name | Source | Scope | Source Assignment | Updated |
5013
+ |------|--------|-------|------------------|---------|
5014
+ `;
4787
5015
  }
5016
+ var init_index_stubs = __esm({
5017
+ "src/templates/index-stubs.ts"() {
5018
+ "use strict";
5019
+ }
5020
+ });
4788
5021
 
4789
- // src/dashboard/watcher.ts
4790
- import { watch } from "chokidar";
4791
- import { relative, sep } from "path";
4792
- function createWatcher(options) {
4793
- const { projectsDir, assignmentsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
4794
- const pendingEvents = /* @__PURE__ */ new Map();
4795
- const projectsWatcher = watch(projectsDir, {
4796
- ignoreInitial: true,
4797
- persistent: true,
4798
- depth: 10,
4799
- ignored: /(^|[\/\\])\../
4800
- });
4801
- function handleProjectChange(filePath) {
4802
- const rel = relative(projectsDir, filePath);
4803
- const parts = rel.split(sep);
4804
- if (parts.length === 0) return;
4805
- const projectSlug = parts[0];
4806
- let assignmentSlug;
4807
- let isProjectTodos = false;
4808
- if (parts.length >= 3 && parts[1] === "assignments") {
4809
- assignmentSlug = parts[2];
4810
- } else if (parts.length >= 2 && parts[1] === "todos") {
4811
- isProjectTodos = true;
4812
- }
4813
- const debounceKey = isProjectTodos ? `todos:${projectSlug}` : assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
4814
- const existing = pendingEvents.get(debounceKey);
4815
- if (existing) clearTimeout(existing);
4816
- const messageType = isProjectTodos ? "todos-updated" : assignmentSlug ? "assignment-updated" : "project-updated";
4817
- pendingEvents.set(
4818
- debounceKey,
4819
- setTimeout(() => {
4820
- pendingEvents.delete(debounceKey);
4821
- const message = isProjectTodos ? {
4822
- type: "todos-updated",
4823
- projectSlug,
4824
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4825
- } : {
4826
- type: messageType,
4827
- projectSlug,
4828
- assignmentSlug,
4829
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4830
- };
4831
- onMessage(message);
4832
- }, debounceMs)
4833
- );
5022
+ // src/templates/playbook.ts
5023
+ function renderPlaybook(params2) {
5024
+ const whenToUse = params2.whenToUse ? escapeYamlString(params2.whenToUse) : "null";
5025
+ return `---
5026
+ name: ${escapeYamlString(params2.name)}
5027
+ slug: ${params2.slug}
5028
+ description: ${escapeYamlString(params2.description)}
5029
+ when_to_use: ${whenToUse}
5030
+ created: "${params2.timestamp}"
5031
+ updated: "${params2.timestamp}"
5032
+ tags: []
5033
+ ---
5034
+
5035
+ # ${params2.name}
5036
+
5037
+ <!-- Write imperative rules and workflows here. Keep it under 50 lines. -->
5038
+ `;
5039
+ }
5040
+ var init_playbook = __esm({
5041
+ "src/templates/playbook.ts"() {
5042
+ "use strict";
5043
+ init_yaml();
4834
5044
  }
4835
- projectsWatcher.on("change", handleProjectChange);
4836
- projectsWatcher.on("add", handleProjectChange);
4837
- projectsWatcher.on("unlink", handleProjectChange);
4838
- let standaloneWatcher = null;
4839
- if (assignmentsDir) {
4840
- let handleStandaloneChange2 = function(filePath) {
4841
- const rel = relative(assignmentsDir, filePath);
4842
- const parts = rel.split(sep);
4843
- if (parts.length === 0) return;
4844
- const assignmentId = parts[0];
4845
- if (!assignmentId) return;
4846
- const debounceKey = `__standalone__/${assignmentId}`;
4847
- const existing = pendingEvents.get(debounceKey);
4848
- if (existing) clearTimeout(existing);
4849
- pendingEvents.set(
4850
- debounceKey,
4851
- setTimeout(() => {
4852
- pendingEvents.delete(debounceKey);
4853
- const message = {
4854
- type: "assignment-updated",
4855
- projectSlug: null,
4856
- assignmentSlug: assignmentId,
4857
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4858
- };
4859
- onMessage(message);
4860
- }, debounceMs)
4861
- );
4862
- };
4863
- var handleStandaloneChange = handleStandaloneChange2;
4864
- standaloneWatcher = watch(assignmentsDir, {
4865
- ignoreInitial: true,
4866
- persistent: true,
4867
- depth: 5,
4868
- ignored: /(^|[\/\\])\../
4869
- });
4870
- standaloneWatcher.on("change", handleStandaloneChange2);
4871
- standaloneWatcher.on("add", handleStandaloneChange2);
4872
- standaloneWatcher.on("unlink", handleStandaloneChange2);
5045
+ });
5046
+
5047
+ // src/templates/cursor-rules.ts
5048
+ var init_cursor_rules = __esm({
5049
+ "src/templates/cursor-rules.ts"() {
5050
+ "use strict";
4873
5051
  }
4874
- let serversWatcher = null;
4875
- if (serversDir2) {
4876
- let handleServerChange2 = function() {
4877
- const debounceKey = "__servers__";
4878
- const existing = pendingEvents.get(debounceKey);
4879
- if (existing) clearTimeout(existing);
4880
- pendingEvents.set(
4881
- debounceKey,
4882
- setTimeout(() => {
4883
- pendingEvents.delete(debounceKey);
4884
- const message = {
4885
- type: "servers-updated",
4886
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4887
- };
4888
- onMessage(message);
4889
- }, debounceMs)
4890
- );
4891
- };
4892
- var handleServerChange = handleServerChange2;
4893
- serversWatcher = watch(serversDir2, {
4894
- ignoreInitial: true,
4895
- persistent: true,
4896
- depth: 1,
4897
- ignored: /(^|[\/\\])\../
4898
- });
4899
- serversWatcher.on("change", handleServerChange2);
4900
- serversWatcher.on("add", handleServerChange2);
4901
- serversWatcher.on("unlink", handleServerChange2);
5052
+ });
5053
+
5054
+ // src/templates/codex-agents.ts
5055
+ var init_codex_agents = __esm({
5056
+ "src/templates/codex-agents.ts"() {
5057
+ "use strict";
4902
5058
  }
4903
- let playbooksWatcher = null;
4904
- if (playbooksDir2) {
4905
- let handlePlaybookChange2 = function() {
4906
- const debounceKey = "__playbooks__";
4907
- const existing = pendingEvents.get(debounceKey);
4908
- if (existing) clearTimeout(existing);
4909
- pendingEvents.set(
4910
- debounceKey,
4911
- setTimeout(() => {
4912
- pendingEvents.delete(debounceKey);
4913
- const message = {
4914
- type: "playbooks-updated",
4915
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4916
- };
4917
- onMessage(message);
4918
- }, debounceMs)
4919
- );
4920
- };
4921
- var handlePlaybookChange = handlePlaybookChange2;
4922
- playbooksWatcher = watch(playbooksDir2, {
4923
- ignoreInitial: true,
4924
- persistent: true,
4925
- depth: 1,
4926
- ignored: /(^|[\/\\])\../
4927
- });
4928
- playbooksWatcher.on("change", handlePlaybookChange2);
4929
- playbooksWatcher.on("add", handlePlaybookChange2);
4930
- playbooksWatcher.on("unlink", handlePlaybookChange2);
5059
+ });
5060
+
5061
+ // src/templates/opencode-config.ts
5062
+ var init_opencode_config = __esm({
5063
+ "src/templates/opencode-config.ts"() {
5064
+ "use strict";
4931
5065
  }
4932
- let todosWatcher = null;
4933
- if (todosDir2) {
4934
- let handleTodoChange2 = function() {
4935
- const debounceKey = "__todos__";
4936
- const existing = pendingEvents.get(debounceKey);
4937
- if (existing) clearTimeout(existing);
4938
- pendingEvents.set(
4939
- debounceKey,
4940
- setTimeout(() => {
4941
- pendingEvents.delete(debounceKey);
4942
- const message = {
4943
- type: "todos-updated",
4944
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4945
- };
4946
- onMessage(message);
4947
- }, debounceMs)
4948
- );
4949
- };
4950
- var handleTodoChange = handleTodoChange2;
4951
- todosWatcher = watch(todosDir2, {
4952
- ignoreInitial: true,
4953
- persistent: true,
4954
- depth: 1,
4955
- ignored: /(^|[\/\\])\../
4956
- });
4957
- todosWatcher.on("change", handleTodoChange2);
4958
- todosWatcher.on("add", handleTodoChange2);
4959
- todosWatcher.on("unlink", handleTodoChange2);
5066
+ });
5067
+
5068
+ // src/templates/index.ts
5069
+ var init_templates = __esm({
5070
+ "src/templates/index.ts"() {
5071
+ "use strict";
5072
+ init_config();
5073
+ init_manifest();
5074
+ init_project();
5075
+ init_assignment();
5076
+ init_plan();
5077
+ init_scratchpad();
5078
+ init_handoff();
5079
+ init_progress();
5080
+ init_comments();
5081
+ init_decision_record();
5082
+ init_index_stubs();
5083
+ init_playbook();
5084
+ init_cursor_rules();
5085
+ init_codex_agents();
5086
+ init_opencode_config();
4960
5087
  }
4961
- return {
4962
- close: async () => {
4963
- pendingEvents.forEach((timeout) => {
4964
- clearTimeout(timeout);
4965
- });
4966
- pendingEvents.clear();
4967
- await projectsWatcher.close();
4968
- if (standaloneWatcher) await standaloneWatcher.close();
4969
- if (serversWatcher) await serversWatcher.close();
4970
- if (playbooksWatcher) await playbooksWatcher.close();
4971
- if (todosWatcher) await todosWatcher.close();
4972
- }
4973
- };
4974
- }
4975
-
4976
- // src/dashboard/server.ts
4977
- init_fs();
4978
- init_config2();
4979
-
4980
- // src/dashboard/api-write.ts
4981
- init_lifecycle();
4982
- import { Router } from "express";
4983
- import { resolve as resolve12 } from "path";
4984
- import { rm, readFile as readFile10 } from "fs/promises";
5088
+ });
4985
5089
 
4986
- // src/utils/slug.ts
4987
- function isValidSlug(slug) {
4988
- return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug);
5090
+ // src/todos/parser.ts
5091
+ var parser_exports = {};
5092
+ __export(parser_exports, {
5093
+ appendLogEntry: () => appendLogEntry2,
5094
+ archivePath: () => archivePath,
5095
+ checklistPath: () => checklistPath,
5096
+ computeCounts: () => computeCounts,
5097
+ decodeMetaValue: () => decodeMetaValue,
5098
+ encodeMetaValue: () => encodeMetaValue,
5099
+ generateShortId: () => generateShortId,
5100
+ generateUniqueId: () => generateUniqueId,
5101
+ logPath: () => logPath,
5102
+ parseChecklist: () => parseChecklist,
5103
+ parseChecklistItem: () => parseChecklistItem,
5104
+ parseLog: () => parseLog,
5105
+ parseMetaToken: () => parseMetaToken,
5106
+ readChecklist: () => readChecklist,
5107
+ readLog: () => readLog,
5108
+ serializeChecklist: () => serializeChecklist,
5109
+ serializeChecklistItem: () => serializeChecklistItem,
5110
+ serializeLogEntry: () => serializeLogEntry,
5111
+ serializeMetaToken: () => serializeMetaToken,
5112
+ writeChecklist: () => writeChecklist
5113
+ });
5114
+ import { randomBytes } from "crypto";
5115
+ import { readFile as readFile12 } from "fs/promises";
5116
+ import { resolve as resolve15 } from "path";
5117
+ function generateShortId() {
5118
+ return randomBytes(2).toString("hex");
4989
5119
  }
4990
-
4991
- // src/utils/uuid.ts
4992
- import { randomUUID } from "crypto";
4993
- function generateId() {
4994
- return randomUUID();
5120
+ function generateUniqueId(existingIds) {
5121
+ let id = generateShortId();
5122
+ let attempts = 0;
5123
+ while (existingIds.has(id) && attempts < 100) {
5124
+ id = generateShortId();
5125
+ attempts++;
5126
+ }
5127
+ return id;
4995
5128
  }
4996
-
4997
- // src/dashboard/api-write.ts
4998
- init_timestamp();
4999
- init_fs();
5000
- init_parser();
5001
-
5002
- // src/dashboard/acceptance-criteria.ts
5003
- function splitFrontmatter(content) {
5004
- const match = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/);
5005
- if (!match) {
5006
- return { prefix: "", body: content };
5129
+ function encodeMetaValue(value) {
5130
+ let out = "";
5131
+ for (const ch of value) {
5132
+ if (META_ENCODE_CHARS.includes(ch)) {
5133
+ out += "%" + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
5134
+ } else {
5135
+ out += ch;
5136
+ }
5007
5137
  }
5008
- return {
5009
- prefix: match[1],
5010
- body: match[2]
5011
- };
5138
+ return out;
5012
5139
  }
5013
- function toggleAcceptanceCriterion(content, index, checked) {
5014
- if (!Number.isInteger(index) || index < 0) {
5015
- return { error: "acceptance criteria index must be a non-negative integer" };
5140
+ function decodeMetaValue(value) {
5141
+ return value.replace(
5142
+ /%([0-9A-Fa-f]{2})/g,
5143
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
5144
+ );
5145
+ }
5146
+ function emptyMetaFields() {
5147
+ return { branch: null, worktreePath: null, createdAt: null, updatedAt: null, planDir: null };
5148
+ }
5149
+ function parseMetaToken(line) {
5150
+ const match = line.match(META_TOKEN_REGEX);
5151
+ if (!match) return emptyMetaFields();
5152
+ const body = match[1];
5153
+ if (!body) return emptyMetaFields();
5154
+ const fields = emptyMetaFields();
5155
+ for (const pair of body.split(";")) {
5156
+ const trimmed = pair.trim();
5157
+ if (!trimmed) continue;
5158
+ const eq = trimmed.indexOf("=");
5159
+ if (eq < 0) continue;
5160
+ const key = trimmed.slice(0, eq).trim();
5161
+ const rawValue = trimmed.slice(eq + 1);
5162
+ const value = decodeMetaValue(rawValue);
5163
+ switch (key) {
5164
+ case "b":
5165
+ fields.branch = value;
5166
+ break;
5167
+ case "w":
5168
+ fields.worktreePath = value;
5169
+ break;
5170
+ case "c":
5171
+ fields.createdAt = value;
5172
+ break;
5173
+ case "u":
5174
+ fields.updatedAt = value;
5175
+ break;
5176
+ case "p":
5177
+ fields.planDir = value;
5178
+ break;
5179
+ }
5016
5180
  }
5017
- const { prefix, body } = splitFrontmatter(content);
5018
- const lines = body.split("\n");
5019
- const sectionStart = lines.findIndex((line) => /^##\s+Acceptance Criteria\s*$/i.test(line.trim()));
5020
- if (sectionStart === -1) {
5021
- return { error: "Acceptance Criteria section not found." };
5181
+ return fields;
5182
+ }
5183
+ function serializeMetaToken(item) {
5184
+ const pairs = [];
5185
+ if (item.branch !== null) pairs.push(`b=${encodeMetaValue(item.branch)}`);
5186
+ if (item.worktreePath !== null) pairs.push(`w=${encodeMetaValue(item.worktreePath)}`);
5187
+ if (item.createdAt !== null) pairs.push(`c=${encodeMetaValue(item.createdAt)}`);
5188
+ if (item.updatedAt !== null) pairs.push(`u=${encodeMetaValue(item.updatedAt)}`);
5189
+ if (item.planDir !== null) pairs.push(`p=${encodeMetaValue(item.planDir)}`);
5190
+ if (pairs.length === 0) return "";
5191
+ return `<${pairs.join(";")}>`;
5192
+ }
5193
+ function parseStatus2(marker) {
5194
+ if (marker === " ") return { status: "open", session: null };
5195
+ if (marker === "x") return { status: "completed", session: null };
5196
+ if (marker === "!") return { status: "blocked", session: null };
5197
+ if (marker.startsWith(">:")) return { status: "in_progress", session: marker.slice(2) };
5198
+ if (marker === ">") return { status: "in_progress", session: null };
5199
+ return { status: "open", session: null };
5200
+ }
5201
+ function sanitizeSession(session) {
5202
+ return session.replace(/[\[\]]/g, "");
5203
+ }
5204
+ function statusToMarker(item) {
5205
+ switch (item.status) {
5206
+ case "open":
5207
+ return " ";
5208
+ case "completed":
5209
+ return "x";
5210
+ case "blocked":
5211
+ return "!";
5212
+ case "in_progress":
5213
+ return item.session ? `>:${sanitizeSession(item.session)}` : ">";
5022
5214
  }
5023
- let sectionEnd = lines.length;
5024
- for (let lineIndex = sectionStart + 1; lineIndex < lines.length; lineIndex += 1) {
5025
- if (/^#{1,2}\s+\S/.test(lines[lineIndex].trim())) {
5026
- sectionEnd = lineIndex;
5027
- break;
5028
- }
5215
+ }
5216
+ function parseChecklistItem(line) {
5217
+ const match = line.match(ITEM_REGEX);
5218
+ if (!match) return null;
5219
+ const marker = match[1];
5220
+ const rest = match[2];
5221
+ const { status, session } = parseStatus2(marker);
5222
+ const idMatch = rest.match(ID_REGEX);
5223
+ const id = idMatch ? idMatch[1] : "";
5224
+ const tags = [];
5225
+ let tagMatch;
5226
+ const tagRegex = new RegExp(TAG_REGEX.source, "g");
5227
+ while ((tagMatch = tagRegex.exec(rest)) !== null) {
5228
+ tags.push(tagMatch[1]);
5029
5229
  }
5030
- const checklistLines = lines.map((line, lineIndex) => ({ line, lineIndex })).filter(
5031
- ({ lineIndex, line }) => lineIndex > sectionStart && lineIndex < sectionEnd && /^\s*[-*]\s+\[( |x|X)\]\s+.*$/.test(line)
5032
- );
5033
- const target = checklistLines[index];
5034
- if (!target) {
5035
- return { error: `Acceptance criteria item ${index} not found.` };
5230
+ let description = rest;
5231
+ const firstTagIdx = rest.search(/#[a-zA-Z0-9_-]/);
5232
+ const firstIdIdx = rest.search(/\[t:[a-f0-9]{4}\]/);
5233
+ const cutPoints = [firstTagIdx, firstIdIdx].filter((i) => i >= 0);
5234
+ if (cutPoints.length > 0) {
5235
+ description = rest.slice(0, Math.min(...cutPoints)).trim();
5036
5236
  }
5037
- const nextLine = target.line.replace(
5038
- /^(\s*[-*]\s+\[)( |x|X)(\]\s+.*)$/,
5039
- `$1${checked ? "x" : " "}$3`
5040
- );
5041
- lines[target.lineIndex] = nextLine;
5237
+ const meta = parseMetaToken(line);
5042
5238
  return {
5043
- content: `${prefix}${lines.join("\n")}`
5239
+ id,
5240
+ description,
5241
+ status,
5242
+ tags,
5243
+ session,
5244
+ branch: meta.branch,
5245
+ worktreePath: meta.worktreePath,
5246
+ createdAt: meta.createdAt,
5247
+ updatedAt: meta.updatedAt,
5248
+ planDir: meta.planDir
5044
5249
  };
5045
5250
  }
5046
-
5047
- // src/dashboard/api-write.ts
5048
- init_api();
5049
- init_assignment_resolver();
5050
-
5051
- // src/templates/index.ts
5052
- init_config();
5053
-
5054
- // src/templates/manifest.ts
5055
- function renderManifest(params2) {
5056
- return `---
5057
- version: "2.0"
5058
- project: ${params2.slug}
5059
- generated: "${params2.timestamp}"
5060
- ---
5061
-
5062
- # Project: ${params2.slug}
5063
-
5064
- ## Overview
5065
- - [Project Overview](./project.md)
5066
-
5067
- ## Indexes
5068
- - [Assignments](./_index-assignments.md)
5069
- - [Plans](./_index-plans.md)
5070
- - [Decision Records](./_index-decisions.md)
5071
- - [Status](./_status.md)
5072
- - [Resources](./resources/_index.md)
5073
- - [Memories](./memories/_index.md)
5074
- `;
5251
+ function serializeChecklistItem(item) {
5252
+ const marker = statusToMarker(item);
5253
+ const tagStr = item.tags.map((t) => `#${t}`).join(" ");
5254
+ const parts = [`- [${marker}] ${item.description}`];
5255
+ if (tagStr) parts.push(tagStr);
5256
+ parts.push(`[t:${item.id}]`);
5257
+ const meta = serializeMetaToken(item);
5258
+ if (meta) parts.push(meta);
5259
+ return parts.join(" ");
5075
5260
  }
5076
-
5077
- // src/utils/yaml.ts
5078
- function escapeYamlString(value) {
5079
- if (value.includes("\n") || value.includes("\r")) {
5080
- throw new Error(
5081
- `YAML string values must be single-line. Got: "${value.slice(0, 50)}..."`
5082
- );
5261
+ function parseChecklist(content) {
5262
+ const [fm, body] = extractFrontmatter2(content);
5263
+ const workspace = getField(fm, "workspace") || "_global";
5264
+ const archiveIntervalRaw = getField(fm, "archiveInterval") || "weekly";
5265
+ const archiveInterval = ["daily", "weekly", "monthly", "never"].includes(archiveIntervalRaw) ? archiveIntervalRaw : "weekly";
5266
+ const items = [];
5267
+ for (const line of body.split("\n")) {
5268
+ const item = parseChecklistItem(line);
5269
+ if (item) items.push(item);
5083
5270
  }
5084
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
5085
- return `"${escaped}"`;
5271
+ return { workspace, archiveInterval, items };
5086
5272
  }
5273
+ function serializeChecklist(checklist) {
5274
+ const fm = [
5275
+ "---",
5276
+ `workspace: ${checklist.workspace}`,
5277
+ `archiveInterval: ${checklist.archiveInterval}`,
5278
+ "---"
5279
+ ].join("\n");
5280
+ const header = "# Quick Todos";
5281
+ const items = checklist.items.map(serializeChecklistItem).join("\n");
5282
+ return `${fm}
5087
5283
 
5088
- // src/templates/project.ts
5089
- function renderProject(params2) {
5090
- const safeTitle = escapeYamlString(params2.title);
5091
- const workspaceLine = params2.workspace ? `
5092
- workspace: ${params2.workspace}` : "";
5093
- return `---
5094
- id: ${params2.id}
5095
- slug: ${params2.slug}
5096
- title: ${safeTitle}
5097
- archived: false
5098
- archivedAt: null
5099
- archivedReason: null
5100
- created: "${params2.timestamp}"
5101
- updated: "${params2.timestamp}"
5102
- externalIds: []
5103
- tags: []${workspaceLine}
5104
- ---
5105
-
5106
- # ${params2.title}
5107
-
5108
- ## Overview
5109
-
5110
- <!-- Describe the project goal, context, and success criteria here. -->
5111
-
5112
- ## Notes
5284
+ ${header}
5113
5285
 
5114
- <!-- Optional human notes, updates, or context. -->
5286
+ ${items}
5115
5287
  `;
5116
5288
  }
5117
-
5118
- // src/templates/assignment.ts
5119
- function renderAssignment(params2) {
5120
- const safeTitle = escapeYamlString(params2.title);
5121
- const dependsOnYaml = params2.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
5122
- - ${params2.dependsOn.join("\n - ")}`;
5123
- const linksYaml = params2.links.length === 0 ? "links: []" : `links:
5124
- - ${params2.links.join("\n - ")}`;
5125
- const projectYaml = `project: ${params2.project == null ? "null" : params2.project}`;
5126
- const workspaceGroupLine = params2.workspaceGroup ? `
5127
- workspaceGroup: ${params2.workspaceGroup}` : "";
5128
- const typeYaml = `type: ${params2.type ?? "feature"}`;
5129
- const todosSection = params2.includeTodos ? `## Todos
5130
-
5131
- <!--
5132
- Checklist of work items for this assignment. Items may be simple tasks
5133
- or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
5134
- When a plan is superseded by a new one, mark the old todo as:
5135
- - [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
5136
- Never delete superseded todos \u2014 preserve the history.
5137
- -->
5138
-
5139
- ` : "";
5140
- return `---
5141
- id: ${params2.id}
5142
- slug: ${params2.slug}
5143
- title: ${safeTitle}
5144
- ${projectYaml}${workspaceGroupLine}
5145
- ${typeYaml}
5146
- status: pending
5147
- priority: ${params2.priority}
5148
- created: "${params2.timestamp}"
5149
- updated: "${params2.timestamp}"
5150
- assignee: null
5151
- externalIds: []
5152
- ${dependsOnYaml}
5153
- ${linksYaml}
5154
- blockedReason: null
5155
- workspace:
5156
- repository: null
5157
- worktreePath: null
5158
- branch: null
5159
- parentBranch: null
5160
- tags: []
5161
- ---
5162
-
5163
- # ${params2.title}
5164
-
5165
- ## Objective
5166
-
5167
- <!-- Clear description of what needs to be done and why. -->
5168
-
5169
- ## Acceptance Criteria
5170
-
5171
- - [ ] <!-- criterion 1 -->
5172
- - [ ] <!-- criterion 2 -->
5173
- - [ ] <!-- criterion 3 -->
5174
-
5175
- ${todosSection}## Context
5176
-
5177
- <!-- Links to relevant docs, code, or other assignments. -->
5178
-
5179
- ## Links
5180
-
5181
- - [Progress](./progress.md)
5182
- - [Comments](./comments.md)
5183
- - [Scratchpad](./scratchpad.md)
5184
- - [Handoff](./handoff.md)
5185
- - [Decision Record](./decision-record.md)
5186
- `;
5289
+ function parseLog(content) {
5290
+ const [fm, body] = extractFrontmatter2(content);
5291
+ const workspace = getField(fm, "workspace") || "_global";
5292
+ const entries = [];
5293
+ const sections = body.split(/^### /m).filter((s) => s.match(/^\d{4}-/));
5294
+ for (const section of sections) {
5295
+ const lines = section.split("\n");
5296
+ const heading = lines[0]?.trim() || "";
5297
+ const headingMatch = heading.match(/^(\S+)\s*—?\s*(.*)/);
5298
+ if (!headingMatch) continue;
5299
+ const timestamp = headingMatch[1];
5300
+ const idsPart = headingMatch[2] || "";
5301
+ const itemIds = [...idsPart.matchAll(/t:([a-f0-9]{4})/g)].map((m) => m[1]);
5302
+ const entry = {
5303
+ timestamp,
5304
+ itemIds,
5305
+ items: "",
5306
+ session: null,
5307
+ branch: null,
5308
+ summary: "",
5309
+ blockers: null,
5310
+ status: null
5311
+ };
5312
+ for (const line of lines.slice(1)) {
5313
+ const fieldMatch = line.match(/^\*\*(\w+):\*\*\s*(.*)/);
5314
+ if (!fieldMatch) continue;
5315
+ const key = fieldMatch[1].toLowerCase();
5316
+ const value = fieldMatch[2].trim();
5317
+ switch (key) {
5318
+ case "items":
5319
+ entry.items = value;
5320
+ break;
5321
+ case "session":
5322
+ entry.session = value;
5323
+ break;
5324
+ case "branch":
5325
+ entry.branch = value;
5326
+ break;
5327
+ case "summary":
5328
+ entry.summary = value;
5329
+ break;
5330
+ case "blockers":
5331
+ entry.blockers = value;
5332
+ break;
5333
+ case "status":
5334
+ entry.status = value;
5335
+ break;
5336
+ }
5337
+ }
5338
+ entries.push(entry);
5339
+ }
5340
+ return { workspace, entries };
5187
5341
  }
5188
-
5189
- // src/templates/scratchpad.ts
5190
- function renderScratchpad(params2) {
5191
- return `---
5192
- assignment: ${params2.assignmentSlug}
5193
- updated: "${params2.timestamp}"
5194
- ---
5195
-
5196
- # Scratchpad
5197
-
5198
- No working notes yet.
5199
- `;
5342
+ function serializeLogEntry(entry) {
5343
+ const idStr = entry.itemIds.map((id) => `t:${id}`).join(", ");
5344
+ const lines = [`### ${entry.timestamp} \u2014 ${idStr}`];
5345
+ if (entry.items) lines.push(`**Items:** ${entry.items}`);
5346
+ if (entry.session) lines.push(`**Session:** ${entry.session}`);
5347
+ if (entry.branch) lines.push(`**Branch:** ${entry.branch}`);
5348
+ if (entry.summary) lines.push(`**Summary:** ${entry.summary}`);
5349
+ if (entry.blockers) lines.push(`**Blockers:** ${entry.blockers}`);
5350
+ if (entry.status) lines.push(`**Status:** ${entry.status}`);
5351
+ return lines.join("\n");
5200
5352
  }
5201
-
5202
- // src/templates/handoff.ts
5203
- function renderHandoff(params2) {
5204
- return `---
5205
- assignment: ${params2.assignmentSlug}
5206
- updated: "${params2.timestamp}"
5207
- handoffCount: 0
5353
+ function checklistPath(todosDir2, workspace) {
5354
+ return resolve15(todosDir2, `${workspace}.md`);
5355
+ }
5356
+ function logPath(todosDir2, workspace) {
5357
+ return resolve15(todosDir2, `${workspace}-log.md`);
5358
+ }
5359
+ function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
5360
+ const year = now.getFullYear();
5361
+ const month = String(now.getMonth() + 1).padStart(2, "0");
5362
+ const day = String(now.getDate()).padStart(2, "0");
5363
+ let suffix;
5364
+ switch (interval) {
5365
+ case "daily":
5366
+ suffix = `${year}-${month}-${day}`;
5367
+ break;
5368
+ case "weekly": {
5369
+ const jan1 = new Date(year, 0, 1);
5370
+ const days = Math.floor((now.getTime() - jan1.getTime()) / 864e5);
5371
+ const week = String(Math.ceil((days + jan1.getDay() + 1) / 7)).padStart(2, "0");
5372
+ suffix = `${year}-W${week}`;
5373
+ break;
5374
+ }
5375
+ case "monthly":
5376
+ suffix = `${year}-${month}`;
5377
+ break;
5378
+ default:
5379
+ suffix = `${year}-${month}-${day}`;
5380
+ }
5381
+ return resolve15(todosDir2, "archive", `${workspace}-${suffix}.md`);
5382
+ }
5383
+ async function readChecklist(todosDir2, workspace) {
5384
+ const path = checklistPath(todosDir2, workspace);
5385
+ if (!await fileExists(path)) {
5386
+ return { workspace, archiveInterval: "weekly", items: [] };
5387
+ }
5388
+ const content = await readFile12(path, "utf-8");
5389
+ return parseChecklist(content);
5390
+ }
5391
+ async function writeChecklist(todosDir2, checklist) {
5392
+ await ensureDir(todosDir2);
5393
+ const path = checklistPath(todosDir2, checklist.workspace);
5394
+ await writeFileForce(path, serializeChecklist(checklist));
5395
+ }
5396
+ async function readLog(todosDir2, workspace) {
5397
+ const path = logPath(todosDir2, workspace);
5398
+ if (!await fileExists(path)) {
5399
+ return { workspace, entries: [] };
5400
+ }
5401
+ const content = await readFile12(path, "utf-8");
5402
+ return parseLog(content);
5403
+ }
5404
+ async function appendLogEntry2(todosDir2, workspace, entry) {
5405
+ await ensureDir(todosDir2);
5406
+ const path = logPath(todosDir2, workspace);
5407
+ let content;
5408
+ if (await fileExists(path)) {
5409
+ content = await readFile12(path, "utf-8");
5410
+ content = content.trimEnd() + "\n\n" + serializeLogEntry(entry) + "\n";
5411
+ } else {
5412
+ const fm = `---
5413
+ workspace: ${workspace}
5208
5414
  ---
5209
5415
 
5210
- # Handoff Log
5416
+ # Todo Log
5211
5417
 
5212
- No handoffs recorded yet.
5213
5418
  `;
5419
+ content = fm + serializeLogEntry(entry) + "\n";
5420
+ }
5421
+ await writeFileForce(path, content);
5422
+ }
5423
+ function computeCounts(items) {
5424
+ const counts = { open: 0, in_progress: 0, completed: 0, blocked: 0, total: items.length };
5425
+ for (const item of items) {
5426
+ counts[item.status]++;
5427
+ }
5428
+ return counts;
5214
5429
  }
5430
+ var ITEM_REGEX, ID_REGEX, TAG_REGEX, META_TOKEN_REGEX, META_ENCODE_CHARS;
5431
+ var init_parser2 = __esm({
5432
+ "src/todos/parser.ts"() {
5433
+ "use strict";
5434
+ init_parser();
5435
+ init_fs();
5436
+ ITEM_REGEX = /^- \[([ x!]|>[^\]]*)\]\s+(.+)$/;
5437
+ ID_REGEX = /\[t:([a-f0-9]{4})\]/;
5438
+ TAG_REGEX = /#([a-zA-Z0-9_-]+)/g;
5439
+ META_TOKEN_REGEX = /\[t:[a-f0-9]{4}\]\s+<([^>]*)>\s*$/;
5440
+ META_ENCODE_CHARS = ["%", "<", ">", "[", "]", "=", ";", "\n", "\r"];
5441
+ }
5442
+ });
5215
5443
 
5216
- // src/templates/progress.ts
5217
- function renderProgress(params2) {
5218
- return `---
5219
- assignment: ${params2.assignment}
5220
- entryCount: 0
5221
- generated: "${params2.timestamp}"
5222
- updated: "${params2.timestamp}"
5223
- ---
5444
+ // src/utils/assignment-todos.ts
5445
+ var assignment_todos_exports = {};
5446
+ __export(assignment_todos_exports, {
5447
+ appendTodosToAssignmentBody: () => appendTodosToAssignmentBody,
5448
+ touchAssignmentUpdated: () => touchAssignmentUpdated
5449
+ });
5450
+ function setTopLevelField2(content, key, value) {
5451
+ const fieldRegex = new RegExp(`^(${key}:)\\s*.*$`, "m");
5452
+ if (fieldRegex.test(content)) {
5453
+ return content.replace(fieldRegex, `$1 ${value}`);
5454
+ }
5455
+ return content;
5456
+ }
5457
+ function appendTodosToAssignmentBody(body, todos) {
5458
+ if (todos.length === 0) return body;
5459
+ const lines = todos.map((t) => {
5460
+ const base = `- [ ] ${t.description.trim()}`;
5461
+ return t.trace ? `${base} <!-- ${t.trace} -->` : base;
5462
+ });
5463
+ const block = lines.join("\n");
5464
+ const todosHeading = /^## Todos\s*$/m;
5465
+ if (todosHeading.test(body)) {
5466
+ return body.replace(
5467
+ /(^## Todos[\s\S]*?)(\n## |\n*$)/m,
5468
+ (_m, section, nextHeading) => {
5469
+ return `${section.trimEnd()}
5470
+ ${block}
5471
+ ${nextHeading}`;
5472
+ }
5473
+ );
5474
+ }
5475
+ return `${body.trimEnd()}
5224
5476
 
5225
- # Progress
5477
+ ## Todos
5226
5478
 
5227
- No progress yet.
5479
+ ${block}
5228
5480
  `;
5229
5481
  }
5230
-
5231
- // src/templates/comments.ts
5232
- function renderComments(params2) {
5233
- return `---
5234
- assignment: ${params2.assignment}
5235
- entryCount: 0
5236
- generated: "${params2.timestamp}"
5237
- updated: "${params2.timestamp}"
5238
- ---
5239
-
5240
- # Comments
5241
-
5242
- No comments yet.
5243
- `;
5482
+ function touchAssignmentUpdated(content, timestamp) {
5483
+ return setTopLevelField2(content, "updated", `"${timestamp}"`);
5244
5484
  }
5245
- function formatCommentEntry(comment) {
5246
- const lines = [];
5247
- lines.push(`## ${comment.id}`);
5248
- lines.push("");
5249
- lines.push(`**Recorded:** ${comment.timestamp}`);
5250
- lines.push(`**Author:** ${comment.author}`);
5251
- lines.push(`**Type:** ${comment.type}`);
5252
- if (comment.replyTo) {
5253
- lines.push(`**Reply to:** ${comment.replyTo}`);
5485
+ var init_assignment_todos = __esm({
5486
+ "src/utils/assignment-todos.ts"() {
5487
+ "use strict";
5254
5488
  }
5255
- if (comment.type === "question") {
5256
- lines.push(`**Resolved:** ${comment.resolved ? "true" : "false"}`);
5489
+ });
5490
+
5491
+ // src/commands/create-assignment.ts
5492
+ var create_assignment_exports = {};
5493
+ __export(create_assignment_exports, {
5494
+ createAssignmentCommand: () => createAssignmentCommand
5495
+ });
5496
+ import { resolve as resolve16 } from "path";
5497
+ async function createAssignmentCommand(title, options) {
5498
+ if (!title.trim()) {
5499
+ throw new Error("Assignment title cannot be empty.");
5257
5500
  }
5258
- lines.push("");
5259
- lines.push(comment.body.trim());
5260
- lines.push("");
5261
- return lines.join("\n");
5501
+ if (options.workspace && options.project) {
5502
+ throw new Error(
5503
+ "Cannot use --workspace with --project (projects already carry a workspace via project.workspace)."
5504
+ );
5505
+ }
5506
+ if (options.workspace && !options.oneOff) {
5507
+ throw new Error("--workspace requires --one-off.");
5508
+ }
5509
+ if (!options.project && !options.oneOff) {
5510
+ throw new Error(
5511
+ "Either --project <slug> or --one-off is required."
5512
+ );
5513
+ }
5514
+ if (options.project && options.oneOff) {
5515
+ throw new Error(
5516
+ "Cannot use both --project and --one-off. Use --project to add to an existing project, or --one-off to create a standalone assignment."
5517
+ );
5518
+ }
5519
+ if (options.project && !isValidSlug(options.project)) {
5520
+ throw new Error(
5521
+ `Invalid project slug "${options.project}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5522
+ );
5523
+ }
5524
+ if (options.workspace && !isValidSlug(options.workspace)) {
5525
+ throw new Error(
5526
+ `Invalid workspace slug "${options.workspace}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5527
+ );
5528
+ }
5529
+ if (options.oneOff && options.dependsOn) {
5530
+ throw new Error("Standalone assignments cannot have dependencies (--depends-on is not allowed with --one-off).");
5531
+ }
5532
+ const assignmentSlug = options.slug || slugify(title);
5533
+ if (!isValidSlug(assignmentSlug)) {
5534
+ throw new Error(
5535
+ `Invalid slug "${assignmentSlug}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5536
+ );
5537
+ }
5538
+ const dependsOn = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()).filter(Boolean) : [];
5539
+ for (const dep of dependsOn) {
5540
+ if (!isValidSlug(dep)) {
5541
+ throw new Error(
5542
+ `Invalid dependency slug "${dep}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5543
+ );
5544
+ }
5545
+ }
5546
+ const links = options.links ? options.links.split(",").map((s) => s.trim()).filter(Boolean) : [];
5547
+ for (const link of links) {
5548
+ const parts = link.split("/");
5549
+ if (parts.length !== 2 || !parts.every(isValidSlug)) {
5550
+ throw new Error(
5551
+ `Invalid link "${link}". Links must be in projectSlug/assignmentSlug format (e.g., "my-project/my-assignment").`
5552
+ );
5553
+ }
5554
+ }
5555
+ const validPriorities = ["low", "medium", "high", "critical"];
5556
+ const priority = options.priority || "medium";
5557
+ if (!validPriorities.includes(priority)) {
5558
+ throw new Error(
5559
+ `Invalid priority "${options.priority}". Must be one of: ${validPriorities.join(", ")}`
5560
+ );
5561
+ }
5562
+ const config = await readConfig();
5563
+ const timestamp = nowTimestamp();
5564
+ const id = generateId();
5565
+ let assignmentDir;
5566
+ let projectSlug;
5567
+ let folderName;
5568
+ if (options.oneOff) {
5569
+ const standaloneRoot = assignmentsDir();
5570
+ folderName = id;
5571
+ assignmentDir = resolve16(standaloneRoot, folderName);
5572
+ projectSlug = null;
5573
+ await ensureDir(standaloneRoot);
5574
+ } else {
5575
+ const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
5576
+ projectSlug = options.project;
5577
+ const projectDir = resolve16(baseDir, projectSlug);
5578
+ const projectMdPath = resolve16(projectDir, "project.md");
5579
+ if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
5580
+ throw new Error(
5581
+ `Project "${projectSlug}" not found at ${projectDir}.
5582
+ Run 'syntaur create-project' first or use --one-off.`
5583
+ );
5584
+ }
5585
+ if (dependsOn.length > 0) {
5586
+ const depDirBase = resolve16(projectDir, "assignments");
5587
+ for (const dep of dependsOn) {
5588
+ const depDir = resolve16(depDirBase, dep);
5589
+ if (!await fileExists(depDir)) {
5590
+ console.warn(
5591
+ `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
5592
+ );
5593
+ }
5594
+ }
5595
+ }
5596
+ folderName = assignmentSlug;
5597
+ assignmentDir = resolve16(projectDir, "assignments", folderName);
5598
+ }
5599
+ if (await fileExists(assignmentDir)) {
5600
+ throw new Error(
5601
+ `Assignment folder already exists: ${assignmentDir}
5602
+ Use --slug to specify a different slug.`
5603
+ );
5604
+ }
5605
+ await ensureDir(assignmentDir);
5606
+ const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
5607
+ const files = [
5608
+ [
5609
+ resolve16(assignmentDir, "assignment.md"),
5610
+ renderAssignment({
5611
+ id,
5612
+ slug: assignmentSlug,
5613
+ title,
5614
+ timestamp,
5615
+ priority,
5616
+ dependsOn,
5617
+ links,
5618
+ project: projectSlug,
5619
+ workspaceGroup: options.workspace ?? null,
5620
+ type: options.type,
5621
+ includeTodos: options.withTodos === true
5622
+ })
5623
+ ],
5624
+ [
5625
+ resolve16(assignmentDir, "scratchpad.md"),
5626
+ renderScratchpad({
5627
+ assignmentSlug: companionAssignmentRef,
5628
+ timestamp
5629
+ })
5630
+ ],
5631
+ [
5632
+ resolve16(assignmentDir, "handoff.md"),
5633
+ renderHandoff({
5634
+ assignmentSlug: companionAssignmentRef,
5635
+ timestamp
5636
+ })
5637
+ ],
5638
+ [
5639
+ resolve16(assignmentDir, "decision-record.md"),
5640
+ renderDecisionRecord({
5641
+ assignmentSlug: companionAssignmentRef,
5642
+ timestamp
5643
+ })
5644
+ ],
5645
+ [
5646
+ resolve16(assignmentDir, "progress.md"),
5647
+ renderProgress({
5648
+ assignment: companionAssignmentRef,
5649
+ timestamp
5650
+ })
5651
+ ],
5652
+ [
5653
+ resolve16(assignmentDir, "comments.md"),
5654
+ renderComments({
5655
+ assignment: companionAssignmentRef,
5656
+ timestamp
5657
+ })
5658
+ ]
5659
+ ];
5660
+ for (const [filePath, content] of files) {
5661
+ await writeFileForce(filePath, content);
5662
+ }
5663
+ if (!options.silent) {
5664
+ if (projectSlug === null) {
5665
+ console.log(
5666
+ `Created standalone assignment "${title}" at ${assignmentDir}/`
5667
+ );
5668
+ console.log(` UUID: ${id}`);
5669
+ console.log(` Slug: ${assignmentSlug} (display only)`);
5670
+ } else {
5671
+ console.log(
5672
+ `Created assignment "${title}" in project "${projectSlug}" at ${assignmentDir}/`
5673
+ );
5674
+ console.log(` Slug: ${assignmentSlug}`);
5675
+ }
5676
+ console.log(` Priority: ${priority}`);
5677
+ if (options.type) {
5678
+ console.log(` Type: ${options.type}`);
5679
+ }
5680
+ if (dependsOn.length > 0) {
5681
+ console.log(` Depends on: ${dependsOn.join(", ")}`);
5682
+ }
5683
+ if (links.length > 0) {
5684
+ console.log(` Links: ${links.join(", ")}`);
5685
+ }
5686
+ console.log(` Files created:`);
5687
+ console.log(` assignment.md`);
5688
+ console.log(` scratchpad.md`);
5689
+ console.log(` handoff.md`);
5690
+ console.log(` decision-record.md`);
5691
+ console.log(` progress.md`);
5692
+ console.log(` comments.md`);
5693
+ console.log(
5694
+ ` Plan files (plan.md, plan-v2.md, ...) are created on demand by /plan-assignment.`
5695
+ );
5696
+ }
5697
+ return { id, slug: assignmentSlug, projectSlug, assignmentDir };
5262
5698
  }
5699
+ var init_create_assignment = __esm({
5700
+ "src/commands/create-assignment.ts"() {
5701
+ "use strict";
5702
+ init_slug();
5703
+ init_timestamp();
5704
+ init_uuid();
5705
+ init_paths();
5706
+ init_fs();
5707
+ init_config2();
5708
+ init_templates();
5709
+ }
5710
+ });
5263
5711
 
5264
- // src/templates/decision-record.ts
5265
- function renderDecisionRecord(params2) {
5266
- return `---
5267
- assignment: ${params2.assignmentSlug}
5268
- updated: "${params2.timestamp}"
5269
- decisionCount: 0
5270
- ---
5712
+ // src/dashboard/server.ts
5713
+ init_paths();
5714
+ init_api();
5715
+ init_assignment_resolver();
5716
+ import express from "express";
5717
+ import { createServer } from "http";
5718
+ import { resolve as resolve19 } from "path";
5719
+ import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
5720
+ import { WebSocketServer, WebSocket } from "ws";
5271
5721
 
5272
- # Decision Record
5722
+ // src/dashboard/agent-sessions.ts
5723
+ init_fs();
5724
+ import { readFile as readFile9 } from "fs/promises";
5725
+ import { resolve as resolve11 } from "path";
5273
5726
 
5274
- No decisions recorded yet.
5727
+ // src/dashboard/session-db.ts
5728
+ init_paths();
5729
+ init_fs();
5730
+ import Database from "better-sqlite3";
5731
+ import { resolve as resolve10 } from "path";
5732
+ import { readdir as readdir7 } from "fs/promises";
5733
+ var db = null;
5734
+ var SCHEMA_VERSION = "3";
5735
+ var SCHEMA_SQL = `
5736
+ CREATE TABLE IF NOT EXISTS sessions (
5737
+ session_id TEXT PRIMARY KEY,
5738
+ project_slug TEXT,
5739
+ assignment_slug TEXT,
5740
+ agent TEXT NOT NULL,
5741
+ started TEXT NOT NULL,
5742
+ ended TEXT,
5743
+ status TEXT NOT NULL DEFAULT 'active',
5744
+ path TEXT,
5745
+ description TEXT,
5746
+ transcript_path TEXT,
5747
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5748
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5749
+ );
5750
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
5751
+ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
5275
5752
  `;
5276
- }
5277
-
5278
- // src/templates/index-stubs.ts
5279
- function renderIndexAssignments(params2) {
5280
- return `---
5281
- project: ${params2.slug}
5282
- generated: "${params2.timestamp}"
5283
- total: 0
5284
- by_status:
5285
- pending: 0
5286
- in_progress: 0
5287
- blocked: 0
5288
- review: 0
5289
- completed: 0
5290
- failed: 0
5291
- ---
5292
-
5293
- # Assignments
5294
-
5295
- | Slug | Title | Status | Priority | Assignee | Dependencies | Updated |
5296
- |------|-------|--------|----------|----------|--------------|---------|
5753
+ var POST_MIGRATION_INDEXES_SQL = `
5754
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
5755
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
5297
5756
  `;
5757
+ function initSessionDb(dbPath) {
5758
+ if (db) return db;
5759
+ const finalPath = dbPath ?? resolve10(syntaurRoot(), "syntaur.db");
5760
+ db = new Database(finalPath);
5761
+ db.pragma("journal_mode = WAL");
5762
+ db.exec(SCHEMA_SQL);
5763
+ db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
5764
+ "schema_version",
5765
+ SCHEMA_VERSION
5766
+ );
5767
+ const database = db;
5768
+ const runMigrations = database.transaction(() => {
5769
+ const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
5770
+ if (vBeforeV2 === "1") {
5771
+ database.exec(`
5772
+ CREATE TABLE sessions_v2 (
5773
+ session_id TEXT PRIMARY KEY,
5774
+ project_slug TEXT,
5775
+ assignment_slug TEXT,
5776
+ agent TEXT NOT NULL,
5777
+ started TEXT NOT NULL,
5778
+ ended TEXT,
5779
+ status TEXT NOT NULL DEFAULT 'active',
5780
+ path TEXT,
5781
+ description TEXT,
5782
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5783
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5784
+ );
5785
+ INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
5786
+ DROP TABLE sessions;
5787
+ ALTER TABLE sessions_v2 RENAME TO sessions;
5788
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
5789
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
5790
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
5791
+ UPDATE meta SET value = '2' WHERE key = 'schema_version';
5792
+ `);
5793
+ }
5794
+ const vBeforeV3 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
5795
+ if (vBeforeV3 === "2") {
5796
+ const v2Columns = database.prepare("PRAGMA table_info(sessions)").all();
5797
+ const v2ColNames = v2Columns.map((c) => c.name);
5798
+ const hasProject = v2ColNames.includes("project_slug");
5799
+ const hasMission = v2ColNames.includes("mission_slug");
5800
+ const projectSlugExpr = hasProject && hasMission ? "COALESCE(project_slug, mission_slug)" : hasProject ? "project_slug" : hasMission ? "mission_slug" : null;
5801
+ if (!projectSlugExpr) {
5802
+ throw new Error(
5803
+ "sessions table has neither project_slug nor mission_slug; cannot migrate from v2 to v3"
5804
+ );
5805
+ }
5806
+ database.exec(`
5807
+ CREATE TABLE sessions_v3 (
5808
+ session_id TEXT PRIMARY KEY,
5809
+ project_slug TEXT,
5810
+ assignment_slug TEXT,
5811
+ agent TEXT NOT NULL,
5812
+ started TEXT NOT NULL,
5813
+ ended TEXT,
5814
+ status TEXT NOT NULL DEFAULT 'active',
5815
+ path TEXT,
5816
+ description TEXT,
5817
+ transcript_path TEXT,
5818
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5819
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5820
+ );
5821
+ INSERT INTO sessions_v3
5822
+ SELECT session_id, ${projectSlugExpr}, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at
5823
+ FROM sessions;
5824
+ DROP TABLE sessions;
5825
+ ALTER TABLE sessions_v3 RENAME TO sessions;
5826
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
5827
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
5828
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
5829
+ UPDATE meta SET value = '3' WHERE key = 'schema_version';
5830
+ `);
5831
+ }
5832
+ });
5833
+ runMigrations.exclusive();
5834
+ db.exec(POST_MIGRATION_INDEXES_SQL);
5835
+ return db;
5298
5836
  }
5299
- function renderIndexPlans(params2) {
5300
- return `---
5301
- project: ${params2.slug}
5302
- generated: "${params2.timestamp}"
5303
- ---
5304
-
5305
- # Plans
5306
-
5307
- | Assignment | Plan Status | Updated |
5308
- |------------|-------------|---------|
5309
- `;
5837
+ function getSessionDb() {
5838
+ if (!db) {
5839
+ throw new Error(
5840
+ "Session database not initialized. Call initSessionDb() first."
5841
+ );
5842
+ }
5843
+ return db;
5310
5844
  }
5311
- function renderIndexDecisions(params2) {
5312
- return `---
5313
- project: ${params2.slug}
5314
- generated: "${params2.timestamp}"
5315
- ---
5316
-
5317
- # Decision Records
5318
-
5319
- | Assignment | Count | Latest Decision | Latest Status | Updated |
5320
- |------------|-------|-----------------|---------------|---------|
5321
- `;
5845
+ function closeSessionDb() {
5846
+ if (db) {
5847
+ db.close();
5848
+ db = null;
5849
+ }
5850
+ }
5851
+ async function migrateFromMarkdown(projectsDir) {
5852
+ const database = getSessionDb();
5853
+ const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
5854
+ if (count.count > 0) return 0;
5855
+ if (!await fileExists(projectsDir)) return 0;
5856
+ const entries = await readdir7(projectsDir, { withFileTypes: true });
5857
+ const allSessions = [];
5858
+ for (const entry of entries) {
5859
+ if (!entry.isDirectory()) continue;
5860
+ const projectDir = resolve10(projectsDir, entry.name);
5861
+ const indexPath = resolve10(projectDir, "_index-sessions.md");
5862
+ if (!await fileExists(indexPath)) continue;
5863
+ const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
5864
+ allSessions.push(...sessions);
5865
+ }
5866
+ if (allSessions.length === 0) return 0;
5867
+ const insert = database.prepare(`
5868
+ INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
5869
+ VALUES (?, ?, ?, ?, ?, ?, ?)
5870
+ `);
5871
+ const insertAll = database.transaction((sessions) => {
5872
+ for (const s of sessions) {
5873
+ insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
5874
+ }
5875
+ });
5876
+ insertAll(allSessions);
5877
+ console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);
5878
+ return allSessions.length;
5879
+ }
5880
+ async function parseMarkdownSessionsIndex(filePath, projectSlug) {
5881
+ const { readFile: readFile15 } = await import("fs/promises");
5882
+ const raw = await readFile15(filePath, "utf-8");
5883
+ const sessions = [];
5884
+ const lines = raw.split("\n");
5885
+ let inTable = false;
5886
+ let headerSeen = false;
5887
+ for (const line of lines) {
5888
+ const trimmed = line.trim();
5889
+ if (!trimmed) continue;
5890
+ if (trimmed.startsWith("| Assignment") || trimmed.startsWith("|Assignment")) {
5891
+ inTable = true;
5892
+ headerSeen = false;
5893
+ continue;
5894
+ }
5895
+ if (inTable && !headerSeen && trimmed.match(/^\|[-\s|]+\|$/)) {
5896
+ headerSeen = true;
5897
+ continue;
5898
+ }
5899
+ if (inTable && headerSeen && trimmed.startsWith("|")) {
5900
+ const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
5901
+ if (cells.length >= 6) {
5902
+ sessions.push({
5903
+ assignmentSlug: cells[0],
5904
+ agent: cells[1],
5905
+ sessionId: cells[2],
5906
+ started: cells[3],
5907
+ status: cells[4] || "active",
5908
+ path: cells[5],
5909
+ projectSlug
5910
+ });
5911
+ }
5912
+ }
5913
+ }
5914
+ return sessions;
5322
5915
  }
5323
- function renderStatus(params2) {
5324
- return `---
5325
- project: ${params2.slug}
5326
- generated: "${params2.timestamp}"
5327
- status: pending
5328
- progress:
5329
- total: 0
5330
- completed: 0
5331
- in_progress: 0
5332
- blocked: 0
5333
- pending: 0
5334
- review: 0
5335
- failed: 0
5336
- needsAttention:
5337
- blockedCount: 0
5338
- failedCount: 0
5339
- openQuestions: 0
5340
- ---
5341
-
5342
- # Project Status: ${params2.title}
5343
-
5344
- **Status:** pending
5345
- **Progress:** 0/0 assignments complete
5346
-
5347
- ## Assignments
5348
-
5349
- No assignments yet.
5350
-
5351
- ## Dependency Graph
5352
-
5353
- No dependencies yet.
5354
5916
 
5355
- ## Needs Attention
5917
+ // src/dashboard/agent-sessions.ts
5918
+ function rowToSession(row) {
5919
+ return {
5920
+ sessionId: row.session_id,
5921
+ projectSlug: row.project_slug ?? null,
5922
+ assignmentSlug: row.assignment_slug ?? null,
5923
+ agent: row.agent,
5924
+ started: row.started,
5925
+ ended: row.ended ?? null,
5926
+ status: row.status,
5927
+ path: row.path ?? "",
5928
+ description: row.description ?? null,
5929
+ transcriptPath: row.transcript_path ?? null
5930
+ };
5931
+ }
5932
+ async function appendSession(_projectDir, session) {
5933
+ const db2 = getSessionDb();
5934
+ db2.prepare(`
5935
+ INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
5936
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
5937
+ ON CONFLICT(session_id) DO UPDATE SET
5938
+ project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
5939
+ assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
5940
+ agent = excluded.agent,
5941
+ status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
5942
+ path = COALESCE(NULLIF(excluded.path, ''), path),
5943
+ description = COALESCE(NULLIF(excluded.description, ''), description),
5944
+ transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
5945
+ updated_at = datetime('now')
5946
+ `).run(
5947
+ session.sessionId,
5948
+ session.projectSlug ?? null,
5949
+ session.assignmentSlug ?? null,
5950
+ session.agent,
5951
+ session.started,
5952
+ session.status,
5953
+ session.path,
5954
+ session.description ?? null,
5955
+ session.transcriptPath ?? null
5956
+ );
5957
+ }
5958
+ async function updateSessionStatus(_projectDir, sessionId, status) {
5959
+ const db2 = getSessionDb();
5960
+ const isTerminal = status === "completed" || status === "stopped";
5961
+ const result = isTerminal ? db2.prepare(
5962
+ "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
5963
+ ).run(status, sessionId) : db2.prepare(
5964
+ "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
5965
+ ).run(status, sessionId);
5966
+ return result.changes > 0;
5967
+ }
5968
+ async function listAllSessions(_projectsDir) {
5969
+ const db2 = getSessionDb();
5970
+ const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
5971
+ return rows.map(rowToSession);
5972
+ }
5973
+ async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
5974
+ const db2 = getSessionDb();
5975
+ if (assignmentSlug) {
5976
+ const rows2 = db2.prepare(
5977
+ "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
5978
+ ).all(projectSlug, assignmentSlug);
5979
+ return rows2.map(rowToSession);
5980
+ }
5981
+ const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
5982
+ return rows.map(rowToSession);
5983
+ }
5984
+ async function deleteSessions(sessionIds) {
5985
+ if (sessionIds.length === 0) return 0;
5986
+ const db2 = getSessionDb();
5987
+ const placeholders = sessionIds.map(() => "?").join(", ");
5988
+ const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
5989
+ return result.changes;
5990
+ }
5991
+ var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
5992
+ async function readAssignmentStatusFromPath(assignmentMdPath) {
5993
+ if (!await fileExists(assignmentMdPath)) return null;
5994
+ const raw = await readFile9(assignmentMdPath, "utf-8");
5995
+ const match = raw.match(/^status:\s*(.+)$/m);
5996
+ return match ? match[1].trim() : null;
5997
+ }
5998
+ async function readAssignmentStatus(projectDir, assignmentSlug) {
5999
+ return readAssignmentStatusFromPath(
6000
+ resolve11(projectDir, "assignments", assignmentSlug, "assignment.md")
6001
+ );
6002
+ }
6003
+ async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
6004
+ const db2 = getSessionDb();
6005
+ const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
6006
+ if (activeSessions.length === 0) return 0;
6007
+ const assignmentStatuses = /* @__PURE__ */ new Map();
6008
+ const seen = /* @__PURE__ */ new Set();
6009
+ for (const session of activeSessions) {
6010
+ const aslug = session.assignment_slug;
6011
+ if (!aslug) continue;
6012
+ const projectKey = session.project_slug ?? "__standalone__";
6013
+ const key = `${projectKey}/${aslug}`;
6014
+ if (seen.has(key)) continue;
6015
+ seen.add(key);
6016
+ if (session.project_slug) {
6017
+ const status = await readAssignmentStatus(
6018
+ resolve11(projectsDir, session.project_slug),
6019
+ aslug
6020
+ );
6021
+ if (status) assignmentStatuses.set(key, status);
6022
+ } else if (assignmentsDir2) {
6023
+ const status = await readAssignmentStatusFromPath(
6024
+ resolve11(assignmentsDir2, aslug, "assignment.md")
6025
+ );
6026
+ if (status) assignmentStatuses.set(key, status);
6027
+ }
6028
+ }
6029
+ let totalUpdated = 0;
6030
+ for (const session of activeSessions) {
6031
+ const projectKey = session.project_slug ?? "__standalone__";
6032
+ const key = `${projectKey}/${session.assignment_slug}`;
6033
+ const assignmentStatus = assignmentStatuses.get(key);
6034
+ if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
6035
+ const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
6036
+ await updateSessionStatus("", session.session_id, newStatus);
6037
+ totalUpdated++;
6038
+ }
6039
+ return totalUpdated;
6040
+ }
6041
+ async function listSessionsByAssignment(projectSlug, assignmentSlug) {
6042
+ const db2 = getSessionDb();
6043
+ const rows = projectSlug === null ? db2.prepare(
6044
+ "SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
6045
+ ).all(assignmentSlug) : db2.prepare(
6046
+ "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
6047
+ ).all(projectSlug, assignmentSlug);
6048
+ return rows.map(rowToSession);
6049
+ }
5356
6050
 
5357
- - **0 blocked** assignments
5358
- - **0 failed** assignments
5359
- - **0 unanswered** questions
5360
- `;
6051
+ // src/dashboard/watcher.ts
6052
+ import { watch } from "chokidar";
6053
+ import { relative, sep } from "path";
6054
+ function createWatcher(options) {
6055
+ const { projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
6056
+ const pendingEvents = /* @__PURE__ */ new Map();
6057
+ const projectsWatcher = watch(projectsDir, {
6058
+ ignoreInitial: true,
6059
+ persistent: true,
6060
+ depth: 10,
6061
+ ignored: /(^|[\/\\])\../
6062
+ });
6063
+ function handleProjectChange(filePath) {
6064
+ const rel = relative(projectsDir, filePath);
6065
+ const parts = rel.split(sep);
6066
+ if (parts.length === 0) return;
6067
+ const projectSlug = parts[0];
6068
+ let assignmentSlug;
6069
+ let isProjectTodos = false;
6070
+ if (parts.length >= 3 && parts[1] === "assignments") {
6071
+ assignmentSlug = parts[2];
6072
+ } else if (parts.length >= 2 && parts[1] === "todos") {
6073
+ isProjectTodos = true;
6074
+ }
6075
+ const debounceKey = isProjectTodos ? `todos:${projectSlug}` : assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
6076
+ const existing = pendingEvents.get(debounceKey);
6077
+ if (existing) clearTimeout(existing);
6078
+ const messageType = isProjectTodos ? "todos-updated" : assignmentSlug ? "assignment-updated" : "project-updated";
6079
+ pendingEvents.set(
6080
+ debounceKey,
6081
+ setTimeout(() => {
6082
+ pendingEvents.delete(debounceKey);
6083
+ const message = isProjectTodos ? {
6084
+ type: "todos-updated",
6085
+ projectSlug,
6086
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6087
+ } : {
6088
+ type: messageType,
6089
+ projectSlug,
6090
+ assignmentSlug,
6091
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6092
+ };
6093
+ onMessage(message);
6094
+ }, debounceMs)
6095
+ );
6096
+ }
6097
+ projectsWatcher.on("change", handleProjectChange);
6098
+ projectsWatcher.on("add", handleProjectChange);
6099
+ projectsWatcher.on("unlink", handleProjectChange);
6100
+ let standaloneWatcher = null;
6101
+ if (assignmentsDir2) {
6102
+ let handleStandaloneChange2 = function(filePath) {
6103
+ const rel = relative(assignmentsDir2, filePath);
6104
+ const parts = rel.split(sep);
6105
+ if (parts.length === 0) return;
6106
+ const assignmentId = parts[0];
6107
+ if (!assignmentId) return;
6108
+ const debounceKey = `__standalone__/${assignmentId}`;
6109
+ const existing = pendingEvents.get(debounceKey);
6110
+ if (existing) clearTimeout(existing);
6111
+ pendingEvents.set(
6112
+ debounceKey,
6113
+ setTimeout(() => {
6114
+ pendingEvents.delete(debounceKey);
6115
+ const message = {
6116
+ type: "assignment-updated",
6117
+ projectSlug: null,
6118
+ assignmentSlug: assignmentId,
6119
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6120
+ };
6121
+ onMessage(message);
6122
+ }, debounceMs)
6123
+ );
6124
+ };
6125
+ var handleStandaloneChange = handleStandaloneChange2;
6126
+ standaloneWatcher = watch(assignmentsDir2, {
6127
+ ignoreInitial: true,
6128
+ persistent: true,
6129
+ depth: 5,
6130
+ ignored: /(^|[\/\\])\../
6131
+ });
6132
+ standaloneWatcher.on("change", handleStandaloneChange2);
6133
+ standaloneWatcher.on("add", handleStandaloneChange2);
6134
+ standaloneWatcher.on("unlink", handleStandaloneChange2);
6135
+ }
6136
+ let serversWatcher = null;
6137
+ if (serversDir2) {
6138
+ let handleServerChange2 = function() {
6139
+ const debounceKey = "__servers__";
6140
+ const existing = pendingEvents.get(debounceKey);
6141
+ if (existing) clearTimeout(existing);
6142
+ pendingEvents.set(
6143
+ debounceKey,
6144
+ setTimeout(() => {
6145
+ pendingEvents.delete(debounceKey);
6146
+ const message = {
6147
+ type: "servers-updated",
6148
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6149
+ };
6150
+ onMessage(message);
6151
+ }, debounceMs)
6152
+ );
6153
+ };
6154
+ var handleServerChange = handleServerChange2;
6155
+ serversWatcher = watch(serversDir2, {
6156
+ ignoreInitial: true,
6157
+ persistent: true,
6158
+ depth: 1,
6159
+ ignored: /(^|[\/\\])\../
6160
+ });
6161
+ serversWatcher.on("change", handleServerChange2);
6162
+ serversWatcher.on("add", handleServerChange2);
6163
+ serversWatcher.on("unlink", handleServerChange2);
6164
+ }
6165
+ let playbooksWatcher = null;
6166
+ if (playbooksDir2) {
6167
+ let handlePlaybookChange2 = function() {
6168
+ const debounceKey = "__playbooks__";
6169
+ const existing = pendingEvents.get(debounceKey);
6170
+ if (existing) clearTimeout(existing);
6171
+ pendingEvents.set(
6172
+ debounceKey,
6173
+ setTimeout(() => {
6174
+ pendingEvents.delete(debounceKey);
6175
+ const message = {
6176
+ type: "playbooks-updated",
6177
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6178
+ };
6179
+ onMessage(message);
6180
+ }, debounceMs)
6181
+ );
6182
+ };
6183
+ var handlePlaybookChange = handlePlaybookChange2;
6184
+ playbooksWatcher = watch(playbooksDir2, {
6185
+ ignoreInitial: true,
6186
+ persistent: true,
6187
+ depth: 1,
6188
+ ignored: /(^|[\/\\])\../
6189
+ });
6190
+ playbooksWatcher.on("change", handlePlaybookChange2);
6191
+ playbooksWatcher.on("add", handlePlaybookChange2);
6192
+ playbooksWatcher.on("unlink", handlePlaybookChange2);
6193
+ }
6194
+ let todosWatcher = null;
6195
+ if (todosDir2) {
6196
+ let handleTodoChange2 = function() {
6197
+ const debounceKey = "__todos__";
6198
+ const existing = pendingEvents.get(debounceKey);
6199
+ if (existing) clearTimeout(existing);
6200
+ pendingEvents.set(
6201
+ debounceKey,
6202
+ setTimeout(() => {
6203
+ pendingEvents.delete(debounceKey);
6204
+ const message = {
6205
+ type: "todos-updated",
6206
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6207
+ };
6208
+ onMessage(message);
6209
+ }, debounceMs)
6210
+ );
6211
+ };
6212
+ var handleTodoChange = handleTodoChange2;
6213
+ todosWatcher = watch(todosDir2, {
6214
+ ignoreInitial: true,
6215
+ persistent: true,
6216
+ depth: 1,
6217
+ ignored: /(^|[\/\\])\../
6218
+ });
6219
+ todosWatcher.on("change", handleTodoChange2);
6220
+ todosWatcher.on("add", handleTodoChange2);
6221
+ todosWatcher.on("unlink", handleTodoChange2);
6222
+ }
6223
+ return {
6224
+ close: async () => {
6225
+ pendingEvents.forEach((timeout) => {
6226
+ clearTimeout(timeout);
6227
+ });
6228
+ pendingEvents.clear();
6229
+ await projectsWatcher.close();
6230
+ if (standaloneWatcher) await standaloneWatcher.close();
6231
+ if (serversWatcher) await serversWatcher.close();
6232
+ if (playbooksWatcher) await playbooksWatcher.close();
6233
+ if (todosWatcher) await todosWatcher.close();
6234
+ }
6235
+ };
5361
6236
  }
5362
- function renderResourcesIndex(params2) {
5363
- return `---
5364
- project: ${params2.slug}
5365
- generated: "${params2.timestamp}"
5366
- total: 0
5367
- ---
5368
-
5369
- # Resources
5370
6237
 
5371
- | Name | Category | Source | Related Assignments | Updated |
5372
- |------|----------|--------|---------------------|---------|
5373
- `;
5374
- }
5375
- function renderMemoriesIndex(params2) {
5376
- return `---
5377
- project: ${params2.slug}
5378
- generated: "${params2.timestamp}"
5379
- total: 0
5380
- ---
6238
+ // src/dashboard/server.ts
6239
+ init_fs();
6240
+ init_config2();
6241
+ init_hotkeysCatalog();
5381
6242
 
5382
- # Memories
6243
+ // src/dashboard/api-write.ts
6244
+ init_lifecycle();
6245
+ init_slug();
6246
+ init_uuid();
6247
+ init_timestamp();
6248
+ init_fs();
6249
+ init_parser();
6250
+ import { Router } from "express";
6251
+ import { resolve as resolve12 } from "path";
6252
+ import { rm, readFile as readFile10 } from "fs/promises";
5383
6253
 
5384
- | Name | Source | Scope | Source Assignment | Updated |
5385
- |------|--------|-------|------------------|---------|
5386
- `;
6254
+ // src/dashboard/acceptance-criteria.ts
6255
+ function splitFrontmatter(content) {
6256
+ const match = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/);
6257
+ if (!match) {
6258
+ return { prefix: "", body: content };
6259
+ }
6260
+ return {
6261
+ prefix: match[1],
6262
+ body: match[2]
6263
+ };
5387
6264
  }
5388
-
5389
- // src/templates/playbook.ts
5390
- function renderPlaybook(params2) {
5391
- const whenToUse = params2.whenToUse ? escapeYamlString(params2.whenToUse) : "null";
5392
- return `---
5393
- name: ${escapeYamlString(params2.name)}
5394
- slug: ${params2.slug}
5395
- description: ${escapeYamlString(params2.description)}
5396
- when_to_use: ${whenToUse}
5397
- created: "${params2.timestamp}"
5398
- updated: "${params2.timestamp}"
5399
- tags: []
5400
- ---
5401
-
5402
- # ${params2.name}
5403
-
5404
- <!-- Write imperative rules and workflows here. Keep it under 50 lines. -->
5405
- `;
6265
+ function toggleAcceptanceCriterion(content, index, checked) {
6266
+ if (!Number.isInteger(index) || index < 0) {
6267
+ return { error: "acceptance criteria index must be a non-negative integer" };
6268
+ }
6269
+ const { prefix, body } = splitFrontmatter(content);
6270
+ const lines = body.split("\n");
6271
+ const sectionStart = lines.findIndex((line) => /^##\s+Acceptance Criteria\s*$/i.test(line.trim()));
6272
+ if (sectionStart === -1) {
6273
+ return { error: "Acceptance Criteria section not found." };
6274
+ }
6275
+ let sectionEnd = lines.length;
6276
+ for (let lineIndex = sectionStart + 1; lineIndex < lines.length; lineIndex += 1) {
6277
+ if (/^#{1,2}\s+\S/.test(lines[lineIndex].trim())) {
6278
+ sectionEnd = lineIndex;
6279
+ break;
6280
+ }
6281
+ }
6282
+ const checklistLines = lines.map((line, lineIndex) => ({ line, lineIndex })).filter(
6283
+ ({ lineIndex, line }) => lineIndex > sectionStart && lineIndex < sectionEnd && /^\s*[-*]\s+\[( |x|X)\]\s+.*$/.test(line)
6284
+ );
6285
+ const target = checklistLines[index];
6286
+ if (!target) {
6287
+ return { error: `Acceptance criteria item ${index} not found.` };
6288
+ }
6289
+ const nextLine = target.line.replace(
6290
+ /^(\s*[-*]\s+\[)( |x|X)(\]\s+.*)$/,
6291
+ `$1${checked ? "x" : " "}$3`
6292
+ );
6293
+ lines[target.lineIndex] = nextLine;
6294
+ return {
6295
+ content: `${prefix}${lines.join("\n")}`
6296
+ };
5406
6297
  }
5407
6298
 
5408
6299
  // src/dashboard/api-write.ts
6300
+ init_api();
6301
+ init_assignment_resolver();
6302
+ init_templates();
5409
6303
  init_lifecycle();
6304
+ init_templates();
5410
6305
  init_parser();
5411
6306
  function extractFrontmatter3(content) {
5412
6307
  const trimmed = content.trimStart();
@@ -5509,7 +6404,7 @@ async function readCurrentDocument(filePath) {
5509
6404
  }
5510
6405
  return readFile10(filePath, "utf-8");
5511
6406
  }
5512
- function createWriteRouter(projectsDir, assignmentsDir) {
6407
+ function createWriteRouter(projectsDir, assignmentsDir2) {
5513
6408
  const router = Router();
5514
6409
  router.get("/api/templates/project", (_req, res) => {
5515
6410
  const content = renderProject({
@@ -6263,7 +7158,7 @@ ${entry}`;
6263
7158
  });
6264
7159
  router.post("/api/assignments", async (req, res) => {
6265
7160
  try {
6266
- if (!assignmentsDir) {
7161
+ if (!assignmentsDir2) {
6267
7162
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6268
7163
  return;
6269
7164
  }
@@ -6304,7 +7199,7 @@ ${entry}`;
6304
7199
  return;
6305
7200
  }
6306
7201
  const id2 = generateId();
6307
- const assignmentDir2 = resolve12(assignmentsDir, id2);
7202
+ const assignmentDir2 = resolve12(assignmentsDir2, id2);
6308
7203
  if (await fileExists(assignmentDir2)) {
6309
7204
  res.status(500).json({ error: "UUID collision \u2014 try again" });
6310
7205
  return;
@@ -6333,7 +7228,7 @@ ${entry}`;
6333
7228
  resolve12(assignmentDir2, "comments.md"),
6334
7229
  renderComments({ assignment: id2, timestamp: timestamp2 })
6335
7230
  );
6336
- const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir, id2);
7231
+ const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir2, id2);
6337
7232
  res.status(201).json({ assignment: detail2 });
6338
7233
  return;
6339
7234
  }
@@ -6348,7 +7243,7 @@ ${entry}`;
6348
7243
  return;
6349
7244
  }
6350
7245
  const id = generateId();
6351
- const assignmentDir = resolve12(assignmentsDir, id);
7246
+ const assignmentDir = resolve12(assignmentsDir2, id);
6352
7247
  if (await fileExists(assignmentDir)) {
6353
7248
  res.status(500).json({ error: "UUID collision \u2014 try again" });
6354
7249
  return;
@@ -6389,7 +7284,7 @@ ${entry}`;
6389
7284
  resolve12(assignmentDir, "comments.md"),
6390
7285
  renderComments({ assignment: id, timestamp })
6391
7286
  );
6392
- const detail = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
7287
+ const detail = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
6393
7288
  res.status(201).json({ assignment: detail });
6394
7289
  } catch (error) {
6395
7290
  console.error("Error creating standalone assignment:", error);
@@ -6398,18 +7293,18 @@ ${entry}`;
6398
7293
  });
6399
7294
  router.post("/api/assignments/:id/comments", async (req, res) => {
6400
7295
  try {
6401
- if (!assignmentsDir) {
7296
+ if (!assignmentsDir2) {
6402
7297
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6403
7298
  return;
6404
7299
  }
6405
7300
  const id = getParam(req.params.id);
6406
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7301
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6407
7302
  if (!resolved) {
6408
7303
  res.status(404).json({ error: `Assignment "${id}" not found` });
6409
7304
  return;
6410
7305
  }
6411
7306
  await appendCommentTo(resolved.assignmentDir, resolved.standalone ? resolved.id : resolved.assignmentSlug, req, res, async () => {
6412
- return resolved.standalone ? getAssignmentDetailById(projectsDir, assignmentsDir, id) : getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
7307
+ return resolved.standalone ? getAssignmentDetailById(projectsDir, assignmentsDir2, id) : getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
6413
7308
  });
6414
7309
  } catch (error) {
6415
7310
  console.error("Error appending comment (by id):", error);
@@ -6418,19 +7313,19 @@ ${entry}`;
6418
7313
  });
6419
7314
  router.patch("/api/assignments/:id/comments/:commentId/resolved", async (req, res) => {
6420
7315
  try {
6421
- if (!assignmentsDir) {
7316
+ if (!assignmentsDir2) {
6422
7317
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6423
7318
  return;
6424
7319
  }
6425
7320
  const id = getParam(req.params.id);
6426
7321
  const commentId = getParam(req.params.commentId);
6427
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7322
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6428
7323
  if (!resolved) {
6429
7324
  res.status(404).json({ error: `Assignment "${id}" not found` });
6430
7325
  return;
6431
7326
  }
6432
7327
  await toggleCommentResolvedAt(resolved.assignmentDir, commentId, req, res, async () => {
6433
- return resolved.standalone ? getAssignmentDetailById(projectsDir, assignmentsDir, id) : getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
7328
+ return resolved.standalone ? getAssignmentDetailById(projectsDir, assignmentsDir2, id) : getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
6434
7329
  });
6435
7330
  } catch (error) {
6436
7331
  console.error("Error toggling comment resolved (by id):", error);
@@ -6438,12 +7333,12 @@ ${entry}`;
6438
7333
  }
6439
7334
  });
6440
7335
  router.get("/api/assignments/:id/edit", async (req, res) => {
6441
- if (!assignmentsDir) {
7336
+ if (!assignmentsDir2) {
6442
7337
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6443
7338
  return;
6444
7339
  }
6445
7340
  const id = getParam(req.params.id);
6446
- const doc = await getEditableDocumentById(projectsDir, assignmentsDir, "assignment", id);
7341
+ const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "assignment", id);
6447
7342
  if (!doc) {
6448
7343
  res.status(404).json({ error: "Assignment not found" });
6449
7344
  return;
@@ -6451,12 +7346,12 @@ ${entry}`;
6451
7346
  res.json(doc);
6452
7347
  });
6453
7348
  router.get("/api/assignments/:id/plan/edit", async (req, res) => {
6454
- if (!assignmentsDir) {
7349
+ if (!assignmentsDir2) {
6455
7350
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6456
7351
  return;
6457
7352
  }
6458
7353
  const id = getParam(req.params.id);
6459
- const doc = await getEditableDocumentById(projectsDir, assignmentsDir, "plan", id);
7354
+ const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "plan", id);
6460
7355
  if (!doc) {
6461
7356
  res.status(404).json({ error: "Plan not found" });
6462
7357
  return;
@@ -6464,12 +7359,12 @@ ${entry}`;
6464
7359
  res.json(doc);
6465
7360
  });
6466
7361
  router.get("/api/assignments/:id/scratchpad/edit", async (req, res) => {
6467
- if (!assignmentsDir) {
7362
+ if (!assignmentsDir2) {
6468
7363
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6469
7364
  return;
6470
7365
  }
6471
7366
  const id = getParam(req.params.id);
6472
- const doc = await getEditableDocumentById(projectsDir, assignmentsDir, "scratchpad", id);
7367
+ const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "scratchpad", id);
6473
7368
  if (!doc) {
6474
7369
  res.status(404).json({ error: "Scratchpad not found" });
6475
7370
  return;
@@ -6477,12 +7372,12 @@ ${entry}`;
6477
7372
  res.json(doc);
6478
7373
  });
6479
7374
  router.get("/api/assignments/:id/handoff/edit", async (req, res) => {
6480
- if (!assignmentsDir) {
7375
+ if (!assignmentsDir2) {
6481
7376
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6482
7377
  return;
6483
7378
  }
6484
7379
  const id = getParam(req.params.id);
6485
- const doc = await getEditableDocumentById(projectsDir, assignmentsDir, "handoff", id);
7380
+ const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "handoff", id);
6486
7381
  if (!doc) {
6487
7382
  res.status(404).json({ error: "Handoff log not found" });
6488
7383
  return;
@@ -6490,12 +7385,12 @@ ${entry}`;
6490
7385
  res.json(doc);
6491
7386
  });
6492
7387
  router.get("/api/assignments/:id/decision-record/edit", async (req, res) => {
6493
- if (!assignmentsDir) {
7388
+ if (!assignmentsDir2) {
6494
7389
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6495
7390
  return;
6496
7391
  }
6497
7392
  const id = getParam(req.params.id);
6498
- const doc = await getEditableDocumentById(projectsDir, assignmentsDir, "decision-record", id);
7393
+ const doc = await getEditableDocumentById(projectsDir, assignmentsDir2, "decision-record", id);
6499
7394
  if (!doc) {
6500
7395
  res.status(404).json({ error: "Decision record not found" });
6501
7396
  return;
@@ -6504,12 +7399,12 @@ ${entry}`;
6504
7399
  });
6505
7400
  router.patch("/api/assignments/:id", async (req, res) => {
6506
7401
  try {
6507
- if (!assignmentsDir) {
7402
+ if (!assignmentsDir2) {
6508
7403
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6509
7404
  return;
6510
7405
  }
6511
7406
  const id = getParam(req.params.id);
6512
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7407
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6513
7408
  if (!resolved) {
6514
7409
  res.status(404).json({ error: `Assignment "${id}" not found` });
6515
7410
  return;
@@ -6537,7 +7432,7 @@ ${entry}`;
6537
7432
  }
6538
7433
  nextContent = setTopLevelField(nextContent, "updated", nowTimestamp());
6539
7434
  await writeFileForce(assignmentPath, nextContent);
6540
- const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
7435
+ const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
6541
7436
  res.json({ assignment, content: nextContent });
6542
7437
  } catch (error) {
6543
7438
  console.error("Error updating standalone assignment:", error);
@@ -6546,12 +7441,12 @@ ${entry}`;
6546
7441
  });
6547
7442
  router.patch("/api/assignments/:id/plan", async (req, res) => {
6548
7443
  try {
6549
- if (!assignmentsDir) {
7444
+ if (!assignmentsDir2) {
6550
7445
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6551
7446
  return;
6552
7447
  }
6553
7448
  const id = getParam(req.params.id);
6554
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7449
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6555
7450
  if (!resolved) {
6556
7451
  res.status(404).json({ error: `Assignment "${id}" not found` });
6557
7452
  return;
@@ -6571,7 +7466,7 @@ ${entry}`;
6571
7466
  }
6572
7467
  const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
6573
7468
  await writeFileForce(planPath, nextContent);
6574
- const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
7469
+ const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
6575
7470
  res.json({ assignment, content: nextContent });
6576
7471
  } catch (error) {
6577
7472
  console.error("Error updating standalone plan:", error);
@@ -6580,12 +7475,12 @@ ${entry}`;
6580
7475
  });
6581
7476
  router.patch("/api/assignments/:id/scratchpad", async (req, res) => {
6582
7477
  try {
6583
- if (!assignmentsDir) {
7478
+ if (!assignmentsDir2) {
6584
7479
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6585
7480
  return;
6586
7481
  }
6587
7482
  const id = getParam(req.params.id);
6588
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7483
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6589
7484
  if (!resolved) {
6590
7485
  res.status(404).json({ error: `Assignment "${id}" not found` });
6591
7486
  return;
@@ -6605,7 +7500,7 @@ ${entry}`;
6605
7500
  }
6606
7501
  const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
6607
7502
  await writeFileForce(scratchpadPath, nextContent);
6608
- const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
7503
+ const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
6609
7504
  res.json({ assignment, content: nextContent });
6610
7505
  } catch (error) {
6611
7506
  console.error("Error updating standalone scratchpad:", error);
@@ -6614,12 +7509,12 @@ ${entry}`;
6614
7509
  });
6615
7510
  router.post("/api/assignments/:id/handoff/entries", async (req, res) => {
6616
7511
  try {
6617
- if (!assignmentsDir) {
7512
+ if (!assignmentsDir2) {
6618
7513
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6619
7514
  return;
6620
7515
  }
6621
7516
  const id = getParam(req.params.id);
6622
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7517
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6623
7518
  if (!resolved) {
6624
7519
  res.status(404).json({ error: `Assignment "${id}" not found` });
6625
7520
  return;
@@ -6645,7 +7540,7 @@ ${entry}`;
6645
7540
  "No handoffs recorded yet."
6646
7541
  );
6647
7542
  await writeFileForce(handoffPath, nextContent);
6648
- const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
7543
+ const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
6649
7544
  res.status(201).json({ assignment, content: nextContent });
6650
7545
  } catch (error) {
6651
7546
  console.error("Error appending standalone handoff entry:", error);
@@ -6654,12 +7549,12 @@ ${entry}`;
6654
7549
  });
6655
7550
  router.post("/api/assignments/:id/decision-record/entries", async (req, res) => {
6656
7551
  try {
6657
- if (!assignmentsDir) {
7552
+ if (!assignmentsDir2) {
6658
7553
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6659
7554
  return;
6660
7555
  }
6661
7556
  const id = getParam(req.params.id);
6662
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7557
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6663
7558
  if (!resolved) {
6664
7559
  res.status(404).json({ error: `Assignment "${id}" not found` });
6665
7560
  return;
@@ -6685,7 +7580,7 @@ ${entry}`;
6685
7580
  "No decisions recorded yet."
6686
7581
  );
6687
7582
  await writeFileForce(decisionPath, nextContent);
6688
- const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
7583
+ const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
6689
7584
  res.status(201).json({ assignment, content: nextContent });
6690
7585
  } catch (error) {
6691
7586
  console.error("Error appending standalone decision entry:", error);
@@ -6694,12 +7589,12 @@ ${entry}`;
6694
7589
  });
6695
7590
  router.post("/api/assignments/:id/status-override", async (req, res) => {
6696
7591
  try {
6697
- if (!assignmentsDir) {
7592
+ if (!assignmentsDir2) {
6698
7593
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6699
7594
  return;
6700
7595
  }
6701
7596
  const id = getParam(req.params.id);
6702
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7597
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6703
7598
  if (!resolved) {
6704
7599
  res.status(404).json({ error: `Assignment "${id}" not found` });
6705
7600
  return;
@@ -6723,7 +7618,7 @@ ${entry}`;
6723
7618
  content = setTopLevelField(content, "blockedReason", null);
6724
7619
  }
6725
7620
  await writeFileForce(assignmentPath, content);
6726
- const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
7621
+ const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
6727
7622
  res.json({ assignment });
6728
7623
  } catch (error) {
6729
7624
  console.error("Error overriding standalone status:", error);
@@ -6732,12 +7627,12 @@ ${entry}`;
6732
7627
  });
6733
7628
  router.patch("/api/assignments/:id/acceptance-criteria/:index", async (req, res) => {
6734
7629
  try {
6735
- if (!assignmentsDir) {
7630
+ if (!assignmentsDir2) {
6736
7631
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6737
7632
  return;
6738
7633
  }
6739
7634
  const id = getParam(req.params.id);
6740
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7635
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6741
7636
  if (!resolved) {
6742
7637
  res.status(404).json({ error: `Assignment "${id}" not found` });
6743
7638
  return;
@@ -6761,7 +7656,7 @@ ${entry}`;
6761
7656
  }
6762
7657
  const nextContent = setTopLevelField(result.content, "updated", nowTimestamp());
6763
7658
  await writeFileForce(assignmentPath, nextContent);
6764
- const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
7659
+ const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
6765
7660
  res.json({ assignment, content: nextContent });
6766
7661
  } catch (error) {
6767
7662
  console.error("Error toggling standalone acceptance criterion:", error);
@@ -6770,13 +7665,13 @@ ${entry}`;
6770
7665
  });
6771
7666
  router.post("/api/assignments/:id/transitions/:command", async (req, res) => {
6772
7667
  try {
6773
- if (!assignmentsDir) {
7668
+ if (!assignmentsDir2) {
6774
7669
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
6775
7670
  return;
6776
7671
  }
6777
7672
  const id = getParam(req.params.id);
6778
7673
  const command = getParam(req.params.command);
6779
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
7674
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, id);
6780
7675
  if (!resolved) {
6781
7676
  res.status(404).json({ error: `Assignment "${id}" not found` });
6782
7677
  return;
@@ -6794,7 +7689,7 @@ ${entry}`;
6794
7689
  res.status(400).json({ error: transitionResult.message, fromStatus: transitionResult.fromStatus });
6795
7690
  return;
6796
7691
  }
6797
- const detail = resolved.standalone ? await getAssignmentDetailById(projectsDir, assignmentsDir, id) : await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
7692
+ const detail = resolved.standalone ? await getAssignmentDetailById(projectsDir, assignmentsDir2, id) : await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
6798
7693
  res.json({ assignment: detail, warnings: transitionResult.warnings ?? [] });
6799
7694
  } catch (error) {
6800
7695
  console.error("Error transitioning by id:", error);
@@ -6889,11 +7784,11 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
6889
7784
  init_servers();
6890
7785
  init_scanner();
6891
7786
  import { Router as Router2 } from "express";
6892
- function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
7787
+ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
6893
7788
  const router = Router2();
6894
7789
  router.get("/", async (_req, res) => {
6895
7790
  try {
6896
- const result = await scanAllSessions(serversDir2, projectsDir, { assignmentsDir });
7791
+ const result = await scanAllSessions(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 });
6897
7792
  res.json(result);
6898
7793
  } catch (error) {
6899
7794
  res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
@@ -6901,7 +7796,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
6901
7796
  });
6902
7797
  router.get("/:name", async (req, res) => {
6903
7798
  try {
6904
- const session = await scanSingleSession(serversDir2, projectsDir, req.params.name, { assignmentsDir });
7799
+ const session = await scanSingleSession(serversDir2, projectsDir, req.params.name, { assignmentsDir: assignmentsDir2 });
6905
7800
  if (!session) {
6906
7801
  res.status(404).json({ error: "Session not found" });
6907
7802
  return;
@@ -6952,7 +7847,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
6952
7847
  await updateLastRefreshed(serversDir2, name);
6953
7848
  }
6954
7849
  clearScanCache();
6955
- const result = await scanAllSessions(serversDir2, projectsDir, { bypassCache: true, assignmentsDir });
7850
+ const result = await scanAllSessions(serversDir2, projectsDir, { bypassCache: true, assignmentsDir: assignmentsDir2 });
6956
7851
  res.json(result);
6957
7852
  } catch (error) {
6958
7853
  res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
@@ -6967,7 +7862,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
6967
7862
  }
6968
7863
  await updateLastRefreshed(serversDir2, req.params.name);
6969
7864
  clearScanCache();
6970
- const session = await scanSingleSession(serversDir2, projectsDir, req.params.name, { assignmentsDir });
7865
+ const session = await scanSingleSession(serversDir2, projectsDir, req.params.name, { assignmentsDir: assignmentsDir2 });
6971
7866
  res.json(session);
6972
7867
  } catch (error) {
6973
7868
  res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
@@ -7006,11 +7901,11 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
7006
7901
  import { Router as Router3 } from "express";
7007
7902
  import { resolve as resolve13 } from "path";
7008
7903
  init_fs();
7009
- function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
7904
+ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
7010
7905
  const router = Router3();
7011
7906
  router.get("/", async (_req, res) => {
7012
7907
  try {
7013
- await reconcileActiveSessions(projectsDir, assignmentsDir);
7908
+ await reconcileActiveSessions(projectsDir, assignmentsDir2);
7014
7909
  const sessions = await listAllSessions(projectsDir);
7015
7910
  res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
7016
7911
  } catch (error) {
@@ -7026,7 +7921,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
7026
7921
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
7027
7922
  return;
7028
7923
  }
7029
- await reconcileActiveSessions(projectsDir, assignmentsDir);
7924
+ await reconcileActiveSessions(projectsDir, assignmentsDir2);
7030
7925
  const sessions = await listProjectSessions(projectsDir, projectSlug, assignment);
7031
7926
  res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
7032
7927
  } catch (error) {
@@ -7114,12 +8009,14 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
7114
8009
  // src/dashboard/api-playbooks.ts
7115
8010
  init_api();
7116
8011
  init_parser();
7117
- import { Router as Router4 } from "express";
7118
- import { resolve as resolve14 } from "path";
7119
- import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
8012
+ init_slug();
7120
8013
  init_timestamp();
7121
8014
  init_fs();
8015
+ init_playbook();
7122
8016
  init_playbooks();
8017
+ import { Router as Router4 } from "express";
8018
+ import { resolve as resolve14 } from "path";
8019
+ import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
7123
8020
  function createPlaybooksRouter(playbooksDir2) {
7124
8021
  const router = Router4();
7125
8022
  router.get("/", async (_req, res) => {
@@ -7296,6 +8193,11 @@ function withLock(lockKey, fn) {
7296
8193
  function wsLock(workspace, fn) {
7297
8194
  return withLock(`ws:${workspace}`, fn);
7298
8195
  }
8196
+ function touchItem(item) {
8197
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8198
+ if (item.createdAt === null) item.createdAt = now;
8199
+ item.updatedAt = now;
8200
+ }
7299
8201
  function createTodosRouter(todosDir2, broadcast) {
7300
8202
  const router = Router5();
7301
8203
  function broadcastUpdate() {
@@ -7358,12 +8260,18 @@ function createTodosRouter(todosDir2, broadcast) {
7358
8260
  const checklist = await readChecklist(todosDir2, workspace);
7359
8261
  const existingIds = new Set(checklist.items.map((i) => i.id));
7360
8262
  const id = generateUniqueId(existingIds);
8263
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7361
8264
  const newItem = {
7362
8265
  id,
7363
8266
  description,
7364
8267
  status: "open",
7365
8268
  tags: Array.isArray(tags) ? tags : [],
7366
- session: null
8269
+ session: null,
8270
+ branch: null,
8271
+ worktreePath: null,
8272
+ createdAt: now,
8273
+ updatedAt: now,
8274
+ planDir: null
7367
8275
  };
7368
8276
  checklist.items.push(newItem);
7369
8277
  await writeChecklist(todosDir2, checklist);
@@ -7418,7 +8326,7 @@ function createTodosRouter(todosDir2, broadcast) {
7418
8326
  router.post("/:workspace/archive", async (req, res) => {
7419
8327
  try {
7420
8328
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
7421
- const { resolve: resolve19 } = await import("path");
8329
+ const { resolve: resolve20 } = await import("path");
7422
8330
  const { readFile: readFile15 } = await import("fs/promises");
7423
8331
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
7424
8332
  const workspace = getWorkspaceParam(req.params.workspace);
@@ -7435,7 +8343,7 @@ function createTodosRouter(todosDir2, broadcast) {
7435
8343
  (e) => e.itemIds.every((id) => completedIds.has(id))
7436
8344
  );
7437
8345
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
7438
- await ensureDir(resolve19(todosDir2, "archive"));
8346
+ await ensureDir(resolve20(todosDir2, "archive"));
7439
8347
  let archContent = "";
7440
8348
  if (await fileExists(archFile)) {
7441
8349
  archContent = await readFile15(archFile, "utf-8");
@@ -7513,6 +8421,7 @@ workspace: ${workspace}
7513
8421
  if (!item) return null;
7514
8422
  if (req.body.description !== void 0) item.description = req.body.description;
7515
8423
  if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
8424
+ touchItem(item);
7516
8425
  await writeChecklist(todosDir2, checklist);
7517
8426
  return { ...item };
7518
8427
  });
@@ -7557,6 +8466,9 @@ workspace: ${workspace}
7557
8466
  if (item.status === "in_progress") return { error: "conflict", session: item.session };
7558
8467
  item.status = "in_progress";
7559
8468
  item.session = req.body.session || null;
8469
+ if (req.body.branch) item.branch = req.body.branch;
8470
+ if (req.body.worktreePath) item.worktreePath = req.body.worktreePath;
8471
+ touchItem(item);
7560
8472
  await writeChecklist(todosDir2, checklist);
7561
8473
  return { item: { ...item } };
7562
8474
  });
@@ -7583,13 +8495,15 @@ workspace: ${workspace}
7583
8495
  if (!item) return null;
7584
8496
  item.status = "completed";
7585
8497
  item.session = null;
8498
+ const branchForLog = req.body.branch || item.branch || null;
8499
+ touchItem(item);
7586
8500
  await writeChecklist(todosDir2, checklist);
7587
8501
  const entry = {
7588
8502
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7589
8503
  itemIds: [item.id],
7590
8504
  items: item.description,
7591
8505
  session: req.body.session || null,
7592
- branch: req.body.branch || null,
8506
+ branch: branchForLog,
7593
8507
  summary: req.body.summary || "Completed.",
7594
8508
  blockers: null,
7595
8509
  status: null
@@ -7617,6 +8531,7 @@ workspace: ${workspace}
7617
8531
  if (!item) return null;
7618
8532
  item.status = "blocked";
7619
8533
  item.session = null;
8534
+ touchItem(item);
7620
8535
  await writeChecklist(todosDir2, checklist);
7621
8536
  const entry = {
7622
8537
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7650,6 +8565,7 @@ workspace: ${workspace}
7650
8565
  if (!item) return null;
7651
8566
  item.status = "open";
7652
8567
  item.session = null;
8568
+ touchItem(item);
7653
8569
  await writeChecklist(todosDir2, checklist);
7654
8570
  return { ...item };
7655
8571
  });
@@ -7663,6 +8579,113 @@ workspace: ${workspace}
7663
8579
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reopen todo" });
7664
8580
  }
7665
8581
  });
8582
+ router.post("/:workspace/promote", async (req, res) => {
8583
+ try {
8584
+ const workspace = getWorkspaceParam(req.params.workspace);
8585
+ const { todoIds, mode, target, title, type, priority, keepSource } = req.body ?? {};
8586
+ if (!Array.isArray(todoIds) || todoIds.length === 0) {
8587
+ res.status(400).json({ error: "todoIds (non-empty array of strings) is required" });
8588
+ return;
8589
+ }
8590
+ if (mode !== "new-assignment" && mode !== "to-assignment") {
8591
+ res.status(400).json({ error: 'mode must be "new-assignment" or "to-assignment"' });
8592
+ return;
8593
+ }
8594
+ const result = await wsLock(workspace, async () => {
8595
+ const checklist = await readChecklist(todosDir2, workspace);
8596
+ const items = [];
8597
+ for (const id of todoIds) {
8598
+ const item = checklist.items.find((i) => i.id === id);
8599
+ if (!item) return { error: `Todo "${id}" not found` };
8600
+ if (item.status === "completed") return { error: `Todo "${id}" is already completed` };
8601
+ items.push(item);
8602
+ }
8603
+ const scopeLabel = workspace === "_global" ? "_global" : `workspace:${workspace}`;
8604
+ const { resolve: resolvePath } = await import("path");
8605
+ const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8606
+ const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
8607
+ const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
8608
+ const { readFile: readFile15 } = await import("fs/promises");
8609
+ const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
8610
+ const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
8611
+ let assignmentRef;
8612
+ let assignmentDir;
8613
+ if (mode === "new-assignment") {
8614
+ const targetProject = target?.project;
8615
+ if (!targetProject) return { error: "target.project is required for new-assignment mode" };
8616
+ if (items.length > 1 && !title) return { error: "title is required when promoting multiple todos" };
8617
+ const { createAssignmentCommand: createAssignmentCommand2 } = await Promise.resolve().then(() => (init_create_assignment(), create_assignment_exports));
8618
+ const created = await createAssignmentCommand2(title || items[0].description, {
8619
+ project: targetProject,
8620
+ type,
8621
+ priority,
8622
+ withTodos: true,
8623
+ silent: true
8624
+ });
8625
+ assignmentDir = created.assignmentDir;
8626
+ assignmentRef = `${created.projectSlug}/${created.slug}`;
8627
+ } else {
8628
+ const tg = target?.assignment || "";
8629
+ if (!tg) return { error: "target.assignment is required for to-assignment mode" };
8630
+ if (tg.includes("/")) {
8631
+ const parts = tg.split("/");
8632
+ if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
8633
+ const config = await readConfig2();
8634
+ assignmentDir = resolvePath(config.defaultProjectDir, parts[0], "assignments", parts[1]);
8635
+ assignmentRef = `${parts[0]}/${parts[1]}`;
8636
+ } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
8637
+ assignmentDir = resolvePath(assignmentsDirFn(), tg);
8638
+ assignmentRef = tg;
8639
+ } else {
8640
+ return { error: `Invalid target.assignment "${tg}"` };
8641
+ }
8642
+ const assignmentMdPath2 = resolvePath(assignmentDir, "assignment.md");
8643
+ if (!await fileExists2(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
8644
+ }
8645
+ const assignmentMdPath = resolvePath(assignmentDir, "assignment.md");
8646
+ let content = await readFile15(assignmentMdPath, "utf-8");
8647
+ content = appendTodosToAssignmentBody2(
8648
+ content,
8649
+ items.map((it) => ({
8650
+ description: it.description,
8651
+ trace: `promoted from t:${it.id} in ${scopeLabel}`
8652
+ }))
8653
+ );
8654
+ content = touchAssignmentUpdated2(content, nowTimestamp3());
8655
+ await writeFileForce2(assignmentMdPath, content);
8656
+ if (!keepSource) {
8657
+ for (const item of items) {
8658
+ item.status = "completed";
8659
+ item.session = null;
8660
+ touchItem(item);
8661
+ }
8662
+ await writeChecklist(todosDir2, checklist);
8663
+ for (const item of items) {
8664
+ const entry = {
8665
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8666
+ itemIds: [item.id],
8667
+ items: item.description,
8668
+ session: null,
8669
+ branch: item.branch || null,
8670
+ summary: `Promoted to assignment ${assignmentRef}`,
8671
+ blockers: null,
8672
+ status: null
8673
+ };
8674
+ await appendLogEntry2(todosDir2, workspace, entry);
8675
+ }
8676
+ }
8677
+ return { assignmentRef, assignmentDir, promoted: items.map((i) => i.id) };
8678
+ });
8679
+ if ("error" in result) {
8680
+ res.status(400).json({ error: result.error });
8681
+ return;
8682
+ }
8683
+ broadcastUpdate();
8684
+ res.json(result);
8685
+ } catch (error) {
8686
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
8687
+ }
8688
+ });
7666
8689
  router.post("/:workspace/:id/unblock", async (req, res) => {
7667
8690
  try {
7668
8691
  const workspace = getWorkspaceParam(req.params.workspace);
@@ -7672,6 +8695,7 @@ workspace: ${workspace}
7672
8695
  if (!item) return null;
7673
8696
  item.status = "open";
7674
8697
  item.session = null;
8698
+ touchItem(item);
7675
8699
  await writeChecklist(todosDir2, checklist);
7676
8700
  return { ...item };
7677
8701
  });
@@ -7692,9 +8716,10 @@ workspace: ${workspace}
7692
8716
  init_parser2();
7693
8717
  init_fs();
7694
8718
  init_paths();
8719
+ init_slug();
7695
8720
  import { Router as Router6 } from "express";
7696
8721
  import { mkdir as mkdir2, readFile as readFile13 } from "fs/promises";
7697
- import { resolve as resolve16 } from "path";
8722
+ import { resolve as resolve17 } from "path";
7698
8723
  var writeLocks2 = /* @__PURE__ */ new Map();
7699
8724
  function projLock(slug, fn) {
7700
8725
  const key = `proj:${slug}`;
@@ -7705,6 +8730,11 @@ function projLock(slug, fn) {
7705
8730
  }));
7706
8731
  return next;
7707
8732
  }
8733
+ function touchItem2(item) {
8734
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8735
+ if (item.createdAt === null) item.createdAt = now;
8736
+ item.updatedAt = now;
8737
+ }
7708
8738
  function getProjectIdParam(value) {
7709
8739
  if (Array.isArray(value)) return value[0] ?? "";
7710
8740
  return value ?? "";
@@ -7713,7 +8743,7 @@ function params(req) {
7713
8743
  return req.params;
7714
8744
  }
7715
8745
  async function projectExists(projectsDir, slug) {
7716
- return fileExists(resolve16(projectsDir, slug, "project.md"));
8746
+ return fileExists(resolve17(projectsDir, slug, "project.md"));
7717
8747
  }
7718
8748
  async function ensureProjectTodosDir(projectsDir, slug) {
7719
8749
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -7730,7 +8760,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
7730
8760
  throw err;
7731
8761
  }
7732
8762
  try {
7733
- await mkdir2(resolve16(todosDir2, "archive"), { recursive: false });
8763
+ await mkdir2(resolve17(todosDir2, "archive"), { recursive: false });
7734
8764
  } catch (err) {
7735
8765
  const code = err.code;
7736
8766
  if (code === "EEXIST") return;
@@ -7797,12 +8827,18 @@ function createProjectTodosRouter(projectsDir, broadcast) {
7797
8827
  const checklist = await readChecklist(todosDir2, slug);
7798
8828
  const existingIds = new Set(checklist.items.map((i) => i.id));
7799
8829
  const id = generateUniqueId(existingIds);
8830
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7800
8831
  const newItem = {
7801
8832
  id,
7802
8833
  description,
7803
8834
  status: "open",
7804
8835
  tags: Array.isArray(tags) ? tags : [],
7805
- session: null
8836
+ session: null,
8837
+ branch: null,
8838
+ worktreePath: null,
8839
+ createdAt: now,
8840
+ updatedAt: now,
8841
+ planDir: null
7806
8842
  };
7807
8843
  checklist.workspace = slug;
7808
8844
  checklist.items.push(newItem);
@@ -8005,6 +9041,7 @@ workspace: ${slug}
8005
9041
  if (!item) return null;
8006
9042
  if (req.body.description !== void 0) item.description = req.body.description;
8007
9043
  if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
9044
+ touchItem2(item);
8008
9045
  checklist.workspace = slug;
8009
9046
  await writeChecklist(todosDir2, checklist);
8010
9047
  return { ...item };
@@ -8081,6 +9118,9 @@ workspace: ${slug}
8081
9118
  if (item.status === "in_progress") return { error: "conflict", session: item.session };
8082
9119
  item.status = "in_progress";
8083
9120
  item.session = req.body.session || null;
9121
+ if (req.body.branch) item.branch = req.body.branch;
9122
+ if (req.body.worktreePath) item.worktreePath = req.body.worktreePath;
9123
+ touchItem2(item);
8084
9124
  checklist.workspace = slug;
8085
9125
  await writeChecklist(todosDir2, checklist);
8086
9126
  return { item: { ...item } };
@@ -8123,6 +9163,8 @@ workspace: ${slug}
8123
9163
  if (!item) return null;
8124
9164
  item.status = "completed";
8125
9165
  item.session = null;
9166
+ const branchForLog = req.body.branch || item.branch || null;
9167
+ touchItem2(item);
8126
9168
  checklist.workspace = slug;
8127
9169
  await writeChecklist(todosDir2, checklist);
8128
9170
  const entry = {
@@ -8130,7 +9172,7 @@ workspace: ${slug}
8130
9172
  itemIds: [item.id],
8131
9173
  items: item.description,
8132
9174
  session: req.body.session || null,
8133
- branch: req.body.branch || null,
9175
+ branch: branchForLog,
8134
9176
  summary: req.body.summary || "Completed.",
8135
9177
  blockers: null,
8136
9178
  status: null
@@ -8173,6 +9215,7 @@ workspace: ${slug}
8173
9215
  if (!item) return null;
8174
9216
  item.status = "blocked";
8175
9217
  item.session = null;
9218
+ touchItem2(item);
8176
9219
  checklist.workspace = slug;
8177
9220
  await writeChecklist(todosDir2, checklist);
8178
9221
  const entry = {
@@ -8222,6 +9265,7 @@ workspace: ${slug}
8222
9265
  if (!item) return null;
8223
9266
  item.status = "open";
8224
9267
  item.session = null;
9268
+ touchItem2(item);
8225
9269
  checklist.workspace = slug;
8226
9270
  await writeChecklist(todosDir2, checklist);
8227
9271
  return { ...item };
@@ -8260,6 +9304,7 @@ workspace: ${slug}
8260
9304
  if (!item) return null;
8261
9305
  item.status = "open";
8262
9306
  item.session = null;
9307
+ touchItem2(item);
8263
9308
  checklist.workspace = slug;
8264
9309
  await writeChecklist(todosDir2, checklist);
8265
9310
  return { ...item };
@@ -8296,7 +9341,7 @@ init_config2();
8296
9341
  import { execFile as execFile2 } from "child_process";
8297
9342
  import { promisify as promisify2 } from "util";
8298
9343
  import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
8299
- import { resolve as resolve17, join as join2 } from "path";
9344
+ import { resolve as resolve18, join as join2 } from "path";
8300
9345
  import { tmpdir } from "os";
8301
9346
  var exec2 = promisify2(execFile2);
8302
9347
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -8336,7 +9381,7 @@ async function resolveCategoryPath(category) {
8336
9381
  case "servers":
8337
9382
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
8338
9383
  case "config":
8339
- return { sourcePath: resolve17(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
9384
+ return { sourcePath: resolve18(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
8340
9385
  }
8341
9386
  }
8342
9387
  async function checkGitInstalled() {
@@ -8347,7 +9392,7 @@ async function checkGitInstalled() {
8347
9392
  }
8348
9393
  }
8349
9394
  async function acquireLock() {
8350
- const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
9395
+ const lockPath = resolve18(syntaurRoot(), LOCK_FILE_NAME);
8351
9396
  await ensureDir(syntaurRoot());
8352
9397
  try {
8353
9398
  const handle = await open(lockPath, "wx");
@@ -8394,7 +9439,7 @@ async function copyRecursive(src, dest) {
8394
9439
  await ensureDir(dest);
8395
9440
  await cp(src, dest, { recursive: true, force: true });
8396
9441
  } else {
8397
- await ensureDir(resolve17(dest, ".."));
9442
+ await ensureDir(resolve18(dest, ".."));
8398
9443
  await cp(src, dest, { force: true });
8399
9444
  }
8400
9445
  }
@@ -8442,7 +9487,7 @@ async function backupToGithub(overrides) {
8442
9487
  }
8443
9488
  if (category === "config") {
8444
9489
  const sanitized = await readSanitizedConfig(sourcePath);
8445
- await ensureDir(resolve17(destPath, ".."));
9490
+ await ensureDir(resolve18(destPath, ".."));
8446
9491
  await writeFile4(destPath, sanitized, "utf-8");
8447
9492
  } else {
8448
9493
  await copyRecursive(sourcePath, destPath);
@@ -8496,7 +9541,7 @@ async function backupToGithub(overrides) {
8496
9541
  }
8497
9542
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
8498
9543
  if (isFile) {
8499
- await ensureDir(resolve17(localPath, ".."));
9544
+ await ensureDir(resolve18(localPath, ".."));
8500
9545
  await cp(repoSrcPath, localPath, { force: true });
8501
9546
  return;
8502
9547
  }
@@ -8597,7 +9642,7 @@ async function restoreFromGithub(overrides) {
8597
9642
  }
8598
9643
  async function getBackupStatus() {
8599
9644
  const config = await readConfig();
8600
- const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
9645
+ const lockPath = resolve18(syntaurRoot(), LOCK_FILE_NAME);
8601
9646
  const locked = await fileExists(lockPath);
8602
9647
  return {
8603
9648
  repo: config.backup?.repo ?? null,
@@ -8763,10 +9808,10 @@ async function listAllTmuxSessions() {
8763
9808
  if (!output) return [];
8764
9809
  return output.split("\n").filter((line) => line.length > 0);
8765
9810
  }
8766
- async function discoverTmuxSessions(serversDir2, projectsDir, existingNames, assignmentsDir) {
9811
+ async function discoverTmuxSessions(serversDir2, projectsDir, existingNames, assignmentsDir2) {
8767
9812
  const tmuxAvailable = await checkTmuxAvailable();
8768
9813
  if (!tmuxAvailable) return false;
8769
- const workspaceRecords = await loadWorkspaceRecords(projectsDir, assignmentsDir);
9814
+ const workspaceRecords = await loadWorkspaceRecords(projectsDir, assignmentsDir2);
8770
9815
  if (workspaceRecords.length === 0) return false;
8771
9816
  const sessions = await listAllTmuxSessions();
8772
9817
  let changed = false;
@@ -8807,8 +9852,8 @@ async function getProcessCwd(pid) {
8807
9852
  }
8808
9853
  return null;
8809
9854
  }
8810
- async function discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids, assignmentsDir) {
8811
- const workspaceRecords = await loadWorkspaceRecords(projectsDir, assignmentsDir);
9855
+ async function discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids, assignmentsDir2) {
9856
+ const workspaceRecords = await loadWorkspaceRecords(projectsDir, assignmentsDir2);
8812
9857
  if (workspaceRecords.length === 0) return false;
8813
9858
  const lsofOutput = await getLsofOutput();
8814
9859
  if (!lsofOutput) return false;
@@ -8873,7 +9918,7 @@ async function isProcessAlive(pid) {
8873
9918
  return false;
8874
9919
  }
8875
9920
  }
8876
- async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir) {
9921
+ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir2) {
8877
9922
  const names = await listSessionFiles(serversDir2);
8878
9923
  const existingFiles = /* @__PURE__ */ new Map();
8879
9924
  for (const name of names) {
@@ -8885,8 +9930,8 @@ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir)
8885
9930
  existingFiles.delete(name);
8886
9931
  }
8887
9932
  const existingNames = new Set(existingFiles.keys());
8888
- const tmuxChanged = await discoverTmuxSessions(serversDir2, projectsDir, existingNames, assignmentsDir);
8889
- const processChanged = await discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids, assignmentsDir);
9933
+ const tmuxChanged = await discoverTmuxSessions(serversDir2, projectsDir, existingNames, assignmentsDir2);
9934
+ const processChanged = await discoverProcesses(serversDir2, projectsDir, existingFiles, excludePids, assignmentsDir2);
8890
9935
  if (tmuxChanged || processChanged || cleanupChanged) {
8891
9936
  clearScanCache();
8892
9937
  }
@@ -8894,7 +9939,7 @@ async function reconcile(serversDir2, projectsDir, excludePids, assignmentsDir)
8894
9939
 
8895
9940
  // src/dashboard/server.ts
8896
9941
  function createDashboardServer(options) {
8897
- const { port, projectsDir, assignmentsDir, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
9942
+ const { port, projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
8898
9943
  const app = express();
8899
9944
  const server = createServer(app);
8900
9945
  const wss = new WebSocketServer({ noServer: true });
@@ -8934,7 +9979,7 @@ function createDashboardServer(options) {
8934
9979
  (async () => {
8935
9980
  try {
8936
9981
  const configResult = await migrateLegacyConfig(
8937
- resolve18(syntaurRoot(), "config.md")
9982
+ resolve19(syntaurRoot(), "config.md")
8938
9983
  );
8939
9984
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
8940
9985
  const summary = summarizeMigration(projectResult, configResult);
@@ -8946,7 +9991,7 @@ function createDashboardServer(options) {
8946
9991
  app.use(express.json());
8947
9992
  app.get("/api/overview", async (_req, res) => {
8948
9993
  try {
8949
- const overview = await getOverview(projectsDir, serversDir2, assignmentsDir);
9994
+ const overview = await getOverview(projectsDir, serversDir2, assignmentsDir2);
8950
9995
  res.json(overview);
8951
9996
  } catch (error) {
8952
9997
  console.error("Error getting overview:", error);
@@ -8955,7 +10000,7 @@ function createDashboardServer(options) {
8955
10000
  });
8956
10001
  app.get("/api/attention", async (_req, res) => {
8957
10002
  try {
8958
- const attention = await getAttention(projectsDir, serversDir2, assignmentsDir);
10003
+ const attention = await getAttention(projectsDir, serversDir2, assignmentsDir2);
8959
10004
  res.json(attention);
8960
10005
  } catch (error) {
8961
10006
  console.error("Error getting attention queue:", error);
@@ -9059,6 +10104,78 @@ function createDashboardServer(options) {
9059
10104
  res.status(500).json({ error: "Failed to reset theme config" });
9060
10105
  }
9061
10106
  });
10107
+ app.get("/api/config/hotkeys", async (_req, res) => {
10108
+ try {
10109
+ const config = await readConfig();
10110
+ const bindings = config.hotkeys?.bindings ?? {};
10111
+ res.json({ bindings, custom: config.hotkeys !== null });
10112
+ } catch (error) {
10113
+ console.error("Error getting hotkeys config:", error);
10114
+ res.status(500).json({ error: "Failed to get hotkeys config" });
10115
+ }
10116
+ });
10117
+ app.put("/api/config/hotkeys", async (req, res) => {
10118
+ try {
10119
+ const raw = req.body && typeof req.body === "object" ? req.body : {};
10120
+ const incoming = raw.bindings;
10121
+ if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) {
10122
+ res.status(400).json({ error: "bindings must be an object keyed by action kind" });
10123
+ return;
10124
+ }
10125
+ const cleaned = {};
10126
+ for (const [rawKind, rawValue] of Object.entries(incoming)) {
10127
+ if (!isBindableActionKind(rawKind)) {
10128
+ res.status(400).json({
10129
+ error: `unknown action kind "${rawKind}" \u2014 expected one of: ${BINDABLE_ACTION_KINDS.join(", ")}`
10130
+ });
10131
+ return;
10132
+ }
10133
+ if (typeof rawValue !== "string" || rawValue.trim() === "") {
10134
+ res.status(400).json({ error: `binding for "${rawKind}" must be a non-empty string` });
10135
+ return;
10136
+ }
10137
+ const canonical = canonicalizeCombo(rawValue);
10138
+ if (!canonical) {
10139
+ res.status(400).json({ error: `binding for "${rawKind}" is not a valid combo` });
10140
+ return;
10141
+ }
10142
+ if (isReservedCombo(canonical)) {
10143
+ res.status(400).json({
10144
+ error: `combo "${canonical}" is reserved by a built-in shortcut`,
10145
+ kind: rawKind,
10146
+ combo: canonical
10147
+ });
10148
+ return;
10149
+ }
10150
+ cleaned[rawKind] = canonical;
10151
+ }
10152
+ const seenCombos = /* @__PURE__ */ new Map();
10153
+ for (const [kind, combo] of Object.entries(cleaned)) {
10154
+ if (seenCombos.has(combo)) {
10155
+ res.status(400).json({
10156
+ error: `combo "${combo}" is bound to multiple actions`,
10157
+ kinds: [seenCombos.get(combo), kind]
10158
+ });
10159
+ return;
10160
+ }
10161
+ seenCombos.set(combo, kind);
10162
+ }
10163
+ await writeHotkeyBindingsConfig({ bindings: cleaned });
10164
+ res.json({ bindings: cleaned, custom: Object.keys(cleaned).length > 0 });
10165
+ } catch (error) {
10166
+ console.error("Error saving hotkeys config:", error);
10167
+ res.status(500).json({ error: "Failed to save hotkeys config" });
10168
+ }
10169
+ });
10170
+ app.delete("/api/config/hotkeys", async (_req, res) => {
10171
+ try {
10172
+ await deleteHotkeyBindingsConfig();
10173
+ res.json({ bindings: {}, custom: false });
10174
+ } catch (error) {
10175
+ console.error("Error resetting hotkeys config:", error);
10176
+ res.status(500).json({ error: "Failed to reset hotkeys config" });
10177
+ }
10178
+ });
9062
10179
  app.get("/api/projects", async (req, res) => {
9063
10180
  try {
9064
10181
  let projects = await listProjects(projectsDir);
@@ -9078,7 +10195,7 @@ function createDashboardServer(options) {
9078
10195
  });
9079
10196
  app.get("/api/workspaces", async (_req, res) => {
9080
10197
  try {
9081
- const result = await listWorkspaces(projectsDir, assignmentsDir);
10198
+ const result = await listWorkspaces(projectsDir, assignmentsDir2);
9082
10199
  res.json(result);
9083
10200
  } catch (error) {
9084
10201
  console.error("Error listing workspaces:", error);
@@ -9112,7 +10229,7 @@ function createDashboardServer(options) {
9112
10229
  });
9113
10230
  app.get("/api/assignments", async (req, res) => {
9114
10231
  try {
9115
- const result = await listAssignmentsBoard(projectsDir, assignmentsDir);
10232
+ const result = await listAssignmentsBoard(projectsDir, assignmentsDir2);
9116
10233
  const workspaceParam = req.query.workspace;
9117
10234
  if (workspaceParam) {
9118
10235
  if (workspaceParam === "_ungrouped") {
@@ -9142,7 +10259,7 @@ function createDashboardServer(options) {
9142
10259
  });
9143
10260
  app.get("/api/assignments/:id", async (req, res) => {
9144
10261
  try {
9145
- const detail = await getAssignmentDetailById(projectsDir, assignmentsDir, req.params.id);
10262
+ const detail = await getAssignmentDetailById(projectsDir, assignmentsDir2, req.params.id);
9146
10263
  if (!detail) {
9147
10264
  res.status(404).json({ error: `Assignment "${req.params.id}" not found` });
9148
10265
  return;
@@ -9155,12 +10272,12 @@ function createDashboardServer(options) {
9155
10272
  });
9156
10273
  app.get("/api/assignments/:id/sessions", async (req, res) => {
9157
10274
  try {
9158
- const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, req.params.id);
10275
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir2, req.params.id);
9159
10276
  if (!resolved) {
9160
10277
  res.status(404).json({ error: `Assignment "${req.params.id}" not found` });
9161
10278
  return;
9162
10279
  }
9163
- await reconcileActiveSessions(projectsDir, assignmentsDir);
10280
+ await reconcileActiveSessions(projectsDir, assignmentsDir2);
9164
10281
  const sessions = await listSessionsByAssignment(
9165
10282
  resolved.standalone ? null : resolved.projectSlug,
9166
10283
  resolved.standalone ? resolved.id : resolved.assignmentSlug
@@ -9190,23 +10307,23 @@ function createDashboardServer(options) {
9190
10307
  res.status(500).json({ error: "Failed to get assignment detail" });
9191
10308
  }
9192
10309
  });
9193
- app.use(createWriteRouter(projectsDir, assignmentsDir));
9194
- app.use("/api/servers", createServersRouter(serversDir2, projectsDir, assignmentsDir));
9195
- app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir));
10310
+ app.use(createWriteRouter(projectsDir, assignmentsDir2));
10311
+ app.use("/api/servers", createServersRouter(serversDir2, projectsDir, assignmentsDir2));
10312
+ app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2));
9196
10313
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
9197
10314
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
9198
10315
  app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast));
9199
10316
  app.use("/api/backup", createBackupRouter());
9200
10317
  if (serveStaticUi && dashboardDistPath) {
9201
10318
  const sendOpts = { dotfiles: "allow" };
9202
- app.use("/assets", express.static(resolve18(dashboardDistPath, "assets"), sendOpts));
10319
+ app.use("/assets", express.static(resolve19(dashboardDistPath, "assets"), sendOpts));
9203
10320
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
9204
10321
  app.get("{*path}", async (req, res) => {
9205
10322
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
9206
10323
  res.status(404).json({ error: "Not Found" });
9207
10324
  return;
9208
10325
  }
9209
- const indexPath = resolve18(dashboardDistPath, "index.html");
10326
+ const indexPath = resolve19(dashboardDistPath, "index.html");
9210
10327
  if (!await fileExists(indexPath)) {
9211
10328
  res.status(503).send(
9212
10329
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -9226,13 +10343,13 @@ function createDashboardServer(options) {
9226
10343
  async start() {
9227
10344
  watcherHandle = createWatcher({
9228
10345
  projectsDir,
9229
- assignmentsDir,
10346
+ assignmentsDir: assignmentsDir2,
9230
10347
  serversDir: serversDir2,
9231
10348
  playbooksDir: playbooksDir2,
9232
10349
  todosDir: todosDir2,
9233
10350
  onMessage: broadcast
9234
10351
  });
9235
- startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir, excludePids: /* @__PURE__ */ new Set([process.pid]) });
10352
+ startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
9236
10353
  return new Promise((resolvePromise, reject) => {
9237
10354
  server.on("error", (err) => {
9238
10355
  if (err.code === "EADDRINUSE") {
@@ -9244,7 +10361,7 @@ function createDashboardServer(options) {
9244
10361
  }
9245
10362
  });
9246
10363
  server.listen(port, () => {
9247
- const portFile = resolve18(syntaurRoot(), "dashboard-port");
10364
+ const portFile = resolve19(syntaurRoot(), "dashboard-port");
9248
10365
  writeFile5(portFile, String(port), "utf-8").catch(() => {
9249
10366
  });
9250
10367
  resolvePromise();
@@ -9261,7 +10378,7 @@ function createDashboardServer(options) {
9261
10378
  client.terminate();
9262
10379
  }
9263
10380
  clients.clear();
9264
- const portFile = resolve18(syntaurRoot(), "dashboard-port");
10381
+ const portFile = resolve19(syntaurRoot(), "dashboard-port");
9265
10382
  await unlink4(portFile).catch(() => {
9266
10383
  });
9267
10384
  server.closeAllConnections?.();