syntaur 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dashboard/dist/assets/{_basePickBy-CWivToyi.js → _basePickBy-Bcut0btZ.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-C-qj6E4l.js → _baseUniq-AQSP2JEk.js} +1 -1
  3. package/dashboard/dist/assets/{arc-Dn5BIqMa.js → arc-BLTpY9lc.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-D5D0K7rY.js → architectureDiagram-2XIMDMQ5-CJtwMY_X.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-DVmYPMbu.js → blockDiagram-WCTKOSBZ-Don-O7X7.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEasxbnl.js → c4Diagram-IC4MRINW-C_M3yTTB.js} +1 -1
  7. package/dashboard/dist/assets/channel-BfXmPwE5.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-LDIrtI5E.js → chunk-4BX2VUAB-CGss0jXe.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-CaEBUJYu.js → chunk-55IACEB6-BatoPJga.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-B-GjCpdr.js → chunk-FMBD7UC4-DxH4wO82.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-BLVVcezm.js → chunk-JSJVCQXG-BL3izAFQ.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-DqCNEw4h.js → chunk-KX2RTZJC-GnqXwnge.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-BCPbFf5I.js → chunk-NQ4KR5QH-gvCn4QMb.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-Ci0C85q_.js → chunk-QZHKN3VN-CYGWogyi.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-VVhAMMYU.js → chunk-WL4C6EOR-D9mVTQ1F.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-D7_G1qy0.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D7_G1qy0.js +1 -0
  18. package/dashboard/dist/assets/clone-BKG-N796.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CO9uwgYO.js → cose-bilkent-S5V4N54A-CUWQCKt4.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-bwLLXcL4.js → dagre-KLK3FWXG-CH3ijEvV.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-RuS5R6V1.js → diagram-E7M64L7V-sq83lpV1.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-BQDJAHQd.js → diagram-IFDJBPK2-BzQG_rtq.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-yLEsgzE5.js → diagram-P4PSJMXO-Dg0eZn0q.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-na6dUhY0.js → erDiagram-INFDFZHY-4b9eQ0uj.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BIcrzwJR.js → flowDiagram-PKNHOUZH-C9fzKcsZ.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DHWRJn-D.js → ganttDiagram-A5KZAMGK-Bzt6i9SH.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-LGxDjL71.js → gitGraphDiagram-K3NZZRJ6-D0wFOagh.js} +1 -1
  28. package/dashboard/dist/assets/{graph-BUqNu277.js → graph-EEIGvqDh.js} +1 -1
  29. package/dashboard/dist/assets/index-Bu6ma6my.css +1 -0
  30. package/dashboard/dist/assets/index-C7f0ySJE.js +481 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DidoA2hb.js → infoDiagram-LFFYTUFH-DLYMsj1D.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CdlZkbhV.js → ishikawaDiagram-PHBUUO56-DVebKkzl.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-luhcz_gn.js → journeyDiagram-4ABVD52K-BsmgOWVw.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Coidw9XE.js → kanban-definition-K7BYSVSG-BTnHf0ey.js} +1 -1
  35. package/dashboard/dist/assets/{layout-_aBAAleE.js → layout-BbM7HRvv.js} +1 -1
  36. package/dashboard/dist/assets/{linear-D8mFnDSx.js → linear-C37bJKPO.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-BpP2keU-.js → mermaid.core-MZ_JgnRL.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-LjvrTe2z.js → mindmap-definition-YRQLILUH-CgHS4hFo.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CkA2iU6e.js → pieDiagram-SKSYHLDU-CmAgopJe.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BRmhKHQG.js → quadrantDiagram-337W2JSQ-BvzYUPR6.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BYCQ4uFX.js → requirementDiagram-Z7DCOOCP-Bs52VP7k.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-C8SVk50M.js → sankeyDiagram-WA2Y5GQK-aXvGPR1o.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CbA_2lnP.js → sequenceDiagram-2WXFIKYE-CzgcfU6K.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D6ZtjAHE.js → stateDiagram-RAJIS63D-BXBJf9Hq.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-QqOtsuOs.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-B2Uf-emK.js → timeline-definition-YZTLITO2-BsXp26Ai.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-CYnFKsuJ.js → treemap-KZPCXAKY-C3WbDii1.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cnj0qiDO.js → vennDiagram-LZ73GAT5-B28LMHWd.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BJy2mbL9.js → xychartDiagram-JWTSCODW-C3Xwz8mS.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +1329 -402
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +1591 -533
  54. package/dist/index.js.map +1 -1
  55. package/examples/playbooks/assignment-creation.md +19 -0
  56. package/examples/playbooks/assignment-planning.md +28 -0
  57. package/package.json +1 -1
  58. package/platforms/claude-code/.orphaned_at +1 -0
  59. package/platforms/claude-code/agents/syntaur-expert.md +2 -2
  60. package/platforms/claude-code/commands/create-assignment/create-assignment.md +1 -1
  61. package/platforms/codex/agents/syntaur-operator.md +2 -2
  62. package/vendor/syntaur-skills/README.md +12 -0
  63. package/vendor/syntaur-skills/skills/create-assignment/SKILL.md +5 -4
  64. package/dashboard/dist/assets/channel-DqU_8tiy.js +0 -1
  65. package/dashboard/dist/assets/classDiagram-VBA2DB6C-D29Eeoe8.js +0 -1
  66. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D29Eeoe8.js +0 -1
  67. package/dashboard/dist/assets/clone-Bok8Q3Jj.js +0 -1
  68. package/dashboard/dist/assets/index-D-fepllQ.js +0 -481
  69. package/dashboard/dist/assets/index-DnHyQJJH.css +0 -1
  70. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DeYN4wV6.js +0 -1
  71. package/examples/playbooks/plan-versioning.md +0 -36
  72. package/examples/playbooks/read-before-plan.md +0 -30
package/dist/index.js CHANGED
@@ -39,6 +39,9 @@ function playbooksDir() {
39
39
  function todosDir() {
40
40
  return resolve(syntaurRoot(), "todos");
41
41
  }
42
+ function projectTodosDir(projectsDir2, projectSlug) {
43
+ return resolve(projectsDir2, projectSlug, "todos");
44
+ }
42
45
  var init_paths = __esm({
43
46
  "src/utils/paths.ts"() {
44
47
  "use strict";
@@ -91,10 +94,10 @@ var init_fs = __esm({
91
94
  });
92
95
 
93
96
  // src/templates/config.ts
94
- function renderConfig(params) {
97
+ function renderConfig(params2) {
95
98
  return `---
96
99
  version: "1.0"
97
- defaultProjectDir: ${params.defaultProjectDir}
100
+ defaultProjectDir: ${params2.defaultProjectDir}
98
101
  onboarding:
99
102
  completed: false
100
103
  agentDefaults:
@@ -247,6 +250,7 @@ function parseAssignmentFull(fileContent) {
247
250
  slug: getField(fm, "slug") ?? "",
248
251
  title: getField(fm, "title") ?? "",
249
252
  project: getField(fm, "project"),
253
+ workspaceGroup: getField(fm, "workspaceGroup"),
250
254
  type: getField(fm, "type"),
251
255
  status: getField(fm, "status") ?? "pending",
252
256
  priority: getField(fm, "priority") ?? "medium",
@@ -414,8 +418,8 @@ var init_timestamp = __esm({
414
418
  });
415
419
 
416
420
  // 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";
421
+ import { readdir, readFile, rename as rename2, writeFile as writeFile2 } from "fs/promises";
422
+ import { resolve as resolve2 } from "path";
419
423
  async function migrateLegacyProjectFiles(projectsDir2) {
420
424
  const result = {
421
425
  renamedProjectFiles: [],
@@ -424,15 +428,15 @@ async function migrateLegacyProjectFiles(projectsDir2) {
424
428
  if (!await fileExists(projectsDir2)) return result;
425
429
  let entries;
426
430
  try {
427
- entries = await readdir3(projectsDir2, { withFileTypes: true });
431
+ entries = await readdir(projectsDir2, { withFileTypes: true });
428
432
  } catch {
429
433
  return result;
430
434
  }
431
435
  for (const entry of entries) {
432
436
  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");
437
+ const projectDir = resolve2(projectsDir2, entry.name);
438
+ const legacy = resolve2(projectDir, "mission.md");
439
+ const target = resolve2(projectDir, "project.md");
436
440
  try {
437
441
  if (await fileExists(legacy) && !await fileExists(target)) {
438
442
  await rename2(legacy, target);
@@ -443,7 +447,7 @@ async function migrateLegacyProjectFiles(projectsDir2) {
443
447
  }
444
448
  for (const stale of ["agent.md", "claude.md"]) {
445
449
  try {
446
- if (await fileExists(resolve4(projectDir, stale))) {
450
+ if (await fileExists(resolve2(projectDir, stale))) {
447
451
  result.legacyExtras.push(`${entry.name}/${stale}`);
448
452
  }
449
453
  } catch {
@@ -461,7 +465,7 @@ async function migrateLegacyConfig(configPath) {
461
465
  if (!await fileExists(configPath)) return result;
462
466
  let content;
463
467
  try {
464
- content = await readFile3(configPath, "utf-8");
468
+ content = await readFile(configPath, "utf-8");
465
469
  } catch {
466
470
  return result;
467
471
  }
@@ -490,7 +494,7 @@ async function migrateLegacyConfig(configPath) {
490
494
  const projectLineRe = /^\s*defaultProjectDir\s*:\s*(.*)$/m;
491
495
  const projectLineMatch = newFmBlock.match(projectLineRe);
492
496
  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;
497
+ const expand = (p) => p.startsWith("~") ? resolve2(process.env.HOME ?? "/", p.slice(p.startsWith("~/") ? 2 : 1)) : p;
494
498
  let resolvedProjectsDir = projectsDirRaw ? expand(projectsDirRaw) : null;
495
499
  if (resolvedProjectsDir && resolvedProjectsDir.endsWith("/missions")) {
496
500
  const siblingProjectsDir = resolvedProjectsDir.replace(/\/missions$/, "/projects");
@@ -549,8 +553,29 @@ var init_fs_migration = __esm({
549
553
  });
550
554
 
551
555
  // src/utils/config.ts
552
- import { readFile as readFile4 } from "fs/promises";
553
- import { resolve as resolve5, isAbsolute } from "path";
556
+ import { readFile as readFile2 } from "fs/promises";
557
+ import { resolve as resolve3, isAbsolute } from "path";
558
+ function cloneDefaultConfig() {
559
+ return {
560
+ ...DEFAULT_CONFIG,
561
+ onboarding: { ...DEFAULT_CONFIG.onboarding },
562
+ agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
563
+ integrations: { ...DEFAULT_CONFIG.integrations },
564
+ backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
565
+ statuses: DEFAULT_CONFIG.statuses ? {
566
+ statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
567
+ order: [...DEFAULT_CONFIG.statuses.order],
568
+ transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
569
+ } : null,
570
+ types: DEFAULT_CONFIG.types ? {
571
+ definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
572
+ default: DEFAULT_CONFIG.types.default
573
+ } : null,
574
+ playbooks: {
575
+ disabled: [...DEFAULT_CONFIG.playbooks.disabled]
576
+ }
577
+ };
578
+ }
554
579
  function parseFrontmatter(content) {
555
580
  const match = content.match(/^---\n([\s\S]*?)\n---/);
556
581
  if (!match) return {};
@@ -722,6 +747,82 @@ function serializeBackupConfig(backup) {
722
747
  lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
723
748
  return lines.join("\n");
724
749
  }
750
+ function serializePlaybooksConfig(playbooks) {
751
+ if (!playbooks.disabled || playbooks.disabled.length === 0) {
752
+ return null;
753
+ }
754
+ const lines = ["playbooks:", " disabled:"];
755
+ for (const slug of playbooks.disabled) {
756
+ lines.push(` - ${slug}`);
757
+ }
758
+ return lines.join("\n");
759
+ }
760
+ function parsePlaybooksConfig(fmBlock) {
761
+ const blockStart = fmBlock.match(/^playbooks:\s*$/m);
762
+ if (!blockStart) {
763
+ return { disabled: [] };
764
+ }
765
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
766
+ const remaining = fmBlock.slice(startIdx).split("\n");
767
+ const disabled = [];
768
+ let currentSection = null;
769
+ for (const line of remaining) {
770
+ const trimmed = line.trimStart();
771
+ const indent = line.length - trimmed.length;
772
+ if (indent === 0 && trimmed.length > 0) break;
773
+ if (trimmed === "") continue;
774
+ if (indent === 2 && trimmed.startsWith("disabled:")) {
775
+ currentSection = "disabled";
776
+ const afterColon = trimmed.slice("disabled:".length).trim();
777
+ if (afterColon === "[]" || afterColon === "") {
778
+ continue;
779
+ }
780
+ continue;
781
+ }
782
+ if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
783
+ const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
784
+ if (raw.length === 0) continue;
785
+ if (/\s/.test(raw)) {
786
+ console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
787
+ continue;
788
+ }
789
+ disabled.push(raw);
790
+ continue;
791
+ }
792
+ }
793
+ return { disabled };
794
+ }
795
+ async function updatePlaybooksConfig(playbooks) {
796
+ const configPath = resolve3(syntaurRoot(), "config.md");
797
+ const current = (await readConfig()).playbooks;
798
+ const nextPlaybooks = {
799
+ disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
800
+ };
801
+ const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
802
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
803
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
804
+ if (!fmMatch) {
805
+ const bodyBlock = playbooksBlock ? `${playbooksBlock}
806
+ ` : "";
807
+ const content = `---
808
+ version: "2.0"
809
+ defaultProjectDir: ${defaultProjectDir()}
810
+ ${bodyBlock}---
811
+ ${existing}`;
812
+ await writeFileForce(configPath, content);
813
+ return;
814
+ }
815
+ const fmBlock = fmMatch[2];
816
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
817
+ const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
818
+ const newFm = playbooksBlock ? `${cleanedFm}
819
+ ${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
820
+ const normalizedFm = newFm.replace(/\n+$/, "");
821
+ const newContent = `---
822
+ ${normalizedFm}
823
+ ---${afterFrontmatter}`;
824
+ await writeFileForce(configPath, newContent);
825
+ }
725
826
  function stripTopLevelBlock(fmBlock, key) {
726
827
  const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
727
828
  if (!blockStart) {
@@ -756,10 +857,10 @@ function parseOptionalAbsolutePath(value, fieldName) {
756
857
  );
757
858
  return null;
758
859
  }
759
- return resolve5(expanded);
860
+ return resolve3(expanded);
760
861
  }
761
862
  async function writeStatusConfig(statuses) {
762
- const configPath = resolve5(syntaurRoot(), "config.md");
863
+ const configPath = resolve3(syntaurRoot(), "config.md");
763
864
  const statusBlock = serializeStatusConfig(statuses);
764
865
  if (!await fileExists(configPath)) {
765
866
  const content = `---
@@ -771,7 +872,7 @@ ${statusBlock}
771
872
  await writeFileForce(configPath, content);
772
873
  return;
773
874
  }
774
- const existing = await readFile4(configPath, "utf-8");
875
+ const existing = await readFile2(configPath, "utf-8");
775
876
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
776
877
  if (!fmMatch) {
777
878
  const content = `---
@@ -813,9 +914,9 @@ ${statusBlock}
813
914
  await writeFileForce(configPath, newContent);
814
915
  }
815
916
  async function deleteStatusConfig() {
816
- const configPath = resolve5(syntaurRoot(), "config.md");
917
+ const configPath = resolve3(syntaurRoot(), "config.md");
817
918
  if (!await fileExists(configPath)) return;
818
- const existing = await readFile4(configPath, "utf-8");
919
+ const existing = await readFile2(configPath, "utf-8");
819
920
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
820
921
  if (!fmMatch) return;
821
922
  const fmBlock = fmMatch[2];
@@ -827,13 +928,13 @@ ${cleanedFm}
827
928
  await writeFileForce(configPath, newContent);
828
929
  }
829
930
  async function updateIntegrationConfig(integrations) {
830
- const configPath = resolve5(syntaurRoot(), "config.md");
931
+ const configPath = resolve3(syntaurRoot(), "config.md");
831
932
  const nextIntegrations = {
832
933
  ...(await readConfig()).integrations,
833
934
  ...integrations
834
935
  };
835
936
  const integrationBlock = serializeIntegrationConfig(nextIntegrations);
836
- const existing = await fileExists(configPath) ? await readFile4(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
937
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
837
938
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
838
939
  if (!fmMatch) {
839
940
  const content = `---
@@ -857,13 +958,13 @@ ${normalizedFm}
857
958
  await writeFileForce(configPath, newContent);
858
959
  }
859
960
  async function updateOnboardingConfig(onboarding) {
860
- const configPath = resolve5(syntaurRoot(), "config.md");
961
+ const configPath = resolve3(syntaurRoot(), "config.md");
861
962
  const nextOnboarding = {
862
963
  ...(await readConfig()).onboarding,
863
964
  ...onboarding
864
965
  };
865
966
  const onboardingBlock = serializeOnboardingConfig(nextOnboarding);
866
- const existing = await fileExists(configPath) ? await readFile4(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
967
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
867
968
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
868
969
  if (!fmMatch) {
869
970
  const content = `---
@@ -887,7 +988,7 @@ ${normalizedFm}
887
988
  await writeFileForce(configPath, newContent);
888
989
  }
889
990
  async function updateBackupConfig(backup) {
890
- const configPath = resolve5(syntaurRoot(), "config.md");
991
+ const configPath = resolve3(syntaurRoot(), "config.md");
891
992
  const current = (await readConfig()).backup;
892
993
  const nextBackup = {
893
994
  repo: current?.repo ?? null,
@@ -897,7 +998,7 @@ async function updateBackupConfig(backup) {
897
998
  ...backup
898
999
  };
899
1000
  const backupBlock = serializeBackupConfig(nextBackup);
900
- const existing = await fileExists(configPath) ? await readFile4(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1001
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
901
1002
  const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
902
1003
  if (!fmMatch) {
903
1004
  const content = `---
@@ -921,19 +1022,19 @@ ${normalizedFm}
921
1022
  await writeFileForce(configPath, newContent);
922
1023
  }
923
1024
  async function readConfig() {
924
- const configPath = resolve5(syntaurRoot(), "config.md");
1025
+ const configPath = resolve3(syntaurRoot(), "config.md");
925
1026
  if (!await fileExists(configPath)) {
926
- return { ...DEFAULT_CONFIG };
1027
+ return cloneDefaultConfig();
927
1028
  }
928
1029
  if (!migratedConfigPaths.has(configPath)) {
929
1030
  migratedConfigPaths.add(configPath);
930
1031
  await migrateLegacyConfig(configPath);
931
1032
  }
932
- const content = await readFile4(configPath, "utf-8");
1033
+ const content = await readFile2(configPath, "utf-8");
933
1034
  const fm = parseFrontmatter(content);
934
1035
  if (Object.keys(fm).length === 0) {
935
1036
  console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
936
- return { ...DEFAULT_CONFIG };
1037
+ return cloneDefaultConfig();
937
1038
  }
938
1039
  let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
939
1040
  if (!isAbsolute(projectDir)) {
@@ -942,6 +1043,7 @@ async function readConfig() {
942
1043
  );
943
1044
  projectDir = DEFAULT_CONFIG.defaultProjectDir;
944
1045
  }
1046
+ const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
945
1047
  return {
946
1048
  version: fm["version"] || DEFAULT_CONFIG.version,
947
1049
  defaultProjectDir: projectDir,
@@ -973,7 +1075,8 @@ async function readConfig() {
973
1075
  lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
974
1076
  } : null,
975
1077
  statuses: parseStatusConfig(content),
976
- types: null
1078
+ types: null,
1079
+ playbooks: parsePlaybooksConfig(fmBlock)
977
1080
  };
978
1081
  }
979
1082
  var DEFAULT_CONFIG, migratedConfigPaths;
@@ -1001,12 +1104,119 @@ var init_config2 = __esm({
1001
1104
  },
1002
1105
  backup: null,
1003
1106
  statuses: null,
1004
- types: null
1107
+ types: null,
1108
+ playbooks: {
1109
+ disabled: []
1110
+ }
1005
1111
  };
1006
1112
  migratedConfigPaths = /* @__PURE__ */ new Set();
1007
1113
  }
1008
1114
  });
1009
1115
 
1116
+ // src/utils/playbooks.ts
1117
+ import { resolve as resolve4 } from "path";
1118
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
1119
+ function isVisiblePlaybookFile(name, isFile) {
1120
+ return isFile && name.endsWith(".md") && !name.startsWith("_") && name !== "manifest.md";
1121
+ }
1122
+ async function resolvePlaybookSlug(playbooksDir3, slug) {
1123
+ if (!await fileExists(playbooksDir3)) return null;
1124
+ const entries = await readdir2(playbooksDir3, { withFileTypes: true });
1125
+ let filenameStemFallback = null;
1126
+ for (const entry of entries) {
1127
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
1128
+ const filePath = resolve4(playbooksDir3, entry.name);
1129
+ const raw = await readFile3(filePath, "utf-8");
1130
+ const parsed = parsePlaybook(raw);
1131
+ const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
1132
+ if (canonical === slug) {
1133
+ return { filename: entry.name, slug: canonical, parsed };
1134
+ }
1135
+ if (!parsed.slug && entry.name.replace(/\.md$/, "") === slug) {
1136
+ filenameStemFallback = { filename: entry.name, slug: canonical, parsed };
1137
+ }
1138
+ }
1139
+ return filenameStemFallback;
1140
+ }
1141
+ async function setPlaybookEnabled(playbooksDir3, slug, enabled) {
1142
+ const resolved = await resolvePlaybookSlug(playbooksDir3, slug);
1143
+ if (!resolved) {
1144
+ throw new Error(`Playbook "${slug}" not found in ${playbooksDir3}`);
1145
+ }
1146
+ const config = await readConfig();
1147
+ const disabledSet = new Set(config.playbooks.disabled);
1148
+ const wasDisabled = disabledSet.has(resolved.slug);
1149
+ const shouldBeDisabled = !enabled;
1150
+ if (wasDisabled === shouldBeDisabled) {
1151
+ return { slug: resolved.slug, enabled, changed: false };
1152
+ }
1153
+ if (shouldBeDisabled) {
1154
+ disabledSet.add(resolved.slug);
1155
+ } else {
1156
+ disabledSet.delete(resolved.slug);
1157
+ }
1158
+ await updatePlaybooksConfig({ disabled: Array.from(disabledSet).sort() });
1159
+ await rebuildPlaybookManifest(playbooksDir3);
1160
+ return { slug: resolved.slug, enabled, changed: true };
1161
+ }
1162
+ async function removeFromDisabledList(slug) {
1163
+ const config = await readConfig();
1164
+ if (!config.playbooks.disabled.includes(slug)) return;
1165
+ await updatePlaybooksConfig({
1166
+ disabled: config.playbooks.disabled.filter((s) => s !== slug)
1167
+ });
1168
+ }
1169
+ async function rebuildPlaybookManifest(playbooksDir3) {
1170
+ if (!await fileExists(playbooksDir3)) return;
1171
+ const config = await readConfig();
1172
+ const disabledSet = new Set(config.playbooks.disabled);
1173
+ const entries = await readdir2(playbooksDir3, { withFileTypes: true });
1174
+ const rows = [];
1175
+ for (const entry of entries) {
1176
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
1177
+ const raw = await readFile3(resolve4(playbooksDir3, entry.name), "utf-8");
1178
+ const parsed = parsePlaybook(raw);
1179
+ const slug = parsed.slug || entry.name.replace(/\.md$/, "");
1180
+ if (disabledSet.has(slug)) continue;
1181
+ rows.push({
1182
+ name: parsed.name || slug,
1183
+ slug,
1184
+ description: parsed.description,
1185
+ whenToUse: parsed.whenToUse
1186
+ });
1187
+ }
1188
+ rows.sort((a, b) => a.name.localeCompare(b.name));
1189
+ const timestamp = nowTimestamp();
1190
+ const lines = [
1191
+ "---",
1192
+ `generated: "${timestamp}"`,
1193
+ `total: ${rows.length}`,
1194
+ "---",
1195
+ "",
1196
+ "# Playbooks",
1197
+ "",
1198
+ "Behavioral rules for AI agents. Read and follow all playbooks before starting work.",
1199
+ ""
1200
+ ];
1201
+ for (const row of rows) {
1202
+ lines.push(`- **[${row.name}](${row.slug}.md)** \u2014 ${row.description}`);
1203
+ if (row.whenToUse) {
1204
+ lines.push(` _When to use: ${row.whenToUse}_`);
1205
+ }
1206
+ }
1207
+ lines.push("");
1208
+ await writeFileForce(resolve4(playbooksDir3, "manifest.md"), lines.join("\n"));
1209
+ }
1210
+ var init_playbooks = __esm({
1211
+ "src/utils/playbooks.ts"() {
1212
+ "use strict";
1213
+ init_fs();
1214
+ init_parser();
1215
+ init_timestamp();
1216
+ init_config2();
1217
+ }
1218
+ });
1219
+
1010
1220
  // src/lifecycle/types.ts
1011
1221
  var DEFAULT_STATUSES, DEFAULT_TERMINAL_STATUSES;
1012
1222
  var init_types = __esm({
@@ -1409,12 +1619,20 @@ async function resolveAssignmentById(projectsDir2, assignmentsDir2, id) {
1409
1619
  const standaloneDir = resolve9(assignmentsDir2, id);
1410
1620
  const standalonePath = resolve9(standaloneDir, "assignment.md");
1411
1621
  if (await fileExists(standalonePath)) {
1622
+ let workspaceGroup = null;
1623
+ try {
1624
+ const content = await readFile6(standalonePath, "utf-8");
1625
+ const [fm] = extractFrontmatter(content);
1626
+ workspaceGroup = getField(fm, "workspaceGroup");
1627
+ } catch {
1628
+ }
1412
1629
  standaloneMatch = {
1413
1630
  assignmentDir: standaloneDir,
1414
1631
  projectSlug: null,
1415
1632
  assignmentSlug: id,
1416
1633
  id,
1417
- standalone: true
1634
+ standalone: true,
1635
+ workspaceGroup
1418
1636
  };
1419
1637
  }
1420
1638
  if (await fileExists(projectsDir2)) {
@@ -1440,7 +1658,8 @@ async function resolveAssignmentById(projectsDir2, assignmentsDir2, id) {
1440
1658
  projectSlug: p.name,
1441
1659
  assignmentSlug: a.name,
1442
1660
  id,
1443
- standalone: false
1661
+ standalone: false,
1662
+ workspaceGroup: null
1444
1663
  };
1445
1664
  break;
1446
1665
  }
@@ -1816,8 +2035,18 @@ var init_help = __esm({
1816
2035
  },
1817
2036
  {
1818
2037
  command: "syntaur list-playbooks",
1819
- description: "List all playbooks in the Syntaur home directory.",
1820
- example: "syntaur list-playbooks"
2038
+ description: "List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.",
2039
+ example: "syntaur list-playbooks --all"
2040
+ },
2041
+ {
2042
+ command: "syntaur enable-playbook",
2043
+ description: "Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.",
2044
+ example: "syntaur enable-playbook commit-discipline"
2045
+ },
2046
+ {
2047
+ command: "syntaur disable-playbook",
2048
+ description: "Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.",
2049
+ example: "syntaur disable-playbook commit-discipline"
1821
2050
  }
1822
2051
  ];
1823
2052
  WORKFLOW = [
@@ -2541,10 +2770,11 @@ async function writeWorkspaceRegistry(projectsDir2, workspaces) {
2541
2770
  const registryPath = resolve12(dirname3(projectsDir2), "workspaces.json");
2542
2771
  await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
2543
2772
  }
2544
- async function listWorkspaces(projectsDir2) {
2545
- const [projectRecords, registered] = await Promise.all([
2773
+ async function listWorkspaces(projectsDir2, assignmentsDir2) {
2774
+ const [projectRecords, registered, standaloneRecords] = await Promise.all([
2546
2775
  listProjectRecords(projectsDir2),
2547
- readWorkspaceRegistry(projectsDir2)
2776
+ readWorkspaceRegistry(projectsDir2),
2777
+ listStandaloneRecords(assignmentsDir2)
2548
2778
  ]);
2549
2779
  const workspaceSet = new Set(registered);
2550
2780
  let hasUngrouped = false;
@@ -2555,6 +2785,13 @@ async function listWorkspaces(projectsDir2) {
2555
2785
  hasUngrouped = true;
2556
2786
  }
2557
2787
  }
2788
+ for (const sr of standaloneRecords) {
2789
+ if (sr.record.workspaceGroup) {
2790
+ workspaceSet.add(sr.record.workspaceGroup);
2791
+ } else {
2792
+ hasUngrouped = true;
2793
+ }
2794
+ }
2558
2795
  const workspaces = Array.from(workspaceSet).sort();
2559
2796
  return { workspaces, hasUngrouped };
2560
2797
  }
@@ -2703,7 +2940,7 @@ async function toStandaloneBoardItem(sr) {
2703
2940
  projectSlug: null,
2704
2941
  projectTitle: null,
2705
2942
  blockedReason: sr.record.blockedReason,
2706
- projectWorkspace: null,
2943
+ projectWorkspace: sr.record.workspaceGroup ?? null,
2707
2944
  availableTransitions: await getStandaloneAvailableTransitions(sr.record)
2708
2945
  };
2709
2946
  }
@@ -3644,6 +3881,8 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
3644
3881
  }
3645
3882
  async function listPlaybooks(playbooksDir3) {
3646
3883
  if (!await fileExists(playbooksDir3)) return [];
3884
+ const config = await readConfig();
3885
+ const disabledSet = new Set(config.playbooks.disabled);
3647
3886
  const entries = await readdir7(playbooksDir3, { withFileTypes: true });
3648
3887
  const playbooks = [];
3649
3888
  for (const entry of entries) {
@@ -3659,25 +3898,28 @@ async function listPlaybooks(playbooksDir3) {
3659
3898
  whenToUse: parsed.whenToUse,
3660
3899
  tags: parsed.tags,
3661
3900
  created: parsed.created,
3662
- updated: parsed.updated
3901
+ updated: parsed.updated,
3902
+ enabled: !disabledSet.has(slug)
3663
3903
  });
3664
3904
  }
3665
3905
  return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
3666
3906
  }
3667
3907
  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);
3908
+ const resolved = await resolvePlaybookSlug(playbooksDir3, slug);
3909
+ if (!resolved) return null;
3910
+ const config = await readConfig();
3911
+ const enabled = !config.playbooks.disabled.includes(resolved.slug);
3912
+ const parsed = resolved.parsed;
3672
3913
  return {
3673
- slug: parsed.slug || slug,
3674
- name: parsed.name || slug,
3914
+ slug: resolved.slug,
3915
+ name: parsed.name || resolved.slug,
3675
3916
  description: parsed.description,
3676
3917
  whenToUse: parsed.whenToUse,
3677
3918
  tags: parsed.tags,
3678
3919
  created: parsed.created,
3679
3920
  updated: parsed.updated,
3680
- body: parsed.body
3921
+ body: parsed.body,
3922
+ enabled
3681
3923
  };
3682
3924
  }
3683
3925
  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 +3929,7 @@ var init_api = __esm({
3687
3929
  init_lifecycle();
3688
3930
  init_fs();
3689
3931
  init_config2();
3932
+ init_playbooks();
3690
3933
  init_fs_migration();
3691
3934
  init_assignment_resolver();
3692
3935
  init_parser();
@@ -4546,8 +4789,8 @@ __export(launch_exports, {
4546
4789
  launchAgent: () => launchAgent
4547
4790
  });
4548
4791
  import { spawn as spawn2 } from "child_process";
4549
- import { mkdir as mkdir4, writeFile as writeFile9 } from "fs/promises";
4550
- import { resolve as resolve31 } from "path";
4792
+ import { mkdir as mkdir5, writeFile as writeFile9 } from "fs/promises";
4793
+ import { resolve as resolve32 } from "path";
4551
4794
  async function launchAgent(options) {
4552
4795
  const { projectsDir: projectsDir2, projectSlug, assignmentSlug, agent } = options;
4553
4796
  const command = AGENT_COMMANDS[agent];
@@ -4557,10 +4800,10 @@ async function launchAgent(options) {
4557
4800
  process.exit(1);
4558
4801
  }
4559
4802
  const workspaceDir = detail.workspace.worktreePath ?? (detail.workspace.repository?.startsWith("/") ? detail.workspace.repository : null) ?? process.cwd();
4560
- const projectDir = resolve31(projectsDir2, projectSlug);
4561
- const assignmentDir = resolve31(projectDir, "assignments", assignmentSlug);
4562
- const contextDir = resolve31(workspaceDir, ".syntaur");
4563
- await mkdir4(contextDir, { recursive: true });
4803
+ const projectDir = resolve32(projectsDir2, projectSlug);
4804
+ const assignmentDir = resolve32(projectDir, "assignments", assignmentSlug);
4805
+ const contextDir = resolve32(workspaceDir, ".syntaur");
4806
+ await mkdir5(contextDir, { recursive: true });
4564
4807
  const context = {
4565
4808
  projectSlug,
4566
4809
  assignmentSlug,
@@ -4572,7 +4815,7 @@ async function launchAgent(options) {
4572
4815
  grabbedAt: (/* @__PURE__ */ new Date()).toISOString()
4573
4816
  };
4574
4817
  await writeFile9(
4575
- resolve31(contextDir, "context.json"),
4818
+ resolve32(contextDir, "context.json"),
4576
4819
  JSON.stringify(context, null, 2) + "\n"
4577
4820
  );
4578
4821
  return new Promise((resolvePromise, reject) => {
@@ -4613,62 +4856,16 @@ import { Command as Command4 } from "commander";
4613
4856
  init_paths();
4614
4857
  init_fs();
4615
4858
  init_config();
4616
- import { resolve as resolve3, dirname as dirname2 } from "path";
4617
- import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
4859
+ init_playbooks();
4860
+ import { resolve as resolve5, dirname as dirname2 } from "path";
4861
+ import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
4618
4862
  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
4863
  async function initCommand(options) {
4667
4864
  const root = syntaurRoot();
4668
4865
  const projectsDir2 = defaultProjectDir();
4669
4866
  const standaloneAssignmentsDir = assignmentsDir();
4670
- const configPath = resolve3(root, "config.md");
4671
- const playbooksDir3 = resolve3(root, "playbooks");
4867
+ const configPath = resolve5(root, "config.md");
4868
+ const playbooksDir3 = resolve5(root, "playbooks");
4672
4869
  await ensureDir(root);
4673
4870
  await ensureDir(projectsDir2);
4674
4871
  await ensureDir(standaloneAssignmentsDir);
@@ -4704,16 +4901,16 @@ async function initCommand(options) {
4704
4901
  }
4705
4902
  async function seedDefaultPlaybooks(playbooksDir3) {
4706
4903
  const __filename = fileURLToPath(import.meta.url);
4707
- const packageRoot = resolve3(dirname2(__filename), "..");
4708
- const examplesDir = resolve3(packageRoot, "examples", "playbooks");
4904
+ const packageRoot = resolve5(dirname2(__filename), "..");
4905
+ const examplesDir = resolve5(packageRoot, "examples", "playbooks");
4709
4906
  if (!await fileExists(examplesDir)) return 0;
4710
- const entries = await readdir2(examplesDir, { withFileTypes: true });
4907
+ const entries = await readdir3(examplesDir, { withFileTypes: true });
4711
4908
  let count = 0;
4712
4909
  for (const entry of entries) {
4713
4910
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4714
- const targetPath = resolve3(playbooksDir3, entry.name);
4911
+ const targetPath = resolve5(playbooksDir3, entry.name);
4715
4912
  if (await fileExists(targetPath)) continue;
4716
- const content = await readFile2(resolve3(examplesDir, entry.name), "utf-8");
4913
+ const content = await readFile4(resolve5(examplesDir, entry.name), "utf-8");
4717
4914
  await writeFileSafe(targetPath, content);
4718
4915
  count++;
4719
4916
  }
@@ -4749,14 +4946,14 @@ init_config2();
4749
4946
  init_config();
4750
4947
 
4751
4948
  // src/templates/manifest.ts
4752
- function renderManifest(params) {
4949
+ function renderManifest(params2) {
4753
4950
  return `---
4754
4951
  version: "2.0"
4755
- project: ${params.slug}
4756
- generated: "${params.timestamp}"
4952
+ project: ${params2.slug}
4953
+ generated: "${params2.timestamp}"
4757
4954
  ---
4758
4955
 
4759
- # Project: ${params.slug}
4956
+ # Project: ${params2.slug}
4760
4957
 
4761
4958
  ## Overview
4762
4959
  - [Project Overview](./project.md)
@@ -4783,24 +4980,24 @@ function escapeYamlString(value) {
4783
4980
  }
4784
4981
 
4785
4982
  // src/templates/project.ts
4786
- function renderProject(params) {
4787
- const safeTitle = escapeYamlString(params.title);
4788
- const workspaceLine = params.workspace ? `
4789
- workspace: ${params.workspace}` : "";
4983
+ function renderProject(params2) {
4984
+ const safeTitle = escapeYamlString(params2.title);
4985
+ const workspaceLine = params2.workspace ? `
4986
+ workspace: ${params2.workspace}` : "";
4790
4987
  return `---
4791
- id: ${params.id}
4792
- slug: ${params.slug}
4988
+ id: ${params2.id}
4989
+ slug: ${params2.slug}
4793
4990
  title: ${safeTitle}
4794
4991
  archived: false
4795
4992
  archivedAt: null
4796
4993
  archivedReason: null
4797
- created: "${params.timestamp}"
4798
- updated: "${params.timestamp}"
4994
+ created: "${params2.timestamp}"
4995
+ updated: "${params2.timestamp}"
4799
4996
  externalIds: []
4800
4997
  tags: []${workspaceLine}
4801
4998
  ---
4802
4999
 
4803
- # ${params.title}
5000
+ # ${params2.title}
4804
5001
 
4805
5002
  ## Overview
4806
5003
 
@@ -4813,24 +5010,37 @@ tags: []${workspaceLine}
4813
5010
  }
4814
5011
 
4815
5012
  // src/templates/assignment.ts
4816
- function renderAssignment(params) {
4817
- const safeTitle = escapeYamlString(params.title);
4818
- const dependsOnYaml = params.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
4819
- - ${params.dependsOn.join("\n - ")}`;
4820
- const linksYaml = params.links.length === 0 ? "links: []" : `links:
4821
- - ${params.links.join("\n - ")}`;
4822
- const projectYaml = `project: ${params.project == null ? "null" : params.project}`;
4823
- const typeYaml = `type: ${params.type ?? "feature"}`;
5013
+ function renderAssignment(params2) {
5014
+ const safeTitle = escapeYamlString(params2.title);
5015
+ const dependsOnYaml = params2.dependsOn.length === 0 ? "dependsOn: []" : `dependsOn:
5016
+ - ${params2.dependsOn.join("\n - ")}`;
5017
+ const linksYaml = params2.links.length === 0 ? "links: []" : `links:
5018
+ - ${params2.links.join("\n - ")}`;
5019
+ const projectYaml = `project: ${params2.project == null ? "null" : params2.project}`;
5020
+ const workspaceGroupLine = params2.workspaceGroup ? `
5021
+ workspaceGroup: ${params2.workspaceGroup}` : "";
5022
+ const typeYaml = `type: ${params2.type ?? "feature"}`;
5023
+ const todosSection = params2.includeTodos ? `## Todos
5024
+
5025
+ <!--
5026
+ Checklist of work items for this assignment. Items may be simple tasks
5027
+ or a markdown link to a plan file (e.g., "- [ ] Execute [plan](./plan.md)").
5028
+ When a plan is superseded by a new one, mark the old todo as:
5029
+ - [x] ~~Execute [old plan](./plan.md)~~ (superseded by plan-v2)
5030
+ Never delete superseded todos \u2014 preserve the history.
5031
+ -->
5032
+
5033
+ ` : "";
4824
5034
  return `---
4825
- id: ${params.id}
4826
- slug: ${params.slug}
5035
+ id: ${params2.id}
5036
+ slug: ${params2.slug}
4827
5037
  title: ${safeTitle}
4828
- ${projectYaml}
5038
+ ${projectYaml}${workspaceGroupLine}
4829
5039
  ${typeYaml}
4830
5040
  status: pending
4831
- priority: ${params.priority}
4832
- created: "${params.timestamp}"
4833
- updated: "${params.timestamp}"
5041
+ priority: ${params2.priority}
5042
+ created: "${params2.timestamp}"
5043
+ updated: "${params2.timestamp}"
4834
5044
  assignee: null
4835
5045
  externalIds: []
4836
5046
  ${dependsOnYaml}
@@ -4844,7 +5054,7 @@ workspace:
4844
5054
  tags: []
4845
5055
  ---
4846
5056
 
4847
- # ${params.title}
5057
+ # ${params2.title}
4848
5058
 
4849
5059
  ## Objective
4850
5060
 
@@ -4856,17 +5066,7 @@ tags: []
4856
5066
  - [ ] <!-- criterion 2 -->
4857
5067
  - [ ] <!-- criterion 3 -->
4858
5068
 
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
5069
+ ${todosSection}## Context
4870
5070
 
4871
5071
  <!-- Links to relevant docs, code, or other assignments. -->
4872
5072
 
@@ -4881,10 +5081,10 @@ Never delete superseded todos \u2014 preserve the history.
4881
5081
  }
4882
5082
 
4883
5083
  // src/templates/scratchpad.ts
4884
- function renderScratchpad(params) {
5084
+ function renderScratchpad(params2) {
4885
5085
  return `---
4886
- assignment: ${params.assignmentSlug}
4887
- updated: "${params.timestamp}"
5086
+ assignment: ${params2.assignmentSlug}
5087
+ updated: "${params2.timestamp}"
4888
5088
  ---
4889
5089
 
4890
5090
  # Scratchpad
@@ -4894,10 +5094,10 @@ No working notes yet.
4894
5094
  }
4895
5095
 
4896
5096
  // src/templates/handoff.ts
4897
- function renderHandoff(params) {
5097
+ function renderHandoff(params2) {
4898
5098
  return `---
4899
- assignment: ${params.assignmentSlug}
4900
- updated: "${params.timestamp}"
5099
+ assignment: ${params2.assignmentSlug}
5100
+ updated: "${params2.timestamp}"
4901
5101
  handoffCount: 0
4902
5102
  ---
4903
5103
 
@@ -4908,12 +5108,12 @@ No handoffs recorded yet.
4908
5108
  }
4909
5109
 
4910
5110
  // src/templates/progress.ts
4911
- function renderProgress(params) {
5111
+ function renderProgress(params2) {
4912
5112
  return `---
4913
- assignment: ${params.assignment}
5113
+ assignment: ${params2.assignment}
4914
5114
  entryCount: 0
4915
- generated: "${params.timestamp}"
4916
- updated: "${params.timestamp}"
5115
+ generated: "${params2.timestamp}"
5116
+ updated: "${params2.timestamp}"
4917
5117
  ---
4918
5118
 
4919
5119
  # Progress
@@ -4923,12 +5123,12 @@ No progress yet.
4923
5123
  }
4924
5124
 
4925
5125
  // src/templates/comments.ts
4926
- function renderComments(params) {
5126
+ function renderComments(params2) {
4927
5127
  return `---
4928
- assignment: ${params.assignment}
5128
+ assignment: ${params2.assignment}
4929
5129
  entryCount: 0
4930
- generated: "${params.timestamp}"
4931
- updated: "${params.timestamp}"
5130
+ generated: "${params2.timestamp}"
5131
+ updated: "${params2.timestamp}"
4932
5132
  ---
4933
5133
 
4934
5134
  # Comments
@@ -4956,10 +5156,10 @@ function formatCommentEntry(comment) {
4956
5156
  }
4957
5157
 
4958
5158
  // src/templates/decision-record.ts
4959
- function renderDecisionRecord(params) {
5159
+ function renderDecisionRecord(params2) {
4960
5160
  return `---
4961
- assignment: ${params.assignmentSlug}
4962
- updated: "${params.timestamp}"
5161
+ assignment: ${params2.assignmentSlug}
5162
+ updated: "${params2.timestamp}"
4963
5163
  decisionCount: 0
4964
5164
  ---
4965
5165
 
@@ -4970,10 +5170,10 @@ No decisions recorded yet.
4970
5170
  }
4971
5171
 
4972
5172
  // src/templates/index-stubs.ts
4973
- function renderIndexAssignments(params) {
5173
+ function renderIndexAssignments(params2) {
4974
5174
  return `---
4975
- project: ${params.slug}
4976
- generated: "${params.timestamp}"
5175
+ project: ${params2.slug}
5176
+ generated: "${params2.timestamp}"
4977
5177
  total: 0
4978
5178
  by_status:
4979
5179
  pending: 0
@@ -4990,10 +5190,10 @@ by_status:
4990
5190
  |------|-------|--------|----------|----------|--------------|---------|
4991
5191
  `;
4992
5192
  }
4993
- function renderIndexPlans(params) {
5193
+ function renderIndexPlans(params2) {
4994
5194
  return `---
4995
- project: ${params.slug}
4996
- generated: "${params.timestamp}"
5195
+ project: ${params2.slug}
5196
+ generated: "${params2.timestamp}"
4997
5197
  ---
4998
5198
 
4999
5199
  # Plans
@@ -5002,10 +5202,10 @@ generated: "${params.timestamp}"
5002
5202
  |------------|-------------|---------|
5003
5203
  `;
5004
5204
  }
5005
- function renderIndexDecisions(params) {
5205
+ function renderIndexDecisions(params2) {
5006
5206
  return `---
5007
- project: ${params.slug}
5008
- generated: "${params.timestamp}"
5207
+ project: ${params2.slug}
5208
+ generated: "${params2.timestamp}"
5009
5209
  ---
5010
5210
 
5011
5211
  # Decision Records
@@ -5014,10 +5214,10 @@ generated: "${params.timestamp}"
5014
5214
  |------------|-------|-----------------|---------------|---------|
5015
5215
  `;
5016
5216
  }
5017
- function renderStatus(params) {
5217
+ function renderStatus(params2) {
5018
5218
  return `---
5019
- project: ${params.slug}
5020
- generated: "${params.timestamp}"
5219
+ project: ${params2.slug}
5220
+ generated: "${params2.timestamp}"
5021
5221
  status: pending
5022
5222
  progress:
5023
5223
  total: 0
@@ -5033,7 +5233,7 @@ needsAttention:
5033
5233
  openQuestions: 0
5034
5234
  ---
5035
5235
 
5036
- # Project Status: ${params.title}
5236
+ # Project Status: ${params2.title}
5037
5237
 
5038
5238
  **Status:** pending
5039
5239
  **Progress:** 0/0 assignments complete
@@ -5053,10 +5253,10 @@ No dependencies yet.
5053
5253
  - **0 unanswered** questions
5054
5254
  `;
5055
5255
  }
5056
- function renderResourcesIndex(params) {
5256
+ function renderResourcesIndex(params2) {
5057
5257
  return `---
5058
- project: ${params.slug}
5059
- generated: "${params.timestamp}"
5258
+ project: ${params2.slug}
5259
+ generated: "${params2.timestamp}"
5060
5260
  total: 0
5061
5261
  ---
5062
5262
 
@@ -5066,10 +5266,10 @@ total: 0
5066
5266
  |------|----------|--------|---------------------|---------|
5067
5267
  `;
5068
5268
  }
5069
- function renderMemoriesIndex(params) {
5269
+ function renderMemoriesIndex(params2) {
5070
5270
  return `---
5071
- project: ${params.slug}
5072
- generated: "${params.timestamp}"
5271
+ project: ${params2.slug}
5272
+ generated: "${params2.timestamp}"
5073
5273
  total: 0
5074
5274
  ---
5075
5275
 
@@ -5081,19 +5281,19 @@ total: 0
5081
5281
  }
5082
5282
 
5083
5283
  // src/templates/playbook.ts
5084
- function renderPlaybook(params) {
5085
- const whenToUse = params.whenToUse ? escapeYamlString(params.whenToUse) : "null";
5284
+ function renderPlaybook(params2) {
5285
+ const whenToUse = params2.whenToUse ? escapeYamlString(params2.whenToUse) : "null";
5086
5286
  return `---
5087
- name: ${escapeYamlString(params.name)}
5088
- slug: ${params.slug}
5089
- description: ${escapeYamlString(params.description)}
5287
+ name: ${escapeYamlString(params2.name)}
5288
+ slug: ${params2.slug}
5289
+ description: ${escapeYamlString(params2.description)}
5090
5290
  when_to_use: ${whenToUse}
5091
- created: "${params.timestamp}"
5092
- updated: "${params.timestamp}"
5291
+ created: "${params2.timestamp}"
5292
+ updated: "${params2.timestamp}"
5093
5293
  tags: []
5094
5294
  ---
5095
5295
 
5096
- # ${params.name}
5296
+ # ${params2.name}
5097
5297
 
5098
5298
  <!-- Write imperative rules and workflows here. Keep it under 50 lines. -->
5099
5299
  `;
@@ -5236,58 +5436,58 @@ Follow the rules in each playbook. They take precedence over default conventions
5236
5436
  - Commit frequently with messages referencing the assignment slug.
5237
5437
  `;
5238
5438
  }
5239
- function renderCursorAssignment(params) {
5439
+ function renderCursorAssignment(params2) {
5240
5440
  return `---
5241
- description: Syntaur assignment context for ${params.projectSlug}/${params.assignmentSlug}
5441
+ description: Syntaur assignment context for ${params2.projectSlug}/${params2.assignmentSlug}
5242
5442
  globs:
5243
5443
  alwaysApply: true
5244
5444
  ---
5245
5445
 
5246
5446
  # Current Assignment Context
5247
5447
 
5248
- - **Project:** ${params.projectSlug}
5249
- - **Assignment:** ${params.assignmentSlug}
5250
- - **Project directory:** ${params.projectDir}
5251
- - **Assignment directory:** ${params.assignmentDir}
5448
+ - **Project:** ${params2.projectSlug}
5449
+ - **Assignment:** ${params2.assignmentSlug}
5450
+ - **Project directory:** ${params2.projectDir}
5451
+ - **Assignment directory:** ${params2.assignmentDir}
5252
5452
 
5253
5453
  ## Reading Order
5254
5454
 
5255
5455
  Before starting work, read these files in order:
5256
- 1. \`${params.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
5257
- 2. \`${params.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
5258
- 3. any \`${params.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
5259
- 4. \`${params.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
5260
- 5. \`${params.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
5261
- 6. \`${params.assignmentDir}/handoff.md\` -- previous session handoff notes
5456
+ 1. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
5457
+ 2. \`${params2.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
5458
+ 3. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
5459
+ 4. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
5460
+ 5. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
5461
+ 6. \`${params2.assignmentDir}/handoff.md\` -- previous session handoff notes
5262
5462
 
5263
5463
  ## Your Writable Files
5264
5464
 
5265
5465
  You may write directly to these files inside your assignment folder:
5266
- - \`${params.assignmentDir}/assignment.md\`
5267
- - \`${params.assignmentDir}/plan*.md\` (0 or more versioned plan files, e.g., \`plan.md\`, \`plan-v2.md\`)
5268
- - \`${params.assignmentDir}/progress.md\` (append timestamped entries, newest first)
5269
- - \`${params.assignmentDir}/scratchpad.md\`
5270
- - \`${params.assignmentDir}/handoff.md\`
5271
- - \`${params.assignmentDir}/decision-record.md\`
5466
+ - \`${params2.assignmentDir}/assignment.md\`
5467
+ - \`${params2.assignmentDir}/plan*.md\` (0 or more versioned plan files, e.g., \`plan.md\`, \`plan-v2.md\`)
5468
+ - \`${params2.assignmentDir}/progress.md\` (append timestamped entries, newest first)
5469
+ - \`${params2.assignmentDir}/scratchpad.md\`
5470
+ - \`${params2.assignmentDir}/handoff.md\`
5471
+ - \`${params2.assignmentDir}/decision-record.md\`
5272
5472
 
5273
- Do NOT edit \`${params.assignmentDir}/comments.md\` directly \u2014 use \`syntaur comment\`. Do NOT edit other assignments' files \u2014 use \`syntaur request\` for cross-assignment todos.
5473
+ Do NOT edit \`${params2.assignmentDir}/comments.md\` directly \u2014 use \`syntaur comment\`. Do NOT edit other assignments' files \u2014 use \`syntaur request\` for cross-assignment todos.
5274
5474
 
5275
5475
  And source code files in your workspace. Read the \`workspace\` field from your assignment's frontmatter to determine the exact boundary. If not set, the current working directory is your workspace.
5276
5476
  `;
5277
5477
  }
5278
5478
 
5279
5479
  // src/templates/codex-agents.ts
5280
- function renderCodexAgents(params) {
5480
+ function renderCodexAgents(params2) {
5281
5481
  return `# Syntaur Protocol -- Agent Instructions
5282
5482
 
5283
5483
  This project uses the Syntaur protocol for multi-agent project coordination.
5284
5484
 
5285
5485
  ## Current Assignment
5286
5486
 
5287
- - **Project:** ${params.projectSlug}
5288
- - **Assignment:** ${params.assignmentSlug}
5289
- - **Project directory:** ${params.projectDir}
5290
- - **Assignment directory:** ${params.assignmentDir}
5487
+ - **Project:** ${params2.projectSlug}
5488
+ - **Assignment:** ${params2.assignmentSlug}
5489
+ - **Project directory:** ${params2.projectDir}
5490
+ - **Assignment directory:** ${params2.assignmentDir}
5291
5491
 
5292
5492
  ## Preferred Workflow
5293
5493
 
@@ -5307,13 +5507,13 @@ If the plugin is unavailable, follow the same workflow manually with the \`synta
5307
5507
  ## Reading Order
5308
5508
 
5309
5509
  Before starting work, read these files in order:
5310
- 1. \`${params.projectDir}/manifest.md\` -- root navigation entry point (project-nested assignments only)
5311
- 2. \`${params.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
5312
- 3. \`${params.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter now includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
5313
- 4. any \`${params.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
5314
- 5. \`${params.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
5315
- 6. \`${params.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
5316
- 7. \`${params.assignmentDir}/handoff.md\` -- previous session handoff notes
5510
+ 1. \`${params2.projectDir}/manifest.md\` -- root navigation entry point (project-nested assignments only)
5511
+ 2. \`${params2.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
5512
+ 3. \`${params2.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter now includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
5513
+ 4. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
5514
+ 5. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
5515
+ 6. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
5516
+ 7. \`${params2.assignmentDir}/handoff.md\` -- previous session handoff notes
5317
5517
 
5318
5518
  ## Context File
5319
5519
 
@@ -5365,10 +5565,10 @@ Before starting work, read these files in order:
5365
5565
  ### Files you may WRITE:
5366
5566
  1. **Your assignment folder** -- only the assignment you are currently working on:
5367
5567
  - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\`, \`decision-record.md\`
5368
- - Path: \`${params.assignmentDir}/\`
5568
+ - Path: \`${params2.assignmentDir}/\`
5369
5569
  2. **Shared resources and memories** at the project level:
5370
- - \`${params.projectDir}/resources/<slug>.md\`
5371
- - \`${params.projectDir}/memories/<slug>.md\`
5570
+ - \`${params2.projectDir}/resources/<slug>.md\`
5571
+ - \`${params2.projectDir}/memories/<slug>.md\`
5372
5572
  3. **Your workspace** -- source code files in the current working directory (the directory where this AGENTS.md lives). If your assignment's frontmatter specifies a \`workspace\` field, read it at runtime to determine the exact boundary.
5373
5573
 
5374
5574
  > **Note:** Workspace boundaries are resolved by the agent at runtime by reading \`assignment.md\` frontmatter. If no \`workspace\` field is set, treat the current working directory as your workspace.
@@ -5413,15 +5613,15 @@ Before starting work, read these files in order:
5413
5613
  ## Lifecycle Commands
5414
5614
 
5415
5615
  Use the \`syntaur\` CLI for state transitions and coordination:
5416
- - \`syntaur assign ${params.assignmentSlug} --agent <name> --project ${params.projectSlug}\` -- set assignee
5417
- - \`syntaur start ${params.assignmentSlug} --project ${params.projectSlug}\` -- pending -> in_progress
5418
- - \`syntaur review ${params.assignmentSlug} --project ${params.projectSlug}\` -- in_progress -> review
5419
- - \`syntaur complete ${params.assignmentSlug} --project ${params.projectSlug}\` -- in_progress/review -> completed
5420
- - \`syntaur block ${params.assignmentSlug} --project ${params.projectSlug} --reason <text>\` -- block
5421
- - \`syntaur unblock ${params.assignmentSlug} --project ${params.projectSlug}\` -- unblock
5422
- - \`syntaur fail ${params.assignmentSlug} --project ${params.projectSlug}\` -- mark as failed
5423
- - \`syntaur comment ${params.assignmentSlug} "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (use for all Q&A; questions support resolve toggle)
5424
- - \`syntaur request ${params.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params.assignmentSlug})\`
5616
+ - \`syntaur assign ${params2.assignmentSlug} --agent <name> --project ${params2.projectSlug}\` -- set assignee
5617
+ - \`syntaur start ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- pending -> in_progress
5618
+ - \`syntaur review ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress -> review
5619
+ - \`syntaur complete ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- in_progress/review -> completed
5620
+ - \`syntaur block ${params2.assignmentSlug} --project ${params2.projectSlug} --reason <text>\` -- block
5621
+ - \`syntaur unblock ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- unblock
5622
+ - \`syntaur fail ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- mark as failed
5623
+ - \`syntaur comment ${params2.assignmentSlug} "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (use for all Q&A; questions support resolve toggle)
5624
+ - \`syntaur request ${params2.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params2.assignmentSlug})\`
5425
5625
 
5426
5626
  ## Troubleshooting
5427
5627
 
@@ -5452,11 +5652,11 @@ Read each linked playbook and follow the rules in its body section. The \`when_t
5452
5652
  }
5453
5653
 
5454
5654
  // src/templates/opencode-config.ts
5455
- function renderOpenCodeConfig(params) {
5655
+ function renderOpenCodeConfig(params2) {
5456
5656
  const config = {
5457
5657
  instructions: [
5458
5658
  `Read AGENTS.md in this directory for Syntaur protocol (v2.0) instructions.`,
5459
- `Read ${params.projectDir}/project.md for project overview (project-nested assignments only).`,
5659
+ `Read ${params2.projectDir}/project.md for project overview (project-nested assignments only).`,
5460
5660
  `Append timestamped progress entries to the assignment's progress.md (not to assignment.md).`,
5461
5661
  `Use 'syntaur comment <slug-or-uuid> "body" --type question|note|feedback' to append to comments.md \u2014 never edit it directly.`,
5462
5662
  `Use 'syntaur request <source> <target> "text"' to append a todo to another assignment's ## Todos.`,
@@ -5552,6 +5752,14 @@ async function createAssignmentCommand(title, options) {
5552
5752
  if (!title.trim()) {
5553
5753
  throw new Error("Assignment title cannot be empty.");
5554
5754
  }
5755
+ if (options.workspace && options.project) {
5756
+ throw new Error(
5757
+ "Cannot use --workspace with --project (projects already carry a workspace via project.workspace)."
5758
+ );
5759
+ }
5760
+ if (options.workspace && !options.oneOff) {
5761
+ throw new Error("--workspace requires --one-off.");
5762
+ }
5555
5763
  if (!options.project && !options.oneOff) {
5556
5764
  throw new Error(
5557
5765
  "Either --project <slug> or --one-off is required."
@@ -5567,6 +5775,11 @@ async function createAssignmentCommand(title, options) {
5567
5775
  `Invalid project slug "${options.project}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5568
5776
  );
5569
5777
  }
5778
+ if (options.workspace && !isValidSlug(options.workspace)) {
5779
+ throw new Error(
5780
+ `Invalid workspace slug "${options.workspace}". Slugs must be lowercase, hyphen-separated, with no special characters.`
5781
+ );
5782
+ }
5570
5783
  if (options.oneOff && options.dependsOn) {
5571
5784
  throw new Error("Standalone assignments cannot have dependencies (--depends-on is not allowed with --one-off).");
5572
5785
  }
@@ -5657,7 +5870,9 @@ Use --slug to specify a different slug.`
5657
5870
  dependsOn,
5658
5871
  links,
5659
5872
  project: projectSlug,
5660
- type: options.type
5873
+ workspaceGroup: options.workspace ?? null,
5874
+ type: options.type,
5875
+ includeTodos: options.withTodos === true
5661
5876
  })
5662
5877
  ],
5663
5878
  [
@@ -5737,7 +5952,7 @@ Use --slug to specify a different slug.`
5737
5952
  init_config2();
5738
5953
  import { spawn } from "child_process";
5739
5954
  import { createServer as createNetServer } from "net";
5740
- import { resolve as resolve21, dirname as dirname4 } from "path";
5955
+ import { resolve as resolve22, dirname as dirname4 } from "path";
5741
5956
  import { fileURLToPath as fileURLToPath2 } from "url";
5742
5957
 
5743
5958
  // src/dashboard/server.ts
@@ -5746,7 +5961,7 @@ init_api();
5746
5961
  init_assignment_resolver();
5747
5962
  import express from "express";
5748
5963
  import { createServer } from "http";
5749
- import { resolve as resolve20 } from "path";
5964
+ import { resolve as resolve21 } from "path";
5750
5965
  import { writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
5751
5966
  import { WebSocketServer, WebSocket } from "ws";
5752
5967
 
@@ -5909,8 +6124,8 @@ async function migrateFromMarkdown(projectsDir2) {
5909
6124
  return allSessions.length;
5910
6125
  }
5911
6126
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
5912
- const { readFile: readFile29 } = await import("fs/promises");
5913
- const raw = await readFile29(filePath, "utf-8");
6127
+ const { readFile: readFile30 } = await import("fs/promises");
6128
+ const raw = await readFile30(filePath, "utf-8");
5914
6129
  const sessions = [];
5915
6130
  const lines = raw.split("\n");
5916
6131
  let inTable = false;
@@ -6097,18 +6312,25 @@ function createWatcher(options) {
6097
6312
  if (parts.length === 0) return;
6098
6313
  const projectSlug = parts[0];
6099
6314
  let assignmentSlug;
6315
+ let isProjectTodos = false;
6100
6316
  if (parts.length >= 3 && parts[1] === "assignments") {
6101
6317
  assignmentSlug = parts[2];
6318
+ } else if (parts.length >= 2 && parts[1] === "todos") {
6319
+ isProjectTodos = true;
6102
6320
  }
6103
- const debounceKey = assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
6321
+ const debounceKey = isProjectTodos ? `todos:${projectSlug}` : assignmentSlug ? `${projectSlug}/${assignmentSlug}` : projectSlug;
6104
6322
  const existing = pendingEvents.get(debounceKey);
6105
6323
  if (existing) clearTimeout(existing);
6106
- const messageType = assignmentSlug ? "assignment-updated" : "project-updated";
6324
+ const messageType = isProjectTodos ? "todos-updated" : assignmentSlug ? "assignment-updated" : "project-updated";
6107
6325
  pendingEvents.set(
6108
6326
  debounceKey,
6109
6327
  setTimeout(() => {
6110
6328
  pendingEvents.delete(debounceKey);
6111
- const message = {
6329
+ const message = isProjectTodos ? {
6330
+ type: "todos-updated",
6331
+ projectSlug,
6332
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6333
+ } : {
6112
6334
  type: messageType,
6113
6335
  projectSlug,
6114
6336
  assignmentSlug,
@@ -6434,7 +6656,15 @@ function createWriteRouter(projectsDir2, assignmentsDir2) {
6434
6656
  });
6435
6657
  res.json({ content });
6436
6658
  });
6437
- router.get("/api/templates/assignment", (_req, res) => {
6659
+ router.get("/api/templates/assignment", (req, res) => {
6660
+ const standalone = req.query.standalone === "1";
6661
+ const workspaceParam = typeof req.query.workspace === "string" ? req.query.workspace : "";
6662
+ if (workspaceParam && !isValidSlug(workspaceParam)) {
6663
+ res.status(400).json({
6664
+ error: `Invalid workspace slug "${workspaceParam}". Slugs must be lowercase, hyphen-separated, with no special characters.`
6665
+ });
6666
+ return;
6667
+ }
6438
6668
  const content = renderAssignment({
6439
6669
  id: generateId(),
6440
6670
  slug: "my-new-assignment",
@@ -6442,7 +6672,9 @@ function createWriteRouter(projectsDir2, assignmentsDir2) {
6442
6672
  timestamp: nowTimestamp(),
6443
6673
  priority: "medium",
6444
6674
  dependsOn: [],
6445
- links: []
6675
+ links: [],
6676
+ project: standalone ? null : void 0,
6677
+ workspaceGroup: standalone && workspaceParam ? workspaceParam : null
6446
6678
  });
6447
6679
  res.json({ content });
6448
6680
  });
@@ -7171,6 +7403,76 @@ ${entry}`;
7171
7403
  res.status(501).json({ error: "Standalone assignments not configured on this server" });
7172
7404
  return;
7173
7405
  }
7406
+ const rawContent = typeof req.body?.content === "string" ? req.body.content : "";
7407
+ if (rawContent.trim()) {
7408
+ const fields = extractFrontmatter3(rawContent);
7409
+ if (!fields) {
7410
+ res.status(400).json({ error: "Invalid frontmatter: missing --- delimiters" });
7411
+ return;
7412
+ }
7413
+ const validation = validateRequired(fields, ["slug", "title"]);
7414
+ if (!validation.valid) {
7415
+ res.status(400).json({ error: `Missing required fields: ${validation.missing.join(", ")}` });
7416
+ return;
7417
+ }
7418
+ const submittedSlug = fields.slug;
7419
+ if (!isValidSlug(submittedSlug)) {
7420
+ res.status(400).json({ error: `Invalid slug "${submittedSlug}". Must be lowercase and hyphen-separated.` });
7421
+ return;
7422
+ }
7423
+ const validPriorities = ["low", "medium", "high", "critical"];
7424
+ const submittedPriority = fields.priority || "medium";
7425
+ if (!validPriorities.includes(submittedPriority)) {
7426
+ res.status(400).json({ error: `Invalid priority "${submittedPriority}". Must be low, medium, high, or critical.` });
7427
+ return;
7428
+ }
7429
+ if (fields.project && fields.project !== "null") {
7430
+ res.status(400).json({
7431
+ error: 'Standalone assignments cannot have a project; remove "project" or set it to null.'
7432
+ });
7433
+ return;
7434
+ }
7435
+ const submittedWorkspaceGroup = fields.workspaceGroup && fields.workspaceGroup !== "null" ? fields.workspaceGroup : "";
7436
+ if (submittedWorkspaceGroup && !isValidSlug(submittedWorkspaceGroup)) {
7437
+ res.status(400).json({
7438
+ error: `Invalid workspace slug "${submittedWorkspaceGroup}". Slugs must be lowercase, hyphen-separated, with no special characters.`
7439
+ });
7440
+ return;
7441
+ }
7442
+ const id2 = generateId();
7443
+ const assignmentDir2 = resolve15(assignmentsDir2, id2);
7444
+ if (await fileExists(assignmentDir2)) {
7445
+ res.status(500).json({ error: "UUID collision \u2014 try again" });
7446
+ return;
7447
+ }
7448
+ const timestamp2 = fields.created || nowTimestamp();
7449
+ await ensureDir(assignmentDir2);
7450
+ const normalizedContent = setTopLevelField(rawContent, "id", id2);
7451
+ await writeFileForce(resolve15(assignmentDir2, "assignment.md"), normalizedContent);
7452
+ await writeFileForce(
7453
+ resolve15(assignmentDir2, "scratchpad.md"),
7454
+ renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
7455
+ );
7456
+ await writeFileForce(
7457
+ resolve15(assignmentDir2, "handoff.md"),
7458
+ renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
7459
+ );
7460
+ await writeFileForce(
7461
+ resolve15(assignmentDir2, "decision-record.md"),
7462
+ renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
7463
+ );
7464
+ await writeFileForce(
7465
+ resolve15(assignmentDir2, "progress.md"),
7466
+ renderProgress({ assignment: id2, timestamp: timestamp2 })
7467
+ );
7468
+ await writeFileForce(
7469
+ resolve15(assignmentDir2, "comments.md"),
7470
+ renderComments({ assignment: id2, timestamp: timestamp2 })
7471
+ );
7472
+ const detail2 = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id2);
7473
+ res.status(201).json({ assignment: detail2 });
7474
+ return;
7475
+ }
7174
7476
  const { title, slug, priority, type } = req.body || {};
7175
7477
  if (!title || typeof title !== "string" || !title.trim()) {
7176
7478
  res.status(400).json({ error: "title is required" });
@@ -7953,6 +8255,7 @@ import { resolve as resolve17 } from "path";
7953
8255
  import { readFile as readFile12, unlink as unlink2 } from "fs/promises";
7954
8256
  init_timestamp();
7955
8257
  init_fs();
8258
+ init_playbooks();
7956
8259
  function createPlaybooksRouter(playbooksDir3) {
7957
8260
  const router = Router4();
7958
8261
  router.get("/", async (_req, res) => {
@@ -7976,6 +8279,32 @@ function createPlaybooksRouter(playbooksDir3) {
7976
8279
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get template" });
7977
8280
  }
7978
8281
  });
8282
+ router.post("/:slug/enable", async (req, res) => {
8283
+ try {
8284
+ const result = await setPlaybookEnabled(playbooksDir3, req.params.slug, true);
8285
+ res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
8286
+ } catch (error) {
8287
+ const msg = error instanceof Error ? error.message : "Failed to enable playbook";
8288
+ if (msg.startsWith("Playbook ")) {
8289
+ res.status(404).json({ error: msg });
8290
+ return;
8291
+ }
8292
+ res.status(500).json({ error: msg });
8293
+ }
8294
+ });
8295
+ router.post("/:slug/disable", async (req, res) => {
8296
+ try {
8297
+ const result = await setPlaybookEnabled(playbooksDir3, req.params.slug, false);
8298
+ res.json({ slug: result.slug, enabled: result.enabled, changed: result.changed });
8299
+ } catch (error) {
8300
+ const msg = error instanceof Error ? error.message : "Failed to disable playbook";
8301
+ if (msg.startsWith("Playbook ")) {
8302
+ res.status(404).json({ error: msg });
8303
+ return;
8304
+ }
8305
+ res.status(500).json({ error: msg });
8306
+ }
8307
+ });
7979
8308
  router.get("/:slug", async (req, res) => {
7980
8309
  try {
7981
8310
  const detail = await getPlaybookDetail(playbooksDir3, req.params.slug);
@@ -7990,17 +8319,18 @@ function createPlaybooksRouter(playbooksDir3) {
7990
8319
  });
7991
8320
  router.get("/:slug/edit", async (req, res) => {
7992
8321
  try {
7993
- const filePath = resolve17(playbooksDir3, `${req.params.slug}.md`);
7994
- if (!await fileExists(filePath)) {
8322
+ const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
8323
+ if (!resolved) {
7995
8324
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
7996
8325
  return;
7997
8326
  }
8327
+ const filePath = resolve17(playbooksDir3, resolved.filename);
7998
8328
  const content = await readFile12(filePath, "utf-8");
7999
8329
  res.json({
8000
8330
  documentType: "playbook",
8001
- title: `Edit Playbook: ${req.params.slug}`,
8331
+ title: `Edit Playbook: ${resolved.slug}`,
8002
8332
  content,
8003
- slug: req.params.slug
8333
+ slug: resolved.slug
8004
8334
  });
8005
8335
  } catch (error) {
8006
8336
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get playbook for editing" });
@@ -8039,14 +8369,15 @@ function createPlaybooksRouter(playbooksDir3) {
8039
8369
  res.status(400).json({ error: "content is required" });
8040
8370
  return;
8041
8371
  }
8042
- const filePath = resolve17(playbooksDir3, `${req.params.slug}.md`);
8043
- if (!await fileExists(filePath)) {
8372
+ const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
8373
+ if (!resolved) {
8044
8374
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
8045
8375
  return;
8046
8376
  }
8377
+ const filePath = resolve17(playbooksDir3, resolved.filename);
8047
8378
  await writeFileForce(filePath, content);
8048
8379
  await rebuildPlaybookManifest(playbooksDir3);
8049
- res.json({ slug: req.params.slug, path: filePath });
8380
+ res.json({ slug: resolved.slug, path: filePath });
8050
8381
  } catch (error) {
8051
8382
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update playbook" });
8052
8383
  }
@@ -8057,14 +8388,16 @@ function createPlaybooksRouter(playbooksDir3) {
8057
8388
  res.status(403).json({ error: "The playbook manifest cannot be deleted" });
8058
8389
  return;
8059
8390
  }
8060
- const filePath = resolve17(playbooksDir3, `${req.params.slug}.md`);
8061
- if (!await fileExists(filePath)) {
8391
+ const resolved = await resolvePlaybookSlug(playbooksDir3, req.params.slug);
8392
+ if (!resolved) {
8062
8393
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
8063
8394
  return;
8064
8395
  }
8396
+ const filePath = resolve17(playbooksDir3, resolved.filename);
8065
8397
  await unlink2(filePath);
8398
+ await removeFromDisabledList(resolved.slug);
8066
8399
  await rebuildPlaybookManifest(playbooksDir3);
8067
- res.json({ deleted: req.params.slug });
8400
+ res.json({ deleted: resolved.slug });
8068
8401
  } catch (error) {
8069
8402
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete playbook" });
8070
8403
  }
@@ -8088,14 +8421,17 @@ function getWorkspaceParam(value) {
8088
8421
  return value ?? "";
8089
8422
  }
8090
8423
  var writeLocks = /* @__PURE__ */ new Map();
8091
- function withLock(workspace, fn) {
8092
- const prev = writeLocks.get(workspace) ?? Promise.resolve();
8424
+ function withLock(lockKey, fn) {
8425
+ const prev = writeLocks.get(lockKey) ?? Promise.resolve();
8093
8426
  const next = prev.then(fn);
8094
- writeLocks.set(workspace, next.then(() => {
8427
+ writeLocks.set(lockKey, next.then(() => {
8095
8428
  }, () => {
8096
8429
  }));
8097
8430
  return next;
8098
8431
  }
8432
+ function wsLock(workspace, fn) {
8433
+ return withLock(`ws:${workspace}`, fn);
8434
+ }
8099
8435
  function createTodosRouter(todosDir2, broadcast) {
8100
8436
  const router = Router5();
8101
8437
  function broadcastUpdate() {
@@ -8154,7 +8490,7 @@ function createTodosRouter(todosDir2, broadcast) {
8154
8490
  res.status(400).json({ error: "description is required" });
8155
8491
  return;
8156
8492
  }
8157
- const item = await withLock(workspace, async () => {
8493
+ const item = await wsLock(workspace, async () => {
8158
8494
  const checklist = await readChecklist(todosDir2, workspace);
8159
8495
  const existingIds = new Set(checklist.items.map((i) => i.id));
8160
8496
  const id = generateUniqueId(existingIds);
@@ -8183,7 +8519,7 @@ function createTodosRouter(todosDir2, broadcast) {
8183
8519
  res.status(400).json({ error: "ids must be an array of strings" });
8184
8520
  return;
8185
8521
  }
8186
- const items = await withLock(workspace, async () => {
8522
+ const items = await wsLock(workspace, async () => {
8187
8523
  const checklist = await readChecklist(todosDir2, workspace);
8188
8524
  const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
8189
8525
  const reordered = [];
@@ -8218,8 +8554,8 @@ function createTodosRouter(todosDir2, broadcast) {
8218
8554
  router.post("/:workspace/archive", async (req, res) => {
8219
8555
  try {
8220
8556
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
8221
- const { resolve: resolve45 } = await import("path");
8222
- const { readFile: readFile29 } = await import("fs/promises");
8557
+ const { resolve: resolve46 } = await import("path");
8558
+ const { readFile: readFile30 } = await import("fs/promises");
8223
8559
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
8224
8560
  const workspace = getWorkspaceParam(req.params.workspace);
8225
8561
  const checklist = await readChecklist(todosDir2, workspace);
@@ -8235,10 +8571,10 @@ function createTodosRouter(todosDir2, broadcast) {
8235
8571
  (e) => e.itemIds.every((id) => completedIds.has(id))
8236
8572
  );
8237
8573
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
8238
- await ensureDir(resolve45(todosDir2, "archive"));
8574
+ await ensureDir(resolve46(todosDir2, "archive"));
8239
8575
  let archContent = "";
8240
8576
  if (await fileExists(archFile)) {
8241
- archContent = await readFile29(archFile, "utf-8");
8577
+ archContent = await readFile30(archFile, "utf-8");
8242
8578
  archContent = archContent.trimEnd() + "\n\n";
8243
8579
  } else {
8244
8580
  archContent = `---
@@ -8307,7 +8643,7 @@ workspace: ${workspace}
8307
8643
  router.patch("/:workspace/:id", async (req, res) => {
8308
8644
  try {
8309
8645
  const workspace = getWorkspaceParam(req.params.workspace);
8310
- const result = await withLock(workspace, async () => {
8646
+ const result = await wsLock(workspace, async () => {
8311
8647
  const checklist = await readChecklist(todosDir2, workspace);
8312
8648
  const item = checklist.items.find((i) => i.id === req.params.id);
8313
8649
  if (!item) return null;
@@ -8329,7 +8665,7 @@ workspace: ${workspace}
8329
8665
  router.delete("/:workspace/:id", async (req, res) => {
8330
8666
  try {
8331
8667
  const workspace = getWorkspaceParam(req.params.workspace);
8332
- const deleted = await withLock(workspace, async () => {
8668
+ const deleted = await wsLock(workspace, async () => {
8333
8669
  const checklist = await readChecklist(todosDir2, workspace);
8334
8670
  const idx = checklist.items.findIndex((i) => i.id === req.params.id);
8335
8671
  if (idx === -1) return false;
@@ -8350,7 +8686,7 @@ workspace: ${workspace}
8350
8686
  router.post("/:workspace/:id/start", async (req, res) => {
8351
8687
  try {
8352
8688
  const workspace = getWorkspaceParam(req.params.workspace);
8353
- const result = await withLock(workspace, async () => {
8689
+ const result = await wsLock(workspace, async () => {
8354
8690
  const checklist = await readChecklist(todosDir2, workspace);
8355
8691
  const item = checklist.items.find((i) => i.id === req.params.id);
8356
8692
  if (!item) return { error: "not_found" };
@@ -8377,7 +8713,7 @@ workspace: ${workspace}
8377
8713
  router.post("/:workspace/:id/complete", async (req, res) => {
8378
8714
  try {
8379
8715
  const workspace = getWorkspaceParam(req.params.workspace);
8380
- const result = await withLock(workspace, async () => {
8716
+ const result = await wsLock(workspace, async () => {
8381
8717
  const checklist = await readChecklist(todosDir2, workspace);
8382
8718
  const item = checklist.items.find((i) => i.id === req.params.id);
8383
8719
  if (!item) return null;
@@ -8411,7 +8747,7 @@ workspace: ${workspace}
8411
8747
  try {
8412
8748
  const reason = req.body.reason || null;
8413
8749
  const workspace = getWorkspaceParam(req.params.workspace);
8414
- const result = await withLock(workspace, async () => {
8750
+ const result = await wsLock(workspace, async () => {
8415
8751
  const checklist = await readChecklist(todosDir2, workspace);
8416
8752
  const item = checklist.items.find((i) => i.id === req.params.id);
8417
8753
  if (!item) return null;
@@ -8444,7 +8780,7 @@ workspace: ${workspace}
8444
8780
  router.post("/:workspace/:id/reopen", async (req, res) => {
8445
8781
  try {
8446
8782
  const workspace = getWorkspaceParam(req.params.workspace);
8447
- const result = await withLock(workspace, async () => {
8783
+ const result = await wsLock(workspace, async () => {
8448
8784
  const checklist = await readChecklist(todosDir2, workspace);
8449
8785
  const item = checklist.items.find((i) => i.id === req.params.id);
8450
8786
  if (!item) return null;
@@ -8466,7 +8802,7 @@ workspace: ${workspace}
8466
8802
  router.post("/:workspace/:id/unblock", async (req, res) => {
8467
8803
  try {
8468
8804
  const workspace = getWorkspaceParam(req.params.workspace);
8469
- const result = await withLock(workspace, async () => {
8805
+ const result = await wsLock(workspace, async () => {
8470
8806
  const checklist = await readChecklist(todosDir2, workspace);
8471
8807
  const item = checklist.items.find((i) => i.id === req.params.id);
8472
8808
  if (!item) return null;
@@ -8488,21 +8824,618 @@ workspace: ${workspace}
8488
8824
  return router;
8489
8825
  }
8490
8826
 
8491
- // src/dashboard/api-backup.ts
8492
- init_config2();
8493
- import { Router as Router6 } from "express";
8494
-
8495
- // src/utils/github-backup.ts
8496
- init_paths();
8827
+ // src/dashboard/api-project-todos.ts
8828
+ init_parser2();
8497
8829
  init_fs();
8498
- init_config2();
8499
- import { execFile as execFile2 } from "child_process";
8500
- import { promisify as promisify2 } from "util";
8501
- import { cp, mkdtemp, rm as rm2, readFile as readFile14, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
8502
- import { resolve as resolve19, join as join2 } from "path";
8503
- import { tmpdir } from "os";
8504
- var exec2 = promisify2(execFile2);
8505
- var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
8830
+ init_paths();
8831
+ import { Router as Router6 } from "express";
8832
+ import { mkdir as mkdir2, readFile as readFile14 } from "fs/promises";
8833
+ import { resolve as resolve19 } from "path";
8834
+ var writeLocks2 = /* @__PURE__ */ new Map();
8835
+ function projLock(slug, fn) {
8836
+ const key = `proj:${slug}`;
8837
+ const prev = writeLocks2.get(key) ?? Promise.resolve();
8838
+ const next = prev.then(fn);
8839
+ writeLocks2.set(key, next.then(() => {
8840
+ }, () => {
8841
+ }));
8842
+ return next;
8843
+ }
8844
+ function getProjectIdParam(value) {
8845
+ if (Array.isArray(value)) return value[0] ?? "";
8846
+ return value ?? "";
8847
+ }
8848
+ function params(req) {
8849
+ return req.params;
8850
+ }
8851
+ async function projectExists(projectsDir2, slug) {
8852
+ return fileExists(resolve19(projectsDir2, slug, "project.md"));
8853
+ }
8854
+ async function ensureProjectTodosDir(projectsDir2, slug) {
8855
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
8856
+ try {
8857
+ await mkdir2(todosDir2, { recursive: false });
8858
+ } catch (err2) {
8859
+ const code = err2.code;
8860
+ if (code === "EEXIST") return;
8861
+ if (code === "ENOENT") {
8862
+ const e = new Error("PROJECT_GONE");
8863
+ e.code = "PROJECT_GONE";
8864
+ throw e;
8865
+ }
8866
+ throw err2;
8867
+ }
8868
+ try {
8869
+ await mkdir2(resolve19(todosDir2, "archive"), { recursive: false });
8870
+ } catch (err2) {
8871
+ const code = err2.code;
8872
+ if (code === "EEXIST") return;
8873
+ if (code === "ENOENT") {
8874
+ const e = new Error("PROJECT_GONE");
8875
+ e.code = "PROJECT_GONE";
8876
+ throw e;
8877
+ }
8878
+ throw err2;
8879
+ }
8880
+ }
8881
+ function notFound(res, slug) {
8882
+ res.status(404).json({ error: `Project "${slug}" not found` });
8883
+ }
8884
+ function createProjectTodosRouter(projectsDir2, broadcast) {
8885
+ const router = Router6({ mergeParams: true });
8886
+ function broadcastUpdate(projectSlug) {
8887
+ broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
8888
+ }
8889
+ function validateProjectId(req, res, next) {
8890
+ const slug = getProjectIdParam(params(req).projectId);
8891
+ if (!slug || !isValidSlug(slug)) {
8892
+ res.status(400).json({ error: `Invalid project slug: "${slug}"` });
8893
+ return;
8894
+ }
8895
+ next();
8896
+ }
8897
+ router.use(validateProjectId);
8898
+ router.get("/", async (req, res) => {
8899
+ try {
8900
+ const slug = getProjectIdParam(params(req).projectId);
8901
+ if (!await projectExists(projectsDir2, slug)) {
8902
+ notFound(res, slug);
8903
+ return;
8904
+ }
8905
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
8906
+ const checklist = await readChecklist(todosDir2, slug);
8907
+ res.json({
8908
+ workspace: checklist.workspace,
8909
+ archiveInterval: checklist.archiveInterval,
8910
+ items: checklist.items,
8911
+ counts: computeCounts(checklist.items)
8912
+ });
8913
+ } catch (error) {
8914
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todos" });
8915
+ }
8916
+ });
8917
+ router.post("/", async (req, res) => {
8918
+ try {
8919
+ const slug = getProjectIdParam(params(req).projectId);
8920
+ const { description, tags } = req.body;
8921
+ if (!description || typeof description !== "string") {
8922
+ res.status(400).json({ error: "description is required" });
8923
+ return;
8924
+ }
8925
+ if (!await projectExists(projectsDir2, slug)) {
8926
+ notFound(res, slug);
8927
+ return;
8928
+ }
8929
+ const item = await projLock(slug, async () => {
8930
+ if (!await projectExists(projectsDir2, slug)) return null;
8931
+ await ensureProjectTodosDir(projectsDir2, slug);
8932
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
8933
+ const checklist = await readChecklist(todosDir2, slug);
8934
+ const existingIds = new Set(checklist.items.map((i) => i.id));
8935
+ const id = generateUniqueId(existingIds);
8936
+ const newItem = {
8937
+ id,
8938
+ description,
8939
+ status: "open",
8940
+ tags: Array.isArray(tags) ? tags : [],
8941
+ session: null
8942
+ };
8943
+ checklist.workspace = slug;
8944
+ checklist.items.push(newItem);
8945
+ await writeChecklist(todosDir2, checklist);
8946
+ return newItem;
8947
+ });
8948
+ if (!item) {
8949
+ notFound(res, slug);
8950
+ return;
8951
+ }
8952
+ broadcastUpdate(slug);
8953
+ res.status(201).json(item);
8954
+ } catch (error) {
8955
+ if (error.code === "PROJECT_GONE") {
8956
+ notFound(res, getProjectIdParam(params(req).projectId));
8957
+ return;
8958
+ }
8959
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to add todo" });
8960
+ }
8961
+ });
8962
+ router.post("/reorder", async (req, res) => {
8963
+ try {
8964
+ const slug = getProjectIdParam(params(req).projectId);
8965
+ const { ids } = req.body;
8966
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string")) {
8967
+ res.status(400).json({ error: "ids must be an array of strings" });
8968
+ return;
8969
+ }
8970
+ if (!await projectExists(projectsDir2, slug)) {
8971
+ notFound(res, slug);
8972
+ return;
8973
+ }
8974
+ const items = await projLock(slug, async () => {
8975
+ if (!await projectExists(projectsDir2, slug)) return null;
8976
+ await ensureProjectTodosDir(projectsDir2, slug);
8977
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
8978
+ const checklist = await readChecklist(todosDir2, slug);
8979
+ const itemMap = new Map(checklist.items.map((i) => [i.id, i]));
8980
+ const reordered = [];
8981
+ for (const id of ids) {
8982
+ const item = itemMap.get(id);
8983
+ if (item) {
8984
+ reordered.push(item);
8985
+ itemMap.delete(id);
8986
+ }
8987
+ }
8988
+ for (const item of itemMap.values()) reordered.push(item);
8989
+ checklist.workspace = slug;
8990
+ checklist.items = reordered;
8991
+ await writeChecklist(todosDir2, checklist);
8992
+ return reordered;
8993
+ });
8994
+ if (!items) {
8995
+ notFound(res, slug);
8996
+ return;
8997
+ }
8998
+ broadcastUpdate(slug);
8999
+ res.json({ items });
9000
+ } catch (error) {
9001
+ if (error.code === "PROJECT_GONE") {
9002
+ notFound(res, getProjectIdParam(params(req).projectId));
9003
+ return;
9004
+ }
9005
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reorder todos" });
9006
+ }
9007
+ });
9008
+ router.get("/log", async (req, res) => {
9009
+ try {
9010
+ const slug = getProjectIdParam(params(req).projectId);
9011
+ if (!await projectExists(projectsDir2, slug)) {
9012
+ notFound(res, slug);
9013
+ return;
9014
+ }
9015
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9016
+ const log = await readLog(todosDir2, slug);
9017
+ res.json(log);
9018
+ } catch (error) {
9019
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
9020
+ }
9021
+ });
9022
+ router.post("/archive", async (req, res) => {
9023
+ try {
9024
+ const slug = getProjectIdParam(params(req).projectId);
9025
+ if (!await projectExists(projectsDir2, slug)) {
9026
+ notFound(res, slug);
9027
+ return;
9028
+ }
9029
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9030
+ await ensureProjectTodosDir(projectsDir2, slug);
9031
+ const checklist = await readChecklist(todosDir2, slug);
9032
+ const log = await readLog(todosDir2, slug);
9033
+ const completedIds = new Set(
9034
+ checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
9035
+ );
9036
+ if (completedIds.size === 0) {
9037
+ res.json({ archived: 0, message: "No completed items to archive" });
9038
+ return;
9039
+ }
9040
+ const toArchive = log.entries.filter(
9041
+ (e) => e.itemIds.every((id) => completedIds.has(id))
9042
+ );
9043
+ const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
9044
+ let archContent = "";
9045
+ if (await fileExists(archFile)) {
9046
+ archContent = await readFile14(archFile, "utf-8");
9047
+ archContent = archContent.trimEnd() + "\n\n";
9048
+ } else {
9049
+ archContent = `---
9050
+ workspace: ${slug}
9051
+ ---
9052
+
9053
+ # Archive
9054
+
9055
+ `;
9056
+ }
9057
+ const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
9058
+ for (const item of completedItems) {
9059
+ archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
9060
+ `;
9061
+ }
9062
+ archContent += "\n";
9063
+ for (const entry of toArchive) {
9064
+ archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
9065
+ `;
9066
+ if (entry.items) archContent += `**Items:** ${entry.items}
9067
+ `;
9068
+ if (entry.session) archContent += `**Session:** ${entry.session}
9069
+ `;
9070
+ if (entry.branch) archContent += `**Branch:** ${entry.branch}
9071
+ `;
9072
+ if (entry.summary) archContent += `**Summary:** ${entry.summary}
9073
+ `;
9074
+ if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
9075
+ `;
9076
+ archContent += "\n";
9077
+ }
9078
+ await writeFileForce(archFile, archContent);
9079
+ checklist.workspace = slug;
9080
+ checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
9081
+ await writeChecklist(todosDir2, checklist);
9082
+ broadcastUpdate(slug);
9083
+ res.json({ archived: completedIds.size, logEntries: toArchive.length });
9084
+ } catch (error) {
9085
+ if (error.code === "PROJECT_GONE") {
9086
+ notFound(res, getProjectIdParam(params(req).projectId));
9087
+ return;
9088
+ }
9089
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to archive" });
9090
+ }
9091
+ });
9092
+ router.get("/log/:id", async (req, res) => {
9093
+ try {
9094
+ const slug = getProjectIdParam(params(req).projectId);
9095
+ if (!await projectExists(projectsDir2, slug)) {
9096
+ notFound(res, slug);
9097
+ return;
9098
+ }
9099
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9100
+ const log = await readLog(todosDir2, slug);
9101
+ const entries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
9102
+ res.json({ workspace: log.workspace, entries });
9103
+ } catch (error) {
9104
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get log" });
9105
+ }
9106
+ });
9107
+ router.get("/:id", async (req, res) => {
9108
+ try {
9109
+ const slug = getProjectIdParam(params(req).projectId);
9110
+ if (!await projectExists(projectsDir2, slug)) {
9111
+ notFound(res, slug);
9112
+ return;
9113
+ }
9114
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9115
+ const checklist = await readChecklist(todosDir2, slug);
9116
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
9117
+ if (!item) {
9118
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
9119
+ return;
9120
+ }
9121
+ const log = await readLog(todosDir2, slug);
9122
+ const logEntries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
9123
+ res.json({ ...item, log: logEntries });
9124
+ } catch (error) {
9125
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
9126
+ }
9127
+ });
9128
+ router.patch("/:id", async (req, res) => {
9129
+ try {
9130
+ const slug = getProjectIdParam(params(req).projectId);
9131
+ if (!await projectExists(projectsDir2, slug)) {
9132
+ notFound(res, slug);
9133
+ return;
9134
+ }
9135
+ const result = await projLock(slug, async () => {
9136
+ if (!await projectExists(projectsDir2, slug)) return "gone";
9137
+ await ensureProjectTodosDir(projectsDir2, slug);
9138
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9139
+ const checklist = await readChecklist(todosDir2, slug);
9140
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
9141
+ if (!item) return null;
9142
+ if (req.body.description !== void 0) item.description = req.body.description;
9143
+ if (Array.isArray(req.body.tags)) item.tags = req.body.tags;
9144
+ checklist.workspace = slug;
9145
+ await writeChecklist(todosDir2, checklist);
9146
+ return { ...item };
9147
+ });
9148
+ if (result === "gone") {
9149
+ notFound(res, slug);
9150
+ return;
9151
+ }
9152
+ if (!result) {
9153
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
9154
+ return;
9155
+ }
9156
+ broadcastUpdate(slug);
9157
+ res.json(result);
9158
+ } catch (error) {
9159
+ if (error.code === "PROJECT_GONE") {
9160
+ notFound(res, getProjectIdParam(params(req).projectId));
9161
+ return;
9162
+ }
9163
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to update todo" });
9164
+ }
9165
+ });
9166
+ router.delete("/:id", async (req, res) => {
9167
+ try {
9168
+ const slug = getProjectIdParam(params(req).projectId);
9169
+ if (!await projectExists(projectsDir2, slug)) {
9170
+ notFound(res, slug);
9171
+ return;
9172
+ }
9173
+ const deleted = await projLock(slug, async () => {
9174
+ if (!await projectExists(projectsDir2, slug)) return "gone";
9175
+ await ensureProjectTodosDir(projectsDir2, slug);
9176
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9177
+ const checklist = await readChecklist(todosDir2, slug);
9178
+ const idx = checklist.items.findIndex((i) => i.id === (params(req).id ?? ""));
9179
+ if (idx === -1) return false;
9180
+ checklist.items.splice(idx, 1);
9181
+ checklist.workspace = slug;
9182
+ await writeChecklist(todosDir2, checklist);
9183
+ return true;
9184
+ });
9185
+ if (deleted === "gone") {
9186
+ notFound(res, slug);
9187
+ return;
9188
+ }
9189
+ if (!deleted) {
9190
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
9191
+ return;
9192
+ }
9193
+ broadcastUpdate(slug);
9194
+ res.json({ deleted: params(req).id ?? "" });
9195
+ } catch (error) {
9196
+ if (error.code === "PROJECT_GONE") {
9197
+ notFound(res, getProjectIdParam(params(req).projectId));
9198
+ return;
9199
+ }
9200
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to delete todo" });
9201
+ }
9202
+ });
9203
+ router.post("/:id/start", async (req, res) => {
9204
+ try {
9205
+ const slug = getProjectIdParam(params(req).projectId);
9206
+ if (!await projectExists(projectsDir2, slug)) {
9207
+ notFound(res, slug);
9208
+ return;
9209
+ }
9210
+ const result = await projLock(slug, async () => {
9211
+ if (!await projectExists(projectsDir2, slug)) return { error: "gone" };
9212
+ await ensureProjectTodosDir(projectsDir2, slug);
9213
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9214
+ const checklist = await readChecklist(todosDir2, slug);
9215
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
9216
+ if (!item) return { error: "not_found" };
9217
+ if (item.status === "in_progress") return { error: "conflict", session: item.session };
9218
+ item.status = "in_progress";
9219
+ item.session = req.body.session || null;
9220
+ checklist.workspace = slug;
9221
+ await writeChecklist(todosDir2, checklist);
9222
+ return { item: { ...item } };
9223
+ });
9224
+ if ("error" in result) {
9225
+ if (result.error === "gone") {
9226
+ notFound(res, slug);
9227
+ return;
9228
+ }
9229
+ if (result.error === "not_found") {
9230
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
9231
+ return;
9232
+ }
9233
+ res.status(409).json({ error: `Todo is already in progress (session: ${result.session})` });
9234
+ return;
9235
+ }
9236
+ broadcastUpdate(slug);
9237
+ res.json(result.item);
9238
+ } catch (error) {
9239
+ if (error.code === "PROJECT_GONE") {
9240
+ notFound(res, getProjectIdParam(params(req).projectId));
9241
+ return;
9242
+ }
9243
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to start todo" });
9244
+ }
9245
+ });
9246
+ router.post("/:id/complete", async (req, res) => {
9247
+ try {
9248
+ const slug = getProjectIdParam(params(req).projectId);
9249
+ if (!await projectExists(projectsDir2, slug)) {
9250
+ notFound(res, slug);
9251
+ return;
9252
+ }
9253
+ const result = await projLock(slug, async () => {
9254
+ if (!await projectExists(projectsDir2, slug)) return "gone";
9255
+ await ensureProjectTodosDir(projectsDir2, slug);
9256
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9257
+ const checklist = await readChecklist(todosDir2, slug);
9258
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
9259
+ if (!item) return null;
9260
+ item.status = "completed";
9261
+ item.session = null;
9262
+ checklist.workspace = slug;
9263
+ await writeChecklist(todosDir2, checklist);
9264
+ const entry = {
9265
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9266
+ itemIds: [item.id],
9267
+ items: item.description,
9268
+ session: req.body.session || null,
9269
+ branch: req.body.branch || null,
9270
+ summary: req.body.summary || "Completed.",
9271
+ blockers: null,
9272
+ status: null
9273
+ };
9274
+ await appendLogEntry2(todosDir2, slug, entry);
9275
+ return { ...item };
9276
+ });
9277
+ if (result === "gone") {
9278
+ notFound(res, slug);
9279
+ return;
9280
+ }
9281
+ if (!result) {
9282
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
9283
+ return;
9284
+ }
9285
+ broadcastUpdate(slug);
9286
+ res.json(result);
9287
+ } catch (error) {
9288
+ if (error.code === "PROJECT_GONE") {
9289
+ notFound(res, getProjectIdParam(params(req).projectId));
9290
+ return;
9291
+ }
9292
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to complete todo" });
9293
+ }
9294
+ });
9295
+ router.post("/:id/block", async (req, res) => {
9296
+ try {
9297
+ const slug = getProjectIdParam(params(req).projectId);
9298
+ const reason = req.body.reason || null;
9299
+ if (!await projectExists(projectsDir2, slug)) {
9300
+ notFound(res, slug);
9301
+ return;
9302
+ }
9303
+ const result = await projLock(slug, async () => {
9304
+ if (!await projectExists(projectsDir2, slug)) return "gone";
9305
+ await ensureProjectTodosDir(projectsDir2, slug);
9306
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9307
+ const checklist = await readChecklist(todosDir2, slug);
9308
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
9309
+ if (!item) return null;
9310
+ item.status = "blocked";
9311
+ item.session = null;
9312
+ checklist.workspace = slug;
9313
+ await writeChecklist(todosDir2, checklist);
9314
+ const entry = {
9315
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9316
+ itemIds: [item.id],
9317
+ items: item.description,
9318
+ session: req.body.session || null,
9319
+ branch: null,
9320
+ summary: reason || "Blocked.",
9321
+ blockers: reason,
9322
+ status: "blocked"
9323
+ };
9324
+ await appendLogEntry2(todosDir2, slug, entry);
9325
+ return { ...item };
9326
+ });
9327
+ if (result === "gone") {
9328
+ notFound(res, slug);
9329
+ return;
9330
+ }
9331
+ if (!result) {
9332
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
9333
+ return;
9334
+ }
9335
+ broadcastUpdate(slug);
9336
+ res.json(result);
9337
+ } catch (error) {
9338
+ if (error.code === "PROJECT_GONE") {
9339
+ notFound(res, getProjectIdParam(params(req).projectId));
9340
+ return;
9341
+ }
9342
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to block todo" });
9343
+ }
9344
+ });
9345
+ router.post("/:id/reopen", async (req, res) => {
9346
+ try {
9347
+ const slug = getProjectIdParam(params(req).projectId);
9348
+ if (!await projectExists(projectsDir2, slug)) {
9349
+ notFound(res, slug);
9350
+ return;
9351
+ }
9352
+ const result = await projLock(slug, async () => {
9353
+ if (!await projectExists(projectsDir2, slug)) return "gone";
9354
+ await ensureProjectTodosDir(projectsDir2, slug);
9355
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9356
+ const checklist = await readChecklist(todosDir2, slug);
9357
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
9358
+ if (!item) return null;
9359
+ item.status = "open";
9360
+ item.session = null;
9361
+ checklist.workspace = slug;
9362
+ await writeChecklist(todosDir2, checklist);
9363
+ return { ...item };
9364
+ });
9365
+ if (result === "gone") {
9366
+ notFound(res, slug);
9367
+ return;
9368
+ }
9369
+ if (!result) {
9370
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
9371
+ return;
9372
+ }
9373
+ broadcastUpdate(slug);
9374
+ res.json(result);
9375
+ } catch (error) {
9376
+ if (error.code === "PROJECT_GONE") {
9377
+ notFound(res, getProjectIdParam(params(req).projectId));
9378
+ return;
9379
+ }
9380
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to reopen todo" });
9381
+ }
9382
+ });
9383
+ router.post("/:id/unblock", async (req, res) => {
9384
+ try {
9385
+ const slug = getProjectIdParam(params(req).projectId);
9386
+ if (!await projectExists(projectsDir2, slug)) {
9387
+ notFound(res, slug);
9388
+ return;
9389
+ }
9390
+ const result = await projLock(slug, async () => {
9391
+ if (!await projectExists(projectsDir2, slug)) return "gone";
9392
+ await ensureProjectTodosDir(projectsDir2, slug);
9393
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
9394
+ const checklist = await readChecklist(todosDir2, slug);
9395
+ const item = checklist.items.find((i) => i.id === (params(req).id ?? ""));
9396
+ if (!item) return null;
9397
+ item.status = "open";
9398
+ item.session = null;
9399
+ checklist.workspace = slug;
9400
+ await writeChecklist(todosDir2, checklist);
9401
+ return { ...item };
9402
+ });
9403
+ if (result === "gone") {
9404
+ notFound(res, slug);
9405
+ return;
9406
+ }
9407
+ if (!result) {
9408
+ res.status(404).json({ error: `Todo "${params(req).id ?? ""}" not found` });
9409
+ return;
9410
+ }
9411
+ broadcastUpdate(slug);
9412
+ res.json(result);
9413
+ } catch (error) {
9414
+ if (error.code === "PROJECT_GONE") {
9415
+ notFound(res, getProjectIdParam(params(req).projectId));
9416
+ return;
9417
+ }
9418
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to unblock todo" });
9419
+ }
9420
+ });
9421
+ return router;
9422
+ }
9423
+
9424
+ // src/dashboard/api-backup.ts
9425
+ init_config2();
9426
+ import { Router as Router7 } from "express";
9427
+
9428
+ // src/utils/github-backup.ts
9429
+ init_paths();
9430
+ init_fs();
9431
+ init_config2();
9432
+ import { execFile as execFile2 } from "child_process";
9433
+ import { promisify as promisify2 } from "util";
9434
+ import { cp, mkdtemp, rm as rm2, readFile as readFile15, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
9435
+ import { resolve as resolve20, join as join2 } from "path";
9436
+ import { tmpdir } from "os";
9437
+ var exec2 = promisify2(execFile2);
9438
+ var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
8506
9439
  var LOCK_FILE_NAME = ".backup-lock";
8507
9440
  function parseCategoriesStrict(cats) {
8508
9441
  const unknown = [];
@@ -8539,7 +9472,7 @@ async function resolveCategoryPath(category) {
8539
9472
  case "servers":
8540
9473
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
8541
9474
  case "config":
8542
- return { sourcePath: resolve19(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
9475
+ return { sourcePath: resolve20(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
8543
9476
  }
8544
9477
  }
8545
9478
  async function checkGitInstalled() {
@@ -8550,7 +9483,7 @@ async function checkGitInstalled() {
8550
9483
  }
8551
9484
  }
8552
9485
  async function acquireLock() {
8553
- const lockPath = resolve19(syntaurRoot(), LOCK_FILE_NAME);
9486
+ const lockPath = resolve20(syntaurRoot(), LOCK_FILE_NAME);
8554
9487
  await ensureDir(syntaurRoot());
8555
9488
  try {
8556
9489
  const handle = await open(lockPath, "wx");
@@ -8559,7 +9492,7 @@ async function acquireLock() {
8559
9492
  return lockPath;
8560
9493
  } catch (err2) {
8561
9494
  if (err2.code === "EEXIST") {
8562
- const pid = await readFile14(lockPath, "utf-8").catch(() => "");
9495
+ const pid = await readFile15(lockPath, "utf-8").catch(() => "");
8563
9496
  throw new Error(
8564
9497
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
8565
9498
  );
@@ -8597,7 +9530,7 @@ async function copyRecursive(src, dest) {
8597
9530
  await ensureDir(dest);
8598
9531
  await cp(src, dest, { recursive: true, force: true });
8599
9532
  } else {
8600
- await ensureDir(resolve19(dest, ".."));
9533
+ await ensureDir(resolve20(dest, ".."));
8601
9534
  await cp(src, dest, { force: true });
8602
9535
  }
8603
9536
  }
@@ -8606,7 +9539,7 @@ function resolveCategoriesStrict(csv) {
8606
9539
  return parseCategoriesStrict(parts);
8607
9540
  }
8608
9541
  async function readSanitizedConfig(configPath) {
8609
- const content = await readFile14(configPath, "utf-8");
9542
+ const content = await readFile15(configPath, "utf-8");
8610
9543
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
8611
9544
  }
8612
9545
  async function backupToGithub(overrides) {
@@ -8645,7 +9578,7 @@ async function backupToGithub(overrides) {
8645
9578
  }
8646
9579
  if (category === "config") {
8647
9580
  const sanitized = await readSanitizedConfig(sourcePath);
8648
- await ensureDir(resolve19(destPath, ".."));
9581
+ await ensureDir(resolve20(destPath, ".."));
8649
9582
  await writeFile4(destPath, sanitized, "utf-8");
8650
9583
  } else {
8651
9584
  await copyRecursive(sourcePath, destPath);
@@ -8699,7 +9632,7 @@ async function backupToGithub(overrides) {
8699
9632
  }
8700
9633
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
8701
9634
  if (isFile) {
8702
- await ensureDir(resolve19(localPath, ".."));
9635
+ await ensureDir(resolve20(localPath, ".."));
8703
9636
  await cp(repoSrcPath, localPath, { force: true });
8704
9637
  return;
8705
9638
  }
@@ -8800,7 +9733,7 @@ async function restoreFromGithub(overrides) {
8800
9733
  }
8801
9734
  async function getBackupStatus() {
8802
9735
  const config = await readConfig();
8803
- const lockPath = resolve19(syntaurRoot(), LOCK_FILE_NAME);
9736
+ const lockPath = resolve20(syntaurRoot(), LOCK_FILE_NAME);
8804
9737
  const locked = await fileExists(lockPath);
8805
9738
  return {
8806
9739
  repo: config.backup?.repo ?? null,
@@ -8813,7 +9746,7 @@ async function getBackupStatus() {
8813
9746
 
8814
9747
  // src/dashboard/api-backup.ts
8815
9748
  function createBackupRouter() {
8816
- const router = Router6();
9749
+ const router = Router7();
8817
9750
  router.get("/", async (_req, res) => {
8818
9751
  try {
8819
9752
  const status = await getBackupStatus();
@@ -9137,7 +10070,7 @@ function createDashboardServer(options) {
9137
10070
  (async () => {
9138
10071
  try {
9139
10072
  const configResult = await migrateLegacyConfig(
9140
- resolve20(syntaurRoot(), "config.md")
10073
+ resolve21(syntaurRoot(), "config.md")
9141
10074
  );
9142
10075
  const projectResult = await migrateLegacyProjectFiles(projectsDir2);
9143
10076
  const summary = summarizeMigration(projectResult, configResult);
@@ -9244,7 +10177,7 @@ function createDashboardServer(options) {
9244
10177
  });
9245
10178
  app.get("/api/workspaces", async (_req, res) => {
9246
10179
  try {
9247
- const result = await listWorkspaces(projectsDir2);
10180
+ const result = await listWorkspaces(projectsDir2, assignmentsDir2);
9248
10181
  res.json(result);
9249
10182
  } catch (error) {
9250
10183
  console.error("Error listing workspaces:", error);
@@ -9361,18 +10294,28 @@ function createDashboardServer(options) {
9361
10294
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2));
9362
10295
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir3));
9363
10296
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
10297
+ app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir2, broadcast));
9364
10298
  app.use("/api/backup", createBackupRouter());
9365
10299
  if (serveStaticUi && dashboardDistPath) {
9366
- app.use(express.static(dashboardDistPath));
9367
- app.get("{*path}", async (_req, res) => {
9368
- const indexPath = resolve20(dashboardDistPath, "index.html");
9369
- if (await fileExists(indexPath)) {
9370
- res.sendFile(indexPath);
9371
- } else {
10300
+ app.use("/assets", express.static(resolve21(dashboardDistPath, "assets")));
10301
+ app.get("{*path}", async (req, res) => {
10302
+ if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
10303
+ res.status(404).json({ error: "Not Found" });
10304
+ return;
10305
+ }
10306
+ const indexPath = resolve21(dashboardDistPath, "index.html");
10307
+ if (!await fileExists(indexPath)) {
9372
10308
  res.status(503).send(
9373
10309
  'Dashboard not built. Run "npm run build:dashboard" first.'
9374
10310
  );
10311
+ return;
9375
10312
  }
10313
+ res.sendFile(indexPath, (err2) => {
10314
+ if (err2) {
10315
+ console.error("Error sending dashboard index.html:", err2);
10316
+ if (!res.headersSent) res.status(500).send("Dashboard load error");
10317
+ }
10318
+ });
9376
10319
  });
9377
10320
  }
9378
10321
  let watcherHandle = null;
@@ -9398,7 +10341,7 @@ function createDashboardServer(options) {
9398
10341
  }
9399
10342
  });
9400
10343
  server.listen(port, () => {
9401
- const portFile = resolve20(syntaurRoot(), "dashboard-port");
10344
+ const portFile = resolve21(syntaurRoot(), "dashboard-port");
9402
10345
  writeFile5(portFile, String(port), "utf-8").catch(() => {
9403
10346
  });
9404
10347
  resolvePromise();
@@ -9415,7 +10358,7 @@ function createDashboardServer(options) {
9415
10358
  client.terminate();
9416
10359
  }
9417
10360
  clients.clear();
9418
- const portFile = resolve20(syntaurRoot(), "dashboard-port");
10361
+ const portFile = resolve21(syntaurRoot(), "dashboard-port");
9419
10362
  await unlink4(portFile).catch(() => {
9420
10363
  });
9421
10364
  server.closeAllConnections?.();
@@ -9495,8 +10438,8 @@ async function dashboardCommand(options) {
9495
10438
  port = availablePort;
9496
10439
  }
9497
10440
  const thisFile = fileURLToPath2(import.meta.url);
9498
- const packageRoot = resolve21(dirname4(thisFile), "..");
9499
- const dashboardDist = resolve21(packageRoot, "dashboard", "dist");
10441
+ const packageRoot = resolve22(dirname4(thisFile), "..");
10442
+ const dashboardDist = resolve22(packageRoot, "dashboard", "dist");
9500
10443
  const server = createDashboardServer({
9501
10444
  port,
9502
10445
  projectsDir: projectsDir2,
@@ -9510,8 +10453,8 @@ async function dashboardCommand(options) {
9510
10453
  await server.start();
9511
10454
  let viteProcess = null;
9512
10455
  if (mode === "dev") {
9513
- const dashboardDir = resolve21(packageRoot, "dashboard");
9514
- const viteBin = resolve21(dashboardDir, "node_modules", ".bin", "vite");
10456
+ const dashboardDir = resolve22(packageRoot, "dashboard");
10457
+ const viteBin = resolve22(dashboardDir, "node_modules", ".bin", "vite");
9515
10458
  if (!await fileExists(viteBin)) {
9516
10459
  console.error(
9517
10460
  'Vite not found. Run "npm ci --prefix dashboard" first, or use the default bundled dashboard mode.'
@@ -9583,7 +10526,7 @@ async function dashboardCommand(options) {
9583
10526
  init_paths();
9584
10527
  init_fs();
9585
10528
  init_config2();
9586
- import { resolve as resolve22 } from "path";
10529
+ import { resolve as resolve23 } from "path";
9587
10530
  init_lifecycle();
9588
10531
  init_assignment_resolver();
9589
10532
  async function runTransition(assignment, command, options = {}) {
@@ -9596,8 +10539,8 @@ async function runTransition(assignment, command, options = {}) {
9596
10539
  if (!isValidSlug(assignment)) {
9597
10540
  throw new Error(`Invalid assignment slug "${assignment}".`);
9598
10541
  }
9599
- const projectDir = resolve22(baseDir, options.project);
9600
- const projectMdPath = resolve22(projectDir, "project.md");
10542
+ const projectDir = resolve23(baseDir, options.project);
10543
+ const projectMdPath = resolve23(projectDir, "project.md");
9601
10544
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
9602
10545
  throw new Error(`Project "${options.project}" not found at ${projectDir}.`);
9603
10546
  }
@@ -9628,8 +10571,8 @@ async function runAssign(assignment, agent, options = {}) {
9628
10571
  if (!isValidSlug(assignment)) {
9629
10572
  throw new Error(`Invalid assignment slug "${assignment}".`);
9630
10573
  }
9631
- const projectDir = resolve22(baseDir, options.project);
9632
- const projectMdPath = resolve22(projectDir, "project.md");
10574
+ const projectDir = resolve23(baseDir, options.project);
10575
+ const projectMdPath = resolve23(projectDir, "project.md");
9633
10576
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
9634
10577
  throw new Error(`Project "${options.project}" not found at ${projectDir}.`);
9635
10578
  }
@@ -9715,7 +10658,7 @@ import {
9715
10658
  readdir as readdir10,
9716
10659
  symlink,
9717
10660
  lstat,
9718
- readFile as readFile15,
10661
+ readFile as readFile16,
9719
10662
  readlink,
9720
10663
  rm as rm3,
9721
10664
  unlink as unlink5,
@@ -9723,20 +10666,20 @@ import {
9723
10666
  } from "fs/promises";
9724
10667
  import { existsSync } from "fs";
9725
10668
  import { homedir as homedir2 } from "os";
9726
- import { basename, dirname as dirname6, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve24 } from "path";
10669
+ import { basename, dirname as dirname6, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve25 } from "path";
9727
10670
 
9728
10671
  // src/utils/package-root.ts
9729
10672
  init_fs();
9730
- import { dirname as dirname5, resolve as resolve23 } from "path";
10673
+ import { dirname as dirname5, resolve as resolve24 } from "path";
9731
10674
  import { fileURLToPath as fileURLToPath3 } from "url";
9732
10675
  async function findPackageRoot(expectedRelativePath) {
9733
10676
  let currentDir = dirname5(fileURLToPath3(import.meta.url));
9734
10677
  while (true) {
9735
- const candidate = resolve23(currentDir, expectedRelativePath);
10678
+ const candidate = resolve24(currentDir, expectedRelativePath);
9736
10679
  if (await fileExists(candidate)) {
9737
10680
  return currentDir;
9738
10681
  }
9739
- const parentDir = resolve23(currentDir, "..");
10682
+ const parentDir = resolve24(currentDir, "..");
9740
10683
  if (parentDir === currentDir) {
9741
10684
  throw new Error(
9742
10685
  `Could not locate package root containing ${expectedRelativePath}.`
@@ -9757,25 +10700,25 @@ function getPluginManifestRelativePath(pluginKind) {
9757
10700
  }
9758
10701
  function getDefaultPluginTargetDir(pluginKind) {
9759
10702
  const home = homedir2();
9760
- return pluginKind === "claude" ? resolve24(home, ".claude", "plugins", "syntaur") : resolve24(home, "plugins", "syntaur");
10703
+ return pluginKind === "claude" ? resolve25(home, ".claude", "plugins", "syntaur") : resolve25(home, "plugins", "syntaur");
9761
10704
  }
9762
10705
  function getDefaultMarketplacePath() {
9763
- return resolve24(homedir2(), ".agents", "plugins", "marketplace.json");
10706
+ return resolve25(homedir2(), ".agents", "plugins", "marketplace.json");
9764
10707
  }
9765
10708
  function getClaudeMarketplacesRoot() {
9766
- return resolve24(homedir2(), ".claude", "plugins", "marketplaces");
10709
+ return resolve25(homedir2(), ".claude", "plugins", "marketplaces");
9767
10710
  }
9768
10711
  function getClaudeKnownMarketplacesPath() {
9769
- return resolve24(homedir2(), ".claude", "plugins", "known_marketplaces.json");
10712
+ return resolve25(homedir2(), ".claude", "plugins", "known_marketplaces.json");
9770
10713
  }
9771
10714
  function getClaudeInstalledPluginsPath() {
9772
- return resolve24(homedir2(), ".claude", "plugins", "installed_plugins.json");
10715
+ return resolve25(homedir2(), ".claude", "plugins", "installed_plugins.json");
9773
10716
  }
9774
10717
  function getInstallMarkerPath(targetDir) {
9775
- return resolve24(targetDir, INSTALL_MARKER_FILENAME);
10718
+ return resolve25(targetDir, INSTALL_MARKER_FILENAME);
9776
10719
  }
9777
10720
  async function readPackageManifest(packageRoot) {
9778
- const raw = await readFile15(resolve24(packageRoot, "package.json"), "utf-8");
10721
+ const raw = await readFile16(resolve25(packageRoot, "package.json"), "utf-8");
9779
10722
  return JSON.parse(raw);
9780
10723
  }
9781
10724
  async function readJsonFileIfExists(pathValue) {
@@ -9783,7 +10726,7 @@ async function readJsonFileIfExists(pathValue) {
9783
10726
  return null;
9784
10727
  }
9785
10728
  try {
9786
- const raw = await readFile15(pathValue, "utf-8");
10729
+ const raw = await readFile16(pathValue, "utf-8");
9787
10730
  return JSON.parse(raw);
9788
10731
  } catch {
9789
10732
  return null;
@@ -9791,15 +10734,15 @@ async function readJsonFileIfExists(pathValue) {
9791
10734
  }
9792
10735
  async function readClaudePluginManifest(pluginDir) {
9793
10736
  return await readJsonFileIfExists(
9794
- resolve24(pluginDir, ".claude-plugin", "plugin.json")
10737
+ resolve25(pluginDir, ".claude-plugin", "plugin.json")
9795
10738
  ) ?? {};
9796
10739
  }
9797
10740
  async function readPluginManifestName(targetDir, pluginKind) {
9798
- const manifestPath = resolve24(targetDir, getPluginManifestRelativePath(pluginKind));
10741
+ const manifestPath = resolve25(targetDir, getPluginManifestRelativePath(pluginKind));
9799
10742
  if (!await fileExists(manifestPath)) {
9800
10743
  return void 0;
9801
10744
  }
9802
- const raw = await readFile15(manifestPath, "utf-8");
10745
+ const raw = await readFile16(manifestPath, "utf-8");
9803
10746
  const parsed = JSON.parse(raw);
9804
10747
  return parsed.name;
9805
10748
  }
@@ -9809,7 +10752,7 @@ async function readInstallMetadata(targetDir) {
9809
10752
  return null;
9810
10753
  }
9811
10754
  try {
9812
- const raw = await readFile15(markerPath, "utf-8");
10755
+ const raw = await readFile16(markerPath, "utf-8");
9813
10756
  return JSON.parse(raw);
9814
10757
  } catch {
9815
10758
  return null;
@@ -9822,7 +10765,7 @@ async function getInstallStatus(targetDir, pluginKind) {
9822
10765
  const info = await lstat(targetDir);
9823
10766
  if (info.isSymbolicLink()) {
9824
10767
  const symlinkTarget = await readlink(targetDir);
9825
- const resolvedTarget = resolve24(dirname6(targetDir), symlinkTarget);
10768
+ const resolvedTarget = resolve25(dirname6(targetDir), symlinkTarget);
9826
10769
  const manifestName2 = await readPluginManifestName(resolvedTarget, pluginKind);
9827
10770
  return {
9828
10771
  exists: true,
@@ -9868,7 +10811,7 @@ async function installLink(paths) {
9868
10811
  await ensureDir(dirname6(paths.targetDir));
9869
10812
  await rm3(paths.targetDir, { recursive: true, force: true });
9870
10813
  await ensureDir(dirname6(paths.targetDir));
9871
- await symlink(resolve24(paths.sourceDir), paths.targetDir, "dir");
10814
+ await symlink(resolve25(paths.sourceDir), paths.targetDir, "dir");
9872
10815
  }
9873
10816
  async function removeInstallMarker(targetDir) {
9874
10817
  const markerPath = getInstallMarkerPath(targetDir);
@@ -9882,13 +10825,13 @@ function normalizeAbsoluteInstallPath(pathValue, label) {
9882
10825
  if (!isAbsolute2(expanded)) {
9883
10826
  throw new Error(`${label} must be an absolute path.`);
9884
10827
  }
9885
- return resolve24(expanded);
10828
+ return resolve25(expanded);
9886
10829
  }
9887
10830
  async function resolvePluginPaths(pluginKind, targetDir) {
9888
10831
  const packageRoot = await findPackageRoot(getPluginRelativePath(pluginKind));
9889
10832
  return {
9890
10833
  packageRoot,
9891
- sourceDir: resolve24(packageRoot, getPluginRelativePath(pluginKind)),
10834
+ sourceDir: resolve25(packageRoot, getPluginRelativePath(pluginKind)),
9892
10835
  targetDir: targetDir ?? getDefaultPluginTargetDir(pluginKind)
9893
10836
  };
9894
10837
  }
@@ -9992,8 +10935,8 @@ async function listClaudeMarketplaceCandidates() {
9992
10935
  if (!entry.isDirectory()) {
9993
10936
  continue;
9994
10937
  }
9995
- const candidateRoot = resolve24(rootDir, entry.name);
9996
- const manifestPath = resolve24(candidateRoot, ".claude-plugin", "marketplace.json");
10938
+ const candidateRoot = resolve25(rootDir, entry.name);
10939
+ const manifestPath = resolve25(candidateRoot, ".claude-plugin", "marketplace.json");
9997
10940
  if (!await fileExists(manifestPath)) {
9998
10941
  continue;
9999
10942
  }
@@ -10020,11 +10963,11 @@ async function listClaudeMarketplaceCandidates() {
10020
10963
  if (!installLocation) {
10021
10964
  continue;
10022
10965
  }
10023
- const candidateRoot = resolve24(expandHome(installLocation));
10966
+ const candidateRoot = resolve25(expandHome(installLocation));
10024
10967
  if (seen.has(candidateRoot)) {
10025
10968
  continue;
10026
10969
  }
10027
- const manifestPath = resolve24(candidateRoot, ".claude-plugin", "marketplace.json");
10970
+ const manifestPath = resolve25(candidateRoot, ".claude-plugin", "marketplace.json");
10028
10971
  if (!await fileExists(manifestPath)) {
10029
10972
  continue;
10030
10973
  }
@@ -10052,7 +10995,7 @@ async function getPreferredClaudeMarketplace() {
10052
10995
  name: candidate.name,
10053
10996
  rootDir: candidate.rootDir,
10054
10997
  manifestPath: candidate.manifestPath,
10055
- targetDir: resolve24(candidate.rootDir, "plugins", "syntaur")
10998
+ targetDir: resolve25(candidate.rootDir, "plugins", "syntaur")
10056
10999
  };
10057
11000
  }
10058
11001
  async function registerKnownClaudeMarketplace(name, rootDir) {
@@ -10079,9 +11022,9 @@ async function ensureClaudeUserMarketplace() {
10079
11022
  if (existing) {
10080
11023
  return existing;
10081
11024
  }
10082
- const rootDir = resolve24(getClaudeMarketplacesRoot(), "user-plugins");
10083
- const manifestPath = resolve24(rootDir, ".claude-plugin", "marketplace.json");
10084
- await ensureDir(resolve24(rootDir, "plugins"));
11025
+ const rootDir = resolve25(getClaudeMarketplacesRoot(), "user-plugins");
11026
+ const manifestPath = resolve25(rootDir, ".claude-plugin", "marketplace.json");
11027
+ await ensureDir(resolve25(rootDir, "plugins"));
10085
11028
  if (!await fileExists(manifestPath)) {
10086
11029
  const scaffold = {
10087
11030
  plugins: []
@@ -10100,7 +11043,7 @@ async function ensureClaudeUserMarketplace() {
10100
11043
  name: "user-plugins",
10101
11044
  rootDir,
10102
11045
  manifestPath,
10103
- targetDir: resolve24(rootDir, "plugins", "syntaur")
11046
+ targetDir: resolve25(rootDir, "plugins", "syntaur")
10104
11047
  };
10105
11048
  }
10106
11049
  async function detectClaudeMarketplaceForTarget(targetDir) {
@@ -10110,7 +11053,7 @@ async function detectClaudeMarketplaceForTarget(targetDir) {
10110
11053
  return null;
10111
11054
  }
10112
11055
  const rootDir = dirname6(pluginsDir);
10113
- const manifestPath = resolve24(rootDir, ".claude-plugin", "marketplace.json");
11056
+ const manifestPath = resolve25(rootDir, ".claude-plugin", "marketplace.json");
10114
11057
  if (!await fileExists(manifestPath)) {
10115
11058
  return null;
10116
11059
  }
@@ -10126,7 +11069,7 @@ async function detectClaudeMarketplaceForTarget(targetDir) {
10126
11069
  async function findManagedClaudeMarketplacePluginDir() {
10127
11070
  const marketplaces = await listClaudeMarketplaceCandidates();
10128
11071
  for (const marketplace of marketplaces) {
10129
- const targetDir = resolve24(marketplace.rootDir, "plugins", "syntaur");
11072
+ const targetDir = resolve25(marketplace.rootDir, "plugins", "syntaur");
10130
11073
  const status = await getInstallStatus(targetDir, "claude");
10131
11074
  if (status.exists && status.managed) {
10132
11075
  return targetDir;
@@ -10251,7 +11194,7 @@ async function installManagedPlugin(options) {
10251
11194
  `${paths.targetDir} already exists and is not a Syntaur-managed install. Remove it manually before installing Syntaur there.`
10252
11195
  );
10253
11196
  }
10254
- if (desiredMode === "link" && existing.exists && existing.installMode === "link" && existing.symlinkTarget === resolve24(paths.sourceDir) && !force) {
11197
+ if (desiredMode === "link" && existing.exists && existing.installMode === "link" && existing.symlinkTarget === resolve25(paths.sourceDir) && !force) {
10255
11198
  return {
10256
11199
  targetDir: paths.targetDir,
10257
11200
  sourceDir: paths.sourceDir,
@@ -10303,7 +11246,7 @@ async function readMarketplaceFile(marketplacePath) {
10303
11246
  plugins: []
10304
11247
  };
10305
11248
  }
10306
- const raw = await readFile15(marketplacePath, "utf-8");
11249
+ const raw = await readFile16(marketplacePath, "utf-8");
10307
11250
  const parsed = JSON.parse(raw);
10308
11251
  return {
10309
11252
  name: parsed.name ?? "local",
@@ -10464,13 +11407,13 @@ async function recommendMarketplacePath() {
10464
11407
  return configuredOrManaged ?? getDefaultMarketplacePath();
10465
11408
  }
10466
11409
  async function isSyntaurDataInstalled() {
10467
- return fileExists(resolve24(syntaurRoot(), "config.md"));
11410
+ return fileExists(resolve25(syntaurRoot(), "config.md"));
10468
11411
  }
10469
11412
  async function removeSyntaurData() {
10470
11413
  await rm3(syntaurRoot(), { recursive: true, force: true });
10471
11414
  }
10472
11415
  async function getConfiguredProjectDir() {
10473
- if (!await fileExists(resolve24(syntaurRoot(), "config.md"))) {
11416
+ if (!await fileExists(resolve25(syntaurRoot(), "config.md"))) {
10474
11417
  return null;
10475
11418
  }
10476
11419
  return (await readConfig()).defaultProjectDir;
@@ -10539,8 +11482,8 @@ async function textPrompt(question, defaultValue) {
10539
11482
 
10540
11483
  // src/utils/install-skills.ts
10541
11484
  init_fs();
10542
- import { readFile as readFile16, readdir as readdir11, mkdir as mkdir2, copyFile, rm as rm4 } from "fs/promises";
10543
- import { dirname as dirname7, resolve as resolve25, relative as relative3, join as join3 } from "path";
11485
+ import { readFile as readFile17, readdir as readdir11, mkdir as mkdir3, copyFile, rm as rm4 } from "fs/promises";
11486
+ import { dirname as dirname7, resolve as resolve26, relative as relative3, join as join3 } from "path";
10544
11487
  import { fileURLToPath as fileURLToPath4 } from "url";
10545
11488
  import { homedir as homedir3 } from "os";
10546
11489
  var REQUIRED_SKILLS = [
@@ -10553,11 +11496,11 @@ var REQUIRED_SKILLS = [
10553
11496
  ];
10554
11497
  function getVendoredSkillsDir() {
10555
11498
  const here = dirname7(fileURLToPath4(import.meta.url));
10556
- return resolve25(here, "..", "vendor", "syntaur-skills", "skills");
11499
+ return resolve26(here, "..", "vendor", "syntaur-skills", "skills");
10557
11500
  }
10558
11501
  function defaultSkillTargetDir(target) {
10559
- if (target === "claude") return resolve25(homedir3(), ".claude", "skills");
10560
- return resolve25(homedir3(), ".codex", "skills");
11502
+ if (target === "claude") return resolve26(homedir3(), ".claude", "skills");
11503
+ return resolve26(homedir3(), ".codex", "skills");
10561
11504
  }
10562
11505
  async function walkFiles(root) {
10563
11506
  const out = [];
@@ -10577,7 +11520,7 @@ async function walkFiles(root) {
10577
11520
  }
10578
11521
  async function filesEqual(a, b) {
10579
11522
  try {
10580
- const [ba, bb] = await Promise.all([readFile16(a), readFile16(b)]);
11523
+ const [ba, bb] = await Promise.all([readFile17(a), readFile17(b)]);
10581
11524
  if (ba.length !== bb.length) return false;
10582
11525
  return ba.equals(bb);
10583
11526
  } catch {
@@ -10585,7 +11528,7 @@ async function filesEqual(a, b) {
10585
11528
  }
10586
11529
  }
10587
11530
  async function copyDir(srcDir, destDir) {
10588
- await mkdir2(destDir, { recursive: true });
11531
+ await mkdir3(destDir, { recursive: true });
10589
11532
  const entries = await readdir11(srcDir, { withFileTypes: true });
10590
11533
  for (const entry of entries) {
10591
11534
  const src = join3(srcDir, entry.name);
@@ -10619,7 +11562,7 @@ async function installSkills(options) {
10619
11562
  );
10620
11563
  }
10621
11564
  const results = [];
10622
- await mkdir2(targetRoot, { recursive: true });
11565
+ await mkdir3(targetRoot, { recursive: true });
10623
11566
  for (const skill of REQUIRED_SKILLS) {
10624
11567
  const srcDir = join3(source, skill);
10625
11568
  const destDir = join3(targetRoot, skill);
@@ -10668,7 +11611,7 @@ async function uninstallSkills(options) {
10668
11611
  if (!await fileExists(destDir)) continue;
10669
11612
  const skillMd = join3(destDir, "SKILL.md");
10670
11613
  if (!await fileExists(skillMd)) continue;
10671
- const content = await readFile16(skillMd, "utf-8").catch(() => "");
11614
+ const content = await readFile17(skillMd, "utf-8").catch(() => "");
10672
11615
  const match = content.match(/^name:\s*(\S+)\s*$/m);
10673
11616
  if (!match || match[1] !== skill) continue;
10674
11617
  await rm4(destDir, { recursive: true, force: true });
@@ -10805,16 +11748,16 @@ async function installPluginCommand(options) {
10805
11748
  // src/commands/install-statusline.ts
10806
11749
  init_paths();
10807
11750
  init_fs();
10808
- import { readFile as readFile18, writeFile as writeFile8, copyFile as copyFile2, rm as rm5, stat as stat3, symlink as symlink2, unlink as unlink6, lstat as lstat2 } from "fs/promises";
10809
- import { resolve as resolve27, dirname as dirname9 } from "path";
11751
+ import { readFile as readFile19, writeFile as writeFile8, copyFile as copyFile2, rm as rm5, stat as stat3, symlink as symlink2, unlink as unlink6, lstat as lstat2 } from "fs/promises";
11752
+ import { resolve as resolve28, dirname as dirname9 } from "path";
10810
11753
  import { homedir as homedir4 } from "os";
10811
11754
  import { fileURLToPath as fileURLToPath5 } from "url";
10812
11755
 
10813
11756
  // src/commands/configure-statusline.ts
10814
11757
  init_paths();
10815
11758
  init_fs();
10816
- import { readFile as readFile17, writeFile as writeFile7 } from "fs/promises";
10817
- import { resolve as resolve26, dirname as dirname8 } from "path";
11759
+ import { readFile as readFile18, writeFile as writeFile7 } from "fs/promises";
11760
+ import { resolve as resolve27, dirname as dirname8 } from "path";
10818
11761
  import { spawnSync } from "child_process";
10819
11762
  import { checkbox, input as input2, confirm } from "@inquirer/prompts";
10820
11763
  var AVAILABLE_SEGMENTS = [
@@ -10835,12 +11778,12 @@ var PRESETS = {
10835
11778
  tracker: { segments: ["git", "assignment", "external", "session"], separator: " \xB7 " }
10836
11779
  };
10837
11780
  function getConfigPath(installRoot) {
10838
- return resolve26(installRoot, "statusline.config.json");
11781
+ return resolve27(installRoot, "statusline.config.json");
10839
11782
  }
10840
11783
  async function readConfig2(path) {
10841
11784
  if (!await fileExists(path)) return null;
10842
11785
  try {
10843
- const raw = await readFile17(path, "utf-8");
11786
+ const raw = await readFile18(path, "utf-8");
10844
11787
  const parsed = JSON.parse(raw);
10845
11788
  if (!parsed || typeof parsed !== "object") return null;
10846
11789
  const segments = Array.isArray(parsed.segments) ? parsed.segments.filter(isSegmentName) : [];
@@ -10993,7 +11936,7 @@ async function configureStatuslineCommand(options = {}) {
10993
11936
  console.log(` segments: ${config.segments.join(", ")}`);
10994
11937
  console.log(` separator: ${JSON.stringify(config.separator)}`);
10995
11938
  if (config.wrap) console.log(` wrap: ${config.wrap}`);
10996
- const script = options.statuslineScript ?? resolve26(installRoot, "statusline.sh");
11939
+ const script = options.statuslineScript ?? resolve27(installRoot, "statusline.sh");
10997
11940
  if (await fileExists(script)) {
10998
11941
  console.log("");
10999
11942
  console.log("Live preview:");
@@ -11024,11 +11967,11 @@ async function writeDefaultConfigIfMissing(installRoot) {
11024
11967
  // src/commands/install-statusline.ts
11025
11968
  function getPackageStatuslineSource() {
11026
11969
  const here = dirname9(fileURLToPath5(import.meta.url));
11027
- return resolve27(here, "..", "statusline", "statusline.sh");
11970
+ return resolve28(here, "..", "statusline", "statusline.sh");
11028
11971
  }
11029
11972
  async function readSettingsJson(settingsPath) {
11030
11973
  if (!await fileExists(settingsPath)) return {};
11031
- const raw = await readFile18(settingsPath, "utf-8");
11974
+ const raw = await readFile19(settingsPath, "utf-8");
11032
11975
  if (raw.trim() === "") return {};
11033
11976
  try {
11034
11977
  const parsed = JSON.parse(raw);
@@ -11108,12 +12051,12 @@ async function installScript(sourceScript, destScript, link) {
11108
12051
  }
11109
12052
  async function installStatuslineCommand(options = {}) {
11110
12053
  const mode = options.mode ?? "ask";
11111
- const settingsPath = options.settingsPath ?? resolve27(homedir4(), ".claude", "settings.json");
12054
+ const settingsPath = options.settingsPath ?? resolve28(homedir4(), ".claude", "settings.json");
11112
12055
  const installRoot = options.installRoot ?? syntaurRoot();
11113
12056
  const sourceScript = options.sourceScript ?? getPackageStatuslineSource();
11114
- const destScript = resolve27(installRoot, "statusline.sh");
11115
- const confPath = resolve27(installRoot, "statusline.conf");
11116
- const backupPath = resolve27(installRoot, "statusline.backup.json");
12057
+ const destScript = resolve28(installRoot, "statusline.sh");
12058
+ const confPath = resolve28(installRoot, "statusline.conf");
12059
+ const backupPath = resolve28(installRoot, "statusline.backup.json");
11117
12060
  if (!await fileExists(sourceScript)) {
11118
12061
  throw new Error(
11119
12062
  `Statusline source script not found at ${sourceScript}. Try re-installing syntaur (npm install -g syntaur) or pass --source-script explicitly.`
@@ -11149,7 +12092,7 @@ async function installStatuslineCommand(options = {}) {
11149
12092
  if (parsed) {
11150
12093
  wrapTarget = parsed;
11151
12094
  } else {
11152
- const wrapperPath = resolve27(installRoot, "statusline-wrapped.sh");
12095
+ const wrapperPath = resolve28(installRoot, "statusline-wrapped.sh");
11153
12096
  const wrapperBody = `#!/usr/bin/env bash
11154
12097
  # Auto-generated by syntaur install-statusline.
11155
12098
  # Executes the previously configured statusLine command.
@@ -11204,19 +12147,19 @@ async function chmodExec(path) {
11204
12147
  }
11205
12148
  }
11206
12149
  async function uninstallStatuslineCommand(options = {}) {
11207
- const settingsPath = options.settingsPath ?? resolve27(homedir4(), ".claude", "settings.json");
12150
+ const settingsPath = options.settingsPath ?? resolve28(homedir4(), ".claude", "settings.json");
11208
12151
  const installRoot = options.installRoot ?? syntaurRoot();
11209
- const destScript = resolve27(installRoot, "statusline.sh");
11210
- const confPath = resolve27(installRoot, "statusline.conf");
11211
- const backupPath = resolve27(installRoot, "statusline.backup.json");
11212
- const wrapperPath = resolve27(installRoot, "statusline-wrapped.sh");
12152
+ const destScript = resolve28(installRoot, "statusline.sh");
12153
+ const confPath = resolve28(installRoot, "statusline.conf");
12154
+ const backupPath = resolve28(installRoot, "statusline.backup.json");
12155
+ const wrapperPath = resolve28(installRoot, "statusline-wrapped.sh");
11213
12156
  const settings = await readSettingsJson(settingsPath);
11214
12157
  const existing = extractExistingCommand(settings);
11215
12158
  const ourCommand = `bash ${destScript}`;
11216
12159
  let restored = null;
11217
12160
  if (await fileExists(backupPath)) {
11218
12161
  try {
11219
- const raw = await readFile18(backupPath, "utf-8");
12162
+ const raw = await readFile19(backupPath, "utf-8");
11220
12163
  const parsed = JSON.parse(raw);
11221
12164
  const prev = parsed?.previousStatusLine;
11222
12165
  if (prev && typeof prev === "object" && typeof prev.command === "string") {
@@ -11237,7 +12180,7 @@ async function uninstallStatuslineCommand(options = {}) {
11237
12180
  await writeSettingsJson(settingsPath, settings);
11238
12181
  }
11239
12182
  if (!options.keepScript) {
11240
- const configPath = resolve27(installRoot, "statusline.config.json");
12183
+ const configPath = resolve28(installRoot, "statusline.config.json");
11241
12184
  for (const path of [destScript, confPath, backupPath, wrapperPath, configPath]) {
11242
12185
  try {
11243
12186
  await rm5(path, { force: true });
@@ -11492,7 +12435,7 @@ async function setupCommand(options) {
11492
12435
  }
11493
12436
 
11494
12437
  // src/commands/uninstall.ts
11495
- import { resolve as resolve28 } from "path";
12438
+ import { resolve as resolve29 } from "path";
11496
12439
  init_paths();
11497
12440
  function expandTargets(options) {
11498
12441
  if (options.all) {
@@ -11572,7 +12515,7 @@ async function uninstallCommand(options) {
11572
12515
  const configuredProjectDir = await getConfiguredProjectDir();
11573
12516
  await removeSyntaurData();
11574
12517
  console.log(`Removed ${syntaurRoot()}`);
11575
- if (configuredProjectDir && resolve28(configuredProjectDir) !== resolve28(syntaurRoot(), "projects")) {
12518
+ if (configuredProjectDir && resolve29(configuredProjectDir) !== resolve29(syntaurRoot(), "projects")) {
11576
12519
  console.warn(
11577
12520
  `Warning: config.md pointed to an external project directory (${configuredProjectDir}). That directory was not removed automatically.`
11578
12521
  );
@@ -11587,7 +12530,7 @@ async function uninstallCommand(options) {
11587
12530
  init_paths();
11588
12531
  init_fs();
11589
12532
  init_config2();
11590
- import { resolve as resolve29 } from "path";
12533
+ import { resolve as resolve30 } from "path";
11591
12534
  var SUPPORTED_FRAMEWORKS = ["cursor", "codex", "opencode"];
11592
12535
  async function setupAdapterCommand(framework, options) {
11593
12536
  if (!SUPPORTED_FRAMEWORKS.includes(framework)) {
@@ -11613,19 +12556,19 @@ async function setupAdapterCommand(framework, options) {
11613
12556
  }
11614
12557
  const config = await readConfig();
11615
12558
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
11616
- const projectDir = resolve29(baseDir, options.project);
11617
- const assignmentDir = resolve29(
12559
+ const projectDir = resolve30(baseDir, options.project);
12560
+ const assignmentDir = resolve30(
11618
12561
  projectDir,
11619
12562
  "assignments",
11620
12563
  options.assignment
11621
12564
  );
11622
- const projectMdPath = resolve29(projectDir, "project.md");
12565
+ const projectMdPath = resolve30(projectDir, "project.md");
11623
12566
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
11624
12567
  throw new Error(
11625
12568
  `Project "${options.project}" not found at ${projectDir}.`
11626
12569
  );
11627
12570
  }
11628
- const assignmentMdPath = resolve29(assignmentDir, "assignment.md");
12571
+ const assignmentMdPath = resolve30(assignmentDir, "assignment.md");
11629
12572
  if (!await fileExists(assignmentDir) || !await fileExists(assignmentMdPath)) {
11630
12573
  throw new Error(
11631
12574
  `Assignment "${options.assignment}" not found at ${assignmentDir}.`
@@ -11653,15 +12596,15 @@ async function setupAdapterCommand(framework, options) {
11653
12596
  }
11654
12597
  }
11655
12598
  if (framework === "cursor") {
11656
- const protocolPath = resolve29(cwd, ".cursor", "rules", "syntaur-protocol.mdc");
11657
- const assignmentPath = resolve29(cwd, ".cursor", "rules", "syntaur-assignment.mdc");
12599
+ const protocolPath = resolve30(cwd, ".cursor", "rules", "syntaur-protocol.mdc");
12600
+ const assignmentPath = resolve30(cwd, ".cursor", "rules", "syntaur-assignment.mdc");
11658
12601
  await writeAdapterFile(protocolPath, renderCursorProtocol());
11659
12602
  await writeAdapterFile(assignmentPath, renderCursorAssignment(rendererParams));
11660
12603
  } else if (framework === "codex" || framework === "opencode") {
11661
- const agentsPath = resolve29(cwd, "AGENTS.md");
12604
+ const agentsPath = resolve30(cwd, "AGENTS.md");
11662
12605
  await writeAdapterFile(agentsPath, renderCodexAgents(rendererParams));
11663
12606
  if (framework === "opencode") {
11664
- const configPath = resolve29(cwd, "opencode.json");
12607
+ const configPath = resolve30(cwd, "opencode.json");
11665
12608
  await writeAdapterFile(configPath, renderOpenCodeConfig({ projectDir }));
11666
12609
  }
11667
12610
  }
@@ -11686,7 +12629,7 @@ async function setupAdapterCommand(framework, options) {
11686
12629
  init_paths();
11687
12630
  init_fs();
11688
12631
  init_config2();
11689
- import { resolve as resolve30 } from "path";
12632
+ import { resolve as resolve31 } from "path";
11690
12633
  async function trackSessionCommand(options) {
11691
12634
  if (!options.agent) {
11692
12635
  throw new Error("--agent <name> is required.");
@@ -11699,7 +12642,7 @@ async function trackSessionCommand(options) {
11699
12642
  if (options.project) {
11700
12643
  const config = await readConfig();
11701
12644
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
11702
- const projectDir = resolve30(baseDir, options.project);
12645
+ const projectDir = resolve31(baseDir, options.project);
11703
12646
  if (!await fileExists(projectDir)) {
11704
12647
  throw new Error(
11705
12648
  `Project "${options.project}" not found at ${projectDir}.`
@@ -11754,10 +12697,11 @@ async function browseCommand(options) {
11754
12697
  }
11755
12698
 
11756
12699
  // src/commands/create-playbook.ts
11757
- import { resolve as resolve32 } from "path";
12700
+ import { resolve as resolve33 } from "path";
11758
12701
  init_timestamp();
11759
12702
  init_paths();
11760
12703
  init_fs();
12704
+ init_playbooks();
11761
12705
  async function createPlaybookCommand(name, options) {
11762
12706
  if (!name.trim()) {
11763
12707
  throw new Error("Playbook name cannot be empty.");
@@ -11770,7 +12714,7 @@ async function createPlaybookCommand(name, options) {
11770
12714
  }
11771
12715
  const dir = playbooksDir();
11772
12716
  await ensureDir(dir);
11773
- const filePath = resolve32(dir, `${slug}.md`);
12717
+ const filePath = resolve33(dir, `${slug}.md`);
11774
12718
  if (await fileExists(filePath)) {
11775
12719
  throw new Error(
11776
12720
  `Playbook "${slug}" already exists at ${filePath}
@@ -11791,32 +12735,97 @@ Use --slug to specify a different slug.`
11791
12735
  init_paths();
11792
12736
  init_fs();
11793
12737
  init_parser();
11794
- import { readdir as readdir12, readFile as readFile19 } from "fs/promises";
11795
- import { resolve as resolve33 } from "path";
11796
- async function listPlaybooksCommand() {
12738
+ init_config2();
12739
+ import { readdir as readdir12, readFile as readFile20 } from "fs/promises";
12740
+ import { resolve as resolve34 } from "path";
12741
+ async function listPlaybooksCommand(options = {}) {
11797
12742
  const dir = playbooksDir();
11798
12743
  if (!await fileExists(dir)) {
11799
12744
  console.log('No playbooks directory found. Run "syntaur init" first.');
11800
12745
  return;
11801
12746
  }
12747
+ const config = await readConfig();
12748
+ const disabledSet = new Set(config.playbooks.disabled);
11802
12749
  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>".');
12750
+ const mdFiles = entries.filter(
12751
+ (e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_") && e.name !== "manifest.md"
12752
+ );
12753
+ const rows = [];
12754
+ for (const entry of mdFiles) {
12755
+ const filePath = resolve34(dir, entry.name);
12756
+ const raw = await readFile20(filePath, "utf-8");
12757
+ const parsed = parsePlaybook(raw);
12758
+ const slug = parsed.slug || entry.name.replace(/\.md$/, "");
12759
+ const disabled = disabledSet.has(slug);
12760
+ if (disabled && !options.all) continue;
12761
+ rows.push({
12762
+ slug,
12763
+ name: parsed.name || slug,
12764
+ desc: parsed.description || "",
12765
+ disabled
12766
+ });
12767
+ }
12768
+ if (rows.length === 0) {
12769
+ if (!options.all && disabledSet.size > 0) {
12770
+ console.log(
12771
+ `No enabled playbooks found (${disabledSet.size} disabled). Use --all to include disabled playbooks.`
12772
+ );
12773
+ } else {
12774
+ console.log('No playbooks found. Create one with "syntaur create-playbook <name>".');
12775
+ }
11806
12776
  return;
11807
12777
  }
11808
- console.log(`Found ${mdFiles.length} playbook(s):
12778
+ const totalLabel = options.all ? `Found ${rows.length} playbook(s) (${[...disabledSet].length} disabled):` : `Found ${rows.length} enabled playbook(s):`;
12779
+ console.log(`${totalLabel}
11809
12780
  `);
11810
12781
  console.log(`${"Slug".padEnd(30)} ${"Name".padEnd(30)} Description`);
11811
12782
  console.log(`${"\u2500".repeat(30)} ${"\u2500".repeat(30)} ${"\u2500".repeat(40)}`);
11812
- for (const entry of mdFiles) {
11813
- const filePath = resolve33(dir, entry.name);
11814
- const raw = await readFile19(filePath, "utf-8");
11815
- const parsed = parsePlaybook(raw);
11816
- 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}`);
12783
+ for (const row of rows) {
12784
+ const suffix = row.disabled ? " (disabled)" : "";
12785
+ const desc = `${row.desc}${suffix}`;
12786
+ console.log(`${row.slug.padEnd(30)} ${row.name.padEnd(30)} ${desc}`);
12787
+ }
12788
+ }
12789
+
12790
+ // src/commands/enable-playbook.ts
12791
+ init_paths();
12792
+ init_playbooks();
12793
+ async function enablePlaybookCommand(slug) {
12794
+ if (!slug.trim()) {
12795
+ throw new Error("Playbook slug cannot be empty.");
12796
+ }
12797
+ if (!isValidSlug(slug)) {
12798
+ throw new Error(
12799
+ `Invalid slug "${slug}". Slugs must be lowercase, hyphen-separated, with no special characters.`
12800
+ );
12801
+ }
12802
+ const dir = playbooksDir();
12803
+ const { slug: canonical, changed } = await setPlaybookEnabled(dir, slug, true);
12804
+ if (changed) {
12805
+ console.log(`Playbook "${canonical}" enabled.`);
12806
+ } else {
12807
+ console.log(`Playbook "${canonical}" is already enabled.`);
12808
+ }
12809
+ }
12810
+
12811
+ // src/commands/disable-playbook.ts
12812
+ init_paths();
12813
+ init_playbooks();
12814
+ async function disablePlaybookCommand(slug) {
12815
+ if (!slug.trim()) {
12816
+ throw new Error("Playbook slug cannot be empty.");
12817
+ }
12818
+ if (!isValidSlug(slug)) {
12819
+ throw new Error(
12820
+ `Invalid slug "${slug}". Slugs must be lowercase, hyphen-separated, with no special characters.`
12821
+ );
12822
+ }
12823
+ const dir = playbooksDir();
12824
+ const { slug: canonical, changed } = await setPlaybookEnabled(dir, slug, false);
12825
+ if (changed) {
12826
+ console.log(`Playbook "${canonical}" disabled.`);
12827
+ } else {
12828
+ console.log(`Playbook "${canonical}" is already disabled.`);
11820
12829
  }
11821
12830
  }
11822
12831
 
@@ -11824,28 +12833,44 @@ async function listPlaybooksCommand() {
11824
12833
  init_paths();
11825
12834
  init_parser2();
11826
12835
  init_fs();
12836
+ init_config2();
11827
12837
  import { Command } from "commander";
11828
- import { readFile as readFile20 } from "fs/promises";
11829
- import { resolve as resolve34 } from "path";
12838
+ import { readFile as readFile21 } from "fs/promises";
12839
+ import { resolve as resolve35 } from "path";
11830
12840
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
11831
- function resolveWorkspace(options) {
11832
- if (options.global) return "_global";
12841
+ async function resolveScope(options) {
12842
+ const flagCount = [Boolean(options.project), Boolean(options.workspace), Boolean(options.global)].filter(Boolean).length;
12843
+ if (flagCount > 1) {
12844
+ throw new Error("Use at most one of --project, --workspace, --global.");
12845
+ }
12846
+ if (options.project) {
12847
+ if (!isValidSlug(options.project)) {
12848
+ throw new Error(`Invalid project slug: "${options.project}".`);
12849
+ }
12850
+ const config = await readConfig();
12851
+ const projectMd = resolve35(config.defaultProjectDir, options.project, "project.md");
12852
+ if (!await fileExists(projectMd)) {
12853
+ throw new Error(`Project "${options.project}" not found.`);
12854
+ }
12855
+ return { kind: "project", id: options.project, todosPath: projectTodosDir(config.defaultProjectDir, options.project) };
12856
+ }
11833
12857
  if (options.workspace) {
11834
12858
  if (!WORKSPACE_REGEX2.test(options.workspace)) {
11835
12859
  throw new Error(`Invalid workspace name: "${options.workspace}". Use lowercase letters, numbers, hyphens, and underscores.`);
11836
12860
  }
11837
- return options.workspace;
12861
+ return { kind: "workspace", id: options.workspace, todosPath: todosDir() };
11838
12862
  }
11839
- return "_global";
12863
+ return { kind: "workspace", id: "_global", todosPath: todosDir() };
11840
12864
  }
11841
12865
  function nowISO() {
11842
12866
  return (/* @__PURE__ */ new Date()).toISOString();
11843
12867
  }
11844
12868
  var todoCommand = new Command("todo").description("Manage quick todos");
11845
- todoCommand.command("add").description("Add a new todo item").argument("<description>", "Todo description").option("--tags <tags>", "Comma-separated tags").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (description, options) => {
12869
+ todoCommand.command("add").description("Add a new todo item").argument("<description>", "Todo description").option("--tags <tags>", "Comma-separated tags").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (description, options) => {
11846
12870
  try {
11847
- const todosPath = todosDir();
11848
- const workspace = resolveWorkspace(options);
12871
+ const scope = await resolveScope(options);
12872
+ const todosPath = scope.todosPath;
12873
+ const workspace = scope.id;
11849
12874
  const checklist = await readChecklist(todosPath, workspace);
11850
12875
  const existingIds = new Set(checklist.items.map((i) => i.id));
11851
12876
  const id = generateUniqueId(existingIds);
@@ -11859,10 +12884,11 @@ todoCommand.command("add").description("Add a new todo item").argument("<descrip
11859
12884
  process.exit(1);
11860
12885
  }
11861
12886
  });
11862
- todoCommand.command("list").description("List todo items").option("--tag <tag>", "Filter by tag").option("--status <status>", "Filter by status (open|in_progress|completed|blocked)").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (options) => {
12887
+ todoCommand.command("list").description("List todo items").option("--tag <tag>", "Filter by tag").option("--status <status>", "Filter by status (open|in_progress|completed|blocked)").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (options) => {
11863
12888
  try {
11864
- const todosPath = todosDir();
11865
- const workspace = resolveWorkspace(options);
12889
+ const scope = await resolveScope(options);
12890
+ const todosPath = scope.todosPath;
12891
+ const workspace = scope.id;
11866
12892
  const checklist = await readChecklist(todosPath, workspace);
11867
12893
  let items = checklist.items;
11868
12894
  if (options.tag) {
@@ -11897,10 +12923,11 @@ ${counts.total} items: ${counts.open} open, ${counts.in_progress} active, ${coun
11897
12923
  function findItem(items, id) {
11898
12924
  return items.find((i) => i.id === id);
11899
12925
  }
11900
- todoCommand.command("start").description("Mark a todo as in-progress").argument("<id>", "Todo short ID (e.g. a3f1)").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
12926
+ todoCommand.command("start").description("Mark a todo as in-progress").argument("<id>", "Todo short ID (e.g. a3f1)").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
11901
12927
  try {
11902
- const todosPath = todosDir();
11903
- const workspace = resolveWorkspace(options);
12928
+ const scope = await resolveScope(options);
12929
+ const todosPath = scope.todosPath;
12930
+ const workspace = scope.id;
11904
12931
  const checklist = await readChecklist(todosPath, workspace);
11905
12932
  const item = findItem(checklist.items, id);
11906
12933
  if (!item) {
@@ -11920,10 +12947,11 @@ todoCommand.command("start").description("Mark a todo as in-progress").argument(
11920
12947
  process.exit(1);
11921
12948
  }
11922
12949
  });
11923
- todoCommand.command("complete").description("Mark a todo as completed").argument("<id>", "Todo short ID").option("--summary <summary>", "Completion summary").option("--branch <branch>", "Git branch name").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
12950
+ todoCommand.command("complete").description("Mark a todo as completed").argument("<id>", "Todo short ID").option("--summary <summary>", "Completion summary").option("--branch <branch>", "Git branch name").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
11924
12951
  try {
11925
- const todosPath = todosDir();
11926
- const workspace = resolveWorkspace(options);
12952
+ const scope = await resolveScope(options);
12953
+ const todosPath = scope.todosPath;
12954
+ const workspace = scope.id;
11927
12955
  const checklist = await readChecklist(todosPath, workspace);
11928
12956
  const item = findItem(checklist.items, id);
11929
12957
  if (!item) {
@@ -11950,10 +12978,11 @@ todoCommand.command("complete").description("Mark a todo as completed").argument
11950
12978
  process.exit(1);
11951
12979
  }
11952
12980
  });
11953
- todoCommand.command("block").description("Mark a todo as blocked").argument("<id>", "Todo short ID").requiredOption("--reason <reason>", "Blocking reason").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
12981
+ todoCommand.command("block").description("Mark a todo as blocked").argument("<id>", "Todo short ID").requiredOption("--reason <reason>", "Blocking reason").option("--session <session>", "Session ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
11954
12982
  try {
11955
- const todosPath = todosDir();
11956
- const workspace = resolveWorkspace(options);
12983
+ const scope = await resolveScope(options);
12984
+ const todosPath = scope.todosPath;
12985
+ const workspace = scope.id;
11957
12986
  const checklist = await readChecklist(todosPath, workspace);
11958
12987
  const item = findItem(checklist.items, id);
11959
12988
  if (!item) {
@@ -11980,10 +13009,11 @@ todoCommand.command("block").description("Mark a todo as blocked").argument("<id
11980
13009
  process.exit(1);
11981
13010
  }
11982
13011
  });
11983
- todoCommand.command("unblock").description("Return a blocked todo to open").argument("<id>", "Todo short ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
13012
+ todoCommand.command("unblock").description("Return a blocked todo to open").argument("<id>", "Todo short ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
11984
13013
  try {
11985
- const todosPath = todosDir();
11986
- const workspace = resolveWorkspace(options);
13014
+ const scope = await resolveScope(options);
13015
+ const todosPath = scope.todosPath;
13016
+ const workspace = scope.id;
11987
13017
  const checklist = await readChecklist(todosPath, workspace);
11988
13018
  const item = findItem(checklist.items, id);
11989
13019
  if (!item) {
@@ -11999,10 +13029,11 @@ todoCommand.command("unblock").description("Return a blocked todo to open").argu
11999
13029
  process.exit(1);
12000
13030
  }
12001
13031
  });
12002
- todoCommand.command("delete").description("Delete a todo item (no log entry)").argument("<id>", "Todo short ID").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
13032
+ todoCommand.command("delete").description("Delete a todo item (no log entry)").argument("<id>", "Todo short ID").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
12003
13033
  try {
12004
- const todosPath = todosDir();
12005
- const workspace = resolveWorkspace(options);
13034
+ const scope = await resolveScope(options);
13035
+ const todosPath = scope.todosPath;
13036
+ const workspace = scope.id;
12006
13037
  const checklist = await readChecklist(todosPath, workspace);
12007
13038
  const idx = checklist.items.findIndex((i) => i.id === id);
12008
13039
  if (idx === -1) {
@@ -12018,10 +13049,11 @@ todoCommand.command("delete").description("Delete a todo item (no log entry)").a
12018
13049
  process.exit(1);
12019
13050
  }
12020
13051
  });
12021
- todoCommand.command("edit").description("Update a todo description").argument("<id>", "Todo short ID").argument("<description>", "New description").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, description, options) => {
13052
+ todoCommand.command("edit").description("Update a todo description").argument("<id>", "Todo short ID").argument("<description>", "New description").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, description, options) => {
12022
13053
  try {
12023
- const todosPath = todosDir();
12024
- const workspace = resolveWorkspace(options);
13054
+ const scope = await resolveScope(options);
13055
+ const todosPath = scope.todosPath;
13056
+ const workspace = scope.id;
12025
13057
  const checklist = await readChecklist(todosPath, workspace);
12026
13058
  const item = findItem(checklist.items, id);
12027
13059
  if (!item) {
@@ -12036,10 +13068,11 @@ todoCommand.command("edit").description("Update a todo description").argument("<
12036
13068
  process.exit(1);
12037
13069
  }
12038
13070
  });
12039
- todoCommand.command("tag").description("Modify tags on a todo").argument("<id>", "Todo short ID").option("--add <tags>", "Tags to add (comma-separated)").option("--remove <tags>", "Tags to remove (comma-separated)").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
13071
+ todoCommand.command("tag").description("Modify tags on a todo").argument("<id>", "Todo short ID").option("--add <tags>", "Tags to add (comma-separated)").option("--remove <tags>", "Tags to remove (comma-separated)").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
12040
13072
  try {
12041
- const todosPath = todosDir();
12042
- const workspace = resolveWorkspace(options);
13073
+ const scope = await resolveScope(options);
13074
+ const todosPath = scope.todosPath;
13075
+ const workspace = scope.id;
12043
13076
  const checklist = await readChecklist(todosPath, workspace);
12044
13077
  const item = findItem(checklist.items, id);
12045
13078
  if (!item) {
@@ -12063,10 +13096,11 @@ todoCommand.command("tag").description("Modify tags on a todo").argument("<id>",
12063
13096
  process.exit(1);
12064
13097
  }
12065
13098
  });
12066
- todoCommand.command("log").description("Show log entries").argument("[id]", "Optional todo short ID to filter").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
13099
+ todoCommand.command("log").description("Show log entries").argument("[id]", "Optional todo short ID to filter").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
12067
13100
  try {
12068
- const todosPath = todosDir();
12069
- const workspace = resolveWorkspace(options);
13101
+ const scope = await resolveScope(options);
13102
+ const todosPath = scope.todosPath;
13103
+ const workspace = scope.id;
12070
13104
  const log = await readLog(todosPath, workspace);
12071
13105
  let entries = log.entries;
12072
13106
  if (id) {
@@ -12090,10 +13124,11 @@ ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}`);
12090
13124
  process.exit(1);
12091
13125
  }
12092
13126
  });
12093
- todoCommand.command("archive").description("Archive completed todos and their log entries").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (options) => {
13127
+ todoCommand.command("archive").description("Archive completed todos and their log entries").option("--workspace <slug>", "Workspace slug").option("--project <slug>", "Project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (options) => {
12094
13128
  try {
12095
- const todosPath = todosDir();
12096
- const workspace = resolveWorkspace(options);
13129
+ const scope = await resolveScope(options);
13130
+ const todosPath = scope.todosPath;
13131
+ const workspace = scope.id;
12097
13132
  const checklist = await readChecklist(todosPath, workspace);
12098
13133
  const log = await readLog(todosPath, workspace);
12099
13134
  const completedIds = new Set(
@@ -12107,10 +13142,10 @@ todoCommand.command("archive").description("Archive completed todos and their lo
12107
13142
  (e) => e.itemIds.every((id) => completedIds.has(id))
12108
13143
  );
12109
13144
  const archFile = archivePath(todosPath, workspace, checklist.archiveInterval);
12110
- await ensureDir(resolve34(todosPath, "archive"));
13145
+ await ensureDir(resolve35(todosPath, "archive"));
12111
13146
  let archContent = "";
12112
13147
  if (await fileExists(archFile)) {
12113
- archContent = await readFile20(archFile, "utf-8");
13148
+ archContent = await readFile21(archFile, "utf-8");
12114
13149
  archContent = archContent.trimEnd() + "\n\n";
12115
13150
  } else {
12116
13151
  archContent = `---
@@ -12185,10 +13220,11 @@ workspace: ${workspace}
12185
13220
  process.exit(1);
12186
13221
  }
12187
13222
  });
12188
- todoCommand.command("promote").description("Promote a todo to a full assignment").argument("<id>", "Todo short ID").requiredOption("--project <slug>", "Target project slug").option("--workspace <slug>", "Workspace slug").option("--global", "Use global todos").action(async (id, options) => {
13223
+ todoCommand.command("promote").description("Promote a todo to a full assignment").argument("<id>", "Todo short ID").requiredOption("--to-project <slug>", "Target project slug for the new assignment").option("--workspace <slug>", "Source workspace slug").option("--project <slug>", "Source project slug (mutually exclusive with --workspace/--global)").option("--global", "Use global todos").action(async (id, options) => {
12189
13224
  try {
12190
- const todosPath = todosDir();
12191
- const workspace = resolveWorkspace(options);
13225
+ const scope = await resolveScope(options);
13226
+ const todosPath = scope.todosPath;
13227
+ const workspace = scope.id;
12192
13228
  const checklist = await readChecklist(todosPath, workspace);
12193
13229
  const item = findItem(checklist.items, id);
12194
13230
  if (!item) {
@@ -12204,13 +13240,13 @@ todoCommand.command("promote").description("Promote a todo to a full assignment"
12204
13240
  items: item.description,
12205
13241
  session: null,
12206
13242
  branch: null,
12207
- summary: `Promoted to assignment in project: ${options.project}`,
13243
+ summary: `Promoted to assignment in project: ${options.toProject}`,
12208
13244
  blockers: null,
12209
13245
  status: null
12210
13246
  };
12211
13247
  await appendLogEntry2(todosPath, workspace, entry);
12212
- console.log(`Promoted [t:${id}] to assignment in project "${options.project}".`);
12213
- console.log(`Run: syntaur create-assignment --project ${options.project} "${item.description}"`);
13248
+ console.log(`Promoted [t:${id}] to assignment in project "${options.toProject}".`);
13249
+ console.log(`Run: syntaur create-assignment --project ${options.toProject} "${item.description}"`);
12214
13250
  } catch (error) {
12215
13251
  console.error("Error:", error instanceof Error ? error.message : String(error));
12216
13252
  process.exit(1);
@@ -12299,7 +13335,7 @@ import { Command as Command3 } from "commander";
12299
13335
 
12300
13336
  // src/utils/doctor/index.ts
12301
13337
  import { fileURLToPath as fileURLToPath7 } from "url";
12302
- import { readFile as readFile24 } from "fs/promises";
13338
+ import { readFile as readFile25 } from "fs/promises";
12303
13339
  import { dirname as dirname11, join as join5 } from "path";
12304
13340
 
12305
13341
  // src/utils/doctor/context.ts
@@ -12307,11 +13343,11 @@ init_config2();
12307
13343
  init_paths();
12308
13344
  init_fs();
12309
13345
  import Database2 from "better-sqlite3";
12310
- import { resolve as resolve35 } from "path";
13346
+ import { resolve as resolve36 } from "path";
12311
13347
  async function buildCheckContext(cwd = process.cwd()) {
12312
13348
  const config = await readConfig();
12313
13349
  const root = syntaurRoot();
12314
- const dbPath = resolve35(root, "syntaur.db");
13350
+ const dbPath = resolve36(root, "syntaur.db");
12315
13351
  let db2 = null;
12316
13352
  let dbError = null;
12317
13353
  if (await fileExists(dbPath)) {
@@ -12345,8 +13381,8 @@ function closeCheckContext(ctx) {
12345
13381
  // src/utils/doctor/checks/env.ts
12346
13382
  init_fs();
12347
13383
  init_paths();
12348
- import { resolve as resolve36, isAbsolute as isAbsolute3 } from "path";
12349
- import { readFile as readFile21, stat as stat4 } from "fs/promises";
13384
+ import { resolve as resolve37, isAbsolute as isAbsolute3 } from "path";
13385
+ import { readFile as readFile22, stat as stat4 } from "fs/promises";
12350
13386
  import { fileURLToPath as fileURLToPath6 } from "url";
12351
13387
  import { dirname as dirname10, join as join4 } from "path";
12352
13388
  var CATEGORY = "env";
@@ -12386,7 +13422,7 @@ var configValid = {
12386
13422
  category: CATEGORY,
12387
13423
  title: "~/.syntaur/config.md is valid",
12388
13424
  async run(ctx) {
12389
- const configPath = resolve36(ctx.syntaurRoot, "config.md");
13425
+ const configPath = resolve37(ctx.syntaurRoot, "config.md");
12390
13426
  if (!await fileExists(configPath)) {
12391
13427
  return {
12392
13428
  id: this.id,
@@ -12403,7 +13439,7 @@ var configValid = {
12403
13439
  autoFixable: false
12404
13440
  };
12405
13441
  }
12406
- const content = await readFile21(configPath, "utf-8");
13442
+ const content = await readFile22(configPath, "utf-8");
12407
13443
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
12408
13444
  if (!fmMatch || fmMatch[1].trim() === "") {
12409
13445
  return {
@@ -12683,7 +13719,7 @@ async function readLocalPkg() {
12683
13719
  for (let i = 0; i < 6; i++) {
12684
13720
  const candidate = join4(dir, "package.json");
12685
13721
  try {
12686
- const text = await readFile21(candidate, "utf-8");
13722
+ const text = await readFile22(candidate, "utf-8");
12687
13723
  return JSON.parse(text);
12688
13724
  } catch {
12689
13725
  dir = dirname10(dir);
@@ -12735,7 +13771,7 @@ function versionGte(a, b) {
12735
13771
 
12736
13772
  // src/utils/doctor/checks/structure.ts
12737
13773
  init_fs();
12738
- import { resolve as resolve37 } from "path";
13774
+ import { resolve as resolve38 } from "path";
12739
13775
  import { readdir as readdir13, stat as stat5 } from "fs/promises";
12740
13776
  var CATEGORY2 = "structure";
12741
13777
  var KNOWN_TOP_LEVEL = /* @__PURE__ */ new Set([
@@ -12755,7 +13791,7 @@ var projectsDir = {
12755
13791
  category: CATEGORY2,
12756
13792
  title: "projects/ directory exists",
12757
13793
  async run(ctx) {
12758
- const p = resolve37(ctx.syntaurRoot, "projects");
13794
+ const p = resolve38(ctx.syntaurRoot, "projects");
12759
13795
  if (!await fileExists(p)) {
12760
13796
  return {
12761
13797
  id: this.id,
@@ -12780,7 +13816,7 @@ var playbooksDir2 = {
12780
13816
  category: CATEGORY2,
12781
13817
  title: "playbooks/ directory exists",
12782
13818
  async run(ctx) {
12783
- const p = resolve37(ctx.syntaurRoot, "playbooks");
13819
+ const p = resolve38(ctx.syntaurRoot, "playbooks");
12784
13820
  if (!await fileExists(p)) {
12785
13821
  return {
12786
13822
  id: this.id,
@@ -12805,7 +13841,7 @@ var todosDirValid = {
12805
13841
  category: CATEGORY2,
12806
13842
  title: "todos/ directory is readable (if present)",
12807
13843
  async run(ctx) {
12808
- const p = resolve37(ctx.syntaurRoot, "todos");
13844
+ const p = resolve38(ctx.syntaurRoot, "todos");
12809
13845
  if (!await fileExists(p)) {
12810
13846
  return {
12811
13847
  id: this.id,
@@ -12836,7 +13872,7 @@ var serversDirValid = {
12836
13872
  category: CATEGORY2,
12837
13873
  title: "servers/ directory is readable (if present)",
12838
13874
  async run(ctx) {
12839
- const p = resolve37(ctx.syntaurRoot, "servers");
13875
+ const p = resolve38(ctx.syntaurRoot, "servers");
12840
13876
  if (!await fileExists(p)) {
12841
13877
  return {
12842
13878
  id: this.id,
@@ -12881,7 +13917,7 @@ var knownFilesRecognized = {
12881
13917
  title: this.title,
12882
13918
  status: "warn",
12883
13919
  detail: `unexpected top-level entries: ${unexpected.join(", ")}`,
12884
- affected: unexpected.map((n) => resolve37(ctx.syntaurRoot, n)),
13920
+ affected: unexpected.map((n) => resolve38(ctx.syntaurRoot, n)),
12885
13921
  remediation: {
12886
13922
  kind: "manual",
12887
13923
  suggestion: "Review these entries \u2014 they may be leftover state from older versions",
@@ -12910,7 +13946,7 @@ function pass2(check) {
12910
13946
 
12911
13947
  // src/utils/doctor/checks/project.ts
12912
13948
  init_fs();
12913
- import { resolve as resolve38 } from "path";
13949
+ import { resolve as resolve39 } from "path";
12914
13950
  import { readdir as readdir14, stat as stat6 } from "fs/promises";
12915
13951
  var CATEGORY3 = "project";
12916
13952
  var REQUIRED_PROJECT_FILES = [
@@ -12940,10 +13976,10 @@ async function listProjects2(ctx) {
12940
13976
  for (const e of entries) {
12941
13977
  if (!e.isDirectory()) continue;
12942
13978
  if (e.name.startsWith(".") || e.name.startsWith("_")) continue;
12943
- const projectDir = resolve38(dir, e.name);
13979
+ const projectDir = resolve39(dir, e.name);
12944
13980
  let looksLikeProject = false;
12945
13981
  for (const marker of PROJECT_MARKERS) {
12946
- if (await fileExists(resolve38(projectDir, marker))) {
13982
+ if (await fileExists(resolve39(projectDir, marker))) {
12947
13983
  looksLikeProject = true;
12948
13984
  break;
12949
13985
  }
@@ -12962,7 +13998,7 @@ var requiredFiles = {
12962
13998
  for (const projectDir of projects) {
12963
13999
  const missing = [];
12964
14000
  for (const rel of REQUIRED_PROJECT_FILES) {
12965
- const p = resolve38(projectDir, rel);
14001
+ const p = resolve39(projectDir, rel);
12966
14002
  if (!await fileExists(p)) missing.push(rel);
12967
14003
  }
12968
14004
  if (missing.length === 0) continue;
@@ -12972,7 +14008,7 @@ var requiredFiles = {
12972
14008
  title: this.title,
12973
14009
  status: "error",
12974
14010
  detail: `project at ${projectDir} is missing: ${missing.join(", ")}`,
12975
- affected: missing.map((m) => resolve38(projectDir, m)),
14011
+ affected: missing.map((m) => resolve39(projectDir, m)),
12976
14012
  remediation: {
12977
14013
  kind: "manual",
12978
14014
  suggestion: "Recreate the missing scaffold files from templates",
@@ -12995,7 +14031,7 @@ var manifestStale = {
12995
14031
  const projects = await listProjects2(ctx);
12996
14032
  const results = [];
12997
14033
  for (const projectDir of projects) {
12998
- const manifestPath = resolve38(projectDir, "manifest.md");
14034
+ const manifestPath = resolve39(projectDir, "manifest.md");
12999
14035
  if (!await fileExists(manifestPath)) continue;
13000
14036
  const manifestMtime = (await stat6(manifestPath)).mtimeMs;
13001
14037
  const newestAssignment = await newestAssignmentMtime(projectDir);
@@ -13044,7 +14080,7 @@ var orphanFiles = {
13044
14080
  title: this.title,
13045
14081
  status: "warn",
13046
14082
  detail: `project at ${projectDir} has unexpected entries: ${orphans.join(", ")}`,
13047
- affected: orphans.map((o) => resolve38(projectDir, o)),
14083
+ affected: orphans.map((o) => resolve39(projectDir, o)),
13048
14084
  autoFixable: false
13049
14085
  });
13050
14086
  }
@@ -13054,7 +14090,7 @@ var orphanFiles = {
13054
14090
  };
13055
14091
  var projectChecks = [requiredFiles, manifestStale, orphanFiles];
13056
14092
  async function newestAssignmentMtime(projectDir) {
13057
- const assignmentsRoot = resolve38(projectDir, "assignments");
14093
+ const assignmentsRoot = resolve39(projectDir, "assignments");
13058
14094
  if (!await fileExists(assignmentsRoot)) return 0;
13059
14095
  let newest = 0;
13060
14096
  let entries;
@@ -13065,7 +14101,7 @@ async function newestAssignmentMtime(projectDir) {
13065
14101
  }
13066
14102
  for (const e of entries) {
13067
14103
  if (!e.isDirectory()) continue;
13068
- const assignmentMd = resolve38(assignmentsRoot, e.name, "assignment.md");
14104
+ const assignmentMd = resolve39(assignmentsRoot, e.name, "assignment.md");
13069
14105
  try {
13070
14106
  const s = await stat6(assignmentMd);
13071
14107
  if (s.mtimeMs > newest) newest = s.mtimeMs;
@@ -13089,8 +14125,8 @@ init_fs();
13089
14125
  init_parser();
13090
14126
  init_types();
13091
14127
  init_paths();
13092
- import { resolve as resolve39 } from "path";
13093
- import { readdir as readdir15, readFile as readFile22 } from "fs/promises";
14128
+ import { resolve as resolve40 } from "path";
14129
+ import { readdir as readdir15, readFile as readFile23 } from "fs/promises";
13094
14130
  var CATEGORY4 = "assignment";
13095
14131
  var STATUSES_REQUIRING_HANDOFF = /* @__PURE__ */ new Set(["review", "completed"]);
13096
14132
  async function listAssignments(ctx) {
@@ -13101,16 +14137,16 @@ async function listAssignments(ctx) {
13101
14137
  for (const m of projects) {
13102
14138
  if (!m.isDirectory()) continue;
13103
14139
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
13104
- const assignmentsDir2 = resolve39(projectsDir2, m.name, "assignments");
14140
+ const assignmentsDir2 = resolve40(projectsDir2, m.name, "assignments");
13105
14141
  if (!await fileExists(assignmentsDir2)) continue;
13106
14142
  const entries = await readdir15(assignmentsDir2, { withFileTypes: true });
13107
14143
  for (const a of entries) {
13108
14144
  if (!a.isDirectory()) continue;
13109
14145
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
13110
- const assignmentDir = resolve39(assignmentsDir2, a.name);
13111
- const assignmentMd = resolve39(assignmentDir, "assignment.md");
14146
+ const assignmentDir = resolve40(assignmentsDir2, a.name);
14147
+ const assignmentMd = resolve40(assignmentDir, "assignment.md");
13112
14148
  const entry = {
13113
- projectDir: resolve39(projectsDir2, m.name),
14149
+ projectDir: resolve40(projectsDir2, m.name),
13114
14150
  projectSlug: m.name,
13115
14151
  assignmentDir,
13116
14152
  assignmentSlug: a.name,
@@ -13130,8 +14166,8 @@ async function listAssignments(ctx) {
13130
14166
  for (const a of entries) {
13131
14167
  if (!a.isDirectory()) continue;
13132
14168
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
13133
- const assignmentDir = resolve39(standaloneRoot, a.name);
13134
- const assignmentMd = resolve39(assignmentDir, "assignment.md");
14169
+ const assignmentDir = resolve40(standaloneRoot, a.name);
14170
+ const assignmentMd = resolve40(assignmentDir, "assignment.md");
13135
14171
  const entry = {
13136
14172
  projectDir: standaloneRoot,
13137
14173
  projectSlug: null,
@@ -13209,7 +14245,7 @@ var invalidStatus = {
13209
14245
  const allowed = configuredStatuses(ctx);
13210
14246
  const results = [];
13211
14247
  for (const a of withAssignmentMd) {
13212
- const path = resolve39(a.assignmentDir, "assignment.md");
14248
+ const path = resolve40(a.assignmentDir, "assignment.md");
13213
14249
  const parsed = await parseSafe(path);
13214
14250
  if (!parsed) continue;
13215
14251
  if (!allowed.has(parsed.status)) {
@@ -13242,7 +14278,7 @@ var workspaceMissing = {
13242
14278
  const terminal = terminalStatuses(ctx);
13243
14279
  const results = [];
13244
14280
  for (const a of withAssignmentMd) {
13245
- const path = resolve39(a.assignmentDir, "assignment.md");
14281
+ const path = resolve40(a.assignmentDir, "assignment.md");
13246
14282
  const parsed = await parseSafe(path);
13247
14283
  if (!parsed) continue;
13248
14284
  if (terminal.has(parsed.status)) continue;
@@ -13289,12 +14325,12 @@ var requiredFilesByStatus = {
13289
14325
  const { withAssignmentMd } = await listAssignments(ctx);
13290
14326
  const results = [];
13291
14327
  for (const a of withAssignmentMd) {
13292
- const assignmentPath = resolve39(a.assignmentDir, "assignment.md");
14328
+ const assignmentPath = resolve40(a.assignmentDir, "assignment.md");
13293
14329
  const parsed = await parseSafe(assignmentPath);
13294
14330
  if (!parsed) continue;
13295
14331
  const missing = [];
13296
14332
  if (STATUSES_REQUIRING_HANDOFF.has(parsed.status)) {
13297
- const handoffPath = resolve39(a.assignmentDir, "handoff.md");
14333
+ const handoffPath = resolve40(a.assignmentDir, "handoff.md");
13298
14334
  if (!await fileExists(handoffPath)) missing.push("handoff.md");
13299
14335
  }
13300
14336
  if (missing.length === 0) continue;
@@ -13304,7 +14340,7 @@ var requiredFilesByStatus = {
13304
14340
  title: this.title,
13305
14341
  status: "warn",
13306
14342
  detail: `${a.projectSlug}/${a.assignmentSlug} (status: ${parsed.status}) is missing ${missing.join(", ")}`,
13307
- affected: missing.map((m) => resolve39(a.assignmentDir, m)),
14343
+ affected: missing.map((m) => resolve40(a.assignmentDir, m)),
13308
14344
  remediation: {
13309
14345
  kind: "manual",
13310
14346
  suggestion: `Create the missing ${missing.join(" and ")} files for this assignment`,
@@ -13327,7 +14363,7 @@ var companionFilesScaffolded = {
13327
14363
  for (const a of withAssignmentMd) {
13328
14364
  const missing = [];
13329
14365
  for (const filename of ["progress.md", "comments.md"]) {
13330
- if (!await fileExists(resolve39(a.assignmentDir, filename))) {
14366
+ if (!await fileExists(resolve40(a.assignmentDir, filename))) {
13331
14367
  missing.push(filename);
13332
14368
  }
13333
14369
  }
@@ -13339,7 +14375,7 @@ var companionFilesScaffolded = {
13339
14375
  title: this.title,
13340
14376
  status: "warn",
13341
14377
  detail: `${label} is missing ${missing.join(" and ")} (pre-v2.0 assignment \u2014 not required, but scaffolding them keeps the dashboard and CLIs consistent)`,
13342
- affected: missing.map((m) => resolve39(a.assignmentDir, m)),
14378
+ affected: missing.map((m) => resolve40(a.assignmentDir, m)),
13343
14379
  remediation: {
13344
14380
  kind: "manual",
13345
14381
  suggestion: `Create ${missing.join(" and ")} with the renderProgress/renderComments templates, or re-scaffold via the CLI`,
@@ -13372,7 +14408,7 @@ var typeDefinition = {
13372
14408
  const { withAssignmentMd } = await listAssignments(ctx);
13373
14409
  const results = [];
13374
14410
  for (const a of withAssignmentMd) {
13375
- const path = resolve39(a.assignmentDir, "assignment.md");
14411
+ const path = resolve40(a.assignmentDir, "assignment.md");
13376
14412
  const parsed = await parseSafe(path);
13377
14413
  if (!parsed) continue;
13378
14414
  if (!parsed.type) continue;
@@ -13406,7 +14442,7 @@ var projectFrontmatterMatchesContainer = {
13406
14442
  const { withAssignmentMd } = await listAssignments(ctx);
13407
14443
  const results = [];
13408
14444
  for (const a of withAssignmentMd) {
13409
- const path = resolve39(a.assignmentDir, "assignment.md");
14445
+ const path = resolve40(a.assignmentDir, "assignment.md");
13410
14446
  const parsed = await parseSafe(path);
13411
14447
  if (!parsed) continue;
13412
14448
  if (a.standalone) {
@@ -13461,7 +14497,7 @@ var assignmentChecks = [
13461
14497
  ];
13462
14498
  async function parseSafe(path) {
13463
14499
  try {
13464
- const content = await readFile22(path, "utf-8");
14500
+ const content = await readFile23(path, "utf-8");
13465
14501
  return parseAssignmentFull(content);
13466
14502
  } catch {
13467
14503
  return null;
@@ -13480,7 +14516,7 @@ function pass4(check, detail) {
13480
14516
 
13481
14517
  // src/utils/doctor/checks/dashboard.ts
13482
14518
  init_fs();
13483
- import { resolve as resolve40 } from "path";
14519
+ import { resolve as resolve41 } from "path";
13484
14520
  var CATEGORY5 = "dashboard";
13485
14521
  var dbReachable = {
13486
14522
  id: "dashboard.db-reachable",
@@ -13494,7 +14530,7 @@ var dbReachable = {
13494
14530
  title: this.title,
13495
14531
  status: "error",
13496
14532
  detail: `could not open syntaur.db: ${ctx.dbError ?? "unknown error"}`,
13497
- affected: [resolve40(ctx.syntaurRoot, "syntaur.db")],
14533
+ affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
13498
14534
  remediation: {
13499
14535
  kind: "manual",
13500
14536
  suggestion: "Start the dashboard once (`syntaur dashboard`) to initialize the DB, or restore it from backup",
@@ -13512,7 +14548,7 @@ var dbReachable = {
13512
14548
  title: this.title,
13513
14549
  status: "error",
13514
14550
  detail: 'syntaur.db is missing the expected "sessions" table',
13515
- affected: [resolve40(ctx.syntaurRoot, "syntaur.db")],
14551
+ affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
13516
14552
  autoFixable: false
13517
14553
  };
13518
14554
  }
@@ -13524,7 +14560,7 @@ var dbReachable = {
13524
14560
  title: this.title,
13525
14561
  status: "error",
13526
14562
  detail: `syntaur.db query failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
13527
- affected: [resolve40(ctx.syntaurRoot, "syntaur.db")],
14563
+ affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
13528
14564
  autoFixable: false
13529
14565
  };
13530
14566
  }
@@ -13550,7 +14586,7 @@ var ghostSessions = {
13550
14586
  const results = [];
13551
14587
  for (const row of rows) {
13552
14588
  if (!row.project_slug) continue;
13553
- const projectPath = resolve40(projectsDir2, row.project_slug, "project.md");
14589
+ const projectPath = resolve41(projectsDir2, row.project_slug, "project.md");
13554
14590
  if (!await fileExists(projectPath)) {
13555
14591
  results.push({
13556
14592
  id: this.id,
@@ -13569,7 +14605,7 @@ var ghostSessions = {
13569
14605
  continue;
13570
14606
  }
13571
14607
  if (row.assignment_slug) {
13572
- const assignmentPath = resolve40(
14608
+ const assignmentPath = resolve41(
13573
14609
  projectsDir2,
13574
14610
  row.project_slug,
13575
14611
  "assignments",
@@ -13726,8 +14762,8 @@ function skipped2(check, reason) {
13726
14762
  init_fs();
13727
14763
  init_parser();
13728
14764
  init_types();
13729
- import { resolve as resolve41 } from "path";
13730
- import { readFile as readFile23 } from "fs/promises";
14765
+ import { resolve as resolve42 } from "path";
14766
+ import { readFile as readFile24 } from "fs/promises";
13731
14767
  var CATEGORY7 = "workspace";
13732
14768
  var ASSIGNMENT_FIELDS = ["projectSlug", "assignmentSlug", "projectDir", "assignmentDir"];
13733
14769
  function hasAnyAssignmentField(ctx) {
@@ -13739,12 +14775,12 @@ function isStandaloneSession(ctx) {
13739
14775
  return !hasAnyAssignmentField(ctx) && typeof ctx.sessionId === "string" && ctx.sessionId.length > 0;
13740
14776
  }
13741
14777
  async function loadContext(ctx) {
13742
- const path = resolve41(ctx.cwd, ".syntaur", "context.json");
14778
+ const path = resolve42(ctx.cwd, ".syntaur", "context.json");
13743
14779
  if (!await fileExists(path)) {
13744
14780
  return { data: null, path, exists: false, parseError: null };
13745
14781
  }
13746
14782
  try {
13747
- const raw = await readFile23(path, "utf-8");
14783
+ const raw = await readFile24(path, "utf-8");
13748
14784
  return { data: JSON.parse(raw), path, exists: true, parseError: null };
13749
14785
  } catch (err2) {
13750
14786
  return {
@@ -13819,7 +14855,7 @@ var contextAssignmentResolves = {
13819
14855
  if (!exists) return skipped3(this, "no context to resolve");
13820
14856
  if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to resolve");
13821
14857
  if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
13822
- const assignmentMd = resolve41(data.assignmentDir, "assignment.md");
14858
+ const assignmentMd = resolve42(data.assignmentDir, "assignment.md");
13823
14859
  if (!await fileExists(assignmentMd)) {
13824
14860
  return {
13825
14861
  id: this.id,
@@ -13848,10 +14884,10 @@ var contextTerminal = {
13848
14884
  if (!exists) return skipped3(this, "no context to check");
13849
14885
  if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to check");
13850
14886
  if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
13851
- const assignmentMd = resolve41(data.assignmentDir, "assignment.md");
14887
+ const assignmentMd = resolve42(data.assignmentDir, "assignment.md");
13852
14888
  if (!await fileExists(assignmentMd)) return skipped3(this, "assignment file missing");
13853
14889
  try {
13854
- const content = await readFile23(assignmentMd, "utf-8");
14890
+ const content = await readFile24(assignmentMd, "utf-8");
13855
14891
  const parsed = parseAssignmentFull(content);
13856
14892
  const terminal = terminalStatuses2(ctx);
13857
14893
  if (terminal.has(parsed.status)) {
@@ -13998,7 +15034,7 @@ async function readVersion() {
13998
15034
  let dir = dirname11(here);
13999
15035
  for (let i = 0; i < 6; i++) {
14000
15036
  try {
14001
- const raw = await readFile24(join5(dir, "package.json"), "utf-8");
15037
+ const raw = await readFile25(join5(dir, "package.json"), "utf-8");
14002
15038
  const parsed = JSON.parse(raw);
14003
15039
  return typeof parsed.version === "string" ? parsed.version : null;
14004
15040
  } catch {
@@ -14135,8 +15171,8 @@ var doctorCommand = new Command3("doctor").description("Diagnose Syntaur state a
14135
15171
  init_paths();
14136
15172
  init_fs();
14137
15173
  init_config2();
14138
- import { resolve as resolve42 } from "path";
14139
- import { readFile as readFile25 } from "fs/promises";
15174
+ import { resolve as resolve43 } from "path";
15175
+ import { readFile as readFile26 } from "fs/promises";
14140
15176
  init_timestamp();
14141
15177
  init_assignment_resolver();
14142
15178
  function shortId() {
@@ -14168,7 +15204,7 @@ async function commentCommand(target, text, options = {}) {
14168
15204
  if (!isValidSlug(target)) {
14169
15205
  throw new Error(`Invalid assignment slug "${target}".`);
14170
15206
  }
14171
- assignmentDir = resolve42(baseDir, options.project, "assignments", target);
15207
+ assignmentDir = resolve43(baseDir, options.project, "assignments", target);
14172
15208
  assignmentRef = target;
14173
15209
  } else {
14174
15210
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -14178,13 +15214,13 @@ async function commentCommand(target, text, options = {}) {
14178
15214
  assignmentDir = resolved.assignmentDir;
14179
15215
  assignmentRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
14180
15216
  }
14181
- const commentsPath = resolve42(assignmentDir, "comments.md");
15217
+ const commentsPath = resolve43(assignmentDir, "comments.md");
14182
15218
  const timestamp = nowTimestamp();
14183
15219
  const author = options.author ?? process.env.USER ?? "unknown";
14184
15220
  let currentContent;
14185
15221
  let currentCount = 0;
14186
15222
  if (await fileExists(commentsPath)) {
14187
- currentContent = await readFile25(commentsPath, "utf-8");
15223
+ currentContent = await readFile26(commentsPath, "utf-8");
14188
15224
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
14189
15225
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
14190
15226
  } else {
@@ -14221,8 +15257,8 @@ ${entry}`;
14221
15257
  init_paths();
14222
15258
  init_fs();
14223
15259
  init_config2();
14224
- import { resolve as resolve43 } from "path";
14225
- import { readFile as readFile26 } from "fs/promises";
15260
+ import { resolve as resolve44 } from "path";
15261
+ import { readFile as readFile27 } from "fs/promises";
14226
15262
  init_timestamp();
14227
15263
  init_assignment_resolver();
14228
15264
  function setTopLevelField3(content, key, value) {
@@ -14247,7 +15283,7 @@ async function requestCommand(target, text, options = {}) {
14247
15283
  if (!isValidSlug(target)) {
14248
15284
  throw new Error(`Invalid assignment slug "${target}".`);
14249
15285
  }
14250
- assignmentDir = resolve43(baseDir, options.project, "assignments", target);
15286
+ assignmentDir = resolve44(baseDir, options.project, "assignments", target);
14251
15287
  targetRef = target;
14252
15288
  } else {
14253
15289
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -14257,12 +15293,12 @@ async function requestCommand(target, text, options = {}) {
14257
15293
  assignmentDir = resolved.assignmentDir;
14258
15294
  targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
14259
15295
  }
14260
- const assignmentMdPath = resolve43(assignmentDir, "assignment.md");
15296
+ const assignmentMdPath = resolve44(assignmentDir, "assignment.md");
14261
15297
  if (!await fileExists(assignmentMdPath)) {
14262
15298
  throw new Error(`assignment.md not found at ${assignmentMdPath}`);
14263
15299
  }
14264
15300
  const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
14265
- let content = await readFile26(assignmentMdPath, "utf-8");
15301
+ let content = await readFile27(assignmentMdPath, "utf-8");
14266
15302
  const todoLine = `- [ ] ${text.trim()} (from: ${source})`;
14267
15303
  const todosHeading = /^## Todos\s*$/m;
14268
15304
  if (todosHeading.test(content)) {
@@ -14330,20 +15366,20 @@ async function getDefaultCommandName() {
14330
15366
  init_paths();
14331
15367
  init_fs();
14332
15368
  import { fileURLToPath as fileURLToPath9 } from "url";
14333
- import { readFile as readFile28 } from "fs/promises";
14334
- import { dirname as dirname13, join as join7, resolve as resolve44 } from "path";
15369
+ import { readFile as readFile29 } from "fs/promises";
15370
+ import { dirname as dirname13, join as join7, resolve as resolve45 } from "path";
14335
15371
  import { spawn as spawn3 } from "child_process";
14336
15372
  import { createInterface as createInterface2 } from "readline/promises";
14337
15373
 
14338
15374
  // src/utils/version.ts
14339
15375
  import { fileURLToPath as fileURLToPath8 } from "url";
14340
- import { readFile as readFile27 } from "fs/promises";
15376
+ import { readFile as readFile28 } from "fs/promises";
14341
15377
  import { dirname as dirname12, join as join6 } from "path";
14342
15378
  async function readPackageVersion(scriptUrl) {
14343
15379
  try {
14344
15380
  const scriptPath = fileURLToPath8(scriptUrl);
14345
15381
  const pkgRoot = dirname12(dirname12(scriptPath));
14346
- const raw = await readFile27(join6(pkgRoot, "package.json"), "utf-8");
15382
+ const raw = await readFile28(join6(pkgRoot, "package.json"), "utf-8");
14347
15383
  const parsed = JSON.parse(raw);
14348
15384
  return typeof parsed.version === "string" ? parsed.version : null;
14349
15385
  } catch {
@@ -14352,7 +15388,7 @@ async function readPackageVersion(scriptUrl) {
14352
15388
  }
14353
15389
 
14354
15390
  // src/utils/npx-prompt.ts
14355
- var STATE_FILE = resolve44(syntaurRoot(), "npx-install.json");
15391
+ var STATE_FILE = resolve45(syntaurRoot(), "npx-install.json");
14356
15392
  var META_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version", "help"]);
14357
15393
  var GLOBAL_VERSION_TIMEOUT_MS = 2e3;
14358
15394
  function isRunningViaNpx(scriptUrl) {
@@ -14373,7 +15409,7 @@ function isRunningViaNpx(scriptUrl) {
14373
15409
  async function readState() {
14374
15410
  if (!await fileExists(STATE_FILE)) return null;
14375
15411
  try {
14376
- const raw = await readFile28(STATE_FILE, "utf-8");
15412
+ const raw = await readFile29(STATE_FILE, "utf-8");
14377
15413
  return JSON.parse(raw);
14378
15414
  } catch {
14379
15415
  return null;
@@ -14432,7 +15468,7 @@ async function readGlobalVersion() {
14432
15468
  try {
14433
15469
  const manifestPath = join7(rootPath, "syntaur", "package.json");
14434
15470
  if (!await fileExists(manifestPath)) return null;
14435
- const raw = await readFile28(manifestPath, "utf-8");
15471
+ const raw = await readFile29(manifestPath, "utf-8");
14436
15472
  const parsed = JSON.parse(raw);
14437
15473
  return typeof parsed.version === "string" ? parsed.version : null;
14438
15474
  } catch {
@@ -14580,7 +15616,7 @@ program.command("create-assignment").description("Create a new assignment within
14580
15616
  "--priority <level>",
14581
15617
  "Priority level (low|medium|high|critical)",
14582
15618
  "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) => {
15619
+ ).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
15620
  try {
14585
15621
  await createAssignmentCommand(title, options);
14586
15622
  } catch (error) {
@@ -14864,9 +15900,31 @@ program.command("create-playbook").description("Create a new playbook").argument
14864
15900
  process.exit(1);
14865
15901
  }
14866
15902
  });
14867
- program.command("list-playbooks").description("List all playbooks").action(async () => {
15903
+ program.command("list-playbooks").description("List playbooks (disabled playbooks are excluded unless --all is passed)").option("--all", "Include disabled playbooks").action(async (options) => {
15904
+ try {
15905
+ await listPlaybooksCommand({ all: Boolean(options?.all) });
15906
+ } catch (error) {
15907
+ console.error(
15908
+ "Error:",
15909
+ error instanceof Error ? error.message : String(error)
15910
+ );
15911
+ process.exit(1);
15912
+ }
15913
+ });
15914
+ program.command("enable-playbook").description("Enable a previously-disabled playbook").argument("<slug>", "Playbook slug").action(async (slug) => {
15915
+ try {
15916
+ await enablePlaybookCommand(slug);
15917
+ } catch (error) {
15918
+ console.error(
15919
+ "Error:",
15920
+ error instanceof Error ? error.message : String(error)
15921
+ );
15922
+ process.exit(1);
15923
+ }
15924
+ });
15925
+ program.command("disable-playbook").description("Disable a playbook so agents no longer load it").argument("<slug>", "Playbook slug").action(async (slug) => {
14868
15926
  try {
14869
- await listPlaybooksCommand();
15927
+ await disablePlaybookCommand(slug);
14870
15928
  } catch (error) {
14871
15929
  console.error(
14872
15930
  "Error:",