syntaur 0.4.4 → 0.5.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 (72) hide show
  1. package/dashboard/dist/assets/{_basePickBy-CWivToyi.js → _basePickBy-Bcut0btZ.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-C-qj6E4l.js → _baseUniq-AQSP2JEk.js} +1 -1
  3. package/dashboard/dist/assets/{arc-Dn5BIqMa.js → arc-BLTpY9lc.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-D5D0K7rY.js → architectureDiagram-2XIMDMQ5-CJtwMY_X.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DVmYPMbu.js → blockDiagram-WCTKOSBZ-Don-O7X7.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEasxbnl.js → c4Diagram-IC4MRINW-C_M3yTTB.js} +1 -1
  7. package/dashboard/dist/assets/channel-BfXmPwE5.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-LDIrtI5E.js → chunk-4BX2VUAB-CGss0jXe.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-CaEBUJYu.js → chunk-55IACEB6-BatoPJga.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-B-GjCpdr.js → chunk-FMBD7UC4-DxH4wO82.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-BLVVcezm.js → chunk-JSJVCQXG-BL3izAFQ.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-DqCNEw4h.js → chunk-KX2RTZJC-GnqXwnge.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-BCPbFf5I.js → chunk-NQ4KR5QH-gvCn4QMb.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-Ci0C85q_.js → chunk-QZHKN3VN-CYGWogyi.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-VVhAMMYU.js → chunk-WL4C6EOR-D9mVTQ1F.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-D7_G1qy0.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D7_G1qy0.js +1 -0
  18. package/dashboard/dist/assets/clone-BKG-N796.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CO9uwgYO.js → cose-bilkent-S5V4N54A-CUWQCKt4.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-bwLLXcL4.js → dagre-KLK3FWXG-CH3ijEvV.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-RuS5R6V1.js → diagram-E7M64L7V-sq83lpV1.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-BQDJAHQd.js → diagram-IFDJBPK2-BzQG_rtq.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-yLEsgzE5.js → diagram-P4PSJMXO-Dg0eZn0q.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-na6dUhY0.js → erDiagram-INFDFZHY-4b9eQ0uj.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BIcrzwJR.js → flowDiagram-PKNHOUZH-C9fzKcsZ.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DHWRJn-D.js → ganttDiagram-A5KZAMGK-Bzt6i9SH.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-LGxDjL71.js → gitGraphDiagram-K3NZZRJ6-D0wFOagh.js} +1 -1
  28. package/dashboard/dist/assets/{graph-BUqNu277.js → graph-EEIGvqDh.js} +1 -1
  29. package/dashboard/dist/assets/index-Bu6ma6my.css +1 -0
  30. package/dashboard/dist/assets/index-C7f0ySJE.js +481 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DidoA2hb.js → infoDiagram-LFFYTUFH-DLYMsj1D.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CdlZkbhV.js → ishikawaDiagram-PHBUUO56-DVebKkzl.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-luhcz_gn.js → journeyDiagram-4ABVD52K-BsmgOWVw.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Coidw9XE.js → kanban-definition-K7BYSVSG-BTnHf0ey.js} +1 -1
  35. package/dashboard/dist/assets/{layout-_aBAAleE.js → layout-BbM7HRvv.js} +1 -1
  36. package/dashboard/dist/assets/{linear-D8mFnDSx.js → linear-C37bJKPO.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-BpP2keU-.js → mermaid.core-MZ_JgnRL.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-LjvrTe2z.js → mindmap-definition-YRQLILUH-CgHS4hFo.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CkA2iU6e.js → pieDiagram-SKSYHLDU-CmAgopJe.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BRmhKHQG.js → quadrantDiagram-337W2JSQ-BvzYUPR6.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BYCQ4uFX.js → requirementDiagram-Z7DCOOCP-Bs52VP7k.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-C8SVk50M.js → sankeyDiagram-WA2Y5GQK-aXvGPR1o.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CbA_2lnP.js → sequenceDiagram-2WXFIKYE-CzgcfU6K.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D6ZtjAHE.js → stateDiagram-RAJIS63D-BXBJf9Hq.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-QqOtsuOs.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-B2Uf-emK.js → timeline-definition-YZTLITO2-BsXp26Ai.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-CYnFKsuJ.js → treemap-KZPCXAKY-C3WbDii1.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cnj0qiDO.js → vennDiagram-LZ73GAT5-B28LMHWd.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BJy2mbL9.js → xychartDiagram-JWTSCODW-C3Xwz8mS.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +1329 -402
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +1591 -533
  54. package/dist/index.js.map +1 -1
  55. package/examples/playbooks/assignment-creation.md +19 -0
  56. package/examples/playbooks/assignment-planning.md +28 -0
  57. package/package.json +1 -1
  58. package/platforms/claude-code/.orphaned_at +1 -0
  59. package/platforms/claude-code/agents/syntaur-expert.md +2 -2
  60. package/platforms/claude-code/commands/create-assignment/create-assignment.md +1 -1
  61. package/platforms/codex/agents/syntaur-operator.md +2 -2
  62. package/vendor/syntaur-skills/README.md +12 -0
  63. package/vendor/syntaur-skills/skills/create-assignment/SKILL.md +5 -4
  64. package/dashboard/dist/assets/channel-DqU_8tiy.js +0 -1
  65. package/dashboard/dist/assets/classDiagram-VBA2DB6C-D29Eeoe8.js +0 -1
  66. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D29Eeoe8.js +0 -1
  67. package/dashboard/dist/assets/clone-Bok8Q3Jj.js +0 -1
  68. package/dashboard/dist/assets/index-D-fepllQ.js +0 -481
  69. package/dashboard/dist/assets/index-DnHyQJJH.css +0 -1
  70. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DeYN4wV6.js +0 -1
  71. package/examples/playbooks/plan-versioning.md +0 -36
  72. package/examples/playbooks/read-before-plan.md +0 -30
@@ -36,6 +36,9 @@ function playbooksDir() {
36
36
  function todosDir() {
37
37
  return resolve(syntaurRoot(), "todos");
38
38
  }
39
+ function projectTodosDir(projectsDir, projectSlug) {
40
+ return resolve(projectsDir, projectSlug, "todos");
41
+ }
39
42
  var init_paths = __esm({
40
43
  "src/utils/paths.ts"() {
41
44
  "use strict";
@@ -457,10 +460,10 @@ var init_lifecycle = __esm({
457
460
  });
458
461
 
459
462
  // src/templates/config.ts
460
- function renderConfig(params) {
463
+ function renderConfig(params2) {
461
464
  return `---
462
465
  version: "1.0"
463
- defaultProjectDir: ${params.defaultProjectDir}
466
+ defaultProjectDir: ${params2.defaultProjectDir}
464
467
  onboarding:
465
468
  completed: false
466
469
  agentDefaults:
@@ -622,6 +625,27 @@ var init_fs_migration = __esm({
622
625
  // src/utils/config.ts
623
626
  import { readFile as readFile3 } from "fs/promises";
624
627
  import { resolve as resolve4, isAbsolute } from "path";
628
+ function cloneDefaultConfig() {
629
+ return {
630
+ ...DEFAULT_CONFIG,
631
+ onboarding: { ...DEFAULT_CONFIG.onboarding },
632
+ agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
633
+ integrations: { ...DEFAULT_CONFIG.integrations },
634
+ backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
635
+ statuses: DEFAULT_CONFIG.statuses ? {
636
+ statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
637
+ order: [...DEFAULT_CONFIG.statuses.order],
638
+ transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
639
+ } : null,
640
+ types: DEFAULT_CONFIG.types ? {
641
+ definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
642
+ default: DEFAULT_CONFIG.types.default
643
+ } : null,
644
+ playbooks: {
645
+ disabled: [...DEFAULT_CONFIG.playbooks.disabled]
646
+ }
647
+ };
648
+ }
625
649
  function parseFrontmatter(content) {
626
650
  const match = content.match(/^---\n([\s\S]*?)\n---/);
627
651
  if (!match) return {};
@@ -774,6 +798,82 @@ function serializeBackupConfig(backup) {
774
798
  lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
775
799
  return lines.join("\n");
776
800
  }
801
+ function serializePlaybooksConfig(playbooks) {
802
+ if (!playbooks.disabled || playbooks.disabled.length === 0) {
803
+ return null;
804
+ }
805
+ const lines = ["playbooks:", " disabled:"];
806
+ for (const slug of playbooks.disabled) {
807
+ lines.push(` - ${slug}`);
808
+ }
809
+ return lines.join("\n");
810
+ }
811
+ function parsePlaybooksConfig(fmBlock) {
812
+ const blockStart = fmBlock.match(/^playbooks:\s*$/m);
813
+ if (!blockStart) {
814
+ return { disabled: [] };
815
+ }
816
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
817
+ const remaining = fmBlock.slice(startIdx).split("\n");
818
+ const disabled = [];
819
+ let currentSection = null;
820
+ for (const line of remaining) {
821
+ const trimmed = line.trimStart();
822
+ const indent = line.length - trimmed.length;
823
+ if (indent === 0 && trimmed.length > 0) break;
824
+ if (trimmed === "") continue;
825
+ if (indent === 2 && trimmed.startsWith("disabled:")) {
826
+ currentSection = "disabled";
827
+ const afterColon = trimmed.slice("disabled:".length).trim();
828
+ if (afterColon === "[]" || afterColon === "") {
829
+ continue;
830
+ }
831
+ continue;
832
+ }
833
+ if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
834
+ const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
835
+ if (raw.length === 0) continue;
836
+ if (/\s/.test(raw)) {
837
+ console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
838
+ continue;
839
+ }
840
+ disabled.push(raw);
841
+ continue;
842
+ }
843
+ }
844
+ return { disabled };
845
+ }
846
+ async function updatePlaybooksConfig(playbooks) {
847
+ const configPath = resolve4(syntaurRoot(), "config.md");
848
+ const current = (await readConfig()).playbooks;
849
+ const nextPlaybooks = {
850
+ disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
851
+ };
852
+ const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
853
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
854
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
855
+ if (!fmMatch) {
856
+ const bodyBlock = playbooksBlock ? `${playbooksBlock}
857
+ ` : "";
858
+ const content = `---
859
+ version: "2.0"
860
+ defaultProjectDir: ${defaultProjectDir()}
861
+ ${bodyBlock}---
862
+ ${existing}`;
863
+ await writeFileForce(configPath, content);
864
+ return;
865
+ }
866
+ const fmBlock = fmMatch[2];
867
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
868
+ const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
869
+ const newFm = playbooksBlock ? `${cleanedFm}
870
+ ${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
871
+ const normalizedFm = newFm.replace(/\n+$/, "");
872
+ const newContent = `---
873
+ ${normalizedFm}
874
+ ---${afterFrontmatter}`;
875
+ await writeFileForce(configPath, newContent);
876
+ }
777
877
  function stripTopLevelBlock(fmBlock, key) {
778
878
  const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
779
879
  if (!blockStart) {
@@ -915,7 +1015,7 @@ ${normalizedFm}
915
1015
  async function readConfig() {
916
1016
  const configPath = resolve4(syntaurRoot(), "config.md");
917
1017
  if (!await fileExists(configPath)) {
918
- return { ...DEFAULT_CONFIG };
1018
+ return cloneDefaultConfig();
919
1019
  }
920
1020
  if (!migratedConfigPaths.has(configPath)) {
921
1021
  migratedConfigPaths.add(configPath);
@@ -925,7 +1025,7 @@ async function readConfig() {
925
1025
  const fm = parseFrontmatter(content);
926
1026
  if (Object.keys(fm).length === 0) {
927
1027
  console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
928
- return { ...DEFAULT_CONFIG };
1028
+ return cloneDefaultConfig();
929
1029
  }
930
1030
  let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
931
1031
  if (!isAbsolute(projectDir)) {
@@ -934,6 +1034,7 @@ async function readConfig() {
934
1034
  );
935
1035
  projectDir = DEFAULT_CONFIG.defaultProjectDir;
936
1036
  }
1037
+ const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
937
1038
  return {
938
1039
  version: fm["version"] || DEFAULT_CONFIG.version,
939
1040
  defaultProjectDir: projectDir,
@@ -965,7 +1066,8 @@ async function readConfig() {
965
1066
  lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
966
1067
  } : null,
967
1068
  statuses: parseStatusConfig(content),
968
- types: null
1069
+ types: null,
1070
+ playbooks: parsePlaybooksConfig(fmBlock)
969
1071
  };
970
1072
  }
971
1073
  var DEFAULT_CONFIG, migratedConfigPaths;
@@ -993,7 +1095,10 @@ var init_config2 = __esm({
993
1095
  },
994
1096
  backup: null,
995
1097
  statuses: null,
996
- types: null
1098
+ types: null,
1099
+ playbooks: {
1100
+ disabled: []
1101
+ }
997
1102
  };
998
1103
  migratedConfigPaths = /* @__PURE__ */ new Set();
999
1104
  }
@@ -1128,6 +1233,7 @@ function parseAssignmentFull(fileContent) {
1128
1233
  slug: getField(fm, "slug") ?? "",
1129
1234
  title: getField(fm, "title") ?? "",
1130
1235
  project: getField(fm, "project"),
1236
+ workspaceGroup: getField(fm, "workspaceGroup"),
1131
1237
  type: getField(fm, "type"),
1132
1238
  status: getField(fm, "status") ?? "pending",
1133
1239
  priority: getField(fm, "priority") ?? "medium",
@@ -1284,47 +1390,160 @@ var init_parser = __esm({
1284
1390
  }
1285
1391
  });
1286
1392
 
1287
- // src/utils/assignment-resolver.ts
1393
+ // src/utils/playbooks.ts
1288
1394
  import { resolve as resolve5 } from "path";
1289
1395
  import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
1396
+ function isVisiblePlaybookFile(name, isFile) {
1397
+ return isFile && name.endsWith(".md") && !name.startsWith("_") && name !== "manifest.md";
1398
+ }
1399
+ async function resolvePlaybookSlug(playbooksDir2, slug) {
1400
+ if (!await fileExists(playbooksDir2)) return null;
1401
+ const entries = await readdir2(playbooksDir2, { withFileTypes: true });
1402
+ let filenameStemFallback = null;
1403
+ for (const entry of entries) {
1404
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
1405
+ const filePath = resolve5(playbooksDir2, entry.name);
1406
+ const raw = await readFile4(filePath, "utf-8");
1407
+ const parsed = parsePlaybook(raw);
1408
+ const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
1409
+ if (canonical === slug) {
1410
+ return { filename: entry.name, slug: canonical, parsed };
1411
+ }
1412
+ if (!parsed.slug && entry.name.replace(/\.md$/, "") === slug) {
1413
+ filenameStemFallback = { filename: entry.name, slug: canonical, parsed };
1414
+ }
1415
+ }
1416
+ return filenameStemFallback;
1417
+ }
1418
+ async function setPlaybookEnabled(playbooksDir2, slug, enabled) {
1419
+ const resolved = await resolvePlaybookSlug(playbooksDir2, slug);
1420
+ if (!resolved) {
1421
+ throw new Error(`Playbook "${slug}" not found in ${playbooksDir2}`);
1422
+ }
1423
+ const config = await readConfig();
1424
+ const disabledSet = new Set(config.playbooks.disabled);
1425
+ const wasDisabled = disabledSet.has(resolved.slug);
1426
+ const shouldBeDisabled = !enabled;
1427
+ if (wasDisabled === shouldBeDisabled) {
1428
+ return { slug: resolved.slug, enabled, changed: false };
1429
+ }
1430
+ if (shouldBeDisabled) {
1431
+ disabledSet.add(resolved.slug);
1432
+ } else {
1433
+ disabledSet.delete(resolved.slug);
1434
+ }
1435
+ await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });
1436
+ await rebuildPlaybookManifest(playbooksDir2);
1437
+ return { slug: resolved.slug, enabled, changed: true };
1438
+ }
1439
+ async function removeFromDisabledList(slug) {
1440
+ const config = await readConfig();
1441
+ if (!config.playbooks.disabled.includes(slug)) return;
1442
+ await updatePlaybooksConfig({
1443
+ disabled: config.playbooks.disabled.filter((s) => s !== slug)
1444
+ });
1445
+ }
1446
+ async function rebuildPlaybookManifest(playbooksDir2) {
1447
+ if (!await fileExists(playbooksDir2)) return;
1448
+ const config = await readConfig();
1449
+ const disabledSet = new Set(config.playbooks.disabled);
1450
+ const entries = await readdir2(playbooksDir2, { withFileTypes: true });
1451
+ const rows = [];
1452
+ for (const entry of entries) {
1453
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
1454
+ const raw = await readFile4(resolve5(playbooksDir2, entry.name), "utf-8");
1455
+ const parsed = parsePlaybook(raw);
1456
+ const slug = parsed.slug || entry.name.replace(/\.md$/, "");
1457
+ if (disabledSet.has(slug)) continue;
1458
+ rows.push({
1459
+ name: parsed.name || slug,
1460
+ slug,
1461
+ description: parsed.description,
1462
+ whenToUse: parsed.whenToUse
1463
+ });
1464
+ }
1465
+ rows.sort((a, b) => a.name.localeCompare(b.name));
1466
+ const timestamp = nowTimestamp();
1467
+ const lines = [
1468
+ "---",
1469
+ `generated: "${timestamp}"`,
1470
+ `total: ${rows.length}`,
1471
+ "---",
1472
+ "",
1473
+ "# Playbooks",
1474
+ "",
1475
+ "Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
1476
+ ""
1477
+ ];
1478
+ for (const row of rows) {
1479
+ lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
1480
+ if (row.whenToUse) {
1481
+ lines.push(` _When to use: ${row.whenToUse}_`);
1482
+ }
1483
+ }
1484
+ lines.push("");
1485
+ await writeFileForce(resolve5(playbooksDir2, "manifest.md"), lines.join("\n"));
1486
+ }
1487
+ var init_playbooks = __esm({
1488
+ "src/utils/playbooks.ts"() {
1489
+ "use strict";
1490
+ init_fs();
1491
+ init_parser();
1492
+ init_timestamp();
1493
+ init_config2();
1494
+ }
1495
+ });
1496
+
1497
+ // src/utils/assignment-resolver.ts
1498
+ import { resolve as resolve6 } from "path";
1499
+ import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
1290
1500
  async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
1291
1501
  let standaloneMatch = null;
1292
1502
  let projectMatch = null;
1293
- const standaloneDir = resolve5(assignmentsDir, id);
1294
- const standalonePath = resolve5(standaloneDir, "assignment.md");
1503
+ const standaloneDir = resolve6(assignmentsDir, id);
1504
+ const standalonePath = resolve6(standaloneDir, "assignment.md");
1295
1505
  if (await fileExists(standalonePath)) {
1506
+ let workspaceGroup = null;
1507
+ try {
1508
+ const content = await readFile5(standalonePath, "utf-8");
1509
+ const [fm] = extractFrontmatter2(content);
1510
+ workspaceGroup = getField(fm, "workspaceGroup");
1511
+ } catch {
1512
+ }
1296
1513
  standaloneMatch = {
1297
1514
  assignmentDir: standaloneDir,
1298
1515
  projectSlug: null,
1299
1516
  assignmentSlug: id,
1300
1517
  id,
1301
- standalone: true
1518
+ standalone: true,
1519
+ workspaceGroup
1302
1520
  };
1303
1521
  }
1304
1522
  if (await fileExists(projectsDir)) {
1305
1523
  try {
1306
- const projects = await readdir2(projectsDir, { withFileTypes: true });
1524
+ const projects = await readdir3(projectsDir, { withFileTypes: true });
1307
1525
  for (const p of projects) {
1308
1526
  if (!p.isDirectory()) continue;
1309
1527
  if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
1310
- const assignmentsPath = resolve5(projectsDir, p.name, "assignments");
1528
+ const assignmentsPath = resolve6(projectsDir, p.name, "assignments");
1311
1529
  if (!await fileExists(assignmentsPath)) continue;
1312
- const entries = await readdir2(assignmentsPath, { withFileTypes: true });
1530
+ const entries = await readdir3(assignmentsPath, { withFileTypes: true });
1313
1531
  for (const a of entries) {
1314
1532
  if (!a.isDirectory()) continue;
1315
- const aPath = resolve5(assignmentsPath, a.name, "assignment.md");
1533
+ const aPath = resolve6(assignmentsPath, a.name, "assignment.md");
1316
1534
  if (!await fileExists(aPath)) continue;
1317
1535
  try {
1318
- const content = await readFile4(aPath, "utf-8");
1536
+ const content = await readFile5(aPath, "utf-8");
1319
1537
  const [fm] = extractFrontmatter2(content);
1320
1538
  const fileId = getField(fm, "id");
1321
1539
  if (fileId === id) {
1322
1540
  projectMatch = {
1323
- assignmentDir: resolve5(assignmentsPath, a.name),
1541
+ assignmentDir: resolve6(assignmentsPath, a.name),
1324
1542
  projectSlug: p.name,
1325
1543
  assignmentSlug: a.name,
1326
1544
  id,
1327
- standalone: false
1545
+ standalone: false,
1546
+ workspaceGroup: null
1328
1547
  };
1329
1548
  break;
1330
1549
  }
@@ -1700,8 +1919,18 @@ var init_help = __esm({
1700
1919
  },
1701
1920
  {
1702
1921
  command: "syntaur list-playbooks",
1703
- description: "List all playbooks in the Syntaur home directory.",
1704
- example: "syntaur list-playbooks"
1922
+ description: "List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.",
1923
+ example: "syntaur list-playbooks --all"
1924
+ },
1925
+ {
1926
+ command: "syntaur enable-playbook",
1927
+ description: "Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.",
1928
+ example: "syntaur enable-playbook commit-discipline"
1929
+ },
1930
+ {
1931
+ command: "syntaur disable-playbook",
1932
+ description: "Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.",
1933
+ example: "syntaur disable-playbook commit-discipline"
1705
1934
  }
1706
1935
  ];
1707
1936
  WORKFLOW = [
@@ -1768,8 +1997,8 @@ var init_help = __esm({
1768
1997
  });
1769
1998
 
1770
1999
  // src/dashboard/servers.ts
1771
- import { readdir as readdir3, readFile as readFile5, unlink } from "fs/promises";
1772
- import { resolve as resolve6 } from "path";
2000
+ import { readdir as readdir4, readFile as readFile6, unlink } from "fs/promises";
2001
+ import { resolve as resolve7 } from "path";
1773
2002
  function sanitizeSessionName(name) {
1774
2003
  return name.replace(/[^a-zA-Z0-9_-]/g, "-");
1775
2004
  }
@@ -1817,18 +2046,18 @@ async function registerSession(dir, rawName) {
1817
2046
  lastRefreshed: now,
1818
2047
  overrides: {}
1819
2048
  });
1820
- await writeFileForce(resolve6(dir, `${name}.md`), content);
2049
+ await writeFileForce(resolve7(dir, `${name}.md`), content);
1821
2050
  return name;
1822
2051
  }
1823
2052
  async function listSessionFiles(dir) {
1824
2053
  if (!await fileExists(dir)) return [];
1825
- const entries = await readdir3(dir);
2054
+ const entries = await readdir4(dir);
1826
2055
  return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
1827
2056
  }
1828
2057
  async function readSessionFile(dir, name) {
1829
- const filePath = resolve6(dir, `${sanitizeSessionName(name)}.md`);
2058
+ const filePath = resolve7(dir, `${sanitizeSessionName(name)}.md`);
1830
2059
  if (!await fileExists(filePath)) return null;
1831
- const raw = await readFile5(filePath, "utf-8");
2060
+ const raw = await readFile6(filePath, "utf-8");
1832
2061
  const [frontmatter] = extractFrontmatter2(raw);
1833
2062
  if (!frontmatter) return null;
1834
2063
  const session = getField(frontmatter, "session") ?? name;
@@ -1868,7 +2097,7 @@ async function readSessionFile(dir, name) {
1868
2097
  };
1869
2098
  }
1870
2099
  async function removeSession(dir, name) {
1871
- const filePath = resolve6(dir, `${sanitizeSessionName(name)}.md`);
2100
+ const filePath = resolve7(dir, `${sanitizeSessionName(name)}.md`);
1872
2101
  if (await fileExists(filePath)) {
1873
2102
  await unlink(filePath);
1874
2103
  }
@@ -1877,7 +2106,7 @@ async function updateLastRefreshed(dir, name) {
1877
2106
  const data = await readSessionFile(dir, name);
1878
2107
  if (!data) return;
1879
2108
  const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
1880
- await writeFileForce(resolve6(dir, `${sanitizeSessionName(name)}.md`), content);
2109
+ await writeFileForce(resolve7(dir, `${sanitizeSessionName(name)}.md`), content);
1881
2110
  }
1882
2111
  async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
1883
2112
  const data = await readSessionFile(dir, sessionName);
@@ -1889,7 +2118,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
1889
2118
  delete data.overrides[key];
1890
2119
  }
1891
2120
  const content = buildSessionContent({ ...data });
1892
- await writeFileForce(resolve6(dir, `${sanitizeSessionName(sessionName)}.md`), content);
2121
+ await writeFileForce(resolve7(dir, `${sanitizeSessionName(sessionName)}.md`), content);
1893
2122
  }
1894
2123
  async function registerAutoSession(dir, rawName, opts) {
1895
2124
  const name = sanitizeSessionName(rawName);
@@ -1906,7 +2135,7 @@ async function registerAutoSession(dir, rawName, opts) {
1906
2135
  ports: opts.ports,
1907
2136
  cwd: opts.cwd
1908
2137
  });
1909
- await writeFileForce(resolve6(dir, `${name}.md`), content);
2138
+ await writeFileForce(resolve7(dir, `${name}.md`), content);
1910
2139
  return name;
1911
2140
  }
1912
2141
  var init_servers = __esm({
@@ -1938,8 +2167,8 @@ __export(scanner_exports, {
1938
2167
  });
1939
2168
  import { execFile } from "child_process";
1940
2169
  import { promisify } from "util";
1941
- import { resolve as resolve7 } from "path";
1942
- import { realpath, readdir as readdir4, readFile as readFile6 } from "fs/promises";
2170
+ import { resolve as resolve8 } from "path";
2171
+ import { realpath, readdir as readdir5, readFile as readFile7 } from "fs/promises";
1943
2172
  function clearScanCache() {
1944
2173
  cache = null;
1945
2174
  }
@@ -2034,8 +2263,8 @@ async function getGitInfo(cwd) {
2034
2263
  let isWorktree = false;
2035
2264
  if (commonDir && gitDir && commonDir !== gitDir) {
2036
2265
  try {
2037
- const resolvedCommon = await realpath(resolve7(cwd, commonDir));
2038
- const resolvedGit = await realpath(resolve7(cwd, gitDir));
2266
+ const resolvedCommon = await realpath(resolve8(cwd, commonDir));
2267
+ const resolvedGit = await realpath(resolve8(cwd, gitDir));
2039
2268
  isWorktree = resolvedCommon !== resolvedGit;
2040
2269
  } catch {
2041
2270
  isWorktree = false;
@@ -2048,17 +2277,17 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
2048
2277
  try {
2049
2278
  const projects = await listProjects(projectsDir);
2050
2279
  for (const project of projects) {
2051
- const projectAssignmentsDir = resolve7(projectsDir, project.slug, "assignments");
2280
+ const projectAssignmentsDir = resolve8(projectsDir, project.slug, "assignments");
2052
2281
  let slugs;
2053
2282
  try {
2054
- slugs = await readdir4(projectAssignmentsDir);
2283
+ slugs = await readdir5(projectAssignmentsDir);
2055
2284
  } catch {
2056
2285
  continue;
2057
2286
  }
2058
2287
  for (const aslug of slugs) {
2059
- const aFile = resolve7(projectAssignmentsDir, aslug, "assignment.md");
2288
+ const aFile = resolve8(projectAssignmentsDir, aslug, "assignment.md");
2060
2289
  try {
2061
- const raw = await readFile6(aFile, "utf-8");
2290
+ const raw = await readFile7(aFile, "utf-8");
2062
2291
  const [fm] = extractFrontmatter2(raw);
2063
2292
  if (!fm) continue;
2064
2293
  records.push({
@@ -2077,12 +2306,12 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
2077
2306
  }
2078
2307
  if (assignmentsDir) {
2079
2308
  try {
2080
- const entries = await readdir4(assignmentsDir);
2309
+ const entries = await readdir5(assignmentsDir);
2081
2310
  for (const id of entries) {
2082
2311
  if (id.startsWith(".") || id.startsWith("_")) continue;
2083
- const aFile = resolve7(assignmentsDir, id, "assignment.md");
2312
+ const aFile = resolve8(assignmentsDir, id, "assignment.md");
2084
2313
  try {
2085
- const raw = await readFile6(aFile, "utf-8");
2314
+ const raw = await readFile7(aFile, "utf-8");
2086
2315
  const [fm] = extractFrontmatter2(raw);
2087
2316
  if (!fm) continue;
2088
2317
  records.push({
@@ -2330,20 +2559,20 @@ var init_scanner = __esm({
2330
2559
  });
2331
2560
 
2332
2561
  // src/dashboard/api.ts
2333
- import { readdir as readdir5, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
2334
- import { resolve as resolve8, dirname as dirname2 } from "path";
2562
+ import { readdir as readdir6, readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
2563
+ import { resolve as resolve9, dirname as dirname2 } from "path";
2335
2564
  async function listStandaloneRecords(assignmentsDir) {
2336
2565
  if (!assignmentsDir) return [];
2337
2566
  if (!await fileExists(assignmentsDir)) return [];
2338
- const entries = await readdir5(assignmentsDir, { withFileTypes: true });
2567
+ const entries = await readdir6(assignmentsDir, { withFileTypes: true });
2339
2568
  const records = [];
2340
2569
  for (const entry of entries) {
2341
2570
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
2342
- const assignmentDir = resolve8(assignmentsDir, entry.name);
2343
- const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
2571
+ const assignmentDir = resolve9(assignmentsDir, entry.name);
2572
+ const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
2344
2573
  if (!await fileExists(assignmentMdPath)) continue;
2345
2574
  try {
2346
- const content = await readFile7(assignmentMdPath, "utf-8");
2575
+ const content = await readFile8(assignmentMdPath, "utf-8");
2347
2576
  const record = parseAssignmentFull(content);
2348
2577
  records.push({ assignmentDir, id: entry.name, record });
2349
2578
  } catch {
@@ -2412,9 +2641,9 @@ async function listProjects(projectsDir) {
2412
2641
  return projectRecords.map((record) => record.summary);
2413
2642
  }
2414
2643
  async function readWorkspaceRegistry(projectsDir) {
2415
- const registryPath = resolve8(dirname2(projectsDir), "workspaces.json");
2644
+ const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
2416
2645
  try {
2417
- const raw = await readFile7(registryPath, "utf-8");
2646
+ const raw = await readFile8(registryPath, "utf-8");
2418
2647
  const parsed = JSON.parse(raw);
2419
2648
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
2420
2649
  } catch {
@@ -2422,13 +2651,14 @@ async function readWorkspaceRegistry(projectsDir) {
2422
2651
  }
2423
2652
  }
2424
2653
  async function writeWorkspaceRegistry(projectsDir, workspaces) {
2425
- const registryPath = resolve8(dirname2(projectsDir), "workspaces.json");
2654
+ const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
2426
2655
  await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
2427
2656
  }
2428
- async function listWorkspaces(projectsDir) {
2429
- const [projectRecords, registered] = await Promise.all([
2657
+ async function listWorkspaces(projectsDir, assignmentsDir) {
2658
+ const [projectRecords, registered, standaloneRecords] = await Promise.all([
2430
2659
  listProjectRecords(projectsDir),
2431
- readWorkspaceRegistry(projectsDir)
2660
+ readWorkspaceRegistry(projectsDir),
2661
+ listStandaloneRecords(assignmentsDir)
2432
2662
  ]);
2433
2663
  const workspaceSet = new Set(registered);
2434
2664
  let hasUngrouped = false;
@@ -2439,6 +2669,13 @@ async function listWorkspaces(projectsDir) {
2439
2669
  hasUngrouped = true;
2440
2670
  }
2441
2671
  }
2672
+ for (const sr of standaloneRecords) {
2673
+ if (sr.record.workspaceGroup) {
2674
+ workspaceSet.add(sr.record.workspaceGroup);
2675
+ } else {
2676
+ hasUngrouped = true;
2677
+ }
2678
+ }
2442
2679
  const workspaces = Array.from(workspaceSet).sort();
2443
2680
  return { workspaces, hasUngrouped };
2444
2681
  }
@@ -2587,7 +2824,7 @@ async function toStandaloneBoardItem(sr) {
2587
2824
  projectSlug: null,
2588
2825
  projectTitle: null,
2589
2826
  blockedReason: sr.record.blockedReason,
2590
- projectWorkspace: null,
2827
+ projectWorkspace: sr.record.workspaceGroup ?? null,
2591
2828
  availableTransitions: await getStandaloneAvailableTransitions(sr.record)
2592
2829
  };
2593
2830
  }
@@ -2622,7 +2859,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
2622
2859
  if (!filePath || !await fileExists(filePath)) {
2623
2860
  return null;
2624
2861
  }
2625
- const content = await readFile7(filePath, "utf-8");
2862
+ const content = await readFile8(filePath, "utf-8");
2626
2863
  const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
2627
2864
  return {
2628
2865
  documentType,
@@ -2646,9 +2883,9 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
2646
2883
  }
2647
2884
  const fileName = documentType === "assignment" ? "assignment.md" : documentType === "plan" ? "plan.md" : documentType === "scratchpad" ? "scratchpad.md" : documentType === "handoff" ? "handoff.md" : documentType === "decision-record" ? "decision-record.md" : null;
2648
2885
  if (!fileName) return null;
2649
- const filePath = resolve8(resolved.assignmentDir, fileName);
2886
+ const filePath = resolve9(resolved.assignmentDir, fileName);
2650
2887
  if (!await fileExists(filePath)) return null;
2651
- const content = await readFile7(filePath, "utf-8");
2888
+ const content = await readFile8(filePath, "utf-8");
2652
2889
  const label = resolved.id;
2653
2890
  const title = documentType === "assignment" ? `Edit Assignment: ${label}` : documentType === "plan" ? `Edit Plan: ${label}` : documentType === "scratchpad" ? `Edit Scratchpad: ${label}` : documentType === "handoff" ? `Append Handoff: ${label}` : `Append Decision: ${label}`;
2654
2891
  return {
@@ -2662,12 +2899,12 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
2662
2899
  };
2663
2900
  }
2664
2901
  async function getProjectDetail(projectsDir, slug) {
2665
- const projectPath = resolve8(projectsDir, slug);
2666
- const projectMdPath = resolve8(projectPath, "project.md");
2902
+ const projectPath = resolve9(projectsDir, slug);
2903
+ const projectMdPath = resolve9(projectPath, "project.md");
2667
2904
  if (!await fileExists(projectMdPath)) {
2668
2905
  return null;
2669
2906
  }
2670
- const projectContent = await readFile7(projectMdPath, "utf-8");
2907
+ const projectContent = await readFile8(projectMdPath, "utf-8");
2671
2908
  const project = parseProject(projectContent);
2672
2909
  const assignments = await listAssignmentRecords(projectPath);
2673
2910
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -2697,17 +2934,17 @@ async function getProjectDetail(projectsDir, slug) {
2697
2934
  };
2698
2935
  }
2699
2936
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2700
- const assignmentDir = resolve8(projectsDir, projectSlug, "assignments", assignmentSlug);
2701
- const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
2937
+ const assignmentDir = resolve9(projectsDir, projectSlug, "assignments", assignmentSlug);
2938
+ const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
2702
2939
  if (!await fileExists(assignmentMdPath)) {
2703
2940
  return null;
2704
2941
  }
2705
- const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
2942
+ const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
2706
2943
  const assignment = parseAssignmentFull(assignmentContent);
2707
2944
  let plan = null;
2708
- const planPath = resolve8(assignmentDir, "plan.md");
2945
+ const planPath = resolve9(assignmentDir, "plan.md");
2709
2946
  if (await fileExists(planPath)) {
2710
- const planContent = await readFile7(planPath, "utf-8");
2947
+ const planContent = await readFile8(planPath, "utf-8");
2711
2948
  const parsed = parsePlan(planContent);
2712
2949
  plan = {
2713
2950
  status: parsed.status,
@@ -2716,9 +2953,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2716
2953
  };
2717
2954
  }
2718
2955
  let scratchpad = null;
2719
- const scratchpadPath = resolve8(assignmentDir, "scratchpad.md");
2956
+ const scratchpadPath = resolve9(assignmentDir, "scratchpad.md");
2720
2957
  if (await fileExists(scratchpadPath)) {
2721
- const scratchpadContent = await readFile7(scratchpadPath, "utf-8");
2958
+ const scratchpadContent = await readFile8(scratchpadPath, "utf-8");
2722
2959
  const parsed = parseScratchpad(scratchpadContent);
2723
2960
  scratchpad = {
2724
2961
  updated: parsed.updated,
@@ -2726,9 +2963,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2726
2963
  };
2727
2964
  }
2728
2965
  let handoff = null;
2729
- const handoffPath = resolve8(assignmentDir, "handoff.md");
2966
+ const handoffPath = resolve9(assignmentDir, "handoff.md");
2730
2967
  if (await fileExists(handoffPath)) {
2731
- const handoffContent = await readFile7(handoffPath, "utf-8");
2968
+ const handoffContent = await readFile8(handoffPath, "utf-8");
2732
2969
  const parsed = parseHandoff(handoffContent);
2733
2970
  handoff = {
2734
2971
  updated: parsed.updated,
@@ -2737,9 +2974,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2737
2974
  };
2738
2975
  }
2739
2976
  let decisionRecord = null;
2740
- const decisionRecordPath = resolve8(assignmentDir, "decision-record.md");
2977
+ const decisionRecordPath = resolve9(assignmentDir, "decision-record.md");
2741
2978
  if (await fileExists(decisionRecordPath)) {
2742
- const decisionRecordContent = await readFile7(decisionRecordPath, "utf-8");
2979
+ const decisionRecordContent = await readFile8(decisionRecordPath, "utf-8");
2743
2980
  const parsed = parseDecisionRecord(decisionRecordContent);
2744
2981
  decisionRecord = {
2745
2982
  updated: parsed.updated,
@@ -2748,9 +2985,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2748
2985
  };
2749
2986
  }
2750
2987
  let progress = null;
2751
- const progressPath = resolve8(assignmentDir, "progress.md");
2988
+ const progressPath = resolve9(assignmentDir, "progress.md");
2752
2989
  if (await fileExists(progressPath)) {
2753
- const progressContent = await readFile7(progressPath, "utf-8");
2990
+ const progressContent = await readFile8(progressPath, "utf-8");
2754
2991
  const parsed = parseProgress(progressContent);
2755
2992
  progress = {
2756
2993
  updated: parsed.updated,
@@ -2759,9 +2996,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2759
2996
  };
2760
2997
  }
2761
2998
  let comments = null;
2762
- const commentsPath = resolve8(assignmentDir, "comments.md");
2999
+ const commentsPath = resolve9(assignmentDir, "comments.md");
2763
3000
  if (await fileExists(commentsPath)) {
2764
- const commentsContent = await readFile7(commentsPath, "utf-8");
3001
+ const commentsContent = await readFile8(commentsPath, "utf-8");
2765
3002
  const parsed = parseComments(commentsContent);
2766
3003
  comments = {
2767
3004
  updated: parsed.updated,
@@ -2875,7 +3112,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2875
3112
  slug: a.slug,
2876
3113
  title: a.title,
2877
3114
  projectSlug: rec.summary.slug,
2878
- assignmentDir: resolve8(rec.projectPath, "assignments", a.slug)
3115
+ assignmentDir: resolve9(rec.projectPath, "assignments", a.slug)
2879
3116
  });
2880
3117
  }
2881
3118
  }
@@ -2908,17 +3145,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2908
3145
  }
2909
3146
  async function countMentionsInAssignment(sourceDir, target) {
2910
3147
  const bodies = [];
2911
- const assignmentMd = resolve8(sourceDir, "assignment.md");
3148
+ const assignmentMd = resolve9(sourceDir, "assignment.md");
2912
3149
  if (await fileExists(assignmentMd)) {
2913
- const content = await readFile7(assignmentMd, "utf-8");
3150
+ const content = await readFile8(assignmentMd, "utf-8");
2914
3151
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
2915
3152
  if (todosMatch) bodies.push(todosMatch[1]);
2916
3153
  }
2917
3154
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
2918
- const path = resolve8(sourceDir, filename);
3155
+ const path = resolve9(sourceDir, filename);
2919
3156
  if (await fileExists(path)) {
2920
3157
  try {
2921
- bodies.push(await readFile7(path, "utf-8"));
3158
+ bodies.push(await readFile8(path, "utf-8"));
2922
3159
  } catch {
2923
3160
  }
2924
3161
  }
@@ -2976,44 +3213,44 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
2976
3213
  }
2977
3214
  async function buildStandaloneAssignmentDetail(resolved) {
2978
3215
  const assignmentDir = resolved.assignmentDir;
2979
- const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
3216
+ const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
2980
3217
  if (!await fileExists(assignmentMdPath)) return null;
2981
- const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
3218
+ const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
2982
3219
  const assignment = parseAssignmentFull(assignmentContent);
2983
3220
  let plan = null;
2984
- const planPath = resolve8(assignmentDir, "plan.md");
3221
+ const planPath = resolve9(assignmentDir, "plan.md");
2985
3222
  if (await fileExists(planPath)) {
2986
- const parsed = parsePlan(await readFile7(planPath, "utf-8"));
3223
+ const parsed = parsePlan(await readFile8(planPath, "utf-8"));
2987
3224
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
2988
3225
  }
2989
3226
  let scratchpad = null;
2990
- const scratchpadPath = resolve8(assignmentDir, "scratchpad.md");
3227
+ const scratchpadPath = resolve9(assignmentDir, "scratchpad.md");
2991
3228
  if (await fileExists(scratchpadPath)) {
2992
- const parsed = parseScratchpad(await readFile7(scratchpadPath, "utf-8"));
3229
+ const parsed = parseScratchpad(await readFile8(scratchpadPath, "utf-8"));
2993
3230
  scratchpad = { updated: parsed.updated, body: parsed.body };
2994
3231
  }
2995
3232
  let handoff = null;
2996
- const handoffPath = resolve8(assignmentDir, "handoff.md");
3233
+ const handoffPath = resolve9(assignmentDir, "handoff.md");
2997
3234
  if (await fileExists(handoffPath)) {
2998
- const parsed = parseHandoff(await readFile7(handoffPath, "utf-8"));
3235
+ const parsed = parseHandoff(await readFile8(handoffPath, "utf-8"));
2999
3236
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
3000
3237
  }
3001
3238
  let decisionRecord = null;
3002
- const decisionRecordPath = resolve8(assignmentDir, "decision-record.md");
3239
+ const decisionRecordPath = resolve9(assignmentDir, "decision-record.md");
3003
3240
  if (await fileExists(decisionRecordPath)) {
3004
- const parsed = parseDecisionRecord(await readFile7(decisionRecordPath, "utf-8"));
3241
+ const parsed = parseDecisionRecord(await readFile8(decisionRecordPath, "utf-8"));
3005
3242
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
3006
3243
  }
3007
3244
  let progress = null;
3008
- const progressPath = resolve8(assignmentDir, "progress.md");
3245
+ const progressPath = resolve9(assignmentDir, "progress.md");
3009
3246
  if (await fileExists(progressPath)) {
3010
- const parsed = parseProgress(await readFile7(progressPath, "utf-8"));
3247
+ const parsed = parseProgress(await readFile8(progressPath, "utf-8"));
3011
3248
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
3012
3249
  }
3013
3250
  let comments = null;
3014
- const commentsPath = resolve8(assignmentDir, "comments.md");
3251
+ const commentsPath = resolve9(assignmentDir, "comments.md");
3015
3252
  if (await fileExists(commentsPath)) {
3016
- const parsed = parseComments(await readFile7(commentsPath, "utf-8"));
3253
+ const parsed = parseComments(await readFile8(commentsPath, "utf-8"));
3017
3254
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
3018
3255
  }
3019
3256
  const detail = {
@@ -3055,16 +3292,16 @@ async function listProjectRecords(projectsDir) {
3055
3292
  migratedProjectsDirs.add(projectsDir);
3056
3293
  await migrateLegacyProjectFiles(projectsDir);
3057
3294
  }
3058
- const entries = await readdir5(projectsDir, { withFileTypes: true });
3295
+ const entries = await readdir6(projectsDir, { withFileTypes: true });
3059
3296
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
3060
3297
  const records = [];
3061
3298
  for (const entry of projectDirs) {
3062
- const projectPath = resolve8(projectsDir, entry.name);
3063
- const projectMdPath = resolve8(projectPath, "project.md");
3299
+ const projectPath = resolve9(projectsDir, entry.name);
3300
+ const projectMdPath = resolve9(projectPath, "project.md");
3064
3301
  if (!await fileExists(projectMdPath)) {
3065
3302
  continue;
3066
3303
  }
3067
- const projectContent = await readFile7(projectMdPath, "utf-8");
3304
+ const projectContent = await readFile8(projectMdPath, "utf-8");
3068
3305
  const project = parseProject(projectContent);
3069
3306
  const assignments = await listAssignmentRecords(projectPath);
3070
3307
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -3095,39 +3332,39 @@ async function listProjectRecords(projectsDir) {
3095
3332
  return records;
3096
3333
  }
3097
3334
  async function listAssignmentRecords(projectPath) {
3098
- const assignmentsDir = resolve8(projectPath, "assignments");
3335
+ const assignmentsDir = resolve9(projectPath, "assignments");
3099
3336
  if (!await fileExists(assignmentsDir)) {
3100
3337
  return [];
3101
3338
  }
3102
- const entries = await readdir5(assignmentsDir, { withFileTypes: true });
3339
+ const entries = await readdir6(assignmentsDir, { withFileTypes: true });
3103
3340
  const records = [];
3104
3341
  for (const entry of entries) {
3105
3342
  if (!entry.isDirectory()) {
3106
3343
  continue;
3107
3344
  }
3108
- const assignmentMd = resolve8(assignmentsDir, entry.name, "assignment.md");
3345
+ const assignmentMd = resolve9(assignmentsDir, entry.name, "assignment.md");
3109
3346
  if (!await fileExists(assignmentMd)) {
3110
3347
  continue;
3111
3348
  }
3112
- const content = await readFile7(assignmentMd, "utf-8");
3349
+ const content = await readFile8(assignmentMd, "utf-8");
3113
3350
  records.push(parseAssignmentFull(content));
3114
3351
  }
3115
3352
  records.sort((left, right) => compareTimestamps(right.updated, left.updated));
3116
3353
  return records;
3117
3354
  }
3118
3355
  async function listResources(projectPath) {
3119
- const resourcesDir = resolve8(projectPath, "resources");
3356
+ const resourcesDir = resolve9(projectPath, "resources");
3120
3357
  if (!await fileExists(resourcesDir)) {
3121
3358
  return [];
3122
3359
  }
3123
- const entries = await readdir5(resourcesDir, { withFileTypes: true });
3360
+ const entries = await readdir6(resourcesDir, { withFileTypes: true });
3124
3361
  const results = [];
3125
3362
  for (const entry of entries) {
3126
3363
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
3127
3364
  continue;
3128
3365
  }
3129
- const filePath = resolve8(resourcesDir, entry.name);
3130
- const content = await readFile7(filePath, "utf-8");
3366
+ const filePath = resolve9(resourcesDir, entry.name);
3367
+ const content = await readFile8(filePath, "utf-8");
3131
3368
  const parsed = parseResource(content);
3132
3369
  results.push({
3133
3370
  name: parsed.name,
@@ -3142,18 +3379,18 @@ async function listResources(projectPath) {
3142
3379
  return results;
3143
3380
  }
3144
3381
  async function listMemories(projectPath) {
3145
- const memoriesDir = resolve8(projectPath, "memories");
3382
+ const memoriesDir = resolve9(projectPath, "memories");
3146
3383
  if (!await fileExists(memoriesDir)) {
3147
3384
  return [];
3148
3385
  }
3149
- const entries = await readdir5(memoriesDir, { withFileTypes: true });
3386
+ const entries = await readdir6(memoriesDir, { withFileTypes: true });
3150
3387
  const results = [];
3151
3388
  for (const entry of entries) {
3152
3389
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
3153
3390
  continue;
3154
3391
  }
3155
- const filePath = resolve8(memoriesDir, entry.name);
3156
- const content = await readFile7(filePath, "utf-8");
3392
+ const filePath = resolve9(memoriesDir, entry.name);
3393
+ const content = await readFile8(filePath, "utf-8");
3157
3394
  const parsed = parseMemory(content);
3158
3395
  results.push({
3159
3396
  name: parsed.name,
@@ -3168,9 +3405,9 @@ async function listMemories(projectPath) {
3168
3405
  return results;
3169
3406
  }
3170
3407
  async function loadDependencyGraph(projectPath, assignments) {
3171
- const statusPath = resolve8(projectPath, "_status.md");
3408
+ const statusPath = resolve9(projectPath, "_status.md");
3172
3409
  if (await fileExists(statusPath)) {
3173
- const statusContent = await readFile7(statusPath, "utf-8");
3410
+ const statusContent = await readFile8(statusPath, "utf-8");
3174
3411
  const parsed = parseStatus(statusContent);
3175
3412
  const derivedGraph = extractMermaidGraph(parsed.body);
3176
3413
  if (derivedGraph) {
@@ -3270,7 +3507,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
3270
3507
  const config = await getStatusConfig();
3271
3508
  const transitionDefs = getTransitionDefinitions(config);
3272
3509
  const actions = [];
3273
- const projectPath = resolve8(projectsDir, projectSlug);
3510
+ const projectPath = resolve9(projectsDir, projectSlug);
3274
3511
  for (const definition of transitionDefs) {
3275
3512
  let warning = null;
3276
3513
  if (definition.command === "start" && !assignment.assignee) {
@@ -3300,12 +3537,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
3300
3537
  const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
3301
3538
  const unmet = [];
3302
3539
  for (const dependency of dependsOn) {
3303
- const dependencyPath = resolve8(projectPath, "assignments", dependency, "assignment.md");
3540
+ const dependencyPath = resolve9(projectPath, "assignments", dependency, "assignment.md");
3304
3541
  if (!await fileExists(dependencyPath)) {
3305
3542
  unmet.push(`${dependency} (missing)`);
3306
3543
  continue;
3307
3544
  }
3308
- const content = await readFile7(dependencyPath, "utf-8");
3545
+ const content = await readFile8(dependencyPath, "utf-8");
3309
3546
  const parsed = parseAssignmentFull(content);
3310
3547
  if (!terminals.has(parsed.status)) {
3311
3548
  unmet.push(`${dependency} (${parsed.status})`);
@@ -3460,7 +3697,7 @@ function isStale(updated) {
3460
3697
  return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
3461
3698
  }
3462
3699
  async function countOpenQuestions(projectPath, assignmentSlug) {
3463
- const commentsPath = resolve8(
3700
+ const commentsPath = resolve9(
3464
3701
  projectPath,
3465
3702
  "assignments",
3466
3703
  assignmentSlug,
@@ -3470,7 +3707,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
3470
3707
  return 0;
3471
3708
  }
3472
3709
  try {
3473
- const content = await readFile7(commentsPath, "utf-8");
3710
+ const content = await readFile8(commentsPath, "utf-8");
3474
3711
  const parsed = parseComments(content);
3475
3712
  return parsed.entries.filter(
3476
3713
  (e) => e.type === "question" && e.resolved !== true
@@ -3491,17 +3728,17 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
3491
3728
  function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
3492
3729
  switch (documentType) {
3493
3730
  case "project":
3494
- return resolve8(projectsDir, projectSlug, "project.md");
3731
+ return resolve9(projectsDir, projectSlug, "project.md");
3495
3732
  case "assignment":
3496
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
3733
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
3497
3734
  case "plan":
3498
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
3735
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
3499
3736
  case "scratchpad":
3500
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
3737
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
3501
3738
  case "handoff":
3502
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
3739
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
3503
3740
  case "decision-record":
3504
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
3741
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
3505
3742
  default:
3506
3743
  return null;
3507
3744
  }
@@ -3528,12 +3765,14 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
3528
3765
  }
3529
3766
  async function listPlaybooks(playbooksDir2) {
3530
3767
  if (!await fileExists(playbooksDir2)) return [];
3531
- const entries = await readdir5(playbooksDir2, { withFileTypes: true });
3768
+ const config = await readConfig();
3769
+ const disabledSet = new Set(config.playbooks.disabled);
3770
+ const entries = await readdir6(playbooksDir2, { withFileTypes: true });
3532
3771
  const playbooks = [];
3533
3772
  for (const entry of entries) {
3534
3773
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
3535
- const filePath = resolve8(playbooksDir2, entry.name);
3536
- const raw = await readFile7(filePath, "utf-8");
3774
+ const filePath = resolve9(playbooksDir2, entry.name);
3775
+ const raw = await readFile8(filePath, "utf-8");
3537
3776
  const parsed = parsePlaybook(raw);
3538
3777
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
3539
3778
  playbooks.push({
@@ -3543,25 +3782,28 @@ async function listPlaybooks(playbooksDir2) {
3543
3782
  whenToUse: parsed.whenToUse,
3544
3783
  tags: parsed.tags,
3545
3784
  created: parsed.created,
3546
- updated: parsed.updated
3785
+ updated: parsed.updated,
3786
+ enabled: !disabledSet.has(slug)
3547
3787
  });
3548
3788
  }
3549
3789
  return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
3550
3790
  }
3551
3791
  async function getPlaybookDetail(playbooksDir2, slug) {
3552
- const filePath = resolve8(playbooksDir2, `${slug}.md`);
3553
- if (!await fileExists(filePath)) return null;
3554
- const raw = await readFile7(filePath, "utf-8");
3555
- const parsed = parsePlaybook(raw);
3792
+ const resolved = await resolvePlaybookSlug(playbooksDir2, slug);
3793
+ if (!resolved) return null;
3794
+ const config = await readConfig();
3795
+ const enabled = !config.playbooks.disabled.includes(resolved.slug);
3796
+ const parsed = resolved.parsed;
3556
3797
  return {
3557
- slug: parsed.slug || slug,
3558
- name: parsed.name || slug,
3798
+ slug: resolved.slug,
3799
+ name: parsed.name || resolved.slug,
3559
3800
  description: parsed.description,
3560
3801
  whenToUse: parsed.whenToUse,
3561
3802
  tags: parsed.tags,
3562
3803
  created: parsed.created,
3563
3804
  updated: parsed.updated,
3564
- body: parsed.body
3805
+ body: parsed.body,
3806
+ enabled
3565
3807
  };
3566
3808
  }
3567
3809
  var STALE_ASSIGNMENT_MS, ATTENTION_PAGE_LIMIT, OVERVIEW_ATTENTION_LIMIT, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
@@ -3571,6 +3813,7 @@ var init_api = __esm({
3571
3813
  init_lifecycle();
3572
3814
  init_fs();
3573
3815
  init_config2();
3816
+ init_playbooks();
3574
3817
  init_fs_migration();
3575
3818
  init_assignment_resolver();
3576
3819
  init_parser();
@@ -3922,21 +4165,21 @@ init_api();
3922
4165
  init_assignment_resolver();
3923
4166
  import express from "express";
3924
4167
  import { createServer } from "http";
3925
- import { resolve as resolve17 } from "path";
4168
+ import { resolve as resolve18 } from "path";
3926
4169
  import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
3927
4170
  import { WebSocketServer, WebSocket } from "ws";
3928
4171
 
3929
4172
  // src/dashboard/agent-sessions.ts
3930
4173
  init_fs();
3931
- import { readFile as readFile8 } from "fs/promises";
3932
- import { resolve as resolve10 } from "path";
4174
+ import { readFile as readFile9 } from "fs/promises";
4175
+ import { resolve as resolve11 } from "path";
3933
4176
 
3934
4177
  // src/dashboard/session-db.ts
3935
4178
  init_paths();
3936
4179
  init_fs();
3937
4180
  import Database from "better-sqlite3";
3938
- import { resolve as resolve9 } from "path";
3939
- import { readdir as readdir6 } from "fs/promises";
4181
+ import { resolve as resolve10 } from "path";
4182
+ import { readdir as readdir7 } from "fs/promises";
3940
4183
  var db = null;
3941
4184
  var SCHEMA_VERSION = "3";
3942
4185
  var SCHEMA_SQL = `
@@ -3963,7 +4206,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, ass
3963
4206
  `;
3964
4207
  function initSessionDb(dbPath) {
3965
4208
  if (db) return db;
3966
- const finalPath = dbPath ?? resolve9(syntaurRoot(), "syntaur.db");
4209
+ const finalPath = dbPath ?? resolve10(syntaurRoot(), "syntaur.db");
3967
4210
  db = new Database(finalPath);
3968
4211
  db.pragma("journal_mode = WAL");
3969
4212
  db.exec(SCHEMA_SQL);
@@ -4060,12 +4303,12 @@ async function migrateFromMarkdown(projectsDir) {
4060
4303
  const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
4061
4304
  if (count.count > 0) return 0;
4062
4305
  if (!await fileExists(projectsDir)) return 0;
4063
- const entries = await readdir6(projectsDir, { withFileTypes: true });
4306
+ const entries = await readdir7(projectsDir, { withFileTypes: true });
4064
4307
  const allSessions = [];
4065
4308
  for (const entry of entries) {
4066
4309
  if (!entry.isDirectory()) continue;
4067
- const projectDir = resolve9(projectsDir, entry.name);
4068
- const indexPath = resolve9(projectDir, "_index-sessions.md");
4310
+ const projectDir = resolve10(projectsDir, entry.name);
4311
+ const indexPath = resolve10(projectDir, "_index-sessions.md");
4069
4312
  if (!await fileExists(indexPath)) continue;
4070
4313
  const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
4071
4314
  allSessions.push(...sessions);
@@ -4085,8 +4328,8 @@ async function migrateFromMarkdown(projectsDir) {
4085
4328
  return allSessions.length;
4086
4329
  }
4087
4330
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
4088
- const { readFile: readFile14 } = await import("fs/promises");
4089
- const raw = await readFile14(filePath, "utf-8");
4331
+ const { readFile: readFile15 } = await import("fs/promises");
4332
+ const raw = await readFile15(filePath, "utf-8");
4090
4333
  const sessions = [];
4091
4334
  const lines = raw.split("\n");
4092
4335
  let inTable = false;
@@ -4198,13 +4441,13 @@ async function deleteSessions(sessionIds) {
4198
4441
  var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
4199
4442
  async function readAssignmentStatusFromPath(assignmentMdPath) {
4200
4443
  if (!await fileExists(assignmentMdPath)) return null;
4201
- const raw = await readFile8(assignmentMdPath, "utf-8");
4444
+ const raw = await readFile9(assignmentMdPath, "utf-8");
4202
4445
  const match = raw.match(/^status:\s*(.+)$/m);
4203
4446
  return match ? match[1].trim() : null;
4204
4447
  }
4205
4448
  async function readAssignmentStatus(projectDir, assignmentSlug) {
4206
4449
  return readAssignmentStatusFromPath(
4207
- resolve10(projectDir, "assignments", assignmentSlug, "assignment.md")
4450
+ resolve11(projectDir, "assignments", assignmentSlug, "assignment.md")
4208
4451
  );
4209
4452
  }
4210
4453
  async function reconcileActiveSessions(projectsDir, assignmentsDir) {
@@ -4222,13 +4465,13 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir) {
4222
4465
  seen.add(key);
4223
4466
  if (session.project_slug) {
4224
4467
  const status = await readAssignmentStatus(
4225
- resolve10(projectsDir, session.project_slug),
4468
+ resolve11(projectsDir, session.project_slug),
4226
4469
  aslug
4227
4470
  );
4228
4471
  if (status) assignmentStatuses.set(key, status);
4229
4472
  } else if (assignmentsDir) {
4230
4473
  const status = await readAssignmentStatusFromPath(
4231
- resolve10(assignmentsDir, aslug, "assignment.md")
4474
+ resolve11(assignmentsDir, aslug, "assignment.md")
4232
4475
  );
4233
4476
  if (status) assignmentStatuses.set(key, status);
4234
4477
  }
@@ -4273,18 +4516,25 @@ function createWatcher(options) {
4273
4516
  if (parts.length === 0) return;
4274
4517
  const projectSlug = parts[0];
4275
4518
  let assignmentSlug;
4519
+ let isProjectTodos = false;
4276
4520
  if (parts.length >= 3 && parts[1] === "assignments") {
4277
4521
  assignmentSlug = parts[2];
4522
+ } else if (parts.length >= 2 && parts[1] === "todos") {
4523
+ isProjectTodos = true;
4278
4524
  }
4279
- const debounceKey = assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
4525
+ const debounceKey = isProjectTodos ? `todos:${projectSlug}` : assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
4280
4526
  const existing = pendingEvents.get(debounceKey);
4281
4527
  if (existing) clearTimeout(existing);
4282
- const messageType = assignmentSlug ? "assignment-updated" : "project-updated";
4528
+ const messageType = isProjectTodos ? "todos-updated" : assignmentSlug ? "assignment-updated" : "project-updated";
4283
4529
  pendingEvents.set(
4284
4530
  debounceKey,
4285
4531
  setTimeout(() => {
4286
4532
  pendingEvents.delete(debounceKey);
4287
- const message = {
4533
+ const message = isProjectTodos ? {
4534
+ type: "todos-updated",
4535
+ projectSlug,
4536
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4537
+ } : {
4288
4538
  type: messageType,
4289
4539
  projectSlug,
4290
4540
  assignmentSlug,
@@ -4442,8 +4692,8 @@ init_config2();
4442
4692
  // src/dashboard/api-write.ts
4443
4693
  init_lifecycle();
4444
4694
  import { Router } from "express";
4445
- import { resolve as resolve11 } from "path";
4446
- import { rm, readFile as readFile9 } from "fs/promises";
4695
+ import { resolve as resolve12 } from "path";
4696
+ import { rm, readFile as readFile10 } from "fs/promises";
4447
4697
 
4448
4698
  // src/utils/slug.ts
4449
4699
  function isValidSlug(slug) {
@@ -4514,14 +4764,14 @@ init_assignment_resolver();
4514
4764
  init_config();
4515
4765
 
4516
4766
  // src/templates/manifest.ts
4517
- function renderManifest(params) {
4767
+ function renderManifest(params2) {
4518
4768
  return `---
4519
4769
  version: "2.0"
4520
- project: ${params.slug}
4521
- generated: "${params.timestamp}"
4770
+ project: ${params2.slug}
4771
+ generated: "${params2.timestamp}"
4522
4772
  ---
4523
4773
 
4524
- # Project: ${params.slug}
4774
+ # Project: ${params2.slug}
4525
4775
 
4526
4776
  ## Overview
4527
4777
  - [Project Overview](./project.md)
@@ -4548,24 +4798,24 @@ function escapeYamlString(value) {
4548
4798
  }
4549
4799
 
4550
4800
  // src/templates/project.ts
4551
- function renderProject(params) {
4552
- const safeTitle = escapeYamlString(params.title);
4553
- const workspaceLine = params.workspace ? `
4554
- workspace: ${params.workspace}` : "";
4801
+ function renderProject(params2) {
4802
+ const safeTitle = escapeYamlString(params2.title);
4803
+ const workspaceLine = params2.workspace ? `
4804
+ workspace: ${params2.workspace}` : "";
4555
4805
  return `---
4556
- id: ${params.id}
4557
- slug: ${params.slug}
4806
+ id: ${params2.id}
4807
+ slug: ${params2.slug}
4558
4808
  title: ${safeTitle}
4559
4809
  archived: false
4560
4810
  archivedAt: null
4561
4811
  archivedReason: null
4562
- created: "${params.timestamp}"
4563
- updated: "${params.timestamp}"
4812
+ created: "${params2.timestamp}"
4813
+ updated: "${params2.timestamp}"
4564
4814
  externalIds: []
4565
4815
  tags: []${workspaceLine}
4566
4816
  ---
4567
4817
 
4568
- # ${params.title}
4818
+ # ${params2.title}
4569
4819
 
4570
4820
  ## Overview
4571
4821
 
@@ -4578,24 +4828,37 @@ tags: []${workspaceLine}
4578
4828
  }
4579
4829
 
4580
4830
  // src/templates/assignment.ts
4581
- function renderAssignment(params) {
4582
- const safeTitle = escapeYamlString(params.title);
4583
- const dependsOnYaml = params.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
4584
- - ${params.dependsOn.join("\n - ")}`;
4585
- const linksYaml = params.links.length === 0 ? "links: []" : `links:
4586
- - ${params.links.join("\n - ")}`;
4587
- const projectYaml = `project: ${params.project == null ? "null" : params.project}`;
4588
- const typeYaml = `type: ${params.type ?? "feature"}`;
4831
+ function renderAssignment(params2) {
4832
+ const safeTitle = escapeYamlString(params2.title);
4833
+ const dependsOnYaml = params2.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
4834
+ - ${params2.dependsOn.join("\n - ")}`;
4835
+ const linksYaml = params2.links.length === 0 ? "links: []" : `links:
4836
+ - ${params2.links.join("\n - ")}`;
4837
+ const projectYaml = `project: ${params2.project == null ? "null" : params2.project}`;
4838
+ const workspaceGroupLine = params2.workspaceGroup ? `
4839
+ workspaceGroup: ${params2.workspaceGroup}` : "";
4840
+ const typeYaml = `type: ${params2.type ?? "feature"}`;
4841
+ const todosSection = params2.includeTodos ? `## Todos
4842
+
4843
+ <!--
4844
+ Checklist of work items for this assignment. Items may be simple tasks
4845
+ or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
4846
+ When a plan is superseded by a new one, mark the old todo as:
4847
+ - [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
4848
+ Never delete superseded todos \u2014 preserve the history.
4849
+ -->
4850
+
4851
+ ` : "";
4589
4852
  return `---
4590
- id: ${params.id}
4591
- slug: ${params.slug}
4853
+ id: ${params2.id}
4854
+ slug: ${params2.slug}
4592
4855
  title: ${safeTitle}
4593
- ${projectYaml}
4856
+ ${projectYaml}${workspaceGroupLine}
4594
4857
  ${typeYaml}
4595
4858
  status: pending
4596
- priority: ${params.priority}
4597
- created: "${params.timestamp}"
4598
- updated: "${params.timestamp}"
4859
+ priority: ${params2.priority}
4860
+ created: "${params2.timestamp}"
4861
+ updated: "${params2.timestamp}"
4599
4862
  assignee: null
4600
4863
  externalIds: []
4601
4864
  ${dependsOnYaml}
@@ -4609,7 +4872,7 @@ workspace:
4609
4872
  tags: []
4610
4873
  ---
4611
4874
 
4612
- # ${params.title}
4875
+ # ${params2.title}
4613
4876
 
4614
4877
  ## Objective
4615
4878
 
@@ -4621,17 +4884,7 @@ tags: []
4621
4884
  - [ ] <!-- criterion 2 -->
4622
4885
  - [ ] <!-- criterion 3 -->
4623
4886
 
4624
- ## Todos
4625
-
4626
- <!--
4627
- Checklist of work items for this assignment. Items may be simple tasks
4628
- or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
4629
- When a plan is superseded by a new one, mark the old todo as:
4630
- - [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
4631
- Never delete superseded todos \u2014 preserve the history.
4632
- -->
4633
-
4634
- ## Context
4887
+ ${todosSection}## Context
4635
4888
 
4636
4889
  <!-- Links to relevant docs, code, or other assignments. -->
4637
4890
 
@@ -4646,10 +4899,10 @@ Never delete superseded todos \u2014 preserve the history.
4646
4899
  }
4647
4900
 
4648
4901
  // src/templates/scratchpad.ts
4649
- function renderScratchpad(params) {
4902
+ function renderScratchpad(params2) {
4650
4903
  return `---
4651
- assignment: ${params.assignmentSlug}
4652
- updated: "${params.timestamp}"
4904
+ assignment: ${params2.assignmentSlug}
4905
+ updated: "${params2.timestamp}"
4653
4906
  ---
4654
4907
 
4655
4908
  # Scratchpad
@@ -4659,10 +4912,10 @@ No working notes yet.
4659
4912
  }
4660
4913
 
4661
4914
  // src/templates/handoff.ts
4662
- function renderHandoff(params) {
4915
+ function renderHandoff(params2) {
4663
4916
  return `---
4664
- assignment: ${params.assignmentSlug}
4665
- updated: "${params.timestamp}"
4917
+ assignment: ${params2.assignmentSlug}
4918
+ updated: "${params2.timestamp}"
4666
4919
  handoffCount: 0
4667
4920
  ---
4668
4921
 
@@ -4673,12 +4926,12 @@ No handoffs recorded yet.
4673
4926
  }
4674
4927
 
4675
4928
  // src/templates/progress.ts
4676
- function renderProgress(params) {
4929
+ function renderProgress(params2) {
4677
4930
  return `---
4678
- assignment: ${params.assignment}
4931
+ assignment: ${params2.assignment}
4679
4932
  entryCount: 0
4680
- generated: "${params.timestamp}"
4681
- updated: "${params.timestamp}"
4933
+ generated: "${params2.timestamp}"
4934
+ updated: "${params2.timestamp}"
4682
4935
  ---
4683
4936
 
4684
4937
  # Progress
@@ -4688,12 +4941,12 @@ No progress yet.
4688
4941
  }
4689
4942
 
4690
4943
  // src/templates/comments.ts
4691
- function renderComments(params) {
4944
+ function renderComments(params2) {
4692
4945
  return `---
4693
- assignment: ${params.assignment}
4946
+ assignment: ${params2.assignment}
4694
4947
  entryCount: 0
4695
- generated: "${params.timestamp}"
4696
- updated: "${params.timestamp}"
4948
+ generated: "${params2.timestamp}"
4949
+ updated: "${params2.timestamp}"
4697
4950
  ---
4698
4951
 
4699
4952
  # Comments
@@ -4721,10 +4974,10 @@ function formatCommentEntry(comment) {
4721
4974
  }
4722
4975
 
4723
4976
  // src/templates/decision-record.ts
4724
- function renderDecisionRecord(params) {
4977
+ function renderDecisionRecord(params2) {
4725
4978
  return `---
4726
- assignment: ${params.assignmentSlug}
4727
- updated: "${params.timestamp}"
4979
+ assignment: ${params2.assignmentSlug}
4980
+ updated: "${params2.timestamp}"
4728
4981
  decisionCount: 0
4729
4982
  ---
4730
4983
 
@@ -4735,10 +4988,10 @@ No decisions recorded yet.
4735
4988
  }
4736
4989
 
4737
4990
  // src/templates/index-stubs.ts
4738
- function renderIndexAssignments(params) {
4991
+ function renderIndexAssignments(params2) {
4739
4992
  return `---
4740
- project: ${params.slug}
4741
- generated: "${params.timestamp}"
4993
+ project: ${params2.slug}
4994
+ generated: "${params2.timestamp}"
4742
4995
  total: 0
4743
4996
  by_status:
4744
4997
  pending: 0
@@ -4755,10 +5008,10 @@ by_status:
4755
5008
  |------|-------|--------|----------|----------|--------------|---------|
4756
5009
  `;
4757
5010
  }
4758
- function renderIndexPlans(params) {
5011
+ function renderIndexPlans(params2) {
4759
5012
  return `---
4760
- project: ${params.slug}
4761
- generated: "${params.timestamp}"
5013
+ project: ${params2.slug}
5014
+ generated: "${params2.timestamp}"
4762
5015
  ---
4763
5016
 
4764
5017
  # Plans
@@ -4767,10 +5020,10 @@ generated: "${params.timestamp}"
4767
5020
  |------------|-------------|---------|
4768
5021
  `;
4769
5022
  }
4770
- function renderIndexDecisions(params) {
5023
+ function renderIndexDecisions(params2) {
4771
5024
  return `---
4772
- project: ${params.slug}
4773
- generated: "${params.timestamp}"
5025
+ project: ${params2.slug}
5026
+ generated: "${params2.timestamp}"
4774
5027
  ---
4775
5028
 
4776
5029
  # Decision Records
@@ -4779,10 +5032,10 @@ generated: "${params.timestamp}"
4779
5032
  |------------|-------|-----------------|---------------|---------|
4780
5033
  `;
4781
5034
  }
4782
- function renderStatus(params) {
5035
+ function renderStatus(params2) {
4783
5036
  return `---
4784
- project: ${params.slug}
4785
- generated: "${params.timestamp}"
5037
+ project: ${params2.slug}
5038
+ generated: "${params2.timestamp}"
4786
5039
  status: pending
4787
5040
  progress:
4788
5041
  total: 0
@@ -4798,7 +5051,7 @@ needsAttention:
4798
5051
  openQuestions: 0
4799
5052
  ---
4800
5053
 
4801
- # Project Status: ${params.title}
5054
+ # Project Status: ${params2.title}
4802
5055
 
4803
5056
  **Status:** pending
4804
5057
  **Progress:** 0/0 assignments complete
@@ -4818,10 +5071,10 @@ No dependencies yet.
4818
5071
  - **0 unanswered** questions
4819
5072
  `;
4820
5073
  }
4821
- function renderResourcesIndex(params) {
5074
+ function renderResourcesIndex(params2) {
4822
5075
  return `---
4823
- project: ${params.slug}
4824
- generated: "${params.timestamp}"
5076
+ project: ${params2.slug}
5077
+ generated: "${params2.timestamp}"
4825
5078
  total: 0
4826
5079
  ---
4827
5080
 
@@ -4831,10 +5084,10 @@ total: 0
4831
5084
  |------|----------|--------|---------------------|---------|
4832
5085
  `;
4833
5086
  }
4834
- function renderMemoriesIndex(params) {
5087
+ function renderMemoriesIndex(params2) {
4835
5088
  return `---
4836
- project: ${params.slug}
4837
- generated: "${params.timestamp}"
5089
+ project: ${params2.slug}
5090
+ generated: "${params2.timestamp}"
4838
5091
  total: 0
4839
5092
  ---
4840
5093
 
@@ -4846,19 +5099,19 @@ total: 0
4846
5099
  }
4847
5100
 
4848
5101
  // src/templates/playbook.ts
4849
- function renderPlaybook(params) {
4850
- const whenToUse = params.whenToUse ? escapeYamlString(params.whenToUse) : "null";
5102
+ function renderPlaybook(params2) {
5103
+ const whenToUse = params2.whenToUse ? escapeYamlString(params2.whenToUse) : "null";
4851
5104
  return `---
4852
- name: ${escapeYamlString(params.name)}
4853
- slug: ${params.slug}
4854
- description: ${escapeYamlString(params.description)}
5105
+ name: ${escapeYamlString(params2.name)}
5106
+ slug: ${params2.slug}
5107
+ description: ${escapeYamlString(params2.description)}
4855
5108
  when_to_use: ${whenToUse}
4856
- created: "${params.timestamp}"
4857
- updated: "${params.timestamp}"
5109
+ created: "${params2.timestamp}"
5110
+ updated: "${params2.timestamp}"
4858
5111
  tags: []
4859
5112
  ---
4860
5113
 
4861
- # ${params.name}
5114
+ # ${params2.name}
4862
5115
 
4863
5116
  <!-- Write imperative rules and workflows here. Keep it under 50 lines. -->
4864
5117
  `;
@@ -4966,7 +5219,7 @@ async function readCurrentDocument(filePath) {
4966
5219
  if (!await fileExists(filePath)) {
4967
5220
  return null;
4968
5221
  }
4969
- return readFile9(filePath, "utf-8");
5222
+ return readFile10(filePath, "utf-8");
4970
5223
  }
4971
5224
  function createWriteRouter(projectsDir, assignmentsDir) {
4972
5225
  const router = Router();
@@ -4979,7 +5232,15 @@ function createWriteRouter(projectsDir, assignmentsDir) {
4979
5232
  });
4980
5233
  res.json({ content });
4981
5234
  });
4982
- router.get("/api/templates/assignment", (_req, res) => {
5235
+ router.get("/api/templates/assignment", (req, res) => {
5236
+ const standalone = req.query.standalone === "1";
5237
+ const workspaceParam = typeof req.query.workspace === "string" ? req.query.workspace : "";
5238
+ if (workspaceParam && !isValidSlug(workspaceParam)) {
5239
+ res.status(400).json({
5240
+ error: `Invalid workspace slug "${workspaceParam}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5241
+ });
5242
+ return;
5243
+ }
4983
5244
  const content = renderAssignment({
4984
5245
  id: generateId(),
4985
5246
  slug: "my-new-assignment",
@@ -4987,7 +5248,9 @@ function createWriteRouter(projectsDir, assignmentsDir) {
4987
5248
  timestamp: nowTimestamp(),
4988
5249
  priority: "medium",
4989
5250
  dependsOn: [],
4990
- links: []
5251
+ links: [],
5252
+ project: standalone ? null : void 0,
5253
+ workspaceGroup: standalone && workspaceParam ? workspaceParam : null
4991
5254
  });
4992
5255
  res.json({ content });
4993
5256
  });
@@ -5096,26 +5359,26 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5096
5359
  res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
5097
5360
  return;
5098
5361
  }
5099
- const projectDir = resolve11(projectsDir, slug);
5362
+ const projectDir = resolve12(projectsDir, slug);
5100
5363
  if (await fileExists(projectDir)) {
5101
5364
  res.status(409).json({ error: `Project "${slug}" already exists` });
5102
5365
  return;
5103
5366
  }
5104
5367
  const title = fields.title;
5105
5368
  const timestamp = fields.created || nowTimestamp();
5106
- await ensureDir(resolve11(projectDir, "assignments"));
5107
- await ensureDir(resolve11(projectDir, "resources"));
5108
- await ensureDir(resolve11(projectDir, "memories"));
5109
- await writeFileForce(resolve11(projectDir, "project.md"), content);
5369
+ await ensureDir(resolve12(projectDir, "assignments"));
5370
+ await ensureDir(resolve12(projectDir, "resources"));
5371
+ await ensureDir(resolve12(projectDir, "memories"));
5372
+ await writeFileForce(resolve12(projectDir, "project.md"), content);
5110
5373
  try {
5111
5374
  const companions = [
5112
- [resolve11(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
5113
- [resolve11(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
5114
- [resolve11(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
5115
- [resolve11(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
5116
- [resolve11(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
5117
- [resolve11(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
5118
- [resolve11(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
5375
+ [resolve12(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
5376
+ [resolve12(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
5377
+ [resolve12(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
5378
+ [resolve12(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
5379
+ [resolve12(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
5380
+ [resolve12(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
5381
+ [resolve12(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
5119
5382
  ];
5120
5383
  for (const [filePath, fileContent] of companions) {
5121
5384
  await writeFileForce(filePath, fileContent);
@@ -5136,8 +5399,8 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5136
5399
  router.post("/api/projects/:slug/assignments", async (req, res) => {
5137
5400
  try {
5138
5401
  const projectSlug = getParam(req.params.slug);
5139
- const projectDir = resolve11(projectsDir, projectSlug);
5140
- const projectMdPath = resolve11(projectDir, "project.md");
5402
+ const projectDir = resolve12(projectsDir, projectSlug);
5403
+ const projectMdPath = resolve12(projectDir, "project.md");
5141
5404
  if (!await fileExists(projectMdPath)) {
5142
5405
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5143
5406
  return;
@@ -5167,7 +5430,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5167
5430
  res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
5168
5431
  return;
5169
5432
  }
5170
- const assignmentDir = resolve11(projectDir, "assignments", assignmentSlug);
5433
+ const assignmentDir = resolve12(projectDir, "assignments", assignmentSlug);
5171
5434
  if (await fileExists(assignmentDir)) {
5172
5435
  res.status(409).json({
5173
5436
  error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
@@ -5176,12 +5439,12 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5176
5439
  }
5177
5440
  const timestamp = fields.created || nowTimestamp();
5178
5441
  await ensureDir(assignmentDir);
5179
- await writeFileForce(resolve11(assignmentDir, "assignment.md"), content);
5442
+ await writeFileForce(resolve12(assignmentDir, "assignment.md"), content);
5180
5443
  try {
5181
5444
  const companions = [
5182
- [resolve11(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
5183
- [resolve11(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
5184
- [resolve11(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
5445
+ [resolve12(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
5446
+ [resolve12(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
5447
+ [resolve12(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
5185
5448
  ];
5186
5449
  for (const [filePath, fileContent] of companions) {
5187
5450
  await writeFileForce(filePath, fileContent);
@@ -5202,7 +5465,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5202
5465
  router.patch("/api/projects/:slug", async (req, res) => {
5203
5466
  try {
5204
5467
  const projectSlug = getParam(req.params.slug);
5205
- const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5468
+ const projectPath = resolve12(projectsDir, projectSlug, "project.md");
5206
5469
  const currentContent = await readCurrentDocument(projectPath);
5207
5470
  if (!currentContent) {
5208
5471
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
@@ -5235,7 +5498,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5235
5498
  try {
5236
5499
  const projectSlug = getParam(req.params.slug);
5237
5500
  const assignmentSlug = getParam(req.params.aslug);
5238
- const assignmentPath = resolve11(
5501
+ const assignmentPath = resolve12(
5239
5502
  projectsDir,
5240
5503
  projectSlug,
5241
5504
  "assignments",
@@ -5278,7 +5541,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5278
5541
  try {
5279
5542
  const projectSlug = getParam(req.params.slug);
5280
5543
  const assignmentSlug = getParam(req.params.aslug);
5281
- const assignmentPath = resolve11(
5544
+ const assignmentPath = resolve12(
5282
5545
  projectsDir,
5283
5546
  projectSlug,
5284
5547
  "assignments",
@@ -5314,7 +5577,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5314
5577
  try {
5315
5578
  const projectSlug = getParam(req.params.slug);
5316
5579
  const assignmentSlug = getParam(req.params.aslug);
5317
- const planPath = resolve11(
5580
+ const planPath = resolve12(
5318
5581
  projectsDir,
5319
5582
  projectSlug,
5320
5583
  "assignments",
@@ -5352,7 +5615,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5352
5615
  try {
5353
5616
  const projectSlug = getParam(req.params.slug);
5354
5617
  const assignmentSlug = getParam(req.params.aslug);
5355
- const scratchpadPath = resolve11(
5618
+ const scratchpadPath = resolve12(
5356
5619
  projectsDir,
5357
5620
  projectSlug,
5358
5621
  "assignments",
@@ -5390,7 +5653,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5390
5653
  try {
5391
5654
  const projectSlug = getParam(req.params.slug);
5392
5655
  const assignmentSlug = getParam(req.params.aslug);
5393
- const handoffPath = resolve11(
5656
+ const handoffPath = resolve12(
5394
5657
  projectsDir,
5395
5658
  projectSlug,
5396
5659
  "assignments",
@@ -5428,7 +5691,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5428
5691
  try {
5429
5692
  const projectSlug = getParam(req.params.slug);
5430
5693
  const assignmentSlug = getParam(req.params.aslug);
5431
- const decisionPath = resolve11(
5694
+ const decisionPath = resolve12(
5432
5695
  projectsDir,
5433
5696
  projectSlug,
5434
5697
  "assignments",
@@ -5466,7 +5729,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5466
5729
  try {
5467
5730
  const projectSlug = getParam(req.params.slug);
5468
5731
  const assignmentSlug = getParam(req.params.aslug);
5469
- const commentsPath = resolve11(
5732
+ const commentsPath = resolve12(
5470
5733
  projectsDir,
5471
5734
  projectSlug,
5472
5735
  "assignments",
@@ -5484,7 +5747,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5484
5747
  let currentContent;
5485
5748
  let currentCount = 0;
5486
5749
  if (await fileExists(commentsPath)) {
5487
- currentContent = await readFile9(commentsPath, "utf-8");
5750
+ currentContent = await readFile10(commentsPath, "utf-8");
5488
5751
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
5489
5752
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
5490
5753
  } else {
@@ -5525,7 +5788,7 @@ ${entry}`;
5525
5788
  const projectSlug = getParam(req.params.slug);
5526
5789
  const assignmentSlug = getParam(req.params.aslug);
5527
5790
  const commentId = getParam(req.params.commentId);
5528
- const commentsPath = resolve11(
5791
+ const commentsPath = resolve12(
5529
5792
  projectsDir,
5530
5793
  projectSlug,
5531
5794
  "assignments",
@@ -5541,7 +5804,7 @@ ${entry}`;
5541
5804
  res.status(400).json({ error: "resolved (boolean) is required" });
5542
5805
  return;
5543
5806
  }
5544
- const content = await readFile9(commentsPath, "utf-8");
5807
+ const content = await readFile10(commentsPath, "utf-8");
5545
5808
  const parsed = parseComments(content);
5546
5809
  const target = parsed.entries.find((e) => e.id === commentId);
5547
5810
  if (!target) {
@@ -5576,7 +5839,7 @@ ${entry}`;
5576
5839
  router.post("/api/projects/:slug/move-workspace", async (req, res) => {
5577
5840
  try {
5578
5841
  const projectSlug = getParam(req.params.slug);
5579
- const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5842
+ const projectPath = resolve12(projectsDir, projectSlug, "project.md");
5580
5843
  if (!await fileExists(projectPath)) {
5581
5844
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5582
5845
  return;
@@ -5586,7 +5849,7 @@ ${entry}`;
5586
5849
  res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
5587
5850
  return;
5588
5851
  }
5589
- let content = await readFile9(projectPath, "utf-8");
5852
+ let content = await readFile10(projectPath, "utf-8");
5590
5853
  content = setTopLevelField(content, "workspace", workspace ?? null);
5591
5854
  content = setTopLevelField(content, "updated", nowTimestamp());
5592
5855
  await writeFileForce(projectPath, content);
@@ -5600,7 +5863,7 @@ ${entry}`;
5600
5863
  router.post("/api/projects/:slug/status-override", async (req, res) => {
5601
5864
  try {
5602
5865
  const projectSlug = getParam(req.params.slug);
5603
- const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5866
+ const projectPath = resolve12(projectsDir, projectSlug, "project.md");
5604
5867
  if (!await fileExists(projectPath)) {
5605
5868
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5606
5869
  return;
@@ -5612,7 +5875,7 @@ ${entry}`;
5612
5875
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
5613
5876
  return;
5614
5877
  }
5615
- let content = await readFile9(projectPath, "utf-8");
5878
+ let content = await readFile10(projectPath, "utf-8");
5616
5879
  content = setTopLevelField(content, "statusOverride", status ?? null);
5617
5880
  content = setTopLevelField(content, "updated", nowTimestamp());
5618
5881
  await writeFileForce(projectPath, content);
@@ -5627,7 +5890,7 @@ ${entry}`;
5627
5890
  try {
5628
5891
  const projectSlug = getParam(req.params.slug);
5629
5892
  const assignmentSlug = getParam(req.params.aslug);
5630
- const assignmentPath = resolve11(
5893
+ const assignmentPath = resolve12(
5631
5894
  projectsDir,
5632
5895
  projectSlug,
5633
5896
  "assignments",
@@ -5645,7 +5908,7 @@ ${entry}`;
5645
5908
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
5646
5909
  return;
5647
5910
  }
5648
- let content = await readFile9(assignmentPath, "utf-8");
5911
+ let content = await readFile10(assignmentPath, "utf-8");
5649
5912
  content = setTopLevelField(content, "status", status);
5650
5913
  content = setTopLevelField(content, "updated", nowTimestamp());
5651
5914
  if (status !== "blocked") {
@@ -5670,8 +5933,8 @@ ${entry}`;
5670
5933
  res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
5671
5934
  return;
5672
5935
  }
5673
- const projectDir = resolve11(projectsDir, projectSlug);
5674
- const assignmentPath = resolve11(projectDir, "assignments", assignmentSlug, "assignment.md");
5936
+ const projectDir = resolve12(projectsDir, projectSlug);
5937
+ const assignmentPath = resolve12(projectDir, "assignments", assignmentSlug, "assignment.md");
5675
5938
  if (!await fileExists(assignmentPath)) {
5676
5939
  res.status(404).json({ error: "Assignment not found" });
5677
5940
  return;
@@ -5697,8 +5960,8 @@ ${entry}`;
5697
5960
  try {
5698
5961
  const projectSlug = getParam(req.params.slug);
5699
5962
  const assignmentSlug = getParam(req.params.aslug);
5700
- const assignmentDir = resolve11(projectsDir, projectSlug, "assignments", assignmentSlug);
5701
- const assignmentPath = resolve11(assignmentDir, "assignment.md");
5963
+ const assignmentDir = resolve12(projectsDir, projectSlug, "assignments", assignmentSlug);
5964
+ const assignmentPath = resolve12(assignmentDir, "assignment.md");
5702
5965
  if (!await fileExists(assignmentPath)) {
5703
5966
  res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
5704
5967
  return;
@@ -5716,6 +5979,76 @@ ${entry}`;
5716
5979
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
5717
5980
  return;
5718
5981
  }
5982
+ const rawContent = typeof req.body?.content === "string" ? req.body.content : "";
5983
+ if (rawContent.trim()) {
5984
+ const fields = extractFrontmatter3(rawContent);
5985
+ if (!fields) {
5986
+ res.status(400).json({ error: "Invalid frontmatter: missing --- delimiters" });
5987
+ return;
5988
+ }
5989
+ const validation = validateRequired(fields, ["slug", "title"]);
5990
+ if (!validation.valid) {
5991
+ res.status(400).json({ error: `Missing required fields: ${validation.missing.join(", ")}` });
5992
+ return;
5993
+ }
5994
+ const submittedSlug = fields.slug;
5995
+ if (!isValidSlug(submittedSlug)) {
5996
+ res.status(400).json({ error: `Invalid slug "${submittedSlug}". Must be lowercase and hyphen-separated.` });
5997
+ return;
5998
+ }
5999
+ const validPriorities = ["low", "medium", "high", "critical"];
6000
+ const submittedPriority = fields.priority || "medium";
6001
+ if (!validPriorities.includes(submittedPriority)) {
6002
+ res.status(400).json({ error: `Invalid priority "${submittedPriority}". Must be low, medium, high, or critical.` });
6003
+ return;
6004
+ }
6005
+ if (fields.project && fields.project !== "null") {
6006
+ res.status(400).json({
6007
+ error: 'Standalone assignments cannot have a project; remove "project" or set it to null.'
6008
+ });
6009
+ return;
6010
+ }
6011
+ const submittedWorkspaceGroup = fields.workspaceGroup && fields.workspaceGroup !== "null" ? fields.workspaceGroup : "";
6012
+ if (submittedWorkspaceGroup && !isValidSlug(submittedWorkspaceGroup)) {
6013
+ res.status(400).json({
6014
+ error: `Invalid workspace slug "${submittedWorkspaceGroup}". Slugs must be lowercase, hyphen-separated, with no special characters.`
6015
+ });
6016
+ return;
6017
+ }
6018
+ const id2 = generateId();
6019
+ const assignmentDir2 = resolve12(assignmentsDir, id2);
6020
+ if (await fileExists(assignmentDir2)) {
6021
+ res.status(500).json({ error: "UUID collision \u2014 try again" });
6022
+ return;
6023
+ }
6024
+ const timestamp2 = fields.created || nowTimestamp();
6025
+ await ensureDir(assignmentDir2);
6026
+ const normalizedContent = setTopLevelField(rawContent, "id", id2);
6027
+ await writeFileForce(resolve12(assignmentDir2, "assignment.md"), normalizedContent);
6028
+ await writeFileForce(
6029
+ resolve12(assignmentDir2, "scratchpad.md"),
6030
+ renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
6031
+ );
6032
+ await writeFileForce(
6033
+ resolve12(assignmentDir2, "handoff.md"),
6034
+ renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
6035
+ );
6036
+ await writeFileForce(
6037
+ resolve12(assignmentDir2, "decision-record.md"),
6038
+ renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
6039
+ );
6040
+ await writeFileForce(
6041
+ resolve12(assignmentDir2, "progress.md"),
6042
+ renderProgress({ assignment: id2, timestamp: timestamp2 })
6043
+ );
6044
+ await writeFileForce(
6045
+ resolve12(assignmentDir2, "comments.md"),
6046
+ renderComments({ assignment: id2, timestamp: timestamp2 })
6047
+ );
6048
+ const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir, id2);
6049
+ res.status(201).json({ assignment: detail2 });
6050
+ return;
6051
+ }
5719
6052
  const { title, slug, priority, type } = req.body || {};
5720
6053
  if (!title || typeof title !== "string" || !title.trim()) {
5721
6054
  res.status(400).json({ error: "title is required" });
@@ -5727,7 +6060,7 @@ ${entry}`;
5727
6060
  return;
5728
6061
  }
5729
6062
  const id = generateId();
5730
- const assignmentDir = resolve11(assignmentsDir, id);
6063
+ const assignmentDir = resolve12(assignmentsDir, id);
5731
6064
  if (await fileExists(assignmentDir)) {
5732
6065
  res.status(500).json({ error: "UUID collision \u2014 try again" });
5733
6066
  return;
@@ -5747,25 +6080,25 @@ ${entry}`;
5747
6080
  project: null,
5748
6081
  type: typeof type === "string" ? type : void 0
5749
6082
  });
5750
- await writeFileForce(resolve11(assignmentDir, "assignment.md"), assignmentContent);
6083
+ await writeFileForce(resolve12(assignmentDir, "assignment.md"), assignmentContent);
5751
6084
  await writeFileForce(
5752
- resolve11(assignmentDir, "scratchpad.md"),
6085
+ resolve12(assignmentDir, "scratchpad.md"),
5753
6086
  renderScratchpad({ assignmentSlug: id, timestamp })
5754
6087
  );
5755
6088
  await writeFileForce(
5756
- resolve11(assignmentDir, "handoff.md"),
6089
+ resolve12(assignmentDir, "handoff.md"),
5757
6090
  renderHandoff({ assignmentSlug: id, timestamp })
5758
6091
  );
5759
6092
  await writeFileForce(
5760
- resolve11(assignmentDir, "decision-record.md"),
6093
+ resolve12(assignmentDir, "decision-record.md"),
5761
6094
  renderDecisionRecord({ assignmentSlug: id, timestamp })
5762
6095
  );
5763
6096
  await writeFileForce(
5764
- resolve11(assignmentDir, "progress.md"),
6097
+ resolve12(assignmentDir, "progress.md"),
5765
6098
  renderProgress({ assignment: id, timestamp })
5766
6099
  );
5767
6100
  await writeFileForce(
5768
- resolve11(assignmentDir, "comments.md"),
6101
+ resolve12(assignmentDir, "comments.md"),
5769
6102
  renderComments({ assignment: id, timestamp })
5770
6103
  );
5771
6104
  const detail = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
@@ -5893,7 +6226,7 @@ ${entry}`;
5893
6226
  res.status(404).json({ error: `Assignment "${id}" not found` });
5894
6227
  return;
5895
6228
  }
5896
- const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
6229
+ const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
5897
6230
  const currentContent = await readCurrentDocument(assignmentPath);
5898
6231
  if (!currentContent) {
5899
6232
  res.status(404).json({ error: "Assignment not found" });
@@ -5935,7 +6268,7 @@ ${entry}`;
5935
6268
  res.status(404).json({ error: `Assignment "${id}" not found` });
5936
6269
  return;
5937
6270
  }
5938
- const planPath = resolve11(resolved.assignmentDir, "plan.md");
6271
+ const planPath = resolve12(resolved.assignmentDir, "plan.md");
5939
6272
  const currentContent = await readCurrentDocument(planPath);
5940
6273
  if (!currentContent) {
5941
6274
  res.status(404).json({ error: "Plan not found" });
@@ -5969,7 +6302,7 @@ ${entry}`;
5969
6302
  res.status(404).json({ error: `Assignment "${id}" not found` });
5970
6303
  return;
5971
6304
  }
5972
- const scratchpadPath = resolve11(resolved.assignmentDir, "scratchpad.md");
6305
+ const scratchpadPath = resolve12(resolved.assignmentDir, "scratchpad.md");
5973
6306
  const currentContent = await readCurrentDocument(scratchpadPath);
5974
6307
  if (!currentContent) {
5975
6308
  res.status(404).json({ error: "Scratchpad not found" });
@@ -6003,7 +6336,7 @@ ${entry}`;
6003
6336
  res.status(404).json({ error: `Assignment "${id}" not found` });
6004
6337
  return;
6005
6338
  }
6006
- const handoffPath = resolve11(resolved.assignmentDir, "handoff.md");
6339
+ const handoffPath = resolve12(resolved.assignmentDir, "handoff.md");
6007
6340
  const currentContent = await readCurrentDocument(handoffPath);
6008
6341
  if (!currentContent) {
6009
6342
  res.status(404).json({ error: "Handoff log not found" });
@@ -6043,7 +6376,7 @@ ${entry}`;
6043
6376
  res.status(404).json({ error: `Assignment "${id}" not found` });
6044
6377
  return;
6045
6378
  }
6046
- const decisionPath = resolve11(resolved.assignmentDir, "decision-record.md");
6379
+ const decisionPath = resolve12(resolved.assignmentDir, "decision-record.md");
6047
6380
  const currentContent = await readCurrentDocument(decisionPath);
6048
6381
  if (!currentContent) {
6049
6382
  res.status(404).json({ error: "Decision record not found" });
@@ -6083,7 +6416,7 @@ ${entry}`;
6083
6416
  res.status(404).json({ error: `Assignment "${id}" not found` });
6084
6417
  return;
6085
6418
  }
6086
- const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
6419
+ const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
6087
6420
  if (!await fileExists(assignmentPath)) {
6088
6421
  res.status(404).json({ error: "Assignment not found" });
6089
6422
  return;
@@ -6095,7 +6428,7 @@ ${entry}`;
6095
6428
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
6096
6429
  return;
6097
6430
  }
6098
- let content = await readFile9(assignmentPath, "utf-8");
6431
+ let content = await readFile10(assignmentPath, "utf-8");
6099
6432
  content = setTopLevelField(content, "status", status);
6100
6433
  content = setTopLevelField(content, "updated", nowTimestamp());
6101
6434
  if (status !== "blocked") {
@@ -6121,7 +6454,7 @@ ${entry}`;
6121
6454
  res.status(404).json({ error: `Assignment "${id}" not found` });
6122
6455
  return;
6123
6456
  }
6124
- const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
6457
+ const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
6125
6458
  const currentContent = await readCurrentDocument(assignmentPath);
6126
6459
  if (!currentContent) {
6127
6460
  res.status(404).json({ error: "Assignment not found" });
@@ -6186,7 +6519,7 @@ function slugifyLocal(input) {
6186
6519
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
6187
6520
  }
6188
6521
  async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
6189
- const commentsPath = resolve11(assignmentDir, "comments.md");
6522
+ const commentsPath = resolve12(assignmentDir, "comments.md");
6190
6523
  const { body, author, type, replyTo } = req.body || {};
6191
6524
  if (!body || typeof body !== "string" || !body.trim()) {
6192
6525
  res.status(400).json({ error: "body is required" });
@@ -6198,7 +6531,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
6198
6531
  let currentContent;
6199
6532
  let currentCount = 0;
6200
6533
  if (await fileExists(commentsPath)) {
6201
- currentContent = await readFile9(commentsPath, "utf-8");
6534
+ currentContent = await readFile10(commentsPath, "utf-8");
6202
6535
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
6203
6536
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
6204
6537
  } else {
@@ -6228,7 +6561,7 @@ ${entry}`;
6228
6561
  res.status(201).json({ assignment, comment: { id: comment.id } });
6229
6562
  }
6230
6563
  async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
6231
- const commentsPath = resolve11(assignmentDir, "comments.md");
6564
+ const commentsPath = resolve12(assignmentDir, "comments.md");
6232
6565
  if (!await fileExists(commentsPath)) {
6233
6566
  res.status(404).json({ error: "Comments file not found" });
6234
6567
  return;
@@ -6238,7 +6571,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
6238
6571
  res.status(400).json({ error: "resolved (boolean) is required" });
6239
6572
  return;
6240
6573
  }
6241
- const content = await readFile9(commentsPath, "utf-8");
6574
+ const content = await readFile10(commentsPath, "utf-8");
6242
6575
  const parsed = parseComments(content);
6243
6576
  const target = parsed.entries.find((e) => e.id === commentId);
6244
6577
  if (!target) {
@@ -6383,7 +6716,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
6383
6716
 
6384
6717
  // src/dashboard/api-agent-sessions.ts
6385
6718
  import { Router as Router3 } from "express";
6386
- import { resolve as resolve12 } from "path";
6719
+ import { resolve as resolve13 } from "path";
6387
6720
  init_fs();
6388
6721
  function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6389
6722
  const router = Router3();
@@ -6400,7 +6733,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6400
6733
  try {
6401
6734
  const { projectSlug } = req.params;
6402
6735
  const assignment = req.query.assignment;
6403
- const projectDir = resolve12(projectsDir, projectSlug);
6736
+ const projectDir = resolve13(projectsDir, projectSlug);
6404
6737
  if (!await fileExists(projectDir)) {
6405
6738
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
6406
6739
  return;
@@ -6426,7 +6759,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6426
6759
  return;
6427
6760
  }
6428
6761
  if (projectSlug) {
6429
- const projectDir = resolve12(projectsDir, projectSlug);
6762
+ const projectDir = resolve13(projectsDir, projectSlug);
6430
6763
  if (!await fileExists(projectDir)) {
6431
6764
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
6432
6765
  return;
@@ -6498,53 +6831,7 @@ import { resolve as resolve14 } from "path";
6498
6831
  import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
6499
6832
  init_timestamp();
6500
6833
  init_fs();
6501
-
6502
- // src/utils/playbooks.ts
6503
- init_fs();
6504
- init_parser();
6505
- init_timestamp();
6506
- import { resolve as resolve13 } from "path";
6507
- import { readdir as readdir7, readFile as readFile10 } from "fs/promises";
6508
- async function rebuildPlaybookManifest(playbooksDir2) {
6509
- if (!await fileExists(playbooksDir2)) return;
6510
- const entries = await readdir7(playbooksDir2, { withFileTypes: true });
6511
- const rows = [];
6512
- for (const entry of entries) {
6513
- if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
6514
- const raw = await readFile10(resolve13(playbooksDir2, entry.name), "utf-8");
6515
- const parsed = parsePlaybook(raw);
6516
- const slug = parsed.slug || entry.name.replace(/\.md$/, "");
6517
- rows.push({
6518
- name: parsed.name || slug,
6519
- slug,
6520
- description: parsed.description,
6521
- whenToUse: parsed.whenToUse
6522
- });
6523
- }
6524
- rows.sort((a, b) => a.name.localeCompare(b.name));
6525
- const timestamp = nowTimestamp();
6526
- const lines = [
6527
- "---",
6528
- `generated: "${timestamp}"`,
6529
- `total: ${rows.length}`,
6530
- "---",
6531
- "",
6532
- "# Playbooks",
6533
- "",
6534
- "Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
6535
- ""
6536
- ];
6537
- for (const row of rows) {
6538
- lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
6539
- if (row.whenToUse) {
6540
- lines.push(` _When to use: ${row.whenToUse}_`);
6541
- }
6542
- }
6543
- lines.push("");
6544
- await writeFileForce(resolve13(playbooksDir2, "manifest.md"), lines.join("\n"));
6545
- }
6546
-
6547
- // src/dashboard/api-playbooks.ts
6834
+ init_playbooks();
6548
6835
  function createPlaybooksRouter(playbooksDir2) {
6549
6836
  const router = Router4();
6550
6837
  router.get("/", async (_req, res) => {
@@ -6568,6 +6855,32 @@ function createPlaybooksRouter(playbooksDir2) {
6568
6855
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get template" });
6569
6856
  }
6570
6857
  });
6858
+ router.post("/:slug/enable", async (req, res) => {
6859
+ try {
6860
+ const result = await setPlaybookEnabled(playbooksDir2, req.params.slug, true);
6861
+ res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
6862
+ } catch (error) {
6863
+ const msg = error instanceof Error ? error.message : "Failed to enable playbook";
6864
+ if (msg.startsWith("Playbook ")) {
6865
+ res.status(404).json({ error: msg });
6866
+ return;
6867
+ }
6868
+ res.status(500).json({ error: msg });
6869
+ }
6870
+ });
6871
+ router.post("/:slug/disable", async (req, res) => {
6872
+ try {
6873
+ const result = await setPlaybookEnabled(playbooksDir2, req.params.slug, false);
6874
+ res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
6875
+ } catch (error) {
6876
+ const msg = error instanceof Error ? error.message : "Failed to disable playbook";
6877
+ if (msg.startsWith("Playbook ")) {
6878
+ res.status(404).json({ error: msg });
6879
+ return;
6880
+ }
6881
+ res.status(500).json({ error: msg });
6882
+ }
6883
+ });
6571
6884
  router.get("/:slug", async (req, res) => {
6572
6885
  try {
6573
6886
  const detail = await getPlaybookDetail(playbooksDir2, req.params.slug);
@@ -6582,17 +6895,18 @@ function createPlaybooksRouter(playbooksDir2) {
6582
6895
  });
6583
6896
  router.get("/:slug/edit", async (req, res) => {
6584
6897
  try {
6585
- const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6586
- if (!await fileExists(filePath)) {
6898
+ const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
6899
+ if (!resolved) {
6587
6900
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6588
6901
  return;
6589
6902
  }
6903
+ const filePath = resolve14(playbooksDir2, resolved.filename);
6590
6904
  const content = await readFile11(filePath, "utf-8");
6591
6905
  res.json({
6592
6906
  documentType: "playbook",
6593
- title: `Edit Playbook: ${req.params.slug}`,
6907
+ title: `Edit Playbook: ${resolved.slug}`,
6594
6908
  content,
6595
- slug: req.params.slug
6909
+ slug: resolved.slug
6596
6910
  });
6597
6911
  } catch (error) {
6598
6912
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get playbook for editing" });
@@ -6631,14 +6945,15 @@ function createPlaybooksRouter(playbooksDir2) {
6631
6945
  res.status(400).json({ error: "content is required" });
6632
6946
  return;
6633
6947
  }
6634
- const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6635
- if (!await fileExists(filePath)) {
6948
+ const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
6949
+ if (!resolved) {
6636
6950
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6637
6951
  return;
6638
6952
  }
6953
+ const filePath = resolve14(playbooksDir2, resolved.filename);
6639
6954
  await writeFileForce(filePath, content);
6640
6955
  await rebuildPlaybookManifest(playbooksDir2);
6641
- res.json({ slug: req.params.slug, path: filePath });
6956
+ res.json({ slug: resolved.slug, path: filePath });
6642
6957
  } catch (error) {
6643
6958
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
6644
6959
  }
@@ -6649,14 +6964,16 @@ function createPlaybooksRouter(playbooksDir2) {
6649
6964
  res.status(403).json({ error: "The playbook manifest cannot be deleted" });
6650
6965
  return;
6651
6966
  }
6652
- const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6653
- if (!await fileExists(filePath)) {
6967
+ const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
6968
+ if (!resolved) {
6654
6969
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6655
6970
  return;
6656
6971
  }
6972
+ const filePath = resolve14(playbooksDir2, resolved.filename);
6657
6973
  await unlink2(filePath);
6974
+ await removeFromDisabledList(resolved.slug);
6658
6975
  await rebuildPlaybookManifest(playbooksDir2);
6659
- res.json({ deleted: req.params.slug });
6976
+ res.json({ deleted: resolved.slug });
6660
6977
  } catch (error) {
6661
6978
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
6662
6979
  }
@@ -6680,14 +6997,17 @@ function getWorkspaceParam(value) {
6680
6997
  return value ?? "";
6681
6998
  }
6682
6999
  var writeLocks = /* @__PURE__ */ new Map();
6683
- function withLock(workspace, fn) {
6684
- const prev = writeLocks.get(workspace) ?? Promise.resolve();
7000
+ function withLock(lockKey, fn) {
7001
+ const prev = writeLocks.get(lockKey) ?? Promise.resolve();
6685
7002
  const next = prev.then(fn);
6686
- writeLocks.set(workspace, next.then(() => {
7003
+ writeLocks.set(lockKey, next.then(() => {
6687
7004
  }, () => {
6688
7005
  }));
6689
7006
  return next;
6690
7007
  }
7008
+ function wsLock(workspace, fn) {
7009
+ return withLock(`ws:${workspace}`, fn);
7010
+ }
6691
7011
  function createTodosRouter(todosDir2, broadcast) {
6692
7012
  const router = Router5();
6693
7013
  function broadcastUpdate() {
@@ -6746,7 +7066,7 @@ function createTodosRouter(todosDir2, broadcast) {
6746
7066
  res.status(400).json({ error: "description is required" });
6747
7067
  return;
6748
7068
  }
6749
- const item = await withLock(workspace, async () => {
7069
+ const item = await wsLock(workspace, async () => {
6750
7070
  const checklist = await readChecklist(todosDir2, workspace);
6751
7071
  const existingIds = new Set(checklist.items.map((i) => i.id));
6752
7072
  const id = generateUniqueId(existingIds);
@@ -6775,7 +7095,7 @@ function createTodosRouter(todosDir2, broadcast) {
6775
7095
  res.status(400).json({ error: "ids must be an array of strings" });
6776
7096
  return;
6777
7097
  }
6778
- const items = await withLock(workspace, async () => {
7098
+ const items = await wsLock(workspace, async () => {
6779
7099
  const checklist = await readChecklist(todosDir2, workspace);
6780
7100
  const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
6781
7101
  const reordered = [];
@@ -6810,8 +7130,8 @@ function createTodosRouter(todosDir2, broadcast) {
6810
7130
  router.post("/:workspace/archive", async (req, res) => {
6811
7131
  try {
6812
7132
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
6813
- const { resolve: resolve18 } = await import("path");
6814
- const { readFile: readFile14 } = await import("fs/promises");
7133
+ const { resolve: resolve19 } = await import("path");
7134
+ const { readFile: readFile15 } = await import("fs/promises");
6815
7135
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
6816
7136
  const workspace = getWorkspaceParam(req.params.workspace);
6817
7137
  const checklist = await readChecklist(todosDir2, workspace);
@@ -6827,10 +7147,10 @@ function createTodosRouter(todosDir2, broadcast) {
6827
7147
  (e) => e.itemIds.every((id) => completedIds.has(id))
6828
7148
  );
6829
7149
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
6830
- await ensureDir(resolve18(todosDir2, "archive"));
7150
+ await ensureDir(resolve19(todosDir2, "archive"));
6831
7151
  let archContent = "";
6832
7152
  if (await fileExists(archFile)) {
6833
- archContent = await readFile14(archFile, "utf-8");
7153
+ archContent = await readFile15(archFile, "utf-8");
6834
7154
  archContent = archContent.trimEnd() + "\n\n";
6835
7155
  } else {
6836
7156
  archContent = `---
@@ -6899,7 +7219,7 @@ workspace: ${workspace}
6899
7219
  router.patch("/:workspace/:id", async (req, res) => {
6900
7220
  try {
6901
7221
  const workspace = getWorkspaceParam(req.params.workspace);
6902
- const result = await withLock(workspace, async () => {
7222
+ const result = await wsLock(workspace, async () => {
6903
7223
  const checklist = await readChecklist(todosDir2, workspace);
6904
7224
  const item = checklist.items.find((i) => i.id === req.params.id);
6905
7225
  if (!item) return null;
@@ -6921,7 +7241,7 @@ workspace: ${workspace}
6921
7241
  router.delete("/:workspace/:id", async (req, res) => {
6922
7242
  try {
6923
7243
  const workspace = getWorkspaceParam(req.params.workspace);
6924
- const deleted = await withLock(workspace, async () => {
7244
+ const deleted = await wsLock(workspace, async () => {
6925
7245
  const checklist = await readChecklist(todosDir2, workspace);
6926
7246
  const idx = checklist.items.findIndex((i) => i.id === req.params.id);
6927
7247
  if (idx === -1) return false;
@@ -6942,7 +7262,7 @@ workspace: ${workspace}
6942
7262
  router.post("/:workspace/:id/start", async (req, res) => {
6943
7263
  try {
6944
7264
  const workspace = getWorkspaceParam(req.params.workspace);
6945
- const result = await withLock(workspace, async () => {
7265
+ const result = await wsLock(workspace, async () => {
6946
7266
  const checklist = await readChecklist(todosDir2, workspace);
6947
7267
  const item = checklist.items.find((i) => i.id === req.params.id);
6948
7268
  if (!item) return { error: "not_found" };
@@ -6969,7 +7289,7 @@ workspace: ${workspace}
6969
7289
  router.post("/:workspace/:id/complete", async (req, res) => {
6970
7290
  try {
6971
7291
  const workspace = getWorkspaceParam(req.params.workspace);
6972
- const result = await withLock(workspace, async () => {
7292
+ const result = await wsLock(workspace, async () => {
6973
7293
  const checklist = await readChecklist(todosDir2, workspace);
6974
7294
  const item = checklist.items.find((i) => i.id === req.params.id);
6975
7295
  if (!item) return null;
@@ -7003,7 +7323,7 @@ workspace: ${workspace}
7003
7323
  try {
7004
7324
  const reason = req.body.reason || null;
7005
7325
  const workspace = getWorkspaceParam(req.params.workspace);
7006
- const result = await withLock(workspace, async () => {
7326
+ const result = await wsLock(workspace, async () => {
7007
7327
  const checklist = await readChecklist(todosDir2, workspace);
7008
7328
  const item = checklist.items.find((i) => i.id === req.params.id);
7009
7329
  if (!item) return null;
@@ -7036,7 +7356,7 @@ workspace: ${workspace}
7036
7356
  router.post("/:workspace/:id/reopen", async (req, res) => {
7037
7357
  try {
7038
7358
  const workspace = getWorkspaceParam(req.params.workspace);
7039
- const result = await withLock(workspace, async () => {
7359
+ const result = await wsLock(workspace, async () => {
7040
7360
  const checklist = await readChecklist(todosDir2, workspace);
7041
7361
  const item = checklist.items.find((i) => i.id === req.params.id);
7042
7362
  if (!item) return null;
@@ -7058,7 +7378,7 @@ workspace: ${workspace}
7058
7378
  router.post("/:workspace/:id/unblock", async (req, res) => {
7059
7379
  try {
7060
7380
  const workspace = getWorkspaceParam(req.params.workspace);
7061
- const result = await withLock(workspace, async () => {
7381
+ const result = await wsLock(workspace, async () => {
7062
7382
  const checklist = await readChecklist(todosDir2, workspace);
7063
7383
  const item = checklist.items.find((i) => i.id === req.params.id);
7064
7384
  if (!item) return null;
@@ -7080,18 +7400,615 @@ workspace: ${workspace}
7080
7400
  return router;
7081
7401
  }
7082
7402
 
7083
- // src/dashboard/api-backup.ts
7084
- init_config2();
7085
- import { Router as Router6 } from "express";
7086
-
7087
- // src/utils/github-backup.ts
7088
- init_paths();
7403
+ // src/dashboard/api-project-todos.ts
7404
+ init_parser2();
7089
7405
  init_fs();
7090
- init_config2();
7406
+ init_paths();
7407
+ import { Router as Router6 } from "express";
7408
+ import { mkdir as mkdir2, readFile as readFile13 } from "fs/promises";
7409
+ import { resolve as resolve16 } from "path";
7410
+ var writeLocks2 = /* @__PURE__ */ new Map();
7411
+ function projLock(slug, fn) {
7412
+ const key = `proj:${slug}`;
7413
+ const prev = writeLocks2.get(key) ?? Promise.resolve();
7414
+ const next = prev.then(fn);
7415
+ writeLocks2.set(key, next.then(() => {
7416
+ }, () => {
7417
+ }));
7418
+ return next;
7419
+ }
7420
+ function getProjectIdParam(value) {
7421
+ if (Array.isArray(value)) return value[0] ?? "";
7422
+ return value ?? "";
7423
+ }
7424
+ function params(req) {
7425
+ return req.params;
7426
+ }
7427
+ async function projectExists(projectsDir, slug) {
7428
+ return fileExists(resolve16(projectsDir, slug, "project.md"));
7429
+ }
7430
+ async function ensureProjectTodosDir(projectsDir, slug) {
7431
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7432
+ try {
7433
+ await mkdir2(todosDir2, { recursive: false });
7434
+ } catch (err) {
7435
+ const code = err.code;
7436
+ if (code === "EEXIST") return;
7437
+ if (code === "ENOENT") {
7438
+ const e = new Error("PROJECT_GONE");
7439
+ e.code = "PROJECT_GONE";
7440
+ throw e;
7441
+ }
7442
+ throw err;
7443
+ }
7444
+ try {
7445
+ await mkdir2(resolve16(todosDir2, "archive"), { recursive: false });
7446
+ } catch (err) {
7447
+ const code = err.code;
7448
+ if (code === "EEXIST") return;
7449
+ if (code === "ENOENT") {
7450
+ const e = new Error("PROJECT_GONE");
7451
+ e.code = "PROJECT_GONE";
7452
+ throw e;
7453
+ }
7454
+ throw err;
7455
+ }
7456
+ }
7457
+ function notFound(res, slug) {
7458
+ res.status(404).json({ error: `Project "${slug}" not found` });
7459
+ }
7460
+ function createProjectTodosRouter(projectsDir, broadcast) {
7461
+ const router = Router6({ mergeParams: true });
7462
+ function broadcastUpdate(projectSlug) {
7463
+ broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
7464
+ }
7465
+ function validateProjectId(req, res, next) {
7466
+ const slug = getProjectIdParam(params(req).projectId);
7467
+ if (!slug || !isValidSlug(slug)) {
7468
+ res.status(400).json({ error: `Invalid project slug: "${slug}"` });
7469
+ return;
7470
+ }
7471
+ next();
7472
+ }
7473
+ router.use(validateProjectId);
7474
+ router.get("/", async (req, res) => {
7475
+ try {
7476
+ const slug = getProjectIdParam(params(req).projectId);
7477
+ if (!await projectExists(projectsDir, slug)) {
7478
+ notFound(res, slug);
7479
+ return;
7480
+ }
7481
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7482
+ const checklist = await readChecklist(todosDir2, slug);
7483
+ res.json({
7484
+ workspace: checklist.workspace,
7485
+ archiveInterval: checklist.archiveInterval,
7486
+ items: checklist.items,
7487
+ counts: computeCounts(checklist.items)
7488
+ });
7489
+ } catch (error) {
7490
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todos" });
7491
+ }
7492
+ });
7493
+ router.post("/", async (req, res) => {
7494
+ try {
7495
+ const slug = getProjectIdParam(params(req).projectId);
7496
+ const { description, tags } = req.body;
7497
+ if (!description || typeof description !== "string") {
7498
+ res.status(400).json({ error: "description is required" });
7499
+ return;
7500
+ }
7501
+ if (!await projectExists(projectsDir, slug)) {
7502
+ notFound(res, slug);
7503
+ return;
7504
+ }
7505
+ const item = await projLock(slug, async () => {
7506
+ if (!await projectExists(projectsDir, slug)) return null;
7507
+ await ensureProjectTodosDir(projectsDir, slug);
7508
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7509
+ const checklist = await readChecklist(todosDir2, slug);
7510
+ const existingIds = new Set(checklist.items.map((i) => i.id));
7511
+ const id = generateUniqueId(existingIds);
7512
+ const newItem = {
7513
+ id,
7514
+ description,
7515
+ status: "open",
7516
+ tags: Array.isArray(tags) ? tags : [],
7517
+ session: null
7518
+ };
7519
+ checklist.workspace = slug;
7520
+ checklist.items.push(newItem);
7521
+ await writeChecklist(todosDir2, checklist);
7522
+ return newItem;
7523
+ });
7524
+ if (!item) {
7525
+ notFound(res, slug);
7526
+ return;
7527
+ }
7528
+ broadcastUpdate(slug);
7529
+ res.status(201).json(item);
7530
+ } catch (error) {
7531
+ if (error.code === "PROJECT_GONE") {
7532
+ notFound(res, getProjectIdParam(params(req).projectId));
7533
+ return;
7534
+ }
7535
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to add todo" });
7536
+ }
7537
+ });
7538
+ router.post("/reorder", async (req, res) => {
7539
+ try {
7540
+ const slug = getProjectIdParam(params(req).projectId);
7541
+ const { ids } = req.body;
7542
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string")) {
7543
+ res.status(400).json({ error: "ids must be an array of strings" });
7544
+ return;
7545
+ }
7546
+ if (!await projectExists(projectsDir, slug)) {
7547
+ notFound(res, slug);
7548
+ return;
7549
+ }
7550
+ const items = await projLock(slug, async () => {
7551
+ if (!await projectExists(projectsDir, slug)) return null;
7552
+ await ensureProjectTodosDir(projectsDir, slug);
7553
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7554
+ const checklist = await readChecklist(todosDir2, slug);
7555
+ const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
7556
+ const reordered = [];
7557
+ for (const id of ids) {
7558
+ const item = itemMap.get(id);
7559
+ if (item) {
7560
+ reordered.push(item);
7561
+ itemMap.delete(id);
7562
+ }
7563
+ }
7564
+ for (const item of itemMap.values()) reordered.push(item);
7565
+ checklist.workspace = slug;
7566
+ checklist.items = reordered;
7567
+ await writeChecklist(todosDir2, checklist);
7568
+ return reordered;
7569
+ });
7570
+ if (!items) {
7571
+ notFound(res, slug);
7572
+ return;
7573
+ }
7574
+ broadcastUpdate(slug);
7575
+ res.json({ items });
7576
+ } catch (error) {
7577
+ if (error.code === "PROJECT_GONE") {
7578
+ notFound(res, getProjectIdParam(params(req).projectId));
7579
+ return;
7580
+ }
7581
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reorder todos" });
7582
+ }
7583
+ });
7584
+ router.get("/log", async (req, res) => {
7585
+ try {
7586
+ const slug = getProjectIdParam(params(req).projectId);
7587
+ if (!await projectExists(projectsDir, slug)) {
7588
+ notFound(res, slug);
7589
+ return;
7590
+ }
7591
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7592
+ const log = await readLog(todosDir2, slug);
7593
+ res.json(log);
7594
+ } catch (error) {
7595
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
7596
+ }
7597
+ });
7598
+ router.post("/archive", async (req, res) => {
7599
+ try {
7600
+ const slug = getProjectIdParam(params(req).projectId);
7601
+ if (!await projectExists(projectsDir, slug)) {
7602
+ notFound(res, slug);
7603
+ return;
7604
+ }
7605
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7606
+ await ensureProjectTodosDir(projectsDir, slug);
7607
+ const checklist = await readChecklist(todosDir2, slug);
7608
+ const log = await readLog(todosDir2, slug);
7609
+ const completedIds = new Set(
7610
+ checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
7611
+ );
7612
+ if (completedIds.size === 0) {
7613
+ res.json({ archived: 0, message: "No completed items to archive" });
7614
+ return;
7615
+ }
7616
+ const toArchive = log.entries.filter(
7617
+ (e) => e.itemIds.every((id) => completedIds.has(id))
7618
+ );
7619
+ const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
7620
+ let archContent = "";
7621
+ if (await fileExists(archFile)) {
7622
+ archContent = await readFile13(archFile, "utf-8");
7623
+ archContent = archContent.trimEnd() + "\n\n";
7624
+ } else {
7625
+ archContent = `---
7626
+ workspace: ${slug}
7627
+ ---
7628
+
7629
+ # Archive
7630
+
7631
+ `;
7632
+ }
7633
+ const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
7634
+ for (const item of completedItems) {
7635
+ archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
7636
+ `;
7637
+ }
7638
+ archContent += "\n";
7639
+ for (const entry of toArchive) {
7640
+ archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
7641
+ `;
7642
+ if (entry.items) archContent += `**Items:** ${entry.items}
7643
+ `;
7644
+ if (entry.session) archContent += `**Session:** ${entry.session}
7645
+ `;
7646
+ if (entry.branch) archContent += `**Branch:** ${entry.branch}
7647
+ `;
7648
+ if (entry.summary) archContent += `**Summary:** ${entry.summary}
7649
+ `;
7650
+ if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
7651
+ `;
7652
+ archContent += "\n";
7653
+ }
7654
+ await writeFileForce(archFile, archContent);
7655
+ checklist.workspace = slug;
7656
+ checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
7657
+ await writeChecklist(todosDir2, checklist);
7658
+ broadcastUpdate(slug);
7659
+ res.json({ archived: completedIds.size, logEntries: toArchive.length });
7660
+ } catch (error) {
7661
+ if (error.code === "PROJECT_GONE") {
7662
+ notFound(res, getProjectIdParam(params(req).projectId));
7663
+ return;
7664
+ }
7665
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to archive" });
7666
+ }
7667
+ });
7668
+ router.get("/log/:id", async (req, res) => {
7669
+ try {
7670
+ const slug = getProjectIdParam(params(req).projectId);
7671
+ if (!await projectExists(projectsDir, slug)) {
7672
+ notFound(res, slug);
7673
+ return;
7674
+ }
7675
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7676
+ const log = await readLog(todosDir2, slug);
7677
+ const entries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
7678
+ res.json({ workspace: log.workspace, entries });
7679
+ } catch (error) {
7680
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
7681
+ }
7682
+ });
7683
+ router.get("/:id", async (req, res) => {
7684
+ try {
7685
+ const slug = getProjectIdParam(params(req).projectId);
7686
+ if (!await projectExists(projectsDir, slug)) {
7687
+ notFound(res, slug);
7688
+ return;
7689
+ }
7690
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7691
+ const checklist = await readChecklist(todosDir2, slug);
7692
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
7693
+ if (!item) {
7694
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
7695
+ return;
7696
+ }
7697
+ const log = await readLog(todosDir2, slug);
7698
+ const logEntries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
7699
+ res.json({ ...item, log: logEntries });
7700
+ } catch (error) {
7701
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
7702
+ }
7703
+ });
7704
+ router.patch("/:id", async (req, res) => {
7705
+ try {
7706
+ const slug = getProjectIdParam(params(req).projectId);
7707
+ if (!await projectExists(projectsDir, slug)) {
7708
+ notFound(res, slug);
7709
+ return;
7710
+ }
7711
+ const result = await projLock(slug, async () => {
7712
+ if (!await projectExists(projectsDir, slug)) return "gone";
7713
+ await ensureProjectTodosDir(projectsDir, slug);
7714
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7715
+ const checklist = await readChecklist(todosDir2, slug);
7716
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
7717
+ if (!item) return null;
7718
+ if (req.body.description !== void 0) item.description = req.body.description;
7719
+ if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
7720
+ checklist.workspace = slug;
7721
+ await writeChecklist(todosDir2, checklist);
7722
+ return { ...item };
7723
+ });
7724
+ if (result === "gone") {
7725
+ notFound(res, slug);
7726
+ return;
7727
+ }
7728
+ if (!result) {
7729
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
7730
+ return;
7731
+ }
7732
+ broadcastUpdate(slug);
7733
+ res.json(result);
7734
+ } catch (error) {
7735
+ if (error.code === "PROJECT_GONE") {
7736
+ notFound(res, getProjectIdParam(params(req).projectId));
7737
+ return;
7738
+ }
7739
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update todo" });
7740
+ }
7741
+ });
7742
+ router.delete("/:id", async (req, res) => {
7743
+ try {
7744
+ const slug = getProjectIdParam(params(req).projectId);
7745
+ if (!await projectExists(projectsDir, slug)) {
7746
+ notFound(res, slug);
7747
+ return;
7748
+ }
7749
+ const deleted = await projLock(slug, async () => {
7750
+ if (!await projectExists(projectsDir, slug)) return "gone";
7751
+ await ensureProjectTodosDir(projectsDir, slug);
7752
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7753
+ const checklist = await readChecklist(todosDir2, slug);
7754
+ const idx = checklist.items.findIndex((i) => i.id === (params(req).id ?? ""));
7755
+ if (idx === -1) return false;
7756
+ checklist.items.splice(idx, 1);
7757
+ checklist.workspace = slug;
7758
+ await writeChecklist(todosDir2, checklist);
7759
+ return true;
7760
+ });
7761
+ if (deleted === "gone") {
7762
+ notFound(res, slug);
7763
+ return;
7764
+ }
7765
+ if (!deleted) {
7766
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
7767
+ return;
7768
+ }
7769
+ broadcastUpdate(slug);
7770
+ res.json({ deleted: params(req).id ?? "" });
7771
+ } catch (error) {
7772
+ if (error.code === "PROJECT_GONE") {
7773
+ notFound(res, getProjectIdParam(params(req).projectId));
7774
+ return;
7775
+ }
7776
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete todo" });
7777
+ }
7778
+ });
7779
+ router.post("/:id/start", async (req, res) => {
7780
+ try {
7781
+ const slug = getProjectIdParam(params(req).projectId);
7782
+ if (!await projectExists(projectsDir, slug)) {
7783
+ notFound(res, slug);
7784
+ return;
7785
+ }
7786
+ const result = await projLock(slug, async () => {
7787
+ if (!await projectExists(projectsDir, slug)) return { error: "gone" };
7788
+ await ensureProjectTodosDir(projectsDir, slug);
7789
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7790
+ const checklist = await readChecklist(todosDir2, slug);
7791
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
7792
+ if (!item) return { error: "not_found" };
7793
+ if (item.status === "in_progress") return { error: "conflict", session: item.session };
7794
+ item.status = "in_progress";
7795
+ item.session = req.body.session || null;
7796
+ checklist.workspace = slug;
7797
+ await writeChecklist(todosDir2, checklist);
7798
+ return { item: { ...item } };
7799
+ });
7800
+ if ("error" in result) {
7801
+ if (result.error === "gone") {
7802
+ notFound(res, slug);
7803
+ return;
7804
+ }
7805
+ if (result.error === "not_found") {
7806
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
7807
+ return;
7808
+ }
7809
+ res.status(409).json({ error: `Todo is already in progress (session: ${result.session})` });
7810
+ return;
7811
+ }
7812
+ broadcastUpdate(slug);
7813
+ res.json(result.item);
7814
+ } catch (error) {
7815
+ if (error.code === "PROJECT_GONE") {
7816
+ notFound(res, getProjectIdParam(params(req).projectId));
7817
+ return;
7818
+ }
7819
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to start todo" });
7820
+ }
7821
+ });
7822
+ router.post("/:id/complete", async (req, res) => {
7823
+ try {
7824
+ const slug = getProjectIdParam(params(req).projectId);
7825
+ if (!await projectExists(projectsDir, slug)) {
7826
+ notFound(res, slug);
7827
+ return;
7828
+ }
7829
+ const result = await projLock(slug, async () => {
7830
+ if (!await projectExists(projectsDir, slug)) return "gone";
7831
+ await ensureProjectTodosDir(projectsDir, slug);
7832
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7833
+ const checklist = await readChecklist(todosDir2, slug);
7834
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
7835
+ if (!item) return null;
7836
+ item.status = "completed";
7837
+ item.session = null;
7838
+ checklist.workspace = slug;
7839
+ await writeChecklist(todosDir2, checklist);
7840
+ const entry = {
7841
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7842
+ itemIds: [item.id],
7843
+ items: item.description,
7844
+ session: req.body.session || null,
7845
+ branch: req.body.branch || null,
7846
+ summary: req.body.summary || "Completed.",
7847
+ blockers: null,
7848
+ status: null
7849
+ };
7850
+ await appendLogEntry2(todosDir2, slug, entry);
7851
+ return { ...item };
7852
+ });
7853
+ if (result === "gone") {
7854
+ notFound(res, slug);
7855
+ return;
7856
+ }
7857
+ if (!result) {
7858
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
7859
+ return;
7860
+ }
7861
+ broadcastUpdate(slug);
7862
+ res.json(result);
7863
+ } catch (error) {
7864
+ if (error.code === "PROJECT_GONE") {
7865
+ notFound(res, getProjectIdParam(params(req).projectId));
7866
+ return;
7867
+ }
7868
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to complete todo" });
7869
+ }
7870
+ });
7871
+ router.post("/:id/block", async (req, res) => {
7872
+ try {
7873
+ const slug = getProjectIdParam(params(req).projectId);
7874
+ const reason = req.body.reason || null;
7875
+ if (!await projectExists(projectsDir, slug)) {
7876
+ notFound(res, slug);
7877
+ return;
7878
+ }
7879
+ const result = await projLock(slug, async () => {
7880
+ if (!await projectExists(projectsDir, slug)) return "gone";
7881
+ await ensureProjectTodosDir(projectsDir, slug);
7882
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7883
+ const checklist = await readChecklist(todosDir2, slug);
7884
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
7885
+ if (!item) return null;
7886
+ item.status = "blocked";
7887
+ item.session = null;
7888
+ checklist.workspace = slug;
7889
+ await writeChecklist(todosDir2, checklist);
7890
+ const entry = {
7891
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7892
+ itemIds: [item.id],
7893
+ items: item.description,
7894
+ session: req.body.session || null,
7895
+ branch: null,
7896
+ summary: reason || "Blocked.",
7897
+ blockers: reason,
7898
+ status: "blocked"
7899
+ };
7900
+ await appendLogEntry2(todosDir2, slug, entry);
7901
+ return { ...item };
7902
+ });
7903
+ if (result === "gone") {
7904
+ notFound(res, slug);
7905
+ return;
7906
+ }
7907
+ if (!result) {
7908
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
7909
+ return;
7910
+ }
7911
+ broadcastUpdate(slug);
7912
+ res.json(result);
7913
+ } catch (error) {
7914
+ if (error.code === "PROJECT_GONE") {
7915
+ notFound(res, getProjectIdParam(params(req).projectId));
7916
+ return;
7917
+ }
7918
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to block todo" });
7919
+ }
7920
+ });
7921
+ router.post("/:id/reopen", async (req, res) => {
7922
+ try {
7923
+ const slug = getProjectIdParam(params(req).projectId);
7924
+ if (!await projectExists(projectsDir, slug)) {
7925
+ notFound(res, slug);
7926
+ return;
7927
+ }
7928
+ const result = await projLock(slug, async () => {
7929
+ if (!await projectExists(projectsDir, slug)) return "gone";
7930
+ await ensureProjectTodosDir(projectsDir, slug);
7931
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7932
+ const checklist = await readChecklist(todosDir2, slug);
7933
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
7934
+ if (!item) return null;
7935
+ item.status = "open";
7936
+ item.session = null;
7937
+ checklist.workspace = slug;
7938
+ await writeChecklist(todosDir2, checklist);
7939
+ return { ...item };
7940
+ });
7941
+ if (result === "gone") {
7942
+ notFound(res, slug);
7943
+ return;
7944
+ }
7945
+ if (!result) {
7946
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
7947
+ return;
7948
+ }
7949
+ broadcastUpdate(slug);
7950
+ res.json(result);
7951
+ } catch (error) {
7952
+ if (error.code === "PROJECT_GONE") {
7953
+ notFound(res, getProjectIdParam(params(req).projectId));
7954
+ return;
7955
+ }
7956
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reopen todo" });
7957
+ }
7958
+ });
7959
+ router.post("/:id/unblock", async (req, res) => {
7960
+ try {
7961
+ const slug = getProjectIdParam(params(req).projectId);
7962
+ if (!await projectExists(projectsDir, slug)) {
7963
+ notFound(res, slug);
7964
+ return;
7965
+ }
7966
+ const result = await projLock(slug, async () => {
7967
+ if (!await projectExists(projectsDir, slug)) return "gone";
7968
+ await ensureProjectTodosDir(projectsDir, slug);
7969
+ const todosDir2 = projectTodosDir(projectsDir, slug);
7970
+ const checklist = await readChecklist(todosDir2, slug);
7971
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
7972
+ if (!item) return null;
7973
+ item.status = "open";
7974
+ item.session = null;
7975
+ checklist.workspace = slug;
7976
+ await writeChecklist(todosDir2, checklist);
7977
+ return { ...item };
7978
+ });
7979
+ if (result === "gone") {
7980
+ notFound(res, slug);
7981
+ return;
7982
+ }
7983
+ if (!result) {
7984
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
7985
+ return;
7986
+ }
7987
+ broadcastUpdate(slug);
7988
+ res.json(result);
7989
+ } catch (error) {
7990
+ if (error.code === "PROJECT_GONE") {
7991
+ notFound(res, getProjectIdParam(params(req).projectId));
7992
+ return;
7993
+ }
7994
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to unblock todo" });
7995
+ }
7996
+ });
7997
+ return router;
7998
+ }
7999
+
8000
+ // src/dashboard/api-backup.ts
8001
+ init_config2();
8002
+ import { Router as Router7 } from "express";
8003
+
8004
+ // src/utils/github-backup.ts
8005
+ init_paths();
8006
+ init_fs();
8007
+ init_config2();
7091
8008
  import { execFile as execFile2 } from "child_process";
7092
8009
  import { promisify as promisify2 } from "util";
7093
- import { cp, mkdtemp, rm as rm2, readFile as readFile13, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
7094
- import { resolve as resolve16, join as join2 } from "path";
8010
+ import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
8011
+ import { resolve as resolve17, join as join2 } from "path";
7095
8012
  import { tmpdir } from "os";
7096
8013
  var exec2 = promisify2(execFile2);
7097
8014
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -7131,7 +8048,7 @@ async function resolveCategoryPath(category) {
7131
8048
  case "servers":
7132
8049
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
7133
8050
  case "config":
7134
- return { sourcePath: resolve16(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
8051
+ return { sourcePath: resolve17(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
7135
8052
  }
7136
8053
  }
7137
8054
  async function checkGitInstalled() {
@@ -7142,7 +8059,7 @@ async function checkGitInstalled() {
7142
8059
  }
7143
8060
  }
7144
8061
  async function acquireLock() {
7145
- const lockPath = resolve16(syntaurRoot(), LOCK_FILE_NAME);
8062
+ const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
7146
8063
  await ensureDir(syntaurRoot());
7147
8064
  try {
7148
8065
  const handle = await open(lockPath, "wx");
@@ -7151,7 +8068,7 @@ async function acquireLock() {
7151
8068
  return lockPath;
7152
8069
  } catch (err) {
7153
8070
  if (err.code === "EEXIST") {
7154
- const pid = await readFile13(lockPath, "utf-8").catch(() => "");
8071
+ const pid = await readFile14(lockPath, "utf-8").catch(() => "");
7155
8072
  throw new Error(
7156
8073
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
7157
8074
  );
@@ -7189,7 +8106,7 @@ async function copyRecursive(src, dest) {
7189
8106
  await ensureDir(dest);
7190
8107
  await cp(src, dest, { recursive: true, force: true });
7191
8108
  } else {
7192
- await ensureDir(resolve16(dest, ".."));
8109
+ await ensureDir(resolve17(dest, ".."));
7193
8110
  await cp(src, dest, { force: true });
7194
8111
  }
7195
8112
  }
@@ -7198,7 +8115,7 @@ function resolveCategoriesStrict(csv) {
7198
8115
  return parseCategoriesStrict(parts);
7199
8116
  }
7200
8117
  async function readSanitizedConfig(configPath) {
7201
- const content = await readFile13(configPath, "utf-8");
8118
+ const content = await readFile14(configPath, "utf-8");
7202
8119
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
7203
8120
  }
7204
8121
  async function backupToGithub(overrides) {
@@ -7237,7 +8154,7 @@ async function backupToGithub(overrides) {
7237
8154
  }
7238
8155
  if (category === "config") {
7239
8156
  const sanitized = await readSanitizedConfig(sourcePath);
7240
- await ensureDir(resolve16(destPath, ".."));
8157
+ await ensureDir(resolve17(destPath, ".."));
7241
8158
  await writeFile4(destPath, sanitized, "utf-8");
7242
8159
  } else {
7243
8160
  await copyRecursive(sourcePath, destPath);
@@ -7291,7 +8208,7 @@ async function backupToGithub(overrides) {
7291
8208
  }
7292
8209
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
7293
8210
  if (isFile) {
7294
- await ensureDir(resolve16(localPath, ".."));
8211
+ await ensureDir(resolve17(localPath, ".."));
7295
8212
  await cp(repoSrcPath, localPath, { force: true });
7296
8213
  return;
7297
8214
  }
@@ -7392,7 +8309,7 @@ async function restoreFromGithub(overrides) {
7392
8309
  }
7393
8310
  async function getBackupStatus() {
7394
8311
  const config = await readConfig();
7395
- const lockPath = resolve16(syntaurRoot(), LOCK_FILE_NAME);
8312
+ const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
7396
8313
  const locked = await fileExists(lockPath);
7397
8314
  return {
7398
8315
  repo: config.backup?.repo ?? null,
@@ -7405,7 +8322,7 @@ async function getBackupStatus() {
7405
8322
 
7406
8323
  // src/dashboard/api-backup.ts
7407
8324
  function createBackupRouter() {
7408
- const router = Router6();
8325
+ const router = Router7();
7409
8326
  router.get("/", async (_req, res) => {
7410
8327
  try {
7411
8328
  const status = await getBackupStatus();
@@ -7729,7 +8646,7 @@ function createDashboardServer(options) {
7729
8646
  (async () => {
7730
8647
  try {
7731
8648
  const configResult = await migrateLegacyConfig(
7732
- resolve17(syntaurRoot(), "config.md")
8649
+ resolve18(syntaurRoot(), "config.md")
7733
8650
  );
7734
8651
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
7735
8652
  const summary = summarizeMigration(projectResult, configResult);
@@ -7836,7 +8753,7 @@ function createDashboardServer(options) {
7836
8753
  });
7837
8754
  app.get("/api/workspaces", async (_req, res) => {
7838
8755
  try {
7839
- const result = await listWorkspaces(projectsDir);
8756
+ const result = await listWorkspaces(projectsDir, assignmentsDir);
7840
8757
  res.json(result);
7841
8758
  } catch (error) {
7842
8759
  console.error("Error listing workspaces:", error);
@@ -7953,18 +8870,28 @@ function createDashboardServer(options) {
7953
8870
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir));
7954
8871
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir2));
7955
8872
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
8873
+ app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir, broadcast));
7956
8874
  app.use("/api/backup", createBackupRouter());
7957
8875
  if (serveStaticUi && dashboardDistPath) {
7958
- app.use(express.static(dashboardDistPath));
7959
- app.get("{*path}", async (_req, res) => {
7960
- const indexPath = resolve17(dashboardDistPath, "index.html");
7961
- if (await fileExists(indexPath)) {
7962
- res.sendFile(indexPath);
7963
- } else {
8876
+ app.use("/assets", express.static(resolve18(dashboardDistPath, "assets")));
8877
+ app.get("{*path}", async (req, res) => {
8878
+ if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
8879
+ res.status(404).json({ error: "Not Found" });
8880
+ return;
8881
+ }
8882
+ const indexPath = resolve18(dashboardDistPath, "index.html");
8883
+ if (!await fileExists(indexPath)) {
7964
8884
  res.status(503).send(
7965
8885
  'Dashboard not built. Run "npm run build:dashboard" first.'
7966
8886
  );
8887
+ return;
7967
8888
  }
8889
+ res.sendFile(indexPath, (err) => {
8890
+ if (err) {
8891
+ console.error("Error sending dashboard index.html:", err);
8892
+ if (!res.headersSent) res.status(500).send("Dashboard load error");
8893
+ }
8894
+ });
7968
8895
  });
7969
8896
  }
7970
8897
  let watcherHandle = null;
@@ -7990,7 +8917,7 @@ function createDashboardServer(options) {
7990
8917
  }
7991
8918
  });
7992
8919
  server.listen(port, () => {
7993
- const portFile = resolve17(syntaurRoot(), "dashboard-port");
8920
+ const portFile = resolve18(syntaurRoot(), "dashboard-port");
7994
8921
  writeFile5(portFile, String(port), "utf-8").catch(() => {
7995
8922
  });
7996
8923
  resolvePromise();
@@ -8007,7 +8934,7 @@ function createDashboardServer(options) {
8007
8934
  client.terminate();
8008
8935
  }
8009
8936
  clients.clear();
8010
- const portFile = resolve17(syntaurRoot(), "dashboard-port");
8937
+ const portFile = resolve18(syntaurRoot(), "dashboard-port");
8011
8938
  await unlink4(portFile).catch(() => {
8012
8939
  });
8013
8940
  server.closeAllConnections?.();