syntaur 0.3.3 → 0.4.1

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 (33) hide show
  1. package/README.md +236 -49
  2. package/dist/index.js +886 -162
  3. package/dist/index.js.map +1 -1
  4. package/package.json +9 -2
  5. package/platforms/claude-code/.claude-plugin/plugin.json +1 -5
  6. package/platforms/claude-code/agents/syntaur-expert.md +30 -18
  7. package/platforms/claude-code/commands/complete-assignment/complete-assignment.md +20 -0
  8. package/platforms/claude-code/commands/create-assignment/create-assignment.md +20 -0
  9. package/platforms/claude-code/commands/create-project/create-project.md +20 -0
  10. package/platforms/claude-code/commands/grab-assignment/grab-assignment.md +20 -0
  11. package/platforms/claude-code/commands/plan-assignment/plan-assignment.md +20 -0
  12. package/statusline/statusline.sh +224 -0
  13. package/vendor/syntaur-skills/LICENSE +21 -0
  14. package/vendor/syntaur-skills/README.md +43 -0
  15. package/vendor/syntaur-skills/skills/complete-assignment/SKILL.md +146 -0
  16. package/vendor/syntaur-skills/skills/create-assignment/SKILL.md +72 -0
  17. package/vendor/syntaur-skills/skills/create-project/SKILL.md +56 -0
  18. package/vendor/syntaur-skills/skills/grab-assignment/SKILL.md +158 -0
  19. package/vendor/syntaur-skills/skills/plan-assignment/SKILL.md +137 -0
  20. package/vendor/syntaur-skills/skills/syntaur-protocol/SKILL.md +119 -0
  21. package/vendor/syntaur-skills/skills/syntaur-protocol/references/file-ownership.md +67 -0
  22. package/vendor/syntaur-skills/skills/syntaur-protocol/references/protocol-summary.md +82 -0
  23. package/platforms/claude-code/hooks/statusline.sh +0 -110
  24. package/platforms/claude-code/skills/complete-assignment/SKILL.md +0 -155
  25. package/platforms/claude-code/skills/create-assignment/SKILL.md +0 -67
  26. package/platforms/claude-code/skills/grab-assignment/SKILL.md +0 -202
  27. package/platforms/claude-code/skills/plan-assignment/SKILL.md +0 -156
  28. package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +0 -86
  29. package/platforms/codex/skills/complete-assignment/SKILL.md +0 -64
  30. package/platforms/codex/skills/create-assignment/SKILL.md +0 -49
  31. package/platforms/codex/skills/grab-assignment/SKILL.md +0 -73
  32. package/platforms/codex/skills/plan-assignment/SKILL.md +0 -61
  33. package/platforms/codex/skills/syntaur-protocol/SKILL.md +0 -102
package/dist/index.js CHANGED
@@ -4429,7 +4429,7 @@ function App({ projectsDir: projectsDir2, onLaunch }) {
4429
4429
  collapseNode,
4430
4430
  currentNode
4431
4431
  } = useTreeState(nodes, filteredIds);
4432
- useInput((input2, key) => {
4432
+ useInput((input3, key) => {
4433
4433
  if (searchActive) {
4434
4434
  if (key.escape) {
4435
4435
  setSearchActive(false);
@@ -4442,19 +4442,19 @@ function App({ projectsDir: projectsDir2, onLaunch }) {
4442
4442
  }
4443
4443
  return;
4444
4444
  }
4445
- if (input2 === "q" || key.escape) {
4445
+ if (input3 === "q" || key.escape) {
4446
4446
  exit();
4447
4447
  return;
4448
4448
  }
4449
- if (input2 === "/") {
4449
+ if (input3 === "/") {
4450
4450
  setSearchActive(true);
4451
4451
  return;
4452
4452
  }
4453
- if (key.upArrow || input2 === "k") {
4453
+ if (key.upArrow || input3 === "k") {
4454
4454
  moveUp();
4455
4455
  return;
4456
4456
  }
4457
- if (key.downArrow || input2 === "j") {
4457
+ if (key.downArrow || input3 === "j") {
4458
4458
  moveDown();
4459
4459
  return;
4460
4460
  }
@@ -4546,8 +4546,8 @@ __export(launch_exports, {
4546
4546
  launchAgent: () => launchAgent
4547
4547
  });
4548
4548
  import { spawn as spawn2 } from "child_process";
4549
- import { mkdir as mkdir2, writeFile as writeFile7 } from "fs/promises";
4550
- import { resolve as resolve28 } from "path";
4549
+ import { mkdir as mkdir4, writeFile as writeFile9 } from "fs/promises";
4550
+ import { resolve as resolve31 } from "path";
4551
4551
  async function launchAgent(options) {
4552
4552
  const { projectsDir: projectsDir2, projectSlug, assignmentSlug, agent } = options;
4553
4553
  const command = AGENT_COMMANDS[agent];
@@ -4557,10 +4557,10 @@ async function launchAgent(options) {
4557
4557
  process.exit(1);
4558
4558
  }
4559
4559
  const workspaceDir = detail.workspace.worktreePath ?? (detail.workspace.repository?.startsWith("/") ? detail.workspace.repository : null) ?? process.cwd();
4560
- const projectDir = resolve28(projectsDir2, projectSlug);
4561
- const assignmentDir = resolve28(projectDir, "assignments", assignmentSlug);
4562
- const contextDir = resolve28(workspaceDir, ".syntaur");
4563
- await mkdir2(contextDir, { recursive: true });
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 });
4564
4564
  const context = {
4565
4565
  projectSlug,
4566
4566
  assignmentSlug,
@@ -4571,8 +4571,8 @@ async function launchAgent(options) {
4571
4571
  branch: detail.workspace.branch ?? null,
4572
4572
  grabbedAt: (/* @__PURE__ */ new Date()).toISOString()
4573
4573
  };
4574
- await writeFile7(
4575
- resolve28(contextDir, "context.json"),
4574
+ await writeFile9(
4575
+ resolve31(contextDir, "context.json"),
4576
4576
  JSON.stringify(context, null, 2) + "\n"
4577
4577
  );
4578
4578
  return new Promise((resolvePromise, reject) => {
@@ -5909,8 +5909,8 @@ async function migrateFromMarkdown(projectsDir2) {
5909
5909
  return allSessions.length;
5910
5910
  }
5911
5911
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
5912
- const { readFile: readFile26 } = await import("fs/promises");
5913
- const raw = await readFile26(filePath, "utf-8");
5912
+ const { readFile: readFile29 } = await import("fs/promises");
5913
+ const raw = await readFile29(filePath, "utf-8");
5914
5914
  const sessions = [];
5915
5915
  const lines = raw.split("\n");
5916
5916
  let inTable = false;
@@ -7637,8 +7637,8 @@ ${entry}`;
7637
7637
  });
7638
7638
  return router;
7639
7639
  }
7640
- function slugifyLocal(input2) {
7641
- return input2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
7640
+ function slugifyLocal(input3) {
7641
+ return input3.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
7642
7642
  }
7643
7643
  async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
7644
7644
  const commentsPath = resolve15(assignmentDir, "comments.md");
@@ -8218,8 +8218,8 @@ function createTodosRouter(todosDir2, broadcast) {
8218
8218
  router.post("/:workspace/archive", async (req, res) => {
8219
8219
  try {
8220
8220
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
8221
- const { resolve: resolve42 } = await import("path");
8222
- const { readFile: readFile26 } = await import("fs/promises");
8221
+ const { resolve: resolve45 } = await import("path");
8222
+ const { readFile: readFile29 } = await import("fs/promises");
8223
8223
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
8224
8224
  const workspace = getWorkspaceParam(req.params.workspace);
8225
8225
  const checklist = await readChecklist(todosDir2, workspace);
@@ -8235,10 +8235,10 @@ function createTodosRouter(todosDir2, broadcast) {
8235
8235
  (e) => e.itemIds.every((id) => completedIds.has(id))
8236
8236
  );
8237
8237
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
8238
- await ensureDir(resolve42(todosDir2, "archive"));
8238
+ await ensureDir(resolve45(todosDir2, "archive"));
8239
8239
  let archContent = "";
8240
8240
  if (await fileExists(archFile)) {
8241
- archContent = await readFile26(archFile, "utf-8");
8241
+ archContent = await readFile29(archFile, "utf-8");
8242
8242
  archContent = archContent.trimEnd() + "\n\n";
8243
8243
  } else {
8244
8244
  archContent = `---
@@ -10489,6 +10489,165 @@ async function textPrompt(question, defaultValue) {
10489
10489
  }
10490
10490
  }
10491
10491
 
10492
+ // src/utils/install-skills.ts
10493
+ init_fs();
10494
+ import { readFile as readFile16, readdir as readdir11, mkdir as mkdir2, copyFile, rm as rm4 } from "fs/promises";
10495
+ import { dirname as dirname7, resolve as resolve25, relative as relative3, join as join3 } from "path";
10496
+ import { fileURLToPath as fileURLToPath4 } from "url";
10497
+ import { homedir as homedir3 } from "os";
10498
+ var REQUIRED_SKILLS = [
10499
+ "syntaur-protocol",
10500
+ "grab-assignment",
10501
+ "plan-assignment",
10502
+ "complete-assignment",
10503
+ "create-assignment",
10504
+ "create-project"
10505
+ ];
10506
+ function getVendoredSkillsDir() {
10507
+ const here = dirname7(fileURLToPath4(import.meta.url));
10508
+ return resolve25(here, "..", "vendor", "syntaur-skills", "skills");
10509
+ }
10510
+ function defaultSkillTargetDir(target) {
10511
+ if (target === "claude") return resolve25(homedir3(), ".claude", "skills");
10512
+ return resolve25(homedir3(), ".codex", "skills");
10513
+ }
10514
+ async function walkFiles(root) {
10515
+ const out = [];
10516
+ async function walk(dir) {
10517
+ const entries = await readdir11(dir, { withFileTypes: true });
10518
+ for (const entry of entries) {
10519
+ const full = join3(dir, entry.name);
10520
+ if (entry.isDirectory()) {
10521
+ await walk(full);
10522
+ } else if (entry.isFile()) {
10523
+ out.push(full);
10524
+ }
10525
+ }
10526
+ }
10527
+ await walk(root);
10528
+ return out.sort();
10529
+ }
10530
+ async function filesEqual(a, b) {
10531
+ try {
10532
+ const [ba, bb] = await Promise.all([readFile16(a), readFile16(b)]);
10533
+ if (ba.length !== bb.length) return false;
10534
+ return ba.equals(bb);
10535
+ } catch {
10536
+ return false;
10537
+ }
10538
+ }
10539
+ async function copyDir(srcDir, destDir) {
10540
+ await mkdir2(destDir, { recursive: true });
10541
+ const entries = await readdir11(srcDir, { withFileTypes: true });
10542
+ for (const entry of entries) {
10543
+ const src = join3(srcDir, entry.name);
10544
+ const dest = join3(destDir, entry.name);
10545
+ if (entry.isDirectory()) {
10546
+ await copyDir(src, dest);
10547
+ } else if (entry.isFile()) {
10548
+ await copyFile(src, dest);
10549
+ }
10550
+ }
10551
+ }
10552
+ async function skillMatches(srcDir, destDir) {
10553
+ if (!await fileExists(destDir)) return false;
10554
+ const srcFiles = await walkFiles(srcDir);
10555
+ for (const srcFile of srcFiles) {
10556
+ const rel = relative3(srcDir, srcFile);
10557
+ const destFile = join3(destDir, rel);
10558
+ if (!await filesEqual(srcFile, destFile)) return false;
10559
+ }
10560
+ const destFiles = await walkFiles(destDir);
10561
+ if (destFiles.length !== srcFiles.length) return false;
10562
+ return true;
10563
+ }
10564
+ async function installSkills(options) {
10565
+ const source = options.sourceDir ?? getVendoredSkillsDir();
10566
+ const targetRoot = options.targetDir ?? defaultSkillTargetDir(options.target);
10567
+ const force = options.force ?? false;
10568
+ if (!await fileExists(source)) {
10569
+ throw new Error(
10570
+ `Vendored skills not found at ${source}. Reinstall syntaur: npm install -g syntaur@latest`
10571
+ );
10572
+ }
10573
+ const results = [];
10574
+ await mkdir2(targetRoot, { recursive: true });
10575
+ for (const skill of REQUIRED_SKILLS) {
10576
+ const srcDir = join3(source, skill);
10577
+ const destDir = join3(targetRoot, skill);
10578
+ if (!await fileExists(srcDir)) continue;
10579
+ if (!await fileExists(destDir)) {
10580
+ await copyDir(srcDir, destDir);
10581
+ results.push({
10582
+ skill,
10583
+ status: "installed",
10584
+ targetPath: destDir
10585
+ });
10586
+ continue;
10587
+ }
10588
+ if (await skillMatches(srcDir, destDir)) {
10589
+ results.push({
10590
+ skill,
10591
+ status: "already-current",
10592
+ targetPath: destDir
10593
+ });
10594
+ continue;
10595
+ }
10596
+ if (force) {
10597
+ await rm4(destDir, { recursive: true, force: true });
10598
+ await copyDir(srcDir, destDir);
10599
+ results.push({
10600
+ skill,
10601
+ status: "overwritten",
10602
+ targetPath: destDir
10603
+ });
10604
+ } else {
10605
+ results.push({
10606
+ skill,
10607
+ status: "differs-preserved",
10608
+ targetPath: destDir
10609
+ });
10610
+ }
10611
+ }
10612
+ return results;
10613
+ }
10614
+ async function uninstallSkills(options) {
10615
+ const targetRoot = options.targetDir ?? defaultSkillTargetDir(options.target);
10616
+ if (!await fileExists(targetRoot)) return [];
10617
+ const removed = [];
10618
+ for (const skill of REQUIRED_SKILLS) {
10619
+ const destDir = join3(targetRoot, skill);
10620
+ if (!await fileExists(destDir)) continue;
10621
+ const skillMd = join3(destDir, "SKILL.md");
10622
+ if (!await fileExists(skillMd)) continue;
10623
+ const content = await readFile16(skillMd, "utf-8").catch(() => "");
10624
+ const match = content.match(/^name:\s*(\S+)\s*$/m);
10625
+ if (!match || match[1] !== skill) continue;
10626
+ await rm4(destDir, { recursive: true, force: true });
10627
+ removed.push(destDir);
10628
+ }
10629
+ return removed;
10630
+ }
10631
+ function formatInstallReport(results, target) {
10632
+ const lines = [];
10633
+ lines.push(`Skill install (${target}):`);
10634
+ for (const r of results) {
10635
+ const marker = r.status === "installed" ? "+" : r.status === "overwritten" ? "!" : r.status === "differs-preserved" ? "?" : "=";
10636
+ lines.push(` ${marker} ${r.skill} (${r.status})`);
10637
+ }
10638
+ const diffs = results.filter((r) => r.status === "differs-preserved");
10639
+ if (diffs.length > 0) {
10640
+ lines.push("");
10641
+ lines.push(
10642
+ ` Note: ${diffs.length} skill(s) already exist with different content and were preserved.`
10643
+ );
10644
+ lines.push(
10645
+ " Run with --force-skills to overwrite with the vendored version."
10646
+ );
10647
+ }
10648
+ return lines.join("\n");
10649
+ }
10650
+
10492
10651
  // src/commands/install-plugin.ts
10493
10652
  async function promptForInstallPath(question, recommendedPath) {
10494
10653
  while (true) {
@@ -10571,10 +10730,474 @@ async function installPluginCommand(options) {
10571
10730
  if (currentMarketplace) {
10572
10731
  console.log(` marketplace: ${currentMarketplace.manifestPath}`);
10573
10732
  }
10733
+ if (!options.skipSkills) {
10734
+ try {
10735
+ const skillResults = await installSkills({
10736
+ target: "claude",
10737
+ force: options.forceSkills
10738
+ });
10739
+ console.log("");
10740
+ console.log(formatInstallReport(skillResults, "claude"));
10741
+ } catch (error) {
10742
+ console.warn(
10743
+ `Warning: skill install failed \u2014 ${error instanceof Error ? error.message : String(error)}`
10744
+ );
10745
+ }
10746
+ }
10574
10747
  console.log("\nThe plugin is now available in Claude Code.");
10575
- console.log(" Skills: /grab-assignment, /plan-assignment, /complete-assignment");
10576
- console.log(" Background: syntaur-protocol (auto-invoked)");
10577
- console.log(" Hook: write boundary enforcement (PreToolUse)");
10748
+ console.log(" Slash commands: /grab-assignment, /plan-assignment, /complete-assignment, /create-assignment, /create-project");
10749
+ console.log(" Background: syntaur-protocol skill (auto-invoked)");
10750
+ console.log(" Hook: write boundary enforcement (PreToolUse) + SessionStart/End");
10751
+ }
10752
+
10753
+ // src/commands/install-statusline.ts
10754
+ init_paths();
10755
+ init_fs();
10756
+ 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";
10757
+ import { resolve as resolve27, dirname as dirname9 } from "path";
10758
+ import { homedir as homedir4 } from "os";
10759
+ import { fileURLToPath as fileURLToPath5 } from "url";
10760
+
10761
+ // src/commands/configure-statusline.ts
10762
+ init_paths();
10763
+ init_fs();
10764
+ import { readFile as readFile17, writeFile as writeFile7 } from "fs/promises";
10765
+ import { resolve as resolve26, dirname as dirname8 } from "path";
10766
+ import { spawnSync } from "child_process";
10767
+ import { checkbox, input as input2, confirm } from "@inquirer/prompts";
10768
+ var AVAILABLE_SEGMENTS = [
10769
+ { name: "git", preview: "syntaur:main* +2", description: "repo:branch (with dirty marker and ahead/behind)" },
10770
+ { name: "assignment", preview: "my-proj/demo-assn \u2014 Demo Assignment", description: "active syntaur assignment (project/slug or standalone/uuid)" },
10771
+ { name: "session", preview: "\u2026ccddeeff", description: "Claude Code session id \u2014 last 8 chars prefixed by \u2026" },
10772
+ { name: "model", preview: "Opus 4.7", description: "Claude model display name" },
10773
+ { name: "ctx", preview: "ctx:[####------] 42%", description: "context window fill bar" },
10774
+ { name: "cwd", preview: "syntaur", description: "basename of current working directory" },
10775
+ { name: "wrap", preview: "<output of an external script>", description: "compose another statusline script as a leading segment" }
10776
+ ];
10777
+ var PRESETS = {
10778
+ minimal: { segments: ["git", "session"], separator: " \xB7 " },
10779
+ syntaur: { segments: ["git", "assignment", "session"], separator: " \xB7 " },
10780
+ full: { segments: ["wrap", "git", "assignment", "model", "ctx", "session"], separator: " \xB7 " },
10781
+ dev: { segments: ["git", "assignment", "ctx", "session"], separator: " \xB7 " }
10782
+ };
10783
+ function getConfigPath(installRoot) {
10784
+ return resolve26(installRoot, "statusline.config.json");
10785
+ }
10786
+ async function readConfig2(path) {
10787
+ if (!await fileExists(path)) return null;
10788
+ try {
10789
+ const raw = await readFile17(path, "utf-8");
10790
+ const parsed = JSON.parse(raw);
10791
+ if (!parsed || typeof parsed !== "object") return null;
10792
+ const segments = Array.isArray(parsed.segments) ? parsed.segments.filter(isSegmentName) : [];
10793
+ const separator = typeof parsed.separator === "string" ? parsed.separator : " \xB7 ";
10794
+ const wrap = typeof parsed.wrap === "string" ? parsed.wrap : void 0;
10795
+ return { segments, separator, wrap };
10796
+ } catch {
10797
+ return null;
10798
+ }
10799
+ }
10800
+ function isSegmentName(value) {
10801
+ return typeof value === "string" && AVAILABLE_SEGMENTS.some((s) => s.name === value);
10802
+ }
10803
+ function parseSegmentsFlag(flag) {
10804
+ const parts = flag.split(",").map((s) => s.trim()).filter(Boolean);
10805
+ const invalid = parts.filter((p) => !AVAILABLE_SEGMENTS.some((s) => s.name === p));
10806
+ if (invalid.length > 0) {
10807
+ throw new Error(
10808
+ `Unknown segment${invalid.length > 1 ? "s" : ""}: ${invalid.join(", ")}. Valid segments: ${AVAILABLE_SEGMENTS.map((s) => s.name).join(", ")}.`
10809
+ );
10810
+ }
10811
+ return parts;
10812
+ }
10813
+ async function promptSegmentsInteractive(current) {
10814
+ const canonicalOrder = ["wrap", "git", "assignment", "session", "model", "ctx", "cwd"];
10815
+ const selectedSet = new Set(current?.segments ?? ["git", "assignment", "session"]);
10816
+ const choices = canonicalOrder.map((name) => {
10817
+ const def = AVAILABLE_SEGMENTS.find((s) => s.name === name);
10818
+ return {
10819
+ name: `${def.name.padEnd(11)} ${def.description}`,
10820
+ value: name,
10821
+ checked: selectedSet.has(name),
10822
+ description: `preview: ${def.preview}`
10823
+ };
10824
+ });
10825
+ const selected = await checkbox({
10826
+ message: "Pick segments (space to toggle, enter to confirm):",
10827
+ choices,
10828
+ loop: false,
10829
+ pageSize: choices.length,
10830
+ required: true
10831
+ });
10832
+ const defaultReorderHint = selected.join(", ");
10833
+ let orderedSegments = [...selected];
10834
+ const wantReorder = await confirm({
10835
+ message: `Order will be: ${defaultReorderHint}. Customize order?`,
10836
+ default: false
10837
+ });
10838
+ if (wantReorder) {
10839
+ const raw = await input2({
10840
+ message: `Enter the segments in the order you want, comma-separated:`,
10841
+ default: defaultReorderHint,
10842
+ validate: (value) => {
10843
+ const parts = value.split(",").map((s) => s.trim()).filter(Boolean);
10844
+ const invalid = parts.filter((p) => !canonicalOrder.includes(p));
10845
+ if (invalid.length > 0) {
10846
+ return `Unknown: ${invalid.join(", ")}. Valid: ${canonicalOrder.join(", ")}.`;
10847
+ }
10848
+ const missing = selected.filter((s) => !parts.includes(s));
10849
+ if (missing.length > 0) {
10850
+ return `Missing previously-selected segment(s): ${missing.join(", ")}. Include all of them or go back.`;
10851
+ }
10852
+ return true;
10853
+ }
10854
+ });
10855
+ orderedSegments = raw.split(",").map((s) => s.trim()).filter(Boolean);
10856
+ }
10857
+ const separator = await input2({
10858
+ message: "Separator between segments:",
10859
+ default: current?.separator ?? " \xB7 "
10860
+ });
10861
+ let wrap = current?.wrap;
10862
+ if (orderedSegments.includes("wrap")) {
10863
+ wrap = await input2({
10864
+ message: "Path to external script to wrap (leave blank to skip):",
10865
+ default: current?.wrap ?? ""
10866
+ });
10867
+ wrap = wrap.trim() ? wrap.trim() : void 0;
10868
+ }
10869
+ return { segments: orderedSegments, separator, wrap };
10870
+ }
10871
+ function renderPreview(config, statuslineScript, cwd) {
10872
+ const payload = {
10873
+ session_id: "preview-demo-0000000000abcdef12",
10874
+ cwd,
10875
+ model: { display_name: "Opus 4.7" },
10876
+ context_window: { used_percentage: 42 }
10877
+ };
10878
+ const res = spawnSync("bash", [statuslineScript], {
10879
+ input: JSON.stringify(payload),
10880
+ encoding: "utf-8",
10881
+ env: {
10882
+ ...process.env,
10883
+ // Force the child to pick up the freshly-written config from install root.
10884
+ HOME: dirname8(dirname8(statuslineScript))
10885
+ }
10886
+ });
10887
+ if (res.status !== 0) return null;
10888
+ return res.stdout;
10889
+ }
10890
+ async function configureStatuslineCommand(options = {}) {
10891
+ const installRoot = options.installRoot ?? syntaurRoot();
10892
+ const configPath = getConfigPath(installRoot);
10893
+ const current = await readConfig2(configPath);
10894
+ let segments;
10895
+ let separator;
10896
+ let wrap;
10897
+ if (options.preset) {
10898
+ const preset = PRESETS[options.preset.toLowerCase()];
10899
+ if (!preset) {
10900
+ throw new Error(
10901
+ `Unknown preset "${options.preset}". Presets: ${Object.keys(PRESETS).join(", ")}.`
10902
+ );
10903
+ }
10904
+ segments = [...preset.segments];
10905
+ separator = options.separator ?? preset.separator;
10906
+ wrap = options.wrap ?? current?.wrap;
10907
+ } else if (options.segments) {
10908
+ segments = parseSegmentsFlag(options.segments);
10909
+ separator = options.separator ?? current?.separator ?? " \xB7 ";
10910
+ wrap = options.wrap ?? current?.wrap;
10911
+ } else if (isInteractiveTerminal()) {
10912
+ const answers = await promptSegmentsInteractive(current);
10913
+ segments = answers.segments;
10914
+ separator = answers.separator;
10915
+ wrap = answers.wrap;
10916
+ } else {
10917
+ throw new Error(
10918
+ "Non-interactive invocation requires --preset, --segments, or run in a TTY."
10919
+ );
10920
+ }
10921
+ if (segments.includes("wrap") && !wrap) {
10922
+ console.warn(
10923
+ `Note: the "wrap" segment is selected but no wrap path is configured. Set one with --wrap <path> or edit ${configPath} afterwards.`
10924
+ );
10925
+ }
10926
+ const config = { segments, separator, ...wrap ? { wrap } : {} };
10927
+ if (options.preview) {
10928
+ console.log("Segments: " + config.segments.join(", "));
10929
+ console.log("Separator: " + JSON.stringify(config.separator));
10930
+ if (config.wrap) console.log("Wrap: " + config.wrap);
10931
+ console.log("");
10932
+ console.log("(preview mode \u2014 config NOT written)");
10933
+ return;
10934
+ }
10935
+ await ensureDir(dirname8(configPath));
10936
+ await writeFile7(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
10937
+ console.log("Wrote statusline config:");
10938
+ console.log(` path: ${configPath}`);
10939
+ console.log(` segments: ${config.segments.join(", ")}`);
10940
+ console.log(` separator: ${JSON.stringify(config.separator)}`);
10941
+ if (config.wrap) console.log(` wrap: ${config.wrap}`);
10942
+ const script = options.statuslineScript ?? resolve26(installRoot, "statusline.sh");
10943
+ if (await fileExists(script)) {
10944
+ console.log("");
10945
+ console.log("Live preview:");
10946
+ const out = renderPreview(config, script, process.cwd());
10947
+ if (out) {
10948
+ console.log(" " + out);
10949
+ } else {
10950
+ console.log(" (preview failed \u2014 run `syntaur install-statusline` if the script is missing)");
10951
+ }
10952
+ } else {
10953
+ console.log("");
10954
+ console.log(
10955
+ "(statusline script not yet installed \u2014 run `syntaur install-statusline` to wire it up)"
10956
+ );
10957
+ }
10958
+ }
10959
+ async function writeDefaultConfigIfMissing(installRoot) {
10960
+ const path = getConfigPath(installRoot);
10961
+ if (await fileExists(path)) return;
10962
+ await ensureDir(dirname8(path));
10963
+ const defaultConfig = {
10964
+ segments: ["git", "assignment", "session"],
10965
+ separator: " \xB7 "
10966
+ };
10967
+ await writeFile7(path, JSON.stringify(defaultConfig, null, 2) + "\n", "utf-8");
10968
+ }
10969
+
10970
+ // src/commands/install-statusline.ts
10971
+ function getPackageStatuslineSource() {
10972
+ const here = dirname9(fileURLToPath5(import.meta.url));
10973
+ return resolve27(here, "..", "statusline", "statusline.sh");
10974
+ }
10975
+ async function readSettingsJson(settingsPath) {
10976
+ if (!await fileExists(settingsPath)) return {};
10977
+ const raw = await readFile18(settingsPath, "utf-8");
10978
+ if (raw.trim() === "") return {};
10979
+ try {
10980
+ const parsed = JSON.parse(raw);
10981
+ return parsed && typeof parsed === "object" ? parsed : {};
10982
+ } catch (error) {
10983
+ throw new Error(
10984
+ `Unable to parse ${settingsPath}: ${error.message}. Fix the JSON and re-run.`
10985
+ );
10986
+ }
10987
+ }
10988
+ async function writeSettingsJson(settingsPath, data) {
10989
+ await ensureDir(dirname9(settingsPath));
10990
+ await writeFile8(settingsPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
10991
+ }
10992
+ async function resolveMode(mode, existingCommand, ourCommand) {
10993
+ if (mode !== "ask") return mode;
10994
+ if (!existingCommand || existingCommand === ourCommand) return "replace";
10995
+ if (!isInteractiveTerminal()) {
10996
+ return "wrap";
10997
+ }
10998
+ console.log(
10999
+ `A Claude Code statusLine is already configured:
11000
+ ${existingCommand}
11001
+ `
11002
+ );
11003
+ const wantWrap = await confirmPrompt(
11004
+ "Compose (wrap) your existing statusline with syntaur segments? [Y to wrap / n to replace]",
11005
+ true
11006
+ );
11007
+ if (wantWrap) return "wrap";
11008
+ const confirmReplace = await confirmPrompt(
11009
+ "Replace your existing statusline with syntaur only?",
11010
+ false
11011
+ );
11012
+ return confirmReplace ? "replace" : "skip";
11013
+ }
11014
+ function extractExistingCommand(settings) {
11015
+ const sl = settings.statusLine;
11016
+ if (!sl || typeof sl !== "object") return void 0;
11017
+ const obj = sl;
11018
+ return {
11019
+ type: typeof obj.type === "string" ? obj.type : void 0,
11020
+ command: typeof obj.command === "string" ? obj.command : void 0
11021
+ };
11022
+ }
11023
+ async function backupSettings(settingsSnapshot, backupPath) {
11024
+ await ensureDir(dirname9(backupPath));
11025
+ await writeFile8(
11026
+ backupPath,
11027
+ JSON.stringify(
11028
+ {
11029
+ version: 1,
11030
+ takenAt: (/* @__PURE__ */ new Date()).toISOString(),
11031
+ settingsPath: settingsSnapshot.settingsPath,
11032
+ previousStatusLine: settingsSnapshot.existingStatusLine ?? null
11033
+ },
11034
+ null,
11035
+ 2
11036
+ ) + "\n",
11037
+ "utf-8"
11038
+ );
11039
+ }
11040
+ async function installScript(sourceScript, destScript, link) {
11041
+ await ensureDir(dirname9(destScript));
11042
+ try {
11043
+ const s = await lstat2(destScript);
11044
+ if (s.isSymbolicLink() || s.isFile()) {
11045
+ await unlink6(destScript);
11046
+ }
11047
+ } catch {
11048
+ }
11049
+ if (link) {
11050
+ await symlink2(sourceScript, destScript);
11051
+ } else {
11052
+ await copyFile2(sourceScript, destScript);
11053
+ }
11054
+ }
11055
+ async function installStatuslineCommand(options = {}) {
11056
+ const mode = options.mode ?? "ask";
11057
+ const settingsPath = options.settingsPath ?? resolve27(homedir4(), ".claude", "settings.json");
11058
+ const installRoot = options.installRoot ?? syntaurRoot();
11059
+ const sourceScript = options.sourceScript ?? getPackageStatuslineSource();
11060
+ const destScript = resolve27(installRoot, "statusline.sh");
11061
+ const confPath = resolve27(installRoot, "statusline.conf");
11062
+ const backupPath = resolve27(installRoot, "statusline.backup.json");
11063
+ if (!await fileExists(sourceScript)) {
11064
+ throw new Error(
11065
+ `Statusline source script not found at ${sourceScript}. Try re-installing syntaur (npm install -g syntaur) or pass --source-script explicitly.`
11066
+ );
11067
+ }
11068
+ await installScript(sourceScript, destScript, Boolean(options.link));
11069
+ await writeDefaultConfigIfMissing(installRoot);
11070
+ const settings = await readSettingsJson(settingsPath);
11071
+ const existingStatusLine = extractExistingCommand(settings);
11072
+ const existingCommand = existingStatusLine?.command;
11073
+ const ourCommand = `bash ${destScript}`;
11074
+ const resolvedMode = await resolveMode(mode, existingCommand, ourCommand);
11075
+ if (resolvedMode === "skip") {
11076
+ console.log("Installed statusline script only:");
11077
+ console.log(` script: ${destScript}`);
11078
+ console.log(` source: ${sourceScript}`);
11079
+ console.log(
11080
+ " (settings.json left unchanged \u2014 run with --mode=replace or --mode=wrap to wire it up)"
11081
+ );
11082
+ return;
11083
+ }
11084
+ await backupSettings(
11085
+ {
11086
+ settingsPath,
11087
+ existingStatusLine,
11088
+ existingCommand
11089
+ },
11090
+ backupPath
11091
+ );
11092
+ let wrapTarget = "";
11093
+ if (resolvedMode === "wrap" && existingCommand && existingCommand !== ourCommand) {
11094
+ const parsed = parseWrapCommand(existingCommand);
11095
+ if (parsed) {
11096
+ wrapTarget = parsed;
11097
+ } else {
11098
+ const wrapperPath = resolve27(installRoot, "statusline-wrapped.sh");
11099
+ const wrapperBody = `#!/usr/bin/env bash
11100
+ # Auto-generated by syntaur install-statusline.
11101
+ # Executes the previously configured statusLine command.
11102
+ exec ${existingCommand}
11103
+ `;
11104
+ await writeFile8(wrapperPath, wrapperBody, "utf-8");
11105
+ await chmodExec(wrapperPath);
11106
+ wrapTarget = wrapperPath;
11107
+ }
11108
+ }
11109
+ await ensureDir(dirname9(confPath));
11110
+ await writeFile8(
11111
+ confPath,
11112
+ wrapTarget ? `# Wrap target \u2014 the command below is invoked with the same stdin; its
11113
+ # stdout becomes the leading segment of the statusline. Remove this
11114
+ # line or comment it out to disable wrapping.
11115
+ ${wrapTarget}
11116
+ ` : `# syntaur statusline config. Add a single path on a non-comment line to
11117
+ # wrap another statusline script with syntaur segments appended.
11118
+ `,
11119
+ "utf-8"
11120
+ );
11121
+ settings.statusLine = {
11122
+ type: "command",
11123
+ command: ourCommand
11124
+ };
11125
+ await writeSettingsJson(settingsPath, settings);
11126
+ console.log("Installed syntaur statusline:");
11127
+ console.log(` script: ${destScript}`);
11128
+ console.log(` source: ${sourceScript}`);
11129
+ console.log(` mode: ${resolvedMode}`);
11130
+ console.log(` settings.json:${settingsPath}`);
11131
+ console.log(` backup: ${backupPath}`);
11132
+ if (wrapTarget) {
11133
+ console.log(` wrap target: ${wrapTarget}`);
11134
+ console.log(` (edit ${confPath} to change or disable wrapping)`);
11135
+ }
11136
+ }
11137
+ function parseWrapCommand(command) {
11138
+ const trimmed = command.trim();
11139
+ const bashMatch = trimmed.match(/^bash\s+(\S+)$/);
11140
+ if (bashMatch) return bashMatch[1];
11141
+ if (/^\S+\.(sh|bash)$/.test(trimmed)) return trimmed;
11142
+ return null;
11143
+ }
11144
+ async function chmodExec(path) {
11145
+ const fs = await import("fs/promises");
11146
+ try {
11147
+ const s = await stat3(path);
11148
+ await fs.chmod(path, s.mode | 73);
11149
+ } catch {
11150
+ }
11151
+ }
11152
+ async function uninstallStatuslineCommand(options = {}) {
11153
+ const settingsPath = options.settingsPath ?? resolve27(homedir4(), ".claude", "settings.json");
11154
+ const installRoot = options.installRoot ?? syntaurRoot();
11155
+ const destScript = resolve27(installRoot, "statusline.sh");
11156
+ const confPath = resolve27(installRoot, "statusline.conf");
11157
+ const backupPath = resolve27(installRoot, "statusline.backup.json");
11158
+ const wrapperPath = resolve27(installRoot, "statusline-wrapped.sh");
11159
+ const settings = await readSettingsJson(settingsPath);
11160
+ const existing = extractExistingCommand(settings);
11161
+ const ourCommand = `bash ${destScript}`;
11162
+ let restored = null;
11163
+ if (await fileExists(backupPath)) {
11164
+ try {
11165
+ const raw = await readFile18(backupPath, "utf-8");
11166
+ const parsed = JSON.parse(raw);
11167
+ const prev = parsed?.previousStatusLine;
11168
+ if (prev && typeof prev === "object" && typeof prev.command === "string") {
11169
+ restored = { command: prev.command };
11170
+ }
11171
+ } catch {
11172
+ }
11173
+ }
11174
+ if (existing?.command === ourCommand) {
11175
+ if (restored) {
11176
+ settings.statusLine = {
11177
+ type: "command",
11178
+ command: restored.command
11179
+ };
11180
+ } else {
11181
+ delete settings.statusLine;
11182
+ }
11183
+ await writeSettingsJson(settingsPath, settings);
11184
+ }
11185
+ if (!options.keepScript) {
11186
+ const configPath = resolve27(installRoot, "statusline.config.json");
11187
+ for (const path of [destScript, confPath, backupPath, wrapperPath, configPath]) {
11188
+ try {
11189
+ await rm5(path, { force: true });
11190
+ } catch {
11191
+ }
11192
+ }
11193
+ }
11194
+ console.log("Uninstalled syntaur statusline.");
11195
+ if (restored) {
11196
+ console.log(` Restored previous command: ${restored.command}`);
11197
+ } else {
11198
+ console.log(" Removed statusLine entry from settings.json.");
11199
+ }
11200
+ console.log(` settings.json: ${settingsPath}`);
10578
11201
  }
10579
11202
 
10580
11203
  // src/commands/install-codex-plugin.ts
@@ -10656,14 +11279,61 @@ async function installCodexPluginCommand(options) {
10656
11279
  console.log(` source: ${result.sourceDir}`);
10657
11280
  console.log(` mode: ${result.mode}`);
10658
11281
  console.log(` marketplace: ${marketplace.marketplacePath}`);
11282
+ if (!options.skipSkills) {
11283
+ try {
11284
+ const skillResults = await installSkills({
11285
+ target: "codex",
11286
+ force: options.forceSkills
11287
+ });
11288
+ console.log("");
11289
+ console.log(formatInstallReport(skillResults, "codex"));
11290
+ } catch (error) {
11291
+ console.warn(
11292
+ `Warning: skill install failed \u2014 ${error instanceof Error ? error.message : String(error)}`
11293
+ );
11294
+ }
11295
+ }
10659
11296
  console.log("\nThe plugin is now available to Codex.");
10660
11297
  console.log(
10661
- " Skills: syntaur-protocol, create-project, create-assignment, grab-assignment, plan-assignment, complete-assignment, track-session"
11298
+ " Protocol skills: syntaur-protocol, create-project, create-assignment, grab-assignment, plan-assignment, complete-assignment"
10662
11299
  );
10663
- console.log(" Command: /track-session");
11300
+ console.log(" Codex-specific: track-session skill (rollout path aware)");
10664
11301
  console.log(" Hooks: write boundary enforcement, session cleanup");
10665
11302
  }
10666
11303
 
11304
+ // src/commands/uninstall-skills.ts
11305
+ async function uninstallSkillsCommand(options) {
11306
+ const runClaude = Boolean(options.claude || options.all);
11307
+ const runCodex = Boolean(options.codex || options.all);
11308
+ if (!runClaude && !runCodex) {
11309
+ throw new Error(
11310
+ "Specify --claude, --codex, or --all (use one or more)."
11311
+ );
11312
+ }
11313
+ let totalRemoved = 0;
11314
+ if (runClaude) {
11315
+ const removed = await uninstallSkills({ target: "claude" });
11316
+ totalRemoved += removed.length;
11317
+ console.log(
11318
+ `Removed ${removed.length} Syntaur protocol skill(s) from ~/.claude/skills:`
11319
+ );
11320
+ for (const p of removed) console.log(` - ${p}`);
11321
+ }
11322
+ if (runCodex) {
11323
+ const removed = await uninstallSkills({ target: "codex" });
11324
+ totalRemoved += removed.length;
11325
+ console.log(
11326
+ `Removed ${removed.length} Syntaur protocol skill(s) from ~/.codex/skills:`
11327
+ );
11328
+ for (const p of removed) console.log(` - ${p}`);
11329
+ }
11330
+ if (totalRemoved === 0) {
11331
+ console.log(
11332
+ "No Syntaur protocol skills found to remove. (User-authored skills with matching directory names are preserved.)"
11333
+ );
11334
+ }
11335
+ }
11336
+
10667
11337
  // src/commands/setup.ts
10668
11338
  import { execSync } from "child_process";
10669
11339
  init_config2();
@@ -10768,7 +11438,7 @@ async function setupCommand(options) {
10768
11438
  }
10769
11439
 
10770
11440
  // src/commands/uninstall.ts
10771
- import { resolve as resolve25 } from "path";
11441
+ import { resolve as resolve28 } from "path";
10772
11442
  init_paths();
10773
11443
  function expandTargets(options) {
10774
11444
  if (options.all) {
@@ -10848,7 +11518,7 @@ async function uninstallCommand(options) {
10848
11518
  const configuredProjectDir = await getConfiguredProjectDir();
10849
11519
  await removeSyntaurData();
10850
11520
  console.log(`Removed ${syntaurRoot()}`);
10851
- if (configuredProjectDir && resolve25(configuredProjectDir) !== resolve25(syntaurRoot(), "projects")) {
11521
+ if (configuredProjectDir && resolve28(configuredProjectDir) !== resolve28(syntaurRoot(), "projects")) {
10852
11522
  console.warn(
10853
11523
  `Warning: config.md pointed to an external project directory (${configuredProjectDir}). That directory was not removed automatically.`
10854
11524
  );
@@ -10863,7 +11533,7 @@ async function uninstallCommand(options) {
10863
11533
  init_paths();
10864
11534
  init_fs();
10865
11535
  init_config2();
10866
- import { resolve as resolve26 } from "path";
11536
+ import { resolve as resolve29 } from "path";
10867
11537
  var SUPPORTED_FRAMEWORKS = ["cursor", "codex", "opencode"];
10868
11538
  async function setupAdapterCommand(framework, options) {
10869
11539
  if (!SUPPORTED_FRAMEWORKS.includes(framework)) {
@@ -10889,19 +11559,19 @@ async function setupAdapterCommand(framework, options) {
10889
11559
  }
10890
11560
  const config = await readConfig();
10891
11561
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
10892
- const projectDir = resolve26(baseDir, options.project);
10893
- const assignmentDir = resolve26(
11562
+ const projectDir = resolve29(baseDir, options.project);
11563
+ const assignmentDir = resolve29(
10894
11564
  projectDir,
10895
11565
  "assignments",
10896
11566
  options.assignment
10897
11567
  );
10898
- const projectMdPath = resolve26(projectDir, "project.md");
11568
+ const projectMdPath = resolve29(projectDir, "project.md");
10899
11569
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
10900
11570
  throw new Error(
10901
11571
  `Project "${options.project}" not found at ${projectDir}.`
10902
11572
  );
10903
11573
  }
10904
- const assignmentMdPath = resolve26(assignmentDir, "assignment.md");
11574
+ const assignmentMdPath = resolve29(assignmentDir, "assignment.md");
10905
11575
  if (!await fileExists(assignmentDir) || !await fileExists(assignmentMdPath)) {
10906
11576
  throw new Error(
10907
11577
  `Assignment "${options.assignment}" not found at ${assignmentDir}.`
@@ -10929,15 +11599,15 @@ async function setupAdapterCommand(framework, options) {
10929
11599
  }
10930
11600
  }
10931
11601
  if (framework === "cursor") {
10932
- const protocolPath = resolve26(cwd, ".cursor", "rules", "syntaur-protocol.mdc");
10933
- const assignmentPath = resolve26(cwd, ".cursor", "rules", "syntaur-assignment.mdc");
11602
+ const protocolPath = resolve29(cwd, ".cursor", "rules", "syntaur-protocol.mdc");
11603
+ const assignmentPath = resolve29(cwd, ".cursor", "rules", "syntaur-assignment.mdc");
10934
11604
  await writeAdapterFile(protocolPath, renderCursorProtocol());
10935
11605
  await writeAdapterFile(assignmentPath, renderCursorAssignment(rendererParams));
10936
11606
  } else if (framework === "codex" || framework === "opencode") {
10937
- const agentsPath = resolve26(cwd, "AGENTS.md");
11607
+ const agentsPath = resolve29(cwd, "AGENTS.md");
10938
11608
  await writeAdapterFile(agentsPath, renderCodexAgents(rendererParams));
10939
11609
  if (framework === "opencode") {
10940
- const configPath = resolve26(cwd, "opencode.json");
11610
+ const configPath = resolve29(cwd, "opencode.json");
10941
11611
  await writeAdapterFile(configPath, renderOpenCodeConfig({ projectDir }));
10942
11612
  }
10943
11613
  }
@@ -10962,7 +11632,7 @@ async function setupAdapterCommand(framework, options) {
10962
11632
  init_paths();
10963
11633
  init_fs();
10964
11634
  init_config2();
10965
- import { resolve as resolve27 } from "path";
11635
+ import { resolve as resolve30 } from "path";
10966
11636
  async function trackSessionCommand(options) {
10967
11637
  if (!options.agent) {
10968
11638
  throw new Error("--agent <name> is required.");
@@ -10975,7 +11645,7 @@ async function trackSessionCommand(options) {
10975
11645
  if (options.project) {
10976
11646
  const config = await readConfig();
10977
11647
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
10978
- const projectDir = resolve27(baseDir, options.project);
11648
+ const projectDir = resolve30(baseDir, options.project);
10979
11649
  if (!await fileExists(projectDir)) {
10980
11650
  throw new Error(
10981
11651
  `Project "${options.project}" not found at ${projectDir}.`
@@ -11030,7 +11700,7 @@ async function browseCommand(options) {
11030
11700
  }
11031
11701
 
11032
11702
  // src/commands/create-playbook.ts
11033
- import { resolve as resolve29 } from "path";
11703
+ import { resolve as resolve32 } from "path";
11034
11704
  init_timestamp();
11035
11705
  init_paths();
11036
11706
  init_fs();
@@ -11046,7 +11716,7 @@ async function createPlaybookCommand(name, options) {
11046
11716
  }
11047
11717
  const dir = playbooksDir();
11048
11718
  await ensureDir(dir);
11049
- const filePath = resolve29(dir, `${slug}.md`);
11719
+ const filePath = resolve32(dir, `${slug}.md`);
11050
11720
  if (await fileExists(filePath)) {
11051
11721
  throw new Error(
11052
11722
  `Playbook "${slug}" already exists at ${filePath}
@@ -11067,15 +11737,15 @@ Use --slug to specify a different slug.`
11067
11737
  init_paths();
11068
11738
  init_fs();
11069
11739
  init_parser();
11070
- import { readdir as readdir11, readFile as readFile16 } from "fs/promises";
11071
- import { resolve as resolve30 } from "path";
11740
+ import { readdir as readdir12, readFile as readFile19 } from "fs/promises";
11741
+ import { resolve as resolve33 } from "path";
11072
11742
  async function listPlaybooksCommand() {
11073
11743
  const dir = playbooksDir();
11074
11744
  if (!await fileExists(dir)) {
11075
11745
  console.log('No playbooks directory found. Run "syntaur init" first.');
11076
11746
  return;
11077
11747
  }
11078
- const entries = await readdir11(dir, { withFileTypes: true });
11748
+ const entries = await readdir12(dir, { withFileTypes: true });
11079
11749
  const mdFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_") && e.name !== "manifest.md");
11080
11750
  if (mdFiles.length === 0) {
11081
11751
  console.log('No playbooks found. Create one with "syntaur create-playbook <name>".');
@@ -11086,8 +11756,8 @@ async function listPlaybooksCommand() {
11086
11756
  console.log(`${"Slug".padEnd(30)} ${"Name".padEnd(30)} Description`);
11087
11757
  console.log(`${"\u2500".repeat(30)} ${"\u2500".repeat(30)} ${"\u2500".repeat(40)}`);
11088
11758
  for (const entry of mdFiles) {
11089
- const filePath = resolve30(dir, entry.name);
11090
- const raw = await readFile16(filePath, "utf-8");
11759
+ const filePath = resolve33(dir, entry.name);
11760
+ const raw = await readFile19(filePath, "utf-8");
11091
11761
  const parsed = parsePlaybook(raw);
11092
11762
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
11093
11763
  const name = parsed.name || slug;
@@ -11101,8 +11771,8 @@ init_paths();
11101
11771
  init_parser2();
11102
11772
  init_fs();
11103
11773
  import { Command } from "commander";
11104
- import { readFile as readFile17 } from "fs/promises";
11105
- import { resolve as resolve31 } from "path";
11774
+ import { readFile as readFile20 } from "fs/promises";
11775
+ import { resolve as resolve34 } from "path";
11106
11776
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
11107
11777
  function resolveWorkspace(options) {
11108
11778
  if (options.global) return "_global";
@@ -11383,10 +12053,10 @@ todoCommand.command("archive").description("Archive completed todos and their lo
11383
12053
  (e) => e.itemIds.every((id) => completedIds.has(id))
11384
12054
  );
11385
12055
  const archFile = archivePath(todosPath, workspace, checklist.archiveInterval);
11386
- await ensureDir(resolve31(todosPath, "archive"));
12056
+ await ensureDir(resolve34(todosPath, "archive"));
11387
12057
  let archContent = "";
11388
12058
  if (await fileExists(archFile)) {
11389
- archContent = await readFile17(archFile, "utf-8");
12059
+ archContent = await readFile20(archFile, "utf-8");
11390
12060
  archContent = archContent.trimEnd() + "\n\n";
11391
12061
  } else {
11392
12062
  archContent = `---
@@ -11574,20 +12244,20 @@ backupCommand.command("config").description("Show or update backup configuration
11574
12244
  import { Command as Command3 } from "commander";
11575
12245
 
11576
12246
  // src/utils/doctor/index.ts
11577
- import { fileURLToPath as fileURLToPath5 } from "url";
11578
- import { readFile as readFile21 } from "fs/promises";
11579
- import { dirname as dirname8, join as join4 } from "path";
12247
+ import { fileURLToPath as fileURLToPath7 } from "url";
12248
+ import { readFile as readFile24 } from "fs/promises";
12249
+ import { dirname as dirname11, join as join5 } from "path";
11580
12250
 
11581
12251
  // src/utils/doctor/context.ts
11582
12252
  init_config2();
11583
12253
  init_paths();
11584
12254
  init_fs();
11585
12255
  import Database2 from "better-sqlite3";
11586
- import { resolve as resolve32 } from "path";
12256
+ import { resolve as resolve35 } from "path";
11587
12257
  async function buildCheckContext(cwd = process.cwd()) {
11588
12258
  const config = await readConfig();
11589
12259
  const root = syntaurRoot();
11590
- const dbPath = resolve32(root, "syntaur.db");
12260
+ const dbPath = resolve35(root, "syntaur.db");
11591
12261
  let db2 = null;
11592
12262
  let dbError = null;
11593
12263
  if (await fileExists(dbPath)) {
@@ -11621,10 +12291,10 @@ function closeCheckContext(ctx) {
11621
12291
  // src/utils/doctor/checks/env.ts
11622
12292
  init_fs();
11623
12293
  init_paths();
11624
- import { resolve as resolve33, isAbsolute as isAbsolute3 } from "path";
11625
- import { readFile as readFile18, stat as stat2 } from "fs/promises";
11626
- import { fileURLToPath as fileURLToPath4 } from "url";
11627
- import { dirname as dirname7, join as join3 } from "path";
12294
+ import { resolve as resolve36, isAbsolute as isAbsolute3 } from "path";
12295
+ import { readFile as readFile21, stat as stat4 } from "fs/promises";
12296
+ import { fileURLToPath as fileURLToPath6 } from "url";
12297
+ import { dirname as dirname10, join as join4 } from "path";
11628
12298
  var CATEGORY = "env";
11629
12299
  var syntaurRootExists = {
11630
12300
  id: "env.syntaur-root-exists",
@@ -11632,7 +12302,7 @@ var syntaurRootExists = {
11632
12302
  title: "~/.syntaur/ directory exists",
11633
12303
  async run(ctx) {
11634
12304
  try {
11635
- const s = await stat2(ctx.syntaurRoot);
12305
+ const s = await stat4(ctx.syntaurRoot);
11636
12306
  if (!s.isDirectory()) {
11637
12307
  return err(this, `${ctx.syntaurRoot} exists but is not a directory`, [
11638
12308
  ctx.syntaurRoot
@@ -11662,7 +12332,7 @@ var configValid = {
11662
12332
  category: CATEGORY,
11663
12333
  title: "~/.syntaur/config.md is valid",
11664
12334
  async run(ctx) {
11665
- const configPath = resolve33(ctx.syntaurRoot, "config.md");
12335
+ const configPath = resolve36(ctx.syntaurRoot, "config.md");
11666
12336
  if (!await fileExists(configPath)) {
11667
12337
  return {
11668
12338
  id: this.id,
@@ -11679,7 +12349,7 @@ var configValid = {
11679
12349
  autoFixable: false
11680
12350
  };
11681
12351
  }
11682
- const content = await readFile18(configPath, "utf-8");
12352
+ const content = await readFile21(configPath, "utf-8");
11683
12353
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
11684
12354
  if (!fmMatch || fmMatch[1].trim() === "") {
11685
12355
  return {
@@ -11954,15 +12624,15 @@ async function readLocalVersion() {
11954
12624
  }
11955
12625
  async function readLocalPkg() {
11956
12626
  try {
11957
- const here = fileURLToPath4(import.meta.url);
11958
- let dir = dirname7(here);
12627
+ const here = fileURLToPath6(import.meta.url);
12628
+ let dir = dirname10(here);
11959
12629
  for (let i = 0; i < 6; i++) {
11960
- const candidate = join3(dir, "package.json");
12630
+ const candidate = join4(dir, "package.json");
11961
12631
  try {
11962
- const text = await readFile18(candidate, "utf-8");
12632
+ const text = await readFile21(candidate, "utf-8");
11963
12633
  return JSON.parse(text);
11964
12634
  } catch {
11965
- dir = dirname7(dir);
12635
+ dir = dirname10(dir);
11966
12636
  }
11967
12637
  }
11968
12638
  return null;
@@ -12011,8 +12681,8 @@ function versionGte(a, b) {
12011
12681
 
12012
12682
  // src/utils/doctor/checks/structure.ts
12013
12683
  init_fs();
12014
- import { resolve as resolve34 } from "path";
12015
- import { readdir as readdir12, stat as stat3 } from "fs/promises";
12684
+ import { resolve as resolve37 } from "path";
12685
+ import { readdir as readdir13, stat as stat5 } from "fs/promises";
12016
12686
  var CATEGORY2 = "structure";
12017
12687
  var KNOWN_TOP_LEVEL = /* @__PURE__ */ new Set([
12018
12688
  "projects",
@@ -12031,7 +12701,7 @@ var projectsDir = {
12031
12701
  category: CATEGORY2,
12032
12702
  title: "projects/ directory exists",
12033
12703
  async run(ctx) {
12034
- const p = resolve34(ctx.syntaurRoot, "projects");
12704
+ const p = resolve37(ctx.syntaurRoot, "projects");
12035
12705
  if (!await fileExists(p)) {
12036
12706
  return {
12037
12707
  id: this.id,
@@ -12056,7 +12726,7 @@ var playbooksDir2 = {
12056
12726
  category: CATEGORY2,
12057
12727
  title: "playbooks/ directory exists",
12058
12728
  async run(ctx) {
12059
- const p = resolve34(ctx.syntaurRoot, "playbooks");
12729
+ const p = resolve37(ctx.syntaurRoot, "playbooks");
12060
12730
  if (!await fileExists(p)) {
12061
12731
  return {
12062
12732
  id: this.id,
@@ -12081,7 +12751,7 @@ var todosDirValid = {
12081
12751
  category: CATEGORY2,
12082
12752
  title: "todos/ directory is readable (if present)",
12083
12753
  async run(ctx) {
12084
- const p = resolve34(ctx.syntaurRoot, "todos");
12754
+ const p = resolve37(ctx.syntaurRoot, "todos");
12085
12755
  if (!await fileExists(p)) {
12086
12756
  return {
12087
12757
  id: this.id,
@@ -12092,7 +12762,7 @@ var todosDirValid = {
12092
12762
  autoFixable: false
12093
12763
  };
12094
12764
  }
12095
- const s = await stat3(p);
12765
+ const s = await stat5(p);
12096
12766
  if (!s.isDirectory()) {
12097
12767
  return {
12098
12768
  id: this.id,
@@ -12112,7 +12782,7 @@ var serversDirValid = {
12112
12782
  category: CATEGORY2,
12113
12783
  title: "servers/ directory is readable (if present)",
12114
12784
  async run(ctx) {
12115
- const p = resolve34(ctx.syntaurRoot, "servers");
12785
+ const p = resolve37(ctx.syntaurRoot, "servers");
12116
12786
  if (!await fileExists(p)) {
12117
12787
  return {
12118
12788
  id: this.id,
@@ -12123,7 +12793,7 @@ var serversDirValid = {
12123
12793
  autoFixable: false
12124
12794
  };
12125
12795
  }
12126
- const s = await stat3(p);
12796
+ const s = await stat5(p);
12127
12797
  if (!s.isDirectory()) {
12128
12798
  return {
12129
12799
  id: this.id,
@@ -12143,7 +12813,7 @@ var knownFilesRecognized = {
12143
12813
  category: CATEGORY2,
12144
12814
  title: "No unexpected top-level entries under ~/.syntaur/",
12145
12815
  async run(ctx) {
12146
- const entries = await readdir12(ctx.syntaurRoot, { withFileTypes: true });
12816
+ const entries = await readdir13(ctx.syntaurRoot, { withFileTypes: true });
12147
12817
  const unexpected = [];
12148
12818
  for (const e of entries) {
12149
12819
  if (e.name.startsWith(".")) continue;
@@ -12157,7 +12827,7 @@ var knownFilesRecognized = {
12157
12827
  title: this.title,
12158
12828
  status: "warn",
12159
12829
  detail: `unexpected top-level entries: ${unexpected.join(", ")}`,
12160
- affected: unexpected.map((n) => resolve34(ctx.syntaurRoot, n)),
12830
+ affected: unexpected.map((n) => resolve37(ctx.syntaurRoot, n)),
12161
12831
  remediation: {
12162
12832
  kind: "manual",
12163
12833
  suggestion: "Review these entries \u2014 they may be leftover state from older versions",
@@ -12186,8 +12856,8 @@ function pass2(check) {
12186
12856
 
12187
12857
  // src/utils/doctor/checks/project.ts
12188
12858
  init_fs();
12189
- import { resolve as resolve35 } from "path";
12190
- import { readdir as readdir13, stat as stat4 } from "fs/promises";
12859
+ import { resolve as resolve38 } from "path";
12860
+ import { readdir as readdir14, stat as stat6 } from "fs/promises";
12191
12861
  var CATEGORY3 = "project";
12192
12862
  var REQUIRED_PROJECT_FILES = [
12193
12863
  "project.md",
@@ -12211,15 +12881,15 @@ var PROJECT_MARKERS = ["project.md", "manifest.md", "assignments"];
12211
12881
  async function listProjects2(ctx) {
12212
12882
  const dir = ctx.config.defaultProjectDir;
12213
12883
  if (!await fileExists(dir)) return [];
12214
- const entries = await readdir13(dir, { withFileTypes: true });
12884
+ const entries = await readdir14(dir, { withFileTypes: true });
12215
12885
  const result = [];
12216
12886
  for (const e of entries) {
12217
12887
  if (!e.isDirectory()) continue;
12218
12888
  if (e.name.startsWith(".") || e.name.startsWith("_")) continue;
12219
- const projectDir = resolve35(dir, e.name);
12889
+ const projectDir = resolve38(dir, e.name);
12220
12890
  let looksLikeProject = false;
12221
12891
  for (const marker of PROJECT_MARKERS) {
12222
- if (await fileExists(resolve35(projectDir, marker))) {
12892
+ if (await fileExists(resolve38(projectDir, marker))) {
12223
12893
  looksLikeProject = true;
12224
12894
  break;
12225
12895
  }
@@ -12238,7 +12908,7 @@ var requiredFiles = {
12238
12908
  for (const projectDir of projects) {
12239
12909
  const missing = [];
12240
12910
  for (const rel of REQUIRED_PROJECT_FILES) {
12241
- const p = resolve35(projectDir, rel);
12911
+ const p = resolve38(projectDir, rel);
12242
12912
  if (!await fileExists(p)) missing.push(rel);
12243
12913
  }
12244
12914
  if (missing.length === 0) continue;
@@ -12248,7 +12918,7 @@ var requiredFiles = {
12248
12918
  title: this.title,
12249
12919
  status: "error",
12250
12920
  detail: `project at ${projectDir} is missing: ${missing.join(", ")}`,
12251
- affected: missing.map((m) => resolve35(projectDir, m)),
12921
+ affected: missing.map((m) => resolve38(projectDir, m)),
12252
12922
  remediation: {
12253
12923
  kind: "manual",
12254
12924
  suggestion: "Recreate the missing scaffold files from templates",
@@ -12271,9 +12941,9 @@ var manifestStale = {
12271
12941
  const projects = await listProjects2(ctx);
12272
12942
  const results = [];
12273
12943
  for (const projectDir of projects) {
12274
- const manifestPath = resolve35(projectDir, "manifest.md");
12944
+ const manifestPath = resolve38(projectDir, "manifest.md");
12275
12945
  if (!await fileExists(manifestPath)) continue;
12276
- const manifestMtime = (await stat4(manifestPath)).mtimeMs;
12946
+ const manifestMtime = (await stat6(manifestPath)).mtimeMs;
12277
12947
  const newestAssignment = await newestAssignmentMtime(projectDir);
12278
12948
  if (newestAssignment === 0) continue;
12279
12949
  if (newestAssignment > manifestMtime) {
@@ -12305,7 +12975,7 @@ var orphanFiles = {
12305
12975
  const projects = await listProjects2(ctx);
12306
12976
  const results = [];
12307
12977
  for (const projectDir of projects) {
12308
- const entries = await readdir13(projectDir, { withFileTypes: true });
12978
+ const entries = await readdir14(projectDir, { withFileTypes: true });
12309
12979
  const orphans = [];
12310
12980
  for (const e of entries) {
12311
12981
  if (e.name.startsWith(".")) continue;
@@ -12320,7 +12990,7 @@ var orphanFiles = {
12320
12990
  title: this.title,
12321
12991
  status: "warn",
12322
12992
  detail: `project at ${projectDir} has unexpected entries: ${orphans.join(", ")}`,
12323
- affected: orphans.map((o) => resolve35(projectDir, o)),
12993
+ affected: orphans.map((o) => resolve38(projectDir, o)),
12324
12994
  autoFixable: false
12325
12995
  });
12326
12996
  }
@@ -12330,20 +13000,20 @@ var orphanFiles = {
12330
13000
  };
12331
13001
  var projectChecks = [requiredFiles, manifestStale, orphanFiles];
12332
13002
  async function newestAssignmentMtime(projectDir) {
12333
- const assignmentsRoot = resolve35(projectDir, "assignments");
13003
+ const assignmentsRoot = resolve38(projectDir, "assignments");
12334
13004
  if (!await fileExists(assignmentsRoot)) return 0;
12335
13005
  let newest = 0;
12336
13006
  let entries;
12337
13007
  try {
12338
- entries = await readdir13(assignmentsRoot, { withFileTypes: true });
13008
+ entries = await readdir14(assignmentsRoot, { withFileTypes: true });
12339
13009
  } catch {
12340
13010
  return 0;
12341
13011
  }
12342
13012
  for (const e of entries) {
12343
13013
  if (!e.isDirectory()) continue;
12344
- const assignmentMd = resolve35(assignmentsRoot, e.name, "assignment.md");
13014
+ const assignmentMd = resolve38(assignmentsRoot, e.name, "assignment.md");
12345
13015
  try {
12346
- const s = await stat4(assignmentMd);
13016
+ const s = await stat6(assignmentMd);
12347
13017
  if (s.mtimeMs > newest) newest = s.mtimeMs;
12348
13018
  } catch {
12349
13019
  }
@@ -12365,28 +13035,28 @@ init_fs();
12365
13035
  init_parser();
12366
13036
  init_types();
12367
13037
  init_paths();
12368
- import { resolve as resolve36 } from "path";
12369
- import { readdir as readdir14, readFile as readFile19 } from "fs/promises";
13038
+ import { resolve as resolve39 } from "path";
13039
+ import { readdir as readdir15, readFile as readFile22 } from "fs/promises";
12370
13040
  var CATEGORY4 = "assignment";
12371
13041
  var STATUSES_REQUIRING_HANDOFF = /* @__PURE__ */ new Set(["review", "completed"]);
12372
13042
  async function listAssignments(ctx) {
12373
13043
  const result = { withAssignmentMd: [], orphanFolders: [] };
12374
13044
  const projectsDir2 = ctx.config.defaultProjectDir;
12375
13045
  if (await fileExists(projectsDir2)) {
12376
- const projects = await readdir14(projectsDir2, { withFileTypes: true });
13046
+ const projects = await readdir15(projectsDir2, { withFileTypes: true });
12377
13047
  for (const m of projects) {
12378
13048
  if (!m.isDirectory()) continue;
12379
13049
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
12380
- const assignmentsDir2 = resolve36(projectsDir2, m.name, "assignments");
13050
+ const assignmentsDir2 = resolve39(projectsDir2, m.name, "assignments");
12381
13051
  if (!await fileExists(assignmentsDir2)) continue;
12382
- const entries = await readdir14(assignmentsDir2, { withFileTypes: true });
13052
+ const entries = await readdir15(assignmentsDir2, { withFileTypes: true });
12383
13053
  for (const a of entries) {
12384
13054
  if (!a.isDirectory()) continue;
12385
13055
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
12386
- const assignmentDir = resolve36(assignmentsDir2, a.name);
12387
- const assignmentMd = resolve36(assignmentDir, "assignment.md");
13056
+ const assignmentDir = resolve39(assignmentsDir2, a.name);
13057
+ const assignmentMd = resolve39(assignmentDir, "assignment.md");
12388
13058
  const entry = {
12389
- projectDir: resolve36(projectsDir2, m.name),
13059
+ projectDir: resolve39(projectsDir2, m.name),
12390
13060
  projectSlug: m.name,
12391
13061
  assignmentDir,
12392
13062
  assignmentSlug: a.name,
@@ -12402,12 +13072,12 @@ async function listAssignments(ctx) {
12402
13072
  }
12403
13073
  const standaloneRoot = assignmentsDir();
12404
13074
  if (await fileExists(standaloneRoot)) {
12405
- const entries = await readdir14(standaloneRoot, { withFileTypes: true });
13075
+ const entries = await readdir15(standaloneRoot, { withFileTypes: true });
12406
13076
  for (const a of entries) {
12407
13077
  if (!a.isDirectory()) continue;
12408
13078
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
12409
- const assignmentDir = resolve36(standaloneRoot, a.name);
12410
- const assignmentMd = resolve36(assignmentDir, "assignment.md");
13079
+ const assignmentDir = resolve39(standaloneRoot, a.name);
13080
+ const assignmentMd = resolve39(assignmentDir, "assignment.md");
12411
13081
  const entry = {
12412
13082
  projectDir: standaloneRoot,
12413
13083
  projectSlug: null,
@@ -12485,7 +13155,7 @@ var invalidStatus = {
12485
13155
  const allowed = configuredStatuses(ctx);
12486
13156
  const results = [];
12487
13157
  for (const a of withAssignmentMd) {
12488
- const path = resolve36(a.assignmentDir, "assignment.md");
13158
+ const path = resolve39(a.assignmentDir, "assignment.md");
12489
13159
  const parsed = await parseSafe(path);
12490
13160
  if (!parsed) continue;
12491
13161
  if (!allowed.has(parsed.status)) {
@@ -12518,7 +13188,7 @@ var workspaceMissing = {
12518
13188
  const terminal = terminalStatuses(ctx);
12519
13189
  const results = [];
12520
13190
  for (const a of withAssignmentMd) {
12521
- const path = resolve36(a.assignmentDir, "assignment.md");
13191
+ const path = resolve39(a.assignmentDir, "assignment.md");
12522
13192
  const parsed = await parseSafe(path);
12523
13193
  if (!parsed) continue;
12524
13194
  if (terminal.has(parsed.status)) continue;
@@ -12565,12 +13235,12 @@ var requiredFilesByStatus = {
12565
13235
  const { withAssignmentMd } = await listAssignments(ctx);
12566
13236
  const results = [];
12567
13237
  for (const a of withAssignmentMd) {
12568
- const assignmentPath = resolve36(a.assignmentDir, "assignment.md");
13238
+ const assignmentPath = resolve39(a.assignmentDir, "assignment.md");
12569
13239
  const parsed = await parseSafe(assignmentPath);
12570
13240
  if (!parsed) continue;
12571
13241
  const missing = [];
12572
13242
  if (STATUSES_REQUIRING_HANDOFF.has(parsed.status)) {
12573
- const handoffPath = resolve36(a.assignmentDir, "handoff.md");
13243
+ const handoffPath = resolve39(a.assignmentDir, "handoff.md");
12574
13244
  if (!await fileExists(handoffPath)) missing.push("handoff.md");
12575
13245
  }
12576
13246
  if (missing.length === 0) continue;
@@ -12580,7 +13250,7 @@ var requiredFilesByStatus = {
12580
13250
  title: this.title,
12581
13251
  status: "warn",
12582
13252
  detail: `${a.projectSlug}/${a.assignmentSlug} (status: ${parsed.status}) is missing ${missing.join(", ")}`,
12583
- affected: missing.map((m) => resolve36(a.assignmentDir, m)),
13253
+ affected: missing.map((m) => resolve39(a.assignmentDir, m)),
12584
13254
  remediation: {
12585
13255
  kind: "manual",
12586
13256
  suggestion: `Create the missing ${missing.join(" and ")} files for this assignment`,
@@ -12603,7 +13273,7 @@ var companionFilesScaffolded = {
12603
13273
  for (const a of withAssignmentMd) {
12604
13274
  const missing = [];
12605
13275
  for (const filename of ["progress.md", "comments.md"]) {
12606
- if (!await fileExists(resolve36(a.assignmentDir, filename))) {
13276
+ if (!await fileExists(resolve39(a.assignmentDir, filename))) {
12607
13277
  missing.push(filename);
12608
13278
  }
12609
13279
  }
@@ -12615,7 +13285,7 @@ var companionFilesScaffolded = {
12615
13285
  title: this.title,
12616
13286
  status: "warn",
12617
13287
  detail: `${label} is missing ${missing.join(" and ")} (pre-v2.0 assignment \u2014 not required, but scaffolding them keeps the dashboard and CLIs consistent)`,
12618
- affected: missing.map((m) => resolve36(a.assignmentDir, m)),
13288
+ affected: missing.map((m) => resolve39(a.assignmentDir, m)),
12619
13289
  remediation: {
12620
13290
  kind: "manual",
12621
13291
  suggestion: `Create ${missing.join(" and ")} with the renderProgress/renderComments templates, or re-scaffold via the CLI`,
@@ -12648,7 +13318,7 @@ var typeDefinition = {
12648
13318
  const { withAssignmentMd } = await listAssignments(ctx);
12649
13319
  const results = [];
12650
13320
  for (const a of withAssignmentMd) {
12651
- const path = resolve36(a.assignmentDir, "assignment.md");
13321
+ const path = resolve39(a.assignmentDir, "assignment.md");
12652
13322
  const parsed = await parseSafe(path);
12653
13323
  if (!parsed) continue;
12654
13324
  if (!parsed.type) continue;
@@ -12682,7 +13352,7 @@ var projectFrontmatterMatchesContainer = {
12682
13352
  const { withAssignmentMd } = await listAssignments(ctx);
12683
13353
  const results = [];
12684
13354
  for (const a of withAssignmentMd) {
12685
- const path = resolve36(a.assignmentDir, "assignment.md");
13355
+ const path = resolve39(a.assignmentDir, "assignment.md");
12686
13356
  const parsed = await parseSafe(path);
12687
13357
  if (!parsed) continue;
12688
13358
  if (a.standalone) {
@@ -12737,7 +13407,7 @@ var assignmentChecks = [
12737
13407
  ];
12738
13408
  async function parseSafe(path) {
12739
13409
  try {
12740
- const content = await readFile19(path, "utf-8");
13410
+ const content = await readFile22(path, "utf-8");
12741
13411
  return parseAssignmentFull(content);
12742
13412
  } catch {
12743
13413
  return null;
@@ -12756,7 +13426,7 @@ function pass4(check, detail) {
12756
13426
 
12757
13427
  // src/utils/doctor/checks/dashboard.ts
12758
13428
  init_fs();
12759
- import { resolve as resolve37 } from "path";
13429
+ import { resolve as resolve40 } from "path";
12760
13430
  var CATEGORY5 = "dashboard";
12761
13431
  var dbReachable = {
12762
13432
  id: "dashboard.db-reachable",
@@ -12770,7 +13440,7 @@ var dbReachable = {
12770
13440
  title: this.title,
12771
13441
  status: "error",
12772
13442
  detail: `could not open syntaur.db: ${ctx.dbError ?? "unknown error"}`,
12773
- affected: [resolve37(ctx.syntaurRoot, "syntaur.db")],
13443
+ affected: [resolve40(ctx.syntaurRoot, "syntaur.db")],
12774
13444
  remediation: {
12775
13445
  kind: "manual",
12776
13446
  suggestion: "Start the dashboard once (`syntaur dashboard`) to initialize the DB, or restore it from backup",
@@ -12788,7 +13458,7 @@ var dbReachable = {
12788
13458
  title: this.title,
12789
13459
  status: "error",
12790
13460
  detail: 'syntaur.db is missing the expected "sessions" table',
12791
- affected: [resolve37(ctx.syntaurRoot, "syntaur.db")],
13461
+ affected: [resolve40(ctx.syntaurRoot, "syntaur.db")],
12792
13462
  autoFixable: false
12793
13463
  };
12794
13464
  }
@@ -12800,7 +13470,7 @@ var dbReachable = {
12800
13470
  title: this.title,
12801
13471
  status: "error",
12802
13472
  detail: `syntaur.db query failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
12803
- affected: [resolve37(ctx.syntaurRoot, "syntaur.db")],
13473
+ affected: [resolve40(ctx.syntaurRoot, "syntaur.db")],
12804
13474
  autoFixable: false
12805
13475
  };
12806
13476
  }
@@ -12826,7 +13496,7 @@ var ghostSessions = {
12826
13496
  const results = [];
12827
13497
  for (const row of rows) {
12828
13498
  if (!row.project_slug) continue;
12829
- const projectPath = resolve37(projectsDir2, row.project_slug, "project.md");
13499
+ const projectPath = resolve40(projectsDir2, row.project_slug, "project.md");
12830
13500
  if (!await fileExists(projectPath)) {
12831
13501
  results.push({
12832
13502
  id: this.id,
@@ -12845,7 +13515,7 @@ var ghostSessions = {
12845
13515
  continue;
12846
13516
  }
12847
13517
  if (row.assignment_slug) {
12848
- const assignmentPath = resolve37(
13518
+ const assignmentPath = resolve40(
12849
13519
  projectsDir2,
12850
13520
  row.project_slug,
12851
13521
  "assignments",
@@ -12897,7 +13567,7 @@ function skipped(check, reason) {
12897
13567
 
12898
13568
  // src/utils/doctor/checks/integrations.ts
12899
13569
  init_fs();
12900
- import { readdir as readdir15 } from "fs/promises";
13570
+ import { readdir as readdir16 } from "fs/promises";
12901
13571
  var CATEGORY6 = "integrations";
12902
13572
  var claudePluginLinked = {
12903
13573
  id: "integrations.claude-plugin-linked",
@@ -12959,7 +13629,7 @@ var backupConfigured = {
12959
13629
  if (ctx.config.backup?.repo) return pass6(this);
12960
13630
  const projectsDir2 = ctx.config.defaultProjectDir;
12961
13631
  if (!await fileExists(projectsDir2)) return skipped2(this, "no projects dir");
12962
- const entries = await readdir15(projectsDir2, { withFileTypes: true });
13632
+ const entries = await readdir16(projectsDir2, { withFileTypes: true });
12963
13633
  const hasProjects = entries.some((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_"));
12964
13634
  if (!hasProjects) return skipped2(this, "no projects yet");
12965
13635
  return {
@@ -13002,8 +13672,8 @@ function skipped2(check, reason) {
13002
13672
  init_fs();
13003
13673
  init_parser();
13004
13674
  init_types();
13005
- import { resolve as resolve38 } from "path";
13006
- import { readFile as readFile20 } from "fs/promises";
13675
+ import { resolve as resolve41 } from "path";
13676
+ import { readFile as readFile23 } from "fs/promises";
13007
13677
  var CATEGORY7 = "workspace";
13008
13678
  var ASSIGNMENT_FIELDS = ["projectSlug", "assignmentSlug", "projectDir", "assignmentDir"];
13009
13679
  function hasAnyAssignmentField(ctx) {
@@ -13015,12 +13685,12 @@ function isStandaloneSession(ctx) {
13015
13685
  return !hasAnyAssignmentField(ctx) && typeof ctx.sessionId === "string" && ctx.sessionId.length > 0;
13016
13686
  }
13017
13687
  async function loadContext(ctx) {
13018
- const path = resolve38(ctx.cwd, ".syntaur", "context.json");
13688
+ const path = resolve41(ctx.cwd, ".syntaur", "context.json");
13019
13689
  if (!await fileExists(path)) {
13020
13690
  return { data: null, path, exists: false, parseError: null };
13021
13691
  }
13022
13692
  try {
13023
- const raw = await readFile20(path, "utf-8");
13693
+ const raw = await readFile23(path, "utf-8");
13024
13694
  return { data: JSON.parse(raw), path, exists: true, parseError: null };
13025
13695
  } catch (err2) {
13026
13696
  return {
@@ -13095,7 +13765,7 @@ var contextAssignmentResolves = {
13095
13765
  if (!exists) return skipped3(this, "no context to resolve");
13096
13766
  if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to resolve");
13097
13767
  if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
13098
- const assignmentMd = resolve38(data.assignmentDir, "assignment.md");
13768
+ const assignmentMd = resolve41(data.assignmentDir, "assignment.md");
13099
13769
  if (!await fileExists(assignmentMd)) {
13100
13770
  return {
13101
13771
  id: this.id,
@@ -13124,10 +13794,10 @@ var contextTerminal = {
13124
13794
  if (!exists) return skipped3(this, "no context to check");
13125
13795
  if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to check");
13126
13796
  if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
13127
- const assignmentMd = resolve38(data.assignmentDir, "assignment.md");
13797
+ const assignmentMd = resolve41(data.assignmentDir, "assignment.md");
13128
13798
  if (!await fileExists(assignmentMd)) return skipped3(this, "assignment file missing");
13129
13799
  try {
13130
- const content = await readFile20(assignmentMd, "utf-8");
13800
+ const content = await readFile23(assignmentMd, "utf-8");
13131
13801
  const parsed = parseAssignmentFull(content);
13132
13802
  const terminal = terminalStatuses2(ctx);
13133
13803
  if (terminal.has(parsed.status)) {
@@ -13270,15 +13940,15 @@ async function finalize(checks) {
13270
13940
  }
13271
13941
  async function readVersion() {
13272
13942
  try {
13273
- const here = fileURLToPath5(import.meta.url);
13274
- let dir = dirname8(here);
13943
+ const here = fileURLToPath7(import.meta.url);
13944
+ let dir = dirname11(here);
13275
13945
  for (let i = 0; i < 6; i++) {
13276
13946
  try {
13277
- const raw = await readFile21(join4(dir, "package.json"), "utf-8");
13947
+ const raw = await readFile24(join5(dir, "package.json"), "utf-8");
13278
13948
  const parsed = JSON.parse(raw);
13279
13949
  return typeof parsed.version === "string" ? parsed.version : null;
13280
13950
  } catch {
13281
- dir = dirname8(dir);
13951
+ dir = dirname11(dir);
13282
13952
  }
13283
13953
  }
13284
13954
  return null;
@@ -13411,8 +14081,8 @@ var doctorCommand = new Command3("doctor").description("Diagnose Syntaur state a
13411
14081
  init_paths();
13412
14082
  init_fs();
13413
14083
  init_config2();
13414
- import { resolve as resolve39 } from "path";
13415
- import { readFile as readFile22 } from "fs/promises";
14084
+ import { resolve as resolve42 } from "path";
14085
+ import { readFile as readFile25 } from "fs/promises";
13416
14086
  init_timestamp();
13417
14087
  init_assignment_resolver();
13418
14088
  function shortId() {
@@ -13444,7 +14114,7 @@ async function commentCommand(target, text, options = {}) {
13444
14114
  if (!isValidSlug(target)) {
13445
14115
  throw new Error(`Invalid assignment slug "${target}".`);
13446
14116
  }
13447
- assignmentDir = resolve39(baseDir, options.project, "assignments", target);
14117
+ assignmentDir = resolve42(baseDir, options.project, "assignments", target);
13448
14118
  assignmentRef = target;
13449
14119
  } else {
13450
14120
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -13454,13 +14124,13 @@ async function commentCommand(target, text, options = {}) {
13454
14124
  assignmentDir = resolved.assignmentDir;
13455
14125
  assignmentRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
13456
14126
  }
13457
- const commentsPath = resolve39(assignmentDir, "comments.md");
14127
+ const commentsPath = resolve42(assignmentDir, "comments.md");
13458
14128
  const timestamp = nowTimestamp();
13459
14129
  const author = options.author ?? process.env.USER ?? "unknown";
13460
14130
  let currentContent;
13461
14131
  let currentCount = 0;
13462
14132
  if (await fileExists(commentsPath)) {
13463
- currentContent = await readFile22(commentsPath, "utf-8");
14133
+ currentContent = await readFile25(commentsPath, "utf-8");
13464
14134
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
13465
14135
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
13466
14136
  } else {
@@ -13497,8 +14167,8 @@ ${entry}`;
13497
14167
  init_paths();
13498
14168
  init_fs();
13499
14169
  init_config2();
13500
- import { resolve as resolve40 } from "path";
13501
- import { readFile as readFile23 } from "fs/promises";
14170
+ import { resolve as resolve43 } from "path";
14171
+ import { readFile as readFile26 } from "fs/promises";
13502
14172
  init_timestamp();
13503
14173
  init_assignment_resolver();
13504
14174
  function setTopLevelField3(content, key, value) {
@@ -13523,7 +14193,7 @@ async function requestCommand(target, text, options = {}) {
13523
14193
  if (!isValidSlug(target)) {
13524
14194
  throw new Error(`Invalid assignment slug "${target}".`);
13525
14195
  }
13526
- assignmentDir = resolve40(baseDir, options.project, "assignments", target);
14196
+ assignmentDir = resolve43(baseDir, options.project, "assignments", target);
13527
14197
  targetRef = target;
13528
14198
  } else {
13529
14199
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -13533,12 +14203,12 @@ async function requestCommand(target, text, options = {}) {
13533
14203
  assignmentDir = resolved.assignmentDir;
13534
14204
  targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
13535
14205
  }
13536
- const assignmentMdPath = resolve40(assignmentDir, "assignment.md");
14206
+ const assignmentMdPath = resolve43(assignmentDir, "assignment.md");
13537
14207
  if (!await fileExists(assignmentMdPath)) {
13538
14208
  throw new Error(`assignment.md not found at ${assignmentMdPath}`);
13539
14209
  }
13540
14210
  const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
13541
- let content = await readFile23(assignmentMdPath, "utf-8");
14211
+ let content = await readFile26(assignmentMdPath, "utf-8");
13542
14212
  const todoLine = `- [ ] ${text.trim()} (from: ${source})`;
13543
14213
  const todosHeading = /^## Todos\s*$/m;
13544
14214
  if (todosHeading.test(content)) {
@@ -13566,10 +14236,10 @@ ${todoLine}
13566
14236
 
13567
14237
  // src/cli-default-command.ts
13568
14238
  init_config2();
13569
- import { readdir as readdir16 } from "fs/promises";
14239
+ import { readdir as readdir17 } from "fs/promises";
13570
14240
  async function hasAnyProjectContent(projectsDir2) {
13571
14241
  try {
13572
- const entries = await readdir16(projectsDir2, { withFileTypes: true });
14242
+ const entries = await readdir17(projectsDir2, { withFileTypes: true });
13573
14243
  return entries.some((entry) => entry.isDirectory());
13574
14244
  } catch {
13575
14245
  return false;
@@ -13605,21 +14275,21 @@ async function getDefaultCommandName() {
13605
14275
  // src/utils/npx-prompt.ts
13606
14276
  init_paths();
13607
14277
  init_fs();
13608
- import { fileURLToPath as fileURLToPath7 } from "url";
13609
- import { readFile as readFile25 } from "fs/promises";
13610
- import { dirname as dirname10, join as join6, resolve as resolve41 } from "path";
14278
+ import { fileURLToPath as fileURLToPath9 } from "url";
14279
+ import { readFile as readFile28 } from "fs/promises";
14280
+ import { dirname as dirname13, join as join7, resolve as resolve44 } from "path";
13611
14281
  import { spawn as spawn3 } from "child_process";
13612
14282
  import { createInterface as createInterface2 } from "readline/promises";
13613
14283
 
13614
14284
  // src/utils/version.ts
13615
- import { fileURLToPath as fileURLToPath6 } from "url";
13616
- import { readFile as readFile24 } from "fs/promises";
13617
- import { dirname as dirname9, join as join5 } from "path";
14285
+ import { fileURLToPath as fileURLToPath8 } from "url";
14286
+ import { readFile as readFile27 } from "fs/promises";
14287
+ import { dirname as dirname12, join as join6 } from "path";
13618
14288
  async function readPackageVersion(scriptUrl) {
13619
14289
  try {
13620
- const scriptPath = fileURLToPath6(scriptUrl);
13621
- const pkgRoot = dirname9(dirname9(scriptPath));
13622
- const raw = await readFile24(join5(pkgRoot, "package.json"), "utf-8");
14290
+ const scriptPath = fileURLToPath8(scriptUrl);
14291
+ const pkgRoot = dirname12(dirname12(scriptPath));
14292
+ const raw = await readFile27(join6(pkgRoot, "package.json"), "utf-8");
13623
14293
  const parsed = JSON.parse(raw);
13624
14294
  return typeof parsed.version === "string" ? parsed.version : null;
13625
14295
  } catch {
@@ -13628,13 +14298,13 @@ async function readPackageVersion(scriptUrl) {
13628
14298
  }
13629
14299
 
13630
14300
  // src/utils/npx-prompt.ts
13631
- var STATE_FILE = resolve41(syntaurRoot(), "npx-install.json");
14301
+ var STATE_FILE = resolve44(syntaurRoot(), "npx-install.json");
13632
14302
  var META_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version", "help"]);
13633
14303
  var GLOBAL_VERSION_TIMEOUT_MS = 2e3;
13634
14304
  function isRunningViaNpx(scriptUrl) {
13635
14305
  let scriptPath;
13636
14306
  try {
13637
- scriptPath = fileURLToPath7(scriptUrl);
14307
+ scriptPath = fileURLToPath9(scriptUrl);
13638
14308
  } catch {
13639
14309
  return false;
13640
14310
  }
@@ -13649,7 +14319,7 @@ function isRunningViaNpx(scriptUrl) {
13649
14319
  async function readState() {
13650
14320
  if (!await fileExists(STATE_FILE)) return null;
13651
14321
  try {
13652
- const raw = await readFile25(STATE_FILE, "utf-8");
14322
+ const raw = await readFile28(STATE_FILE, "utf-8");
13653
14323
  return JSON.parse(raw);
13654
14324
  } catch {
13655
14325
  return null;
@@ -13660,10 +14330,10 @@ async function writeState(state) {
13660
14330
  `);
13661
14331
  }
13662
14332
  async function resolveNpmBin() {
13663
- const nodeDir = dirname10(process.execPath);
14333
+ const nodeDir = dirname13(process.execPath);
13664
14334
  const isWin = process.platform === "win32";
13665
14335
  const npmName = isWin ? "npm.cmd" : "npm";
13666
- const nearNode = join6(nodeDir, npmName);
14336
+ const nearNode = join7(nodeDir, npmName);
13667
14337
  if (await fileExists(nearNode)) {
13668
14338
  return { cmd: nearNode, shell: false };
13669
14339
  }
@@ -13706,9 +14376,9 @@ async function readGlobalVersion() {
13706
14376
  });
13707
14377
  if (!rootPath) return null;
13708
14378
  try {
13709
- const manifestPath = join6(rootPath, "syntaur", "package.json");
14379
+ const manifestPath = join7(rootPath, "syntaur", "package.json");
13710
14380
  if (!await fileExists(manifestPath)) return null;
13711
- const raw = await readFile25(manifestPath, "utf-8");
14381
+ const raw = await readFile28(manifestPath, "utf-8");
13712
14382
  const parsed = JSON.parse(raw);
13713
14383
  return typeof parsed.version === "string" ? parsed.version : null;
13714
14384
  } catch {
@@ -14003,7 +14673,7 @@ program.command("setup").description("Initialize Syntaur and optionally install
14003
14673
  process.exit(1);
14004
14674
  }
14005
14675
  });
14006
- program.command("install-plugin").description("Install the Syntaur Claude Code plugin").option("--force", "Overwrite an existing Syntaur-managed install").option("--target-dir <path>", "Install the plugin at a specific directory").option("--link", "Use a symlink instead of copying files (repo-local dev only)").action(async (options) => {
14676
+ program.command("install-plugin").description("Install the Syntaur Claude Code plugin").option("--force", "Overwrite an existing Syntaur-managed install").option("--target-dir <path>", "Install the plugin at a specific directory").option("--link", "Use a symlink instead of copying files (repo-local dev only)").option("--force-skills", "Overwrite user-edited skills in ~/.claude/skills").option("--skip-skills", "Do not install protocol skills into ~/.claude/skills").action(async (options) => {
14007
14677
  try {
14008
14678
  await installPluginCommand({ ...options, promptForTarget: true });
14009
14679
  } catch (error) {
@@ -14014,7 +14684,61 @@ program.command("install-plugin").description("Install the Syntaur Claude Code p
14014
14684
  process.exit(1);
14015
14685
  }
14016
14686
  });
14017
- program.command("install-codex-plugin").description("Install the Syntaur Codex plugin and marketplace entry").option("--force", "Overwrite an existing Syntaur-managed install").option("--target-dir <path>", "Install the plugin at a specific directory").option("--marketplace-path <path>", "Write the marketplace entry to a specific file").option("--link", "Use a symlink instead of copying files (repo-local dev only)").action(async (options) => {
14687
+ program.command("install-statusline").description(
14688
+ "Install the syntaur statusLine for Claude Code. Augments ~/.claude/settings.json; wraps any existing script by default."
14689
+ ).option("--mode <mode>", "replace | wrap | skip | ask (default: ask, wrap in non-TTY)", "ask").option("--link", "Symlink the installed script to the package source (dev mode)").action(async (options) => {
14690
+ try {
14691
+ const rawMode = (options.mode ?? "ask").toLowerCase();
14692
+ const valid = ["replace", "wrap", "skip", "ask"];
14693
+ if (!valid.includes(rawMode)) {
14694
+ throw new Error(
14695
+ `Invalid --mode "${rawMode}". Must be one of: ${valid.join(", ")}.`
14696
+ );
14697
+ }
14698
+ await installStatuslineCommand({
14699
+ mode: rawMode,
14700
+ link: options.link
14701
+ });
14702
+ } catch (error) {
14703
+ console.error("Error:", error instanceof Error ? error.message : String(error));
14704
+ process.exit(1);
14705
+ }
14706
+ });
14707
+ program.command("uninstall-statusline").description(
14708
+ "Remove the syntaur statusLine. Restores the previously configured command from backup if present."
14709
+ ).option("--keep-script", "Leave ~/.syntaur/statusline.sh on disk (only edit settings.json)").action(async (options) => {
14710
+ try {
14711
+ await uninstallStatuslineCommand({ keepScript: options.keepScript });
14712
+ } catch (error) {
14713
+ console.error("Error:", error instanceof Error ? error.message : String(error));
14714
+ process.exit(1);
14715
+ }
14716
+ });
14717
+ program.command("configure-statusline").description(
14718
+ "Configure which segments (git, assignment, session, model, ctx, cwd, wrap) appear in the syntaur statusLine and in what order."
14719
+ ).option(
14720
+ "--preset <name>",
14721
+ `Preset shortcut. Choices: ${Object.keys(PRESETS).join(", ")}.`
14722
+ ).option(
14723
+ "--segments <list>",
14724
+ 'Comma-separated segment list, e.g. "git,assignment,session,model,ctx".'
14725
+ ).option("--separator <string>", 'Segment separator (default " \xB7 ")').option("--wrap <path>", 'Path to an external statusline script to compose as a "wrap" segment').option("--preview", "Print the resolved config and a preview line without writing").action(async (options) => {
14726
+ try {
14727
+ await configureStatuslineCommand(options);
14728
+ } catch (error) {
14729
+ console.error("Error:", error instanceof Error ? error.message : String(error));
14730
+ process.exit(1);
14731
+ }
14732
+ });
14733
+ program.command("uninstall-skills").description("Remove Syntaur protocol skills from ~/.claude/skills and/or ~/.codex/skills").option("--claude", "Remove from ~/.claude/skills").option("--codex", "Remove from ~/.codex/skills").option("--all", "Remove from both").action(async (options) => {
14734
+ try {
14735
+ await uninstallSkillsCommand(options);
14736
+ } catch (error) {
14737
+ console.error("Error:", error instanceof Error ? error.message : String(error));
14738
+ process.exit(1);
14739
+ }
14740
+ });
14741
+ program.command("install-codex-plugin").description("Install the Syntaur Codex plugin and marketplace entry").option("--force", "Overwrite an existing Syntaur-managed install").option("--target-dir <path>", "Install the plugin at a specific directory").option("--marketplace-path <path>", "Write the marketplace entry to a specific file").option("--link", "Use a symlink instead of copying files (repo-local dev only)").option("--force-skills", "Overwrite user-edited skills in ~/.codex/skills").option("--skip-skills", "Do not install protocol skills into ~/.codex/skills").action(async (options) => {
14018
14742
  try {
14019
14743
  await installCodexPluginCommand({ ...options, promptForTarget: true });
14020
14744
  } catch (error) {