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
package/dist/index.js CHANGED
@@ -247,6 +247,7 @@ function parseAssignmentFull(fileContent) {
247
247
  slug: getField(fm, "slug") ?? "",
248
248
  title: getField(fm, "title") ?? "",
249
249
  project: getField(fm, "project"),
250
+ workspaceGroup: getField(fm, "workspaceGroup"),
250
251
  type: getField(fm, "type"),
251
252
  status: getField(fm, "status") ?? "pending",
252
253
  priority: getField(fm, "priority") ?? "medium",
@@ -414,8 +415,8 @@ var init_timestamp = __esm({
414
415
  });
415
416
 
416
417
  // src/utils/fs-migration.ts
417
- import { readdir as readdir3, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "fs/promises";
418
- import { resolve as resolve4 } from "path";
418
+ import { readdir, readFile, rename as rename2, writeFile as writeFile2 } from "fs/promises";
419
+ import { resolve as resolve2 } from "path";
419
420
  async function migrateLegacyProjectFiles(projectsDir2) {
420
421
  const result = {
421
422
  renamedProjectFiles: [],
@@ -424,15 +425,15 @@ async function migrateLegacyProjectFiles(projectsDir2) {
424
425
  if (!await fileExists(projectsDir2)) return result;
425
426
  let entries;
426
427
  try {
427
- entries = await readdir3(projectsDir2, { withFileTypes: true });
428
+ entries = await readdir(projectsDir2, { withFileTypes: true });
428
429
  } catch {
429
430
  return result;
430
431
  }
431
432
  for (const entry of entries) {
432
433
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
433
- const projectDir = resolve4(projectsDir2, entry.name);
434
- const legacy = resolve4(projectDir, "mission.md");
435
- const target = resolve4(projectDir, "project.md");
434
+ const projectDir = resolve2(projectsDir2, entry.name);
435
+ const legacy = resolve2(projectDir, "mission.md");
436
+ const target = resolve2(projectDir, "project.md");
436
437
  try {
437
438
  if (await fileExists(legacy) && !await fileExists(target)) {
438
439
  await rename2(legacy, target);
@@ -443,7 +444,7 @@ async function migrateLegacyProjectFiles(projectsDir2) {
443
444
  }
444
445
  for (const stale of ["agent.md", "claude.md"]) {
445
446
  try {
446
- if (await fileExists(resolve4(projectDir, stale))) {
447
+ if (await fileExists(resolve2(projectDir, stale))) {
447
448
  result.legacyExtras.push(`${entry.name}/${stale}`);
448
449
  }
449
450
  } catch {
@@ -461,7 +462,7 @@ async function migrateLegacyConfig(configPath) {
461
462
  if (!await fileExists(configPath)) return result;
462
463
  let content;
463
464
  try {
464
- content = await readFile3(configPath, "utf-8");
465
+ content = await readFile(configPath, "utf-8");
465
466
  } catch {
466
467
  return result;
467
468
  }
@@ -490,7 +491,7 @@ async function migrateLegacyConfig(configPath) {
490
491
  const projectLineRe = /^\s*defaultProjectDir\s*:\s*(.*)$/m;
491
492
  const projectLineMatch = newFmBlock.match(projectLineRe);
492
493
  const projectsDirRaw = projectLineMatch ? projectLineMatch[1].trim().replace(/^['"]|['"]$/g, "") : missionValue;
493
- const expand = (p) => p.startsWith("~") ? resolve4(process.env.HOME ?? "/", p.slice(p.startsWith("~/") ? 2 : 1)) : p;
494
+ const expand = (p) => p.startsWith("~") ? resolve2(process.env.HOME ?? "/", p.slice(p.startsWith("~/") ? 2 : 1)) : p;
494
495
  let resolvedProjectsDir = projectsDirRaw ? expand(projectsDirRaw) : null;
495
496
  if (resolvedProjectsDir && resolvedProjectsDir.endsWith("/missions")) {
496
497
  const siblingProjectsDir = resolvedProjectsDir.replace(/\/missions$/, "/projects");
@@ -549,8 +550,29 @@ var init_fs_migration = __esm({
549
550
  });
550
551
 
551
552
  // src/utils/config.ts
552
- import { readFile as readFile4 } from "fs/promises";
553
- import { resolve as resolve5, isAbsolute } from "path";
553
+ import { readFile as readFile2 } from "fs/promises";
554
+ import { resolve as resolve3, isAbsolute } from "path";
555
+ function cloneDefaultConfig() {
556
+ return {
557
+ ...DEFAULT_CONFIG,
558
+ onboarding: { ...DEFAULT_CONFIG.onboarding },
559
+ agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
560
+ integrations: { ...DEFAULT_CONFIG.integrations },
561
+ backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
562
+ statuses: DEFAULT_CONFIG.statuses ? {
563
+ statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
564
+ order: [...DEFAULT_CONFIG.statuses.order],
565
+ transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
566
+ } : null,
567
+ types: DEFAULT_CONFIG.types ? {
568
+ definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
569
+ default: DEFAULT_CONFIG.types.default
570
+ } : null,
571
+ playbooks: {
572
+ disabled: [...DEFAULT_CONFIG.playbooks.disabled]
573
+ }
574
+ };
575
+ }
554
576
  function parseFrontmatter(content) {
555
577
  const match = content.match(/^---\n([\s\S]*?)\n---/);
556
578
  if (!match) return {};
@@ -722,6 +744,82 @@ function serializeBackupConfig(backup) {
722
744
  lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
723
745
  return lines.join("\n");
724
746
  }
747
+ function serializePlaybooksConfig(playbooks) {
748
+ if (!playbooks.disabled || playbooks.disabled.length === 0) {
749
+ return null;
750
+ }
751
+ const lines = ["playbooks:", " disabled:"];
752
+ for (const slug of playbooks.disabled) {
753
+ lines.push(` - ${slug}`);
754
+ }
755
+ return lines.join("\n");
756
+ }
757
+ function parsePlaybooksConfig(fmBlock) {
758
+ const blockStart = fmBlock.match(/^playbooks:\s*$/m);
759
+ if (!blockStart) {
760
+ return { disabled: [] };
761
+ }
762
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
763
+ const remaining = fmBlock.slice(startIdx).split("\n");
764
+ const disabled = [];
765
+ let currentSection = null;
766
+ for (const line of remaining) {
767
+ const trimmed = line.trimStart();
768
+ const indent = line.length - trimmed.length;
769
+ if (indent === 0 && trimmed.length > 0) break;
770
+ if (trimmed === "") continue;
771
+ if (indent === 2 && trimmed.startsWith("disabled:")) {
772
+ currentSection = "disabled";
773
+ const afterColon = trimmed.slice("disabled:".length).trim();
774
+ if (afterColon === "[]" || afterColon === "") {
775
+ continue;
776
+ }
777
+ continue;
778
+ }
779
+ if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
780
+ const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
781
+ if (raw.length === 0) continue;
782
+ if (/\s/.test(raw)) {
783
+ console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
784
+ continue;
785
+ }
786
+ disabled.push(raw);
787
+ continue;
788
+ }
789
+ }
790
+ return { disabled };
791
+ }
792
+ async function updatePlaybooksConfig(playbooks) {
793
+ const configPath = resolve3(syntaurRoot(), "config.md");
794
+ const current = (await readConfig()).playbooks;
795
+ const nextPlaybooks = {
796
+ disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
797
+ };
798
+ const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
799
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
800
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
801
+ if (!fmMatch) {
802
+ const bodyBlock = playbooksBlock ? `${playbooksBlock}
803
+ ` : "";
804
+ const content = `---
805
+ version: "2.0"
806
+ defaultProjectDir: ${defaultProjectDir()}
807
+ ${bodyBlock}---
808
+ ${existing}`;
809
+ await writeFileForce(configPath, content);
810
+ return;
811
+ }
812
+ const fmBlock = fmMatch[2];
813
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
814
+ const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
815
+ const newFm = playbooksBlock ? `${cleanedFm}
816
+ ${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
817
+ const normalizedFm = newFm.replace(/\n+$/, "");
818
+ const newContent = `---
819
+ ${normalizedFm}
820
+ ---${afterFrontmatter}`;
821
+ await writeFileForce(configPath, newContent);
822
+ }
725
823
  function stripTopLevelBlock(fmBlock, key) {
726
824
  const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
727
825
  if (!blockStart) {
@@ -756,10 +854,10 @@ function parseOptionalAbsolutePath(value, fieldName) {
756
854
  );
757
855
  return null;
758
856
  }
759
- return resolve5(expanded);
857
+ return resolve3(expanded);
760
858
  }
761
859
  async function writeStatusConfig(statuses) {
762
- const configPath = resolve5(syntaurRoot(), "config.md");
860
+ const configPath = resolve3(syntaurRoot(), "config.md");
763
861
  const statusBlock = serializeStatusConfig(statuses);
764
862
  if (!await fileExists(configPath)) {
765
863
  const content = `---
@@ -771,7 +869,7 @@ ${statusBlock}
771
869
  await writeFileForce(configPath, content);
772
870
  return;
773
871
  }
774
- const existing = await readFile4(configPath, "utf-8");
872
+ const existing = await readFile2(configPath, "utf-8");
775
873
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
776
874
  if (!fmMatch) {
777
875
  const content = `---
@@ -813,9 +911,9 @@ ${statusBlock}
813
911
  await writeFileForce(configPath, newContent);
814
912
  }
815
913
  async function deleteStatusConfig() {
816
- const configPath = resolve5(syntaurRoot(), "config.md");
914
+ const configPath = resolve3(syntaurRoot(), "config.md");
817
915
  if (!await fileExists(configPath)) return;
818
- const existing = await readFile4(configPath, "utf-8");
916
+ const existing = await readFile2(configPath, "utf-8");
819
917
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
820
918
  if (!fmMatch) return;
821
919
  const fmBlock = fmMatch[2];
@@ -827,13 +925,13 @@ ${cleanedFm}
827
925
  await writeFileForce(configPath, newContent);
828
926
  }
829
927
  async function updateIntegrationConfig(integrations) {
830
- const configPath = resolve5(syntaurRoot(), "config.md");
928
+ const configPath = resolve3(syntaurRoot(), "config.md");
831
929
  const nextIntegrations = {
832
930
  ...(await readConfig()).integrations,
833
931
  ...integrations
834
932
  };
835
933
  const integrationBlock = serializeIntegrationConfig(nextIntegrations);
836
- const existing = await fileExists(configPath) ? await readFile4(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
934
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
837
935
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
838
936
  if (!fmMatch) {
839
937
  const content = `---
@@ -857,13 +955,13 @@ ${normalizedFm}
857
955
  await writeFileForce(configPath, newContent);
858
956
  }
859
957
  async function updateOnboardingConfig(onboarding) {
860
- const configPath = resolve5(syntaurRoot(), "config.md");
958
+ const configPath = resolve3(syntaurRoot(), "config.md");
861
959
  const nextOnboarding = {
862
960
  ...(await readConfig()).onboarding,
863
961
  ...onboarding
864
962
  };
865
963
  const onboardingBlock = serializeOnboardingConfig(nextOnboarding);
866
- const existing = await fileExists(configPath) ? await readFile4(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
964
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
867
965
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
868
966
  if (!fmMatch) {
869
967
  const content = `---
@@ -887,7 +985,7 @@ ${normalizedFm}
887
985
  await writeFileForce(configPath, newContent);
888
986
  }
889
987
  async function updateBackupConfig(backup) {
890
- const configPath = resolve5(syntaurRoot(), "config.md");
988
+ const configPath = resolve3(syntaurRoot(), "config.md");
891
989
  const current = (await readConfig()).backup;
892
990
  const nextBackup = {
893
991
  repo: current?.repo ?? null,
@@ -897,7 +995,7 @@ async function updateBackupConfig(backup) {
897
995
  ...backup
898
996
  };
899
997
  const backupBlock = serializeBackupConfig(nextBackup);
900
- const existing = await fileExists(configPath) ? await readFile4(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
998
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
901
999
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
902
1000
  if (!fmMatch) {
903
1001
  const content = `---
@@ -921,19 +1019,19 @@ ${normalizedFm}
921
1019
  await writeFileForce(configPath, newContent);
922
1020
  }
923
1021
  async function readConfig() {
924
- const configPath = resolve5(syntaurRoot(), "config.md");
1022
+ const configPath = resolve3(syntaurRoot(), "config.md");
925
1023
  if (!await fileExists(configPath)) {
926
- return { ...DEFAULT_CONFIG };
1024
+ return cloneDefaultConfig();
927
1025
  }
928
1026
  if (!migratedConfigPaths.has(configPath)) {
929
1027
  migratedConfigPaths.add(configPath);
930
1028
  await migrateLegacyConfig(configPath);
931
1029
  }
932
- const content = await readFile4(configPath, "utf-8");
1030
+ const content = await readFile2(configPath, "utf-8");
933
1031
  const fm = parseFrontmatter(content);
934
1032
  if (Object.keys(fm).length === 0) {
935
1033
  console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
936
- return { ...DEFAULT_CONFIG };
1034
+ return cloneDefaultConfig();
937
1035
  }
938
1036
  let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
939
1037
  if (!isAbsolute(projectDir)) {
@@ -942,6 +1040,7 @@ async function readConfig() {
942
1040
  );
943
1041
  projectDir = DEFAULT_CONFIG.defaultProjectDir;
944
1042
  }
1043
+ const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
945
1044
  return {
946
1045
  version: fm["version"] || DEFAULT_CONFIG.version,
947
1046
  defaultProjectDir: projectDir,
@@ -973,7 +1072,8 @@ async function readConfig() {
973
1072
  lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
974
1073
  } : null,
975
1074
  statuses: parseStatusConfig(content),
976
- types: null
1075
+ types: null,
1076
+ playbooks: parsePlaybooksConfig(fmBlock)
977
1077
  };
978
1078
  }
979
1079
  var DEFAULT_CONFIG, migratedConfigPaths;
@@ -1001,12 +1101,119 @@ var init_config2 = __esm({
1001
1101
  },
1002
1102
  backup: null,
1003
1103
  statuses: null,
1004
- types: null
1104
+ types: null,
1105
+ playbooks: {
1106
+ disabled: []
1107
+ }
1005
1108
  };
1006
1109
  migratedConfigPaths = /* @__PURE__ */ new Set();
1007
1110
  }
1008
1111
  });
1009
1112
 
1113
+ // src/utils/playbooks.ts
1114
+ import { resolve as resolve4 } from "path";
1115
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
1116
+ function isVisiblePlaybookFile(name, isFile) {
1117
+ return isFile && name.endsWith(".md") && !name.startsWith("_") && name !== "manifest.md";
1118
+ }
1119
+ async function resolvePlaybookSlug(playbooksDir3, slug) {
1120
+ if (!await fileExists(playbooksDir3)) return null;
1121
+ const entries = await readdir2(playbooksDir3, { withFileTypes: true });
1122
+ let filenameStemFallback = null;
1123
+ for (const entry of entries) {
1124
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
1125
+ const filePath = resolve4(playbooksDir3, entry.name);
1126
+ const raw = await readFile3(filePath, "utf-8");
1127
+ const parsed = parsePlaybook(raw);
1128
+ const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
1129
+ if (canonical === slug) {
1130
+ return { filename: entry.name, slug: canonical, parsed };
1131
+ }
1132
+ if (!parsed.slug && entry.name.replace(/\.md$/, "") === slug) {
1133
+ filenameStemFallback = { filename: entry.name, slug: canonical, parsed };
1134
+ }
1135
+ }
1136
+ return filenameStemFallback;
1137
+ }
1138
+ async function setPlaybookEnabled(playbooksDir3, slug, enabled) {
1139
+ const resolved = await resolvePlaybookSlug(playbooksDir3, slug);
1140
+ if (!resolved) {
1141
+ throw new Error(`Playbook "${slug}" not found in ${playbooksDir3}`);
1142
+ }
1143
+ const config = await readConfig();
1144
+ const disabledSet = new Set(config.playbooks.disabled);
1145
+ const wasDisabled = disabledSet.has(resolved.slug);
1146
+ const shouldBeDisabled = !enabled;
1147
+ if (wasDisabled === shouldBeDisabled) {
1148
+ return { slug: resolved.slug, enabled, changed: false };
1149
+ }
1150
+ if (shouldBeDisabled) {
1151
+ disabledSet.add(resolved.slug);
1152
+ } else {
1153
+ disabledSet.delete(resolved.slug);
1154
+ }
1155
+ await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });
1156
+ await rebuildPlaybookManifest(playbooksDir3);
1157
+ return { slug: resolved.slug, enabled, changed: true };
1158
+ }
1159
+ async function removeFromDisabledList(slug) {
1160
+ const config = await readConfig();
1161
+ if (!config.playbooks.disabled.includes(slug)) return;
1162
+ await updatePlaybooksConfig({
1163
+ disabled: config.playbooks.disabled.filter((s) => s !== slug)
1164
+ });
1165
+ }
1166
+ async function rebuildPlaybookManifest(playbooksDir3) {
1167
+ if (!await fileExists(playbooksDir3)) return;
1168
+ const config = await readConfig();
1169
+ const disabledSet = new Set(config.playbooks.disabled);
1170
+ const entries = await readdir2(playbooksDir3, { withFileTypes: true });
1171
+ const rows = [];
1172
+ for (const entry of entries) {
1173
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
1174
+ const raw = await readFile3(resolve4(playbooksDir3, entry.name), "utf-8");
1175
+ const parsed = parsePlaybook(raw);
1176
+ const slug = parsed.slug || entry.name.replace(/\.md$/, "");
1177
+ if (disabledSet.has(slug)) continue;
1178
+ rows.push({
1179
+ name: parsed.name || slug,
1180
+ slug,
1181
+ description: parsed.description,
1182
+ whenToUse: parsed.whenToUse
1183
+ });
1184
+ }
1185
+ rows.sort((a, b) => a.name.localeCompare(b.name));
1186
+ const timestamp = nowTimestamp();
1187
+ const lines = [
1188
+ "---",
1189
+ `generated: "${timestamp}"`,
1190
+ `total: ${rows.length}`,
1191
+ "---",
1192
+ "",
1193
+ "# Playbooks",
1194
+ "",
1195
+ "Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
1196
+ ""
1197
+ ];
1198
+ for (const row of rows) {
1199
+ lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
1200
+ if (row.whenToUse) {
1201
+ lines.push(` _When to use: ${row.whenToUse}_`);
1202
+ }
1203
+ }
1204
+ lines.push("");
1205
+ await writeFileForce(resolve4(playbooksDir3, "manifest.md"), lines.join("\n"));
1206
+ }
1207
+ var init_playbooks = __esm({
1208
+ "src/utils/playbooks.ts"() {
1209
+ "use strict";
1210
+ init_fs();
1211
+ init_parser();
1212
+ init_timestamp();
1213
+ init_config2();
1214
+ }
1215
+ });
1216
+
1010
1217
  // src/lifecycle/types.ts
1011
1218
  var DEFAULT_STATUSES, DEFAULT_TERMINAL_STATUSES;
1012
1219
  var init_types = __esm({
@@ -1409,12 +1616,20 @@ async function resolveAssignmentById(projectsDir2, assignmentsDir2, id) {
1409
1616
  const standaloneDir = resolve9(assignmentsDir2, id);
1410
1617
  const standalonePath = resolve9(standaloneDir, "assignment.md");
1411
1618
  if (await fileExists(standalonePath)) {
1619
+ let workspaceGroup = null;
1620
+ try {
1621
+ const content = await readFile6(standalonePath, "utf-8");
1622
+ const [fm] = extractFrontmatter(content);
1623
+ workspaceGroup = getField(fm, "workspaceGroup");
1624
+ } catch {
1625
+ }
1412
1626
  standaloneMatch = {
1413
1627
  assignmentDir: standaloneDir,
1414
1628
  projectSlug: null,
1415
1629
  assignmentSlug: id,
1416
1630
  id,
1417
- standalone: true
1631
+ standalone: true,
1632
+ workspaceGroup
1418
1633
  };
1419
1634
  }
1420
1635
  if (await fileExists(projectsDir2)) {
@@ -1440,7 +1655,8 @@ async function resolveAssignmentById(projectsDir2, assignmentsDir2, id) {
1440
1655
  projectSlug: p.name,
1441
1656
  assignmentSlug: a.name,
1442
1657
  id,
1443
- standalone: false
1658
+ standalone: false,
1659
+ workspaceGroup: null
1444
1660
  };
1445
1661
  break;
1446
1662
  }
@@ -1816,8 +2032,18 @@ var init_help = __esm({
1816
2032
  },
1817
2033
  {
1818
2034
  command: "syntaur list-playbooks",
1819
- description: "List all playbooks in the Syntaur home directory.",
1820
- example: "syntaur list-playbooks"
2035
+ description: "List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.",
2036
+ example: "syntaur list-playbooks --all"
2037
+ },
2038
+ {
2039
+ command: "syntaur enable-playbook",
2040
+ description: "Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.",
2041
+ example: "syntaur enable-playbook commit-discipline"
2042
+ },
2043
+ {
2044
+ command: "syntaur disable-playbook",
2045
+ description: "Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.",
2046
+ example: "syntaur disable-playbook commit-discipline"
1821
2047
  }
1822
2048
  ];
1823
2049
  WORKFLOW = [
@@ -2541,10 +2767,11 @@ async function writeWorkspaceRegistry(projectsDir2, workspaces) {
2541
2767
  const registryPath = resolve12(dirname3(projectsDir2), "workspaces.json");
2542
2768
  await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
2543
2769
  }
2544
- async function listWorkspaces(projectsDir2) {
2545
- const [projectRecords, registered] = await Promise.all([
2770
+ async function listWorkspaces(projectsDir2, assignmentsDir2) {
2771
+ const [projectRecords, registered, standaloneRecords] = await Promise.all([
2546
2772
  listProjectRecords(projectsDir2),
2547
- readWorkspaceRegistry(projectsDir2)
2773
+ readWorkspaceRegistry(projectsDir2),
2774
+ listStandaloneRecords(assignmentsDir2)
2548
2775
  ]);
2549
2776
  const workspaceSet = new Set(registered);
2550
2777
  let hasUngrouped = false;
@@ -2555,6 +2782,13 @@ async function listWorkspaces(projectsDir2) {
2555
2782
  hasUngrouped = true;
2556
2783
  }
2557
2784
  }
2785
+ for (const sr of standaloneRecords) {
2786
+ if (sr.record.workspaceGroup) {
2787
+ workspaceSet.add(sr.record.workspaceGroup);
2788
+ } else {
2789
+ hasUngrouped = true;
2790
+ }
2791
+ }
2558
2792
  const workspaces = Array.from(workspaceSet).sort();
2559
2793
  return { workspaces, hasUngrouped };
2560
2794
  }
@@ -2703,7 +2937,7 @@ async function toStandaloneBoardItem(sr) {
2703
2937
  projectSlug: null,
2704
2938
  projectTitle: null,
2705
2939
  blockedReason: sr.record.blockedReason,
2706
- projectWorkspace: null,
2940
+ projectWorkspace: sr.record.workspaceGroup ?? null,
2707
2941
  availableTransitions: await getStandaloneAvailableTransitions(sr.record)
2708
2942
  };
2709
2943
  }
@@ -3644,6 +3878,8 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
3644
3878
  }
3645
3879
  async function listPlaybooks(playbooksDir3) {
3646
3880
  if (!await fileExists(playbooksDir3)) return [];
3881
+ const config = await readConfig();
3882
+ const disabledSet = new Set(config.playbooks.disabled);
3647
3883
  const entries = await readdir7(playbooksDir3, { withFileTypes: true });
3648
3884
  const playbooks = [];
3649
3885
  for (const entry of entries) {
@@ -3659,25 +3895,28 @@ async function listPlaybooks(playbooksDir3) {
3659
3895
  whenToUse: parsed.whenToUse,
3660
3896
  tags: parsed.tags,
3661
3897
  created: parsed.created,
3662
- updated: parsed.updated
3898
+ updated: parsed.updated,
3899
+ enabled: !disabledSet.has(slug)
3663
3900
  });
3664
3901
  }
3665
3902
  return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
3666
3903
  }
3667
3904
  async function getPlaybookDetail(playbooksDir3, slug) {
3668
- const filePath = resolve12(playbooksDir3, `${slug}.md`);
3669
- if (!await fileExists(filePath)) return null;
3670
- const raw = await readFile9(filePath, "utf-8");
3671
- const parsed = parsePlaybook(raw);
3905
+ const resolved = await resolvePlaybookSlug(playbooksDir3, slug);
3906
+ if (!resolved) return null;
3907
+ const config = await readConfig();
3908
+ const enabled = !config.playbooks.disabled.includes(resolved.slug);
3909
+ const parsed = resolved.parsed;
3672
3910
  return {
3673
- slug: parsed.slug || slug,
3674
- name: parsed.name || slug,
3911
+ slug: resolved.slug,
3912
+ name: parsed.name || resolved.slug,
3675
3913
  description: parsed.description,
3676
3914
  whenToUse: parsed.whenToUse,
3677
3915
  tags: parsed.tags,
3678
3916
  created: parsed.created,
3679
3917
  updated: parsed.updated,
3680
- body: parsed.body
3918
+ body: parsed.body,
3919
+ enabled
3681
3920
  };
3682
3921
  }
3683
3922
  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;
@@ -3687,6 +3926,7 @@ var init_api = __esm({
3687
3926
  init_lifecycle();
3688
3927
  init_fs();
3689
3928
  init_config2();
3929
+ init_playbooks();
3690
3930
  init_fs_migration();
3691
3931
  init_assignment_resolver();
3692
3932
  init_parser();
@@ -4613,62 +4853,16 @@ import { Command as Command4 } from "commander";
4613
4853
  init_paths();
4614
4854
  init_fs();
4615
4855
  init_config();
4616
- import { resolve as resolve3, dirname as dirname2 } from "path";
4617
- import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
4856
+ init_playbooks();
4857
+ import { resolve as resolve5, dirname as dirname2 } from "path";
4858
+ import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
4618
4859
  import { fileURLToPath } from "url";
4619
-
4620
- // src/utils/playbooks.ts
4621
- init_fs();
4622
- init_parser();
4623
- init_timestamp();
4624
- import { resolve as resolve2 } from "path";
4625
- import { readdir, readFile } from "fs/promises";
4626
- async function rebuildPlaybookManifest(playbooksDir3) {
4627
- if (!await fileExists(playbooksDir3)) return;
4628
- const entries = await readdir(playbooksDir3, { withFileTypes: true });
4629
- const rows = [];
4630
- for (const entry of entries) {
4631
- if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
4632
- const raw = await readFile(resolve2(playbooksDir3, entry.name), "utf-8");
4633
- const parsed = parsePlaybook(raw);
4634
- const slug = parsed.slug || entry.name.replace(/\.md$/, "");
4635
- rows.push({
4636
- name: parsed.name || slug,
4637
- slug,
4638
- description: parsed.description,
4639
- whenToUse: parsed.whenToUse
4640
- });
4641
- }
4642
- rows.sort((a, b) => a.name.localeCompare(b.name));
4643
- const timestamp = nowTimestamp();
4644
- const lines = [
4645
- "---",
4646
- `generated: "${timestamp}"`,
4647
- `total: ${rows.length}`,
4648
- "---",
4649
- "",
4650
- "# Playbooks",
4651
- "",
4652
- "Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
4653
- ""
4654
- ];
4655
- for (const row of rows) {
4656
- lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
4657
- if (row.whenToUse) {
4658
- lines.push(` _When to use: ${row.whenToUse}_`);
4659
- }
4660
- }
4661
- lines.push("");
4662
- await writeFileForce(resolve2(playbooksDir3, "manifest.md"), lines.join("\n"));
4663
- }
4664
-
4665
- // src/commands/init.ts
4666
4860
  async function initCommand(options) {
4667
4861
  const root = syntaurRoot();
4668
4862
  const projectsDir2 = defaultProjectDir();
4669
4863
  const standaloneAssignmentsDir = assignmentsDir();
4670
- const configPath = resolve3(root, "config.md");
4671
- const playbooksDir3 = resolve3(root, "playbooks");
4864
+ const configPath = resolve5(root, "config.md");
4865
+ const playbooksDir3 = resolve5(root, "playbooks");
4672
4866
  await ensureDir(root);
4673
4867
  await ensureDir(projectsDir2);
4674
4868
  await ensureDir(standaloneAssignmentsDir);
@@ -4704,16 +4898,16 @@ async function initCommand(options) {
4704
4898
  }
4705
4899
  async function seedDefaultPlaybooks(playbooksDir3) {
4706
4900
  const __filename = fileURLToPath(import.meta.url);
4707
- const packageRoot = resolve3(dirname2(__filename), "..");
4708
- const examplesDir = resolve3(packageRoot, "examples", "playbooks");
4901
+ const packageRoot = resolve5(dirname2(__filename), "..");
4902
+ const examplesDir = resolve5(packageRoot, "examples", "playbooks");
4709
4903
  if (!await fileExists(examplesDir)) return 0;
4710
- const entries = await readdir2(examplesDir, { withFileTypes: true });
4904
+ const entries = await readdir3(examplesDir, { withFileTypes: true });
4711
4905
  let count = 0;
4712
4906
  for (const entry of entries) {
4713
4907
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4714
- const targetPath = resolve3(playbooksDir3, entry.name);
4908
+ const targetPath = resolve5(playbooksDir3, entry.name);
4715
4909
  if (await fileExists(targetPath)) continue;
4716
- const content = await readFile2(resolve3(examplesDir, entry.name), "utf-8");
4910
+ const content = await readFile4(resolve5(examplesDir, entry.name), "utf-8");
4717
4911
  await writeFileSafe(targetPath, content);
4718
4912
  count++;
4719
4913
  }
@@ -4820,12 +5014,25 @@ function renderAssignment(params) {
4820
5014
  const linksYaml = params.links.length === 0 ? "links: []" : `links:
4821
5015
  - ${params.links.join("\n - ")}`;
4822
5016
  const projectYaml = `project: ${params.project == null ? "null" : params.project}`;
5017
+ const workspaceGroupLine = params.workspaceGroup ? `
5018
+ workspaceGroup: ${params.workspaceGroup}` : "";
4823
5019
  const typeYaml = `type: ${params.type ?? "feature"}`;
5020
+ const todosSection = params.includeTodos ? `## Todos
5021
+
5022
+ <!--
5023
+ Checklist of work items for this assignment. Items may be simple tasks
5024
+ or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
5025
+ When a plan is superseded by a new one, mark the old todo as:
5026
+ - [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
5027
+ Never delete superseded todos \u2014 preserve the history.
5028
+ -->
5029
+
5030
+ ` : "";
4824
5031
  return `---
4825
5032
  id: ${params.id}
4826
5033
  slug: ${params.slug}
4827
5034
  title: ${safeTitle}
4828
- ${projectYaml}
5035
+ ${projectYaml}${workspaceGroupLine}
4829
5036
  ${typeYaml}
4830
5037
  status: pending
4831
5038
  priority: ${params.priority}
@@ -4856,17 +5063,7 @@ tags: []
4856
5063
  - [ ] <!-- criterion 2 -->
4857
5064
  - [ ] <!-- criterion 3 -->
4858
5065
 
4859
- ## Todos
4860
-
4861
- <!--
4862
- Checklist of work items for this assignment. Items may be simple tasks
4863
- or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
4864
- When a plan is superseded by a new one, mark the old todo as:
4865
- - [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
4866
- Never delete superseded todos \u2014 preserve the history.
4867
- -->
4868
-
4869
- ## Context
5066
+ ${todosSection}## Context
4870
5067
 
4871
5068
  <!-- Links to relevant docs, code, or other assignments. -->
4872
5069
 
@@ -5552,6 +5749,14 @@ async function createAssignmentCommand(title, options) {
5552
5749
  if (!title.trim()) {
5553
5750
  throw new Error("Assignment title cannot be empty.");
5554
5751
  }
5752
+ if (options.workspace && options.project) {
5753
+ throw new Error(
5754
+ "Cannot use --workspace with --project (projects already carry a workspace via project.workspace)."
5755
+ );
5756
+ }
5757
+ if (options.workspace && !options.oneOff) {
5758
+ throw new Error("--workspace requires --one-off.");
5759
+ }
5555
5760
  if (!options.project && !options.oneOff) {
5556
5761
  throw new Error(
5557
5762
  "Either --project <slug> or --one-off is required."
@@ -5567,6 +5772,11 @@ async function createAssignmentCommand(title, options) {
5567
5772
  `Invalid project slug "${options.project}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5568
5773
  );
5569
5774
  }
5775
+ if (options.workspace && !isValidSlug(options.workspace)) {
5776
+ throw new Error(
5777
+ `Invalid workspace slug "${options.workspace}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5778
+ );
5779
+ }
5570
5780
  if (options.oneOff && options.dependsOn) {
5571
5781
  throw new Error("Standalone assignments cannot have dependencies (--depends-on is not allowed with --one-off).");
5572
5782
  }
@@ -5657,7 +5867,9 @@ Use --slug to specify a different slug.`
5657
5867
  dependsOn,
5658
5868
  links,
5659
5869
  project: projectSlug,
5660
- type: options.type
5870
+ workspaceGroup: options.workspace ?? null,
5871
+ type: options.type,
5872
+ includeTodos: options.withTodos === true
5661
5873
  })
5662
5874
  ],
5663
5875
  [
@@ -6434,7 +6646,15 @@ function createWriteRouter(projectsDir2, assignmentsDir2) {
6434
6646
  });
6435
6647
  res.json({ content });
6436
6648
  });
6437
- router.get("/api/templates/assignment", (_req, res) => {
6649
+ router.get("/api/templates/assignment", (req, res) => {
6650
+ const standalone = req.query.standalone === "1";
6651
+ const workspaceParam = typeof req.query.workspace === "string" ? req.query.workspace : "";
6652
+ if (workspaceParam && !isValidSlug(workspaceParam)) {
6653
+ res.status(400).json({
6654
+ error: `Invalid workspace slug "${workspaceParam}". Slugs must be lowercase, hyphen-separated, with no special characters.`
6655
+ });
6656
+ return;
6657
+ }
6438
6658
  const content = renderAssignment({
6439
6659
  id: generateId(),
6440
6660
  slug: "my-new-assignment",
@@ -6442,7 +6662,9 @@ function createWriteRouter(projectsDir2, assignmentsDir2) {
6442
6662
  timestamp: nowTimestamp(),
6443
6663
  priority: "medium",
6444
6664
  dependsOn: [],
6445
- links: []
6665
+ links: [],
6666
+ project: standalone ? null : void 0,
6667
+ workspaceGroup: standalone && workspaceParam ? workspaceParam : null
6446
6668
  });
6447
6669
  res.json({ content });
6448
6670
  });
@@ -7171,6 +7393,76 @@ ${entry}`;
7171
7393
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
7172
7394
  return;
7173
7395
  }
7396
+ const rawContent = typeof req.body?.content === "string" ? req.body.content : "";
7397
+ if (rawContent.trim()) {
7398
+ const fields = extractFrontmatter3(rawContent);
7399
+ if (!fields) {
7400
+ res.status(400).json({ error: "Invalid frontmatter: missing --- delimiters" });
7401
+ return;
7402
+ }
7403
+ const validation = validateRequired(fields, ["slug", "title"]);
7404
+ if (!validation.valid) {
7405
+ res.status(400).json({ error: `Missing required fields: ${validation.missing.join(", ")}` });
7406
+ return;
7407
+ }
7408
+ const submittedSlug = fields.slug;
7409
+ if (!isValidSlug(submittedSlug)) {
7410
+ res.status(400).json({ error: `Invalid slug "${submittedSlug}". Must be lowercase and hyphen-separated.` });
7411
+ return;
7412
+ }
7413
+ const validPriorities = ["low", "medium", "high", "critical"];
7414
+ const submittedPriority = fields.priority || "medium";
7415
+ if (!validPriorities.includes(submittedPriority)) {
7416
+ res.status(400).json({ error: `Invalid priority "${submittedPriority}". Must be low, medium, high, or critical.` });
7417
+ return;
7418
+ }
7419
+ if (fields.project && fields.project !== "null") {
7420
+ res.status(400).json({
7421
+ error: 'Standalone assignments cannot have a project; remove "project" or set it to null.'
7422
+ });
7423
+ return;
7424
+ }
7425
+ const submittedWorkspaceGroup = fields.workspaceGroup && fields.workspaceGroup !== "null" ? fields.workspaceGroup : "";
7426
+ if (submittedWorkspaceGroup && !isValidSlug(submittedWorkspaceGroup)) {
7427
+ res.status(400).json({
7428
+ error: `Invalid workspace slug "${submittedWorkspaceGroup}". Slugs must be lowercase, hyphen-separated, with no special characters.`
7429
+ });
7430
+ return;
7431
+ }
7432
+ const id2 = generateId();
7433
+ const assignmentDir2 = resolve15(assignmentsDir2, id2);
7434
+ if (await fileExists(assignmentDir2)) {
7435
+ res.status(500).json({ error: "UUID collision \u2014 try again" });
7436
+ return;
7437
+ }
7438
+ const timestamp2 = fields.created || nowTimestamp();
7439
+ await ensureDir(assignmentDir2);
7440
+ const normalizedContent = setTopLevelField(rawContent, "id", id2);
7441
+ await writeFileForce(resolve15(assignmentDir2, "assignment.md"), normalizedContent);
7442
+ await writeFileForce(
7443
+ resolve15(assignmentDir2, "scratchpad.md"),
7444
+ renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
7445
+ );
7446
+ await writeFileForce(
7447
+ resolve15(assignmentDir2, "handoff.md"),
7448
+ renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
7449
+ );
7450
+ await writeFileForce(
7451
+ resolve15(assignmentDir2, "decision-record.md"),
7452
+ renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
7453
+ );
7454
+ await writeFileForce(
7455
+ resolve15(assignmentDir2, "progress.md"),
7456
+ renderProgress({ assignment: id2, timestamp: timestamp2 })
7457
+ );
7458
+ await writeFileForce(
7459
+ resolve15(assignmentDir2, "comments.md"),
7460
+ renderComments({ assignment: id2, timestamp: timestamp2 })
7461
+ );
7462
+ const detail2 = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id2);
7463
+ res.status(201).json({ assignment: detail2 });
7464
+ return;
7465
+ }
7174
7466
  const { title, slug, priority, type } = req.body || {};
7175
7467
  if (!title || typeof title !== "string" || !title.trim()) {
7176
7468
  res.status(400).json({ error: "title is required" });
@@ -7953,6 +8245,7 @@ import { resolve as resolve17 } from "path";
7953
8245
  import { readFile as readFile12, unlink as unlink2 } from "fs/promises";
7954
8246
  init_timestamp();
7955
8247
  init_fs();
8248
+ init_playbooks();
7956
8249
  function createPlaybooksRouter(playbooksDir3) {
7957
8250
  const router = Router4();
7958
8251
  router.get("/", async (_req, res) => {
@@ -7976,6 +8269,32 @@ function createPlaybooksRouter(playbooksDir3) {
7976
8269
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get template" });
7977
8270
  }
7978
8271
  });
8272
+ router.post("/:slug/enable", async (req, res) => {
8273
+ try {
8274
+ const result = await setPlaybookEnabled(playbooksDir3, req.params.slug, true);
8275
+ res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
8276
+ } catch (error) {
8277
+ const msg = error instanceof Error ? error.message : "Failed to enable playbook";
8278
+ if (msg.startsWith("Playbook ")) {
8279
+ res.status(404).json({ error: msg });
8280
+ return;
8281
+ }
8282
+ res.status(500).json({ error: msg });
8283
+ }
8284
+ });
8285
+ router.post("/:slug/disable", async (req, res) => {
8286
+ try {
8287
+ const result = await setPlaybookEnabled(playbooksDir3, req.params.slug, false);
8288
+ res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
8289
+ } catch (error) {
8290
+ const msg = error instanceof Error ? error.message : "Failed to disable playbook";
8291
+ if (msg.startsWith("Playbook ")) {
8292
+ res.status(404).json({ error: msg });
8293
+ return;
8294
+ }
8295
+ res.status(500).json({ error: msg });
8296
+ }
8297
+ });
7979
8298
  router.get("/:slug", async (req, res) => {
7980
8299
  try {
7981
8300
  const detail = await getPlaybookDetail(playbooksDir3, req.params.slug);
@@ -7990,17 +8309,18 @@ function createPlaybooksRouter(playbooksDir3) {
7990
8309
  });
7991
8310
  router.get("/:slug/edit", async (req, res) => {
7992
8311
  try {
7993
- const filePath = resolve17(playbooksDir3, `${req.params.slug}.md`);
7994
- if (!await fileExists(filePath)) {
8312
+ const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
8313
+ if (!resolved) {
7995
8314
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
7996
8315
  return;
7997
8316
  }
8317
+ const filePath = resolve17(playbooksDir3, resolved.filename);
7998
8318
  const content = await readFile12(filePath, "utf-8");
7999
8319
  res.json({
8000
8320
  documentType: "playbook",
8001
- title: `Edit Playbook: ${req.params.slug}`,
8321
+ title: `Edit Playbook: ${resolved.slug}`,
8002
8322
  content,
8003
- slug: req.params.slug
8323
+ slug: resolved.slug
8004
8324
  });
8005
8325
  } catch (error) {
8006
8326
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get playbook for editing" });
@@ -8039,14 +8359,15 @@ function createPlaybooksRouter(playbooksDir3) {
8039
8359
  res.status(400).json({ error: "content is required" });
8040
8360
  return;
8041
8361
  }
8042
- const filePath = resolve17(playbooksDir3, `${req.params.slug}.md`);
8043
- if (!await fileExists(filePath)) {
8362
+ const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
8363
+ if (!resolved) {
8044
8364
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
8045
8365
  return;
8046
8366
  }
8367
+ const filePath = resolve17(playbooksDir3, resolved.filename);
8047
8368
  await writeFileForce(filePath, content);
8048
8369
  await rebuildPlaybookManifest(playbooksDir3);
8049
- res.json({ slug: req.params.slug, path: filePath });
8370
+ res.json({ slug: resolved.slug, path: filePath });
8050
8371
  } catch (error) {
8051
8372
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
8052
8373
  }
@@ -8057,14 +8378,16 @@ function createPlaybooksRouter(playbooksDir3) {
8057
8378
  res.status(403).json({ error: "The playbook manifest cannot be deleted" });
8058
8379
  return;
8059
8380
  }
8060
- const filePath = resolve17(playbooksDir3, `${req.params.slug}.md`);
8061
- if (!await fileExists(filePath)) {
8381
+ const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
8382
+ if (!resolved) {
8062
8383
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
8063
8384
  return;
8064
8385
  }
8386
+ const filePath = resolve17(playbooksDir3, resolved.filename);
8065
8387
  await unlink2(filePath);
8388
+ await removeFromDisabledList(resolved.slug);
8066
8389
  await rebuildPlaybookManifest(playbooksDir3);
8067
- res.json({ deleted: req.params.slug });
8390
+ res.json({ deleted: resolved.slug });
8068
8391
  } catch (error) {
8069
8392
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
8070
8393
  }
@@ -9244,7 +9567,7 @@ function createDashboardServer(options) {
9244
9567
  });
9245
9568
  app.get("/api/workspaces", async (_req, res) => {
9246
9569
  try {
9247
- const result = await listWorkspaces(projectsDir2);
9570
+ const result = await listWorkspaces(projectsDir2, assignmentsDir2);
9248
9571
  res.json(result);
9249
9572
  } catch (error) {
9250
9573
  console.error("Error listing workspaces:", error);
@@ -9363,16 +9686,25 @@ function createDashboardServer(options) {
9363
9686
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
9364
9687
  app.use("/api/backup", createBackupRouter());
9365
9688
  if (serveStaticUi && dashboardDistPath) {
9366
- app.use(express.static(dashboardDistPath));
9367
- app.get("{*path}", async (_req, res) => {
9689
+ app.use("/assets", express.static(resolve20(dashboardDistPath, "assets")));
9690
+ app.get("{*path}", async (req, res) => {
9691
+ if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
9692
+ res.status(404).json({ error: "Not Found" });
9693
+ return;
9694
+ }
9368
9695
  const indexPath = resolve20(dashboardDistPath, "index.html");
9369
- if (await fileExists(indexPath)) {
9370
- res.sendFile(indexPath);
9371
- } else {
9696
+ if (!await fileExists(indexPath)) {
9372
9697
  res.status(503).send(
9373
9698
  'Dashboard not built. Run "npm run build:dashboard" first.'
9374
9699
  );
9700
+ return;
9375
9701
  }
9702
+ res.sendFile(indexPath, (err2) => {
9703
+ if (err2) {
9704
+ console.error("Error sending dashboard index.html:", err2);
9705
+ if (!res.headersSent) res.status(500).send("Dashboard load error");
9706
+ }
9707
+ });
9376
9708
  });
9377
9709
  }
9378
9710
  let watcherHandle = null;
@@ -11758,6 +12090,7 @@ import { resolve as resolve32 } from "path";
11758
12090
  init_timestamp();
11759
12091
  init_paths();
11760
12092
  init_fs();
12093
+ init_playbooks();
11761
12094
  async function createPlaybookCommand(name, options) {
11762
12095
  if (!name.trim()) {
11763
12096
  throw new Error("Playbook name cannot be empty.");
@@ -11791,32 +12124,97 @@ Use --slug to specify a different slug.`
11791
12124
  init_paths();
11792
12125
  init_fs();
11793
12126
  init_parser();
12127
+ init_config2();
11794
12128
  import { readdir as readdir12, readFile as readFile19 } from "fs/promises";
11795
12129
  import { resolve as resolve33 } from "path";
11796
- async function listPlaybooksCommand() {
12130
+ async function listPlaybooksCommand(options = {}) {
11797
12131
  const dir = playbooksDir();
11798
12132
  if (!await fileExists(dir)) {
11799
12133
  console.log('No playbooks directory found. Run "syntaur init" first.');
11800
12134
  return;
11801
12135
  }
12136
+ const config = await readConfig();
12137
+ const disabledSet = new Set(config.playbooks.disabled);
11802
12138
  const entries = await readdir12(dir, { withFileTypes: true });
11803
- const mdFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_") && e.name !== "manifest.md");
11804
- if (mdFiles.length === 0) {
11805
- console.log('No playbooks found. Create one with "syntaur create-playbook <name>".');
11806
- return;
11807
- }
11808
- console.log(`Found ${mdFiles.length} playbook(s):
11809
- `);
11810
- console.log(`${"Slug".padEnd(30)} ${"Name".padEnd(30)} Description`);
11811
- console.log(`${"\u2500".repeat(30)} ${"\u2500".repeat(30)} ${"\u2500".repeat(40)}`);
12139
+ const mdFiles = entries.filter(
12140
+ (e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_") && e.name !== "manifest.md"
12141
+ );
12142
+ const rows = [];
11812
12143
  for (const entry of mdFiles) {
11813
12144
  const filePath = resolve33(dir, entry.name);
11814
12145
  const raw = await readFile19(filePath, "utf-8");
11815
12146
  const parsed = parsePlaybook(raw);
11816
12147
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
11817
- const name = parsed.name || slug;
11818
- const desc = parsed.description || "";
11819
- console.log(`${slug.padEnd(30)} ${name.padEnd(30)} ${desc}`);
12148
+ const disabled = disabledSet.has(slug);
12149
+ if (disabled && !options.all) continue;
12150
+ rows.push({
12151
+ slug,
12152
+ name: parsed.name || slug,
12153
+ desc: parsed.description || "",
12154
+ disabled
12155
+ });
12156
+ }
12157
+ if (rows.length === 0) {
12158
+ if (!options.all && disabledSet.size > 0) {
12159
+ console.log(
12160
+ `No enabled playbooks found (${disabledSet.size} disabled). Use --all to include disabled playbooks.`
12161
+ );
12162
+ } else {
12163
+ console.log('No playbooks found. Create one with "syntaur create-playbook <name>".');
12164
+ }
12165
+ return;
12166
+ }
12167
+ const totalLabel = options.all ? `Found ${rows.length} playbook(s) (${[...disabledSet].length} disabled):` : `Found ${rows.length} enabled playbook(s):`;
12168
+ console.log(`${totalLabel}
12169
+ `);
12170
+ console.log(`${"Slug".padEnd(30)} ${"Name".padEnd(30)} Description`);
12171
+ console.log(`${"\u2500".repeat(30)} ${"\u2500".repeat(30)} ${"\u2500".repeat(40)}`);
12172
+ for (const row of rows) {
12173
+ const suffix = row.disabled ? " (disabled)" : "";
12174
+ const desc = `${row.desc}${suffix}`;
12175
+ console.log(`${row.slug.padEnd(30)} ${row.name.padEnd(30)} ${desc}`);
12176
+ }
12177
+ }
12178
+
12179
+ // src/commands/enable-playbook.ts
12180
+ init_paths();
12181
+ init_playbooks();
12182
+ async function enablePlaybookCommand(slug) {
12183
+ if (!slug.trim()) {
12184
+ throw new Error("Playbook slug cannot be empty.");
12185
+ }
12186
+ if (!isValidSlug(slug)) {
12187
+ throw new Error(
12188
+ `Invalid slug "${slug}". Slugs must be lowercase, hyphen-separated, with no special characters.`
12189
+ );
12190
+ }
12191
+ const dir = playbooksDir();
12192
+ const { slug: canonical, changed } = await setPlaybookEnabled(dir, slug, true);
12193
+ if (changed) {
12194
+ console.log(`Playbook "${canonical}" enabled.`);
12195
+ } else {
12196
+ console.log(`Playbook "${canonical}" is already enabled.`);
12197
+ }
12198
+ }
12199
+
12200
+ // src/commands/disable-playbook.ts
12201
+ init_paths();
12202
+ init_playbooks();
12203
+ async function disablePlaybookCommand(slug) {
12204
+ if (!slug.trim()) {
12205
+ throw new Error("Playbook slug cannot be empty.");
12206
+ }
12207
+ if (!isValidSlug(slug)) {
12208
+ throw new Error(
12209
+ `Invalid slug "${slug}". Slugs must be lowercase, hyphen-separated, with no special characters.`
12210
+ );
12211
+ }
12212
+ const dir = playbooksDir();
12213
+ const { slug: canonical, changed } = await setPlaybookEnabled(dir, slug, false);
12214
+ if (changed) {
12215
+ console.log(`Playbook "${canonical}" disabled.`);
12216
+ } else {
12217
+ console.log(`Playbook "${canonical}" is already disabled.`);
11820
12218
  }
11821
12219
  }
11822
12220
 
@@ -14580,7 +14978,7 @@ program.command("create-assignment").description("Create a new assignment within
14580
14978
  "--priority <level>",
14581
14979
  "Priority level (low|medium|high|critical)",
14582
14980
  "medium"
14583
- ).option("--type <type>", "Assignment type (e.g. feature, bug, refactor)").option("--depends-on <slugs>", "Comma-separated dependency slugs (not allowed with --one-off)").option("--links <slugs>", "Comma-separated linked assignment slugs (projectSlug/assignmentSlug format)").option("--dir <path>", "Override default project directory (ignored for --one-off)").action(async (title, options) => {
14981
+ ).option("--type <type>", "Assignment type (e.g. feature, bug, refactor)").option("--depends-on <slugs>", "Comma-separated dependency slugs (not allowed with --one-off)").option("--links <slugs>", "Comma-separated linked assignment slugs (projectSlug/assignmentSlug format)").option("--dir <path>", "Override default project directory (ignored for --one-off)").option("--with-todos", "Scaffold a ## Todos section in assignment.md (omitted by default; typically populated by /plan-assignment)").option("--workspace <slug>", "Workspace group slug (only valid with --one-off; mutually exclusive with --project)").action(async (title, options) => {
14584
14982
  try {
14585
14983
  await createAssignmentCommand(title, options);
14586
14984
  } catch (error) {
@@ -14864,9 +15262,31 @@ program.command("create-playbook").description("Create a new playbook").argument
14864
15262
  process.exit(1);
14865
15263
  }
14866
15264
  });
14867
- program.command("list-playbooks").description("List all playbooks").action(async () => {
15265
+ program.command("list-playbooks").description("List playbooks (disabled playbooks are excluded unless --all is passed)").option("--all", "Include disabled playbooks").action(async (options) => {
15266
+ try {
15267
+ await listPlaybooksCommand({ all: Boolean(options?.all) });
15268
+ } catch (error) {
15269
+ console.error(
15270
+ "Error:",
15271
+ error instanceof Error ? error.message : String(error)
15272
+ );
15273
+ process.exit(1);
15274
+ }
15275
+ });
15276
+ program.command("enable-playbook").description("Enable a previously-disabled playbook").argument("<slug>", "Playbook slug").action(async (slug) => {
15277
+ try {
15278
+ await enablePlaybookCommand(slug);
15279
+ } catch (error) {
15280
+ console.error(
15281
+ "Error:",
15282
+ error instanceof Error ? error.message : String(error)
15283
+ );
15284
+ process.exit(1);
15285
+ }
15286
+ });
15287
+ program.command("disable-playbook").description("Disable a playbook so agents no longer load it").argument("<slug>", "Playbook slug").action(async (slug) => {
14868
15288
  try {
14869
- await listPlaybooksCommand();
15289
+ await disablePlaybookCommand(slug);
14870
15290
  } catch (error) {
14871
15291
  console.error(
14872
15292
  "Error:",