syntaur 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +332 -6
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +1253 -129
  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);
@@ -10298,6 +10936,7 @@ function createDashboardServer(options) {
10298
10936
  app.use("/api/backup", createBackupRouter());
10299
10937
  if (serveStaticUi && dashboardDistPath) {
10300
10938
  app.use("/assets", express.static(resolve21(dashboardDistPath, "assets")));
10939
+ app.use(express.static(dashboardDistPath, { index: false, fallthrough: true }));
10301
10940
  app.get("{*path}", async (req, res) => {
10302
10941
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
10303
10942
  res.status(404).json({ error: "Not Found" });
@@ -12673,10 +13312,29 @@ async function trackSessionCommand(options) {
12673
13312
 
12674
13313
  // src/commands/browse.ts
12675
13314
  init_config2();
13315
+ init_paths();
13316
+ init_fs();
13317
+ import { spawnSync as spawnSync2 } from "child_process";
13318
+ import { resolve as resolve33, isAbsolute as isAbsolute4 } from "path";
13319
+ import { readFile as readFile21 } from "fs/promises";
13320
+ import { select, confirm as confirm2, input as input3 } from "@inquirer/prompts";
12676
13321
  async function browseCommand(options) {
12677
13322
  const config = await readConfig();
12678
13323
  const projectsDir2 = config.defaultProjectDir;
12679
- const agent = options.agent;
13324
+ const agents = getAgents(config);
13325
+ if (agents.length === 0) {
13326
+ console.error(
13327
+ "No agents configured. Add one with `syntaur agents add --id <id> --label <label> --command <path>`."
13328
+ );
13329
+ process.exit(1);
13330
+ }
13331
+ const preSelectedAgent = options.agent ? agents.find((a) => a.id === options.agent) : void 0;
13332
+ if (options.agent && !preSelectedAgent) {
13333
+ console.error(
13334
+ `Unknown agent id "${options.agent}". Configured: ${agents.map((a) => a.id).join(", ")}`
13335
+ );
13336
+ process.exit(1);
13337
+ }
12680
13338
  const { render } = await import("ink");
12681
13339
  const React2 = await import("react");
12682
13340
  const { App: App2 } = await Promise.resolve().then(() => (init_App(), App_exports));
@@ -12687,7 +13345,19 @@ async function browseCommand(options) {
12687
13345
  unmount();
12688
13346
  unmount = null;
12689
13347
  }
12690
- await launchAgent2({ ...launchOpts, agent });
13348
+ const agent = preSelectedAgent ?? await pickAgent(agents);
13349
+ const cwdOverride = await ensureWorktree({
13350
+ projectsDir: launchOpts.projectsDir,
13351
+ projectSlug: launchOpts.projectSlug,
13352
+ assignmentSlug: launchOpts.assignmentSlug,
13353
+ worktreePromptEnabled: options.worktreePrompt !== false,
13354
+ autoCreateWorktree: config.agentDefaults.autoCreateWorktree
13355
+ });
13356
+ await launchAgent2({
13357
+ ...launchOpts,
13358
+ agent,
13359
+ ...cwdOverride ? { cwdOverride } : {}
13360
+ });
12691
13361
  };
12692
13362
  const instance = render(
12693
13363
  React2.createElement(App2, { projectsDir: projectsDir2, onLaunch })
@@ -12695,9 +13365,164 @@ async function browseCommand(options) {
12695
13365
  unmount = instance.unmount;
12696
13366
  await instance.waitUntilExit();
12697
13367
  }
13368
+ async function pickAgent(agents) {
13369
+ if (agents.length === 1) return agents[0];
13370
+ if (!isInteractiveTerminal()) {
13371
+ const fallback = agents.find((a) => a.default) ?? agents[0];
13372
+ console.warn(
13373
+ `syntaur: multiple agents configured but no TTY \u2014 using "${fallback.id}".`
13374
+ );
13375
+ return fallback;
13376
+ }
13377
+ const defaultAgent = agents.find((a) => a.default) ?? agents[0];
13378
+ const id = await select({
13379
+ message: "Launch which agent?",
13380
+ choices: agents.map((a) => ({ name: a.label, value: a.id, description: a.command })),
13381
+ default: defaultAgent.id
13382
+ });
13383
+ const picked = agents.find((a) => a.id === id);
13384
+ if (!picked) throw new Error(`Internal error: picker returned unknown agent id "${id}"`);
13385
+ return picked;
13386
+ }
13387
+ async function ensureWorktree(opts) {
13388
+ const assignmentPath = resolve33(
13389
+ opts.projectsDir,
13390
+ opts.projectSlug,
13391
+ "assignments",
13392
+ opts.assignmentSlug,
13393
+ "assignment.md"
13394
+ );
13395
+ if (!await fileExists(assignmentPath)) {
13396
+ return void 0;
13397
+ }
13398
+ const content = await readFile21(assignmentPath, "utf-8");
13399
+ const { parseAssignmentFrontmatter: parseAssignmentFrontmatter2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
13400
+ const fm = parseAssignmentFrontmatter2(content);
13401
+ const { workspace } = fm;
13402
+ if (workspace.worktreePath && workspace.branch) {
13403
+ return void 0;
13404
+ }
13405
+ if (!opts.worktreePromptEnabled) {
13406
+ return void 0;
13407
+ }
13408
+ if (opts.autoCreateWorktree === "skip") {
13409
+ return void 0;
13410
+ }
13411
+ const defaults = computeWorktreeDefaults({
13412
+ projectSlug: opts.projectSlug,
13413
+ assignmentSlug: opts.assignmentSlug,
13414
+ existing: workspace
13415
+ });
13416
+ if (!defaults.repository) {
13417
+ console.warn(
13418
+ `syntaur: cannot infer repository for ${opts.assignmentSlug} \u2014 skipping worktree prompt`
13419
+ );
13420
+ return void 0;
13421
+ }
13422
+ if (opts.autoCreateWorktree === "always") {
13423
+ return await runCreate({
13424
+ assignmentPath,
13425
+ repository: defaults.repository,
13426
+ branch: defaults.branch,
13427
+ parentBranch: defaults.parentBranch,
13428
+ worktreePath: defaults.worktreePath
13429
+ });
13430
+ }
13431
+ if (!isInteractiveTerminal()) {
13432
+ return void 0;
13433
+ }
13434
+ const proceed = await confirm2({
13435
+ message: `This assignment has no git worktree/branch configured. Create one?`,
13436
+ default: true
13437
+ });
13438
+ if (!proceed) {
13439
+ return void 0;
13440
+ }
13441
+ const repository = await input3({
13442
+ message: "Repository path:",
13443
+ default: defaults.repository
13444
+ });
13445
+ const branch = await input3({
13446
+ message: "Branch name:",
13447
+ default: defaults.branch
13448
+ });
13449
+ const parentBranch = await input3({
13450
+ message: "Parent branch:",
13451
+ default: defaults.parentBranch
13452
+ });
13453
+ const worktreePath = await input3({
13454
+ message: "Worktree path:",
13455
+ default: defaults.worktreePath
13456
+ });
13457
+ return await runCreate({
13458
+ assignmentPath,
13459
+ repository,
13460
+ branch,
13461
+ parentBranch,
13462
+ worktreePath
13463
+ });
13464
+ }
13465
+ function computeWorktreeDefaults(opts) {
13466
+ const repository = opts.existing.repository ?? detectCurrentGitRoot();
13467
+ const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
13468
+ const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch() ?? "main";
13469
+ const worktreeBase = resolve33(
13470
+ syntaurRoot(),
13471
+ "worktrees",
13472
+ opts.projectSlug || "standalone",
13473
+ opts.assignmentSlug
13474
+ );
13475
+ return {
13476
+ ...repository ? { repository } : {},
13477
+ branch,
13478
+ parentBranch,
13479
+ worktreePath: worktreeBase
13480
+ };
13481
+ }
13482
+ function detectCurrentGitRoot() {
13483
+ const result = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
13484
+ encoding: "utf-8"
13485
+ });
13486
+ if (result.status !== 0) return void 0;
13487
+ const out = result.stdout.trim();
13488
+ return out.length > 0 ? out : void 0;
13489
+ }
13490
+ function detectCurrentBranch() {
13491
+ const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
13492
+ encoding: "utf-8"
13493
+ });
13494
+ if (result.status !== 0) return void 0;
13495
+ const out = result.stdout.trim();
13496
+ if (!out || out === "HEAD") return void 0;
13497
+ return out;
13498
+ }
13499
+ async function runCreate(opts) {
13500
+ const { createWorktreeAndRecord: createWorktreeAndRecord2, GitWorktreeError: GitWorktreeError2 } = await Promise.resolve().then(() => (init_git_worktree(), git_worktree_exports));
13501
+ const expandedWorktree = expandHome(opts.worktreePath);
13502
+ const absWorktree = isAbsolute4(expandedWorktree) ? expandedWorktree : resolve33(expandedWorktree);
13503
+ try {
13504
+ await createWorktreeAndRecord2({
13505
+ assignmentPath: opts.assignmentPath,
13506
+ repository: opts.repository,
13507
+ branch: opts.branch,
13508
+ worktreePath: absWorktree,
13509
+ parentBranch: opts.parentBranch
13510
+ });
13511
+ console.log(`syntaur: created worktree at ${absWorktree} on branch ${opts.branch}`);
13512
+ return absWorktree;
13513
+ } catch (err2) {
13514
+ const msg = err2 instanceof Error ? err2.message : String(err2);
13515
+ if (err2 instanceof GitWorktreeError2) {
13516
+ console.error(`syntaur: ${msg}`);
13517
+ } else {
13518
+ console.error(`syntaur: ${msg}`);
13519
+ }
13520
+ process.exit(1);
13521
+ }
13522
+ }
12698
13523
 
12699
13524
  // src/commands/create-playbook.ts
12700
- import { resolve as resolve33 } from "path";
13525
+ import { resolve as resolve34 } from "path";
12701
13526
  init_timestamp();
12702
13527
  init_paths();
12703
13528
  init_fs();
@@ -12714,7 +13539,7 @@ async function createPlaybookCommand(name, options) {
12714
13539
  }
12715
13540
  const dir = playbooksDir();
12716
13541
  await ensureDir(dir);
12717
- const filePath = resolve33(dir, `${slug}.md`);
13542
+ const filePath = resolve34(dir, `${slug}.md`);
12718
13543
  if (await fileExists(filePath)) {
12719
13544
  throw new Error(
12720
13545
  `Playbook "${slug}" already exists at ${filePath}
@@ -12736,8 +13561,8 @@ init_paths();
12736
13561
  init_fs();
12737
13562
  init_parser();
12738
13563
  init_config2();
12739
- import { readdir as readdir12, readFile as readFile20 } from "fs/promises";
12740
- import { resolve as resolve34 } from "path";
13564
+ import { readdir as readdir12, readFile as readFile22 } from "fs/promises";
13565
+ import { resolve as resolve35 } from "path";
12741
13566
  async function listPlaybooksCommand(options = {}) {
12742
13567
  const dir = playbooksDir();
12743
13568
  if (!await fileExists(dir)) {
@@ -12752,8 +13577,8 @@ async function listPlaybooksCommand(options = {}) {
12752
13577
  );
12753
13578
  const rows = [];
12754
13579
  for (const entry of mdFiles) {
12755
- const filePath = resolve34(dir, entry.name);
12756
- const raw = await readFile20(filePath, "utf-8");
13580
+ const filePath = resolve35(dir, entry.name);
13581
+ const raw = await readFile22(filePath, "utf-8");
12757
13582
  const parsed = parsePlaybook(raw);
12758
13583
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
12759
13584
  const disabled = disabledSet.has(slug);
@@ -12835,8 +13660,8 @@ init_parser2();
12835
13660
  init_fs();
12836
13661
  init_config2();
12837
13662
  import { Command } from "commander";
12838
- import { readFile as readFile21 } from "fs/promises";
12839
- import { resolve as resolve35 } from "path";
13663
+ import { readFile as readFile23 } from "fs/promises";
13664
+ import { resolve as resolve36 } from "path";
12840
13665
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
12841
13666
  async function resolveScope(options) {
12842
13667
  const flagCount = [Boolean(options.project), Boolean(options.workspace), Boolean(options.global)].filter(Boolean).length;
@@ -12848,7 +13673,7 @@ async function resolveScope(options) {
12848
13673
  throw new Error(`Invalid project slug: "${options.project}".`);
12849
13674
  }
12850
13675
  const config = await readConfig();
12851
- const projectMd = resolve35(config.defaultProjectDir, options.project, "project.md");
13676
+ const projectMd = resolve36(config.defaultProjectDir, options.project, "project.md");
12852
13677
  if (!await fileExists(projectMd)) {
12853
13678
  throw new Error(`Project "${options.project}" not found.`);
12854
13679
  }
@@ -13142,10 +13967,10 @@ todoCommand.command("archive").description("Archive completed todos and their lo
13142
13967
  (e) => e.itemIds.every((id) => completedIds.has(id))
13143
13968
  );
13144
13969
  const archFile = archivePath(todosPath, workspace, checklist.archiveInterval);
13145
- await ensureDir(resolve35(todosPath, "archive"));
13970
+ await ensureDir(resolve36(todosPath, "archive"));
13146
13971
  let archContent = "";
13147
13972
  if (await fileExists(archFile)) {
13148
- archContent = await readFile21(archFile, "utf-8");
13973
+ archContent = await readFile23(archFile, "utf-8");
13149
13974
  archContent = archContent.trimEnd() + "\n\n";
13150
13975
  } else {
13151
13976
  archContent = `---
@@ -13335,7 +14160,7 @@ import { Command as Command3 } from "commander";
13335
14160
 
13336
14161
  // src/utils/doctor/index.ts
13337
14162
  import { fileURLToPath as fileURLToPath7 } from "url";
13338
- import { readFile as readFile25 } from "fs/promises";
14163
+ import { readFile as readFile27 } from "fs/promises";
13339
14164
  import { dirname as dirname11, join as join5 } from "path";
13340
14165
 
13341
14166
  // src/utils/doctor/context.ts
@@ -13343,11 +14168,11 @@ init_config2();
13343
14168
  init_paths();
13344
14169
  init_fs();
13345
14170
  import Database2 from "better-sqlite3";
13346
- import { resolve as resolve36 } from "path";
14171
+ import { resolve as resolve37 } from "path";
13347
14172
  async function buildCheckContext(cwd = process.cwd()) {
13348
14173
  const config = await readConfig();
13349
14174
  const root = syntaurRoot();
13350
- const dbPath = resolve36(root, "syntaur.db");
14175
+ const dbPath = resolve37(root, "syntaur.db");
13351
14176
  let db2 = null;
13352
14177
  let dbError = null;
13353
14178
  if (await fileExists(dbPath)) {
@@ -13381,8 +14206,8 @@ function closeCheckContext(ctx) {
13381
14206
  // src/utils/doctor/checks/env.ts
13382
14207
  init_fs();
13383
14208
  init_paths();
13384
- import { resolve as resolve37, isAbsolute as isAbsolute3 } from "path";
13385
- import { readFile as readFile22, stat as stat4 } from "fs/promises";
14209
+ import { resolve as resolve38, isAbsolute as isAbsolute5 } from "path";
14210
+ import { readFile as readFile24, stat as stat4 } from "fs/promises";
13386
14211
  import { fileURLToPath as fileURLToPath6 } from "url";
13387
14212
  import { dirname as dirname10, join as join4 } from "path";
13388
14213
  var CATEGORY = "env";
@@ -13422,7 +14247,7 @@ var configValid = {
13422
14247
  category: CATEGORY,
13423
14248
  title: "~/.syntaur/config.md is valid",
13424
14249
  async run(ctx) {
13425
- const configPath = resolve37(ctx.syntaurRoot, "config.md");
14250
+ const configPath = resolve38(ctx.syntaurRoot, "config.md");
13426
14251
  if (!await fileExists(configPath)) {
13427
14252
  return {
13428
14253
  id: this.id,
@@ -13439,7 +14264,7 @@ var configValid = {
13439
14264
  autoFixable: false
13440
14265
  };
13441
14266
  }
13442
- const content = await readFile22(configPath, "utf-8");
14267
+ const content = await readFile24(configPath, "utf-8");
13443
14268
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
13444
14269
  if (!fmMatch || fmMatch[1].trim() === "") {
13445
14270
  return {
@@ -13476,7 +14301,7 @@ var configValid = {
13476
14301
  };
13477
14302
  }
13478
14303
  const expanded = expandHome(rawProjectDir);
13479
- if (!isAbsolute3(expanded)) {
14304
+ if (!isAbsolute5(expanded)) {
13480
14305
  return {
13481
14306
  id: this.id,
13482
14307
  category: this.category,
@@ -13719,7 +14544,7 @@ async function readLocalPkg() {
13719
14544
  for (let i = 0; i < 6; i++) {
13720
14545
  const candidate = join4(dir, "package.json");
13721
14546
  try {
13722
- const text = await readFile22(candidate, "utf-8");
14547
+ const text = await readFile24(candidate, "utf-8");
13723
14548
  return JSON.parse(text);
13724
14549
  } catch {
13725
14550
  dir = dirname10(dir);
@@ -13771,7 +14596,7 @@ function versionGte(a, b) {
13771
14596
 
13772
14597
  // src/utils/doctor/checks/structure.ts
13773
14598
  init_fs();
13774
- import { resolve as resolve38 } from "path";
14599
+ import { resolve as resolve39 } from "path";
13775
14600
  import { readdir as readdir13, stat as stat5 } from "fs/promises";
13776
14601
  var CATEGORY2 = "structure";
13777
14602
  var KNOWN_TOP_LEVEL = /* @__PURE__ */ new Set([
@@ -13791,7 +14616,7 @@ var projectsDir = {
13791
14616
  category: CATEGORY2,
13792
14617
  title: "projects/ directory exists",
13793
14618
  async run(ctx) {
13794
- const p = resolve38(ctx.syntaurRoot, "projects");
14619
+ const p = resolve39(ctx.syntaurRoot, "projects");
13795
14620
  if (!await fileExists(p)) {
13796
14621
  return {
13797
14622
  id: this.id,
@@ -13816,7 +14641,7 @@ var playbooksDir2 = {
13816
14641
  category: CATEGORY2,
13817
14642
  title: "playbooks/ directory exists",
13818
14643
  async run(ctx) {
13819
- const p = resolve38(ctx.syntaurRoot, "playbooks");
14644
+ const p = resolve39(ctx.syntaurRoot, "playbooks");
13820
14645
  if (!await fileExists(p)) {
13821
14646
  return {
13822
14647
  id: this.id,
@@ -13841,7 +14666,7 @@ var todosDirValid = {
13841
14666
  category: CATEGORY2,
13842
14667
  title: "todos/ directory is readable (if present)",
13843
14668
  async run(ctx) {
13844
- const p = resolve38(ctx.syntaurRoot, "todos");
14669
+ const p = resolve39(ctx.syntaurRoot, "todos");
13845
14670
  if (!await fileExists(p)) {
13846
14671
  return {
13847
14672
  id: this.id,
@@ -13872,7 +14697,7 @@ var serversDirValid = {
13872
14697
  category: CATEGORY2,
13873
14698
  title: "servers/ directory is readable (if present)",
13874
14699
  async run(ctx) {
13875
- const p = resolve38(ctx.syntaurRoot, "servers");
14700
+ const p = resolve39(ctx.syntaurRoot, "servers");
13876
14701
  if (!await fileExists(p)) {
13877
14702
  return {
13878
14703
  id: this.id,
@@ -13917,7 +14742,7 @@ var knownFilesRecognized = {
13917
14742
  title: this.title,
13918
14743
  status: "warn",
13919
14744
  detail: `unexpected top-level entries: ${unexpected.join(", ")}`,
13920
- affected: unexpected.map((n) => resolve38(ctx.syntaurRoot, n)),
14745
+ affected: unexpected.map((n) => resolve39(ctx.syntaurRoot, n)),
13921
14746
  remediation: {
13922
14747
  kind: "manual",
13923
14748
  suggestion: "Review these entries \u2014 they may be leftover state from older versions",
@@ -13946,7 +14771,7 @@ function pass2(check) {
13946
14771
 
13947
14772
  // src/utils/doctor/checks/project.ts
13948
14773
  init_fs();
13949
- import { resolve as resolve39 } from "path";
14774
+ import { resolve as resolve40 } from "path";
13950
14775
  import { readdir as readdir14, stat as stat6 } from "fs/promises";
13951
14776
  var CATEGORY3 = "project";
13952
14777
  var REQUIRED_PROJECT_FILES = [
@@ -13976,10 +14801,10 @@ async function listProjects2(ctx) {
13976
14801
  for (const e of entries) {
13977
14802
  if (!e.isDirectory()) continue;
13978
14803
  if (e.name.startsWith(".") || e.name.startsWith("_")) continue;
13979
- const projectDir = resolve39(dir, e.name);
14804
+ const projectDir = resolve40(dir, e.name);
13980
14805
  let looksLikeProject = false;
13981
14806
  for (const marker of PROJECT_MARKERS) {
13982
- if (await fileExists(resolve39(projectDir, marker))) {
14807
+ if (await fileExists(resolve40(projectDir, marker))) {
13983
14808
  looksLikeProject = true;
13984
14809
  break;
13985
14810
  }
@@ -13998,7 +14823,7 @@ var requiredFiles = {
13998
14823
  for (const projectDir of projects) {
13999
14824
  const missing = [];
14000
14825
  for (const rel of REQUIRED_PROJECT_FILES) {
14001
- const p = resolve39(projectDir, rel);
14826
+ const p = resolve40(projectDir, rel);
14002
14827
  if (!await fileExists(p)) missing.push(rel);
14003
14828
  }
14004
14829
  if (missing.length === 0) continue;
@@ -14008,7 +14833,7 @@ var requiredFiles = {
14008
14833
  title: this.title,
14009
14834
  status: "error",
14010
14835
  detail: `project at ${projectDir} is missing: ${missing.join(", ")}`,
14011
- affected: missing.map((m) => resolve39(projectDir, m)),
14836
+ affected: missing.map((m) => resolve40(projectDir, m)),
14012
14837
  remediation: {
14013
14838
  kind: "manual",
14014
14839
  suggestion: "Recreate the missing scaffold files from templates",
@@ -14031,7 +14856,7 @@ var manifestStale = {
14031
14856
  const projects = await listProjects2(ctx);
14032
14857
  const results = [];
14033
14858
  for (const projectDir of projects) {
14034
- const manifestPath = resolve39(projectDir, "manifest.md");
14859
+ const manifestPath = resolve40(projectDir, "manifest.md");
14035
14860
  if (!await fileExists(manifestPath)) continue;
14036
14861
  const manifestMtime = (await stat6(manifestPath)).mtimeMs;
14037
14862
  const newestAssignment = await newestAssignmentMtime(projectDir);
@@ -14080,7 +14905,7 @@ var orphanFiles = {
14080
14905
  title: this.title,
14081
14906
  status: "warn",
14082
14907
  detail: `project at ${projectDir} has unexpected entries: ${orphans.join(", ")}`,
14083
- affected: orphans.map((o) => resolve39(projectDir, o)),
14908
+ affected: orphans.map((o) => resolve40(projectDir, o)),
14084
14909
  autoFixable: false
14085
14910
  });
14086
14911
  }
@@ -14090,7 +14915,7 @@ var orphanFiles = {
14090
14915
  };
14091
14916
  var projectChecks = [requiredFiles, manifestStale, orphanFiles];
14092
14917
  async function newestAssignmentMtime(projectDir) {
14093
- const assignmentsRoot = resolve39(projectDir, "assignments");
14918
+ const assignmentsRoot = resolve40(projectDir, "assignments");
14094
14919
  if (!await fileExists(assignmentsRoot)) return 0;
14095
14920
  let newest = 0;
14096
14921
  let entries;
@@ -14101,7 +14926,7 @@ async function newestAssignmentMtime(projectDir) {
14101
14926
  }
14102
14927
  for (const e of entries) {
14103
14928
  if (!e.isDirectory()) continue;
14104
- const assignmentMd = resolve39(assignmentsRoot, e.name, "assignment.md");
14929
+ const assignmentMd = resolve40(assignmentsRoot, e.name, "assignment.md");
14105
14930
  try {
14106
14931
  const s = await stat6(assignmentMd);
14107
14932
  if (s.mtimeMs > newest) newest = s.mtimeMs;
@@ -14125,8 +14950,8 @@ init_fs();
14125
14950
  init_parser();
14126
14951
  init_types();
14127
14952
  init_paths();
14128
- import { resolve as resolve40 } from "path";
14129
- import { readdir as readdir15, readFile as readFile23 } from "fs/promises";
14953
+ import { resolve as resolve41 } from "path";
14954
+ import { readdir as readdir15, readFile as readFile25 } from "fs/promises";
14130
14955
  var CATEGORY4 = "assignment";
14131
14956
  var STATUSES_REQUIRING_HANDOFF = /* @__PURE__ */ new Set(["review", "completed"]);
14132
14957
  async function listAssignments(ctx) {
@@ -14137,16 +14962,16 @@ async function listAssignments(ctx) {
14137
14962
  for (const m of projects) {
14138
14963
  if (!m.isDirectory()) continue;
14139
14964
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
14140
- const assignmentsDir2 = resolve40(projectsDir2, m.name, "assignments");
14965
+ const assignmentsDir2 = resolve41(projectsDir2, m.name, "assignments");
14141
14966
  if (!await fileExists(assignmentsDir2)) continue;
14142
14967
  const entries = await readdir15(assignmentsDir2, { withFileTypes: true });
14143
14968
  for (const a of entries) {
14144
14969
  if (!a.isDirectory()) continue;
14145
14970
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
14146
- const assignmentDir = resolve40(assignmentsDir2, a.name);
14147
- const assignmentMd = resolve40(assignmentDir, "assignment.md");
14971
+ const assignmentDir = resolve41(assignmentsDir2, a.name);
14972
+ const assignmentMd = resolve41(assignmentDir, "assignment.md");
14148
14973
  const entry = {
14149
- projectDir: resolve40(projectsDir2, m.name),
14974
+ projectDir: resolve41(projectsDir2, m.name),
14150
14975
  projectSlug: m.name,
14151
14976
  assignmentDir,
14152
14977
  assignmentSlug: a.name,
@@ -14166,8 +14991,8 @@ async function listAssignments(ctx) {
14166
14991
  for (const a of entries) {
14167
14992
  if (!a.isDirectory()) continue;
14168
14993
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
14169
- const assignmentDir = resolve40(standaloneRoot, a.name);
14170
- const assignmentMd = resolve40(assignmentDir, "assignment.md");
14994
+ const assignmentDir = resolve41(standaloneRoot, a.name);
14995
+ const assignmentMd = resolve41(assignmentDir, "assignment.md");
14171
14996
  const entry = {
14172
14997
  projectDir: standaloneRoot,
14173
14998
  projectSlug: null,
@@ -14245,7 +15070,7 @@ var invalidStatus = {
14245
15070
  const allowed = configuredStatuses(ctx);
14246
15071
  const results = [];
14247
15072
  for (const a of withAssignmentMd) {
14248
- const path = resolve40(a.assignmentDir, "assignment.md");
15073
+ const path = resolve41(a.assignmentDir, "assignment.md");
14249
15074
  const parsed = await parseSafe(path);
14250
15075
  if (!parsed) continue;
14251
15076
  if (!allowed.has(parsed.status)) {
@@ -14278,7 +15103,7 @@ var workspaceMissing = {
14278
15103
  const terminal = terminalStatuses(ctx);
14279
15104
  const results = [];
14280
15105
  for (const a of withAssignmentMd) {
14281
- const path = resolve40(a.assignmentDir, "assignment.md");
15106
+ const path = resolve41(a.assignmentDir, "assignment.md");
14282
15107
  const parsed = await parseSafe(path);
14283
15108
  if (!parsed) continue;
14284
15109
  if (terminal.has(parsed.status)) continue;
@@ -14325,12 +15150,12 @@ var requiredFilesByStatus = {
14325
15150
  const { withAssignmentMd } = await listAssignments(ctx);
14326
15151
  const results = [];
14327
15152
  for (const a of withAssignmentMd) {
14328
- const assignmentPath = resolve40(a.assignmentDir, "assignment.md");
15153
+ const assignmentPath = resolve41(a.assignmentDir, "assignment.md");
14329
15154
  const parsed = await parseSafe(assignmentPath);
14330
15155
  if (!parsed) continue;
14331
15156
  const missing = [];
14332
15157
  if (STATUSES_REQUIRING_HANDOFF.has(parsed.status)) {
14333
- const handoffPath = resolve40(a.assignmentDir, "handoff.md");
15158
+ const handoffPath = resolve41(a.assignmentDir, "handoff.md");
14334
15159
  if (!await fileExists(handoffPath)) missing.push("handoff.md");
14335
15160
  }
14336
15161
  if (missing.length === 0) continue;
@@ -14340,7 +15165,7 @@ var requiredFilesByStatus = {
14340
15165
  title: this.title,
14341
15166
  status: "warn",
14342
15167
  detail: `${a.projectSlug}/${a.assignmentSlug} (status: ${parsed.status}) is missing ${missing.join(", ")}`,
14343
- affected: missing.map((m) => resolve40(a.assignmentDir, m)),
15168
+ affected: missing.map((m) => resolve41(a.assignmentDir, m)),
14344
15169
  remediation: {
14345
15170
  kind: "manual",
14346
15171
  suggestion: `Create the missing ${missing.join(" and ")} files for this assignment`,
@@ -14363,7 +15188,7 @@ var companionFilesScaffolded = {
14363
15188
  for (const a of withAssignmentMd) {
14364
15189
  const missing = [];
14365
15190
  for (const filename of ["progress.md", "comments.md"]) {
14366
- if (!await fileExists(resolve40(a.assignmentDir, filename))) {
15191
+ if (!await fileExists(resolve41(a.assignmentDir, filename))) {
14367
15192
  missing.push(filename);
14368
15193
  }
14369
15194
  }
@@ -14375,7 +15200,7 @@ var companionFilesScaffolded = {
14375
15200
  title: this.title,
14376
15201
  status: "warn",
14377
15202
  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)),
15203
+ affected: missing.map((m) => resolve41(a.assignmentDir, m)),
14379
15204
  remediation: {
14380
15205
  kind: "manual",
14381
15206
  suggestion: `Create ${missing.join(" and ")} with the renderProgress/renderComments templates, or re-scaffold via the CLI`,
@@ -14408,7 +15233,7 @@ var typeDefinition = {
14408
15233
  const { withAssignmentMd } = await listAssignments(ctx);
14409
15234
  const results = [];
14410
15235
  for (const a of withAssignmentMd) {
14411
- const path = resolve40(a.assignmentDir, "assignment.md");
15236
+ const path = resolve41(a.assignmentDir, "assignment.md");
14412
15237
  const parsed = await parseSafe(path);
14413
15238
  if (!parsed) continue;
14414
15239
  if (!parsed.type) continue;
@@ -14442,7 +15267,7 @@ var projectFrontmatterMatchesContainer = {
14442
15267
  const { withAssignmentMd } = await listAssignments(ctx);
14443
15268
  const results = [];
14444
15269
  for (const a of withAssignmentMd) {
14445
- const path = resolve40(a.assignmentDir, "assignment.md");
15270
+ const path = resolve41(a.assignmentDir, "assignment.md");
14446
15271
  const parsed = await parseSafe(path);
14447
15272
  if (!parsed) continue;
14448
15273
  if (a.standalone) {
@@ -14497,7 +15322,7 @@ var assignmentChecks = [
14497
15322
  ];
14498
15323
  async function parseSafe(path) {
14499
15324
  try {
14500
- const content = await readFile23(path, "utf-8");
15325
+ const content = await readFile25(path, "utf-8");
14501
15326
  return parseAssignmentFull(content);
14502
15327
  } catch {
14503
15328
  return null;
@@ -14516,7 +15341,7 @@ function pass4(check, detail) {
14516
15341
 
14517
15342
  // src/utils/doctor/checks/dashboard.ts
14518
15343
  init_fs();
14519
- import { resolve as resolve41 } from "path";
15344
+ import { resolve as resolve42 } from "path";
14520
15345
  var CATEGORY5 = "dashboard";
14521
15346
  var dbReachable = {
14522
15347
  id: "dashboard.db-reachable",
@@ -14530,7 +15355,7 @@ var dbReachable = {
14530
15355
  title: this.title,
14531
15356
  status: "error",
14532
15357
  detail: `could not open syntaur.db: ${ctx.dbError ?? "unknown error"}`,
14533
- affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
15358
+ affected: [resolve42(ctx.syntaurRoot, "syntaur.db")],
14534
15359
  remediation: {
14535
15360
  kind: "manual",
14536
15361
  suggestion: "Start the dashboard once (`syntaur dashboard`) to initialize the DB, or restore it from backup",
@@ -14548,7 +15373,7 @@ var dbReachable = {
14548
15373
  title: this.title,
14549
15374
  status: "error",
14550
15375
  detail: 'syntaur.db is missing the expected "sessions" table',
14551
- affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
15376
+ affected: [resolve42(ctx.syntaurRoot, "syntaur.db")],
14552
15377
  autoFixable: false
14553
15378
  };
14554
15379
  }
@@ -14560,7 +15385,7 @@ var dbReachable = {
14560
15385
  title: this.title,
14561
15386
  status: "error",
14562
15387
  detail: `syntaur.db query failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
14563
- affected: [resolve41(ctx.syntaurRoot, "syntaur.db")],
15388
+ affected: [resolve42(ctx.syntaurRoot, "syntaur.db")],
14564
15389
  autoFixable: false
14565
15390
  };
14566
15391
  }
@@ -14586,7 +15411,7 @@ var ghostSessions = {
14586
15411
  const results = [];
14587
15412
  for (const row of rows) {
14588
15413
  if (!row.project_slug) continue;
14589
- const projectPath = resolve41(projectsDir2, row.project_slug, "project.md");
15414
+ const projectPath = resolve42(projectsDir2, row.project_slug, "project.md");
14590
15415
  if (!await fileExists(projectPath)) {
14591
15416
  results.push({
14592
15417
  id: this.id,
@@ -14605,7 +15430,7 @@ var ghostSessions = {
14605
15430
  continue;
14606
15431
  }
14607
15432
  if (row.assignment_slug) {
14608
- const assignmentPath = resolve41(
15433
+ const assignmentPath = resolve42(
14609
15434
  projectsDir2,
14610
15435
  row.project_slug,
14611
15436
  "assignments",
@@ -14762,8 +15587,8 @@ function skipped2(check, reason) {
14762
15587
  init_fs();
14763
15588
  init_parser();
14764
15589
  init_types();
14765
- import { resolve as resolve42 } from "path";
14766
- import { readFile as readFile24 } from "fs/promises";
15590
+ import { resolve as resolve43 } from "path";
15591
+ import { readFile as readFile26 } from "fs/promises";
14767
15592
  var CATEGORY7 = "workspace";
14768
15593
  var ASSIGNMENT_FIELDS = ["projectSlug", "assignmentSlug", "projectDir", "assignmentDir"];
14769
15594
  function hasAnyAssignmentField(ctx) {
@@ -14775,12 +15600,12 @@ function isStandaloneSession(ctx) {
14775
15600
  return !hasAnyAssignmentField(ctx) && typeof ctx.sessionId === "string" && ctx.sessionId.length > 0;
14776
15601
  }
14777
15602
  async function loadContext(ctx) {
14778
- const path = resolve42(ctx.cwd, ".syntaur", "context.json");
15603
+ const path = resolve43(ctx.cwd, ".syntaur", "context.json");
14779
15604
  if (!await fileExists(path)) {
14780
15605
  return { data: null, path, exists: false, parseError: null };
14781
15606
  }
14782
15607
  try {
14783
- const raw = await readFile24(path, "utf-8");
15608
+ const raw = await readFile26(path, "utf-8");
14784
15609
  return { data: JSON.parse(raw), path, exists: true, parseError: null };
14785
15610
  } catch (err2) {
14786
15611
  return {
@@ -14855,7 +15680,7 @@ var contextAssignmentResolves = {
14855
15680
  if (!exists) return skipped3(this, "no context to resolve");
14856
15681
  if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to resolve");
14857
15682
  if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
14858
- const assignmentMd = resolve42(data.assignmentDir, "assignment.md");
15683
+ const assignmentMd = resolve43(data.assignmentDir, "assignment.md");
14859
15684
  if (!await fileExists(assignmentMd)) {
14860
15685
  return {
14861
15686
  id: this.id,
@@ -14884,10 +15709,10 @@ var contextTerminal = {
14884
15709
  if (!exists) return skipped3(this, "no context to check");
14885
15710
  if (isStandaloneSession(data)) return skipped3(this, "standalone session context \u2014 no assignment to check");
14886
15711
  if (!data?.assignmentDir) return skipped3(this, "context has no assignmentDir");
14887
- const assignmentMd = resolve42(data.assignmentDir, "assignment.md");
15712
+ const assignmentMd = resolve43(data.assignmentDir, "assignment.md");
14888
15713
  if (!await fileExists(assignmentMd)) return skipped3(this, "assignment file missing");
14889
15714
  try {
14890
- const content = await readFile24(assignmentMd, "utf-8");
15715
+ const content = await readFile26(assignmentMd, "utf-8");
14891
15716
  const parsed = parseAssignmentFull(content);
14892
15717
  const terminal = terminalStatuses2(ctx);
14893
15718
  if (terminal.has(parsed.status)) {
@@ -14939,6 +15764,97 @@ function skipped3(check, reason) {
14939
15764
  };
14940
15765
  }
14941
15766
 
15767
+ // src/utils/doctor/checks/agents.ts
15768
+ init_config2();
15769
+ import { isAbsolute as isAbsolute6 } from "path";
15770
+ import { access as access2, constants as fsConstants } from "fs/promises";
15771
+ import { spawnSync as spawnSync3 } from "child_process";
15772
+ var CATEGORY8 = "agents";
15773
+ var agentsResolvable = {
15774
+ id: "agents.commands-resolvable",
15775
+ category: CATEGORY8,
15776
+ title: "All configured agent commands resolve",
15777
+ async run(ctx) {
15778
+ const agents = getAgents(ctx.config);
15779
+ if (agents.length === 0) {
15780
+ return {
15781
+ id: this.id,
15782
+ category: this.category,
15783
+ title: this.title,
15784
+ status: "warn",
15785
+ detail: "No agents configured and no built-in defaults available",
15786
+ remediation: {
15787
+ kind: "manual",
15788
+ suggestion: "Run `syntaur agents add --id <id> --label <label> --command <path>`",
15789
+ command: null
15790
+ },
15791
+ autoFixable: false
15792
+ };
15793
+ }
15794
+ const results = [];
15795
+ for (const agent of agents) {
15796
+ results.push(await checkAgent(agent));
15797
+ }
15798
+ return results;
15799
+ }
15800
+ };
15801
+ async function checkAgent(agent) {
15802
+ const base = {
15803
+ id: `agents.resolvable.${agent.id}`,
15804
+ category: CATEGORY8,
15805
+ title: `Agent "${agent.id}" command resolves`
15806
+ };
15807
+ if (agent.resolveFromShellAliases) {
15808
+ return {
15809
+ ...base,
15810
+ status: "pass",
15811
+ detail: `shell-alias resolution enabled for "${agent.command}" \u2014 will run via $SHELL -i -c`,
15812
+ autoFixable: false
15813
+ };
15814
+ }
15815
+ if (isAbsolute6(agent.command)) {
15816
+ try {
15817
+ await access2(agent.command, fsConstants.X_OK);
15818
+ return { ...base, status: "pass", autoFixable: false };
15819
+ } catch (err2) {
15820
+ const code = err2.code;
15821
+ 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"})`;
15822
+ return {
15823
+ ...base,
15824
+ status: "warn",
15825
+ detail,
15826
+ remediation: {
15827
+ kind: "manual",
15828
+ suggestion: `Update with \`syntaur agents set ${agent.id} --command <path>\` or fix the file permissions`,
15829
+ command: null
15830
+ },
15831
+ autoFixable: false
15832
+ };
15833
+ }
15834
+ }
15835
+ const result = spawnSync3("which", [agent.command], { encoding: "utf-8" });
15836
+ if (result.status === 0 && result.stdout.trim().length > 0) {
15837
+ return {
15838
+ ...base,
15839
+ status: "pass",
15840
+ detail: `resolved "${agent.command}" \u2192 ${result.stdout.trim()}`,
15841
+ autoFixable: false
15842
+ };
15843
+ }
15844
+ return {
15845
+ ...base,
15846
+ status: "warn",
15847
+ detail: `bare command "${agent.command}" not found on PATH`,
15848
+ remediation: {
15849
+ kind: "manual",
15850
+ 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\``,
15851
+ command: null
15852
+ },
15853
+ autoFixable: false
15854
+ };
15855
+ }
15856
+ var agentChecks = [agentsResolvable];
15857
+
14942
15858
  // src/utils/doctor/registry.ts
14943
15859
  function allChecks() {
14944
15860
  return [
@@ -14948,7 +15864,8 @@ function allChecks() {
14948
15864
  ...assignmentChecks,
14949
15865
  ...dashboardChecks,
14950
15866
  ...integrationChecks,
14951
- ...workspaceChecks
15867
+ ...workspaceChecks,
15868
+ ...agentChecks
14952
15869
  ];
14953
15870
  }
14954
15871
 
@@ -15034,7 +15951,7 @@ async function readVersion() {
15034
15951
  let dir = dirname11(here);
15035
15952
  for (let i = 0; i < 6; i++) {
15036
15953
  try {
15037
- const raw = await readFile25(join5(dir, "package.json"), "utf-8");
15954
+ const raw = await readFile27(join5(dir, "package.json"), "utf-8");
15038
15955
  const parsed = JSON.parse(raw);
15039
15956
  return typeof parsed.version === "string" ? parsed.version : null;
15040
15957
  } catch {
@@ -15167,12 +16084,218 @@ var doctorCommand = new Command3("doctor").description("Diagnose Syntaur state a
15167
16084
  }
15168
16085
  });
15169
16086
 
16087
+ // src/commands/agents.ts
16088
+ init_config2();
16089
+ import { Command as Command4 } from "commander";
16090
+ var agentsCommand = new Command4("agents").description(
16091
+ "Manage configurable agents used by `syntaur browse` and future launch flows"
16092
+ );
16093
+ agentsCommand.command("list").description("List configured agents (or built-in defaults if none are configured)").action(async () => {
16094
+ try {
16095
+ const config = await readConfig();
16096
+ const agents = getAgents(config);
16097
+ const source = config.agents ? "config" : "built-in defaults";
16098
+ console.log(`Agents (${source}):`);
16099
+ for (const agent of agents) {
16100
+ const flags = [];
16101
+ if (agent.default) flags.push("default");
16102
+ if (agent.resolveFromShellAliases) flags.push("shell-alias");
16103
+ if (agent.promptArgPosition) flags.push(`prompt=${agent.promptArgPosition}`);
16104
+ const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
16105
+ const args = agent.args && agent.args.length > 0 ? ` ${agent.args.join(" ")}` : "";
16106
+ console.log(` ${agent.id.padEnd(12)} ${agent.label.padEnd(20)} ${agent.command}${args}${flagStr}`);
16107
+ }
16108
+ } catch (error) {
16109
+ reportAndExit(error);
16110
+ }
16111
+ });
16112
+ 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) => {
16113
+ try {
16114
+ const agent = buildAgentFromOptions(options, null);
16115
+ const mutation = {
16116
+ kind: "add",
16117
+ apply: (current) => {
16118
+ if (current.some((a) => a.id === agent.id)) {
16119
+ throw new AgentConfigError(`agent "${agent.id}" already exists`);
16120
+ }
16121
+ const next = agent.default ? current.map((a) => ({ ...a, default: false })) : [...current];
16122
+ return [...next, agent];
16123
+ }
16124
+ };
16125
+ const result = await updateAgentsConfig(mutation, {
16126
+ dryRun: Boolean(options.dryRun)
16127
+ });
16128
+ reportMutation("add", result);
16129
+ } catch (error) {
16130
+ reportAndExit(error);
16131
+ }
16132
+ });
16133
+ 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) => {
16134
+ try {
16135
+ const mutation = {
16136
+ kind: "remove",
16137
+ apply: (current) => {
16138
+ if (!current.some((a) => a.id === id)) {
16139
+ throw new AgentConfigError(`unknown agent id "${id}"`);
16140
+ }
16141
+ return current.filter((a) => a.id !== id);
16142
+ }
16143
+ };
16144
+ const result = await updateAgentsConfig(mutation, {
16145
+ dryRun: Boolean(options.dryRun)
16146
+ });
16147
+ reportMutation("remove", result);
16148
+ } catch (error) {
16149
+ reportAndExit(error);
16150
+ }
16151
+ });
16152
+ 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) => {
16153
+ try {
16154
+ const mutation = {
16155
+ kind: "set",
16156
+ apply: (current) => {
16157
+ const existing = current.find((a) => a.id === id);
16158
+ if (!existing) {
16159
+ throw new AgentConfigError(`unknown agent id "${id}"`);
16160
+ }
16161
+ const merged = mergeOptionsIntoAgent(existing, options);
16162
+ const defaultFlip = options.default === true;
16163
+ return current.map((a) => {
16164
+ if (a.id === id) return merged;
16165
+ if (defaultFlip) return { ...a, default: false };
16166
+ return a;
16167
+ });
16168
+ }
16169
+ };
16170
+ const result = await updateAgentsConfig(mutation, {
16171
+ dryRun: Boolean(options.dryRun)
16172
+ });
16173
+ reportMutation("set", result);
16174
+ } catch (error) {
16175
+ reportAndExit(error);
16176
+ }
16177
+ });
16178
+ 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) => {
16179
+ try {
16180
+ const newOrder = ids.split(",").map((s) => s.trim()).filter(Boolean);
16181
+ const mutation = {
16182
+ kind: "reorder",
16183
+ apply: (current) => {
16184
+ const seen = /* @__PURE__ */ new Set();
16185
+ for (const id of newOrder) {
16186
+ if (seen.has(id)) {
16187
+ throw new AgentConfigError(`duplicate id "${id}" in reorder list`);
16188
+ }
16189
+ seen.add(id);
16190
+ }
16191
+ const currentIds = new Set(current.map((a) => a.id));
16192
+ const missing = current.filter((a) => !seen.has(a.id)).map((a) => a.id);
16193
+ const extra = newOrder.filter((id) => !currentIds.has(id));
16194
+ if (missing.length > 0 || extra.length > 0) {
16195
+ const parts = [];
16196
+ if (missing.length) parts.push(`missing: ${missing.join(", ")}`);
16197
+ if (extra.length) parts.push(`unknown: ${extra.join(", ")}`);
16198
+ throw new AgentConfigError(
16199
+ `reorder list does not match current agents (${parts.join("; ")})`
16200
+ );
16201
+ }
16202
+ return newOrder.map((id) => current.find((a) => a.id === id));
16203
+ }
16204
+ };
16205
+ const result = await updateAgentsConfig(mutation, {
16206
+ dryRun: Boolean(options.dryRun)
16207
+ });
16208
+ reportMutation("reorder", result);
16209
+ } catch (error) {
16210
+ reportAndExit(error);
16211
+ }
16212
+ });
16213
+ function buildAgentFromOptions(options, existing) {
16214
+ const agent = {
16215
+ id: options.id,
16216
+ label: options.label,
16217
+ command: parseAgentCommand(options.command, options.id)
16218
+ };
16219
+ const args = parseArgsCsv(options.args);
16220
+ if (args) agent.args = args;
16221
+ if (options.promptArgPosition) {
16222
+ agent.promptArgPosition = options.promptArgPosition;
16223
+ }
16224
+ if (options.default) agent.default = true;
16225
+ if (options.resolveFromShellAliases) agent.resolveFromShellAliases = true;
16226
+ validateAgentList([...existing ? [] : [], agent]);
16227
+ return agent;
16228
+ }
16229
+ function mergeOptionsIntoAgent(existing, options) {
16230
+ const merged = { ...existing };
16231
+ if (options.label !== void 0) merged.label = options.label;
16232
+ if (options.command !== void 0) {
16233
+ merged.command = parseAgentCommand(options.command, existing.id);
16234
+ }
16235
+ if (options.args !== void 0) {
16236
+ const parsed = parseArgsCsv(options.args);
16237
+ if (parsed) merged.args = parsed;
16238
+ else delete merged.args;
16239
+ }
16240
+ if (options.promptArgPosition !== void 0) {
16241
+ merged.promptArgPosition = options.promptArgPosition;
16242
+ }
16243
+ if (options.default === true) merged.default = true;
16244
+ if (options.default === false) delete merged.default;
16245
+ if (options.resolveFromShellAliases === true) merged.resolveFromShellAliases = true;
16246
+ if (options.resolveFromShellAliases === false) delete merged.resolveFromShellAliases;
16247
+ return merged;
16248
+ }
16249
+ function parseArgsCsv(csv) {
16250
+ if (csv === void 0) return null;
16251
+ const parts = csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
16252
+ return parts.length > 0 ? parts : null;
16253
+ }
16254
+ function reportMutation(action, result) {
16255
+ const diff = renderDiff(result.previous, result.next);
16256
+ if (result.written) {
16257
+ console.log(`Agents updated (${action}):`);
16258
+ console.log(diff);
16259
+ } else {
16260
+ console.log(`Dry run (${action}) \u2014 no changes written:`);
16261
+ console.log(diff);
16262
+ }
16263
+ }
16264
+ function renderDiff(prev, next) {
16265
+ const prevLines = prev.map(formatAgentLine);
16266
+ const nextLines = next.map(formatAgentLine);
16267
+ const prevSet = new Set(prevLines);
16268
+ const nextSet = new Set(nextLines);
16269
+ const out = [];
16270
+ for (const line of prevLines) {
16271
+ if (!nextSet.has(line)) out.push(` - ${line}`);
16272
+ }
16273
+ for (const line of nextLines) {
16274
+ if (!prevSet.has(line)) out.push(` + ${line}`);
16275
+ }
16276
+ if (out.length === 0) out.push(" (no changes)");
16277
+ return out.join("\n");
16278
+ }
16279
+ function formatAgentLine(a) {
16280
+ const flags = [];
16281
+ if (a.default) flags.push("default");
16282
+ if (a.resolveFromShellAliases) flags.push("shell-alias");
16283
+ if (a.promptArgPosition) flags.push(`prompt=${a.promptArgPosition}`);
16284
+ if (a.args && a.args.length > 0) flags.push(`args=[${a.args.join(", ")}]`);
16285
+ const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
16286
+ return `${a.id}: ${a.label} \u2192 ${a.command}${suffix}`;
16287
+ }
16288
+ function reportAndExit(error) {
16289
+ console.error("Error:", error instanceof Error ? error.message : String(error));
16290
+ process.exit(1);
16291
+ }
16292
+
15170
16293
  // src/commands/comment.ts
15171
16294
  init_paths();
15172
16295
  init_fs();
15173
16296
  init_config2();
15174
- import { resolve as resolve43 } from "path";
15175
- import { readFile as readFile26 } from "fs/promises";
16297
+ import { resolve as resolve44 } from "path";
16298
+ import { readFile as readFile28 } from "fs/promises";
15176
16299
  init_timestamp();
15177
16300
  init_assignment_resolver();
15178
16301
  function shortId() {
@@ -15204,7 +16327,7 @@ async function commentCommand(target, text, options = {}) {
15204
16327
  if (!isValidSlug(target)) {
15205
16328
  throw new Error(`Invalid assignment slug "${target}".`);
15206
16329
  }
15207
- assignmentDir = resolve43(baseDir, options.project, "assignments", target);
16330
+ assignmentDir = resolve44(baseDir, options.project, "assignments", target);
15208
16331
  assignmentRef = target;
15209
16332
  } else {
15210
16333
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -15214,13 +16337,13 @@ async function commentCommand(target, text, options = {}) {
15214
16337
  assignmentDir = resolved.assignmentDir;
15215
16338
  assignmentRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
15216
16339
  }
15217
- const commentsPath = resolve43(assignmentDir, "comments.md");
16340
+ const commentsPath = resolve44(assignmentDir, "comments.md");
15218
16341
  const timestamp = nowTimestamp();
15219
16342
  const author = options.author ?? process.env.USER ?? "unknown";
15220
16343
  let currentContent;
15221
16344
  let currentCount = 0;
15222
16345
  if (await fileExists(commentsPath)) {
15223
- currentContent = await readFile26(commentsPath, "utf-8");
16346
+ currentContent = await readFile28(commentsPath, "utf-8");
15224
16347
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
15225
16348
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
15226
16349
  } else {
@@ -15257,8 +16380,8 @@ ${entry}`;
15257
16380
  init_paths();
15258
16381
  init_fs();
15259
16382
  init_config2();
15260
- import { resolve as resolve44 } from "path";
15261
- import { readFile as readFile27 } from "fs/promises";
16383
+ import { resolve as resolve45 } from "path";
16384
+ import { readFile as readFile29 } from "fs/promises";
15262
16385
  init_timestamp();
15263
16386
  init_assignment_resolver();
15264
16387
  function setTopLevelField3(content, key, value) {
@@ -15283,7 +16406,7 @@ async function requestCommand(target, text, options = {}) {
15283
16406
  if (!isValidSlug(target)) {
15284
16407
  throw new Error(`Invalid assignment slug "${target}".`);
15285
16408
  }
15286
- assignmentDir = resolve44(baseDir, options.project, "assignments", target);
16409
+ assignmentDir = resolve45(baseDir, options.project, "assignments", target);
15287
16410
  targetRef = target;
15288
16411
  } else {
15289
16412
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -15293,12 +16416,12 @@ async function requestCommand(target, text, options = {}) {
15293
16416
  assignmentDir = resolved.assignmentDir;
15294
16417
  targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
15295
16418
  }
15296
- const assignmentMdPath = resolve44(assignmentDir, "assignment.md");
16419
+ const assignmentMdPath = resolve45(assignmentDir, "assignment.md");
15297
16420
  if (!await fileExists(assignmentMdPath)) {
15298
16421
  throw new Error(`assignment.md not found at ${assignmentMdPath}`);
15299
16422
  }
15300
16423
  const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
15301
- let content = await readFile27(assignmentMdPath, "utf-8");
16424
+ let content = await readFile29(assignmentMdPath, "utf-8");
15302
16425
  const todoLine = `- [ ] ${text.trim()} (from: ${source})`;
15303
16426
  const todosHeading = /^## Todos\s*$/m;
15304
16427
  if (todosHeading.test(content)) {
@@ -15366,20 +16489,20 @@ async function getDefaultCommandName() {
15366
16489
  init_paths();
15367
16490
  init_fs();
15368
16491
  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";
16492
+ import { readFile as readFile31 } from "fs/promises";
16493
+ import { dirname as dirname13, join as join7, resolve as resolve46 } from "path";
16494
+ import { spawn as spawn4 } from "child_process";
15372
16495
  import { createInterface as createInterface2 } from "readline/promises";
15373
16496
 
15374
16497
  // src/utils/version.ts
15375
16498
  import { fileURLToPath as fileURLToPath8 } from "url";
15376
- import { readFile as readFile28 } from "fs/promises";
16499
+ import { readFile as readFile30 } from "fs/promises";
15377
16500
  import { dirname as dirname12, join as join6 } from "path";
15378
16501
  async function readPackageVersion(scriptUrl) {
15379
16502
  try {
15380
16503
  const scriptPath = fileURLToPath8(scriptUrl);
15381
16504
  const pkgRoot = dirname12(dirname12(scriptPath));
15382
- const raw = await readFile28(join6(pkgRoot, "package.json"), "utf-8");
16505
+ const raw = await readFile30(join6(pkgRoot, "package.json"), "utf-8");
15383
16506
  const parsed = JSON.parse(raw);
15384
16507
  return typeof parsed.version === "string" ? parsed.version : null;
15385
16508
  } catch {
@@ -15388,7 +16511,7 @@ async function readPackageVersion(scriptUrl) {
15388
16511
  }
15389
16512
 
15390
16513
  // src/utils/npx-prompt.ts
15391
- var STATE_FILE = resolve45(syntaurRoot(), "npx-install.json");
16514
+ var STATE_FILE = resolve46(syntaurRoot(), "npx-install.json");
15392
16515
  var META_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version", "help"]);
15393
16516
  var GLOBAL_VERSION_TIMEOUT_MS = 2e3;
15394
16517
  function isRunningViaNpx(scriptUrl) {
@@ -15409,7 +16532,7 @@ function isRunningViaNpx(scriptUrl) {
15409
16532
  async function readState() {
15410
16533
  if (!await fileExists(STATE_FILE)) return null;
15411
16534
  try {
15412
- const raw = await readFile29(STATE_FILE, "utf-8");
16535
+ const raw = await readFile31(STATE_FILE, "utf-8");
15413
16536
  return JSON.parse(raw);
15414
16537
  } catch {
15415
16538
  return null;
@@ -15432,7 +16555,7 @@ async function resolveNpmBin() {
15432
16555
  async function installGlobally() {
15433
16556
  const { cmd, shell } = await resolveNpmBin();
15434
16557
  return new Promise((resolvePromise) => {
15435
- const child = spawn3(cmd, ["install", "-g", "syntaur"], {
16558
+ const child = spawn4(cmd, ["install", "-g", "syntaur"], {
15436
16559
  stdio: "inherit",
15437
16560
  shell
15438
16561
  });
@@ -15443,7 +16566,7 @@ async function installGlobally() {
15443
16566
  async function readGlobalVersion() {
15444
16567
  const { cmd, shell } = await resolveNpmBin();
15445
16568
  const rootPath = await new Promise((resolvePromise) => {
15446
- const child = spawn3(cmd, ["root", "-g"], {
16569
+ const child = spawn4(cmd, ["root", "-g"], {
15447
16570
  shell,
15448
16571
  stdio: ["ignore", "pipe", "ignore"]
15449
16572
  });
@@ -15468,7 +16591,7 @@ async function readGlobalVersion() {
15468
16591
  try {
15469
16592
  const manifestPath = join7(rootPath, "syntaur", "package.json");
15470
16593
  if (!await fileExists(manifestPath)) return null;
15471
- const raw = await readFile29(manifestPath, "utf-8");
16594
+ const raw = await readFile31(manifestPath, "utf-8");
15472
16595
  const parsed = JSON.parse(raw);
15473
16596
  return typeof parsed.version === "string" ? parsed.version : null;
15474
16597
  } catch {
@@ -15587,7 +16710,7 @@ async function maybePromptInstall(scriptUrl) {
15587
16710
 
15588
16711
  // src/index.ts
15589
16712
  await maybePromptInstall(import.meta.url);
15590
- var program = new Command4();
16713
+ var program = new Command5();
15591
16714
  var version = await readPackageVersion(import.meta.url) ?? "0.0.0";
15592
16715
  program.name("syntaur").description("CLI scaffolding tool for the Syntaur protocol").version(version);
15593
16716
  program.command("init").description("Initialize ~/.syntaur/ directory structure and config").option("--force", "Overwrite existing config file").action(async (options) => {
@@ -15878,7 +17001,7 @@ program.command("track-session").description("Register an agent session (optiona
15878
17001
  process.exit(1);
15879
17002
  }
15880
17003
  });
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) => {
17004
+ 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
17005
  try {
15883
17006
  await browseCommand(options);
15884
17007
  } catch (error) {
@@ -15936,6 +17059,7 @@ program.command("disable-playbook").description("Disable a playbook so agents no
15936
17059
  program.addCommand(todoCommand);
15937
17060
  program.addCommand(backupCommand);
15938
17061
  program.addCommand(doctorCommand);
17062
+ program.addCommand(agentsCommand);
15939
17063
  if (process.argv.length <= 2) {
15940
17064
  process.argv.push(await getDefaultCommandName());
15941
17065
  }