syntaur 0.4.4 → 0.4.5

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 (71) hide show
  1. package/dashboard/dist/assets/{_basePickBy-CWivToyi.js → _basePickBy-ZY1FrcKw.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-C-qj6E4l.js → _baseUniq-C7xZB6ea.js} +1 -1
  3. package/dashboard/dist/assets/{arc-Dn5BIqMa.js → arc-CPtDVk1A.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-D5D0K7rY.js → architectureDiagram-2XIMDMQ5-Dr5rnxwf.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DVmYPMbu.js → blockDiagram-WCTKOSBZ-SsDboTb2.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEasxbnl.js → c4Diagram-IC4MRINW-CZqjXmV0.js} +1 -1
  7. package/dashboard/dist/assets/channel-ejDeCb7i.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-LDIrtI5E.js → chunk-4BX2VUAB-BYskd63Z.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-CaEBUJYu.js → chunk-55IACEB6-CWLImr1E.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-B-GjCpdr.js → chunk-FMBD7UC4-fQXSIXhy.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-BLVVcezm.js → chunk-JSJVCQXG-DJXtEexG.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-DqCNEw4h.js → chunk-KX2RTZJC-C0ivaZ1M.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-BCPbFf5I.js → chunk-NQ4KR5QH-DlwLjqC5.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-Ci0C85q_.js → chunk-QZHKN3VN-DBEYrRqx.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-VVhAMMYU.js → chunk-WL4C6EOR-BQyYCp1z.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BW_esmsF.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BW_esmsF.js +1 -0
  18. package/dashboard/dist/assets/clone-y13L00zF.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CO9uwgYO.js → cose-bilkent-S5V4N54A-CAYzelqd.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-bwLLXcL4.js → dagre-KLK3FWXG-xpTJb8E7.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-RuS5R6V1.js → diagram-E7M64L7V-DRDjskHd.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-BQDJAHQd.js → diagram-IFDJBPK2-B1r1ZXm3.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-yLEsgzE5.js → diagram-P4PSJMXO-BeE6-ZUH.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-na6dUhY0.js → erDiagram-INFDFZHY-BG6KKBm1.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BIcrzwJR.js → flowDiagram-PKNHOUZH-CSFv3RpZ.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DHWRJn-D.js → ganttDiagram-A5KZAMGK-Off4zK-k.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-LGxDjL71.js → gitGraphDiagram-K3NZZRJ6-Msv_4mYB.js} +1 -1
  28. package/dashboard/dist/assets/{graph-BUqNu277.js → graph-rPPnNEMq.js} +1 -1
  29. package/dashboard/dist/assets/index-Bu6ma6my.css +1 -0
  30. package/dashboard/dist/assets/{index-D-fepllQ.js → index-D_uE8gHg.js} +87 -87
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DidoA2hb.js → infoDiagram-LFFYTUFH-CeBirxFd.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CdlZkbhV.js → ishikawaDiagram-PHBUUO56-BZ_w8PcD.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-luhcz_gn.js → journeyDiagram-4ABVD52K-3FqMPEfs.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Coidw9XE.js → kanban-definition-K7BYSVSG-BlCmFkEx.js} +1 -1
  35. package/dashboard/dist/assets/{layout-_aBAAleE.js → layout-C50nYMhx.js} +1 -1
  36. package/dashboard/dist/assets/{linear-D8mFnDSx.js → linear-D2Cjmh1X.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-BpP2keU-.js → mermaid.core-BXFuNYa4.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-LjvrTe2z.js → mindmap-definition-YRQLILUH-CowIfM07.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CkA2iU6e.js → pieDiagram-SKSYHLDU-DG5IQcKl.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BRmhKHQG.js → quadrantDiagram-337W2JSQ-DKD46CSX.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BYCQ4uFX.js → requirementDiagram-Z7DCOOCP-Dd-qX6Ul.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-C8SVk50M.js → sankeyDiagram-WA2Y5GQK-CDAylULt.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CbA_2lnP.js → sequenceDiagram-2WXFIKYE-D-5Jq5jy.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D6ZtjAHE.js → stateDiagram-RAJIS63D-BmBOa8i0.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Dsk6o9iT.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-B2Uf-emK.js → timeline-definition-YZTLITO2-C9Sdg7du.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-CYnFKsuJ.js → treemap-KZPCXAKY-D-PDxUdK.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cnj0qiDO.js → vennDiagram-LZ73GAT5-CBO5f6MT.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BJy2mbL9.js → xychartDiagram-JWTSCODW-4xZrquqP.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +601 -285
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +567 -147
  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-DnHyQJJH.css +0 -1
  69. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DeYN4wV6.js +0 -1
  70. package/examples/playbooks/plan-versioning.md +0 -36
  71. package/examples/playbooks/read-before-plan.md +0 -30
@@ -622,6 +622,27 @@ var init_fs_migration = __esm({
622
622
  // src/utils/config.ts
623
623
  import { readFile as readFile3 } from "fs/promises";
624
624
  import { resolve as resolve4, isAbsolute } from "path";
625
+ function cloneDefaultConfig() {
626
+ return {
627
+ ...DEFAULT_CONFIG,
628
+ onboarding: { ...DEFAULT_CONFIG.onboarding },
629
+ agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
630
+ integrations: { ...DEFAULT_CONFIG.integrations },
631
+ backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
632
+ statuses: DEFAULT_CONFIG.statuses ? {
633
+ statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
634
+ order: [...DEFAULT_CONFIG.statuses.order],
635
+ transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
636
+ } : null,
637
+ types: DEFAULT_CONFIG.types ? {
638
+ definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
639
+ default: DEFAULT_CONFIG.types.default
640
+ } : null,
641
+ playbooks: {
642
+ disabled: [...DEFAULT_CONFIG.playbooks.disabled]
643
+ }
644
+ };
645
+ }
625
646
  function parseFrontmatter(content) {
626
647
  const match = content.match(/^---\n([\s\S]*?)\n---/);
627
648
  if (!match) return {};
@@ -774,6 +795,82 @@ function serializeBackupConfig(backup) {
774
795
  lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
775
796
  return lines.join("\n");
776
797
  }
798
+ function serializePlaybooksConfig(playbooks) {
799
+ if (!playbooks.disabled || playbooks.disabled.length === 0) {
800
+ return null;
801
+ }
802
+ const lines = ["playbooks:", " disabled:"];
803
+ for (const slug of playbooks.disabled) {
804
+ lines.push(` - ${slug}`);
805
+ }
806
+ return lines.join("\n");
807
+ }
808
+ function parsePlaybooksConfig(fmBlock) {
809
+ const blockStart = fmBlock.match(/^playbooks:\s*$/m);
810
+ if (!blockStart) {
811
+ return { disabled: [] };
812
+ }
813
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
814
+ const remaining = fmBlock.slice(startIdx).split("\n");
815
+ const disabled = [];
816
+ let currentSection = null;
817
+ for (const line of remaining) {
818
+ const trimmed = line.trimStart();
819
+ const indent = line.length - trimmed.length;
820
+ if (indent === 0 && trimmed.length > 0) break;
821
+ if (trimmed === "") continue;
822
+ if (indent === 2 && trimmed.startsWith("disabled:")) {
823
+ currentSection = "disabled";
824
+ const afterColon = trimmed.slice("disabled:".length).trim();
825
+ if (afterColon === "[]" || afterColon === "") {
826
+ continue;
827
+ }
828
+ continue;
829
+ }
830
+ if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
831
+ const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
832
+ if (raw.length === 0) continue;
833
+ if (/\s/.test(raw)) {
834
+ console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
835
+ continue;
836
+ }
837
+ disabled.push(raw);
838
+ continue;
839
+ }
840
+ }
841
+ return { disabled };
842
+ }
843
+ async function updatePlaybooksConfig(playbooks) {
844
+ const configPath = resolve4(syntaurRoot(), "config.md");
845
+ const current = (await readConfig()).playbooks;
846
+ const nextPlaybooks = {
847
+ disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
848
+ };
849
+ const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
850
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
851
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
852
+ if (!fmMatch) {
853
+ const bodyBlock = playbooksBlock ? `${playbooksBlock}
854
+ ` : "";
855
+ const content = `---
856
+ version: "2.0"
857
+ defaultProjectDir: ${defaultProjectDir()}
858
+ ${bodyBlock}---
859
+ ${existing}`;
860
+ await writeFileForce(configPath, content);
861
+ return;
862
+ }
863
+ const fmBlock = fmMatch[2];
864
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
865
+ const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
866
+ const newFm = playbooksBlock ? `${cleanedFm}
867
+ ${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
868
+ const normalizedFm = newFm.replace(/\n+$/, "");
869
+ const newContent = `---
870
+ ${normalizedFm}
871
+ ---${afterFrontmatter}`;
872
+ await writeFileForce(configPath, newContent);
873
+ }
777
874
  function stripTopLevelBlock(fmBlock, key) {
778
875
  const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
779
876
  if (!blockStart) {
@@ -915,7 +1012,7 @@ ${normalizedFm}
915
1012
  async function readConfig() {
916
1013
  const configPath = resolve4(syntaurRoot(), "config.md");
917
1014
  if (!await fileExists(configPath)) {
918
- return { ...DEFAULT_CONFIG };
1015
+ return cloneDefaultConfig();
919
1016
  }
920
1017
  if (!migratedConfigPaths.has(configPath)) {
921
1018
  migratedConfigPaths.add(configPath);
@@ -925,7 +1022,7 @@ async function readConfig() {
925
1022
  const fm = parseFrontmatter(content);
926
1023
  if (Object.keys(fm).length === 0) {
927
1024
  console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
928
- return { ...DEFAULT_CONFIG };
1025
+ return cloneDefaultConfig();
929
1026
  }
930
1027
  let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
931
1028
  if (!isAbsolute(projectDir)) {
@@ -934,6 +1031,7 @@ async function readConfig() {
934
1031
  );
935
1032
  projectDir = DEFAULT_CONFIG.defaultProjectDir;
936
1033
  }
1034
+ const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
937
1035
  return {
938
1036
  version: fm["version"] || DEFAULT_CONFIG.version,
939
1037
  defaultProjectDir: projectDir,
@@ -965,7 +1063,8 @@ async function readConfig() {
965
1063
  lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
966
1064
  } : null,
967
1065
  statuses: parseStatusConfig(content),
968
- types: null
1066
+ types: null,
1067
+ playbooks: parsePlaybooksConfig(fmBlock)
969
1068
  };
970
1069
  }
971
1070
  var DEFAULT_CONFIG, migratedConfigPaths;
@@ -993,7 +1092,10 @@ var init_config2 = __esm({
993
1092
  },
994
1093
  backup: null,
995
1094
  statuses: null,
996
- types: null
1095
+ types: null,
1096
+ playbooks: {
1097
+ disabled: []
1098
+ }
997
1099
  };
998
1100
  migratedConfigPaths = /* @__PURE__ */ new Set();
999
1101
  }
@@ -1128,6 +1230,7 @@ function parseAssignmentFull(fileContent) {
1128
1230
  slug: getField(fm, "slug") ?? "",
1129
1231
  title: getField(fm, "title") ?? "",
1130
1232
  project: getField(fm, "project"),
1233
+ workspaceGroup: getField(fm, "workspaceGroup"),
1131
1234
  type: getField(fm, "type"),
1132
1235
  status: getField(fm, "status") ?? "pending",
1133
1236
  priority: getField(fm, "priority") ?? "medium",
@@ -1284,47 +1387,160 @@ var init_parser = __esm({
1284
1387
  }
1285
1388
  });
1286
1389
 
1287
- // src/utils/assignment-resolver.ts
1390
+ // src/utils/playbooks.ts
1288
1391
  import { resolve as resolve5 } from "path";
1289
1392
  import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
1393
+ function isVisiblePlaybookFile(name, isFile) {
1394
+ return isFile && name.endsWith(".md") && !name.startsWith("_") && name !== "manifest.md";
1395
+ }
1396
+ async function resolvePlaybookSlug(playbooksDir2, slug) {
1397
+ if (!await fileExists(playbooksDir2)) return null;
1398
+ const entries = await readdir2(playbooksDir2, { withFileTypes: true });
1399
+ let filenameStemFallback = null;
1400
+ for (const entry of entries) {
1401
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
1402
+ const filePath = resolve5(playbooksDir2, entry.name);
1403
+ const raw = await readFile4(filePath, "utf-8");
1404
+ const parsed = parsePlaybook(raw);
1405
+ const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
1406
+ if (canonical === slug) {
1407
+ return { filename: entry.name, slug: canonical, parsed };
1408
+ }
1409
+ if (!parsed.slug && entry.name.replace(/\.md$/, "") === slug) {
1410
+ filenameStemFallback = { filename: entry.name, slug: canonical, parsed };
1411
+ }
1412
+ }
1413
+ return filenameStemFallback;
1414
+ }
1415
+ async function setPlaybookEnabled(playbooksDir2, slug, enabled) {
1416
+ const resolved = await resolvePlaybookSlug(playbooksDir2, slug);
1417
+ if (!resolved) {
1418
+ throw new Error(`Playbook "${slug}" not found in ${playbooksDir2}`);
1419
+ }
1420
+ const config = await readConfig();
1421
+ const disabledSet = new Set(config.playbooks.disabled);
1422
+ const wasDisabled = disabledSet.has(resolved.slug);
1423
+ const shouldBeDisabled = !enabled;
1424
+ if (wasDisabled === shouldBeDisabled) {
1425
+ return { slug: resolved.slug, enabled, changed: false };
1426
+ }
1427
+ if (shouldBeDisabled) {
1428
+ disabledSet.add(resolved.slug);
1429
+ } else {
1430
+ disabledSet.delete(resolved.slug);
1431
+ }
1432
+ await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });
1433
+ await rebuildPlaybookManifest(playbooksDir2);
1434
+ return { slug: resolved.slug, enabled, changed: true };
1435
+ }
1436
+ async function removeFromDisabledList(slug) {
1437
+ const config = await readConfig();
1438
+ if (!config.playbooks.disabled.includes(slug)) return;
1439
+ await updatePlaybooksConfig({
1440
+ disabled: config.playbooks.disabled.filter((s) => s !== slug)
1441
+ });
1442
+ }
1443
+ async function rebuildPlaybookManifest(playbooksDir2) {
1444
+ if (!await fileExists(playbooksDir2)) return;
1445
+ const config = await readConfig();
1446
+ const disabledSet = new Set(config.playbooks.disabled);
1447
+ const entries = await readdir2(playbooksDir2, { withFileTypes: true });
1448
+ const rows = [];
1449
+ for (const entry of entries) {
1450
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
1451
+ const raw = await readFile4(resolve5(playbooksDir2, entry.name), "utf-8");
1452
+ const parsed = parsePlaybook(raw);
1453
+ const slug = parsed.slug || entry.name.replace(/\.md$/, "");
1454
+ if (disabledSet.has(slug)) continue;
1455
+ rows.push({
1456
+ name: parsed.name || slug,
1457
+ slug,
1458
+ description: parsed.description,
1459
+ whenToUse: parsed.whenToUse
1460
+ });
1461
+ }
1462
+ rows.sort((a, b) => a.name.localeCompare(b.name));
1463
+ const timestamp = nowTimestamp();
1464
+ const lines = [
1465
+ "---",
1466
+ `generated: "${timestamp}"`,
1467
+ `total: ${rows.length}`,
1468
+ "---",
1469
+ "",
1470
+ "# Playbooks",
1471
+ "",
1472
+ "Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
1473
+ ""
1474
+ ];
1475
+ for (const row of rows) {
1476
+ lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
1477
+ if (row.whenToUse) {
1478
+ lines.push(` _When to use: ${row.whenToUse}_`);
1479
+ }
1480
+ }
1481
+ lines.push("");
1482
+ await writeFileForce(resolve5(playbooksDir2, "manifest.md"), lines.join("\n"));
1483
+ }
1484
+ var init_playbooks = __esm({
1485
+ "src/utils/playbooks.ts"() {
1486
+ "use strict";
1487
+ init_fs();
1488
+ init_parser();
1489
+ init_timestamp();
1490
+ init_config2();
1491
+ }
1492
+ });
1493
+
1494
+ // src/utils/assignment-resolver.ts
1495
+ import { resolve as resolve6 } from "path";
1496
+ import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
1290
1497
  async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
1291
1498
  let standaloneMatch = null;
1292
1499
  let projectMatch = null;
1293
- const standaloneDir = resolve5(assignmentsDir, id);
1294
- const standalonePath = resolve5(standaloneDir, "assignment.md");
1500
+ const standaloneDir = resolve6(assignmentsDir, id);
1501
+ const standalonePath = resolve6(standaloneDir, "assignment.md");
1295
1502
  if (await fileExists(standalonePath)) {
1503
+ let workspaceGroup = null;
1504
+ try {
1505
+ const content = await readFile5(standalonePath, "utf-8");
1506
+ const [fm] = extractFrontmatter2(content);
1507
+ workspaceGroup = getField(fm, "workspaceGroup");
1508
+ } catch {
1509
+ }
1296
1510
  standaloneMatch = {
1297
1511
  assignmentDir: standaloneDir,
1298
1512
  projectSlug: null,
1299
1513
  assignmentSlug: id,
1300
1514
  id,
1301
- standalone: true
1515
+ standalone: true,
1516
+ workspaceGroup
1302
1517
  };
1303
1518
  }
1304
1519
  if (await fileExists(projectsDir)) {
1305
1520
  try {
1306
- const projects = await readdir2(projectsDir, { withFileTypes: true });
1521
+ const projects = await readdir3(projectsDir, { withFileTypes: true });
1307
1522
  for (const p of projects) {
1308
1523
  if (!p.isDirectory()) continue;
1309
1524
  if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
1310
- const assignmentsPath = resolve5(projectsDir, p.name, "assignments");
1525
+ const assignmentsPath = resolve6(projectsDir, p.name, "assignments");
1311
1526
  if (!await fileExists(assignmentsPath)) continue;
1312
- const entries = await readdir2(assignmentsPath, { withFileTypes: true });
1527
+ const entries = await readdir3(assignmentsPath, { withFileTypes: true });
1313
1528
  for (const a of entries) {
1314
1529
  if (!a.isDirectory()) continue;
1315
- const aPath = resolve5(assignmentsPath, a.name, "assignment.md");
1530
+ const aPath = resolve6(assignmentsPath, a.name, "assignment.md");
1316
1531
  if (!await fileExists(aPath)) continue;
1317
1532
  try {
1318
- const content = await readFile4(aPath, "utf-8");
1533
+ const content = await readFile5(aPath, "utf-8");
1319
1534
  const [fm] = extractFrontmatter2(content);
1320
1535
  const fileId = getField(fm, "id");
1321
1536
  if (fileId === id) {
1322
1537
  projectMatch = {
1323
- assignmentDir: resolve5(assignmentsPath, a.name),
1538
+ assignmentDir: resolve6(assignmentsPath, a.name),
1324
1539
  projectSlug: p.name,
1325
1540
  assignmentSlug: a.name,
1326
1541
  id,
1327
- standalone: false
1542
+ standalone: false,
1543
+ workspaceGroup: null
1328
1544
  };
1329
1545
  break;
1330
1546
  }
@@ -1700,8 +1916,18 @@ var init_help = __esm({
1700
1916
  },
1701
1917
  {
1702
1918
  command: "syntaur list-playbooks",
1703
- description: "List all playbooks in the Syntaur home directory.",
1704
- example: "syntaur list-playbooks"
1919
+ description: "List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.",
1920
+ example: "syntaur list-playbooks --all"
1921
+ },
1922
+ {
1923
+ command: "syntaur enable-playbook",
1924
+ description: "Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.",
1925
+ example: "syntaur enable-playbook commit-discipline"
1926
+ },
1927
+ {
1928
+ command: "syntaur disable-playbook",
1929
+ description: "Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.",
1930
+ example: "syntaur disable-playbook commit-discipline"
1705
1931
  }
1706
1932
  ];
1707
1933
  WORKFLOW = [
@@ -1768,8 +1994,8 @@ var init_help = __esm({
1768
1994
  });
1769
1995
 
1770
1996
  // src/dashboard/servers.ts
1771
- import { readdir as readdir3, readFile as readFile5, unlink } from "fs/promises";
1772
- import { resolve as resolve6 } from "path";
1997
+ import { readdir as readdir4, readFile as readFile6, unlink } from "fs/promises";
1998
+ import { resolve as resolve7 } from "path";
1773
1999
  function sanitizeSessionName(name) {
1774
2000
  return name.replace(/[^a-zA-Z0-9_-]/g, "-");
1775
2001
  }
@@ -1817,18 +2043,18 @@ async function registerSession(dir, rawName) {
1817
2043
  lastRefreshed: now,
1818
2044
  overrides: {}
1819
2045
  });
1820
- await writeFileForce(resolve6(dir, `${name}.md`), content);
2046
+ await writeFileForce(resolve7(dir, `${name}.md`), content);
1821
2047
  return name;
1822
2048
  }
1823
2049
  async function listSessionFiles(dir) {
1824
2050
  if (!await fileExists(dir)) return [];
1825
- const entries = await readdir3(dir);
2051
+ const entries = await readdir4(dir);
1826
2052
  return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
1827
2053
  }
1828
2054
  async function readSessionFile(dir, name) {
1829
- const filePath = resolve6(dir, `${sanitizeSessionName(name)}.md`);
2055
+ const filePath = resolve7(dir, `${sanitizeSessionName(name)}.md`);
1830
2056
  if (!await fileExists(filePath)) return null;
1831
- const raw = await readFile5(filePath, "utf-8");
2057
+ const raw = await readFile6(filePath, "utf-8");
1832
2058
  const [frontmatter] = extractFrontmatter2(raw);
1833
2059
  if (!frontmatter) return null;
1834
2060
  const session = getField(frontmatter, "session") ?? name;
@@ -1868,7 +2094,7 @@ async function readSessionFile(dir, name) {
1868
2094
  };
1869
2095
  }
1870
2096
  async function removeSession(dir, name) {
1871
- const filePath = resolve6(dir, `${sanitizeSessionName(name)}.md`);
2097
+ const filePath = resolve7(dir, `${sanitizeSessionName(name)}.md`);
1872
2098
  if (await fileExists(filePath)) {
1873
2099
  await unlink(filePath);
1874
2100
  }
@@ -1877,7 +2103,7 @@ async function updateLastRefreshed(dir, name) {
1877
2103
  const data = await readSessionFile(dir, name);
1878
2104
  if (!data) return;
1879
2105
  const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
1880
- await writeFileForce(resolve6(dir, `${sanitizeSessionName(name)}.md`), content);
2106
+ await writeFileForce(resolve7(dir, `${sanitizeSessionName(name)}.md`), content);
1881
2107
  }
1882
2108
  async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
1883
2109
  const data = await readSessionFile(dir, sessionName);
@@ -1889,7 +2115,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
1889
2115
  delete data.overrides[key];
1890
2116
  }
1891
2117
  const content = buildSessionContent({ ...data });
1892
- await writeFileForce(resolve6(dir, `${sanitizeSessionName(sessionName)}.md`), content);
2118
+ await writeFileForce(resolve7(dir, `${sanitizeSessionName(sessionName)}.md`), content);
1893
2119
  }
1894
2120
  async function registerAutoSession(dir, rawName, opts) {
1895
2121
  const name = sanitizeSessionName(rawName);
@@ -1906,7 +2132,7 @@ async function registerAutoSession(dir, rawName, opts) {
1906
2132
  ports: opts.ports,
1907
2133
  cwd: opts.cwd
1908
2134
  });
1909
- await writeFileForce(resolve6(dir, `${name}.md`), content);
2135
+ await writeFileForce(resolve7(dir, `${name}.md`), content);
1910
2136
  return name;
1911
2137
  }
1912
2138
  var init_servers = __esm({
@@ -1938,8 +2164,8 @@ __export(scanner_exports, {
1938
2164
  });
1939
2165
  import { execFile } from "child_process";
1940
2166
  import { promisify } from "util";
1941
- import { resolve as resolve7 } from "path";
1942
- import { realpath, readdir as readdir4, readFile as readFile6 } from "fs/promises";
2167
+ import { resolve as resolve8 } from "path";
2168
+ import { realpath, readdir as readdir5, readFile as readFile7 } from "fs/promises";
1943
2169
  function clearScanCache() {
1944
2170
  cache = null;
1945
2171
  }
@@ -2034,8 +2260,8 @@ async function getGitInfo(cwd) {
2034
2260
  let isWorktree = false;
2035
2261
  if (commonDir && gitDir && commonDir !== gitDir) {
2036
2262
  try {
2037
- const resolvedCommon = await realpath(resolve7(cwd, commonDir));
2038
- const resolvedGit = await realpath(resolve7(cwd, gitDir));
2263
+ const resolvedCommon = await realpath(resolve8(cwd, commonDir));
2264
+ const resolvedGit = await realpath(resolve8(cwd, gitDir));
2039
2265
  isWorktree = resolvedCommon !== resolvedGit;
2040
2266
  } catch {
2041
2267
  isWorktree = false;
@@ -2048,17 +2274,17 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
2048
2274
  try {
2049
2275
  const projects = await listProjects(projectsDir);
2050
2276
  for (const project of projects) {
2051
- const projectAssignmentsDir = resolve7(projectsDir, project.slug, "assignments");
2277
+ const projectAssignmentsDir = resolve8(projectsDir, project.slug, "assignments");
2052
2278
  let slugs;
2053
2279
  try {
2054
- slugs = await readdir4(projectAssignmentsDir);
2280
+ slugs = await readdir5(projectAssignmentsDir);
2055
2281
  } catch {
2056
2282
  continue;
2057
2283
  }
2058
2284
  for (const aslug of slugs) {
2059
- const aFile = resolve7(projectAssignmentsDir, aslug, "assignment.md");
2285
+ const aFile = resolve8(projectAssignmentsDir, aslug, "assignment.md");
2060
2286
  try {
2061
- const raw = await readFile6(aFile, "utf-8");
2287
+ const raw = await readFile7(aFile, "utf-8");
2062
2288
  const [fm] = extractFrontmatter2(raw);
2063
2289
  if (!fm) continue;
2064
2290
  records.push({
@@ -2077,12 +2303,12 @@ async function loadWorkspaceRecords(projectsDir, assignmentsDir) {
2077
2303
  }
2078
2304
  if (assignmentsDir) {
2079
2305
  try {
2080
- const entries = await readdir4(assignmentsDir);
2306
+ const entries = await readdir5(assignmentsDir);
2081
2307
  for (const id of entries) {
2082
2308
  if (id.startsWith(".") || id.startsWith("_")) continue;
2083
- const aFile = resolve7(assignmentsDir, id, "assignment.md");
2309
+ const aFile = resolve8(assignmentsDir, id, "assignment.md");
2084
2310
  try {
2085
- const raw = await readFile6(aFile, "utf-8");
2311
+ const raw = await readFile7(aFile, "utf-8");
2086
2312
  const [fm] = extractFrontmatter2(raw);
2087
2313
  if (!fm) continue;
2088
2314
  records.push({
@@ -2330,20 +2556,20 @@ var init_scanner = __esm({
2330
2556
  });
2331
2557
 
2332
2558
  // 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";
2559
+ import { readdir as readdir6, readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
2560
+ import { resolve as resolve9, dirname as dirname2 } from "path";
2335
2561
  async function listStandaloneRecords(assignmentsDir) {
2336
2562
  if (!assignmentsDir) return [];
2337
2563
  if (!await fileExists(assignmentsDir)) return [];
2338
- const entries = await readdir5(assignmentsDir, { withFileTypes: true });
2564
+ const entries = await readdir6(assignmentsDir, { withFileTypes: true });
2339
2565
  const records = [];
2340
2566
  for (const entry of entries) {
2341
2567
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
2342
- const assignmentDir = resolve8(assignmentsDir, entry.name);
2343
- const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
2568
+ const assignmentDir = resolve9(assignmentsDir, entry.name);
2569
+ const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
2344
2570
  if (!await fileExists(assignmentMdPath)) continue;
2345
2571
  try {
2346
- const content = await readFile7(assignmentMdPath, "utf-8");
2572
+ const content = await readFile8(assignmentMdPath, "utf-8");
2347
2573
  const record = parseAssignmentFull(content);
2348
2574
  records.push({ assignmentDir, id: entry.name, record });
2349
2575
  } catch {
@@ -2412,9 +2638,9 @@ async function listProjects(projectsDir) {
2412
2638
  return projectRecords.map((record) => record.summary);
2413
2639
  }
2414
2640
  async function readWorkspaceRegistry(projectsDir) {
2415
- const registryPath = resolve8(dirname2(projectsDir), "workspaces.json");
2641
+ const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
2416
2642
  try {
2417
- const raw = await readFile7(registryPath, "utf-8");
2643
+ const raw = await readFile8(registryPath, "utf-8");
2418
2644
  const parsed = JSON.parse(raw);
2419
2645
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
2420
2646
  } catch {
@@ -2422,13 +2648,14 @@ async function readWorkspaceRegistry(projectsDir) {
2422
2648
  }
2423
2649
  }
2424
2650
  async function writeWorkspaceRegistry(projectsDir, workspaces) {
2425
- const registryPath = resolve8(dirname2(projectsDir), "workspaces.json");
2651
+ const registryPath = resolve9(dirname2(projectsDir), "workspaces.json");
2426
2652
  await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
2427
2653
  }
2428
- async function listWorkspaces(projectsDir) {
2429
- const [projectRecords, registered] = await Promise.all([
2654
+ async function listWorkspaces(projectsDir, assignmentsDir) {
2655
+ const [projectRecords, registered, standaloneRecords] = await Promise.all([
2430
2656
  listProjectRecords(projectsDir),
2431
- readWorkspaceRegistry(projectsDir)
2657
+ readWorkspaceRegistry(projectsDir),
2658
+ listStandaloneRecords(assignmentsDir)
2432
2659
  ]);
2433
2660
  const workspaceSet = new Set(registered);
2434
2661
  let hasUngrouped = false;
@@ -2439,6 +2666,13 @@ async function listWorkspaces(projectsDir) {
2439
2666
  hasUngrouped = true;
2440
2667
  }
2441
2668
  }
2669
+ for (const sr of standaloneRecords) {
2670
+ if (sr.record.workspaceGroup) {
2671
+ workspaceSet.add(sr.record.workspaceGroup);
2672
+ } else {
2673
+ hasUngrouped = true;
2674
+ }
2675
+ }
2442
2676
  const workspaces = Array.from(workspaceSet).sort();
2443
2677
  return { workspaces, hasUngrouped };
2444
2678
  }
@@ -2587,7 +2821,7 @@ async function toStandaloneBoardItem(sr) {
2587
2821
  projectSlug: null,
2588
2822
  projectTitle: null,
2589
2823
  blockedReason: sr.record.blockedReason,
2590
- projectWorkspace: null,
2824
+ projectWorkspace: sr.record.workspaceGroup ?? null,
2591
2825
  availableTransitions: await getStandaloneAvailableTransitions(sr.record)
2592
2826
  };
2593
2827
  }
@@ -2622,7 +2856,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
2622
2856
  if (!filePath || !await fileExists(filePath)) {
2623
2857
  return null;
2624
2858
  }
2625
- const content = await readFile7(filePath, "utf-8");
2859
+ const content = await readFile8(filePath, "utf-8");
2626
2860
  const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
2627
2861
  return {
2628
2862
  documentType,
@@ -2646,9 +2880,9 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
2646
2880
  }
2647
2881
  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
2882
  if (!fileName) return null;
2649
- const filePath = resolve8(resolved.assignmentDir, fileName);
2883
+ const filePath = resolve9(resolved.assignmentDir, fileName);
2650
2884
  if (!await fileExists(filePath)) return null;
2651
- const content = await readFile7(filePath, "utf-8");
2885
+ const content = await readFile8(filePath, "utf-8");
2652
2886
  const label = resolved.id;
2653
2887
  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
2888
  return {
@@ -2662,12 +2896,12 @@ async function getEditableDocumentById(projectsDir, assignmentsDir, documentType
2662
2896
  };
2663
2897
  }
2664
2898
  async function getProjectDetail(projectsDir, slug) {
2665
- const projectPath = resolve8(projectsDir, slug);
2666
- const projectMdPath = resolve8(projectPath, "project.md");
2899
+ const projectPath = resolve9(projectsDir, slug);
2900
+ const projectMdPath = resolve9(projectPath, "project.md");
2667
2901
  if (!await fileExists(projectMdPath)) {
2668
2902
  return null;
2669
2903
  }
2670
- const projectContent = await readFile7(projectMdPath, "utf-8");
2904
+ const projectContent = await readFile8(projectMdPath, "utf-8");
2671
2905
  const project = parseProject(projectContent);
2672
2906
  const assignments = await listAssignmentRecords(projectPath);
2673
2907
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -2697,17 +2931,17 @@ async function getProjectDetail(projectsDir, slug) {
2697
2931
  };
2698
2932
  }
2699
2933
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2700
- const assignmentDir = resolve8(projectsDir, projectSlug, "assignments", assignmentSlug);
2701
- const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
2934
+ const assignmentDir = resolve9(projectsDir, projectSlug, "assignments", assignmentSlug);
2935
+ const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
2702
2936
  if (!await fileExists(assignmentMdPath)) {
2703
2937
  return null;
2704
2938
  }
2705
- const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
2939
+ const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
2706
2940
  const assignment = parseAssignmentFull(assignmentContent);
2707
2941
  let plan = null;
2708
- const planPath = resolve8(assignmentDir, "plan.md");
2942
+ const planPath = resolve9(assignmentDir, "plan.md");
2709
2943
  if (await fileExists(planPath)) {
2710
- const planContent = await readFile7(planPath, "utf-8");
2944
+ const planContent = await readFile8(planPath, "utf-8");
2711
2945
  const parsed = parsePlan(planContent);
2712
2946
  plan = {
2713
2947
  status: parsed.status,
@@ -2716,9 +2950,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2716
2950
  };
2717
2951
  }
2718
2952
  let scratchpad = null;
2719
- const scratchpadPath = resolve8(assignmentDir, "scratchpad.md");
2953
+ const scratchpadPath = resolve9(assignmentDir, "scratchpad.md");
2720
2954
  if (await fileExists(scratchpadPath)) {
2721
- const scratchpadContent = await readFile7(scratchpadPath, "utf-8");
2955
+ const scratchpadContent = await readFile8(scratchpadPath, "utf-8");
2722
2956
  const parsed = parseScratchpad(scratchpadContent);
2723
2957
  scratchpad = {
2724
2958
  updated: parsed.updated,
@@ -2726,9 +2960,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2726
2960
  };
2727
2961
  }
2728
2962
  let handoff = null;
2729
- const handoffPath = resolve8(assignmentDir, "handoff.md");
2963
+ const handoffPath = resolve9(assignmentDir, "handoff.md");
2730
2964
  if (await fileExists(handoffPath)) {
2731
- const handoffContent = await readFile7(handoffPath, "utf-8");
2965
+ const handoffContent = await readFile8(handoffPath, "utf-8");
2732
2966
  const parsed = parseHandoff(handoffContent);
2733
2967
  handoff = {
2734
2968
  updated: parsed.updated,
@@ -2737,9 +2971,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2737
2971
  };
2738
2972
  }
2739
2973
  let decisionRecord = null;
2740
- const decisionRecordPath = resolve8(assignmentDir, "decision-record.md");
2974
+ const decisionRecordPath = resolve9(assignmentDir, "decision-record.md");
2741
2975
  if (await fileExists(decisionRecordPath)) {
2742
- const decisionRecordContent = await readFile7(decisionRecordPath, "utf-8");
2976
+ const decisionRecordContent = await readFile8(decisionRecordPath, "utf-8");
2743
2977
  const parsed = parseDecisionRecord(decisionRecordContent);
2744
2978
  decisionRecord = {
2745
2979
  updated: parsed.updated,
@@ -2748,9 +2982,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2748
2982
  };
2749
2983
  }
2750
2984
  let progress = null;
2751
- const progressPath = resolve8(assignmentDir, "progress.md");
2985
+ const progressPath = resolve9(assignmentDir, "progress.md");
2752
2986
  if (await fileExists(progressPath)) {
2753
- const progressContent = await readFile7(progressPath, "utf-8");
2987
+ const progressContent = await readFile8(progressPath, "utf-8");
2754
2988
  const parsed = parseProgress(progressContent);
2755
2989
  progress = {
2756
2990
  updated: parsed.updated,
@@ -2759,9 +2993,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2759
2993
  };
2760
2994
  }
2761
2995
  let comments = null;
2762
- const commentsPath = resolve8(assignmentDir, "comments.md");
2996
+ const commentsPath = resolve9(assignmentDir, "comments.md");
2763
2997
  if (await fileExists(commentsPath)) {
2764
- const commentsContent = await readFile7(commentsPath, "utf-8");
2998
+ const commentsContent = await readFile8(commentsPath, "utf-8");
2765
2999
  const parsed = parseComments(commentsContent);
2766
3000
  comments = {
2767
3001
  updated: parsed.updated,
@@ -2875,7 +3109,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2875
3109
  slug: a.slug,
2876
3110
  title: a.title,
2877
3111
  projectSlug: rec.summary.slug,
2878
- assignmentDir: resolve8(rec.projectPath, "assignments", a.slug)
3112
+ assignmentDir: resolve9(rec.projectPath, "assignments", a.slug)
2879
3113
  });
2880
3114
  }
2881
3115
  }
@@ -2908,17 +3142,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2908
3142
  }
2909
3143
  async function countMentionsInAssignment(sourceDir, target) {
2910
3144
  const bodies = [];
2911
- const assignmentMd = resolve8(sourceDir, "assignment.md");
3145
+ const assignmentMd = resolve9(sourceDir, "assignment.md");
2912
3146
  if (await fileExists(assignmentMd)) {
2913
- const content = await readFile7(assignmentMd, "utf-8");
3147
+ const content = await readFile8(assignmentMd, "utf-8");
2914
3148
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
2915
3149
  if (todosMatch) bodies.push(todosMatch[1]);
2916
3150
  }
2917
3151
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
2918
- const path = resolve8(sourceDir, filename);
3152
+ const path = resolve9(sourceDir, filename);
2919
3153
  if (await fileExists(path)) {
2920
3154
  try {
2921
- bodies.push(await readFile7(path, "utf-8"));
3155
+ bodies.push(await readFile8(path, "utf-8"));
2922
3156
  } catch {
2923
3157
  }
2924
3158
  }
@@ -2976,44 +3210,44 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
2976
3210
  }
2977
3211
  async function buildStandaloneAssignmentDetail(resolved) {
2978
3212
  const assignmentDir = resolved.assignmentDir;
2979
- const assignmentMdPath = resolve8(assignmentDir, "assignment.md");
3213
+ const assignmentMdPath = resolve9(assignmentDir, "assignment.md");
2980
3214
  if (!await fileExists(assignmentMdPath)) return null;
2981
- const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
3215
+ const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
2982
3216
  const assignment = parseAssignmentFull(assignmentContent);
2983
3217
  let plan = null;
2984
- const planPath = resolve8(assignmentDir, "plan.md");
3218
+ const planPath = resolve9(assignmentDir, "plan.md");
2985
3219
  if (await fileExists(planPath)) {
2986
- const parsed = parsePlan(await readFile7(planPath, "utf-8"));
3220
+ const parsed = parsePlan(await readFile8(planPath, "utf-8"));
2987
3221
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
2988
3222
  }
2989
3223
  let scratchpad = null;
2990
- const scratchpadPath = resolve8(assignmentDir, "scratchpad.md");
3224
+ const scratchpadPath = resolve9(assignmentDir, "scratchpad.md");
2991
3225
  if (await fileExists(scratchpadPath)) {
2992
- const parsed = parseScratchpad(await readFile7(scratchpadPath, "utf-8"));
3226
+ const parsed = parseScratchpad(await readFile8(scratchpadPath, "utf-8"));
2993
3227
  scratchpad = { updated: parsed.updated, body: parsed.body };
2994
3228
  }
2995
3229
  let handoff = null;
2996
- const handoffPath = resolve8(assignmentDir, "handoff.md");
3230
+ const handoffPath = resolve9(assignmentDir, "handoff.md");
2997
3231
  if (await fileExists(handoffPath)) {
2998
- const parsed = parseHandoff(await readFile7(handoffPath, "utf-8"));
3232
+ const parsed = parseHandoff(await readFile8(handoffPath, "utf-8"));
2999
3233
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
3000
3234
  }
3001
3235
  let decisionRecord = null;
3002
- const decisionRecordPath = resolve8(assignmentDir, "decision-record.md");
3236
+ const decisionRecordPath = resolve9(assignmentDir, "decision-record.md");
3003
3237
  if (await fileExists(decisionRecordPath)) {
3004
- const parsed = parseDecisionRecord(await readFile7(decisionRecordPath, "utf-8"));
3238
+ const parsed = parseDecisionRecord(await readFile8(decisionRecordPath, "utf-8"));
3005
3239
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
3006
3240
  }
3007
3241
  let progress = null;
3008
- const progressPath = resolve8(assignmentDir, "progress.md");
3242
+ const progressPath = resolve9(assignmentDir, "progress.md");
3009
3243
  if (await fileExists(progressPath)) {
3010
- const parsed = parseProgress(await readFile7(progressPath, "utf-8"));
3244
+ const parsed = parseProgress(await readFile8(progressPath, "utf-8"));
3011
3245
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
3012
3246
  }
3013
3247
  let comments = null;
3014
- const commentsPath = resolve8(assignmentDir, "comments.md");
3248
+ const commentsPath = resolve9(assignmentDir, "comments.md");
3015
3249
  if (await fileExists(commentsPath)) {
3016
- const parsed = parseComments(await readFile7(commentsPath, "utf-8"));
3250
+ const parsed = parseComments(await readFile8(commentsPath, "utf-8"));
3017
3251
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
3018
3252
  }
3019
3253
  const detail = {
@@ -3055,16 +3289,16 @@ async function listProjectRecords(projectsDir) {
3055
3289
  migratedProjectsDirs.add(projectsDir);
3056
3290
  await migrateLegacyProjectFiles(projectsDir);
3057
3291
  }
3058
- const entries = await readdir5(projectsDir, { withFileTypes: true });
3292
+ const entries = await readdir6(projectsDir, { withFileTypes: true });
3059
3293
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
3060
3294
  const records = [];
3061
3295
  for (const entry of projectDirs) {
3062
- const projectPath = resolve8(projectsDir, entry.name);
3063
- const projectMdPath = resolve8(projectPath, "project.md");
3296
+ const projectPath = resolve9(projectsDir, entry.name);
3297
+ const projectMdPath = resolve9(projectPath, "project.md");
3064
3298
  if (!await fileExists(projectMdPath)) {
3065
3299
  continue;
3066
3300
  }
3067
- const projectContent = await readFile7(projectMdPath, "utf-8");
3301
+ const projectContent = await readFile8(projectMdPath, "utf-8");
3068
3302
  const project = parseProject(projectContent);
3069
3303
  const assignments = await listAssignmentRecords(projectPath);
3070
3304
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -3095,39 +3329,39 @@ async function listProjectRecords(projectsDir) {
3095
3329
  return records;
3096
3330
  }
3097
3331
  async function listAssignmentRecords(projectPath) {
3098
- const assignmentsDir = resolve8(projectPath, "assignments");
3332
+ const assignmentsDir = resolve9(projectPath, "assignments");
3099
3333
  if (!await fileExists(assignmentsDir)) {
3100
3334
  return [];
3101
3335
  }
3102
- const entries = await readdir5(assignmentsDir, { withFileTypes: true });
3336
+ const entries = await readdir6(assignmentsDir, { withFileTypes: true });
3103
3337
  const records = [];
3104
3338
  for (const entry of entries) {
3105
3339
  if (!entry.isDirectory()) {
3106
3340
  continue;
3107
3341
  }
3108
- const assignmentMd = resolve8(assignmentsDir, entry.name, "assignment.md");
3342
+ const assignmentMd = resolve9(assignmentsDir, entry.name, "assignment.md");
3109
3343
  if (!await fileExists(assignmentMd)) {
3110
3344
  continue;
3111
3345
  }
3112
- const content = await readFile7(assignmentMd, "utf-8");
3346
+ const content = await readFile8(assignmentMd, "utf-8");
3113
3347
  records.push(parseAssignmentFull(content));
3114
3348
  }
3115
3349
  records.sort((left, right) => compareTimestamps(right.updated, left.updated));
3116
3350
  return records;
3117
3351
  }
3118
3352
  async function listResources(projectPath) {
3119
- const resourcesDir = resolve8(projectPath, "resources");
3353
+ const resourcesDir = resolve9(projectPath, "resources");
3120
3354
  if (!await fileExists(resourcesDir)) {
3121
3355
  return [];
3122
3356
  }
3123
- const entries = await readdir5(resourcesDir, { withFileTypes: true });
3357
+ const entries = await readdir6(resourcesDir, { withFileTypes: true });
3124
3358
  const results = [];
3125
3359
  for (const entry of entries) {
3126
3360
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
3127
3361
  continue;
3128
3362
  }
3129
- const filePath = resolve8(resourcesDir, entry.name);
3130
- const content = await readFile7(filePath, "utf-8");
3363
+ const filePath = resolve9(resourcesDir, entry.name);
3364
+ const content = await readFile8(filePath, "utf-8");
3131
3365
  const parsed = parseResource(content);
3132
3366
  results.push({
3133
3367
  name: parsed.name,
@@ -3142,18 +3376,18 @@ async function listResources(projectPath) {
3142
3376
  return results;
3143
3377
  }
3144
3378
  async function listMemories(projectPath) {
3145
- const memoriesDir = resolve8(projectPath, "memories");
3379
+ const memoriesDir = resolve9(projectPath, "memories");
3146
3380
  if (!await fileExists(memoriesDir)) {
3147
3381
  return [];
3148
3382
  }
3149
- const entries = await readdir5(memoriesDir, { withFileTypes: true });
3383
+ const entries = await readdir6(memoriesDir, { withFileTypes: true });
3150
3384
  const results = [];
3151
3385
  for (const entry of entries) {
3152
3386
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
3153
3387
  continue;
3154
3388
  }
3155
- const filePath = resolve8(memoriesDir, entry.name);
3156
- const content = await readFile7(filePath, "utf-8");
3389
+ const filePath = resolve9(memoriesDir, entry.name);
3390
+ const content = await readFile8(filePath, "utf-8");
3157
3391
  const parsed = parseMemory(content);
3158
3392
  results.push({
3159
3393
  name: parsed.name,
@@ -3168,9 +3402,9 @@ async function listMemories(projectPath) {
3168
3402
  return results;
3169
3403
  }
3170
3404
  async function loadDependencyGraph(projectPath, assignments) {
3171
- const statusPath = resolve8(projectPath, "_status.md");
3405
+ const statusPath = resolve9(projectPath, "_status.md");
3172
3406
  if (await fileExists(statusPath)) {
3173
- const statusContent = await readFile7(statusPath, "utf-8");
3407
+ const statusContent = await readFile8(statusPath, "utf-8");
3174
3408
  const parsed = parseStatus(statusContent);
3175
3409
  const derivedGraph = extractMermaidGraph(parsed.body);
3176
3410
  if (derivedGraph) {
@@ -3270,7 +3504,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
3270
3504
  const config = await getStatusConfig();
3271
3505
  const transitionDefs = getTransitionDefinitions(config);
3272
3506
  const actions = [];
3273
- const projectPath = resolve8(projectsDir, projectSlug);
3507
+ const projectPath = resolve9(projectsDir, projectSlug);
3274
3508
  for (const definition of transitionDefs) {
3275
3509
  let warning = null;
3276
3510
  if (definition.command === "start" && !assignment.assignee) {
@@ -3300,12 +3534,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
3300
3534
  const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
3301
3535
  const unmet = [];
3302
3536
  for (const dependency of dependsOn) {
3303
- const dependencyPath = resolve8(projectPath, "assignments", dependency, "assignment.md");
3537
+ const dependencyPath = resolve9(projectPath, "assignments", dependency, "assignment.md");
3304
3538
  if (!await fileExists(dependencyPath)) {
3305
3539
  unmet.push(`${dependency} (missing)`);
3306
3540
  continue;
3307
3541
  }
3308
- const content = await readFile7(dependencyPath, "utf-8");
3542
+ const content = await readFile8(dependencyPath, "utf-8");
3309
3543
  const parsed = parseAssignmentFull(content);
3310
3544
  if (!terminals.has(parsed.status)) {
3311
3545
  unmet.push(`${dependency} (${parsed.status})`);
@@ -3460,7 +3694,7 @@ function isStale(updated) {
3460
3694
  return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
3461
3695
  }
3462
3696
  async function countOpenQuestions(projectPath, assignmentSlug) {
3463
- const commentsPath = resolve8(
3697
+ const commentsPath = resolve9(
3464
3698
  projectPath,
3465
3699
  "assignments",
3466
3700
  assignmentSlug,
@@ -3470,7 +3704,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
3470
3704
  return 0;
3471
3705
  }
3472
3706
  try {
3473
- const content = await readFile7(commentsPath, "utf-8");
3707
+ const content = await readFile8(commentsPath, "utf-8");
3474
3708
  const parsed = parseComments(content);
3475
3709
  return parsed.entries.filter(
3476
3710
  (e) => e.type === "question" && e.resolved !== true
@@ -3491,17 +3725,17 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
3491
3725
  function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
3492
3726
  switch (documentType) {
3493
3727
  case "project":
3494
- return resolve8(projectsDir, projectSlug, "project.md");
3728
+ return resolve9(projectsDir, projectSlug, "project.md");
3495
3729
  case "assignment":
3496
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
3730
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
3497
3731
  case "plan":
3498
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
3732
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
3499
3733
  case "scratchpad":
3500
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
3734
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
3501
3735
  case "handoff":
3502
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
3736
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
3503
3737
  case "decision-record":
3504
- return assignmentSlug ? resolve8(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
3738
+ return assignmentSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
3505
3739
  default:
3506
3740
  return null;
3507
3741
  }
@@ -3528,12 +3762,14 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
3528
3762
  }
3529
3763
  async function listPlaybooks(playbooksDir2) {
3530
3764
  if (!await fileExists(playbooksDir2)) return [];
3531
- const entries = await readdir5(playbooksDir2, { withFileTypes: true });
3765
+ const config = await readConfig();
3766
+ const disabledSet = new Set(config.playbooks.disabled);
3767
+ const entries = await readdir6(playbooksDir2, { withFileTypes: true });
3532
3768
  const playbooks = [];
3533
3769
  for (const entry of entries) {
3534
3770
  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");
3771
+ const filePath = resolve9(playbooksDir2, entry.name);
3772
+ const raw = await readFile8(filePath, "utf-8");
3537
3773
  const parsed = parsePlaybook(raw);
3538
3774
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
3539
3775
  playbooks.push({
@@ -3543,25 +3779,28 @@ async function listPlaybooks(playbooksDir2) {
3543
3779
  whenToUse: parsed.whenToUse,
3544
3780
  tags: parsed.tags,
3545
3781
  created: parsed.created,
3546
- updated: parsed.updated
3782
+ updated: parsed.updated,
3783
+ enabled: !disabledSet.has(slug)
3547
3784
  });
3548
3785
  }
3549
3786
  return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
3550
3787
  }
3551
3788
  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);
3789
+ const resolved = await resolvePlaybookSlug(playbooksDir2, slug);
3790
+ if (!resolved) return null;
3791
+ const config = await readConfig();
3792
+ const enabled = !config.playbooks.disabled.includes(resolved.slug);
3793
+ const parsed = resolved.parsed;
3556
3794
  return {
3557
- slug: parsed.slug || slug,
3558
- name: parsed.name || slug,
3795
+ slug: resolved.slug,
3796
+ name: parsed.name || resolved.slug,
3559
3797
  description: parsed.description,
3560
3798
  whenToUse: parsed.whenToUse,
3561
3799
  tags: parsed.tags,
3562
3800
  created: parsed.created,
3563
3801
  updated: parsed.updated,
3564
- body: parsed.body
3802
+ body: parsed.body,
3803
+ enabled
3565
3804
  };
3566
3805
  }
3567
3806
  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 +3810,7 @@ var init_api = __esm({
3571
3810
  init_lifecycle();
3572
3811
  init_fs();
3573
3812
  init_config2();
3813
+ init_playbooks();
3574
3814
  init_fs_migration();
3575
3815
  init_assignment_resolver();
3576
3816
  init_parser();
@@ -3928,15 +4168,15 @@ import { WebSocketServer, WebSocket } from "ws";
3928
4168
 
3929
4169
  // src/dashboard/agent-sessions.ts
3930
4170
  init_fs();
3931
- import { readFile as readFile8 } from "fs/promises";
3932
- import { resolve as resolve10 } from "path";
4171
+ import { readFile as readFile9 } from "fs/promises";
4172
+ import { resolve as resolve11 } from "path";
3933
4173
 
3934
4174
  // src/dashboard/session-db.ts
3935
4175
  init_paths();
3936
4176
  init_fs();
3937
4177
  import Database from "better-sqlite3";
3938
- import { resolve as resolve9 } from "path";
3939
- import { readdir as readdir6 } from "fs/promises";
4178
+ import { resolve as resolve10 } from "path";
4179
+ import { readdir as readdir7 } from "fs/promises";
3940
4180
  var db = null;
3941
4181
  var SCHEMA_VERSION = "3";
3942
4182
  var SCHEMA_SQL = `
@@ -3963,7 +4203,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, ass
3963
4203
  `;
3964
4204
  function initSessionDb(dbPath) {
3965
4205
  if (db) return db;
3966
- const finalPath = dbPath ?? resolve9(syntaurRoot(), "syntaur.db");
4206
+ const finalPath = dbPath ?? resolve10(syntaurRoot(), "syntaur.db");
3967
4207
  db = new Database(finalPath);
3968
4208
  db.pragma("journal_mode = WAL");
3969
4209
  db.exec(SCHEMA_SQL);
@@ -4060,12 +4300,12 @@ async function migrateFromMarkdown(projectsDir) {
4060
4300
  const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
4061
4301
  if (count.count > 0) return 0;
4062
4302
  if (!await fileExists(projectsDir)) return 0;
4063
- const entries = await readdir6(projectsDir, { withFileTypes: true });
4303
+ const entries = await readdir7(projectsDir, { withFileTypes: true });
4064
4304
  const allSessions = [];
4065
4305
  for (const entry of entries) {
4066
4306
  if (!entry.isDirectory()) continue;
4067
- const projectDir = resolve9(projectsDir, entry.name);
4068
- const indexPath = resolve9(projectDir, "_index-sessions.md");
4307
+ const projectDir = resolve10(projectsDir, entry.name);
4308
+ const indexPath = resolve10(projectDir, "_index-sessions.md");
4069
4309
  if (!await fileExists(indexPath)) continue;
4070
4310
  const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
4071
4311
  allSessions.push(...sessions);
@@ -4198,13 +4438,13 @@ async function deleteSessions(sessionIds) {
4198
4438
  var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
4199
4439
  async function readAssignmentStatusFromPath(assignmentMdPath) {
4200
4440
  if (!await fileExists(assignmentMdPath)) return null;
4201
- const raw = await readFile8(assignmentMdPath, "utf-8");
4441
+ const raw = await readFile9(assignmentMdPath, "utf-8");
4202
4442
  const match = raw.match(/^status:\s*(.+)$/m);
4203
4443
  return match ? match[1].trim() : null;
4204
4444
  }
4205
4445
  async function readAssignmentStatus(projectDir, assignmentSlug) {
4206
4446
  return readAssignmentStatusFromPath(
4207
- resolve10(projectDir, "assignments", assignmentSlug, "assignment.md")
4447
+ resolve11(projectDir, "assignments", assignmentSlug, "assignment.md")
4208
4448
  );
4209
4449
  }
4210
4450
  async function reconcileActiveSessions(projectsDir, assignmentsDir) {
@@ -4222,13 +4462,13 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir) {
4222
4462
  seen.add(key);
4223
4463
  if (session.project_slug) {
4224
4464
  const status = await readAssignmentStatus(
4225
- resolve10(projectsDir, session.project_slug),
4465
+ resolve11(projectsDir, session.project_slug),
4226
4466
  aslug
4227
4467
  );
4228
4468
  if (status) assignmentStatuses.set(key, status);
4229
4469
  } else if (assignmentsDir) {
4230
4470
  const status = await readAssignmentStatusFromPath(
4231
- resolve10(assignmentsDir, aslug, "assignment.md")
4471
+ resolve11(assignmentsDir, aslug, "assignment.md")
4232
4472
  );
4233
4473
  if (status) assignmentStatuses.set(key, status);
4234
4474
  }
@@ -4442,8 +4682,8 @@ init_config2();
4442
4682
  // src/dashboard/api-write.ts
4443
4683
  init_lifecycle();
4444
4684
  import { Router } from "express";
4445
- import { resolve as resolve11 } from "path";
4446
- import { rm, readFile as readFile9 } from "fs/promises";
4685
+ import { resolve as resolve12 } from "path";
4686
+ import { rm, readFile as readFile10 } from "fs/promises";
4447
4687
 
4448
4688
  // src/utils/slug.ts
4449
4689
  function isValidSlug(slug) {
@@ -4585,12 +4825,25 @@ function renderAssignment(params) {
4585
4825
  const linksYaml = params.links.length === 0 ? "links: []" : `links:
4586
4826
  - ${params.links.join("\n - ")}`;
4587
4827
  const projectYaml = `project: ${params.project == null ? "null" : params.project}`;
4828
+ const workspaceGroupLine = params.workspaceGroup ? `
4829
+ workspaceGroup: ${params.workspaceGroup}` : "";
4588
4830
  const typeYaml = `type: ${params.type ?? "feature"}`;
4831
+ const todosSection = params.includeTodos ? `## Todos
4832
+
4833
+ <!--
4834
+ Checklist of work items for this assignment. Items may be simple tasks
4835
+ or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
4836
+ When a plan is superseded by a new one, mark the old todo as:
4837
+ - [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
4838
+ Never delete superseded todos \u2014 preserve the history.
4839
+ -->
4840
+
4841
+ ` : "";
4589
4842
  return `---
4590
4843
  id: ${params.id}
4591
4844
  slug: ${params.slug}
4592
4845
  title: ${safeTitle}
4593
- ${projectYaml}
4846
+ ${projectYaml}${workspaceGroupLine}
4594
4847
  ${typeYaml}
4595
4848
  status: pending
4596
4849
  priority: ${params.priority}
@@ -4621,17 +4874,7 @@ tags: []
4621
4874
  - [ ] <!-- criterion 2 -->
4622
4875
  - [ ] <!-- criterion 3 -->
4623
4876
 
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
4877
+ ${todosSection}## Context
4635
4878
 
4636
4879
  <!-- Links to relevant docs, code, or other assignments. -->
4637
4880
 
@@ -4966,7 +5209,7 @@ async function readCurrentDocument(filePath) {
4966
5209
  if (!await fileExists(filePath)) {
4967
5210
  return null;
4968
5211
  }
4969
- return readFile9(filePath, "utf-8");
5212
+ return readFile10(filePath, "utf-8");
4970
5213
  }
4971
5214
  function createWriteRouter(projectsDir, assignmentsDir) {
4972
5215
  const router = Router();
@@ -4979,7 +5222,15 @@ function createWriteRouter(projectsDir, assignmentsDir) {
4979
5222
  });
4980
5223
  res.json({ content });
4981
5224
  });
4982
- router.get("/api/templates/assignment", (_req, res) => {
5225
+ router.get("/api/templates/assignment", (req, res) => {
5226
+ const standalone = req.query.standalone === "1";
5227
+ const workspaceParam = typeof req.query.workspace === "string" ? req.query.workspace : "";
5228
+ if (workspaceParam && !isValidSlug(workspaceParam)) {
5229
+ res.status(400).json({
5230
+ error: `Invalid workspace slug "${workspaceParam}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5231
+ });
5232
+ return;
5233
+ }
4983
5234
  const content = renderAssignment({
4984
5235
  id: generateId(),
4985
5236
  slug: "my-new-assignment",
@@ -4987,7 +5238,9 @@ function createWriteRouter(projectsDir, assignmentsDir) {
4987
5238
  timestamp: nowTimestamp(),
4988
5239
  priority: "medium",
4989
5240
  dependsOn: [],
4990
- links: []
5241
+ links: [],
5242
+ project: standalone ? null : void 0,
5243
+ workspaceGroup: standalone && workspaceParam ? workspaceParam : null
4991
5244
  });
4992
5245
  res.json({ content });
4993
5246
  });
@@ -5096,26 +5349,26 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5096
5349
  res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
5097
5350
  return;
5098
5351
  }
5099
- const projectDir = resolve11(projectsDir, slug);
5352
+ const projectDir = resolve12(projectsDir, slug);
5100
5353
  if (await fileExists(projectDir)) {
5101
5354
  res.status(409).json({ error: `Project "${slug}" already exists` });
5102
5355
  return;
5103
5356
  }
5104
5357
  const title = fields.title;
5105
5358
  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);
5359
+ await ensureDir(resolve12(projectDir, "assignments"));
5360
+ await ensureDir(resolve12(projectDir, "resources"));
5361
+ await ensureDir(resolve12(projectDir, "memories"));
5362
+ await writeFileForce(resolve12(projectDir, "project.md"), content);
5110
5363
  try {
5111
5364
  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 })]
5365
+ [resolve12(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
5366
+ [resolve12(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
5367
+ [resolve12(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
5368
+ [resolve12(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
5369
+ [resolve12(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
5370
+ [resolve12(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
5371
+ [resolve12(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
5119
5372
  ];
5120
5373
  for (const [filePath, fileContent] of companions) {
5121
5374
  await writeFileForce(filePath, fileContent);
@@ -5136,8 +5389,8 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5136
5389
  router.post("/api/projects/:slug/assignments", async (req, res) => {
5137
5390
  try {
5138
5391
  const projectSlug = getParam(req.params.slug);
5139
- const projectDir = resolve11(projectsDir, projectSlug);
5140
- const projectMdPath = resolve11(projectDir, "project.md");
5392
+ const projectDir = resolve12(projectsDir, projectSlug);
5393
+ const projectMdPath = resolve12(projectDir, "project.md");
5141
5394
  if (!await fileExists(projectMdPath)) {
5142
5395
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5143
5396
  return;
@@ -5167,7 +5420,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5167
5420
  res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
5168
5421
  return;
5169
5422
  }
5170
- const assignmentDir = resolve11(projectDir, "assignments", assignmentSlug);
5423
+ const assignmentDir = resolve12(projectDir, "assignments", assignmentSlug);
5171
5424
  if (await fileExists(assignmentDir)) {
5172
5425
  res.status(409).json({
5173
5426
  error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
@@ -5176,12 +5429,12 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5176
5429
  }
5177
5430
  const timestamp = fields.created || nowTimestamp();
5178
5431
  await ensureDir(assignmentDir);
5179
- await writeFileForce(resolve11(assignmentDir, "assignment.md"), content);
5432
+ await writeFileForce(resolve12(assignmentDir, "assignment.md"), content);
5180
5433
  try {
5181
5434
  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 })]
5435
+ [resolve12(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
5436
+ [resolve12(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
5437
+ [resolve12(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
5185
5438
  ];
5186
5439
  for (const [filePath, fileContent] of companions) {
5187
5440
  await writeFileForce(filePath, fileContent);
@@ -5202,7 +5455,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5202
5455
  router.patch("/api/projects/:slug", async (req, res) => {
5203
5456
  try {
5204
5457
  const projectSlug = getParam(req.params.slug);
5205
- const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5458
+ const projectPath = resolve12(projectsDir, projectSlug, "project.md");
5206
5459
  const currentContent = await readCurrentDocument(projectPath);
5207
5460
  if (!currentContent) {
5208
5461
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
@@ -5235,7 +5488,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5235
5488
  try {
5236
5489
  const projectSlug = getParam(req.params.slug);
5237
5490
  const assignmentSlug = getParam(req.params.aslug);
5238
- const assignmentPath = resolve11(
5491
+ const assignmentPath = resolve12(
5239
5492
  projectsDir,
5240
5493
  projectSlug,
5241
5494
  "assignments",
@@ -5278,7 +5531,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5278
5531
  try {
5279
5532
  const projectSlug = getParam(req.params.slug);
5280
5533
  const assignmentSlug = getParam(req.params.aslug);
5281
- const assignmentPath = resolve11(
5534
+ const assignmentPath = resolve12(
5282
5535
  projectsDir,
5283
5536
  projectSlug,
5284
5537
  "assignments",
@@ -5314,7 +5567,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5314
5567
  try {
5315
5568
  const projectSlug = getParam(req.params.slug);
5316
5569
  const assignmentSlug = getParam(req.params.aslug);
5317
- const planPath = resolve11(
5570
+ const planPath = resolve12(
5318
5571
  projectsDir,
5319
5572
  projectSlug,
5320
5573
  "assignments",
@@ -5352,7 +5605,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5352
5605
  try {
5353
5606
  const projectSlug = getParam(req.params.slug);
5354
5607
  const assignmentSlug = getParam(req.params.aslug);
5355
- const scratchpadPath = resolve11(
5608
+ const scratchpadPath = resolve12(
5356
5609
  projectsDir,
5357
5610
  projectSlug,
5358
5611
  "assignments",
@@ -5390,7 +5643,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5390
5643
  try {
5391
5644
  const projectSlug = getParam(req.params.slug);
5392
5645
  const assignmentSlug = getParam(req.params.aslug);
5393
- const handoffPath = resolve11(
5646
+ const handoffPath = resolve12(
5394
5647
  projectsDir,
5395
5648
  projectSlug,
5396
5649
  "assignments",
@@ -5428,7 +5681,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5428
5681
  try {
5429
5682
  const projectSlug = getParam(req.params.slug);
5430
5683
  const assignmentSlug = getParam(req.params.aslug);
5431
- const decisionPath = resolve11(
5684
+ const decisionPath = resolve12(
5432
5685
  projectsDir,
5433
5686
  projectSlug,
5434
5687
  "assignments",
@@ -5466,7 +5719,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5466
5719
  try {
5467
5720
  const projectSlug = getParam(req.params.slug);
5468
5721
  const assignmentSlug = getParam(req.params.aslug);
5469
- const commentsPath = resolve11(
5722
+ const commentsPath = resolve12(
5470
5723
  projectsDir,
5471
5724
  projectSlug,
5472
5725
  "assignments",
@@ -5484,7 +5737,7 @@ function createWriteRouter(projectsDir, assignmentsDir) {
5484
5737
  let currentContent;
5485
5738
  let currentCount = 0;
5486
5739
  if (await fileExists(commentsPath)) {
5487
- currentContent = await readFile9(commentsPath, "utf-8");
5740
+ currentContent = await readFile10(commentsPath, "utf-8");
5488
5741
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
5489
5742
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
5490
5743
  } else {
@@ -5525,7 +5778,7 @@ ${entry}`;
5525
5778
  const projectSlug = getParam(req.params.slug);
5526
5779
  const assignmentSlug = getParam(req.params.aslug);
5527
5780
  const commentId = getParam(req.params.commentId);
5528
- const commentsPath = resolve11(
5781
+ const commentsPath = resolve12(
5529
5782
  projectsDir,
5530
5783
  projectSlug,
5531
5784
  "assignments",
@@ -5541,7 +5794,7 @@ ${entry}`;
5541
5794
  res.status(400).json({ error: "resolved (boolean) is required" });
5542
5795
  return;
5543
5796
  }
5544
- const content = await readFile9(commentsPath, "utf-8");
5797
+ const content = await readFile10(commentsPath, "utf-8");
5545
5798
  const parsed = parseComments(content);
5546
5799
  const target = parsed.entries.find((e) => e.id === commentId);
5547
5800
  if (!target) {
@@ -5576,7 +5829,7 @@ ${entry}`;
5576
5829
  router.post("/api/projects/:slug/move-workspace", async (req, res) => {
5577
5830
  try {
5578
5831
  const projectSlug = getParam(req.params.slug);
5579
- const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5832
+ const projectPath = resolve12(projectsDir, projectSlug, "project.md");
5580
5833
  if (!await fileExists(projectPath)) {
5581
5834
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5582
5835
  return;
@@ -5586,7 +5839,7 @@ ${entry}`;
5586
5839
  res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
5587
5840
  return;
5588
5841
  }
5589
- let content = await readFile9(projectPath, "utf-8");
5842
+ let content = await readFile10(projectPath, "utf-8");
5590
5843
  content = setTopLevelField(content, "workspace", workspace ?? null);
5591
5844
  content = setTopLevelField(content, "updated", nowTimestamp());
5592
5845
  await writeFileForce(projectPath, content);
@@ -5600,7 +5853,7 @@ ${entry}`;
5600
5853
  router.post("/api/projects/:slug/status-override", async (req, res) => {
5601
5854
  try {
5602
5855
  const projectSlug = getParam(req.params.slug);
5603
- const projectPath = resolve11(projectsDir, projectSlug, "project.md");
5856
+ const projectPath = resolve12(projectsDir, projectSlug, "project.md");
5604
5857
  if (!await fileExists(projectPath)) {
5605
5858
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5606
5859
  return;
@@ -5612,7 +5865,7 @@ ${entry}`;
5612
5865
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
5613
5866
  return;
5614
5867
  }
5615
- let content = await readFile9(projectPath, "utf-8");
5868
+ let content = await readFile10(projectPath, "utf-8");
5616
5869
  content = setTopLevelField(content, "statusOverride", status ?? null);
5617
5870
  content = setTopLevelField(content, "updated", nowTimestamp());
5618
5871
  await writeFileForce(projectPath, content);
@@ -5627,7 +5880,7 @@ ${entry}`;
5627
5880
  try {
5628
5881
  const projectSlug = getParam(req.params.slug);
5629
5882
  const assignmentSlug = getParam(req.params.aslug);
5630
- const assignmentPath = resolve11(
5883
+ const assignmentPath = resolve12(
5631
5884
  projectsDir,
5632
5885
  projectSlug,
5633
5886
  "assignments",
@@ -5645,7 +5898,7 @@ ${entry}`;
5645
5898
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
5646
5899
  return;
5647
5900
  }
5648
- let content = await readFile9(assignmentPath, "utf-8");
5901
+ let content = await readFile10(assignmentPath, "utf-8");
5649
5902
  content = setTopLevelField(content, "status", status);
5650
5903
  content = setTopLevelField(content, "updated", nowTimestamp());
5651
5904
  if (status !== "blocked") {
@@ -5670,8 +5923,8 @@ ${entry}`;
5670
5923
  res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
5671
5924
  return;
5672
5925
  }
5673
- const projectDir = resolve11(projectsDir, projectSlug);
5674
- const assignmentPath = resolve11(projectDir, "assignments", assignmentSlug, "assignment.md");
5926
+ const projectDir = resolve12(projectsDir, projectSlug);
5927
+ const assignmentPath = resolve12(projectDir, "assignments", assignmentSlug, "assignment.md");
5675
5928
  if (!await fileExists(assignmentPath)) {
5676
5929
  res.status(404).json({ error: "Assignment not found" });
5677
5930
  return;
@@ -5697,8 +5950,8 @@ ${entry}`;
5697
5950
  try {
5698
5951
  const projectSlug = getParam(req.params.slug);
5699
5952
  const assignmentSlug = getParam(req.params.aslug);
5700
- const assignmentDir = resolve11(projectsDir, projectSlug, "assignments", assignmentSlug);
5701
- const assignmentPath = resolve11(assignmentDir, "assignment.md");
5953
+ const assignmentDir = resolve12(projectsDir, projectSlug, "assignments", assignmentSlug);
5954
+ const assignmentPath = resolve12(assignmentDir, "assignment.md");
5702
5955
  if (!await fileExists(assignmentPath)) {
5703
5956
  res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
5704
5957
  return;
@@ -5716,6 +5969,76 @@ ${entry}`;
5716
5969
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
5717
5970
  return;
5718
5971
  }
5972
+ const rawContent = typeof req.body?.content === "string" ? req.body.content : "";
5973
+ if (rawContent.trim()) {
5974
+ const fields = extractFrontmatter3(rawContent);
5975
+ if (!fields) {
5976
+ res.status(400).json({ error: "Invalid frontmatter: missing --- delimiters" });
5977
+ return;
5978
+ }
5979
+ const validation = validateRequired(fields, ["slug", "title"]);
5980
+ if (!validation.valid) {
5981
+ res.status(400).json({ error: `Missing required fields: ${validation.missing.join(", ")}` });
5982
+ return;
5983
+ }
5984
+ const submittedSlug = fields.slug;
5985
+ if (!isValidSlug(submittedSlug)) {
5986
+ res.status(400).json({ error: `Invalid slug "${submittedSlug}". Must be lowercase and hyphen-separated.` });
5987
+ return;
5988
+ }
5989
+ const validPriorities = ["low", "medium", "high", "critical"];
5990
+ const submittedPriority = fields.priority || "medium";
5991
+ if (!validPriorities.includes(submittedPriority)) {
5992
+ res.status(400).json({ error: `Invalid priority "${submittedPriority}". Must be low, medium, high, or critical.` });
5993
+ return;
5994
+ }
5995
+ if (fields.project && fields.project !== "null") {
5996
+ res.status(400).json({
5997
+ error: 'Standalone assignments cannot have a project; remove "project" or set it to null.'
5998
+ });
5999
+ return;
6000
+ }
6001
+ const submittedWorkspaceGroup = fields.workspaceGroup && fields.workspaceGroup !== "null" ? fields.workspaceGroup : "";
6002
+ if (submittedWorkspaceGroup && !isValidSlug(submittedWorkspaceGroup)) {
6003
+ res.status(400).json({
6004
+ error: `Invalid workspace slug "${submittedWorkspaceGroup}". Slugs must be lowercase, hyphen-separated, with no special characters.`
6005
+ });
6006
+ return;
6007
+ }
6008
+ const id2 = generateId();
6009
+ const assignmentDir2 = resolve12(assignmentsDir, id2);
6010
+ if (await fileExists(assignmentDir2)) {
6011
+ res.status(500).json({ error: "UUID collision \u2014 try again" });
6012
+ return;
6013
+ }
6014
+ const timestamp2 = fields.created || nowTimestamp();
6015
+ await ensureDir(assignmentDir2);
6016
+ const normalizedContent = setTopLevelField(rawContent, "id", id2);
6017
+ await writeFileForce(resolve12(assignmentDir2, "assignment.md"), normalizedContent);
6018
+ await writeFileForce(
6019
+ resolve12(assignmentDir2, "scratchpad.md"),
6020
+ renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
6021
+ );
6022
+ await writeFileForce(
6023
+ resolve12(assignmentDir2, "handoff.md"),
6024
+ renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
6025
+ );
6026
+ await writeFileForce(
6027
+ resolve12(assignmentDir2, "decision-record.md"),
6028
+ renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
6029
+ );
6030
+ await writeFileForce(
6031
+ resolve12(assignmentDir2, "progress.md"),
6032
+ renderProgress({ assignment: id2, timestamp: timestamp2 })
6033
+ );
6034
+ await writeFileForce(
6035
+ resolve12(assignmentDir2, "comments.md"),
6036
+ renderComments({ assignment: id2, timestamp: timestamp2 })
6037
+ );
6038
+ const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir, id2);
6039
+ res.status(201).json({ assignment: detail2 });
6040
+ return;
6041
+ }
5719
6042
  const { title, slug, priority, type } = req.body || {};
5720
6043
  if (!title || typeof title !== "string" || !title.trim()) {
5721
6044
  res.status(400).json({ error: "title is required" });
@@ -5727,7 +6050,7 @@ ${entry}`;
5727
6050
  return;
5728
6051
  }
5729
6052
  const id = generateId();
5730
- const assignmentDir = resolve11(assignmentsDir, id);
6053
+ const assignmentDir = resolve12(assignmentsDir, id);
5731
6054
  if (await fileExists(assignmentDir)) {
5732
6055
  res.status(500).json({ error: "UUID collision \u2014 try again" });
5733
6056
  return;
@@ -5747,25 +6070,25 @@ ${entry}`;
5747
6070
  project: null,
5748
6071
  type: typeof type === "string" ? type : void 0
5749
6072
  });
5750
- await writeFileForce(resolve11(assignmentDir, "assignment.md"), assignmentContent);
6073
+ await writeFileForce(resolve12(assignmentDir, "assignment.md"), assignmentContent);
5751
6074
  await writeFileForce(
5752
- resolve11(assignmentDir, "scratchpad.md"),
6075
+ resolve12(assignmentDir, "scratchpad.md"),
5753
6076
  renderScratchpad({ assignmentSlug: id, timestamp })
5754
6077
  );
5755
6078
  await writeFileForce(
5756
- resolve11(assignmentDir, "handoff.md"),
6079
+ resolve12(assignmentDir, "handoff.md"),
5757
6080
  renderHandoff({ assignmentSlug: id, timestamp })
5758
6081
  );
5759
6082
  await writeFileForce(
5760
- resolve11(assignmentDir, "decision-record.md"),
6083
+ resolve12(assignmentDir, "decision-record.md"),
5761
6084
  renderDecisionRecord({ assignmentSlug: id, timestamp })
5762
6085
  );
5763
6086
  await writeFileForce(
5764
- resolve11(assignmentDir, "progress.md"),
6087
+ resolve12(assignmentDir, "progress.md"),
5765
6088
  renderProgress({ assignment: id, timestamp })
5766
6089
  );
5767
6090
  await writeFileForce(
5768
- resolve11(assignmentDir, "comments.md"),
6091
+ resolve12(assignmentDir, "comments.md"),
5769
6092
  renderComments({ assignment: id, timestamp })
5770
6093
  );
5771
6094
  const detail = await getAssignmentDetailById(projectsDir, assignmentsDir, id);
@@ -5893,7 +6216,7 @@ ${entry}`;
5893
6216
  res.status(404).json({ error: `Assignment "${id}" not found` });
5894
6217
  return;
5895
6218
  }
5896
- const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
6219
+ const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
5897
6220
  const currentContent = await readCurrentDocument(assignmentPath);
5898
6221
  if (!currentContent) {
5899
6222
  res.status(404).json({ error: "Assignment not found" });
@@ -5935,7 +6258,7 @@ ${entry}`;
5935
6258
  res.status(404).json({ error: `Assignment "${id}" not found` });
5936
6259
  return;
5937
6260
  }
5938
- const planPath = resolve11(resolved.assignmentDir, "plan.md");
6261
+ const planPath = resolve12(resolved.assignmentDir, "plan.md");
5939
6262
  const currentContent = await readCurrentDocument(planPath);
5940
6263
  if (!currentContent) {
5941
6264
  res.status(404).json({ error: "Plan not found" });
@@ -5969,7 +6292,7 @@ ${entry}`;
5969
6292
  res.status(404).json({ error: `Assignment "${id}" not found` });
5970
6293
  return;
5971
6294
  }
5972
- const scratchpadPath = resolve11(resolved.assignmentDir, "scratchpad.md");
6295
+ const scratchpadPath = resolve12(resolved.assignmentDir, "scratchpad.md");
5973
6296
  const currentContent = await readCurrentDocument(scratchpadPath);
5974
6297
  if (!currentContent) {
5975
6298
  res.status(404).json({ error: "Scratchpad not found" });
@@ -6003,7 +6326,7 @@ ${entry}`;
6003
6326
  res.status(404).json({ error: `Assignment "${id}" not found` });
6004
6327
  return;
6005
6328
  }
6006
- const handoffPath = resolve11(resolved.assignmentDir, "handoff.md");
6329
+ const handoffPath = resolve12(resolved.assignmentDir, "handoff.md");
6007
6330
  const currentContent = await readCurrentDocument(handoffPath);
6008
6331
  if (!currentContent) {
6009
6332
  res.status(404).json({ error: "Handoff log not found" });
@@ -6043,7 +6366,7 @@ ${entry}`;
6043
6366
  res.status(404).json({ error: `Assignment "${id}" not found` });
6044
6367
  return;
6045
6368
  }
6046
- const decisionPath = resolve11(resolved.assignmentDir, "decision-record.md");
6369
+ const decisionPath = resolve12(resolved.assignmentDir, "decision-record.md");
6047
6370
  const currentContent = await readCurrentDocument(decisionPath);
6048
6371
  if (!currentContent) {
6049
6372
  res.status(404).json({ error: "Decision record not found" });
@@ -6083,7 +6406,7 @@ ${entry}`;
6083
6406
  res.status(404).json({ error: `Assignment "${id}" not found` });
6084
6407
  return;
6085
6408
  }
6086
- const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
6409
+ const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
6087
6410
  if (!await fileExists(assignmentPath)) {
6088
6411
  res.status(404).json({ error: "Assignment not found" });
6089
6412
  return;
@@ -6095,7 +6418,7 @@ ${entry}`;
6095
6418
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
6096
6419
  return;
6097
6420
  }
6098
- let content = await readFile9(assignmentPath, "utf-8");
6421
+ let content = await readFile10(assignmentPath, "utf-8");
6099
6422
  content = setTopLevelField(content, "status", status);
6100
6423
  content = setTopLevelField(content, "updated", nowTimestamp());
6101
6424
  if (status !== "blocked") {
@@ -6121,7 +6444,7 @@ ${entry}`;
6121
6444
  res.status(404).json({ error: `Assignment "${id}" not found` });
6122
6445
  return;
6123
6446
  }
6124
- const assignmentPath = resolve11(resolved.assignmentDir, "assignment.md");
6447
+ const assignmentPath = resolve12(resolved.assignmentDir, "assignment.md");
6125
6448
  const currentContent = await readCurrentDocument(assignmentPath);
6126
6449
  if (!currentContent) {
6127
6450
  res.status(404).json({ error: "Assignment not found" });
@@ -6186,7 +6509,7 @@ function slugifyLocal(input) {
6186
6509
  return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
6187
6510
  }
6188
6511
  async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
6189
- const commentsPath = resolve11(assignmentDir, "comments.md");
6512
+ const commentsPath = resolve12(assignmentDir, "comments.md");
6190
6513
  const { body, author, type, replyTo } = req.body || {};
6191
6514
  if (!body || typeof body !== "string" || !body.trim()) {
6192
6515
  res.status(400).json({ error: "body is required" });
@@ -6198,7 +6521,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
6198
6521
  let currentContent;
6199
6522
  let currentCount = 0;
6200
6523
  if (await fileExists(commentsPath)) {
6201
- currentContent = await readFile9(commentsPath, "utf-8");
6524
+ currentContent = await readFile10(commentsPath, "utf-8");
6202
6525
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
6203
6526
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
6204
6527
  } else {
@@ -6228,7 +6551,7 @@ ${entry}`;
6228
6551
  res.status(201).json({ assignment, comment: { id: comment.id } });
6229
6552
  }
6230
6553
  async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
6231
- const commentsPath = resolve11(assignmentDir, "comments.md");
6554
+ const commentsPath = resolve12(assignmentDir, "comments.md");
6232
6555
  if (!await fileExists(commentsPath)) {
6233
6556
  res.status(404).json({ error: "Comments file not found" });
6234
6557
  return;
@@ -6238,7 +6561,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
6238
6561
  res.status(400).json({ error: "resolved (boolean) is required" });
6239
6562
  return;
6240
6563
  }
6241
- const content = await readFile9(commentsPath, "utf-8");
6564
+ const content = await readFile10(commentsPath, "utf-8");
6242
6565
  const parsed = parseComments(content);
6243
6566
  const target = parsed.entries.find((e) => e.id === commentId);
6244
6567
  if (!target) {
@@ -6383,7 +6706,7 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir) {
6383
6706
 
6384
6707
  // src/dashboard/api-agent-sessions.ts
6385
6708
  import { Router as Router3 } from "express";
6386
- import { resolve as resolve12 } from "path";
6709
+ import { resolve as resolve13 } from "path";
6387
6710
  init_fs();
6388
6711
  function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6389
6712
  const router = Router3();
@@ -6400,7 +6723,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6400
6723
  try {
6401
6724
  const { projectSlug } = req.params;
6402
6725
  const assignment = req.query.assignment;
6403
- const projectDir = resolve12(projectsDir, projectSlug);
6726
+ const projectDir = resolve13(projectsDir, projectSlug);
6404
6727
  if (!await fileExists(projectDir)) {
6405
6728
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
6406
6729
  return;
@@ -6426,7 +6749,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir) {
6426
6749
  return;
6427
6750
  }
6428
6751
  if (projectSlug) {
6429
- const projectDir = resolve12(projectsDir, projectSlug);
6752
+ const projectDir = resolve13(projectsDir, projectSlug);
6430
6753
  if (!await fileExists(projectDir)) {
6431
6754
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
6432
6755
  return;
@@ -6498,53 +6821,7 @@ import { resolve as resolve14 } from "path";
6498
6821
  import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
6499
6822
  init_timestamp();
6500
6823
  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
6824
+ init_playbooks();
6548
6825
  function createPlaybooksRouter(playbooksDir2) {
6549
6826
  const router = Router4();
6550
6827
  router.get("/", async (_req, res) => {
@@ -6568,6 +6845,32 @@ function createPlaybooksRouter(playbooksDir2) {
6568
6845
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get template" });
6569
6846
  }
6570
6847
  });
6848
+ router.post("/:slug/enable", async (req, res) => {
6849
+ try {
6850
+ const result = await setPlaybookEnabled(playbooksDir2, req.params.slug, true);
6851
+ res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
6852
+ } catch (error) {
6853
+ const msg = error instanceof Error ? error.message : "Failed to enable playbook";
6854
+ if (msg.startsWith("Playbook ")) {
6855
+ res.status(404).json({ error: msg });
6856
+ return;
6857
+ }
6858
+ res.status(500).json({ error: msg });
6859
+ }
6860
+ });
6861
+ router.post("/:slug/disable", async (req, res) => {
6862
+ try {
6863
+ const result = await setPlaybookEnabled(playbooksDir2, req.params.slug, false);
6864
+ res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
6865
+ } catch (error) {
6866
+ const msg = error instanceof Error ? error.message : "Failed to disable playbook";
6867
+ if (msg.startsWith("Playbook ")) {
6868
+ res.status(404).json({ error: msg });
6869
+ return;
6870
+ }
6871
+ res.status(500).json({ error: msg });
6872
+ }
6873
+ });
6571
6874
  router.get("/:slug", async (req, res) => {
6572
6875
  try {
6573
6876
  const detail = await getPlaybookDetail(playbooksDir2, req.params.slug);
@@ -6582,17 +6885,18 @@ function createPlaybooksRouter(playbooksDir2) {
6582
6885
  });
6583
6886
  router.get("/:slug/edit", async (req, res) => {
6584
6887
  try {
6585
- const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6586
- if (!await fileExists(filePath)) {
6888
+ const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
6889
+ if (!resolved) {
6587
6890
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6588
6891
  return;
6589
6892
  }
6893
+ const filePath = resolve14(playbooksDir2, resolved.filename);
6590
6894
  const content = await readFile11(filePath, "utf-8");
6591
6895
  res.json({
6592
6896
  documentType: "playbook",
6593
- title: `Edit Playbook: ${req.params.slug}`,
6897
+ title: `Edit Playbook: ${resolved.slug}`,
6594
6898
  content,
6595
- slug: req.params.slug
6899
+ slug: resolved.slug
6596
6900
  });
6597
6901
  } catch (error) {
6598
6902
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get playbook for editing" });
@@ -6631,14 +6935,15 @@ function createPlaybooksRouter(playbooksDir2) {
6631
6935
  res.status(400).json({ error: "content is required" });
6632
6936
  return;
6633
6937
  }
6634
- const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6635
- if (!await fileExists(filePath)) {
6938
+ const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
6939
+ if (!resolved) {
6636
6940
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6637
6941
  return;
6638
6942
  }
6943
+ const filePath = resolve14(playbooksDir2, resolved.filename);
6639
6944
  await writeFileForce(filePath, content);
6640
6945
  await rebuildPlaybookManifest(playbooksDir2);
6641
- res.json({ slug: req.params.slug, path: filePath });
6946
+ res.json({ slug: resolved.slug, path: filePath });
6642
6947
  } catch (error) {
6643
6948
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
6644
6949
  }
@@ -6649,14 +6954,16 @@ function createPlaybooksRouter(playbooksDir2) {
6649
6954
  res.status(403).json({ error: "The playbook manifest cannot be deleted" });
6650
6955
  return;
6651
6956
  }
6652
- const filePath = resolve14(playbooksDir2, `${req.params.slug}.md`);
6653
- if (!await fileExists(filePath)) {
6957
+ const resolved = await resolvePlaybookSlug(playbooksDir2, req.params.slug);
6958
+ if (!resolved) {
6654
6959
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6655
6960
  return;
6656
6961
  }
6962
+ const filePath = resolve14(playbooksDir2, resolved.filename);
6657
6963
  await unlink2(filePath);
6964
+ await removeFromDisabledList(resolved.slug);
6658
6965
  await rebuildPlaybookManifest(playbooksDir2);
6659
- res.json({ deleted: req.params.slug });
6966
+ res.json({ deleted: resolved.slug });
6660
6967
  } catch (error) {
6661
6968
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
6662
6969
  }
@@ -7836,7 +8143,7 @@ function createDashboardServer(options) {
7836
8143
  });
7837
8144
  app.get("/api/workspaces", async (_req, res) => {
7838
8145
  try {
7839
- const result = await listWorkspaces(projectsDir);
8146
+ const result = await listWorkspaces(projectsDir, assignmentsDir);
7840
8147
  res.json(result);
7841
8148
  } catch (error) {
7842
8149
  console.error("Error listing workspaces:", error);
@@ -7955,16 +8262,25 @@ function createDashboardServer(options) {
7955
8262
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
7956
8263
  app.use("/api/backup", createBackupRouter());
7957
8264
  if (serveStaticUi && dashboardDistPath) {
7958
- app.use(express.static(dashboardDistPath));
7959
- app.get("{*path}", async (_req, res) => {
8265
+ app.use("/assets", express.static(resolve17(dashboardDistPath, "assets")));
8266
+ app.get("{*path}", async (req, res) => {
8267
+ if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
8268
+ res.status(404).json({ error: "Not Found" });
8269
+ return;
8270
+ }
7960
8271
  const indexPath = resolve17(dashboardDistPath, "index.html");
7961
- if (await fileExists(indexPath)) {
7962
- res.sendFile(indexPath);
7963
- } else {
8272
+ if (!await fileExists(indexPath)) {
7964
8273
  res.status(503).send(
7965
8274
  'Dashboard not built. Run "npm run build:dashboard" first.'
7966
8275
  );
8276
+ return;
7967
8277
  }
8278
+ res.sendFile(indexPath, (err) => {
8279
+ if (err) {
8280
+ console.error("Error sending dashboard index.html:", err);
8281
+ if (!res.headersSent) res.status(500).send("Dashboard load error");
8282
+ }
8283
+ });
7968
8284
  });
7969
8285
  }
7970
8286
  let watcherHandle = null;