syntaur 0.5.0 → 0.5.2

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 (63) hide show
  1. package/dashboard/dist/assets/{_basePickBy-Bcut0btZ.js → _basePickBy-ij-Ukp6s.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-AQSP2JEk.js → _baseUniq-CZKk9gZR.js} +1 -1
  3. package/dashboard/dist/assets/{arc-BLTpY9lc.js → arc-C30UbJZB.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CJtwMY_X.js → architectureDiagram-2XIMDMQ5-BDVieGIr.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-Don-O7X7.js → blockDiagram-WCTKOSBZ-DZYY4t9w.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-C_M3yTTB.js → c4Diagram-IC4MRINW-B019MXol.js} +1 -1
  7. package/dashboard/dist/assets/channel-DH4gshIt.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-CGss0jXe.js → chunk-4BX2VUAB-DrkTwY15.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-BatoPJga.js → chunk-55IACEB6-mFTAE8DD.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-DxH4wO82.js → chunk-FMBD7UC4-VgDZaNoS.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-BL3izAFQ.js → chunk-JSJVCQXG-C_KXaq-c.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-GnqXwnge.js → chunk-KX2RTZJC-DI-P_pPL.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-gvCn4QMb.js → chunk-NQ4KR5QH-TgYAsxTk.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-CYGWogyi.js → chunk-QZHKN3VN-Drfv_VpM.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-D9mVTQ1F.js → chunk-WL4C6EOR-CpLwvo_U.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-emsfh8H4.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-emsfh8H4.js +1 -0
  18. package/dashboard/dist/assets/clone-gdeRwgBN.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CUWQCKt4.js → cose-bilkent-S5V4N54A-CkKtF37m.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-CH3ijEvV.js → dagre-KLK3FWXG-BBlY_FL3.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-sq83lpV1.js → diagram-E7M64L7V-DLsFDLHm.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-BzQG_rtq.js → diagram-IFDJBPK2--sb7diMG.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-Dg0eZn0q.js → diagram-P4PSJMXO-D2LuEWVt.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-4b9eQ0uj.js → erDiagram-INFDFZHY-C1BEeili.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-C9fzKcsZ.js → flowDiagram-PKNHOUZH-BpbapQbU.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-Bzt6i9SH.js → ganttDiagram-A5KZAMGK-Io60qUuG.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-D0wFOagh.js → gitGraphDiagram-K3NZZRJ6-oemlGgRh.js} +1 -1
  28. package/dashboard/dist/assets/{graph-EEIGvqDh.js → graph-BZb-lGfH.js} +1 -1
  29. package/dashboard/dist/assets/index-BSVCsfvM.css +1 -0
  30. package/dashboard/dist/assets/index-CXWVuGs-.js +481 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DLYMsj1D.js → infoDiagram-LFFYTUFH-Ca4mwnZF.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DVebKkzl.js → ishikawaDiagram-PHBUUO56-9zuQ8y8W.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-BsmgOWVw.js → journeyDiagram-4ABVD52K-OdeeOdMx.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-BTnHf0ey.js → kanban-definition-K7BYSVSG-Cie4JtFn.js} +1 -1
  35. package/dashboard/dist/assets/{layout-BbM7HRvv.js → layout-Bmx2mvFv.js} +1 -1
  36. package/dashboard/dist/assets/{linear-C37bJKPO.js → linear-CW6K_-MX.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-MZ_JgnRL.js → mermaid.core-DmfO6BgK.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-CgHS4hFo.js → mindmap-definition-YRQLILUH-L6b3vG79.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CmAgopJe.js → pieDiagram-SKSYHLDU-CkHTCIWg.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BvzYUPR6.js → quadrantDiagram-337W2JSQ-B9MqhhIC.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-Bs52VP7k.js → requirementDiagram-Z7DCOOCP-CyHAfXCK.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-aXvGPR1o.js → sankeyDiagram-WA2Y5GQK-DHNzGGyE.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CzgcfU6K.js → sequenceDiagram-2WXFIKYE-BVvcJkrx.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-BXBJf9Hq.js → stateDiagram-RAJIS63D-CZ2cknh7.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-C4CPervD.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BsXp26Ai.js → timeline-definition-YZTLITO2-BXUtlVyd.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-C3WbDii1.js → treemap-KZPCXAKY-Dgi-hMKM.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-B28LMHWd.js → vennDiagram-LZ73GAT5-C9zGrrUQ.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-C3Xwz8mS.js → xychartDiagram-JWTSCODW-Dq71BUtc.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dashboard/dist/syntaur-logo.svg +14 -0
  52. package/dist/dashboard/server.js +335 -8
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +1256 -131
  55. package/dist/index.js.map +1 -1
  56. package/package.json +1 -1
  57. package/dashboard/dist/assets/channel-BfXmPwE5.js +0 -1
  58. package/dashboard/dist/assets/classDiagram-VBA2DB6C-D7_G1qy0.js +0 -1
  59. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-D7_G1qy0.js +0 -1
  60. package/dashboard/dist/assets/clone-BKG-N796.js +0 -1
  61. package/dashboard/dist/assets/index-Bu6ma6my.css +0 -1
  62. package/dashboard/dist/assets/index-C7f0ySJE.js +0 -481
  63. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-QqOtsuOs.js +0 -1
package/dist/index.js CHANGED
@@ -555,6 +555,53 @@ var init_fs_migration = __esm({
555
555
  // src/utils/config.ts
556
556
  import { readFile as readFile2 } from "fs/promises";
557
557
  import { resolve as resolve3, isAbsolute } from "path";
558
+ function parseAgentCommand(value, agentId) {
559
+ if (typeof value !== "string" || value.trim() === "") {
560
+ throw new AgentConfigError(
561
+ `agent${agentId ? ` "${agentId}"` : ""} has empty command`
562
+ );
563
+ }
564
+ const expanded = expandHome(value.trim());
565
+ if (isAbsolute(expanded)) {
566
+ return resolve3(expanded);
567
+ }
568
+ if (expanded.includes("/")) {
569
+ throw new AgentConfigError(
570
+ `agent${agentId ? ` "${agentId}"` : ""} command "${value}" is a relative path \u2014 use an absolute path or a bare binary name`
571
+ );
572
+ }
573
+ return expanded;
574
+ }
575
+ function validateAgentList(agents) {
576
+ const seen = /* @__PURE__ */ new Set();
577
+ let defaults = 0;
578
+ for (const agent of agents) {
579
+ if (!AGENT_ID_PATTERN.test(agent.id)) {
580
+ throw new AgentConfigError(
581
+ `agent id "${agent.id}" is invalid \u2014 must match /^[a-z0-9][a-z0-9_-]*$/`
582
+ );
583
+ }
584
+ if (seen.has(agent.id)) {
585
+ throw new AgentConfigError(`duplicate agent id "${agent.id}"`);
586
+ }
587
+ seen.add(agent.id);
588
+ if (!agent.label || agent.label.trim() === "") {
589
+ throw new AgentConfigError(`agent "${agent.id}" has empty label`);
590
+ }
591
+ parseAgentCommand(agent.command, agent.id);
592
+ if (agent.promptArgPosition !== void 0 && !PROMPT_ARG_POSITIONS.includes(agent.promptArgPosition)) {
593
+ throw new AgentConfigError(
594
+ `agent "${agent.id}" has invalid promptArgPosition "${agent.promptArgPosition}" \u2014 expected first|last|none`
595
+ );
596
+ }
597
+ if (agent.default) defaults++;
598
+ }
599
+ if (defaults > 1) {
600
+ throw new AgentConfigError(
601
+ `more than one agent is marked default: true (only one is allowed)`
602
+ );
603
+ }
604
+ }
558
605
  function cloneDefaultConfig() {
559
606
  return {
560
607
  ...DEFAULT_CONFIG,
@@ -571,9 +618,14 @@ function cloneDefaultConfig() {
571
618
  definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
572
619
  default: DEFAULT_CONFIG.types.default
573
620
  } : null,
621
+ agents: DEFAULT_CONFIG.agents ? DEFAULT_CONFIG.agents.map((a) => ({
622
+ ...a,
623
+ ...a.args ? { args: [...a.args] } : {}
624
+ })) : null,
574
625
  playbooks: {
575
626
  disabled: [...DEFAULT_CONFIG.playbooks.disabled]
576
- }
627
+ },
628
+ theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null
577
629
  };
578
630
  }
579
631
  function parseFrontmatter(content) {
@@ -823,6 +875,71 @@ ${normalizedFm}
823
875
  ---${afterFrontmatter}`;
824
876
  await writeFileForce(configPath, newContent);
825
877
  }
878
+ function parseThemeConfig(content) {
879
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
880
+ if (!match) return null;
881
+ const fmBlock = match[1];
882
+ const blockStart = fmBlock.match(/^theme:\s*$/m);
883
+ if (!blockStart) return null;
884
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
885
+ const remaining = fmBlock.slice(startIdx).split("\n");
886
+ let preset = null;
887
+ for (const line of remaining) {
888
+ const trimmed = line.trimStart();
889
+ const indent = line.length - trimmed.length;
890
+ if (indent === 0 && trimmed.length > 0) break;
891
+ if (trimmed === "") continue;
892
+ if (indent === 2 && trimmed.startsWith("preset:")) {
893
+ const value = trimmed.slice("preset:".length).trim().replace(/^["']|["']$/g, "");
894
+ if (value.length > 0) preset = value;
895
+ }
896
+ }
897
+ if (!preset) return null;
898
+ return { preset };
899
+ }
900
+ function serializeThemeConfig(theme) {
901
+ return ["theme:", ` preset: ${theme.preset}`].join("\n");
902
+ }
903
+ async function writeThemeConfig(theme) {
904
+ const configPath = resolve3(syntaurRoot(), "config.md");
905
+ const themeBlock = serializeThemeConfig(theme);
906
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
907
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
908
+ if (!fmMatch) {
909
+ const content = `---
910
+ version: "2.0"
911
+ defaultProjectDir: ${defaultProjectDir()}
912
+ ${themeBlock}
913
+ ---
914
+ ${existing}`;
915
+ await writeFileForce(configPath, content);
916
+ return;
917
+ }
918
+ const fmBlock = fmMatch[2];
919
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
920
+ const cleanedFm = stripTopLevelBlock(fmBlock, "theme");
921
+ const newFm = `${cleanedFm}
922
+ ${themeBlock}`.replace(/^\n+/, "");
923
+ const normalizedFm = newFm.replace(/\n+$/, "");
924
+ const newContent = `---
925
+ ${normalizedFm}
926
+ ---${afterFrontmatter}`;
927
+ await writeFileForce(configPath, newContent);
928
+ }
929
+ async function deleteThemeConfig() {
930
+ const configPath = resolve3(syntaurRoot(), "config.md");
931
+ if (!await fileExists(configPath)) return;
932
+ const existing = await readFile2(configPath, "utf-8");
933
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
934
+ if (!fmMatch) return;
935
+ const fmBlock = fmMatch[2];
936
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
937
+ const cleanedFm = stripTopLevelBlock(fmBlock, "theme");
938
+ const newContent = `---
939
+ ${cleanedFm}
940
+ ---${afterFrontmatter}`;
941
+ await writeFileForce(configPath, newContent);
942
+ }
826
943
  function stripTopLevelBlock(fmBlock, key) {
827
944
  const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
828
945
  if (!blockStart) {
@@ -859,6 +976,226 @@ function parseOptionalAbsolutePath(value, fieldName) {
859
976
  }
860
977
  return resolve3(expanded);
861
978
  }
979
+ function parseAgentsConfig(content) {
980
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
981
+ if (!match) return null;
982
+ const fmBlock = match[1];
983
+ const agentsStart = fmBlock.match(/^agents:\s*$/m);
984
+ if (!agentsStart) return null;
985
+ const startIdx = fmBlock.indexOf(agentsStart[0]) + agentsStart[0].length;
986
+ const remaining = fmBlock.slice(startIdx);
987
+ const lines = remaining.split("\n");
988
+ const agents = [];
989
+ let current = null;
990
+ let argsCapture = null;
991
+ let argsBaseIndent = 0;
992
+ function flushCurrent() {
993
+ if (!current) return;
994
+ if (!current.id || !current.command || !current.label) {
995
+ current = null;
996
+ return;
997
+ }
998
+ agents.push({
999
+ id: current.id,
1000
+ label: current.label,
1001
+ command: current.command,
1002
+ ...current.args && current.args.length > 0 ? { args: current.args } : {},
1003
+ ...current.promptArgPosition ? { promptArgPosition: current.promptArgPosition } : {},
1004
+ ...current.default ? { default: true } : {},
1005
+ ...current.resolveFromShellAliases ? { resolveFromShellAliases: true } : {}
1006
+ });
1007
+ current = null;
1008
+ argsCapture = null;
1009
+ }
1010
+ for (let i = 0; i < lines.length; i++) {
1011
+ const line = lines[i];
1012
+ const trimmed = line.trimStart();
1013
+ const indent = line.length - trimmed.length;
1014
+ if (indent === 0 && trimmed !== "" && !trimmed.startsWith("#")) {
1015
+ break;
1016
+ }
1017
+ if (argsCapture) {
1018
+ if (indent > argsBaseIndent && trimmed.startsWith("- ")) {
1019
+ argsCapture.push(decodeYamlScalar(trimmed.slice(2).trim()));
1020
+ continue;
1021
+ } else {
1022
+ argsCapture = null;
1023
+ if (current) current.args = current.args ?? [];
1024
+ }
1025
+ }
1026
+ if (indent === 2 && trimmed.startsWith("- ")) {
1027
+ flushCurrent();
1028
+ current = {};
1029
+ const rest = trimmed.slice(2).trim();
1030
+ const colonIdx = rest.indexOf(":");
1031
+ if (colonIdx > 0) {
1032
+ const k = rest.slice(0, colonIdx).trim();
1033
+ const v = rest.slice(colonIdx + 1).trim();
1034
+ assignAgentField(current, k, v);
1035
+ }
1036
+ continue;
1037
+ }
1038
+ if (indent >= 4 && current) {
1039
+ const colonIdx = trimmed.indexOf(":");
1040
+ if (colonIdx <= 0) continue;
1041
+ const k = trimmed.slice(0, colonIdx).trim();
1042
+ const v = trimmed.slice(colonIdx + 1).trim();
1043
+ if (k === "args" && v === "") {
1044
+ argsCapture = [];
1045
+ argsBaseIndent = indent;
1046
+ current.args = argsCapture;
1047
+ continue;
1048
+ }
1049
+ assignAgentField(current, k, v);
1050
+ }
1051
+ }
1052
+ flushCurrent();
1053
+ if (agents.length === 0) return [];
1054
+ return agents;
1055
+ }
1056
+ function normalizeAgentsFromConfig(agents) {
1057
+ if (agents === null) return null;
1058
+ try {
1059
+ const normalized = agents.map((agent) => ({
1060
+ ...agent,
1061
+ command: parseAgentCommand(agent.command, agent.id)
1062
+ }));
1063
+ validateAgentList(normalized);
1064
+ return normalized;
1065
+ } catch (err2) {
1066
+ const msg = err2 instanceof Error ? err2.message : String(err2);
1067
+ console.warn(
1068
+ `Warning: ~/.syntaur/config.md agents block is invalid (${msg}) \u2014 using built-in defaults`
1069
+ );
1070
+ return null;
1071
+ }
1072
+ }
1073
+ function decodeYamlScalar(value) {
1074
+ const trimmed = value.trim();
1075
+ if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
1076
+ const body = trimmed.slice(1, -1);
1077
+ let out = "";
1078
+ for (let i = 0; i < body.length; i++) {
1079
+ const ch = body[i];
1080
+ if (ch === "\\" && i + 1 < body.length) {
1081
+ const next = body[i + 1];
1082
+ switch (next) {
1083
+ case "\\":
1084
+ out += "\\";
1085
+ break;
1086
+ case '"':
1087
+ out += '"';
1088
+ break;
1089
+ case "n":
1090
+ out += "\n";
1091
+ break;
1092
+ case "t":
1093
+ out += " ";
1094
+ break;
1095
+ case "r":
1096
+ out += "\r";
1097
+ break;
1098
+ default:
1099
+ out += next;
1100
+ break;
1101
+ }
1102
+ i++;
1103
+ continue;
1104
+ }
1105
+ out += ch;
1106
+ }
1107
+ return out;
1108
+ }
1109
+ if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
1110
+ return trimmed.slice(1, -1).replace(/''/g, "'");
1111
+ }
1112
+ return trimmed;
1113
+ }
1114
+ function assignAgentField(target, key, rawValue) {
1115
+ const value = decodeYamlScalar(rawValue);
1116
+ switch (key) {
1117
+ case "id":
1118
+ target.id = value;
1119
+ break;
1120
+ case "label":
1121
+ target.label = value;
1122
+ break;
1123
+ case "command":
1124
+ target.command = value;
1125
+ break;
1126
+ case "promptArgPosition":
1127
+ target.promptArgPosition = value;
1128
+ break;
1129
+ case "default":
1130
+ target.default = value === "true";
1131
+ break;
1132
+ case "resolveFromShellAliases":
1133
+ target.resolveFromShellAliases = value === "true";
1134
+ break;
1135
+ }
1136
+ }
1137
+ function yamlQuoteScalar(value) {
1138
+ if (/[\r\n]/.test(value)) {
1139
+ throw new AgentConfigError(
1140
+ `value contains newlines, which the agents config serializer does not support: ${JSON.stringify(value)}`
1141
+ );
1142
+ }
1143
+ if (value === "" || /[:#{}[\],&*?|>!%@`"'\\\t]/.test(value) || /^\s|\s$/.test(value)) {
1144
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t");
1145
+ return `"${escaped}"`;
1146
+ }
1147
+ return value;
1148
+ }
1149
+ function serializeAgentsConfig(agents) {
1150
+ const lines = ["agents:"];
1151
+ for (const a of agents) {
1152
+ lines.push(` - id: ${yamlQuoteScalar(a.id)}`);
1153
+ lines.push(` label: ${yamlQuoteScalar(a.label)}`);
1154
+ lines.push(` command: ${yamlQuoteScalar(a.command)}`);
1155
+ if (a.args && a.args.length > 0) {
1156
+ lines.push(` args:`);
1157
+ for (const arg of a.args) {
1158
+ lines.push(` - ${yamlQuoteScalar(arg)}`);
1159
+ }
1160
+ }
1161
+ if (a.promptArgPosition && a.promptArgPosition !== "first") {
1162
+ lines.push(` promptArgPosition: ${a.promptArgPosition}`);
1163
+ }
1164
+ if (a.default) {
1165
+ lines.push(` default: true`);
1166
+ }
1167
+ if (a.resolveFromShellAliases) {
1168
+ lines.push(` resolveFromShellAliases: true`);
1169
+ }
1170
+ }
1171
+ return lines.join("\n");
1172
+ }
1173
+ async function writeAgentsConfig(agents) {
1174
+ validateAgentList(agents);
1175
+ const configPath = resolve3(syntaurRoot(), "config.md");
1176
+ const agentsBlock = serializeAgentsConfig(agents);
1177
+ const existing = await fileExists(configPath) ? await readFile2(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1178
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1179
+ if (!fmMatch) {
1180
+ const content = `---
1181
+ version: "2.0"
1182
+ defaultProjectDir: ${defaultProjectDir()}
1183
+ ${agentsBlock}
1184
+ ---
1185
+ ${existing}`;
1186
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
1187
+ return;
1188
+ }
1189
+ const fmBlock = fmMatch[2];
1190
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1191
+ const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
1192
+ const newFm = `${cleanedFm}
1193
+ ${agentsBlock}`.replace(/^\n+/, "").replace(/\n+$/, "");
1194
+ const newContent = `---
1195
+ ${newFm}
1196
+ ---${afterFrontmatter}`;
1197
+ await writeFileForce(configPath, newContent);
1198
+ }
862
1199
  async function writeStatusConfig(statuses) {
863
1200
  const configPath = resolve3(syntaurRoot(), "config.md");
864
1201
  const statusBlock = serializeStatusConfig(statuses);
@@ -1052,7 +1389,10 @@ async function readConfig() {
1052
1389
  },
1053
1390
  agentDefaults: {
1054
1391
  trustLevel: fm["agentDefaults.trustLevel"] || DEFAULT_CONFIG.agentDefaults.trustLevel,
1055
- autoApprove: fm["agentDefaults.autoApprove"] === "true" || DEFAULT_CONFIG.agentDefaults.autoApprove
1392
+ autoApprove: fm["agentDefaults.autoApprove"] === "true" || DEFAULT_CONFIG.agentDefaults.autoApprove,
1393
+ autoCreateWorktree: AUTO_CREATE_WORKTREE_VALUES.includes(
1394
+ fm["agentDefaults.autoCreateWorktree"]
1395
+ ) ? fm["agentDefaults.autoCreateWorktree"] : DEFAULT_CONFIG.agentDefaults.autoCreateWorktree
1056
1396
  },
1057
1397
  integrations: {
1058
1398
  claudePluginDir: parseOptionalAbsolutePath(
@@ -1076,10 +1416,26 @@ async function readConfig() {
1076
1416
  } : null,
1077
1417
  statuses: parseStatusConfig(content),
1078
1418
  types: null,
1079
- playbooks: parsePlaybooksConfig(fmBlock)
1419
+ agents: normalizeAgentsFromConfig(parseAgentsConfig(content)),
1420
+ playbooks: parsePlaybooksConfig(fmBlock),
1421
+ theme: parseThemeConfig(content)
1080
1422
  };
1081
1423
  }
1082
- var DEFAULT_CONFIG, migratedConfigPaths;
1424
+ function getAgents(config) {
1425
+ return config.agents ?? BUILTIN_AGENTS;
1426
+ }
1427
+ async function updateAgentsConfig(mutation, options = {}) {
1428
+ const config = await readConfig();
1429
+ const previous = config.agents ?? [...BUILTIN_AGENTS];
1430
+ const next = mutation.apply(previous);
1431
+ validateAgentList(next);
1432
+ if (options.dryRun) {
1433
+ return { previous, next, written: false };
1434
+ }
1435
+ await writeAgentsConfig(next);
1436
+ return { previous, next, written: true };
1437
+ }
1438
+ var DEFAULT_CONFIG, BUILTIN_AGENTS, AGENT_ID_PATTERN, PROMPT_ARG_POSITIONS, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, migratedConfigPaths;
1083
1439
  var init_config2 = __esm({
1084
1440
  "src/utils/config.ts"() {
1085
1441
  "use strict";
@@ -1095,7 +1451,8 @@ var init_config2 = __esm({
1095
1451
  },
1096
1452
  agentDefaults: {
1097
1453
  trustLevel: "medium",
1098
- autoApprove: false
1454
+ autoApprove: false,
1455
+ autoCreateWorktree: "ask"
1099
1456
  },
1100
1457
  integrations: {
1101
1458
  claudePluginDir: null,
@@ -1105,9 +1462,20 @@ var init_config2 = __esm({
1105
1462
  backup: null,
1106
1463
  statuses: null,
1107
1464
  types: null,
1465
+ agents: null,
1108
1466
  playbooks: {
1109
1467
  disabled: []
1110
- }
1468
+ },
1469
+ theme: null
1470
+ };
1471
+ BUILTIN_AGENTS = [
1472
+ { id: "claude", label: "Claude", command: "claude", default: true },
1473
+ { id: "codex", label: "Codex", command: "codex" }
1474
+ ];
1475
+ AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
1476
+ PROMPT_ARG_POSITIONS = ["first", "last", "none"];
1477
+ AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
1478
+ AgentConfigError = class extends Error {
1111
1479
  };
1112
1480
  migratedConfigPaths = /* @__PURE__ */ new Set();
1113
1481
  }
@@ -1283,6 +1651,12 @@ var init_state_machine = __esm({
1283
1651
  });
1284
1652
 
1285
1653
  // src/lifecycle/frontmatter.ts
1654
+ var frontmatter_exports = {};
1655
+ __export(frontmatter_exports, {
1656
+ parseAssignmentFrontmatter: () => parseAssignmentFrontmatter,
1657
+ updateAssignmentFile: () => updateAssignmentFile,
1658
+ updateAssignmentWorkspace: () => updateAssignmentWorkspace
1659
+ });
1286
1660
  function extractFrontmatter2(fileContent) {
1287
1661
  const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
1288
1662
  if (!match) {
@@ -1435,6 +1809,62 @@ function updateAssignmentFile(fileContent, updates) {
1435
1809
  }
1436
1810
  return result;
1437
1811
  }
1812
+ function findWorkspaceBlock(fmBlock) {
1813
+ const headerMatch = fmBlock.match(/^workspace:\s*$/m);
1814
+ if (!headerMatch) return null;
1815
+ const headerStart = fmBlock.indexOf(headerMatch[0]);
1816
+ const bodyStart = headerStart + headerMatch[0].length + 1;
1817
+ const after = fmBlock.slice(bodyStart);
1818
+ const lines = after.split("\n");
1819
+ let consumed = 0;
1820
+ for (let i = 0; i < lines.length; i++) {
1821
+ const line = lines[i];
1822
+ if (line.length === 0) {
1823
+ consumed += line.length + 1;
1824
+ continue;
1825
+ }
1826
+ if (line[0] !== " ") break;
1827
+ consumed += line.length + 1;
1828
+ }
1829
+ const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);
1830
+ return { headerStart, bodyStart, bodyEnd };
1831
+ }
1832
+ function updateAssignmentWorkspace(fileContent, partial) {
1833
+ const fmMatch = fileContent.match(/^(---\n)([\s\S]*?)(\n---)/);
1834
+ if (!fmMatch) {
1835
+ throw new Error("No frontmatter found in assignment file. Expected --- delimiters.");
1836
+ }
1837
+ const fmBlock = fmMatch[2];
1838
+ const fields = ["repository", "worktreePath", "branch", "parentBranch"];
1839
+ const block = findWorkspaceBlock(fmBlock);
1840
+ let newFm = fmBlock;
1841
+ if (block) {
1842
+ let body = fmBlock.slice(block.bodyStart, block.bodyEnd);
1843
+ for (const field of fields) {
1844
+ if (!(field in partial)) continue;
1845
+ const value = partial[field] ?? null;
1846
+ const formatted = formatYamlValue(value);
1847
+ const lineRegex = new RegExp(`^(\\s+${field}:)\\s*.*$`, "m");
1848
+ if (lineRegex.test(body)) {
1849
+ body = body.replace(lineRegex, `$1 ${formatted}`);
1850
+ } else {
1851
+ const trimmed = body.replace(/\n+$/, "");
1852
+ body = `${trimmed}${trimmed.length > 0 ? "\n" : ""} ${field}: ${formatted}
1853
+ `;
1854
+ }
1855
+ }
1856
+ newFm = fmBlock.slice(0, block.bodyStart) + body + fmBlock.slice(block.bodyEnd);
1857
+ } else {
1858
+ const lines = ["workspace:"];
1859
+ for (const field of fields) {
1860
+ const value = field in partial ? partial[field] ?? null : null;
1861
+ lines.push(` ${field}: ${formatYamlValue(value)}`);
1862
+ }
1863
+ newFm = `${fmBlock.replace(/\n+$/, "")}
1864
+ ${lines.join("\n")}`;
1865
+ }
1866
+ return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;
1867
+ }
1438
1868
  var init_frontmatter = __esm({
1439
1869
  "src/lifecycle/frontmatter.ts"() {
1440
1870
  "use strict";
@@ -4672,7 +5102,7 @@ function App({ projectsDir: projectsDir2, onLaunch }) {
4672
5102
  collapseNode,
4673
5103
  currentNode
4674
5104
  } = useTreeState(nodes, filteredIds);
4675
- useInput((input3, key) => {
5105
+ useInput((input4, key) => {
4676
5106
  if (searchActive) {
4677
5107
  if (key.escape) {
4678
5108
  setSearchActive(false);
@@ -4685,19 +5115,19 @@ function App({ projectsDir: projectsDir2, onLaunch }) {
4685
5115
  }
4686
5116
  return;
4687
5117
  }
4688
- if (input3 === "q" || key.escape) {
5118
+ if (input4 === "q" || key.escape) {
4689
5119
  exit();
4690
5120
  return;
4691
5121
  }
4692
- if (input3 === "/") {
5122
+ if (input4 === "/") {
4693
5123
  setSearchActive(true);
4694
5124
  return;
4695
5125
  }
4696
- if (key.upArrow || input3 === "k") {
5126
+ if (key.upArrow || input4 === "k") {
4697
5127
  moveUp();
4698
5128
  return;
4699
5129
  }
4700
- if (key.downArrow || input3 === "j") {
5130
+ if (key.downArrow || input4 === "j") {
4701
5131
  moveDown();
4702
5132
  return;
4703
5133
  }
@@ -4786,22 +5216,71 @@ var init_App = __esm({
4786
5216
  // src/tui/launch.ts
4787
5217
  var launch_exports = {};
4788
5218
  __export(launch_exports, {
4789
- launchAgent: () => launchAgent
5219
+ INITIAL_PROMPT: () => INITIAL_PROMPT,
5220
+ buildAgentArgv: () => buildAgentArgv,
5221
+ formatFallbackCwdWarning: () => formatFallbackCwdWarning,
5222
+ launchAgent: () => launchAgent,
5223
+ shellQuote: () => shellQuote
4790
5224
  });
4791
5225
  import { spawn as spawn2 } from "child_process";
4792
5226
  import { mkdir as mkdir5, writeFile as writeFile9 } from "fs/promises";
4793
- import { resolve as resolve32 } from "path";
5227
+ import { isAbsolute as isAbsolute3, resolve as resolve32 } from "path";
5228
+ function formatFallbackCwdWarning(opts) {
5229
+ const missing = [];
5230
+ if (!opts.worktreePath) missing.push("worktreePath");
5231
+ if (!opts.branch) missing.push("branch");
5232
+ if (missing.length === 0) return null;
5233
+ const fields = missing.map((m) => `workspace.${m}`).join(" and ");
5234
+ return `syntaur: ${fields} not set for ${opts.assignmentSlug} \u2014 launching in ${opts.workspaceDir}`;
5235
+ }
5236
+ function shellQuote(arg) {
5237
+ if (arg === "") return "''";
5238
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
5239
+ }
5240
+ function buildAgentArgv(agent, prompt, env = process.env) {
5241
+ const position = agent.promptArgPosition ?? "first";
5242
+ const baseArgs = [...agent.args ?? []];
5243
+ const agentArgs = position === "first" ? [prompt, ...baseArgs] : position === "last" ? [...baseArgs, prompt] : baseArgs;
5244
+ if (agent.resolveFromShellAliases) {
5245
+ const requested = env.SHELL;
5246
+ let shell = requested;
5247
+ let warning = null;
5248
+ if (!shell || !isAbsolute3(shell)) {
5249
+ warning = `syntaur: $SHELL ${requested ? `("${requested}") is not absolute` : "is unset"} \u2014 falling back to /bin/sh for shell-alias resolution`;
5250
+ shell = "/bin/sh";
5251
+ }
5252
+ const quoted = [agent.command, ...agentArgs].map(shellQuote).join(" ");
5253
+ return {
5254
+ argv: { command: shell, args: ["-i", "-c", quoted] },
5255
+ shellFallbackWarning: warning
5256
+ };
5257
+ }
5258
+ return {
5259
+ argv: { command: agent.command, args: agentArgs },
5260
+ shellFallbackWarning: null
5261
+ };
5262
+ }
4794
5263
  async function launchAgent(options) {
4795
- const { projectsDir: projectsDir2, projectSlug, assignmentSlug, agent } = options;
4796
- const command = AGENT_COMMANDS[agent];
5264
+ const { projectsDir: projectsDir2, projectSlug, assignmentSlug, agent, cwdOverride } = options;
5265
+ const exitWith = options.onExit ?? ((code) => process.exit(code));
4797
5266
  const detail = await getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug);
4798
5267
  if (!detail) {
4799
5268
  console.error(`Assignment not found: ${projectSlug}/${assignmentSlug}`);
4800
5269
  process.exit(1);
4801
5270
  }
4802
- const workspaceDir = detail.workspace.worktreePath ?? (detail.workspace.repository?.startsWith("/") ? detail.workspace.repository : null) ?? process.cwd();
4803
5271
  const projectDir = resolve32(projectsDir2, projectSlug);
4804
5272
  const assignmentDir = resolve32(projectDir, "assignments", assignmentSlug);
5273
+ const resolvedFromWorkspace = cwdOverride ?? detail.workspace.worktreePath ?? (detail.workspace.repository?.startsWith("/") ? detail.workspace.repository : null);
5274
+ const workspaceDir = resolvedFromWorkspace ?? process.cwd();
5275
+ if (!cwdOverride) {
5276
+ const warning = formatFallbackCwdWarning({
5277
+ assignmentSlug,
5278
+ workspaceDir,
5279
+ worktreePath: detail.workspace.worktreePath,
5280
+ branch: detail.workspace.branch
5281
+ });
5282
+ if (warning) console.warn(warning);
5283
+ }
4805
5284
  const contextDir = resolve32(workspaceDir, ".syntaur");
4806
5285
  await mkdir5(contextDir, { recursive: true });
4807
5286
  const context = {
@@ -4818,39 +5297,161 @@ async function launchAgent(options) {
4818
5297
  resolve32(contextDir, "context.json"),
4819
5298
  JSON.stringify(context, null, 2) + "\n"
4820
5299
  );
4821
- return new Promise((resolvePromise, reject) => {
4822
- const initialPrompt = `Read the current Syntaur assignment at ${assignmentDir}/assignment.md and give me a brief summary: title, status, priority, objective, and acceptance criteria.`;
4823
- const child = spawn2(command, [initialPrompt], {
5300
+ const { argv, shellFallbackWarning } = buildAgentArgv(
5301
+ agent,
5302
+ INITIAL_PROMPT(assignmentDir)
5303
+ );
5304
+ if (shellFallbackWarning) {
5305
+ console.warn(shellFallbackWarning);
5306
+ }
5307
+ return new Promise((resolvePromise) => {
5308
+ const child = spawn2(argv.command, argv.args, {
4824
5309
  cwd: workspaceDir,
4825
5310
  stdio: "inherit"
4826
5311
  });
4827
5312
  child.on("error", (err2) => {
4828
- if (err2.code === "ENOENT") {
4829
- console.error(`${agent} CLI not found. Is \`${command}\` installed and in your PATH?`);
5313
+ const code = err2.code;
5314
+ if (code === "ENOENT") {
5315
+ console.error(
5316
+ `syntaur: agent "${agent.id}" command "${agent.command}" not found. If "${agent.command}" is a shell alias, set resolveFromShellAliases: true on this agent in ~/.syntaur/config.md.`
5317
+ );
5318
+ } else if (code === "EACCES") {
5319
+ console.error(
5320
+ `syntaur: agent "${agent.id}" command "${agent.command}" is not executable (EACCES). Check file permissions.`
5321
+ );
4830
5322
  } else {
4831
- console.error(`Failed to launch ${agent}:`, err2.message);
5323
+ console.error(
5324
+ `syntaur: failed to launch agent "${agent.id}" (${code ?? "unknown"}): ${err2.message}`
5325
+ );
4832
5326
  }
4833
- process.exit(1);
5327
+ resolvePromise();
5328
+ exitWith(1);
4834
5329
  });
4835
5330
  child.on("exit", (code) => {
4836
- process.exit(code ?? 0);
5331
+ resolvePromise();
5332
+ exitWith(code ?? 0);
4837
5333
  });
4838
5334
  });
4839
5335
  }
4840
- var AGENT_COMMANDS;
5336
+ var INITIAL_PROMPT;
4841
5337
  var init_launch = __esm({
4842
5338
  "src/tui/launch.ts"() {
4843
5339
  "use strict";
4844
5340
  init_api();
4845
- AGENT_COMMANDS = {
4846
- claude: "c",
4847
- codex: "cx"
5341
+ INITIAL_PROMPT = (assignmentDir) => `Read the current Syntaur assignment at ${assignmentDir}/assignment.md and give me a brief summary: title, status, priority, objective, and acceptance criteria.`;
5342
+ }
5343
+ });
5344
+
5345
+ // src/utils/git-worktree.ts
5346
+ var git_worktree_exports = {};
5347
+ __export(git_worktree_exports, {
5348
+ GitWorktreeError: () => GitWorktreeError,
5349
+ createWorktree: () => createWorktree,
5350
+ createWorktreeAndRecord: () => createWorktreeAndRecord,
5351
+ deleteBranch: () => deleteBranch,
5352
+ formatRollbackError: () => formatRollbackError,
5353
+ removeWorktree: () => removeWorktree
5354
+ });
5355
+ import { spawn as spawn3 } from "child_process";
5356
+ import { readFile as readFile20 } from "fs/promises";
5357
+ function run(command, args, cwd) {
5358
+ return new Promise((resolvePromise) => {
5359
+ const child = spawn3(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
5360
+ let stdout = "";
5361
+ let stderr = "";
5362
+ child.stdout.on("data", (chunk) => stdout += chunk.toString());
5363
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
5364
+ child.on("error", (err2) => {
5365
+ resolvePromise({ code: -1, stdout, stderr: stderr + String(err2) });
5366
+ });
5367
+ child.on("close", (code) => {
5368
+ resolvePromise({ code: code ?? -1, stdout, stderr });
5369
+ });
5370
+ });
5371
+ }
5372
+ async function createWorktree(opts) {
5373
+ const { repository, branch, worktreePath, parentBranch } = opts;
5374
+ const result = await run(
5375
+ "git",
5376
+ ["-C", repository, "worktree", "add", "-b", branch, worktreePath, parentBranch]
5377
+ );
5378
+ if (result.code !== 0) {
5379
+ throw new GitWorktreeError(
5380
+ `git worktree add failed (exit ${result.code}): ${result.stderr.trim() || "(no stderr)"}`,
5381
+ result.stderr
5382
+ );
5383
+ }
5384
+ }
5385
+ async function removeWorktree(repository, worktreePath) {
5386
+ const result = await run(
5387
+ "git",
5388
+ ["-C", repository, "worktree", "remove", "--force", worktreePath]
5389
+ );
5390
+ return { ok: result.code === 0, stderr: result.stderr };
5391
+ }
5392
+ async function deleteBranch(repository, branch) {
5393
+ const result = await run("git", ["-C", repository, "branch", "-D", branch]);
5394
+ return { ok: result.code === 0, stderr: result.stderr };
5395
+ }
5396
+ async function createWorktreeAndRecord(opts) {
5397
+ const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
5398
+ await createWorktree({ repository, branch, worktreePath, parentBranch });
5399
+ try {
5400
+ const content = await readFile20(assignmentPath, "utf-8");
5401
+ const updated = updateAssignmentWorkspace(content, {
5402
+ repository,
5403
+ worktreePath,
5404
+ branch,
5405
+ parentBranch
5406
+ });
5407
+ await writeFileForce(assignmentPath, updated);
5408
+ } catch (writeErr) {
5409
+ const cleanup = await removeWorktree(repository, worktreePath);
5410
+ const branchCleanup = await deleteBranch(repository, branch);
5411
+ const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);
5412
+ throw new Error(
5413
+ formatRollbackError({
5414
+ writeMsg,
5415
+ worktreePath,
5416
+ branch,
5417
+ worktreeCleanup: cleanup,
5418
+ branchCleanup
5419
+ })
5420
+ );
5421
+ }
5422
+ }
5423
+ function formatRollbackError(opts) {
5424
+ const { writeMsg, worktreePath, branch, worktreeCleanup, branchCleanup } = opts;
5425
+ const wtMsg = worktreeCleanup.stderr.trim() || "(no stderr)";
5426
+ const brMsg = branchCleanup.stderr.trim() || "(no stderr)";
5427
+ if (!worktreeCleanup.ok && !branchCleanup.ok) {
5428
+ return `Failed to update assignment frontmatter AND failed to clean up both worktree and branch. Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Branch cleanup error: ${brMsg}. Orphan worktree at ${worktreePath} and orphan branch "${branch}" \u2014 remove them manually.`;
5429
+ }
5430
+ if (!worktreeCleanup.ok) {
5431
+ return `Failed to update assignment frontmatter AND failed to clean up worktree. Write error: ${writeMsg}. Worktree cleanup error: ${wtMsg}. Orphan worktree at ${worktreePath} \u2014 remove it manually.`;
5432
+ }
5433
+ if (!branchCleanup.ok) {
5434
+ return `Failed to update assignment frontmatter: ${writeMsg}. Rolled back git worktree at ${worktreePath}, but could not delete branch "${branch}": ${brMsg}. Remove the branch manually.`;
5435
+ }
5436
+ return `Failed to update assignment frontmatter: ${writeMsg}. Rolled back git worktree at ${worktreePath} and branch "${branch}".`;
5437
+ }
5438
+ var GitWorktreeError;
5439
+ var init_git_worktree = __esm({
5440
+ "src/utils/git-worktree.ts"() {
5441
+ "use strict";
5442
+ init_frontmatter();
5443
+ init_fs();
5444
+ GitWorktreeError = class extends Error {
5445
+ constructor(message, stderr) {
5446
+ super(message);
5447
+ this.stderr = stderr;
5448
+ }
4848
5449
  };
4849
5450
  }
4850
5451
  });
4851
5452
 
4852
5453
  // src/index.ts
4853
- import { Command as Command4 } from "commander";
5454
+ import { Command as Command5 } from "commander";
4854
5455
 
4855
5456
  // src/commands/init.ts
4856
5457
  init_paths();
@@ -6124,8 +6725,8 @@ async function migrateFromMarkdown(projectsDir2) {
6124
6725
  return allSessions.length;
6125
6726
  }
6126
6727
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
6127
- const { readFile: readFile30 } = await import("fs/promises");
6128
- const raw = await readFile30(filePath, "utf-8");
6728
+ const { readFile: readFile32 } = await import("fs/promises");
6729
+ const raw = await readFile32(filePath, "utf-8");
6129
6730
  const sessions = [];
6130
6731
  const lines = raw.split("\n");
6131
6732
  let inTable = false;
@@ -7939,8 +8540,8 @@ ${entry}`;
7939
8540
  });
7940
8541
  return router;
7941
8542
  }
7942
- function slugifyLocal(input3) {
7943
- return input3.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
8543
+ function slugifyLocal(input4) {
8544
+ return input4.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
7944
8545
  }
7945
8546
  async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
7946
8547
  const commentsPath = resolve15(assignmentDir, "comments.md");
@@ -8554,8 +9155,8 @@ function createTodosRouter(todosDir2, broadcast) {
8554
9155
  router.post("/:workspace/archive", async (req, res) => {
8555
9156
  try {
8556
9157
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
8557
- const { resolve: resolve46 } = await import("path");
8558
- const { readFile: readFile30 } = await import("fs/promises");
9158
+ const { resolve: resolve47 } = await import("path");
9159
+ const { readFile: readFile32 } = await import("fs/promises");
8559
9160
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
8560
9161
  const workspace = getWorkspaceParam(req.params.workspace);
8561
9162
  const checklist = await readChecklist(todosDir2, workspace);
@@ -8571,10 +9172,10 @@ function createTodosRouter(todosDir2, broadcast) {
8571
9172
  (e) => e.itemIds.every((id) => completedIds.has(id))
8572
9173
  );
8573
9174
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
8574
- await ensureDir(resolve46(todosDir2, "archive"));
9175
+ await ensureDir(resolve47(todosDir2, "archive"));
8575
9176
  let archContent = "";
8576
9177
  if (await fileExists(archFile)) {
8577
- archContent = await readFile30(archFile, "utf-8");
9178
+ archContent = await readFile32(archFile, "utf-8");
8578
9179
  archContent = archContent.trimEnd() + "\n\n";
8579
9180
  } else {
8580
9181
  archContent = `---
@@ -10158,6 +10759,43 @@ function createDashboardServer(options) {
10158
10759
  res.status(500).json({ error: "Failed to reset status config" });
10159
10760
  }
10160
10761
  });
10762
+ const THEME_PRESET_SLUGS = ["default", "ocean", "forest", "sunset"];
10763
+ const DEFAULT_THEME_PRESET = "default";
10764
+ app.get("/api/config/theme", async (_req, res) => {
10765
+ try {
10766
+ const config = await readConfig();
10767
+ const preset = config.theme?.preset ?? DEFAULT_THEME_PRESET;
10768
+ res.json({ preset, custom: config.theme !== null });
10769
+ } catch (error) {
10770
+ console.error("Error getting theme config:", error);
10771
+ res.status(500).json({ error: "Failed to get theme config" });
10772
+ }
10773
+ });
10774
+ app.post("/api/config/theme", async (req, res) => {
10775
+ try {
10776
+ const { preset } = req.body ?? {};
10777
+ if (typeof preset !== "string" || !THEME_PRESET_SLUGS.includes(preset)) {
10778
+ res.status(400).json({
10779
+ error: `preset must be one of: ${THEME_PRESET_SLUGS.join(", ")}`
10780
+ });
10781
+ return;
10782
+ }
10783
+ await writeThemeConfig({ preset });
10784
+ res.json({ preset, custom: true });
10785
+ } catch (error) {
10786
+ console.error("Error saving theme config:", error);
10787
+ res.status(500).json({ error: "Failed to save theme config" });
10788
+ }
10789
+ });
10790
+ app.delete("/api/config/theme", async (_req, res) => {
10791
+ try {
10792
+ await deleteThemeConfig();
10793
+ res.json({ preset: DEFAULT_THEME_PRESET, custom: false });
10794
+ } catch (error) {
10795
+ console.error("Error resetting theme config:", error);
10796
+ res.status(500).json({ error: "Failed to reset theme config" });
10797
+ }
10798
+ });
10161
10799
  app.get("/api/projects", async (req, res) => {
10162
10800
  try {
10163
10801
  let projects = await listProjects(projectsDir2);
@@ -10297,7 +10935,9 @@ function createDashboardServer(options) {
10297
10935
  app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir2, broadcast));
10298
10936
  app.use("/api/backup", createBackupRouter());
10299
10937
  if (serveStaticUi && dashboardDistPath) {
10300
- app.use("/assets", express.static(resolve21(dashboardDistPath, "assets")));
10938
+ const sendOpts = { dotfiles: "allow" };
10939
+ app.use("/assets", express.static(resolve21(dashboardDistPath, "assets"), sendOpts));
10940
+ app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
10301
10941
  app.get("{*path}", async (req, res) => {
10302
10942
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
10303
10943
  res.status(404).json({ error: "Not Found" });
@@ -10310,7 +10950,7 @@ function createDashboardServer(options) {
10310
10950
  );
10311
10951
  return;
10312
10952
  }
10313
- res.sendFile(indexPath, (err2) => {
10953
+ res.sendFile(indexPath, sendOpts, (err2) => {
10314
10954
  if (err2) {
10315
10955
  console.error("Error sending dashboard index.html:", err2);
10316
10956
  if (!res.headersSent) res.status(500).send("Dashboard load error");
@@ -12673,10 +13313,29 @@ async function trackSessionCommand(options) {
12673
13313
 
12674
13314
  // src/commands/browse.ts
12675
13315
  init_config2();
13316
+ init_paths();
13317
+ init_fs();
13318
+ import { spawnSync as spawnSync2 } from "child_process";
13319
+ import { resolve as resolve33, isAbsolute as isAbsolute4 } from "path";
13320
+ import { readFile as readFile21 } from "fs/promises";
13321
+ import { select, confirm as confirm2, input as input3 } from "@inquirer/prompts";
12676
13322
  async function browseCommand(options) {
12677
13323
  const config = await readConfig();
12678
13324
  const projectsDir2 = config.defaultProjectDir;
12679
- const agent = options.agent;
13325
+ const agents = getAgents(config);
13326
+ if (agents.length === 0) {
13327
+ console.error(
13328
+ "No agents configured. Add one with `syntaur agents add --id <id> --label <label> --command <path>`."
13329
+ );
13330
+ process.exit(1);
13331
+ }
13332
+ const preSelectedAgent = options.agent ? agents.find((a) => a.id === options.agent) : void 0;
13333
+ if (options.agent && !preSelectedAgent) {
13334
+ console.error(
13335
+ `Unknown agent id "${options.agent}". Configured: ${agents.map((a) => a.id).join(", ")}`
13336
+ );
13337
+ process.exit(1);
13338
+ }
12680
13339
  const { render } = await import("ink");
12681
13340
  const React2 = await import("react");
12682
13341
  const { App: App2 } = await Promise.resolve().then(() => (init_App(), App_exports));
@@ -12687,7 +13346,19 @@ async function browseCommand(options) {
12687
13346
  unmount();
12688
13347
  unmount = null;
12689
13348
  }
12690
- await launchAgent2({ ...launchOpts, agent });
13349
+ const agent = preSelectedAgent ?? await pickAgent(agents);
13350
+ const cwdOverride = await ensureWorktree({
13351
+ projectsDir: launchOpts.projectsDir,
13352
+ projectSlug: launchOpts.projectSlug,
13353
+ assignmentSlug: launchOpts.assignmentSlug,
13354
+ worktreePromptEnabled: options.worktreePrompt !== false,
13355
+ autoCreateWorktree: config.agentDefaults.autoCreateWorktree
13356
+ });
13357
+ await launchAgent2({
13358
+ ...launchOpts,
13359
+ agent,
13360
+ ...cwdOverride ? { cwdOverride } : {}
13361
+ });
12691
13362
  };
12692
13363
  const instance = render(
12693
13364
  React2.createElement(App2, { projectsDir: projectsDir2, onLaunch })
@@ -12695,9 +13366,164 @@ async function browseCommand(options) {
12695
13366
  unmount = instance.unmount;
12696
13367
  await instance.waitUntilExit();
12697
13368
  }
13369
+ async function pickAgent(agents) {
13370
+ if (agents.length === 1) return agents[0];
13371
+ if (!isInteractiveTerminal()) {
13372
+ const fallback = agents.find((a) => a.default) ?? agents[0];
13373
+ console.warn(
13374
+ `syntaur: multiple agents configured but no TTY \u2014 using "${fallback.id}".`
13375
+ );
13376
+ return fallback;
13377
+ }
13378
+ const defaultAgent = agents.find((a) => a.default) ?? agents[0];
13379
+ const id = await select({
13380
+ message: "Launch which agent?",
13381
+ choices: agents.map((a) => ({ name: a.label, value: a.id, description: a.command })),
13382
+ default: defaultAgent.id
13383
+ });
13384
+ const picked = agents.find((a) => a.id === id);
13385
+ if (!picked) throw new Error(`Internal error: picker returned unknown agent id "${id}"`);
13386
+ return picked;
13387
+ }
13388
+ async function ensureWorktree(opts) {
13389
+ const assignmentPath = resolve33(
13390
+ opts.projectsDir,
13391
+ opts.projectSlug,
13392
+ "assignments",
13393
+ opts.assignmentSlug,
13394
+ "assignment.md"
13395
+ );
13396
+ if (!await fileExists(assignmentPath)) {
13397
+ return void 0;
13398
+ }
13399
+ const content = await readFile21(assignmentPath, "utf-8");
13400
+ const { parseAssignmentFrontmatter: parseAssignmentFrontmatter2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
13401
+ const fm = parseAssignmentFrontmatter2(content);
13402
+ const { workspace } = fm;
13403
+ if (workspace.worktreePath && workspace.branch) {
13404
+ return void 0;
13405
+ }
13406
+ if (!opts.worktreePromptEnabled) {
13407
+ return void 0;
13408
+ }
13409
+ if (opts.autoCreateWorktree === "skip") {
13410
+ return void 0;
13411
+ }
13412
+ const defaults = computeWorktreeDefaults({
13413
+ projectSlug: opts.projectSlug,
13414
+ assignmentSlug: opts.assignmentSlug,
13415
+ existing: workspace
13416
+ });
13417
+ if (!defaults.repository) {
13418
+ console.warn(
13419
+ `syntaur: cannot infer repository for ${opts.assignmentSlug} \u2014 skipping worktree prompt`
13420
+ );
13421
+ return void 0;
13422
+ }
13423
+ if (opts.autoCreateWorktree === "always") {
13424
+ return await runCreate({
13425
+ assignmentPath,
13426
+ repository: defaults.repository,
13427
+ branch: defaults.branch,
13428
+ parentBranch: defaults.parentBranch,
13429
+ worktreePath: defaults.worktreePath
13430
+ });
13431
+ }
13432
+ if (!isInteractiveTerminal()) {
13433
+ return void 0;
13434
+ }
13435
+ const proceed = await confirm2({
13436
+ message: `This assignment has no git worktree/branch configured. Create one?`,
13437
+ default: true
13438
+ });
13439
+ if (!proceed) {
13440
+ return void 0;
13441
+ }
13442
+ const repository = await input3({
13443
+ message: "Repository path:",
13444
+ default: defaults.repository
13445
+ });
13446
+ const branch = await input3({
13447
+ message: "Branch name:",
13448
+ default: defaults.branch
13449
+ });
13450
+ const parentBranch = await input3({
13451
+ message: "Parent branch:",
13452
+ default: defaults.parentBranch
13453
+ });
13454
+ const worktreePath = await input3({
13455
+ message: "Worktree path:",
13456
+ default: defaults.worktreePath
13457
+ });
13458
+ return await runCreate({
13459
+ assignmentPath,
13460
+ repository,
13461
+ branch,
13462
+ parentBranch,
13463
+ worktreePath
13464
+ });
13465
+ }
13466
+ function computeWorktreeDefaults(opts) {
13467
+ const repository = opts.existing.repository ?? detectCurrentGitRoot();
13468
+ const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
13469
+ const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch() ?? "main";
13470
+ const worktreeBase = resolve33(
13471
+ syntaurRoot(),
13472
+ "worktrees",
13473
+ opts.projectSlug || "standalone",
13474
+ opts.assignmentSlug
13475
+ );
13476
+ return {
13477
+ ...repository ? { repository } : {},
13478
+ branch,
13479
+ parentBranch,
13480
+ worktreePath: worktreeBase
13481
+ };
13482
+ }
13483
+ function detectCurrentGitRoot() {
13484
+ const result = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
13485
+ encoding: "utf-8"
13486
+ });
13487
+ if (result.status !== 0) return void 0;
13488
+ const out = result.stdout.trim();
13489
+ return out.length > 0 ? out : void 0;
13490
+ }
13491
+ function detectCurrentBranch() {
13492
+ const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
13493
+ encoding: "utf-8"
13494
+ });
13495
+ if (result.status !== 0) return void 0;
13496
+ const out = result.stdout.trim();
13497
+ if (!out || out === "HEAD") return void 0;
13498
+ return out;
13499
+ }
13500
+ async function runCreate(opts) {
13501
+ const { createWorktreeAndRecord: createWorktreeAndRecord2, GitWorktreeError: GitWorktreeError2 } = await Promise.resolve().then(() => (init_git_worktree(), git_worktree_exports));
13502
+ const expandedWorktree = expandHome(opts.worktreePath);
13503
+ const absWorktree = isAbsolute4(expandedWorktree) ? expandedWorktree : resolve33(expandedWorktree);
13504
+ try {
13505
+ await createWorktreeAndRecord2({
13506
+ assignmentPath: opts.assignmentPath,
13507
+ repository: opts.repository,
13508
+ branch: opts.branch,
13509
+ worktreePath: absWorktree,
13510
+ parentBranch: opts.parentBranch
13511
+ });
13512
+ console.log(`syntaur: created worktree at ${absWorktree} on branch ${opts.branch}`);
13513
+ return absWorktree;
13514
+ } catch (err2) {
13515
+ const msg = err2 instanceof Error ? err2.message : String(err2);
13516
+ if (err2 instanceof GitWorktreeError2) {
13517
+ console.error(`syntaur: ${msg}`);
13518
+ } else {
13519
+ console.error(`syntaur: ${msg}`);
13520
+ }
13521
+ process.exit(1);
13522
+ }
13523
+ }
12698
13524
 
12699
13525
  // src/commands/create-playbook.ts
12700
- import { resolve as resolve33 } from "path";
13526
+ import { resolve as resolve34 } from "path";
12701
13527
  init_timestamp();
12702
13528
  init_paths();
12703
13529
  init_fs();
@@ -12714,7 +13540,7 @@ async function createPlaybookCommand(name, options) {
12714
13540
  }
12715
13541
  const dir = playbooksDir();
12716
13542
  await ensureDir(dir);
12717
- const filePath = resolve33(dir, `${slug}.md`);
13543
+ const filePath = resolve34(dir, `${slug}.md`);
12718
13544
  if (await fileExists(filePath)) {
12719
13545
  throw new Error(
12720
13546
  `Playbook "${slug}" already exists at ${filePath}
@@ -12736,8 +13562,8 @@ init_paths();
12736
13562
  init_fs();
12737
13563
  init_parser();
12738
13564
  init_config2();
12739
- import { readdir as readdir12, readFile as readFile20 } from "fs/promises";
12740
- import { resolve as resolve34 } from "path";
13565
+ import { readdir as readdir12, readFile as readFile22 } from "fs/promises";
13566
+ import { resolve as resolve35 } from "path";
12741
13567
  async function listPlaybooksCommand(options = {}) {
12742
13568
  const dir = playbooksDir();
12743
13569
  if (!await fileExists(dir)) {
@@ -12752,8 +13578,8 @@ async function listPlaybooksCommand(options = {}) {
12752
13578
  );
12753
13579
  const rows = [];
12754
13580
  for (const entry of mdFiles) {
12755
- const filePath = resolve34(dir, entry.name);
12756
- const raw = await readFile20(filePath, "utf-8");
13581
+ const filePath = resolve35(dir, entry.name);
13582
+ const raw = await readFile22(filePath, "utf-8");
12757
13583
  const parsed = parsePlaybook(raw);
12758
13584
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
12759
13585
  const disabled = disabledSet.has(slug);
@@ -12835,8 +13661,8 @@ init_parser2();
12835
13661
  init_fs();
12836
13662
  init_config2();
12837
13663
  import { Command } from "commander";
12838
- import { readFile as readFile21 } from "fs/promises";
12839
- import { resolve as resolve35 } from "path";
13664
+ import { readFile as readFile23 } from "fs/promises";
13665
+ import { resolve as resolve36 } from "path";
12840
13666
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
12841
13667
  async function resolveScope(options) {
12842
13668
  const flagCount = [Boolean(options.project), Boolean(options.workspace), Boolean(options.global)].filter(Boolean).length;
@@ -12848,7 +13674,7 @@ async function resolveScope(options) {
12848
13674
  throw new Error(`Invalid project slug: "${options.project}".`);
12849
13675
  }
12850
13676
  const config = await readConfig();
12851
- const projectMd = resolve35(config.defaultProjectDir, options.project, "project.md");
13677
+ const projectMd = resolve36(config.defaultProjectDir, options.project, "project.md");
12852
13678
  if (!await fileExists(projectMd)) {
12853
13679
  throw new Error(`Project "${options.project}" not found.`);
12854
13680
  }
@@ -13142,10 +13968,10 @@ todoCommand.command("archive").description("Archive completed todos and their lo
13142
13968
  (e) => e.itemIds.every((id) => completedIds.has(id))
13143
13969
  );
13144
13970
  const archFile = archivePath(todosPath, workspace, checklist.archiveInterval);
13145
- await ensureDir(resolve35(todosPath, "archive"));
13971
+ await ensureDir(resolve36(todosPath, "archive"));
13146
13972
  let archContent = "";
13147
13973
  if (await fileExists(archFile)) {
13148
- archContent = await readFile21(archFile, "utf-8");
13974
+ archContent = await readFile23(archFile, "utf-8");
13149
13975
  archContent = archContent.trimEnd() + "\n\n";
13150
13976
  } else {
13151
13977
  archContent = `---
@@ -13335,7 +14161,7 @@ import { Command as Command3 } from "commander";
13335
14161
 
13336
14162
  // src/utils/doctor/index.ts
13337
14163
  import { fileURLToPath as fileURLToPath7 } from "url";
13338
- import { readFile as readFile25 } from "fs/promises";
14164
+ import { readFile as readFile27 } from "fs/promises";
13339
14165
  import { dirname as dirname11, join as join5 } from "path";
13340
14166
 
13341
14167
  // src/utils/doctor/context.ts
@@ -13343,11 +14169,11 @@ init_config2();
13343
14169
  init_paths();
13344
14170
  init_fs();
13345
14171
  import Database2 from "better-sqlite3";
13346
- import { resolve as resolve36 } from "path";
14172
+ import { resolve as resolve37 } from "path";
13347
14173
  async function buildCheckContext(cwd = process.cwd()) {
13348
14174
  const config = await readConfig();
13349
14175
  const root = syntaurRoot();
13350
- const dbPath = resolve36(root, "syntaur.db");
14176
+ const dbPath = resolve37(root, "syntaur.db");
13351
14177
  let db2 = null;
13352
14178
  let dbError = null;
13353
14179
  if (await fileExists(dbPath)) {
@@ -13381,8 +14207,8 @@ function closeCheckContext(ctx) {
13381
14207
  // src/utils/doctor/checks/env.ts
13382
14208
  init_fs();
13383
14209
  init_paths();
13384
- import { resolve as resolve37, isAbsolute as isAbsolute3 } from "path";
13385
- import { readFile as readFile22, stat as stat4 } from "fs/promises";
14210
+ import { resolve as resolve38, isAbsolute as isAbsolute5 } from "path";
14211
+ import { readFile as readFile24, stat as stat4 } from "fs/promises";
13386
14212
  import { fileURLToPath as fileURLToPath6 } from "url";
13387
14213
  import { dirname as dirname10, join as join4 } from "path";
13388
14214
  var CATEGORY = "env";
@@ -13422,7 +14248,7 @@ var configValid = {
13422
14248
  category: CATEGORY,
13423
14249
  title: "~/.syntaur/config.md is valid",
13424
14250
  async run(ctx) {
13425
- const configPath = resolve37(ctx.syntaurRoot, "config.md");
14251
+ const configPath = resolve38(ctx.syntaurRoot, "config.md");
13426
14252
  if (!await fileExists(configPath)) {
13427
14253
  return {
13428
14254
  id: this.id,
@@ -13439,7 +14265,7 @@ var configValid = {
13439
14265
  autoFixable: false
13440
14266
  };
13441
14267
  }
13442
- const content = await readFile22(configPath, "utf-8");
14268
+ const content = await readFile24(configPath, "utf-8");
13443
14269
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
13444
14270
  if (!fmMatch || fmMatch[1].trim() === "") {
13445
14271
  return {
@@ -13476,7 +14302,7 @@ var configValid = {
13476
14302
  };
13477
14303
  }
13478
14304
  const expanded = expandHome(rawProjectDir);
13479
- if (!isAbsolute3(expanded)) {
14305
+ if (!isAbsolute5(expanded)) {
13480
14306
  return {
13481
14307
  id: this.id,
13482
14308
  category: this.category,
@@ -13719,7 +14545,7 @@ async function readLocalPkg() {
13719
14545
  for (let i = 0; i < 6; i++) {
13720
14546
  const candidate = join4(dir, "package.json");
13721
14547
  try {
13722
- const text = await readFile22(candidate, "utf-8");
14548
+ const text = await readFile24(candidate, "utf-8");
13723
14549
  return JSON.parse(text);
13724
14550
  } catch {
13725
14551
  dir = dirname10(dir);
@@ -13771,7 +14597,7 @@ function versionGte(a, b) {
13771
14597
 
13772
14598
  // src/utils/doctor/checks/structure.ts
13773
14599
  init_fs();
13774
- import { resolve as resolve38 } from "path";
14600
+ import { resolve as resolve39 } from "path";
13775
14601
  import { readdir as readdir13, stat as stat5 } from "fs/promises";
13776
14602
  var CATEGORY2 = "structure";
13777
14603
  var KNOWN_TOP_LEVEL = /* @__PURE__ */ new Set([
@@ -13791,7 +14617,7 @@ var projectsDir = {
13791
14617
  category: CATEGORY2,
13792
14618
  title: "projects/ directory exists",
13793
14619
  async run(ctx) {
13794
- const p = resolve38(ctx.syntaurRoot, "projects");
14620
+ const p = resolve39(ctx.syntaurRoot, "projects");
13795
14621
  if (!await fileExists(p)) {
13796
14622
  return {
13797
14623
  id: this.id,
@@ -13816,7 +14642,7 @@ var playbooksDir2 = {
13816
14642
  category: CATEGORY2,
13817
14643
  title: "playbooks/ directory exists",
13818
14644
  async run(ctx) {
13819
- const p = resolve38(ctx.syntaurRoot, "playbooks");
14645
+ const p = resolve39(ctx.syntaurRoot, "playbooks");
13820
14646
  if (!await fileExists(p)) {
13821
14647
  return {
13822
14648
  id: this.id,
@@ -13841,7 +14667,7 @@ var todosDirValid = {
13841
14667
  category: CATEGORY2,
13842
14668
  title: "todos/ directory is readable (if present)",
13843
14669
  async run(ctx) {
13844
- const p = resolve38(ctx.syntaurRoot, "todos");
14670
+ const p = resolve39(ctx.syntaurRoot, "todos");
13845
14671
  if (!await fileExists(p)) {
13846
14672
  return {
13847
14673
  id: this.id,
@@ -13872,7 +14698,7 @@ var serversDirValid = {
13872
14698
  category: CATEGORY2,
13873
14699
  title: "servers/ directory is readable (if present)",
13874
14700
  async run(ctx) {
13875
- const p = resolve38(ctx.syntaurRoot, "servers");
14701
+ const p = resolve39(ctx.syntaurRoot, "servers");
13876
14702
  if (!await fileExists(p)) {
13877
14703
  return {
13878
14704
  id: this.id,
@@ -13917,7 +14743,7 @@ var knownFilesRecognized = {
13917
14743
  title: this.title,
13918
14744
  status: "warn",
13919
14745
  detail: `unexpected top-level entries: ${unexpected.join(", ")}`,
13920
- affected: unexpected.map((n) => resolve38(ctx.syntaurRoot, n)),
14746
+ affected: unexpected.map((n) => resolve39(ctx.syntaurRoot, n)),
13921
14747
  remediation: {
13922
14748
  kind: "manual",
13923
14749
  suggestion: "Review these entries \u2014 they may be leftover state from older versions",
@@ -13946,7 +14772,7 @@ function pass2(check) {
13946
14772
 
13947
14773
  // src/utils/doctor/checks/project.ts
13948
14774
  init_fs();
13949
- import { resolve as resolve39 } from "path";
14775
+ import { resolve as resolve40 } from "path";
13950
14776
  import { readdir as readdir14, stat as stat6 } from "fs/promises";
13951
14777
  var CATEGORY3 = "project";
13952
14778
  var REQUIRED_PROJECT_FILES = [
@@ -13976,10 +14802,10 @@ async function listProjects2(ctx) {
13976
14802
  for (const e of entries) {
13977
14803
  if (!e.isDirectory()) continue;
13978
14804
  if (e.name.startsWith(".") || e.name.startsWith("_")) continue;
13979
- const projectDir = resolve39(dir, e.name);
14805
+ const projectDir = resolve40(dir, e.name);
13980
14806
  let looksLikeProject = false;
13981
14807
  for (const marker of PROJECT_MARKERS) {
13982
- if (await fileExists(resolve39(projectDir, marker))) {
14808
+ if (await fileExists(resolve40(projectDir, marker))) {
13983
14809
  looksLikeProject = true;
13984
14810
  break;
13985
14811
  }
@@ -13998,7 +14824,7 @@ var requiredFiles = {
13998
14824
  for (const projectDir of projects) {
13999
14825
  const missing = [];
14000
14826
  for (const rel of REQUIRED_PROJECT_FILES) {
14001
- const p = resolve39(projectDir, rel);
14827
+ const p = resolve40(projectDir, rel);
14002
14828
  if (!await fileExists(p)) missing.push(rel);
14003
14829
  }
14004
14830
  if (missing.length === 0) continue;
@@ -14008,7 +14834,7 @@ var requiredFiles = {
14008
14834
  title: this.title,
14009
14835
  status: "error",
14010
14836
  detail: `project at ${projectDir} is missing: ${missing.join(", ")}`,
14011
- affected: missing.map((m) => resolve39(projectDir, m)),
14837
+ affected: missing.map((m) => resolve40(projectDir, m)),
14012
14838
  remediation: {
14013
14839
  kind: "manual",
14014
14840
  suggestion: "Recreate the missing scaffold files from templates",
@@ -14031,7 +14857,7 @@ var manifestStale = {
14031
14857
  const projects = await listProjects2(ctx);
14032
14858
  const results = [];
14033
14859
  for (const projectDir of projects) {
14034
- const manifestPath = resolve39(projectDir, "manifest.md");
14860
+ const manifestPath = resolve40(projectDir, "manifest.md");
14035
14861
  if (!await fileExists(manifestPath)) continue;
14036
14862
  const manifestMtime = (await stat6(manifestPath)).mtimeMs;
14037
14863
  const newestAssignment = await newestAssignmentMtime(projectDir);
@@ -14080,7 +14906,7 @@ var orphanFiles = {
14080
14906
  title: this.title,
14081
14907
  status: "warn",
14082
14908
  detail: `project at ${projectDir} has unexpected entries: ${orphans.join(", ")}`,
14083
- affected: orphans.map((o) => resolve39(projectDir, o)),
14909
+ affected: orphans.map((o) => resolve40(projectDir, o)),
14084
14910
  autoFixable: false
14085
14911
  });
14086
14912
  }
@@ -14090,7 +14916,7 @@ var orphanFiles = {
14090
14916
  };
14091
14917
  var projectChecks = [requiredFiles, manifestStale, orphanFiles];
14092
14918
  async function newestAssignmentMtime(projectDir) {
14093
- const assignmentsRoot = resolve39(projectDir, "assignments");
14919
+ const assignmentsRoot = resolve40(projectDir, "assignments");
14094
14920
  if (!await fileExists(assignmentsRoot)) return 0;
14095
14921
  let newest = 0;
14096
14922
  let entries;
@@ -14101,7 +14927,7 @@ async function newestAssignmentMtime(projectDir) {
14101
14927
  }
14102
14928
  for (const e of entries) {
14103
14929
  if (!e.isDirectory()) continue;
14104
- const assignmentMd = resolve39(assignmentsRoot, e.name, "assignment.md");
14930
+ const assignmentMd = resolve40(assignmentsRoot, e.name, "assignment.md");
14105
14931
  try {
14106
14932
  const s = await stat6(assignmentMd);
14107
14933
  if (s.mtimeMs > newest) newest = s.mtimeMs;
@@ -14125,8 +14951,8 @@ init_fs();
14125
14951
  init_parser();
14126
14952
  init_types();
14127
14953
  init_paths();
14128
- import { resolve as resolve40 } from "path";
14129
- import { readdir as readdir15, readFile as readFile23 } from "fs/promises";
14954
+ import { resolve as resolve41 } from "path";
14955
+ import { readdir as readdir15, readFile as readFile25 } from "fs/promises";
14130
14956
  var CATEGORY4 = "assignment";
14131
14957
  var STATUSES_REQUIRING_HANDOFF = /* @__PURE__ */ new Set(["review", "completed"]);
14132
14958
  async function listAssignments(ctx) {
@@ -14137,16 +14963,16 @@ async function listAssignments(ctx) {
14137
14963
  for (const m of projects) {
14138
14964
  if (!m.isDirectory()) continue;
14139
14965
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
14140
- const assignmentsDir2 = resolve40(projectsDir2, m.name, "assignments");
14966
+ const assignmentsDir2 = resolve41(projectsDir2, m.name, "assignments");
14141
14967
  if (!await fileExists(assignmentsDir2)) continue;
14142
14968
  const entries = await readdir15(assignmentsDir2, { withFileTypes: true });
14143
14969
  for (const a of entries) {
14144
14970
  if (!a.isDirectory()) continue;
14145
14971
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
14146
- const assignmentDir = resolve40(assignmentsDir2, a.name);
14147
- const assignmentMd = resolve40(assignmentDir, "assignment.md");
14972
+ const assignmentDir = resolve41(assignmentsDir2, a.name);
14973
+ const assignmentMd = resolve41(assignmentDir, "assignment.md");
14148
14974
  const entry = {
14149
- projectDir: resolve40(projectsDir2, m.name),
14975
+ projectDir: resolve41(projectsDir2, m.name),
14150
14976
  projectSlug: m.name,
14151
14977
  assignmentDir,
14152
14978
  assignmentSlug: a.name,
@@ -14166,8 +14992,8 @@ async function listAssignments(ctx) {
14166
14992
  for (const a of entries) {
14167
14993
  if (!a.isDirectory()) continue;
14168
14994
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
14169
- const assignmentDir = resolve40(standaloneRoot, a.name);
14170
- const assignmentMd = resolve40(assignmentDir, "assignment.md");
14995
+ const assignmentDir = resolve41(standaloneRoot, a.name);
14996
+ const assignmentMd = resolve41(assignmentDir, "assignment.md");
14171
14997
  const entry = {
14172
14998
  projectDir: standaloneRoot,
14173
14999
  projectSlug: null,
@@ -14245,7 +15071,7 @@ var invalidStatus = {
14245
15071
  const allowed = configuredStatuses(ctx);
14246
15072
  const results = [];
14247
15073
  for (const a of withAssignmentMd) {
14248
- const path = resolve40(a.assignmentDir, "assignment.md");
15074
+ const path = resolve41(a.assignmentDir, "assignment.md");
14249
15075
  const parsed = await parseSafe(path);
14250
15076
  if (!parsed) continue;
14251
15077
  if (!allowed.has(parsed.status)) {
@@ -14278,7 +15104,7 @@ var workspaceMissing = {
14278
15104
  const terminal = terminalStatuses(ctx);
14279
15105
  const results = [];
14280
15106
  for (const a of withAssignmentMd) {
14281
- const path = resolve40(a.assignmentDir, "assignment.md");
15107
+ const path = resolve41(a.assignmentDir, "assignment.md");
14282
15108
  const parsed = await parseSafe(path);
14283
15109
  if (!parsed) continue;
14284
15110
  if (terminal.has(parsed.status)) continue;
@@ -14325,12 +15151,12 @@ var requiredFilesByStatus = {
14325
15151
  const { withAssignmentMd } = await listAssignments(ctx);
14326
15152
  const results = [];
14327
15153
  for (const a of withAssignmentMd) {
14328
- const assignmentPath = resolve40(a.assignmentDir, "assignment.md");
15154
+ const assignmentPath = resolve41(a.assignmentDir, "assignment.md");
14329
15155
  const parsed = await parseSafe(assignmentPath);
14330
15156
  if (!parsed) continue;
14331
15157
  const missing = [];
14332
15158
  if (STATUSES_REQUIRING_HANDOFF.has(parsed.status)) {
14333
- const handoffPath = resolve40(a.assignmentDir, "handoff.md");
15159
+ const handoffPath = resolve41(a.assignmentDir, "handoff.md");
14334
15160
  if (!await fileExists(handoffPath)) missing.push("handoff.md");
14335
15161
  }
14336
15162
  if (missing.length === 0) continue;
@@ -14340,7 +15166,7 @@ var requiredFilesByStatus = {
14340
15166
  title: this.title,
14341
15167
  status: "warn",
14342
15168
  detail: `${a.projectSlug}/${a.assignmentSlug} (status: ${parsed.status}) is missing ${missing.join(", ")}`,
14343
- affected: missing.map((m) => resolve40(a.assignmentDir, m)),
15169
+ affected: missing.map((m) => resolve41(a.assignmentDir, m)),
14344
15170
  remediation: {
14345
15171
  kind: "manual",
14346
15172
  suggestion: `Create the missing ${missing.join(" and ")} files for this assignment`,
@@ -14363,7 +15189,7 @@ var companionFilesScaffolded = {
14363
15189
  for (const a of withAssignmentMd) {
14364
15190
  const missing = [];
14365
15191
  for (const filename of ["progress.md", "comments.md"]) {
14366
- if (!await fileExists(resolve40(a.assignmentDir, filename))) {
15192
+ if (!await fileExists(resolve41(a.assignmentDir, filename))) {
14367
15193
  missing.push(filename);
14368
15194
  }
14369
15195
  }
@@ -14375,7 +15201,7 @@ var companionFilesScaffolded = {
14375
15201
  title: this.title,
14376
15202
  status: "warn",
14377
15203
  detail: `${label} is missing ${missing.join(" and ")} (pre-v2.0 assignment \u2014 not required, but scaffolding them keeps the dashboard and CLIs consistent)`,
14378
- affected: missing.map((m) => resolve40(a.assignmentDir, m)),
15204
+ affected: missing.map((m) => resolve41(a.assignmentDir, m)),
14379
15205
  remediation: {
14380
15206
  kind: "manual",
14381
15207
  suggestion: `Create ${missing.join(" and ")} with the renderProgress/renderComments templates, or re-scaffold via the CLI`,
@@ -14408,7 +15234,7 @@ var typeDefinition = {
14408
15234
  const { withAssignmentMd } = await listAssignments(ctx);
14409
15235
  const results = [];
14410
15236
  for (const a of withAssignmentMd) {
14411
- const path = resolve40(a.assignmentDir, "assignment.md");
15237
+ const path = resolve41(a.assignmentDir, "assignment.md");
14412
15238
  const parsed = await parseSafe(path);
14413
15239
  if (!parsed) continue;
14414
15240
  if (!parsed.type) continue;
@@ -14442,7 +15268,7 @@ var projectFrontmatterMatchesContainer = {
14442
15268
  const { withAssignmentMd } = await listAssignments(ctx);
14443
15269
  const results = [];
14444
15270
  for (const a of withAssignmentMd) {
14445
- const path = resolve40(a.assignmentDir, "assignment.md");
15271
+ const path = resolve41(a.assignmentDir, "assignment.md");
14446
15272
  const parsed = await parseSafe(path);
14447
15273
  if (!parsed) continue;
14448
15274
  if (a.standalone) {
@@ -14497,7 +15323,7 @@ var assignmentChecks = [
14497
15323
  ];
14498
15324
  async function parseSafe(path) {
14499
15325
  try {
14500
- const content = await readFile23(path, "utf-8");
15326
+ const content = await readFile25(path, "utf-8");
14501
15327
  return parseAssignmentFull(content);
14502
15328
  } catch {
14503
15329
  return null;
@@ -14516,7 +15342,7 @@ function pass4(check, detail) {
14516
15342
 
14517
15343
  // src/utils/doctor/checks/dashboard.ts
14518
15344
  init_fs();
14519
- import { resolve as resolve41 } from "path";
15345
+ import { resolve as resolve42 } from "path";
14520
15346
  var CATEGORY5 = "dashboard";
14521
15347
  var dbReachable = {
14522
15348
  id: "dashboard.db-reachable",
@@ -14530,7 +15356,7 @@ var dbReachable = {
14530
15356
  title: this.title,
14531
15357
  status: "error",
14532
15358
  detail: `could not open syntaur.db: ${ctx.dbError ?? "unknown error"}`,
14533
- affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
15359
+ affected: [resolve42(ctx.syntaurRoot, "syntaur.db")],
14534
15360
  remediation: {
14535
15361
  kind: "manual",
14536
15362
  suggestion: "Start the dashboard once (`syntaur dashboard`) to initialize the DB, or restore it from backup",
@@ -14548,7 +15374,7 @@ var dbReachable = {
14548
15374
  title: this.title,
14549
15375
  status: "error",
14550
15376
  detail: 'syntaur.db is missing the expected "sessions" table',
14551
- affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
15377
+ affected: [resolve42(ctx.syntaurRoot, "syntaur.db")],
14552
15378
  autoFixable: false
14553
15379
  };
14554
15380
  }
@@ -14560,7 +15386,7 @@ var dbReachable = {
14560
15386
  title: this.title,
14561
15387
  status: "error",
14562
15388
  detail: `syntaur.db query failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
14563
- affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
15389
+ affected: [resolve42(ctx.syntaurRoot, "syntaur.db")],
14564
15390
  autoFixable: false
14565
15391
  };
14566
15392
  }
@@ -14586,7 +15412,7 @@ var ghostSessions = {
14586
15412
  const results = [];
14587
15413
  for (const row of rows) {
14588
15414
  if (!row.project_slug) continue;
14589
- const projectPath = resolve41(projectsDir2, row.project_slug, "project.md");
15415
+ const projectPath = resolve42(projectsDir2, row.project_slug, "project.md");
14590
15416
  if (!await fileExists(projectPath)) {
14591
15417
  results.push({
14592
15418
  id: this.id,
@@ -14605,7 +15431,7 @@ var ghostSessions = {
14605
15431
  continue;
14606
15432
  }
14607
15433
  if (row.assignment_slug) {
14608
- const assignmentPath = resolve41(
15434
+ const assignmentPath = resolve42(
14609
15435
  projectsDir2,
14610
15436
  row.project_slug,
14611
15437
  "assignments",
@@ -14762,8 +15588,8 @@ function skipped2(check, reason) {
14762
15588
  init_fs();
14763
15589
  init_parser();
14764
15590
  init_types();
14765
- import { resolve as resolve42 } from "path";
14766
- import { readFile as readFile24 } from "fs/promises";
15591
+ import { resolve as resolve43 } from "path";
15592
+ import { readFile as readFile26 } from "fs/promises";
14767
15593
  var CATEGORY7 = "workspace";
14768
15594
  var ASSIGNMENT_FIELDS = ["projectSlug", "assignmentSlug", "projectDir", "assignmentDir"];
14769
15595
  function hasAnyAssignmentField(ctx) {
@@ -14775,12 +15601,12 @@ function isStandaloneSession(ctx) {
14775
15601
  return !hasAnyAssignmentField(ctx) && typeof ctx.sessionId === "string" && ctx.sessionId.length > 0;
14776
15602
  }
14777
15603
  async function loadContext(ctx) {
14778
- const path = resolve42(ctx.cwd, ".syntaur", "context.json");
15604
+ const path = resolve43(ctx.cwd, ".syntaur", "context.json");
14779
15605
  if (!await fileExists(path)) {
14780
15606
  return { data: null, path, exists: false, parseError: null };
14781
15607
  }
14782
15608
  try {
14783
- const raw = await readFile24(path, "utf-8");
15609
+ const raw = await readFile26(path, "utf-8");
14784
15610
  return { data: JSON.parse(raw), path, exists: true, parseError: null };
14785
15611
  } catch (err2) {
14786
15612
  return {
@@ -14855,7 +15681,7 @@ var contextAssignmentResolves = {
14855
15681
  if (!exists) return skipped3(this, "no context to resolve");
14856
15682
  if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to resolve");
14857
15683
  if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
14858
- const assignmentMd = resolve42(data.assignmentDir, "assignment.md");
15684
+ const assignmentMd = resolve43(data.assignmentDir, "assignment.md");
14859
15685
  if (!await fileExists(assignmentMd)) {
14860
15686
  return {
14861
15687
  id: this.id,
@@ -14884,10 +15710,10 @@ var contextTerminal = {
14884
15710
  if (!exists) return skipped3(this, "no context to check");
14885
15711
  if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to check");
14886
15712
  if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
14887
- const assignmentMd = resolve42(data.assignmentDir, "assignment.md");
15713
+ const assignmentMd = resolve43(data.assignmentDir, "assignment.md");
14888
15714
  if (!await fileExists(assignmentMd)) return skipped3(this, "assignment file missing");
14889
15715
  try {
14890
- const content = await readFile24(assignmentMd, "utf-8");
15716
+ const content = await readFile26(assignmentMd, "utf-8");
14891
15717
  const parsed = parseAssignmentFull(content);
14892
15718
  const terminal = terminalStatuses2(ctx);
14893
15719
  if (terminal.has(parsed.status)) {
@@ -14939,6 +15765,97 @@ function skipped3(check, reason) {
14939
15765
  };
14940
15766
  }
14941
15767
 
15768
+ // src/utils/doctor/checks/agents.ts
15769
+ init_config2();
15770
+ import { isAbsolute as isAbsolute6 } from "path";
15771
+ import { access as access2, constants as fsConstants } from "fs/promises";
15772
+ import { spawnSync as spawnSync3 } from "child_process";
15773
+ var CATEGORY8 = "agents";
15774
+ var agentsResolvable = {
15775
+ id: "agents.commands-resolvable",
15776
+ category: CATEGORY8,
15777
+ title: "All configured agent commands resolve",
15778
+ async run(ctx) {
15779
+ const agents = getAgents(ctx.config);
15780
+ if (agents.length === 0) {
15781
+ return {
15782
+ id: this.id,
15783
+ category: this.category,
15784
+ title: this.title,
15785
+ status: "warn",
15786
+ detail: "No agents configured and no built-in defaults available",
15787
+ remediation: {
15788
+ kind: "manual",
15789
+ suggestion: "Run `syntaur agents add --id <id> --label <label> --command <path>`",
15790
+ command: null
15791
+ },
15792
+ autoFixable: false
15793
+ };
15794
+ }
15795
+ const results = [];
15796
+ for (const agent of agents) {
15797
+ results.push(await checkAgent(agent));
15798
+ }
15799
+ return results;
15800
+ }
15801
+ };
15802
+ async function checkAgent(agent) {
15803
+ const base = {
15804
+ id: `agents.resolvable.${agent.id}`,
15805
+ category: CATEGORY8,
15806
+ title: `Agent "${agent.id}" command resolves`
15807
+ };
15808
+ if (agent.resolveFromShellAliases) {
15809
+ return {
15810
+ ...base,
15811
+ status: "pass",
15812
+ detail: `shell-alias resolution enabled for "${agent.command}" \u2014 will run via $SHELL -i -c`,
15813
+ autoFixable: false
15814
+ };
15815
+ }
15816
+ if (isAbsolute6(agent.command)) {
15817
+ try {
15818
+ await access2(agent.command, fsConstants.X_OK);
15819
+ return { ...base, status: "pass", autoFixable: false };
15820
+ } catch (err2) {
15821
+ const code = err2.code;
15822
+ const detail = code === "ENOENT" ? `absolute path "${agent.command}" does not exist` : code === "EACCES" ? `absolute path "${agent.command}" exists but is not executable (chmod +x?)` : `absolute path "${agent.command}" failed access check (${code ?? "unknown"})`;
15823
+ return {
15824
+ ...base,
15825
+ status: "warn",
15826
+ detail,
15827
+ remediation: {
15828
+ kind: "manual",
15829
+ suggestion: `Update with \`syntaur agents set ${agent.id} --command <path>\` or fix the file permissions`,
15830
+ command: null
15831
+ },
15832
+ autoFixable: false
15833
+ };
15834
+ }
15835
+ }
15836
+ const result = spawnSync3("which", [agent.command], { encoding: "utf-8" });
15837
+ if (result.status === 0 && result.stdout.trim().length > 0) {
15838
+ return {
15839
+ ...base,
15840
+ status: "pass",
15841
+ detail: `resolved "${agent.command}" \u2192 ${result.stdout.trim()}`,
15842
+ autoFixable: false
15843
+ };
15844
+ }
15845
+ return {
15846
+ ...base,
15847
+ status: "warn",
15848
+ detail: `bare command "${agent.command}" not found on PATH`,
15849
+ remediation: {
15850
+ kind: "manual",
15851
+ suggestion: `Install the binary, point at an absolute path with \`syntaur agents set ${agent.id} --command <abs-path>\`, or enable shell-alias resolution with \`syntaur agents set ${agent.id} --resolve-from-shell-aliases\``,
15852
+ command: null
15853
+ },
15854
+ autoFixable: false
15855
+ };
15856
+ }
15857
+ var agentChecks = [agentsResolvable];
15858
+
14942
15859
  // src/utils/doctor/registry.ts
14943
15860
  function allChecks() {
14944
15861
  return [
@@ -14948,7 +15865,8 @@ function allChecks() {
14948
15865
  ...assignmentChecks,
14949
15866
  ...dashboardChecks,
14950
15867
  ...integrationChecks,
14951
- ...workspaceChecks
15868
+ ...workspaceChecks,
15869
+ ...agentChecks
14952
15870
  ];
14953
15871
  }
14954
15872
 
@@ -15034,7 +15952,7 @@ async function readVersion() {
15034
15952
  let dir = dirname11(here);
15035
15953
  for (let i = 0; i < 6; i++) {
15036
15954
  try {
15037
- const raw = await readFile25(join5(dir, "package.json"), "utf-8");
15955
+ const raw = await readFile27(join5(dir, "package.json"), "utf-8");
15038
15956
  const parsed = JSON.parse(raw);
15039
15957
  return typeof parsed.version === "string" ? parsed.version : null;
15040
15958
  } catch {
@@ -15167,12 +16085,218 @@ var doctorCommand = new Command3("doctor").description("Diagnose Syntaur state a
15167
16085
  }
15168
16086
  });
15169
16087
 
16088
+ // src/commands/agents.ts
16089
+ init_config2();
16090
+ import { Command as Command4 } from "commander";
16091
+ var agentsCommand = new Command4("agents").description(
16092
+ "Manage configurable agents used by `syntaur browse` and future launch flows"
16093
+ );
16094
+ agentsCommand.command("list").description("List configured agents (or built-in defaults if none are configured)").action(async () => {
16095
+ try {
16096
+ const config = await readConfig();
16097
+ const agents = getAgents(config);
16098
+ const source = config.agents ? "config" : "built-in defaults";
16099
+ console.log(`Agents (${source}):`);
16100
+ for (const agent of agents) {
16101
+ const flags = [];
16102
+ if (agent.default) flags.push("default");
16103
+ if (agent.resolveFromShellAliases) flags.push("shell-alias");
16104
+ if (agent.promptArgPosition) flags.push(`prompt=${agent.promptArgPosition}`);
16105
+ const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
16106
+ const args = agent.args && agent.args.length > 0 ? ` ${agent.args.join(" ")}` : "";
16107
+ console.log(` ${agent.id.padEnd(12)} ${agent.label.padEnd(20)} ${agent.command}${args}${flagStr}`);
16108
+ }
16109
+ } catch (error) {
16110
+ reportAndExit(error);
16111
+ }
16112
+ });
16113
+ agentsCommand.command("add").description("Add a new agent to ~/.syntaur/config.md").requiredOption("--id <id>", "Agent id (slug)").requiredOption("--label <label>", "Display label").requiredOption("--command <command>", "Absolute path or bare binary name").option("--args <csv>", "Comma-separated default args").option("--prompt-arg-position <position>", "first | last | none").option("--default", "Mark this agent as the default launch target").option("--resolve-from-shell-aliases", "Run via $SHELL -i -c (for shell aliases)").option("--dry-run", "Validate and print the proposed config without writing").action(async (options) => {
16114
+ try {
16115
+ const agent = buildAgentFromOptions(options, null);
16116
+ const mutation = {
16117
+ kind: "add",
16118
+ apply: (current) => {
16119
+ if (current.some((a) => a.id === agent.id)) {
16120
+ throw new AgentConfigError(`agent "${agent.id}" already exists`);
16121
+ }
16122
+ const next = agent.default ? current.map((a) => ({ ...a, default: false })) : [...current];
16123
+ return [...next, agent];
16124
+ }
16125
+ };
16126
+ const result = await updateAgentsConfig(mutation, {
16127
+ dryRun: Boolean(options.dryRun)
16128
+ });
16129
+ reportMutation("add", result);
16130
+ } catch (error) {
16131
+ reportAndExit(error);
16132
+ }
16133
+ });
16134
+ agentsCommand.command("remove <id>").description("Remove an agent from ~/.syntaur/config.md").option("--dry-run", "Validate and print the proposed config without writing").action(async (id, options) => {
16135
+ try {
16136
+ const mutation = {
16137
+ kind: "remove",
16138
+ apply: (current) => {
16139
+ if (!current.some((a) => a.id === id)) {
16140
+ throw new AgentConfigError(`unknown agent id "${id}"`);
16141
+ }
16142
+ return current.filter((a) => a.id !== id);
16143
+ }
16144
+ };
16145
+ const result = await updateAgentsConfig(mutation, {
16146
+ dryRun: Boolean(options.dryRun)
16147
+ });
16148
+ reportMutation("remove", result);
16149
+ } catch (error) {
16150
+ reportAndExit(error);
16151
+ }
16152
+ });
16153
+ agentsCommand.command("set <id>").description("Update one or more fields on an existing agent").option("--label <label>", "Display label").option("--command <command>", "Absolute path or bare binary name").option("--args <csv>", "Comma-separated default args").option("--prompt-arg-position <position>", "first | last | none").option("--default", "Mark this agent as the default (clears any prior default)").option("--no-default", "Unset the default flag on this agent").option("--resolve-from-shell-aliases", "Run via $SHELL -i -c (for shell aliases)").option("--no-resolve-from-shell-aliases", "Disable shell-alias resolution for this agent").option("--dry-run", "Validate and print the proposed config without writing").action(async (id, options) => {
16154
+ try {
16155
+ const mutation = {
16156
+ kind: "set",
16157
+ apply: (current) => {
16158
+ const existing = current.find((a) => a.id === id);
16159
+ if (!existing) {
16160
+ throw new AgentConfigError(`unknown agent id "${id}"`);
16161
+ }
16162
+ const merged = mergeOptionsIntoAgent(existing, options);
16163
+ const defaultFlip = options.default === true;
16164
+ return current.map((a) => {
16165
+ if (a.id === id) return merged;
16166
+ if (defaultFlip) return { ...a, default: false };
16167
+ return a;
16168
+ });
16169
+ }
16170
+ };
16171
+ const result = await updateAgentsConfig(mutation, {
16172
+ dryRun: Boolean(options.dryRun)
16173
+ });
16174
+ reportMutation("set", result);
16175
+ } catch (error) {
16176
+ reportAndExit(error);
16177
+ }
16178
+ });
16179
+ agentsCommand.command("reorder <ids>").description("Reorder agents (comma-separated ids, must cover every configured agent exactly once)").option("--dry-run", "Validate and print the proposed config without writing").action(async (ids, options) => {
16180
+ try {
16181
+ const newOrder = ids.split(",").map((s) => s.trim()).filter(Boolean);
16182
+ const mutation = {
16183
+ kind: "reorder",
16184
+ apply: (current) => {
16185
+ const seen = /* @__PURE__ */ new Set();
16186
+ for (const id of newOrder) {
16187
+ if (seen.has(id)) {
16188
+ throw new AgentConfigError(`duplicate id "${id}" in reorder list`);
16189
+ }
16190
+ seen.add(id);
16191
+ }
16192
+ const currentIds = new Set(current.map((a) => a.id));
16193
+ const missing = current.filter((a) => !seen.has(a.id)).map((a) => a.id);
16194
+ const extra = newOrder.filter((id) => !currentIds.has(id));
16195
+ if (missing.length > 0 || extra.length > 0) {
16196
+ const parts = [];
16197
+ if (missing.length) parts.push(`missing: ${missing.join(", ")}`);
16198
+ if (extra.length) parts.push(`unknown: ${extra.join(", ")}`);
16199
+ throw new AgentConfigError(
16200
+ `reorder list does not match current agents (${parts.join("; ")})`
16201
+ );
16202
+ }
16203
+ return newOrder.map((id) => current.find((a) => a.id === id));
16204
+ }
16205
+ };
16206
+ const result = await updateAgentsConfig(mutation, {
16207
+ dryRun: Boolean(options.dryRun)
16208
+ });
16209
+ reportMutation("reorder", result);
16210
+ } catch (error) {
16211
+ reportAndExit(error);
16212
+ }
16213
+ });
16214
+ function buildAgentFromOptions(options, existing) {
16215
+ const agent = {
16216
+ id: options.id,
16217
+ label: options.label,
16218
+ command: parseAgentCommand(options.command, options.id)
16219
+ };
16220
+ const args = parseArgsCsv(options.args);
16221
+ if (args) agent.args = args;
16222
+ if (options.promptArgPosition) {
16223
+ agent.promptArgPosition = options.promptArgPosition;
16224
+ }
16225
+ if (options.default) agent.default = true;
16226
+ if (options.resolveFromShellAliases) agent.resolveFromShellAliases = true;
16227
+ validateAgentList([...existing ? [] : [], agent]);
16228
+ return agent;
16229
+ }
16230
+ function mergeOptionsIntoAgent(existing, options) {
16231
+ const merged = { ...existing };
16232
+ if (options.label !== void 0) merged.label = options.label;
16233
+ if (options.command !== void 0) {
16234
+ merged.command = parseAgentCommand(options.command, existing.id);
16235
+ }
16236
+ if (options.args !== void 0) {
16237
+ const parsed = parseArgsCsv(options.args);
16238
+ if (parsed) merged.args = parsed;
16239
+ else delete merged.args;
16240
+ }
16241
+ if (options.promptArgPosition !== void 0) {
16242
+ merged.promptArgPosition = options.promptArgPosition;
16243
+ }
16244
+ if (options.default === true) merged.default = true;
16245
+ if (options.default === false) delete merged.default;
16246
+ if (options.resolveFromShellAliases === true) merged.resolveFromShellAliases = true;
16247
+ if (options.resolveFromShellAliases === false) delete merged.resolveFromShellAliases;
16248
+ return merged;
16249
+ }
16250
+ function parseArgsCsv(csv) {
16251
+ if (csv === void 0) return null;
16252
+ const parts = csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
16253
+ return parts.length > 0 ? parts : null;
16254
+ }
16255
+ function reportMutation(action, result) {
16256
+ const diff = renderDiff(result.previous, result.next);
16257
+ if (result.written) {
16258
+ console.log(`Agents updated (${action}):`);
16259
+ console.log(diff);
16260
+ } else {
16261
+ console.log(`Dry run (${action}) \u2014 no changes written:`);
16262
+ console.log(diff);
16263
+ }
16264
+ }
16265
+ function renderDiff(prev, next) {
16266
+ const prevLines = prev.map(formatAgentLine);
16267
+ const nextLines = next.map(formatAgentLine);
16268
+ const prevSet = new Set(prevLines);
16269
+ const nextSet = new Set(nextLines);
16270
+ const out = [];
16271
+ for (const line of prevLines) {
16272
+ if (!nextSet.has(line)) out.push(` - ${line}`);
16273
+ }
16274
+ for (const line of nextLines) {
16275
+ if (!prevSet.has(line)) out.push(` + ${line}`);
16276
+ }
16277
+ if (out.length === 0) out.push(" (no changes)");
16278
+ return out.join("\n");
16279
+ }
16280
+ function formatAgentLine(a) {
16281
+ const flags = [];
16282
+ if (a.default) flags.push("default");
16283
+ if (a.resolveFromShellAliases) flags.push("shell-alias");
16284
+ if (a.promptArgPosition) flags.push(`prompt=${a.promptArgPosition}`);
16285
+ if (a.args && a.args.length > 0) flags.push(`args=[${a.args.join(", ")}]`);
16286
+ const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
16287
+ return `${a.id}: ${a.label} \u2192 ${a.command}${suffix}`;
16288
+ }
16289
+ function reportAndExit(error) {
16290
+ console.error("Error:", error instanceof Error ? error.message : String(error));
16291
+ process.exit(1);
16292
+ }
16293
+
15170
16294
  // src/commands/comment.ts
15171
16295
  init_paths();
15172
16296
  init_fs();
15173
16297
  init_config2();
15174
- import { resolve as resolve43 } from "path";
15175
- import { readFile as readFile26 } from "fs/promises";
16298
+ import { resolve as resolve44 } from "path";
16299
+ import { readFile as readFile28 } from "fs/promises";
15176
16300
  init_timestamp();
15177
16301
  init_assignment_resolver();
15178
16302
  function shortId() {
@@ -15204,7 +16328,7 @@ async function commentCommand(target, text, options = {}) {
15204
16328
  if (!isValidSlug(target)) {
15205
16329
  throw new Error(`Invalid assignment slug "${target}".`);
15206
16330
  }
15207
- assignmentDir = resolve43(baseDir, options.project, "assignments", target);
16331
+ assignmentDir = resolve44(baseDir, options.project, "assignments", target);
15208
16332
  assignmentRef = target;
15209
16333
  } else {
15210
16334
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -15214,13 +16338,13 @@ async function commentCommand(target, text, options = {}) {
15214
16338
  assignmentDir = resolved.assignmentDir;
15215
16339
  assignmentRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
15216
16340
  }
15217
- const commentsPath = resolve43(assignmentDir, "comments.md");
16341
+ const commentsPath = resolve44(assignmentDir, "comments.md");
15218
16342
  const timestamp = nowTimestamp();
15219
16343
  const author = options.author ?? process.env.USER ?? "unknown";
15220
16344
  let currentContent;
15221
16345
  let currentCount = 0;
15222
16346
  if (await fileExists(commentsPath)) {
15223
- currentContent = await readFile26(commentsPath, "utf-8");
16347
+ currentContent = await readFile28(commentsPath, "utf-8");
15224
16348
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
15225
16349
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
15226
16350
  } else {
@@ -15257,8 +16381,8 @@ ${entry}`;
15257
16381
  init_paths();
15258
16382
  init_fs();
15259
16383
  init_config2();
15260
- import { resolve as resolve44 } from "path";
15261
- import { readFile as readFile27 } from "fs/promises";
16384
+ import { resolve as resolve45 } from "path";
16385
+ import { readFile as readFile29 } from "fs/promises";
15262
16386
  init_timestamp();
15263
16387
  init_assignment_resolver();
15264
16388
  function setTopLevelField3(content, key, value) {
@@ -15283,7 +16407,7 @@ async function requestCommand(target, text, options = {}) {
15283
16407
  if (!isValidSlug(target)) {
15284
16408
  throw new Error(`Invalid assignment slug "${target}".`);
15285
16409
  }
15286
- assignmentDir = resolve44(baseDir, options.project, "assignments", target);
16410
+ assignmentDir = resolve45(baseDir, options.project, "assignments", target);
15287
16411
  targetRef = target;
15288
16412
  } else {
15289
16413
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -15293,12 +16417,12 @@ async function requestCommand(target, text, options = {}) {
15293
16417
  assignmentDir = resolved.assignmentDir;
15294
16418
  targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
15295
16419
  }
15296
- const assignmentMdPath = resolve44(assignmentDir, "assignment.md");
16420
+ const assignmentMdPath = resolve45(assignmentDir, "assignment.md");
15297
16421
  if (!await fileExists(assignmentMdPath)) {
15298
16422
  throw new Error(`assignment.md not found at ${assignmentMdPath}`);
15299
16423
  }
15300
16424
  const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
15301
- let content = await readFile27(assignmentMdPath, "utf-8");
16425
+ let content = await readFile29(assignmentMdPath, "utf-8");
15302
16426
  const todoLine = `- [ ] ${text.trim()} (from: ${source})`;
15303
16427
  const todosHeading = /^## Todos\s*$/m;
15304
16428
  if (todosHeading.test(content)) {
@@ -15366,20 +16490,20 @@ async function getDefaultCommandName() {
15366
16490
  init_paths();
15367
16491
  init_fs();
15368
16492
  import { fileURLToPath as fileURLToPath9 } from "url";
15369
- import { readFile as readFile29 } from "fs/promises";
15370
- import { dirname as dirname13, join as join7, resolve as resolve45 } from "path";
15371
- import { spawn as spawn3 } from "child_process";
16493
+ import { readFile as readFile31 } from "fs/promises";
16494
+ import { dirname as dirname13, join as join7, resolve as resolve46 } from "path";
16495
+ import { spawn as spawn4 } from "child_process";
15372
16496
  import { createInterface as createInterface2 } from "readline/promises";
15373
16497
 
15374
16498
  // src/utils/version.ts
15375
16499
  import { fileURLToPath as fileURLToPath8 } from "url";
15376
- import { readFile as readFile28 } from "fs/promises";
16500
+ import { readFile as readFile30 } from "fs/promises";
15377
16501
  import { dirname as dirname12, join as join6 } from "path";
15378
16502
  async function readPackageVersion(scriptUrl) {
15379
16503
  try {
15380
16504
  const scriptPath = fileURLToPath8(scriptUrl);
15381
16505
  const pkgRoot = dirname12(dirname12(scriptPath));
15382
- const raw = await readFile28(join6(pkgRoot, "package.json"), "utf-8");
16506
+ const raw = await readFile30(join6(pkgRoot, "package.json"), "utf-8");
15383
16507
  const parsed = JSON.parse(raw);
15384
16508
  return typeof parsed.version === "string" ? parsed.version : null;
15385
16509
  } catch {
@@ -15388,7 +16512,7 @@ async function readPackageVersion(scriptUrl) {
15388
16512
  }
15389
16513
 
15390
16514
  // src/utils/npx-prompt.ts
15391
- var STATE_FILE = resolve45(syntaurRoot(), "npx-install.json");
16515
+ var STATE_FILE = resolve46(syntaurRoot(), "npx-install.json");
15392
16516
  var META_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version", "help"]);
15393
16517
  var GLOBAL_VERSION_TIMEOUT_MS = 2e3;
15394
16518
  function isRunningViaNpx(scriptUrl) {
@@ -15409,7 +16533,7 @@ function isRunningViaNpx(scriptUrl) {
15409
16533
  async function readState() {
15410
16534
  if (!await fileExists(STATE_FILE)) return null;
15411
16535
  try {
15412
- const raw = await readFile29(STATE_FILE, "utf-8");
16536
+ const raw = await readFile31(STATE_FILE, "utf-8");
15413
16537
  return JSON.parse(raw);
15414
16538
  } catch {
15415
16539
  return null;
@@ -15432,7 +16556,7 @@ async function resolveNpmBin() {
15432
16556
  async function installGlobally() {
15433
16557
  const { cmd, shell } = await resolveNpmBin();
15434
16558
  return new Promise((resolvePromise) => {
15435
- const child = spawn3(cmd, ["install", "-g", "syntaur"], {
16559
+ const child = spawn4(cmd, ["install", "-g", "syntaur"], {
15436
16560
  stdio: "inherit",
15437
16561
  shell
15438
16562
  });
@@ -15443,7 +16567,7 @@ async function installGlobally() {
15443
16567
  async function readGlobalVersion() {
15444
16568
  const { cmd, shell } = await resolveNpmBin();
15445
16569
  const rootPath = await new Promise((resolvePromise) => {
15446
- const child = spawn3(cmd, ["root", "-g"], {
16570
+ const child = spawn4(cmd, ["root", "-g"], {
15447
16571
  shell,
15448
16572
  stdio: ["ignore", "pipe", "ignore"]
15449
16573
  });
@@ -15468,7 +16592,7 @@ async function readGlobalVersion() {
15468
16592
  try {
15469
16593
  const manifestPath = join7(rootPath, "syntaur", "package.json");
15470
16594
  if (!await fileExists(manifestPath)) return null;
15471
- const raw = await readFile29(manifestPath, "utf-8");
16595
+ const raw = await readFile31(manifestPath, "utf-8");
15472
16596
  const parsed = JSON.parse(raw);
15473
16597
  return typeof parsed.version === "string" ? parsed.version : null;
15474
16598
  } catch {
@@ -15587,7 +16711,7 @@ async function maybePromptInstall(scriptUrl) {
15587
16711
 
15588
16712
  // src/index.ts
15589
16713
  await maybePromptInstall(import.meta.url);
15590
- var program = new Command4();
16714
+ var program = new Command5();
15591
16715
  var version = await readPackageVersion(import.meta.url) ?? "0.0.0";
15592
16716
  program.name("syntaur").description("CLI scaffolding tool for the Syntaur protocol").version(version);
15593
16717
  program.command("init").description("Initialize ~/.syntaur/ directory structure and config").option("--force", "Overwrite existing config file").action(async (options) => {
@@ -15878,7 +17002,7 @@ program.command("track-session").description("Register an agent session (optiona
15878
17002
  process.exit(1);
15879
17003
  }
15880
17004
  });
15881
- program.command("browse").description("Interactive TUI browser for projects and assignments").option("--agent <type>", "Agent to launch: claude or codex", "claude").action(async (options) => {
17005
+ program.command("browse").description("Interactive TUI browser for projects and assignments").option("--agent <id>", "Bypass the agent picker and launch the given configured agent id").option("--no-worktree-prompt", "Skip the prompt to create a worktree when one is missing").action(async (options) => {
15882
17006
  try {
15883
17007
  await browseCommand(options);
15884
17008
  } catch (error) {
@@ -15936,6 +17060,7 @@ program.command("disable-playbook").description("Disable a playbook so agents no
15936
17060
  program.addCommand(todoCommand);
15937
17061
  program.addCommand(backupCommand);
15938
17062
  program.addCommand(doctorCommand);
17063
+ program.addCommand(agentsCommand);
15939
17064
  if (process.argv.length <= 2) {
15940
17065
  process.argv.push(await getDefaultCommandName());
15941
17066
  }