syntaur 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dashboard/dist/assets/{_basePickBy-qEDLAY_J.js → _basePickBy-B1S22H85.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-7tDs18Gn.js → _baseUniq-CJIRCCsh.js} +1 -1
  3. package/dashboard/dist/assets/{arc-Brdw0dYW.js → arc-CdlNYTcd.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BiEdLT9m.js → architectureDiagram-2XIMDMQ5-KVI0HWOy.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-HC-j_hPZ.js → blockDiagram-WCTKOSBZ-ClnG7gaq.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-DA_rWbl7.js → c4Diagram-IC4MRINW-BD9lkttL.js} +1 -1
  7. package/dashboard/dist/assets/channel-Cm_FzKJq.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-CF3hguyj.js → chunk-4BX2VUAB-BM3DXZMG.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-BTeCeIc_.js → chunk-55IACEB6-DUIbFJ3r.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-8qVvxmks.js → chunk-FMBD7UC4-B_cxCRkA.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-DCf8n7EO.js → chunk-JSJVCQXG-2mLf1WoM.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-CcpjTmME.js → chunk-KX2RTZJC-BMz3ND2R.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-BnFPKo4-.js → chunk-NQ4KR5QH-DiCxpw3L.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-IPMVLLv2.js → chunk-QZHKN3VN-BcaWPzd5.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-D1pQmHqX.js → chunk-WL4C6EOR-BbsfhS6J.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-Bdan2Dp5.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Bdan2Dp5.js +1 -0
  18. package/dashboard/dist/assets/clone-9PoPEM_s.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-tvJjvBQr.js → cose-bilkent-S5V4N54A-iuQzyyrA.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-qp1vt6LY.js → dagre-KLK3FWXG-C0TB1zg5.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-VF5eizNr.js → diagram-E7M64L7V-Ck8tvxP2.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-C3pCbJ1e.js → diagram-IFDJBPK2-rQKIi-sc.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-D-taXZwH.js → diagram-P4PSJMXO-DS1RGSgx.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-QjY-NHVs.js → erDiagram-INFDFZHY-BqWCybWW.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-FjO8P5Tq.js → flowDiagram-PKNHOUZH-BCoQtyQ5.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-Bsn7CyK3.js → ganttDiagram-A5KZAMGK-CD1BmicP.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-BZ8DjhnF.js → gitGraphDiagram-K3NZZRJ6-tGDYOZuO.js} +1 -1
  28. package/dashboard/dist/assets/{graph-AdliIYqX.js → graph-BT0s-094.js} +1 -1
  29. package/dashboard/dist/assets/index-BdSkANCC.js +510 -0
  30. package/dashboard/dist/assets/index-D2Yoi6DJ.css +1 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-BfZjLA86.js → infoDiagram-LFFYTUFH-E4dVb8BD.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-BSgSGMp5.js → ishikawaDiagram-PHBUUO56-CubuM_ev.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-BlQ7qeMC.js → journeyDiagram-4ABVD52K-DfyJbgMF.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-CifO0AUu.js → kanban-definition-K7BYSVSG-BXamajyZ.js} +1 -1
  35. package/dashboard/dist/assets/{layout-B9r-gpD9.js → layout-6b_SlWD_.js} +1 -1
  36. package/dashboard/dist/assets/{linear-CaaIRxVZ.js → linear-BE0O-66A.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-C8wCWYw-.js → mermaid.core-DM3qWr07.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-BpX5T7iT.js → mindmap-definition-YRQLILUH-puOtERrG.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-BNGYtWrc.js → pieDiagram-SKSYHLDU-cE6l8UBX.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-CR4GeM9D.js → quadrantDiagram-337W2JSQ-CgDAyZQP.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CXrPkMLl.js → requirementDiagram-Z7DCOOCP-CJrai_8U.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-CIzof7Dm.js → sankeyDiagram-WA2Y5GQK-Bf72l7UW.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BJZLt0O2.js → sequenceDiagram-2WXFIKYE-0D8RN7ds.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-x9gC6zy6.js → stateDiagram-RAJIS63D-B8LucUIt.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-D0dc7wxQ.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-D_ZSQn1K.js → timeline-definition-YZTLITO2-DL3Hq8ii.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-LPpMEco4.js → treemap-KZPCXAKY-wF1X4_uI.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-CRFZQpr7.js → vennDiagram-LZ73GAT5-DDAjoGTZ.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-Bivutynw.js → xychartDiagram-JWTSCODW-T-YmzRW8.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +7664 -5884
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/db/leases-db.js.map +1 -1
  54. package/dist/index.js +6438 -2094
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +318 -0
  57. package/dist/launch/index.js +3122 -0
  58. package/dist/launch/index.js.map +1 -0
  59. package/package.json +15 -2
  60. package/platforms/claude-code/commands/track-session/track-session.md +2 -1
  61. package/platforms/claude-code/hooks/session-start.sh +14 -1
  62. package/platforms/claude-code/references/protocol-summary.md +1 -1
  63. package/platforms/claude-code/skills/capture-artifacts/SKILL.md +2 -1
  64. package/platforms/claude-code/skills/complete-assignment/SKILL.md +2 -0
  65. package/platforms/claude-code/skills/syntaur-protocol/references/protocol-summary.md +1 -1
  66. package/platforms/claude-code/skills/track-session/SKILL.md +2 -1
  67. package/platforms/codex/agents/syntaur-operator.md +2 -2
  68. package/platforms/codex/references/protocol-summary.md +1 -1
  69. package/platforms/codex/skills/capture-artifacts/SKILL.md +2 -1
  70. package/platforms/codex/skills/complete-assignment/SKILL.md +2 -0
  71. package/platforms/codex/skills/syntaur-protocol/references/protocol-summary.md +1 -1
  72. package/platforms/codex/skills/track-session/SKILL.md +2 -1
  73. package/scripts/install-macos-url-handler.mjs +540 -0
  74. package/skills/capture-artifacts/SKILL.md +2 -1
  75. package/skills/complete-assignment/SKILL.md +2 -0
  76. package/skills/syntaur-protocol/references/protocol-summary.md +1 -1
  77. package/skills/track-session/SKILL.md +2 -1
  78. package/dashboard/dist/assets/channel-D2-caOn6.js +0 -1
  79. package/dashboard/dist/assets/classDiagram-VBA2DB6C-HFOb_XEt.js +0 -1
  80. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-HFOb_XEt.js +0 -1
  81. package/dashboard/dist/assets/clone-BrYOwgIq.js +0 -1
  82. package/dashboard/dist/assets/index-BoR_oyUZ.css +0 -1
  83. package/dashboard/dist/assets/index-RCBI4aX2.js +0 -525
  84. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-CN5Dcv7u.js +0 -1
@@ -0,0 +1,3122 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __esm = (fn, res) => function __init() {
3
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
+ };
5
+
6
+ // src/utils/paths.ts
7
+ import { homedir } from "os";
8
+ import { resolve } from "path";
9
+ function expandHome(p) {
10
+ if (p.startsWith("~/") || p === "~") {
11
+ return resolve(homedir(), p.slice(2));
12
+ }
13
+ return p;
14
+ }
15
+ function syntaurRoot() {
16
+ const override = process.env.SYNTAUR_HOME;
17
+ if (override && override.length > 0) {
18
+ return resolve(expandHome(override));
19
+ }
20
+ return resolve(homedir(), ".syntaur");
21
+ }
22
+ function defaultProjectDir() {
23
+ return resolve(syntaurRoot(), "projects");
24
+ }
25
+ var init_paths = __esm({
26
+ "src/utils/paths.ts"() {
27
+ "use strict";
28
+ }
29
+ });
30
+
31
+ // src/utils/fs.ts
32
+ import { mkdir, writeFile, access, rename } from "fs/promises";
33
+ import { dirname, join } from "path";
34
+ async function ensureDir(dir) {
35
+ await mkdir(dir, { recursive: true });
36
+ }
37
+ async function fileExists(filePath) {
38
+ try {
39
+ await access(filePath);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ async function writeFileForce(filePath, content) {
46
+ const dir = dirname(filePath);
47
+ const tempPath = join(
48
+ dir,
49
+ `.${Math.random().toString(36).slice(2)}.${Date.now()}.tmp`
50
+ );
51
+ await ensureDir(dir);
52
+ await writeFile(tempPath, content, "utf-8");
53
+ await rename(tempPath, filePath);
54
+ }
55
+ var init_fs = __esm({
56
+ "src/utils/fs.ts"() {
57
+ "use strict";
58
+ }
59
+ });
60
+
61
+ // src/templates/config.ts
62
+ var init_config = __esm({
63
+ "src/templates/config.ts"() {
64
+ "use strict";
65
+ }
66
+ });
67
+
68
+ // src/utils/fs-migration.ts
69
+ import { readdir, readFile, rename as rename2, writeFile as writeFile2 } from "fs/promises";
70
+ import { resolve as resolve2 } from "path";
71
+ async function migrateLegacyProjectFiles(projectsDir) {
72
+ const result = {
73
+ renamedProjectFiles: [],
74
+ legacyExtras: []
75
+ };
76
+ if (!await fileExists(projectsDir)) return result;
77
+ let entries;
78
+ try {
79
+ entries = await readdir(projectsDir, { withFileTypes: true });
80
+ } catch {
81
+ return result;
82
+ }
83
+ for (const entry of entries) {
84
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
85
+ const projectDir = resolve2(projectsDir, entry.name);
86
+ const legacy = resolve2(projectDir, "mission.md");
87
+ const target = resolve2(projectDir, "project.md");
88
+ try {
89
+ if (await fileExists(legacy) && !await fileExists(target)) {
90
+ await rename2(legacy, target);
91
+ result.renamedProjectFiles.push(`${entry.name}/mission.md`);
92
+ }
93
+ } catch {
94
+ continue;
95
+ }
96
+ for (const stale of ["agent.md", "claude.md"]) {
97
+ try {
98
+ if (await fileExists(resolve2(projectDir, stale))) {
99
+ result.legacyExtras.push(`${entry.name}/${stale}`);
100
+ }
101
+ } catch {
102
+ }
103
+ }
104
+ }
105
+ return result;
106
+ }
107
+ async function migrateLegacyConfig(configPath) {
108
+ const result = {
109
+ renamedField: false,
110
+ renamedDir: false,
111
+ resolvedProjectsDir: null
112
+ };
113
+ if (!await fileExists(configPath)) return result;
114
+ let content;
115
+ try {
116
+ content = await readFile(configPath, "utf-8");
117
+ } catch {
118
+ return result;
119
+ }
120
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
121
+ if (!fmMatch) return result;
122
+ const fmBlock = fmMatch[1];
123
+ const afterFm = content.slice(fmMatch[0].length);
124
+ const missionLineRe = /^(\s*)defaultMissionDir\s*:\s*(.*)$/m;
125
+ const missionLineMatch = fmBlock.match(missionLineRe);
126
+ const hasProjectLine = /^\s*defaultProjectDir\s*:/m.test(fmBlock);
127
+ let newFmBlock = fmBlock;
128
+ let missionValue = null;
129
+ if (missionLineMatch) {
130
+ missionValue = missionLineMatch[2].trim();
131
+ if (!hasProjectLine) {
132
+ newFmBlock = fmBlock.replace(
133
+ missionLineRe,
134
+ `$1defaultProjectDir: ${missionValue}`
135
+ );
136
+ result.renamedField = true;
137
+ } else {
138
+ newFmBlock = fmBlock.replace(missionLineRe, "").replace(/\n{2,}/g, "\n");
139
+ result.renamedField = true;
140
+ }
141
+ }
142
+ const projectLineRe = /^\s*defaultProjectDir\s*:\s*(.*)$/m;
143
+ const projectLineMatch = newFmBlock.match(projectLineRe);
144
+ const projectsDirRaw = projectLineMatch ? projectLineMatch[1].trim().replace(/^['"]|['"]$/g, "") : missionValue;
145
+ const expand = (p) => p.startsWith("~") ? resolve2(process.env.HOME ?? "/", p.slice(p.startsWith("~/") ? 2 : 1)) : p;
146
+ let resolvedProjectsDir = projectsDirRaw ? expand(projectsDirRaw) : null;
147
+ if (resolvedProjectsDir && resolvedProjectsDir.endsWith("/missions")) {
148
+ const siblingProjectsDir = resolvedProjectsDir.replace(/\/missions$/, "/projects");
149
+ if (await fileExists(resolvedProjectsDir) && !await fileExists(siblingProjectsDir)) {
150
+ try {
151
+ await rename2(resolvedProjectsDir, siblingProjectsDir);
152
+ const newValue = projectsDirRaw.endsWith("/missions") ? projectsDirRaw.replace(/\/missions$/, "/projects") : siblingProjectsDir;
153
+ newFmBlock = newFmBlock.replace(
154
+ projectLineRe,
155
+ `defaultProjectDir: ${newValue}`
156
+ );
157
+ resolvedProjectsDir = siblingProjectsDir;
158
+ result.renamedDir = true;
159
+ } catch {
160
+ }
161
+ }
162
+ }
163
+ result.resolvedProjectsDir = resolvedProjectsDir;
164
+ if (result.renamedField || result.renamedDir) {
165
+ const newContent = `---
166
+ ${newFmBlock.replace(/\n+$/, "")}
167
+ ---
168
+ ${afterFm.startsWith("\n") ? afterFm.slice(1) : afterFm}`;
169
+ try {
170
+ await writeFile2(configPath, newContent, "utf-8");
171
+ } catch {
172
+ result.renamedField = false;
173
+ result.renamedDir = false;
174
+ }
175
+ }
176
+ return result;
177
+ }
178
+ var init_fs_migration = __esm({
179
+ "src/utils/fs-migration.ts"() {
180
+ "use strict";
181
+ init_fs();
182
+ }
183
+ });
184
+
185
+ // src/utils/hotkeysCatalog.ts
186
+ function isBindableActionKind(value) {
187
+ return typeof value === "string" && BINDABLE_ACTION_KINDS.includes(value);
188
+ }
189
+ function canonicalizeCombo(input) {
190
+ if (typeof input !== "string") return "";
191
+ const trimmed = input.trim();
192
+ if (!trimmed) return "";
193
+ if (/\s/.test(trimmed) && !trimmed.includes("+")) {
194
+ return trimmed.split(/\s+/).map(canonicalizeCombo).filter((part) => part.length > 0).join(" ");
195
+ }
196
+ const parts = trimmed.split("+").map((p) => p.trim()).filter((p) => p.length > 0);
197
+ if (parts.length === 0) return "";
198
+ if (parts.length === 1) {
199
+ return parts[0].toLowerCase();
200
+ }
201
+ const key = parts[parts.length - 1].toLowerCase();
202
+ const mods = parts.slice(0, -1).map((m) => m.toLowerCase());
203
+ const seen = /* @__PURE__ */ new Set();
204
+ const ordered = [];
205
+ for (const m of MODIFIER_ORDER) {
206
+ if (mods.includes(m) && !seen.has(m)) {
207
+ ordered.push(m);
208
+ seen.add(m);
209
+ }
210
+ }
211
+ for (const m of mods) {
212
+ if (!seen.has(m)) {
213
+ ordered.push(m);
214
+ seen.add(m);
215
+ }
216
+ }
217
+ return [...ordered, key].join("+");
218
+ }
219
+ var BINDABLE_ACTION_KINDS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
220
+ var init_hotkeysCatalog = __esm({
221
+ "src/utils/hotkeysCatalog.ts"() {
222
+ "use strict";
223
+ BINDABLE_ACTION_KINDS = [
224
+ "new-workspace",
225
+ "new-project",
226
+ "new-todo",
227
+ "new-assignment"
228
+ ];
229
+ MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"];
230
+ DEFAULT_BINDABLE_HOTKEYS = {
231
+ "new-workspace": canonicalizeCombo("Mod+Shift+Alt+w"),
232
+ "new-project": canonicalizeCombo("Mod+Shift+Alt+p"),
233
+ "new-todo": canonicalizeCombo("Mod+Shift+Alt+t"),
234
+ "new-assignment": canonicalizeCombo("Mod+Shift+Alt+a")
235
+ };
236
+ }
237
+ });
238
+
239
+ // src/utils/agents-schema.ts
240
+ var BUILTIN_AGENTS, AGENT_ID_PATTERN, PROMPT_ARG_POSITIONS;
241
+ var init_agents_schema = __esm({
242
+ "src/utils/agents-schema.ts"() {
243
+ "use strict";
244
+ BUILTIN_AGENTS = [
245
+ {
246
+ id: "claude",
247
+ label: "Claude",
248
+ command: "claude",
249
+ default: true,
250
+ resume: { args: ["--resume", "{id}"] },
251
+ fork: { args: ["--resume", "{id}", "--fork-session"] }
252
+ },
253
+ {
254
+ id: "codex",
255
+ label: "Codex",
256
+ command: "codex",
257
+ resume: { args: ["resume", "{id}"] },
258
+ fork: { args: ["fork", "{id}"] }
259
+ }
260
+ ];
261
+ AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
262
+ PROMPT_ARG_POSITIONS = ["first", "last", "none"];
263
+ }
264
+ });
265
+
266
+ // src/utils/config.ts
267
+ import { readFile as readFile2 } from "fs/promises";
268
+ import { resolve as resolve3, isAbsolute } from "path";
269
+ function parseAgentCommand(value, agentId) {
270
+ if (typeof value !== "string" || value.trim() === "") {
271
+ throw new AgentConfigError(
272
+ `agent${agentId ? ` "${agentId}"` : ""} has empty command`
273
+ );
274
+ }
275
+ const expanded = expandHome(value.trim());
276
+ if (isAbsolute(expanded)) {
277
+ return resolve3(expanded);
278
+ }
279
+ if (expanded.includes("/")) {
280
+ throw new AgentConfigError(
281
+ `agent${agentId ? ` "${agentId}"` : ""} command "${value}" is a relative path \u2014 use an absolute path or a bare binary name`
282
+ );
283
+ }
284
+ return expanded;
285
+ }
286
+ function validateAgentList(agents) {
287
+ const seen = /* @__PURE__ */ new Set();
288
+ let defaults = 0;
289
+ for (const agent of agents) {
290
+ if (!AGENT_ID_PATTERN.test(agent.id)) {
291
+ throw new AgentConfigError(
292
+ `agent id "${agent.id}" is invalid \u2014 must match /^[a-z0-9][a-z0-9_-]*$/`
293
+ );
294
+ }
295
+ if (seen.has(agent.id)) {
296
+ throw new AgentConfigError(`duplicate agent id "${agent.id}"`);
297
+ }
298
+ seen.add(agent.id);
299
+ if (!agent.label || agent.label.trim() === "") {
300
+ throw new AgentConfigError(`agent "${agent.id}" has empty label`);
301
+ }
302
+ parseAgentCommand(agent.command, agent.id);
303
+ if (agent.promptArgPosition !== void 0 && !PROMPT_ARG_POSITIONS.includes(agent.promptArgPosition)) {
304
+ throw new AgentConfigError(
305
+ `agent "${agent.id}" has invalid promptArgPosition "${agent.promptArgPosition}" \u2014 expected first|last|none`
306
+ );
307
+ }
308
+ validateSessionInvocation(agent, "resume", agent.resume);
309
+ validateSessionInvocation(agent, "fork", agent.fork);
310
+ if (agent.default) defaults++;
311
+ }
312
+ if (defaults > 1) {
313
+ throw new AgentConfigError(
314
+ `more than one agent is marked default: true (only one is allowed)`
315
+ );
316
+ }
317
+ }
318
+ function validateSessionInvocation(agent, mode, invocation) {
319
+ if (invocation === void 0) return;
320
+ if (!Array.isArray(invocation.args)) {
321
+ throw new AgentConfigError(
322
+ `agent "${agent.id}" ${mode}.args must be an array of strings`
323
+ );
324
+ }
325
+ for (const a of invocation.args) {
326
+ if (typeof a !== "string") {
327
+ throw new AgentConfigError(
328
+ `agent "${agent.id}" ${mode}.args must contain only strings`
329
+ );
330
+ }
331
+ }
332
+ if (invocation.command !== void 0 && (typeof invocation.command !== "string" || invocation.command.trim() === "")) {
333
+ throw new AgentConfigError(
334
+ `agent "${agent.id}" ${mode}.command must be a non-empty string when present`
335
+ );
336
+ }
337
+ }
338
+ function cloneDefaultConfig() {
339
+ return {
340
+ ...DEFAULT_CONFIG,
341
+ onboarding: { ...DEFAULT_CONFIG.onboarding },
342
+ agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
343
+ integrations: { ...DEFAULT_CONFIG.integrations },
344
+ backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
345
+ statuses: DEFAULT_CONFIG.statuses ? {
346
+ statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
347
+ order: [...DEFAULT_CONFIG.statuses.order],
348
+ transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
349
+ } : null,
350
+ types: DEFAULT_CONFIG.types ? {
351
+ definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
352
+ default: DEFAULT_CONFIG.types.default
353
+ } : null,
354
+ agents: DEFAULT_CONFIG.agents ? DEFAULT_CONFIG.agents.map((a) => ({
355
+ ...a,
356
+ ...a.args ? { args: [...a.args] } : {},
357
+ ...a.resume ? { resume: { ...a.resume, args: [...a.resume.args] } } : {},
358
+ ...a.fork ? { fork: { ...a.fork, args: [...a.fork.args] } } : {}
359
+ })) : null,
360
+ playbooks: {
361
+ disabled: [...DEFAULT_CONFIG.playbooks.disabled]
362
+ },
363
+ theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null,
364
+ hotkeys: DEFAULT_CONFIG.hotkeys ? { bindings: { ...DEFAULT_CONFIG.hotkeys.bindings } } : null,
365
+ terminal: DEFAULT_CONFIG.terminal
366
+ };
367
+ }
368
+ function parseFrontmatter(content) {
369
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
370
+ if (!match) return {};
371
+ const result = {};
372
+ const lines = match[1].split("\n");
373
+ let currentParent = null;
374
+ for (const line of lines) {
375
+ if (line.trim() === "") continue;
376
+ const indent = line.length - line.trimStart().length;
377
+ const colonIndex = line.indexOf(":");
378
+ if (colonIndex < 0) continue;
379
+ const key = line.slice(0, colonIndex).trim();
380
+ const value = line.slice(colonIndex + 1).trim();
381
+ if (indent === 0) {
382
+ if (value === "" || value === void 0) {
383
+ currentParent = key;
384
+ } else {
385
+ currentParent = null;
386
+ result[key] = value.replace(/^["']|["']$/g, "");
387
+ }
388
+ } else if (indent > 0 && currentParent) {
389
+ result[`${currentParent}.${key}`] = value.replace(/^["']|["']$/g, "");
390
+ }
391
+ }
392
+ return result;
393
+ }
394
+ function parseStatusConfig(content) {
395
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
396
+ if (!match) return null;
397
+ const fmBlock = match[1];
398
+ const statusesStart = fmBlock.match(/^statuses:\s*$/m);
399
+ if (!statusesStart) return null;
400
+ const startIdx = fmBlock.indexOf(statusesStart[0]) + statusesStart[0].length;
401
+ const remaining = fmBlock.slice(startIdx);
402
+ const statuses = [];
403
+ const order = [];
404
+ const transitions = [];
405
+ let currentSection = null;
406
+ const lines = remaining.split("\n");
407
+ function parseListEntry(lineIdx, baseIndent) {
408
+ const entry = {};
409
+ const firstLine = lines[lineIdx].trimStart().slice(2).trim();
410
+ const colonIdx = firstLine.indexOf(":");
411
+ if (colonIdx > 0) {
412
+ entry[firstLine.slice(0, colonIdx).trim()] = firstLine.slice(colonIdx + 1).trim();
413
+ }
414
+ let consumed = 1;
415
+ for (let i = lineIdx + 1; i < lines.length; i++) {
416
+ const next = lines[i];
417
+ const nextTrimmed = next.trimStart();
418
+ const nextIndent = next.length - nextTrimmed.length;
419
+ if (nextIndent <= baseIndent || nextTrimmed.startsWith("- ")) break;
420
+ const ci = nextTrimmed.indexOf(":");
421
+ if (ci > 0) {
422
+ entry[nextTrimmed.slice(0, ci).trim()] = nextTrimmed.slice(ci + 1).trim();
423
+ }
424
+ consumed++;
425
+ }
426
+ return { entry, consumed };
427
+ }
428
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
429
+ const line = lines[lineIdx];
430
+ const trimmed = line.trimStart();
431
+ const indent = line.length - trimmed.length;
432
+ if (indent === 2 && trimmed.endsWith(":")) {
433
+ const key = trimmed.slice(0, -1).trim();
434
+ if (key === "definitions") currentSection = "definitions";
435
+ else if (key === "order") currentSection = "order";
436
+ else if (key === "transitions") currentSection = "transitions";
437
+ else currentSection = null;
438
+ continue;
439
+ }
440
+ if (indent === 0 && trimmed.includes(":")) break;
441
+ if (currentSection === "order" && indent >= 4 && trimmed.startsWith("- ")) {
442
+ order.push(trimmed.slice(2).trim());
443
+ continue;
444
+ }
445
+ if (currentSection === "definitions" && indent >= 4 && trimmed.startsWith("- ")) {
446
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
447
+ if (entry["id"]) {
448
+ statuses.push({
449
+ id: entry["id"],
450
+ label: entry["label"] ?? entry["id"],
451
+ description: entry["description"],
452
+ color: entry["color"],
453
+ icon: entry["icon"],
454
+ terminal: entry["terminal"] === "true"
455
+ });
456
+ }
457
+ lineIdx += consumed - 1;
458
+ continue;
459
+ }
460
+ if (currentSection === "transitions" && indent >= 4 && trimmed.startsWith("- ")) {
461
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
462
+ if (entry["from"] && entry["command"] && entry["to"]) {
463
+ transitions.push({
464
+ from: entry["from"],
465
+ command: entry["command"],
466
+ to: entry["to"],
467
+ label: entry["label"],
468
+ description: entry["description"],
469
+ requiresReason: entry["requiresReason"] === "true"
470
+ });
471
+ }
472
+ lineIdx += consumed - 1;
473
+ continue;
474
+ }
475
+ }
476
+ if (statuses.length === 0) return null;
477
+ return {
478
+ statuses,
479
+ order: order.length > 0 ? order : statuses.map((s) => s.id),
480
+ transitions
481
+ };
482
+ }
483
+ function parsePlaybooksConfig(fmBlock) {
484
+ const blockStart = fmBlock.match(/^playbooks:\s*$/m);
485
+ if (!blockStart) {
486
+ return { disabled: [] };
487
+ }
488
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
489
+ const remaining = fmBlock.slice(startIdx).split("\n");
490
+ const disabled = [];
491
+ let currentSection = null;
492
+ for (const line of remaining) {
493
+ const trimmed = line.trimStart();
494
+ const indent = line.length - trimmed.length;
495
+ if (indent === 0 && trimmed.length > 0) break;
496
+ if (trimmed === "") continue;
497
+ if (indent === 2 && trimmed.startsWith("disabled:")) {
498
+ currentSection = "disabled";
499
+ const afterColon = trimmed.slice("disabled:".length).trim();
500
+ if (afterColon === "[]" || afterColon === "") {
501
+ continue;
502
+ }
503
+ continue;
504
+ }
505
+ if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
506
+ const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
507
+ if (raw.length === 0) continue;
508
+ if (/\s/.test(raw)) {
509
+ console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
510
+ continue;
511
+ }
512
+ disabled.push(raw);
513
+ continue;
514
+ }
515
+ }
516
+ return { disabled };
517
+ }
518
+ function parseThemeConfig(content) {
519
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
520
+ if (!match) return null;
521
+ const fmBlock = match[1];
522
+ const blockStart = fmBlock.match(/^theme:\s*$/m);
523
+ if (!blockStart) return null;
524
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
525
+ const remaining = fmBlock.slice(startIdx).split("\n");
526
+ let preset = null;
527
+ for (const line of remaining) {
528
+ const trimmed = line.trimStart();
529
+ const indent = line.length - trimmed.length;
530
+ if (indent === 0 && trimmed.length > 0) break;
531
+ if (trimmed === "") continue;
532
+ if (indent === 2 && trimmed.startsWith("preset:")) {
533
+ const value = trimmed.slice("preset:".length).trim().replace(/^["']|["']$/g, "");
534
+ if (value.length > 0) preset = value;
535
+ }
536
+ }
537
+ if (!preset) return null;
538
+ return { preset };
539
+ }
540
+ function parseHotkeyBindingsConfig(content) {
541
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
542
+ if (!match) return null;
543
+ const fmBlock = match[1];
544
+ const blockStart = fmBlock.match(/^hotkeys:\s*$/m);
545
+ if (!blockStart) return null;
546
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
547
+ const remaining = fmBlock.slice(startIdx).split("\n");
548
+ const bindings = {};
549
+ let inBindings = false;
550
+ for (const line of remaining) {
551
+ const trimmed = line.trimStart();
552
+ const indent = line.length - trimmed.length;
553
+ if (indent === 0 && trimmed.length > 0) break;
554
+ if (trimmed === "") continue;
555
+ if (indent === 2 && trimmed === "bindings:") {
556
+ inBindings = true;
557
+ continue;
558
+ }
559
+ if (inBindings && indent === 4) {
560
+ const colonIdx = trimmed.indexOf(":");
561
+ if (colonIdx <= 0) continue;
562
+ const rawKind = trimmed.slice(0, colonIdx).trim();
563
+ const rawValue = trimmed.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
564
+ if (!isBindableActionKind(rawKind)) continue;
565
+ if (rawValue.length === 0) continue;
566
+ bindings[rawKind] = canonicalizeCombo(rawValue);
567
+ }
568
+ }
569
+ if (Object.keys(bindings).length === 0) return null;
570
+ return { bindings };
571
+ }
572
+ function parseOptionalAbsolutePath(value, fieldName) {
573
+ if (!value) {
574
+ return null;
575
+ }
576
+ const expanded = expandHome(String(value));
577
+ if (!isAbsolute(expanded)) {
578
+ console.warn(
579
+ `Warning: config.md ${fieldName} is not an absolute path ("${value}"), ignoring it`
580
+ );
581
+ return null;
582
+ }
583
+ return resolve3(expanded);
584
+ }
585
+ function parseAgentsConfig(content) {
586
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
587
+ if (!match) return null;
588
+ const fmBlock = match[1];
589
+ const agentsStart = fmBlock.match(/^agents:\s*$/m);
590
+ if (!agentsStart) return null;
591
+ const startIdx = fmBlock.indexOf(agentsStart[0]) + agentsStart[0].length;
592
+ const remaining = fmBlock.slice(startIdx);
593
+ const lines = remaining.split("\n");
594
+ const agents = [];
595
+ let current = null;
596
+ let argsCapture = null;
597
+ let argsBaseIndent = 0;
598
+ let nestedKey = null;
599
+ let nestedInvocation = null;
600
+ let nestedBaseIndent = 0;
601
+ function flushCurrent() {
602
+ if (!current) return;
603
+ if (!current.id || !current.command || !current.label) {
604
+ current = null;
605
+ return;
606
+ }
607
+ agents.push({
608
+ id: current.id,
609
+ label: current.label,
610
+ command: current.command,
611
+ ...current.args && current.args.length > 0 ? { args: current.args } : {},
612
+ ...current.promptArgPosition ? { promptArgPosition: current.promptArgPosition } : {},
613
+ ...current.default ? { default: true } : {},
614
+ ...current.resolveFromShellAliases ? { resolveFromShellAliases: true } : {},
615
+ ...current.resume ? { resume: current.resume } : {},
616
+ ...current.fork ? { fork: current.fork } : {}
617
+ });
618
+ current = null;
619
+ argsCapture = null;
620
+ nestedKey = null;
621
+ nestedInvocation = null;
622
+ }
623
+ function closeNestedBlock() {
624
+ if (!nestedKey) return;
625
+ if (current && (nestedKey === "resume" || nestedKey === "fork") && nestedInvocation) {
626
+ if (Array.isArray(nestedInvocation.args)) {
627
+ current[nestedKey] = nestedInvocation;
628
+ }
629
+ }
630
+ nestedKey = null;
631
+ nestedInvocation = null;
632
+ argsCapture = null;
633
+ }
634
+ for (let i = 0; i < lines.length; i++) {
635
+ const line = lines[i];
636
+ const trimmed = line.trimStart();
637
+ const indent = line.length - trimmed.length;
638
+ if (indent === 0 && trimmed !== "" && !trimmed.startsWith("#")) {
639
+ closeNestedBlock();
640
+ break;
641
+ }
642
+ if (argsCapture) {
643
+ if (indent > argsBaseIndent && trimmed.startsWith("- ")) {
644
+ argsCapture.push(decodeYamlScalar(trimmed.slice(2).trim()));
645
+ continue;
646
+ } else {
647
+ argsCapture = null;
648
+ }
649
+ }
650
+ if (indent === 2 && trimmed.startsWith("- ")) {
651
+ closeNestedBlock();
652
+ flushCurrent();
653
+ current = {};
654
+ const rest = trimmed.slice(2).trim();
655
+ const colonIdx = rest.indexOf(":");
656
+ if (colonIdx > 0) {
657
+ const k = rest.slice(0, colonIdx).trim();
658
+ const v = rest.slice(colonIdx + 1).trim();
659
+ assignAgentField(current, k, v);
660
+ }
661
+ continue;
662
+ }
663
+ if (!current) continue;
664
+ if (nestedKey && indent > nestedBaseIndent) {
665
+ const colonIdx = trimmed.indexOf(":");
666
+ if (colonIdx <= 0) continue;
667
+ const k = trimmed.slice(0, colonIdx).trim();
668
+ const v = trimmed.slice(colonIdx + 1).trim();
669
+ if (nestedKey === "resume" || nestedKey === "fork") {
670
+ if (!nestedInvocation) nestedInvocation = { args: [] };
671
+ if (k === "args" && v === "") {
672
+ nestedInvocation.args = [];
673
+ argsCapture = nestedInvocation.args;
674
+ argsBaseIndent = indent;
675
+ continue;
676
+ }
677
+ if (k === "command" && v !== "") {
678
+ nestedInvocation.command = decodeYamlScalar(v);
679
+ continue;
680
+ }
681
+ }
682
+ continue;
683
+ }
684
+ if (nestedKey && indent <= nestedBaseIndent) {
685
+ closeNestedBlock();
686
+ }
687
+ if (indent >= 4 && current) {
688
+ const colonIdx = trimmed.indexOf(":");
689
+ if (colonIdx <= 0) continue;
690
+ const k = trimmed.slice(0, colonIdx).trim();
691
+ const v = trimmed.slice(colonIdx + 1).trim();
692
+ if (k === "args" && v === "") {
693
+ argsCapture = [];
694
+ argsBaseIndent = indent;
695
+ current.args = argsCapture;
696
+ continue;
697
+ }
698
+ if ((k === "resume" || k === "fork") && v === "") {
699
+ nestedKey = k;
700
+ nestedInvocation = { args: [] };
701
+ nestedBaseIndent = indent;
702
+ continue;
703
+ }
704
+ if (v === "" && !KNOWN_AGENT_SCALAR_FIELDS.has(k)) {
705
+ nestedKey = "__skip__";
706
+ nestedInvocation = null;
707
+ nestedBaseIndent = indent;
708
+ continue;
709
+ }
710
+ assignAgentField(current, k, v);
711
+ }
712
+ }
713
+ closeNestedBlock();
714
+ flushCurrent();
715
+ if (agents.length === 0) return [];
716
+ return agents;
717
+ }
718
+ function normalizeAgentsFromConfig(agents) {
719
+ if (agents === null) return null;
720
+ try {
721
+ const normalized = agents.map((agent) => ({
722
+ ...agent,
723
+ command: parseAgentCommand(agent.command, agent.id)
724
+ }));
725
+ validateAgentList(normalized);
726
+ return normalized;
727
+ } catch (err) {
728
+ const msg = err instanceof Error ? err.message : String(err);
729
+ console.warn(
730
+ `Warning: ~/.syntaur/config.md agents block is invalid (${msg}) \u2014 using built-in defaults`
731
+ );
732
+ return null;
733
+ }
734
+ }
735
+ function decodeYamlScalar(value) {
736
+ const trimmed = value.trim();
737
+ if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
738
+ const body = trimmed.slice(1, -1);
739
+ let out = "";
740
+ for (let i = 0; i < body.length; i++) {
741
+ const ch = body[i];
742
+ if (ch === "\\" && i + 1 < body.length) {
743
+ const next = body[i + 1];
744
+ switch (next) {
745
+ case "\\":
746
+ out += "\\";
747
+ break;
748
+ case '"':
749
+ out += '"';
750
+ break;
751
+ case "n":
752
+ out += "\n";
753
+ break;
754
+ case "t":
755
+ out += " ";
756
+ break;
757
+ case "r":
758
+ out += "\r";
759
+ break;
760
+ default:
761
+ out += next;
762
+ break;
763
+ }
764
+ i++;
765
+ continue;
766
+ }
767
+ out += ch;
768
+ }
769
+ return out;
770
+ }
771
+ if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
772
+ return trimmed.slice(1, -1).replace(/''/g, "'");
773
+ }
774
+ return trimmed;
775
+ }
776
+ function assignAgentField(target, key, rawValue) {
777
+ const value = decodeYamlScalar(rawValue);
778
+ switch (key) {
779
+ case "id":
780
+ target.id = value;
781
+ break;
782
+ case "label":
783
+ target.label = value;
784
+ break;
785
+ case "command":
786
+ target.command = value;
787
+ break;
788
+ case "promptArgPosition":
789
+ target.promptArgPosition = value;
790
+ break;
791
+ case "default":
792
+ target.default = value === "true";
793
+ break;
794
+ case "resolveFromShellAliases":
795
+ target.resolveFromShellAliases = value === "true";
796
+ break;
797
+ }
798
+ }
799
+ async function readConfig() {
800
+ const configPath = resolve3(syntaurRoot(), "config.md");
801
+ if (!await fileExists(configPath)) {
802
+ return cloneDefaultConfig();
803
+ }
804
+ if (!migratedConfigPaths.has(configPath)) {
805
+ migratedConfigPaths.add(configPath);
806
+ await migrateLegacyConfig(configPath);
807
+ }
808
+ const content = await readFile2(configPath, "utf-8");
809
+ const fm = parseFrontmatter(content);
810
+ if (Object.keys(fm).length === 0) {
811
+ console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
812
+ return cloneDefaultConfig();
813
+ }
814
+ let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
815
+ if (!isAbsolute(projectDir)) {
816
+ console.warn(
817
+ `Warning: config.md defaultProjectDir is not an absolute path ("${fm["defaultProjectDir"]}"), using default`
818
+ );
819
+ projectDir = DEFAULT_CONFIG.defaultProjectDir;
820
+ }
821
+ const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
822
+ return {
823
+ version: fm["version"] || DEFAULT_CONFIG.version,
824
+ defaultProjectDir: projectDir,
825
+ onboarding: {
826
+ completed: fm["onboarding.completed"] === "true"
827
+ },
828
+ agentDefaults: {
829
+ trustLevel: fm["agentDefaults.trustLevel"] || DEFAULT_CONFIG.agentDefaults.trustLevel,
830
+ autoApprove: fm["agentDefaults.autoApprove"] === "true" || DEFAULT_CONFIG.agentDefaults.autoApprove,
831
+ autoCreateWorktree: AUTO_CREATE_WORKTREE_VALUES.includes(
832
+ fm["agentDefaults.autoCreateWorktree"]
833
+ ) ? fm["agentDefaults.autoCreateWorktree"] : DEFAULT_CONFIG.agentDefaults.autoCreateWorktree
834
+ },
835
+ integrations: {
836
+ claudePluginDir: parseOptionalAbsolutePath(
837
+ fm["integrations.claudePluginDir"],
838
+ "integrations.claudePluginDir"
839
+ ),
840
+ codexPluginDir: parseOptionalAbsolutePath(
841
+ fm["integrations.codexPluginDir"],
842
+ "integrations.codexPluginDir"
843
+ ),
844
+ codexMarketplacePath: parseOptionalAbsolutePath(
845
+ fm["integrations.codexMarketplacePath"],
846
+ "integrations.codexMarketplacePath"
847
+ )
848
+ },
849
+ backup: fm["backup.repo"] || fm["backup.categories"] ? {
850
+ repo: fm["backup.repo"] && fm["backup.repo"] !== "null" ? fm["backup.repo"] : null,
851
+ categories: fm["backup.categories"] || "projects, playbooks, todos, servers, config",
852
+ lastBackup: fm["backup.lastBackup"] && fm["backup.lastBackup"] !== "null" ? fm["backup.lastBackup"] : null,
853
+ lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
854
+ } : null,
855
+ statuses: parseStatusConfig(content),
856
+ types: null,
857
+ agents: normalizeAgentsFromConfig(parseAgentsConfig(content)),
858
+ playbooks: parsePlaybooksConfig(fmBlock),
859
+ theme: parseThemeConfig(content),
860
+ hotkeys: parseHotkeyBindingsConfig(content),
861
+ terminal: (() => {
862
+ try {
863
+ return parseTerminalConfig(fm["terminal"]);
864
+ } catch (err) {
865
+ const msg = err instanceof TerminalConfigError ? err.message : String(err);
866
+ console.warn(`Warning: ${msg} \u2014 falling back to default`);
867
+ return null;
868
+ }
869
+ })()
870
+ };
871
+ }
872
+ function getAgents(config) {
873
+ return config.agents ?? BUILTIN_AGENTS;
874
+ }
875
+ function parseTerminalConfig(value) {
876
+ if (value === void 0 || value === null || value === "") return null;
877
+ if (typeof value !== "string") {
878
+ throw new TerminalConfigError(
879
+ `terminal must be a string \u2014 got ${typeof value}`
880
+ );
881
+ }
882
+ const trimmed = value.trim();
883
+ if (trimmed === "") return null;
884
+ if (!TERMINAL_CHOICES.includes(trimmed)) {
885
+ throw new TerminalConfigError(
886
+ `terminal "${trimmed}" is not a known choice \u2014 expected one of ${TERMINAL_CHOICES.join("|")}`
887
+ );
888
+ }
889
+ return trimmed;
890
+ }
891
+ function getTerminal(config) {
892
+ return config.terminal ?? "terminal-app";
893
+ }
894
+ var TERMINAL_CHOICES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
895
+ var init_config2 = __esm({
896
+ "src/utils/config.ts"() {
897
+ "use strict";
898
+ init_paths();
899
+ init_fs();
900
+ init_config();
901
+ init_fs_migration();
902
+ init_hotkeysCatalog();
903
+ init_agents_schema();
904
+ TERMINAL_CHOICES = [
905
+ "terminal-app",
906
+ "iterm",
907
+ "ghostty",
908
+ "alacritty",
909
+ "warp",
910
+ "kitty"
911
+ ];
912
+ DEFAULT_CONFIG = {
913
+ version: "2.0",
914
+ defaultProjectDir: defaultProjectDir(),
915
+ onboarding: {
916
+ completed: false
917
+ },
918
+ agentDefaults: {
919
+ trustLevel: "medium",
920
+ autoApprove: false,
921
+ autoCreateWorktree: "ask"
922
+ },
923
+ integrations: {
924
+ claudePluginDir: null,
925
+ codexPluginDir: null,
926
+ codexMarketplacePath: null
927
+ },
928
+ backup: null,
929
+ statuses: null,
930
+ types: null,
931
+ agents: null,
932
+ playbooks: {
933
+ disabled: []
934
+ },
935
+ theme: null,
936
+ hotkeys: null,
937
+ terminal: null
938
+ };
939
+ AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
940
+ AgentConfigError = class extends Error {
941
+ };
942
+ KNOWN_AGENT_SCALAR_FIELDS = /* @__PURE__ */ new Set([
943
+ "id",
944
+ "label",
945
+ "command",
946
+ "promptArgPosition",
947
+ "default",
948
+ "resolveFromShellAliases"
949
+ ]);
950
+ migratedConfigPaths = /* @__PURE__ */ new Set();
951
+ TerminalConfigError = class extends Error {
952
+ };
953
+ }
954
+ });
955
+
956
+ // src/dashboard/parser.ts
957
+ function extractFrontmatter(fileContent) {
958
+ const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
959
+ if (!match) {
960
+ return ["", fileContent];
961
+ }
962
+ const frontmatterBlock = match[1];
963
+ const body = fileContent.slice(match[0].length).trim();
964
+ return [frontmatterBlock, body];
965
+ }
966
+ function parseSimpleValue(raw) {
967
+ const trimmed = raw.trim();
968
+ if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
969
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
970
+ return trimmed.slice(1, -1);
971
+ }
972
+ return trimmed;
973
+ }
974
+ function getField(frontmatter, key) {
975
+ const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
976
+ if (!match) return null;
977
+ return parseSimpleValue(match[1]);
978
+ }
979
+ function getNestedField(frontmatter, parent, key) {
980
+ const parentRegex = new RegExp(`^${parent}:\\s*\\n((?:\\s+.*\\n?)*)`, "m");
981
+ const parentMatch = frontmatter.match(parentRegex);
982
+ if (!parentMatch) return null;
983
+ const block = parentMatch[1];
984
+ const fieldMatch = block.match(new RegExp(`^\\s+${key}:\\s*(.*)$`, "m"));
985
+ if (!fieldMatch) return null;
986
+ return parseSimpleValue(fieldMatch[1]);
987
+ }
988
+ function parseListField(frontmatter, fieldName) {
989
+ const inlineMatch = frontmatter.match(new RegExp(`^${fieldName}:\\s*\\[\\s*\\]`, "m"));
990
+ if (inlineMatch) return [];
991
+ const results = [];
992
+ const blockMatch = frontmatter.match(
993
+ new RegExp(`^${fieldName}:\\s*\\n((?:\\s+-\\s+.*\\n?)*)`, "m")
994
+ );
995
+ if (blockMatch) {
996
+ let item;
997
+ const regex = /^\s+-\s+(.+)$/gm;
998
+ while ((item = regex.exec(blockMatch[1])) !== null) {
999
+ results.push(item[1].trim());
1000
+ }
1001
+ }
1002
+ return results;
1003
+ }
1004
+ function parseProject(fileContent) {
1005
+ const [fm, body] = extractFrontmatter(fileContent);
1006
+ const slug = getField(fm, "slug") ?? getField(fm, "mission") ?? "";
1007
+ return {
1008
+ id: getField(fm, "id") ?? "",
1009
+ slug,
1010
+ title: getField(fm, "title") ?? "",
1011
+ archived: getField(fm, "archived") === "true",
1012
+ archivedAt: getField(fm, "archivedAt"),
1013
+ archivedReason: getField(fm, "archivedReason"),
1014
+ statusOverride: getField(fm, "statusOverride"),
1015
+ created: getField(fm, "created") ?? "",
1016
+ updated: getField(fm, "updated") ?? "",
1017
+ tags: parseListField(fm, "tags"),
1018
+ workspace: getField(fm, "workspace"),
1019
+ externalIds: parseExternalIds(fm),
1020
+ body
1021
+ };
1022
+ }
1023
+ function parseStatus(fileContent) {
1024
+ const [fm, body] = extractFrontmatter(fileContent);
1025
+ const progress = { total: 0 };
1026
+ const progressMatch = fm.match(/^progress:\s*\n((?:\s+.*\n?)*)/m);
1027
+ if (progressMatch) {
1028
+ const lines = progressMatch[1].split("\n");
1029
+ for (const line of lines) {
1030
+ const kv = line.match(/^\s+(\w+):\s*(\d+)/);
1031
+ if (kv) {
1032
+ progress[kv[1]] = parseInt(kv[2], 10);
1033
+ }
1034
+ }
1035
+ }
1036
+ return {
1037
+ project: getField(fm, "project") ?? "",
1038
+ status: getField(fm, "status") ?? "pending",
1039
+ progress,
1040
+ needsAttention: {
1041
+ blockedCount: parseInt(getNestedField(fm, "needsAttention", "blockedCount") ?? "0", 10),
1042
+ failedCount: parseInt(getNestedField(fm, "needsAttention", "failedCount") ?? "0", 10),
1043
+ openQuestions: parseInt(getNestedField(fm, "needsAttention", "openQuestions") ?? "0", 10)
1044
+ },
1045
+ body
1046
+ };
1047
+ }
1048
+ function parseExternalIds(frontmatter) {
1049
+ const inlineMatch = frontmatter.match(/^externalIds:\s*\[\s*\]/m);
1050
+ if (inlineMatch) return [];
1051
+ const results = [];
1052
+ const blockMatch = frontmatter.match(
1053
+ /^externalIds:\s*\n((?:\s+-\s+[\s\S]*?)(?=^\w|\n---))/m
1054
+ );
1055
+ if (!blockMatch) return [];
1056
+ const itemBlocks = blockMatch[1].split(/\n\s+-\s+/).filter(Boolean);
1057
+ for (const block of itemBlocks) {
1058
+ const lines = block.split("\n");
1059
+ const entry = {};
1060
+ for (const line of lines) {
1061
+ const colonIdx = line.indexOf(":");
1062
+ if (colonIdx < 0) continue;
1063
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
1064
+ if (!key) continue;
1065
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
1066
+ }
1067
+ if (entry["system"] && entry["id"]) {
1068
+ results.push({
1069
+ system: entry["system"],
1070
+ id: entry["id"],
1071
+ url: entry["url"] || null
1072
+ });
1073
+ }
1074
+ }
1075
+ return results;
1076
+ }
1077
+ function parseAssignmentFull(fileContent) {
1078
+ const [fm, body] = extractFrontmatter(fileContent);
1079
+ return {
1080
+ id: getField(fm, "id") ?? "",
1081
+ slug: getField(fm, "slug") ?? "",
1082
+ title: getField(fm, "title") ?? "",
1083
+ project: getField(fm, "project"),
1084
+ workspaceGroup: getField(fm, "workspaceGroup"),
1085
+ type: getField(fm, "type"),
1086
+ status: getField(fm, "status") ?? "pending",
1087
+ priority: getField(fm, "priority") ?? "medium",
1088
+ assignee: getField(fm, "assignee"),
1089
+ dependsOn: parseListField(fm, "dependsOn"),
1090
+ links: parseListField(fm, "links"),
1091
+ blockedReason: getField(fm, "blockedReason"),
1092
+ workspace: {
1093
+ repository: getNestedField(fm, "workspace", "repository"),
1094
+ worktreePath: getNestedField(fm, "workspace", "worktreePath"),
1095
+ branch: getNestedField(fm, "workspace", "branch"),
1096
+ parentBranch: getNestedField(fm, "workspace", "parentBranch")
1097
+ },
1098
+ externalIds: parseExternalIds(fm),
1099
+ tags: parseListField(fm, "tags"),
1100
+ created: getField(fm, "created") ?? "",
1101
+ updated: getField(fm, "updated") ?? "",
1102
+ body
1103
+ };
1104
+ }
1105
+ function parsePlan(fileContent) {
1106
+ const [fm, body] = extractFrontmatter(fileContent);
1107
+ return {
1108
+ assignment: getField(fm, "assignment") ?? "",
1109
+ status: getField(fm, "status") ?? "",
1110
+ created: getField(fm, "created") ?? "",
1111
+ updated: getField(fm, "updated") ?? "",
1112
+ body
1113
+ };
1114
+ }
1115
+ function parseScratchpad(fileContent) {
1116
+ const [fm, body] = extractFrontmatter(fileContent);
1117
+ return {
1118
+ assignment: getField(fm, "assignment") ?? "",
1119
+ updated: getField(fm, "updated") ?? "",
1120
+ body
1121
+ };
1122
+ }
1123
+ function parseHandoff(fileContent) {
1124
+ const [fm, body] = extractFrontmatter(fileContent);
1125
+ return {
1126
+ assignment: getField(fm, "assignment") ?? "",
1127
+ handoffCount: parseInt(getField(fm, "handoffCount") ?? "0", 10),
1128
+ updated: getField(fm, "updated") ?? "",
1129
+ body
1130
+ };
1131
+ }
1132
+ function parseDecisionRecord(fileContent) {
1133
+ const [fm, body] = extractFrontmatter(fileContent);
1134
+ return {
1135
+ assignment: getField(fm, "assignment") ?? "",
1136
+ decisionCount: parseInt(getField(fm, "decisionCount") ?? "0", 10),
1137
+ updated: getField(fm, "updated") ?? "",
1138
+ body
1139
+ };
1140
+ }
1141
+ function parseComments(fileContent) {
1142
+ const [fm, body] = extractFrontmatter(fileContent);
1143
+ const entries = [];
1144
+ const sections = body.split(/^## /m).slice(1);
1145
+ for (const section of sections) {
1146
+ const newlineIdx = section.indexOf("\n");
1147
+ if (newlineIdx === -1) continue;
1148
+ const id = section.slice(0, newlineIdx).trim();
1149
+ const rest = section.slice(newlineIdx + 1);
1150
+ const headerMatch = rest.match(
1151
+ /^\s*\*\*Recorded:\*\*\s*(.*)\n\*\*Author:\*\*\s*(.*)\n\*\*Type:\*\*\s*(question|note|feedback)(?:\n\*\*Reply to:\*\*\s*(.*))?(?:\n\*\*Resolved:\*\*\s*(true|false))?\n+([\s\S]*)$/
1152
+ );
1153
+ if (!headerMatch) continue;
1154
+ const [, timestamp, author, type, replyTo, resolvedStr, entryBody] = headerMatch;
1155
+ const entry = {
1156
+ id,
1157
+ timestamp: timestamp.trim(),
1158
+ author: author.trim(),
1159
+ type,
1160
+ body: entryBody.trim()
1161
+ };
1162
+ if (replyTo) entry.replyTo = replyTo.trim();
1163
+ if (resolvedStr) entry.resolved = resolvedStr === "true";
1164
+ entries.push(entry);
1165
+ }
1166
+ return {
1167
+ assignment: getField(fm, "assignment") ?? "",
1168
+ entryCount: parseInt(getField(fm, "entryCount") ?? "0", 10),
1169
+ updated: getField(fm, "updated") ?? "",
1170
+ entries,
1171
+ body
1172
+ };
1173
+ }
1174
+ function parseProgress(fileContent) {
1175
+ const [fm, body] = extractFrontmatter(fileContent);
1176
+ const entries = [];
1177
+ const sections = body.split(/^## /m).slice(1);
1178
+ for (const section of sections) {
1179
+ const newlineIdx = section.indexOf("\n");
1180
+ if (newlineIdx === -1) continue;
1181
+ const timestamp = section.slice(0, newlineIdx).trim();
1182
+ const entryBody = section.slice(newlineIdx + 1).trim();
1183
+ entries.push({ timestamp, body: entryBody });
1184
+ }
1185
+ return {
1186
+ assignment: getField(fm, "assignment") ?? "",
1187
+ entryCount: parseInt(getField(fm, "entryCount") ?? "0", 10),
1188
+ updated: getField(fm, "updated") ?? "",
1189
+ entries,
1190
+ body
1191
+ };
1192
+ }
1193
+ function extractMermaidGraph(body) {
1194
+ const match = body.match(/```mermaid\n([\s\S]*?)```/);
1195
+ return match ? match[1].trim() : null;
1196
+ }
1197
+ var init_parser = __esm({
1198
+ "src/dashboard/parser.ts"() {
1199
+ "use strict";
1200
+ }
1201
+ });
1202
+
1203
+ // src/utils/assignment-resolver.ts
1204
+ import { resolve as resolve4 } from "path";
1205
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
1206
+ async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
1207
+ let standaloneMatch = null;
1208
+ let projectMatch = null;
1209
+ const standaloneDir = resolve4(assignmentsDir, id);
1210
+ const standalonePath = resolve4(standaloneDir, "assignment.md");
1211
+ if (await fileExists(standalonePath)) {
1212
+ let workspaceGroup = null;
1213
+ try {
1214
+ const content = await readFile3(standalonePath, "utf-8");
1215
+ const [fm] = extractFrontmatter(content);
1216
+ workspaceGroup = getField(fm, "workspaceGroup");
1217
+ } catch {
1218
+ }
1219
+ standaloneMatch = {
1220
+ assignmentDir: standaloneDir,
1221
+ projectSlug: null,
1222
+ assignmentSlug: id,
1223
+ id,
1224
+ standalone: true,
1225
+ workspaceGroup
1226
+ };
1227
+ }
1228
+ if (await fileExists(projectsDir)) {
1229
+ try {
1230
+ const projects = await readdir2(projectsDir, { withFileTypes: true });
1231
+ for (const p of projects) {
1232
+ if (!p.isDirectory()) continue;
1233
+ if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
1234
+ const assignmentsPath = resolve4(projectsDir, p.name, "assignments");
1235
+ if (!await fileExists(assignmentsPath)) continue;
1236
+ const entries = await readdir2(assignmentsPath, { withFileTypes: true });
1237
+ for (const a of entries) {
1238
+ if (!a.isDirectory()) continue;
1239
+ const aPath = resolve4(assignmentsPath, a.name, "assignment.md");
1240
+ if (!await fileExists(aPath)) continue;
1241
+ try {
1242
+ const content = await readFile3(aPath, "utf-8");
1243
+ const [fm] = extractFrontmatter(content);
1244
+ const fileId = getField(fm, "id");
1245
+ if (fileId === id) {
1246
+ projectMatch = {
1247
+ assignmentDir: resolve4(assignmentsPath, a.name),
1248
+ projectSlug: p.name,
1249
+ assignmentSlug: a.name,
1250
+ id,
1251
+ standalone: false,
1252
+ workspaceGroup: null
1253
+ };
1254
+ break;
1255
+ }
1256
+ } catch {
1257
+ }
1258
+ }
1259
+ if (projectMatch) break;
1260
+ }
1261
+ } catch {
1262
+ }
1263
+ }
1264
+ if (standaloneMatch && projectMatch) {
1265
+ console.warn(
1266
+ `Duplicate assignment ID ${id} found in both standalone and project-nested locations; using standalone`
1267
+ );
1268
+ return standaloneMatch;
1269
+ }
1270
+ return standaloneMatch ?? projectMatch ?? null;
1271
+ }
1272
+ var init_assignment_resolver = __esm({
1273
+ "src/utils/assignment-resolver.ts"() {
1274
+ "use strict";
1275
+ init_fs();
1276
+ init_parser();
1277
+ }
1278
+ });
1279
+
1280
+ // src/lifecycle/types.ts
1281
+ var DEFAULT_STATUSES;
1282
+ var init_types = __esm({
1283
+ "src/lifecycle/types.ts"() {
1284
+ "use strict";
1285
+ DEFAULT_STATUSES = [
1286
+ "draft",
1287
+ "pending",
1288
+ "ready_for_planning",
1289
+ "ready_to_implement",
1290
+ "in_progress",
1291
+ "blocked",
1292
+ "review",
1293
+ "completed",
1294
+ "failed"
1295
+ ];
1296
+ }
1297
+ });
1298
+
1299
+ // src/lifecycle/state-machine.ts
1300
+ function buildTransitionTable(transitions) {
1301
+ const table = /* @__PURE__ */ new Map();
1302
+ for (const t of transitions) {
1303
+ table.set(`${t.from}:${t.command}`, t.to);
1304
+ }
1305
+ return table;
1306
+ }
1307
+ function getTargetStatus(_from, command, table) {
1308
+ if (!table || table === DEFAULT_TRANSITION_TABLE) {
1309
+ return DEFAULT_COMMAND_TARGETS.get(command) ?? null;
1310
+ }
1311
+ return table.get(command) ?? table.get(`${_from}:${command}`) ?? null;
1312
+ }
1313
+ var DEFAULT_COMMAND_TARGETS, DEFAULT_TRANSITION_TABLE;
1314
+ var init_state_machine = __esm({
1315
+ "src/lifecycle/state-machine.ts"() {
1316
+ "use strict";
1317
+ init_types();
1318
+ DEFAULT_COMMAND_TARGETS = /* @__PURE__ */ new Map([
1319
+ ["start", "in_progress"],
1320
+ ["shape", "ready_for_planning"],
1321
+ ["plan-ready", "ready_to_implement"],
1322
+ ["implement", "in_progress"],
1323
+ ["block", "blocked"],
1324
+ ["unblock", "in_progress"],
1325
+ ["review", "review"],
1326
+ ["complete", "completed"],
1327
+ ["fail", "failed"],
1328
+ ["reopen", "in_progress"]
1329
+ ]);
1330
+ DEFAULT_TRANSITION_TABLE = /* @__PURE__ */ new Map([
1331
+ ["pending:start", "in_progress"],
1332
+ ["pending:block", "blocked"],
1333
+ ["draft:shape", "ready_for_planning"],
1334
+ ["draft:start", "in_progress"],
1335
+ ["ready_for_planning:plan-ready", "ready_to_implement"],
1336
+ ["ready_for_planning:start", "in_progress"],
1337
+ ["ready_to_implement:implement", "in_progress"],
1338
+ ["in_progress:block", "blocked"],
1339
+ ["in_progress:review", "review"],
1340
+ ["in_progress:complete", "completed"],
1341
+ ["in_progress:fail", "failed"],
1342
+ ["blocked:unblock", "in_progress"],
1343
+ ["review:start", "in_progress"],
1344
+ ["review:complete", "completed"],
1345
+ ["review:fail", "failed"],
1346
+ ["completed:reopen", "in_progress"],
1347
+ ["failed:reopen", "in_progress"]
1348
+ ]);
1349
+ }
1350
+ });
1351
+
1352
+ // src/lifecycle/frontmatter.ts
1353
+ var init_frontmatter = __esm({
1354
+ "src/lifecycle/frontmatter.ts"() {
1355
+ "use strict";
1356
+ }
1357
+ });
1358
+
1359
+ // src/utils/timestamp.ts
1360
+ var init_timestamp = __esm({
1361
+ "src/utils/timestamp.ts"() {
1362
+ "use strict";
1363
+ }
1364
+ });
1365
+
1366
+ // src/todos/parser.ts
1367
+ import { randomBytes } from "crypto";
1368
+ import { readFile as readFile4 } from "fs/promises";
1369
+ import { resolve as resolve5 } from "path";
1370
+ var init_parser2 = __esm({
1371
+ "src/todos/parser.ts"() {
1372
+ "use strict";
1373
+ init_parser();
1374
+ init_fs();
1375
+ }
1376
+ });
1377
+
1378
+ // src/lifecycle/linked-todos.ts
1379
+ import { readdir as readdir3 } from "fs/promises";
1380
+ import { resolve as resolve6 } from "path";
1381
+ var init_linked_todos = __esm({
1382
+ "src/lifecycle/linked-todos.ts"() {
1383
+ "use strict";
1384
+ init_parser2();
1385
+ init_fs();
1386
+ }
1387
+ });
1388
+
1389
+ // src/lifecycle/transitions.ts
1390
+ import { resolve as resolve7 } from "path";
1391
+ import { readFile as readFile5 } from "fs/promises";
1392
+ var init_transitions = __esm({
1393
+ "src/lifecycle/transitions.ts"() {
1394
+ "use strict";
1395
+ init_fs();
1396
+ init_timestamp();
1397
+ init_state_machine();
1398
+ init_frontmatter();
1399
+ init_linked_todos();
1400
+ }
1401
+ });
1402
+
1403
+ // src/lifecycle/index.ts
1404
+ var init_lifecycle = __esm({
1405
+ "src/lifecycle/index.ts"() {
1406
+ "use strict";
1407
+ init_types();
1408
+ init_state_machine();
1409
+ init_frontmatter();
1410
+ init_transitions();
1411
+ }
1412
+ });
1413
+
1414
+ // src/utils/slug.ts
1415
+ var init_slug = __esm({
1416
+ "src/utils/slug.ts"() {
1417
+ "use strict";
1418
+ }
1419
+ });
1420
+
1421
+ // src/utils/playbooks.ts
1422
+ import { resolve as resolve8 } from "path";
1423
+ import { readdir as readdir4, readFile as readFile6, unlink } from "fs/promises";
1424
+ var init_playbooks = __esm({
1425
+ "src/utils/playbooks.ts"() {
1426
+ "use strict";
1427
+ init_fs();
1428
+ init_parser();
1429
+ init_timestamp();
1430
+ init_config2();
1431
+ init_slug();
1432
+ }
1433
+ });
1434
+
1435
+ // src/dashboard/help.ts
1436
+ var CLI_COMMANDS, WORKFLOW;
1437
+ var init_help = __esm({
1438
+ "src/dashboard/help.ts"() {
1439
+ "use strict";
1440
+ init_api();
1441
+ CLI_COMMANDS = [
1442
+ // --- Core setup & scaffolding (indices 0-4) ---
1443
+ {
1444
+ command: "syntaur setup",
1445
+ description: "Initialize Syntaur and optionally install plugins or launch the dashboard.",
1446
+ example: "syntaur setup"
1447
+ },
1448
+ {
1449
+ command: "syntaur init",
1450
+ description: "Initialize the local Syntaur home directory and config scaffolding without any prompts.",
1451
+ example: "syntaur init"
1452
+ },
1453
+ {
1454
+ command: "syntaur create-project",
1455
+ description: "Create a new project folder with the required source and derived files.",
1456
+ example: 'syntaur create-project "Ship dashboard overhaul"'
1457
+ },
1458
+ {
1459
+ command: "syntaur create-assignment",
1460
+ description: "Create a new assignment inside a project.",
1461
+ example: 'syntaur create-assignment "Implement overview API" --project ui-overhaul'
1462
+ },
1463
+ {
1464
+ command: "syntaur assign",
1465
+ description: "Set the assignee for an assignment before work begins.",
1466
+ example: "syntaur assign implement-overview --project ui-overhaul --agent codex-1"
1467
+ },
1468
+ // --- Lifecycle transitions ---
1469
+ {
1470
+ command: "syntaur start",
1471
+ description: "Transition an assignment to in_progress.",
1472
+ example: "syntaur start implement-overview --project ui-overhaul"
1473
+ },
1474
+ {
1475
+ command: "syntaur shape",
1476
+ description: "Transition a draft assignment to ready_for_planning once the Objective and Acceptance Criteria are fleshed out.",
1477
+ example: "syntaur shape implement-overview --project ui-overhaul"
1478
+ },
1479
+ {
1480
+ command: "syntaur plan-ready",
1481
+ description: "Transition a ready_for_planning assignment to ready_to_implement once a plan has been written and approved.",
1482
+ example: "syntaur plan-ready implement-overview --project ui-overhaul"
1483
+ },
1484
+ {
1485
+ command: "syntaur implement",
1486
+ description: "Transition a ready_to_implement assignment to in_progress when coding begins.",
1487
+ example: "syntaur implement implement-overview --project ui-overhaul"
1488
+ },
1489
+ {
1490
+ command: "syntaur migrate-statuses",
1491
+ description: "Suggest pending -> ready_for_planning promotions for fleshed-out assignments. Dry-run by default; pass --apply to write.",
1492
+ example: "syntaur migrate-statuses --apply"
1493
+ },
1494
+ {
1495
+ command: "syntaur review",
1496
+ description: "Move active work into review once implementation is ready for inspection.",
1497
+ example: "syntaur review implement-overview --project ui-overhaul"
1498
+ },
1499
+ {
1500
+ command: "syntaur complete",
1501
+ description: "Mark an assignment completed after review or direct completion.",
1502
+ example: "syntaur complete implement-overview --project ui-overhaul"
1503
+ },
1504
+ {
1505
+ command: "syntaur block",
1506
+ description: "Mark an assignment blocked and record the explicit reason.",
1507
+ example: 'syntaur block implement-overview --project ui-overhaul --reason "Waiting on API spec"'
1508
+ },
1509
+ {
1510
+ command: "syntaur unblock",
1511
+ description: "Move a blocked assignment back to in_progress after the blocker is cleared.",
1512
+ example: "syntaur unblock implement-overview --project ui-overhaul"
1513
+ },
1514
+ {
1515
+ command: "syntaur fail",
1516
+ description: "Mark an assignment failed when it cannot be completed as planned.",
1517
+ example: "syntaur fail implement-overview --project ui-overhaul"
1518
+ },
1519
+ {
1520
+ command: "syntaur reopen",
1521
+ description: "Reopen a completed or failed assignment back to in_progress.",
1522
+ example: "syntaur reopen implement-overview --project ui-overhaul"
1523
+ },
1524
+ // --- Dashboard (index 12) ---
1525
+ {
1526
+ command: "syntaur dashboard",
1527
+ description: "Start the local dashboard UI over the project files on disk.",
1528
+ example: "syntaur dashboard --port 4800"
1529
+ },
1530
+ // --- Plugin & adapter setup (indices 13-16) ---
1531
+ {
1532
+ command: "syntaur install-plugin",
1533
+ description: "Install the Syntaur Claude Code plugin, detecting the local Claude marketplace when available and prompting for the target directory when interactive.",
1534
+ example: "syntaur install-plugin --target-dir ~/.claude/plugins/marketplaces/user-plugins/plugins/syntaur"
1535
+ },
1536
+ {
1537
+ command: "syntaur install-codex-plugin",
1538
+ description: "Install the Syntaur Codex plugin and register its marketplace entry, prompting for both paths when interactive.",
1539
+ example: "syntaur install-codex-plugin --target-dir ~/plugins/syntaur --marketplace-path ~/.agents/plugins/marketplace.json"
1540
+ },
1541
+ {
1542
+ command: "syntaur uninstall",
1543
+ description: "Remove Syntaur plugins and optionally local ~/.syntaur data.",
1544
+ example: "syntaur uninstall --all"
1545
+ },
1546
+ {
1547
+ command: "syntaur setup-adapter",
1548
+ description: "Generate adapter instruction files for cursor, codex, or opencode in the current directory.",
1549
+ example: "syntaur setup-adapter cursor --project ui-overhaul --assignment implement-overview"
1550
+ },
1551
+ // --- Session & server tracking (index 17) ---
1552
+ {
1553
+ command: "syntaur track-session",
1554
+ description: "Register an agent session. Requires --session-id from the agent runtime (real, not generated). Pass --transcript-path for the rollout/transcript file. --project and --assignment are optional.",
1555
+ example: "syntaur track-session --agent claude --session-id <real-id> --transcript-path <path> --project ui-overhaul --assignment implement-overview"
1556
+ },
1557
+ // --- Browsing & playbooks (indices 18-20) ---
1558
+ {
1559
+ command: "syntaur browse",
1560
+ description: "Interactive TUI browser for projects and assignments.",
1561
+ example: "syntaur browse"
1562
+ },
1563
+ {
1564
+ command: "syntaur create-playbook",
1565
+ description: "Create a new playbook with behavioral rules for agents.",
1566
+ example: 'syntaur create-playbook "Code Review Standards"'
1567
+ },
1568
+ {
1569
+ command: "syntaur list-playbooks",
1570
+ description: "List playbooks in the Syntaur home directory. Disabled playbooks are excluded by default; pass --all to include them with a (disabled) tag.",
1571
+ example: "syntaur list-playbooks --all"
1572
+ },
1573
+ {
1574
+ command: "syntaur enable-playbook",
1575
+ description: "Re-enable a previously-disabled playbook so agents load it again. Updates config.md and rebuilds manifest.md.",
1576
+ example: "syntaur enable-playbook commit-discipline"
1577
+ },
1578
+ {
1579
+ command: "syntaur disable-playbook",
1580
+ description: "Disable a playbook so agents no longer list or load it. Playbook file is untouched; state is tracked in config.md.",
1581
+ example: "syntaur disable-playbook commit-discipline"
1582
+ },
1583
+ {
1584
+ command: "syntaur delete-playbook",
1585
+ description: "Delete a playbook from disk and regenerate the manifest. Refuses to delete the manifest itself.",
1586
+ example: "syntaur delete-playbook scratch-foo"
1587
+ }
1588
+ ];
1589
+ WORKFLOW = [
1590
+ {
1591
+ title: "Initialize the workspace",
1592
+ detail: "Run setup once so Syntaur can initialize its local home directory and offer plugin installation.",
1593
+ command: CLI_COMMANDS[0]
1594
+ },
1595
+ {
1596
+ title: "Create a project",
1597
+ detail: "Use a project for a higher-level objective. Projects group assignments, shared resources, and memories.",
1598
+ command: CLI_COMMANDS[2],
1599
+ href: "/create/project"
1600
+ },
1601
+ {
1602
+ title: "Create the first assignment",
1603
+ detail: "Assignments are the execution unit. Create one for each concrete chunk of work inside the project.",
1604
+ command: CLI_COMMANDS[3]
1605
+ },
1606
+ {
1607
+ title: "Assign the work",
1608
+ detail: "Setting an assignee before starting is recommended for clarity, but not required.",
1609
+ command: CLI_COMMANDS[4]
1610
+ },
1611
+ {
1612
+ title: "Start, review, complete, or block through lifecycle actions",
1613
+ detail: "Status changes happen through lifecycle actions, kanban drag-and-drop, or the status override controls.",
1614
+ command: CLI_COMMANDS[5]
1615
+ },
1616
+ {
1617
+ title: "Use the dashboard for triage and context",
1618
+ detail: "Overview shows the current queue, project pages show health, assignment pages show the execution surface.",
1619
+ command: CLI_COMMANDS[12],
1620
+ href: "/"
1621
+ }
1622
+ ];
1623
+ }
1624
+ });
1625
+
1626
+ // src/dashboard/session-db.ts
1627
+ import Database from "better-sqlite3";
1628
+ import { resolve as resolve9 } from "path";
1629
+ import { readdir as readdir5 } from "fs/promises";
1630
+ function getSessionDb() {
1631
+ if (!db) {
1632
+ throw new Error(
1633
+ "Session database not initialized. Call initSessionDb() first."
1634
+ );
1635
+ }
1636
+ return db;
1637
+ }
1638
+ var db;
1639
+ var init_session_db = __esm({
1640
+ "src/dashboard/session-db.ts"() {
1641
+ "use strict";
1642
+ init_paths();
1643
+ init_fs();
1644
+ db = null;
1645
+ }
1646
+ });
1647
+
1648
+ // src/dashboard/agent-sessions.ts
1649
+ import { readFile as readFile7 } from "fs/promises";
1650
+ import { resolve as resolve10 } from "path";
1651
+ function rowToSession(row) {
1652
+ return {
1653
+ sessionId: row.session_id,
1654
+ projectSlug: row.project_slug ?? null,
1655
+ assignmentSlug: row.assignment_slug ?? null,
1656
+ agent: row.agent,
1657
+ started: row.started,
1658
+ ended: row.ended ?? null,
1659
+ status: row.status,
1660
+ path: row.path ?? "",
1661
+ description: row.description ?? null,
1662
+ transcriptPath: row.transcript_path ?? null,
1663
+ pid: row.pid ?? null,
1664
+ pidStartedAt: row.pid_started_at ?? null
1665
+ };
1666
+ }
1667
+ function getSessionById(sessionId) {
1668
+ const db2 = getSessionDb();
1669
+ const row = db2.prepare("SELECT * FROM sessions WHERE session_id = ? LIMIT 1").get(sessionId);
1670
+ return row ? rowToSession(row) : null;
1671
+ }
1672
+ var init_agent_sessions = __esm({
1673
+ "src/dashboard/agent-sessions.ts"() {
1674
+ "use strict";
1675
+ init_fs();
1676
+ init_session_db();
1677
+ }
1678
+ });
1679
+
1680
+ // src/dashboard/overviewCopy.ts
1681
+ var init_overviewCopy = __esm({
1682
+ "src/dashboard/overviewCopy.ts"() {
1683
+ "use strict";
1684
+ }
1685
+ });
1686
+
1687
+ // src/dashboard/api.ts
1688
+ import { readdir as readdir6, readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
1689
+ import { resolve as resolve11, dirname as dirname2, basename } from "path";
1690
+ async function listStandaloneRecords(assignmentsDir) {
1691
+ if (!assignmentsDir) return [];
1692
+ if (!await fileExists(assignmentsDir)) return [];
1693
+ const entries = await readdir6(assignmentsDir, { withFileTypes: true });
1694
+ const records = [];
1695
+ for (const entry of entries) {
1696
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
1697
+ const assignmentDir = resolve11(assignmentsDir, entry.name);
1698
+ const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
1699
+ if (!await fileExists(assignmentMdPath)) continue;
1700
+ try {
1701
+ const content = await readFile8(assignmentMdPath, "utf-8");
1702
+ const record = parseAssignmentFull(content);
1703
+ records.push({ assignmentDir, id: entry.name, record });
1704
+ } catch {
1705
+ }
1706
+ }
1707
+ records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));
1708
+ return records;
1709
+ }
1710
+ function toTitleCase(s) {
1711
+ return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1712
+ }
1713
+ function getTransitionDefinitions(config) {
1714
+ if (!config.custom) return DEFAULT_TRANSITION_DEFINITIONS;
1715
+ const seen = /* @__PURE__ */ new Set();
1716
+ return config.transitions.filter((t) => {
1717
+ if (seen.has(t.command)) return false;
1718
+ seen.add(t.command);
1719
+ return true;
1720
+ }).map((t) => ({
1721
+ command: t.command,
1722
+ label: t.label ?? toTitleCase(t.command),
1723
+ description: t.description ?? `Transition via ${t.command}.`,
1724
+ requiresReason: t.requiresReason ?? false
1725
+ }));
1726
+ }
1727
+ async function getStatusConfig() {
1728
+ if (_cachedConfig) return _cachedConfig;
1729
+ const config = await readConfig();
1730
+ if (config.statuses) {
1731
+ const terminalSet = new Set(
1732
+ config.statuses.statuses.filter((s) => s.terminal).map((s) => s.id)
1733
+ );
1734
+ _cachedConfig = {
1735
+ custom: true,
1736
+ statuses: config.statuses.statuses,
1737
+ order: config.statuses.order,
1738
+ transitions: config.statuses.transitions,
1739
+ transitionTable: buildTransitionTable(config.statuses.transitions),
1740
+ terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
1741
+ };
1742
+ } else {
1743
+ _cachedConfig = {
1744
+ custom: false,
1745
+ statuses: DEFAULT_STATUSES.map((id) => ({
1746
+ id,
1747
+ label: toTitleCase(id),
1748
+ color: DEFAULT_STATUS_COLORS[id] ?? "gray",
1749
+ terminal: id === "completed" || id === "failed"
1750
+ })),
1751
+ order: [...DEFAULT_STATUSES],
1752
+ transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
1753
+ const [from, command] = key.split(":");
1754
+ return { from, command, to };
1755
+ }),
1756
+ transitionTable: DEFAULT_TRANSITION_TABLE,
1757
+ terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
1758
+ };
1759
+ }
1760
+ return _cachedConfig;
1761
+ }
1762
+ async function getStandaloneAvailableTransitions(assignment) {
1763
+ const config = await getStatusConfig();
1764
+ const transitionDefs = getTransitionDefinitions(config);
1765
+ const actions = [];
1766
+ for (const definition of transitionDefs) {
1767
+ let warning = null;
1768
+ if (definition.command === "start" && !assignment.assignee) {
1769
+ warning = "No assignee set \u2014 consider assigning before starting.";
1770
+ }
1771
+ const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
1772
+ actions.push({
1773
+ command: definition.command,
1774
+ label: definition.label,
1775
+ description: definition.description,
1776
+ targetStatus: target ?? definition.command,
1777
+ disabled: false,
1778
+ disabledReason: null,
1779
+ warning,
1780
+ requiresReason: definition.requiresReason
1781
+ });
1782
+ }
1783
+ return actions;
1784
+ }
1785
+ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
1786
+ const assignmentDir = resolve11(projectsDir, projectSlug, "assignments", assignmentSlug);
1787
+ const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
1788
+ if (!await fileExists(assignmentMdPath)) {
1789
+ return null;
1790
+ }
1791
+ const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
1792
+ const assignment = parseAssignmentFull(assignmentContent);
1793
+ let projectWorkspace = null;
1794
+ const projectMdPath = resolve11(projectsDir, projectSlug, "project.md");
1795
+ if (await fileExists(projectMdPath)) {
1796
+ const projectContent = await readFile8(projectMdPath, "utf-8");
1797
+ projectWorkspace = parseProject(projectContent).workspace;
1798
+ }
1799
+ let plan = null;
1800
+ const planPath = resolve11(assignmentDir, "plan.md");
1801
+ if (await fileExists(planPath)) {
1802
+ const planContent = await readFile8(planPath, "utf-8");
1803
+ const parsed = parsePlan(planContent);
1804
+ plan = {
1805
+ status: parsed.status,
1806
+ updated: parsed.updated,
1807
+ body: parsed.body
1808
+ };
1809
+ }
1810
+ let scratchpad = null;
1811
+ const scratchpadPath = resolve11(assignmentDir, "scratchpad.md");
1812
+ if (await fileExists(scratchpadPath)) {
1813
+ const scratchpadContent = await readFile8(scratchpadPath, "utf-8");
1814
+ const parsed = parseScratchpad(scratchpadContent);
1815
+ scratchpad = {
1816
+ updated: parsed.updated,
1817
+ body: parsed.body
1818
+ };
1819
+ }
1820
+ let handoff = null;
1821
+ const handoffPath = resolve11(assignmentDir, "handoff.md");
1822
+ if (await fileExists(handoffPath)) {
1823
+ const handoffContent = await readFile8(handoffPath, "utf-8");
1824
+ const parsed = parseHandoff(handoffContent);
1825
+ handoff = {
1826
+ updated: parsed.updated,
1827
+ handoffCount: parsed.handoffCount,
1828
+ body: parsed.body
1829
+ };
1830
+ }
1831
+ let decisionRecord = null;
1832
+ const decisionRecordPath = resolve11(assignmentDir, "decision-record.md");
1833
+ if (await fileExists(decisionRecordPath)) {
1834
+ const decisionRecordContent = await readFile8(decisionRecordPath, "utf-8");
1835
+ const parsed = parseDecisionRecord(decisionRecordContent);
1836
+ decisionRecord = {
1837
+ updated: parsed.updated,
1838
+ decisionCount: parsed.decisionCount,
1839
+ body: parsed.body
1840
+ };
1841
+ }
1842
+ let progress = null;
1843
+ const progressPath = resolve11(assignmentDir, "progress.md");
1844
+ if (await fileExists(progressPath)) {
1845
+ const progressContent = await readFile8(progressPath, "utf-8");
1846
+ const parsed = parseProgress(progressContent);
1847
+ progress = {
1848
+ updated: parsed.updated,
1849
+ entryCount: parsed.entryCount,
1850
+ entries: parsed.entries
1851
+ };
1852
+ }
1853
+ let comments = null;
1854
+ const commentsPath = resolve11(assignmentDir, "comments.md");
1855
+ if (await fileExists(commentsPath)) {
1856
+ const commentsContent = await readFile8(commentsPath, "utf-8");
1857
+ const parsed = parseComments(commentsContent);
1858
+ comments = {
1859
+ updated: parsed.updated,
1860
+ entryCount: parsed.entryCount,
1861
+ entries: parsed.entries
1862
+ };
1863
+ }
1864
+ const detail = {
1865
+ id: assignment.id,
1866
+ projectSlug,
1867
+ slug: assignment.slug || assignmentSlug,
1868
+ title: assignment.title,
1869
+ status: assignment.status,
1870
+ priority: assignment.priority,
1871
+ assignee: assignment.assignee,
1872
+ dependsOn: assignment.dependsOn,
1873
+ links: assignment.links,
1874
+ reverseLinks: [],
1875
+ enrichedLinks: [],
1876
+ blockedReason: assignment.blockedReason,
1877
+ workspace: assignment.workspace,
1878
+ projectWorkspace,
1879
+ externalIds: assignment.externalIds,
1880
+ tags: assignment.tags,
1881
+ created: assignment.created,
1882
+ updated: assignment.updated,
1883
+ body: assignment.body,
1884
+ plan,
1885
+ scratchpad,
1886
+ handoff,
1887
+ decisionRecord,
1888
+ progress,
1889
+ comments,
1890
+ referencedBy: [],
1891
+ availableTransitions: await getAvailableTransitions(
1892
+ projectsDir,
1893
+ projectSlug,
1894
+ assignmentSlug,
1895
+ assignment
1896
+ )
1897
+ };
1898
+ const selfSlug = `${projectSlug}/${detail.slug}`;
1899
+ const projectRecords = await listProjectRecords(projectsDir);
1900
+ const reverseLinks = [];
1901
+ for (const mr of projectRecords) {
1902
+ for (const a of mr.assignments) {
1903
+ const qualifiedSlug = `${mr.summary.slug}/${a.slug}`;
1904
+ if (qualifiedSlug === selfSlug) continue;
1905
+ if (a.links.includes(selfSlug)) {
1906
+ reverseLinks.push(qualifiedSlug);
1907
+ }
1908
+ }
1909
+ }
1910
+ const isValidLinkFormat = (l) => {
1911
+ const parts = l.split("/");
1912
+ return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
1913
+ };
1914
+ const forwardLinks = assignment.links.filter((l) => l !== selfSlug && isValidLinkFormat(l));
1915
+ const forwardSet = new Set(forwardLinks);
1916
+ const dedupedReverseLinks = reverseLinks.filter((l) => !forwardSet.has(l));
1917
+ detail.links = forwardLinks;
1918
+ detail.reverseLinks = dedupedReverseLinks;
1919
+ const allProjectAssignments = /* @__PURE__ */ new Map();
1920
+ for (const mr of projectRecords) {
1921
+ for (const a of mr.assignments) {
1922
+ allProjectAssignments.set(`${mr.summary.slug}/${a.slug}`, {
1923
+ title: a.title,
1924
+ status: a.status
1925
+ });
1926
+ }
1927
+ }
1928
+ const enrichedLinks = [];
1929
+ for (const linkSlug of forwardLinks) {
1930
+ const [ms, as] = linkSlug.split("/");
1931
+ const info = allProjectAssignments.get(linkSlug);
1932
+ enrichedLinks.push({
1933
+ slug: linkSlug,
1934
+ projectSlug: ms,
1935
+ assignmentSlug: as,
1936
+ title: info?.title ?? linkSlug,
1937
+ status: info?.status ?? "pending",
1938
+ isReverse: false
1939
+ });
1940
+ }
1941
+ for (const linkSlug of dedupedReverseLinks) {
1942
+ const [ms, as] = linkSlug.split("/");
1943
+ const info = allProjectAssignments.get(linkSlug);
1944
+ enrichedLinks.push({
1945
+ slug: linkSlug,
1946
+ projectSlug: ms,
1947
+ assignmentSlug: as,
1948
+ title: info?.title ?? linkSlug,
1949
+ status: info?.status ?? "pending",
1950
+ isReverse: true
1951
+ });
1952
+ }
1953
+ detail.enrichedLinks = enrichedLinks;
1954
+ detail.referencedBy = await computeReferencedBy(
1955
+ { id: assignment.id, projectSlug, slug: detail.slug },
1956
+ projectsDir,
1957
+ void 0
1958
+ );
1959
+ return detail;
1960
+ }
1961
+ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
1962
+ const sources = [];
1963
+ const projectRecords = await listProjectRecords(projectsDir);
1964
+ for (const rec of projectRecords) {
1965
+ for (const a of rec.assignments) {
1966
+ sources.push({
1967
+ id: a.id,
1968
+ slug: a.slug,
1969
+ title: a.title,
1970
+ projectSlug: rec.summary.slug,
1971
+ assignmentDir: resolve11(rec.projectPath, "assignments", a.slug)
1972
+ });
1973
+ }
1974
+ }
1975
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir);
1976
+ for (const sr of standaloneRecords) {
1977
+ sources.push({
1978
+ id: sr.id,
1979
+ slug: sr.record.slug || sr.id,
1980
+ title: sr.record.title,
1981
+ projectSlug: null,
1982
+ assignmentDir: sr.assignmentDir
1983
+ });
1984
+ }
1985
+ const references = [];
1986
+ for (const source of sources) {
1987
+ if (source.id === target.id) continue;
1988
+ const mentions = await countMentionsInAssignment(source.assignmentDir, target);
1989
+ if (mentions > 0) {
1990
+ references.push({
1991
+ sourceId: source.id,
1992
+ sourceSlug: source.slug,
1993
+ sourceTitle: source.title,
1994
+ sourceProjectSlug: source.projectSlug,
1995
+ mentions
1996
+ });
1997
+ }
1998
+ if (references.length >= REFERENCED_BY_LIMIT) break;
1999
+ }
2000
+ return references.slice(0, REFERENCED_BY_LIMIT);
2001
+ }
2002
+ async function countMentionsInAssignment(sourceDir, target) {
2003
+ const bodies = [];
2004
+ const assignmentMd = resolve11(sourceDir, "assignment.md");
2005
+ if (await fileExists(assignmentMd)) {
2006
+ const content = await readFile8(assignmentMd, "utf-8");
2007
+ const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
2008
+ if (todosMatch) bodies.push(todosMatch[1]);
2009
+ }
2010
+ for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
2011
+ const path = resolve11(sourceDir, filename);
2012
+ if (await fileExists(path)) {
2013
+ try {
2014
+ bodies.push(await readFile8(path, "utf-8"));
2015
+ } catch {
2016
+ }
2017
+ }
2018
+ }
2019
+ let total = 0;
2020
+ const patterns = buildLinkPatternsForTarget(target);
2021
+ for (const body of bodies) {
2022
+ for (const pattern of patterns) {
2023
+ const matches = body.match(pattern);
2024
+ if (matches) total += matches.length;
2025
+ }
2026
+ }
2027
+ return total;
2028
+ }
2029
+ function buildLinkPatternsForTarget(target) {
2030
+ const patterns = [];
2031
+ patterns.push(new RegExp(`/assignments/${escapeRegExpLocal(target.id)}(?:/|\\b)`, "g"));
2032
+ if (target.projectSlug) {
2033
+ patterns.push(
2034
+ new RegExp(
2035
+ `/projects/${escapeRegExpLocal(target.projectSlug)}/assignments/${escapeRegExpLocal(target.slug)}(?:/|\\b)`,
2036
+ "g"
2037
+ )
2038
+ );
2039
+ patterns.push(
2040
+ new RegExp(`\\.\\./${escapeRegExpLocal(target.slug)}(?:/|\\b)`, "g")
2041
+ );
2042
+ }
2043
+ return patterns;
2044
+ }
2045
+ function escapeRegExpLocal(value) {
2046
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2047
+ }
2048
+ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
2049
+ const resolved = await resolveAssignmentById(projectsDir, assignmentsDir, id);
2050
+ if (!resolved) return null;
2051
+ if (!resolved.standalone && resolved.projectSlug) {
2052
+ const detail = await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
2053
+ if (!detail) return null;
2054
+ detail.referencedBy = await computeReferencedBy(
2055
+ { id: detail.id, projectSlug: detail.projectSlug, slug: detail.slug },
2056
+ projectsDir,
2057
+ assignmentsDir
2058
+ );
2059
+ return detail;
2060
+ }
2061
+ const standaloneDetail = await buildStandaloneAssignmentDetail(resolved);
2062
+ if (!standaloneDetail) return null;
2063
+ standaloneDetail.referencedBy = await computeReferencedBy(
2064
+ { id: standaloneDetail.id, projectSlug: null, slug: standaloneDetail.slug },
2065
+ projectsDir,
2066
+ assignmentsDir
2067
+ );
2068
+ return standaloneDetail;
2069
+ }
2070
+ async function buildStandaloneAssignmentDetail(resolved) {
2071
+ const assignmentDir = resolved.assignmentDir;
2072
+ const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
2073
+ if (!await fileExists(assignmentMdPath)) return null;
2074
+ const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
2075
+ const assignment = parseAssignmentFull(assignmentContent);
2076
+ let plan = null;
2077
+ const planPath = resolve11(assignmentDir, "plan.md");
2078
+ if (await fileExists(planPath)) {
2079
+ const parsed = parsePlan(await readFile8(planPath, "utf-8"));
2080
+ plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
2081
+ }
2082
+ let scratchpad = null;
2083
+ const scratchpadPath = resolve11(assignmentDir, "scratchpad.md");
2084
+ if (await fileExists(scratchpadPath)) {
2085
+ const parsed = parseScratchpad(await readFile8(scratchpadPath, "utf-8"));
2086
+ scratchpad = { updated: parsed.updated, body: parsed.body };
2087
+ }
2088
+ let handoff = null;
2089
+ const handoffPath = resolve11(assignmentDir, "handoff.md");
2090
+ if (await fileExists(handoffPath)) {
2091
+ const parsed = parseHandoff(await readFile8(handoffPath, "utf-8"));
2092
+ handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
2093
+ }
2094
+ let decisionRecord = null;
2095
+ const decisionRecordPath = resolve11(assignmentDir, "decision-record.md");
2096
+ if (await fileExists(decisionRecordPath)) {
2097
+ const parsed = parseDecisionRecord(await readFile8(decisionRecordPath, "utf-8"));
2098
+ decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
2099
+ }
2100
+ let progress = null;
2101
+ const progressPath = resolve11(assignmentDir, "progress.md");
2102
+ if (await fileExists(progressPath)) {
2103
+ const parsed = parseProgress(await readFile8(progressPath, "utf-8"));
2104
+ progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2105
+ }
2106
+ let comments = null;
2107
+ const commentsPath = resolve11(assignmentDir, "comments.md");
2108
+ if (await fileExists(commentsPath)) {
2109
+ const parsed = parseComments(await readFile8(commentsPath, "utf-8"));
2110
+ comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2111
+ }
2112
+ const detail = {
2113
+ id: assignment.id,
2114
+ projectSlug: null,
2115
+ slug: assignment.slug || resolved.id,
2116
+ title: assignment.title,
2117
+ status: assignment.status,
2118
+ priority: assignment.priority,
2119
+ assignee: assignment.assignee,
2120
+ dependsOn: [],
2121
+ // standalone cannot declare dependencies
2122
+ links: [],
2123
+ reverseLinks: [],
2124
+ enrichedLinks: [],
2125
+ blockedReason: assignment.blockedReason,
2126
+ workspace: assignment.workspace,
2127
+ projectWorkspace: assignment.workspaceGroup,
2128
+ externalIds: assignment.externalIds,
2129
+ tags: assignment.tags,
2130
+ created: assignment.created,
2131
+ updated: assignment.updated,
2132
+ body: assignment.body,
2133
+ plan,
2134
+ scratchpad,
2135
+ handoff,
2136
+ decisionRecord,
2137
+ progress,
2138
+ comments,
2139
+ referencedBy: [],
2140
+ availableTransitions: await getStandaloneAvailableTransitions(assignment)
2141
+ };
2142
+ return detail;
2143
+ }
2144
+ async function listProjectRecords(projectsDir) {
2145
+ if (!await fileExists(projectsDir)) {
2146
+ return [];
2147
+ }
2148
+ if (!migratedProjectsDirs.has(projectsDir)) {
2149
+ migratedProjectsDirs.add(projectsDir);
2150
+ await migrateLegacyProjectFiles(projectsDir);
2151
+ }
2152
+ const entries = await readdir6(projectsDir, { withFileTypes: true });
2153
+ const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
2154
+ const records = [];
2155
+ for (const entry of projectDirs) {
2156
+ const projectPath = resolve11(projectsDir, entry.name);
2157
+ const projectMdPath = resolve11(projectPath, "project.md");
2158
+ if (!await fileExists(projectMdPath)) {
2159
+ continue;
2160
+ }
2161
+ const projectContent = await readFile8(projectMdPath, "utf-8");
2162
+ const project = parseProject(projectContent);
2163
+ const assignments = await listAssignmentRecords(projectPath);
2164
+ const rollup = await buildProjectRollup(projectPath, project, assignments);
2165
+ const updated = getProjectActivityTimestamp(project.updated, assignments);
2166
+ records.push({
2167
+ projectPath,
2168
+ project,
2169
+ assignments,
2170
+ dependencyGraph: await loadDependencyGraph(projectPath, assignments),
2171
+ summary: {
2172
+ slug: project.slug || entry.name,
2173
+ title: project.title,
2174
+ status: rollup.status,
2175
+ statusOverride: project.statusOverride,
2176
+ archived: project.archived,
2177
+ archivedAt: project.archivedAt,
2178
+ archivedReason: project.archivedReason,
2179
+ created: project.created,
2180
+ updated,
2181
+ tags: project.tags,
2182
+ progress: rollup.progress,
2183
+ needsAttention: rollup.needsAttention,
2184
+ workspace: project.workspace
2185
+ }
2186
+ });
2187
+ }
2188
+ records.sort((left, right) => compareTimestamps(right.summary.updated, left.summary.updated));
2189
+ return records;
2190
+ }
2191
+ async function listAssignmentRecords(projectPath) {
2192
+ const assignmentsDir = resolve11(projectPath, "assignments");
2193
+ if (!await fileExists(assignmentsDir)) {
2194
+ return [];
2195
+ }
2196
+ const entries = await readdir6(assignmentsDir, { withFileTypes: true });
2197
+ const records = [];
2198
+ for (const entry of entries) {
2199
+ if (!entry.isDirectory()) {
2200
+ continue;
2201
+ }
2202
+ const assignmentMd = resolve11(assignmentsDir, entry.name, "assignment.md");
2203
+ if (!await fileExists(assignmentMd)) {
2204
+ continue;
2205
+ }
2206
+ const content = await readFile8(assignmentMd, "utf-8");
2207
+ records.push(parseAssignmentFull(content));
2208
+ }
2209
+ records.sort((left, right) => compareTimestamps(right.updated, left.updated));
2210
+ return records;
2211
+ }
2212
+ async function loadDependencyGraph(projectPath, assignments) {
2213
+ const statusPath = resolve11(projectPath, "_status.md");
2214
+ if (await fileExists(statusPath)) {
2215
+ const statusContent = await readFile8(statusPath, "utf-8");
2216
+ const parsed = parseStatus(statusContent);
2217
+ const derivedGraph = extractMermaidGraph(parsed.body);
2218
+ if (derivedGraph) {
2219
+ return derivedGraph;
2220
+ }
2221
+ }
2222
+ return buildDependencyGraph(assignments);
2223
+ }
2224
+ async function buildProjectRollup(projectPath, project, assignments) {
2225
+ const progress = { total: assignments.length };
2226
+ let openQuestions = 0;
2227
+ for (const assignment of assignments) {
2228
+ const s = assignment.status;
2229
+ progress[s] = (progress[s] ?? 0) + 1;
2230
+ openQuestions += await countOpenQuestions(projectPath, assignment.slug);
2231
+ }
2232
+ const needsAttention = {
2233
+ blockedCount: progress["blocked"] ?? 0,
2234
+ failedCount: progress["failed"] ?? 0,
2235
+ openQuestions
2236
+ };
2237
+ let status = "pending";
2238
+ if (project.statusOverride) {
2239
+ status = project.statusOverride;
2240
+ } else if (project.archived) {
2241
+ status = "archived";
2242
+ } else if (progress.total > 0 && (progress["completed"] ?? 0) === progress.total) {
2243
+ status = "completed";
2244
+ } else if ((progress["in_progress"] ?? 0) > 0 || (progress["review"] ?? 0) > 0) {
2245
+ status = "active";
2246
+ } else if ((progress["failed"] ?? 0) > 0) {
2247
+ status = "failed";
2248
+ } else if ((progress["blocked"] ?? 0) > 0) {
2249
+ status = "blocked";
2250
+ } else if (progress.total === 0 || (progress["pending"] ?? 0) === progress.total) {
2251
+ status = "pending";
2252
+ } else {
2253
+ status = "active";
2254
+ }
2255
+ return { progress, needsAttention, status };
2256
+ }
2257
+ function buildDependencyGraph(assignments) {
2258
+ const edges = [];
2259
+ const usedStatuses = /* @__PURE__ */ new Set();
2260
+ for (const assignment of assignments) {
2261
+ for (const dependency of assignment.dependsOn) {
2262
+ const depStatus = findAssignmentStatus(assignments, dependency);
2263
+ usedStatuses.add(depStatus);
2264
+ usedStatuses.add(assignment.status);
2265
+ edges.push(
2266
+ ` ${dependency}:::${depStatus} --> ${assignment.slug}:::${assignment.status}`
2267
+ );
2268
+ }
2269
+ }
2270
+ if (edges.length === 0) {
2271
+ return null;
2272
+ }
2273
+ const classDefs = [];
2274
+ for (const status of usedStatuses) {
2275
+ const colors = DEFAULT_GRAPH_COLORS[status] ?? "fill:#94a3b8,stroke:#64748b,color:#ffffff";
2276
+ classDefs.push(` classDef ${status} ${colors}`);
2277
+ }
2278
+ return ["graph TD", ...edges, ...classDefs].join("\n");
2279
+ }
2280
+ function findAssignmentStatus(assignments, slug) {
2281
+ return assignments.find((assignment) => assignment.slug === slug)?.status ?? "pending";
2282
+ }
2283
+ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug, assignment) {
2284
+ const config = await getStatusConfig();
2285
+ const transitionDefs = getTransitionDefinitions(config);
2286
+ const actions = [];
2287
+ const projectPath = resolve11(projectsDir, projectSlug);
2288
+ for (const definition of transitionDefs) {
2289
+ let warning = null;
2290
+ if (definition.command === "start" && !assignment.assignee) {
2291
+ warning = "No assignee set \u2014 consider assigning before starting.";
2292
+ }
2293
+ if (definition.command === "start" && assignment.dependsOn.length > 0) {
2294
+ const unmetDependencies = await getUnmetDependencies(projectPath, assignment.dependsOn, config.terminalStatuses);
2295
+ if (unmetDependencies.length > 0) {
2296
+ warning = `Unmet dependencies: ${unmetDependencies.join(", ")}.`;
2297
+ }
2298
+ }
2299
+ const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
2300
+ actions.push({
2301
+ command: definition.command,
2302
+ label: definition.label,
2303
+ description: definition.description,
2304
+ targetStatus: target ?? definition.command,
2305
+ disabled: false,
2306
+ disabledReason: null,
2307
+ warning,
2308
+ requiresReason: definition.requiresReason
2309
+ });
2310
+ }
2311
+ return actions;
2312
+ }
2313
+ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
2314
+ const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
2315
+ const unmet = [];
2316
+ for (const dependency of dependsOn) {
2317
+ const dependencyPath = resolve11(projectPath, "assignments", dependency, "assignment.md");
2318
+ if (!await fileExists(dependencyPath)) {
2319
+ unmet.push(`${dependency} (missing)`);
2320
+ continue;
2321
+ }
2322
+ const content = await readFile8(dependencyPath, "utf-8");
2323
+ const parsed = parseAssignmentFull(content);
2324
+ if (!terminals.has(parsed.status)) {
2325
+ unmet.push(`${dependency} (${parsed.status})`);
2326
+ }
2327
+ }
2328
+ return unmet;
2329
+ }
2330
+ function compareTimestamps(left, right) {
2331
+ return parseTimestamp(left) - parseTimestamp(right);
2332
+ }
2333
+ function parseTimestamp(timestamp) {
2334
+ const parsed = Date.parse(timestamp);
2335
+ return Number.isFinite(parsed) ? parsed : 0;
2336
+ }
2337
+ async function countOpenQuestions(projectPath, assignmentSlug) {
2338
+ const commentsPath = resolve11(
2339
+ projectPath,
2340
+ "assignments",
2341
+ assignmentSlug,
2342
+ "comments.md"
2343
+ );
2344
+ if (!await fileExists(commentsPath)) {
2345
+ return 0;
2346
+ }
2347
+ try {
2348
+ const content = await readFile8(commentsPath, "utf-8");
2349
+ const parsed = parseComments(content);
2350
+ return parsed.entries.filter(
2351
+ (e) => e.type === "question" && e.resolved !== true
2352
+ ).length;
2353
+ } catch {
2354
+ return 0;
2355
+ }
2356
+ }
2357
+ function getProjectActivityTimestamp(projectUpdated, assignments) {
2358
+ let latest = projectUpdated;
2359
+ for (const assignment of assignments) {
2360
+ if (compareTimestamps(assignment.updated, latest) > 0) {
2361
+ latest = assignment.updated;
2362
+ }
2363
+ }
2364
+ return latest;
2365
+ }
2366
+ var STALE_ASSIGNMENT_MS, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
2367
+ var init_api = __esm({
2368
+ "src/dashboard/api.ts"() {
2369
+ "use strict";
2370
+ init_lifecycle();
2371
+ init_fs();
2372
+ init_timestamp();
2373
+ init_config2();
2374
+ init_playbooks();
2375
+ init_fs_migration();
2376
+ init_assignment_resolver();
2377
+ init_parser();
2378
+ init_help();
2379
+ init_agent_sessions();
2380
+ init_overviewCopy();
2381
+ STALE_ASSIGNMENT_MS = 7 * 24 * 60 * 60 * 1e3;
2382
+ DEFAULT_TRANSITION_DEFINITIONS = [
2383
+ {
2384
+ command: "start",
2385
+ label: "Start",
2386
+ description: "Move pending or review work into active execution.",
2387
+ requiresReason: false
2388
+ },
2389
+ {
2390
+ command: "shape",
2391
+ label: "Shape",
2392
+ description: "Promote a draft assignment to ready_for_planning once the Objective and Acceptance Criteria are fleshed out.",
2393
+ requiresReason: false
2394
+ },
2395
+ {
2396
+ command: "plan-ready",
2397
+ label: "Plan Ready",
2398
+ description: "Promote a ready_for_planning assignment to ready_to_implement after the plan is written and approved.",
2399
+ requiresReason: false
2400
+ },
2401
+ {
2402
+ command: "implement",
2403
+ label: "Implement",
2404
+ description: "Move a ready_to_implement assignment into in_progress when coding begins.",
2405
+ requiresReason: false
2406
+ },
2407
+ {
2408
+ command: "review",
2409
+ label: "Send To Review",
2410
+ description: "Mark the assignment ready for inspection.",
2411
+ requiresReason: false
2412
+ },
2413
+ {
2414
+ command: "complete",
2415
+ label: "Complete",
2416
+ description: "Mark the assignment done.",
2417
+ requiresReason: false
2418
+ },
2419
+ {
2420
+ command: "block",
2421
+ label: "Block",
2422
+ description: "Record an exceptional blocker and pause work.",
2423
+ requiresReason: true
2424
+ },
2425
+ {
2426
+ command: "unblock",
2427
+ label: "Unblock",
2428
+ description: "Resume active work after the blocker is cleared.",
2429
+ requiresReason: false
2430
+ },
2431
+ {
2432
+ command: "fail",
2433
+ label: "Fail",
2434
+ description: "Mark the assignment as failed when it cannot be completed as planned.",
2435
+ requiresReason: false
2436
+ },
2437
+ {
2438
+ command: "reopen",
2439
+ label: "Reopen",
2440
+ description: "Reopen a completed or failed assignment to resume work.",
2441
+ requiresReason: false
2442
+ }
2443
+ ];
2444
+ DEFAULT_STATUS_COLORS = {
2445
+ pending: "slate",
2446
+ in_progress: "teal",
2447
+ blocked: "amber",
2448
+ review: "violet",
2449
+ completed: "emerald",
2450
+ failed: "rose"
2451
+ };
2452
+ _cachedConfig = null;
2453
+ REFERENCED_BY_LIMIT = 50;
2454
+ migratedProjectsDirs = /* @__PURE__ */ new Set();
2455
+ DEFAULT_GRAPH_COLORS = {
2456
+ completed: "fill:#4ea84f,stroke:#1f6b29,color:#ffffff",
2457
+ in_progress: "fill:#1e6fd9,stroke:#0f3f8f,color:#ffffff",
2458
+ pending: "fill:#c0ccd9,stroke:#738399,color:#163047",
2459
+ blocked: "fill:#db5a3f,stroke:#8d2815,color:#ffffff",
2460
+ failed: "fill:#9f2d2d,stroke:#651616,color:#ffffff",
2461
+ review: "fill:#c6911e,stroke:#7a5a10,color:#ffffff"
2462
+ };
2463
+ }
2464
+ });
2465
+
2466
+ // src/launch/url.ts
2467
+ var OpenUrlError = class extends Error {
2468
+ code;
2469
+ constructor(code, message) {
2470
+ super(message);
2471
+ this.code = code;
2472
+ this.name = "OpenUrlError";
2473
+ }
2474
+ };
2475
+ var SESSION_MODES = ["resume", "fork"];
2476
+ function parseOpenUrl(input) {
2477
+ let url;
2478
+ try {
2479
+ url = new URL(input);
2480
+ } catch {
2481
+ throw new OpenUrlError(
2482
+ "malformed",
2483
+ `Could not parse URL: ${JSON.stringify(input)}`
2484
+ );
2485
+ }
2486
+ if (url.protocol !== "syntaur:") {
2487
+ throw new OpenUrlError(
2488
+ "bad-scheme",
2489
+ `Expected scheme 'syntaur:' but got '${url.protocol}'`
2490
+ );
2491
+ }
2492
+ if (url.hostname !== "open") {
2493
+ throw new OpenUrlError(
2494
+ "bad-host",
2495
+ `Expected host 'open' but got '${url.hostname}'`
2496
+ );
2497
+ }
2498
+ const assignmentVals = url.searchParams.getAll("assignment");
2499
+ const sessionVals = url.searchParams.getAll("session");
2500
+ if (assignmentVals.length > 1) {
2501
+ throw new OpenUrlError(
2502
+ "duplicate-param",
2503
+ "URL has more than one `assignment` query param"
2504
+ );
2505
+ }
2506
+ if (sessionVals.length > 1) {
2507
+ throw new OpenUrlError(
2508
+ "duplicate-param",
2509
+ "URL has more than one `session` query param"
2510
+ );
2511
+ }
2512
+ if (assignmentVals.length === 1 && sessionVals.length === 1) {
2513
+ throw new OpenUrlError(
2514
+ "both-ids",
2515
+ "URL has both `assignment` and `session` query params \u2014 only one is allowed"
2516
+ );
2517
+ }
2518
+ if (assignmentVals.length === 1) {
2519
+ const id = assignmentVals[0];
2520
+ if (id.trim() === "") {
2521
+ throw new OpenUrlError(
2522
+ "missing-id",
2523
+ "`assignment` query param is empty"
2524
+ );
2525
+ }
2526
+ return { kind: "assignment", id };
2527
+ }
2528
+ if (sessionVals.length === 1) {
2529
+ const id = sessionVals[0];
2530
+ if (id.trim() === "") {
2531
+ throw new OpenUrlError("missing-id", "`session` query param is empty");
2532
+ }
2533
+ const modeVals = url.searchParams.getAll("mode");
2534
+ if (modeVals.length > 1) {
2535
+ throw new OpenUrlError(
2536
+ "duplicate-param",
2537
+ "URL has more than one `mode` query param"
2538
+ );
2539
+ }
2540
+ let mode = "resume";
2541
+ if (modeVals.length === 1) {
2542
+ const raw = modeVals[0];
2543
+ if (!SESSION_MODES.includes(raw)) {
2544
+ throw new OpenUrlError(
2545
+ "bad-mode",
2546
+ `\`mode\` must be one of ${SESSION_MODES.join("|")} (got "${raw}")`
2547
+ );
2548
+ }
2549
+ mode = raw;
2550
+ }
2551
+ return { kind: "session", id, mode };
2552
+ }
2553
+ throw new OpenUrlError(
2554
+ "missing-id",
2555
+ "URL must include either `assignment=<id>` or `session=<id>`"
2556
+ );
2557
+ }
2558
+
2559
+ // src/launch/plan.ts
2560
+ init_config2();
2561
+ init_assignment_resolver();
2562
+ init_api();
2563
+ import { isAbsolute as isAbsolute4 } from "path";
2564
+
2565
+ // src/tui/launch.ts
2566
+ init_api();
2567
+ import { spawn } from "child_process";
2568
+ import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
2569
+ import { isAbsolute as isAbsolute2, resolve as resolve12 } from "path";
2570
+ var INITIAL_PROMPT = (params) => {
2571
+ if (params.projectSlug) {
2572
+ return `/grab-assignment ${params.projectSlug} ${params.assignmentSlug}`;
2573
+ }
2574
+ if (params.id) {
2575
+ return `/grab-assignment --id ${params.id}`;
2576
+ }
2577
+ return `/grab-assignment ${params.assignmentSlug}`;
2578
+ };
2579
+ function formatFallbackCwdWarning(opts) {
2580
+ const missing = [];
2581
+ if (!opts.worktreePath) missing.push("worktreePath");
2582
+ if (!opts.branch) missing.push("branch");
2583
+ if (missing.length === 0) return null;
2584
+ const fields = missing.map((m) => `workspace.${m}`).join(" and ");
2585
+ return `syntaur: ${fields} not set for ${opts.assignmentSlug} \u2014 launching in ${opts.workspaceDir}`;
2586
+ }
2587
+ function shellQuote(arg) {
2588
+ if (arg === "") return "''";
2589
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
2590
+ }
2591
+ function buildAgentArgv(agent, prompt, env = process.env) {
2592
+ const position = agent.promptArgPosition ?? "first";
2593
+ const baseArgs = [...agent.args ?? []];
2594
+ const agentArgs = position === "first" ? [prompt, ...baseArgs] : position === "last" ? [...baseArgs, prompt] : baseArgs;
2595
+ if (agent.resolveFromShellAliases) {
2596
+ const requested = env.SHELL;
2597
+ let shell = requested;
2598
+ let warning = null;
2599
+ if (!shell || !isAbsolute2(shell)) {
2600
+ warning = `syntaur: $SHELL ${requested ? `("${requested}") is not absolute` : "is unset"} \u2014 falling back to /bin/sh for shell-alias resolution`;
2601
+ shell = "/bin/sh";
2602
+ }
2603
+ const quoted = [agent.command, ...agentArgs].map(shellQuote).join(" ");
2604
+ return {
2605
+ argv: { command: shell, args: ["-i", "-c", quoted] },
2606
+ shellFallbackWarning: warning
2607
+ };
2608
+ }
2609
+ return {
2610
+ argv: { command: agent.command, args: agentArgs },
2611
+ shellFallbackWarning: null
2612
+ };
2613
+ }
2614
+
2615
+ // src/launch/plan.ts
2616
+ init_agent_sessions();
2617
+
2618
+ // src/launch/argv.ts
2619
+ import { isAbsolute as isAbsolute3 } from "path";
2620
+ var buildFreshArgv = buildAgentArgv;
2621
+ function buildSessionArgv(agent, sessionId, mode, env = process.env) {
2622
+ const invocation = agent[mode];
2623
+ if (!invocation) {
2624
+ throw new LaunchError(
2625
+ "mode-not-supported",
2626
+ `Agent "${agent.id}" does not support ${mode} (no agent.${mode} configured)`
2627
+ );
2628
+ }
2629
+ const substituted = invocation.args.map(
2630
+ (a) => a === "{id}" ? sessionId : a
2631
+ );
2632
+ const command = invocation.command ?? agent.command;
2633
+ const agentArgs = [...agent.args ?? [], ...substituted];
2634
+ if (agent.resolveFromShellAliases) {
2635
+ const requested = env.SHELL;
2636
+ let shell = requested;
2637
+ let warning = null;
2638
+ if (!shell || !isAbsolute3(shell)) {
2639
+ warning = `syntaur: $SHELL ${requested ? `("${requested}") is not absolute` : "is unset"} \u2014 falling back to /bin/sh for shell-alias resolution`;
2640
+ shell = "/bin/sh";
2641
+ }
2642
+ const quoted = [command, ...agentArgs].map(shellQuote).join(" ");
2643
+ return {
2644
+ argv: { command: shell, args: ["-i", "-c", quoted] },
2645
+ shellFallbackWarning: warning
2646
+ };
2647
+ }
2648
+ return {
2649
+ argv: { command, args: agentArgs },
2650
+ shellFallbackWarning: null
2651
+ };
2652
+ }
2653
+
2654
+ // src/launch/plan.ts
2655
+ var LaunchError = class extends Error {
2656
+ code;
2657
+ constructor(code, message) {
2658
+ super(message);
2659
+ this.code = code;
2660
+ this.name = "LaunchError";
2661
+ }
2662
+ };
2663
+ function pickAgent(config) {
2664
+ const agents = getAgents(config);
2665
+ if (agents.length === 0) {
2666
+ throw new LaunchError(
2667
+ "no-agents-configured",
2668
+ "No agents in ~/.syntaur/config.md. Run `syntaur agents add` to configure one."
2669
+ );
2670
+ }
2671
+ return agents.find((a) => a.default) ?? agents[0];
2672
+ }
2673
+ async function resolveLaunchPlan(input) {
2674
+ const terminal = getTerminal(input.config);
2675
+ if (input.kind === "assignment") {
2676
+ return resolveAssignmentPlan(input, terminal);
2677
+ }
2678
+ return resolveSessionPlan(input, terminal);
2679
+ }
2680
+ async function resolveAssignmentPlan(input, terminal) {
2681
+ const resolved = await resolveAssignmentById(
2682
+ input.projectsDir,
2683
+ input.assignmentsDir,
2684
+ input.id
2685
+ );
2686
+ if (!resolved) {
2687
+ throw new LaunchError(
2688
+ "assignment-not-found",
2689
+ `Assignment with id ${JSON.stringify(input.id)} not found`
2690
+ );
2691
+ }
2692
+ const detail = await getAssignmentDetailById(
2693
+ input.projectsDir,
2694
+ input.assignmentsDir,
2695
+ input.id
2696
+ );
2697
+ if (!detail) {
2698
+ throw new LaunchError(
2699
+ "assignment-not-found",
2700
+ `Assignment ${input.id} resolver returned a directory but detail could not be loaded`
2701
+ );
2702
+ }
2703
+ const { cwd, fallbackWarning } = pickCwd({
2704
+ worktreePath: detail.workspace.worktreePath,
2705
+ repository: detail.workspace.repository,
2706
+ branch: detail.workspace.branch,
2707
+ assignmentSlug: resolved.assignmentSlug,
2708
+ fallbackPath: process.cwd()
2709
+ });
2710
+ const agent = pickAgent(input.config);
2711
+ const { argv, shellFallbackWarning } = buildFreshArgv(
2712
+ agent,
2713
+ INITIAL_PROMPT({
2714
+ projectSlug: resolved.projectSlug,
2715
+ assignmentSlug: resolved.assignmentSlug,
2716
+ id: resolved.id
2717
+ })
2718
+ );
2719
+ return {
2720
+ terminal,
2721
+ cwd,
2722
+ argv,
2723
+ env: process.env,
2724
+ agentId: agent.id,
2725
+ fallbackWarning,
2726
+ shellFallbackWarning
2727
+ };
2728
+ }
2729
+ async function resolveSessionPlan(input, terminal) {
2730
+ const session = getSessionById(input.id);
2731
+ if (!session) {
2732
+ throw new LaunchError(
2733
+ "session-not-found",
2734
+ `Session with id ${JSON.stringify(input.id)} not found`
2735
+ );
2736
+ }
2737
+ let cwd = session.path;
2738
+ let fallbackWarning = null;
2739
+ if (session.projectSlug && session.assignmentSlug) {
2740
+ const detail = await getAssignmentDetail(
2741
+ input.projectsDir,
2742
+ session.projectSlug,
2743
+ session.assignmentSlug
2744
+ );
2745
+ if (detail) {
2746
+ const picked = pickCwd({
2747
+ worktreePath: detail.workspace.worktreePath,
2748
+ repository: detail.workspace.repository,
2749
+ branch: detail.workspace.branch,
2750
+ assignmentSlug: session.assignmentSlug,
2751
+ fallbackPath: session.path
2752
+ });
2753
+ cwd = picked.cwd;
2754
+ fallbackWarning = picked.fallbackWarning;
2755
+ }
2756
+ }
2757
+ const agent = getAgents(input.config).find((a) => a.id === session.agent);
2758
+ if (!agent) {
2759
+ throw new LaunchError(
2760
+ "agent-not-configured",
2761
+ `Session ${input.id} was started with agent "${session.agent}" which is not in your agents list. Run \`syntaur agents add ${session.agent}\` or pick a different session.`
2762
+ );
2763
+ }
2764
+ const { argv, shellFallbackWarning } = buildSessionArgv(
2765
+ agent,
2766
+ session.sessionId,
2767
+ input.mode ?? "resume"
2768
+ );
2769
+ return {
2770
+ terminal,
2771
+ cwd,
2772
+ argv,
2773
+ env: process.env,
2774
+ agentId: agent.id,
2775
+ fallbackWarning,
2776
+ shellFallbackWarning
2777
+ };
2778
+ }
2779
+ function pickCwd(input) {
2780
+ if (input.worktreePath && isAbsolute4(input.worktreePath)) {
2781
+ return { cwd: input.worktreePath, fallbackWarning: null };
2782
+ }
2783
+ const workspaceDir = input.repository && isAbsolute4(input.repository) ? input.repository : input.fallbackPath;
2784
+ const fallbackWarning = formatFallbackCwdWarning({
2785
+ assignmentSlug: input.assignmentSlug,
2786
+ workspaceDir,
2787
+ worktreePath: input.worktreePath,
2788
+ branch: input.branch
2789
+ });
2790
+ return { cwd: workspaceDir, fallbackWarning };
2791
+ }
2792
+
2793
+ // src/launch/execute.ts
2794
+ import { spawn as spawn2 } from "child_process";
2795
+ var TerminalNotFoundError = class extends Error {
2796
+ terminal;
2797
+ remediation;
2798
+ constructor(terminal, remediation) {
2799
+ super(
2800
+ `Terminal "${terminal}" is not installed or not invokable. ${remediation}`
2801
+ );
2802
+ this.terminal = terminal;
2803
+ this.remediation = remediation;
2804
+ this.name = "TerminalNotFoundError";
2805
+ }
2806
+ };
2807
+ var realSpawn = (command, args, options) => spawn2(command, args, options);
2808
+ var WRAPPER_COMMANDS = /* @__PURE__ */ new Set(["osascript", "open"]);
2809
+ var WRAPPER_EXIT_TIMEOUT_MS = 1500;
2810
+ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
2811
+ if (plan.terminal === "warp") {
2812
+ console.error(
2813
+ `syntaur: Warp will open a window at ${plan.cwd} but cannot auto-start ${plan.argv.command} \u2014 run it yourself once the window appears`
2814
+ );
2815
+ }
2816
+ const invocation = buildTerminalInvocation(plan);
2817
+ const isWrapper = WRAPPER_COMMANDS.has(invocation.command);
2818
+ let child;
2819
+ try {
2820
+ child = spawnFn(invocation.command, invocation.args, {
2821
+ detached: true,
2822
+ // Wrappers: capture stderr so we can surface error text. Direct CLI
2823
+ // launchers: ignore all streams so they keep running after we detach.
2824
+ stdio: isWrapper ? ["ignore", "ignore", "pipe"] : "ignore",
2825
+ env: plan.env
2826
+ });
2827
+ } catch (err) {
2828
+ const msg = err instanceof Error ? err.message : String(err);
2829
+ throw new TerminalNotFoundError(
2830
+ plan.terminal,
2831
+ `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
2832
+ );
2833
+ }
2834
+ await new Promise((resolve14, reject) => {
2835
+ let settled = false;
2836
+ let stderr = "";
2837
+ const finishOk = () => {
2838
+ if (settled) return;
2839
+ settled = true;
2840
+ try {
2841
+ child.unref();
2842
+ } catch {
2843
+ }
2844
+ resolve14();
2845
+ };
2846
+ const finishErr = (remediation) => {
2847
+ if (settled) return;
2848
+ settled = true;
2849
+ reject(new TerminalNotFoundError(plan.terminal, remediation));
2850
+ };
2851
+ if (child.stderr) {
2852
+ child.stderr.on("data", (chunk) => {
2853
+ stderr += chunk.toString();
2854
+ });
2855
+ }
2856
+ child.once("error", (err) => {
2857
+ finishErr(
2858
+ `Spawn failed: ${err.message}. Verify the terminal is installed and on PATH.`
2859
+ );
2860
+ });
2861
+ if (isWrapper) {
2862
+ child.once("exit", (code, signal) => {
2863
+ if (code === 0 || code === null) {
2864
+ finishOk();
2865
+ } else {
2866
+ const detail = stderr.trim() || (signal ? `terminated by signal ${signal}` : "check that the terminal app is installed and the URL scheme handler is registered");
2867
+ finishErr(`${invocation.command} exited with code ${code}: ${detail}`);
2868
+ }
2869
+ });
2870
+ setTimeout(finishOk, WRAPPER_EXIT_TIMEOUT_MS).unref();
2871
+ } else {
2872
+ child.once("spawn", () => {
2873
+ finishOk();
2874
+ });
2875
+ }
2876
+ });
2877
+ }
2878
+ function buildTerminalInvocation(plan) {
2879
+ const commandLine = [plan.argv.command, ...plan.argv.args].map(shellQuote).join(" ");
2880
+ const cdAndRun = `cd ${shellQuote(plan.cwd)} && ${commandLine}`;
2881
+ switch (plan.terminal) {
2882
+ case "terminal-app":
2883
+ return {
2884
+ command: "osascript",
2885
+ args: [
2886
+ "-e",
2887
+ 'tell application "Terminal"',
2888
+ "-e",
2889
+ "activate",
2890
+ "-e",
2891
+ `do script ${appleScriptString(cdAndRun)}`,
2892
+ "-e",
2893
+ "end tell"
2894
+ ]
2895
+ };
2896
+ case "iterm":
2897
+ return {
2898
+ command: "osascript",
2899
+ args: [
2900
+ "-e",
2901
+ 'tell application "iTerm"',
2902
+ "-e",
2903
+ "activate",
2904
+ "-e",
2905
+ "set newWindow to (create window with default profile)",
2906
+ "-e",
2907
+ `tell current session of newWindow to write text ${appleScriptString(cdAndRun)}`,
2908
+ "-e",
2909
+ "end tell"
2910
+ ]
2911
+ };
2912
+ case "ghostty":
2913
+ return {
2914
+ command: "osascript",
2915
+ args: [
2916
+ "-e",
2917
+ 'tell application "Ghostty"',
2918
+ "-e",
2919
+ "activate",
2920
+ "-e",
2921
+ "set newWin to (new window)",
2922
+ "-e",
2923
+ "delay 0.2",
2924
+ "-e",
2925
+ "set t to terminal 1 of selected tab of newWin",
2926
+ "-e",
2927
+ `input text ${appleScriptString(cdAndRun)} to t`,
2928
+ "-e",
2929
+ 'send key "enter" to t',
2930
+ "-e",
2931
+ "end tell"
2932
+ ]
2933
+ };
2934
+ case "alacritty":
2935
+ return {
2936
+ command: "alacritty",
2937
+ args: [
2938
+ "--working-directory",
2939
+ plan.cwd,
2940
+ "-e",
2941
+ plan.argv.command,
2942
+ ...plan.argv.args
2943
+ ]
2944
+ };
2945
+ case "warp": {
2946
+ const params = new URLSearchParams({ path: plan.cwd });
2947
+ return {
2948
+ command: "open",
2949
+ args: [`warp://action/new_window?${params.toString()}`]
2950
+ };
2951
+ }
2952
+ case "kitty":
2953
+ return {
2954
+ command: "kitty",
2955
+ args: [
2956
+ "--directory",
2957
+ plan.cwd,
2958
+ "--",
2959
+ plan.argv.command,
2960
+ ...plan.argv.args
2961
+ ]
2962
+ };
2963
+ }
2964
+ }
2965
+ function appleScriptString(value) {
2966
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
2967
+ }
2968
+
2969
+ // src/launch/install-detection.ts
2970
+ init_paths();
2971
+ init_fs();
2972
+ import { fileURLToPath } from "url";
2973
+ import { dirname as dirname3, resolve as resolve13, join as join2 } from "path";
2974
+ import { realpathSync, readFileSync, mkdirSync } from "fs";
2975
+ var NPX_PATTERNS = [
2976
+ { kind: "npm", re: /\/_npx\/([^/]+)\/node_modules(?:\/|$)/ },
2977
+ { kind: "pnpm", re: /\/pnpm\/dlx\/([^/]+)\/node_modules(?:\/|$)/ },
2978
+ { kind: "bun", re: /\/bunx-([^/]+)\/node_modules(?:\/|$)/ }
2979
+ ];
2980
+ var GLOBAL_PATTERN = /\/lib\/node_modules\/syntaur(?:\/|$)/;
2981
+ function resolveScriptPath(scriptUrl, realpath) {
2982
+ let p;
2983
+ try {
2984
+ p = fileURLToPath(scriptUrl);
2985
+ } catch {
2986
+ return null;
2987
+ }
2988
+ try {
2989
+ return realpath(p);
2990
+ } catch {
2991
+ return p;
2992
+ }
2993
+ }
2994
+ function normalizeSlashes(p) {
2995
+ return p.replace(/\\/g, "/");
2996
+ }
2997
+ function detectInstallKind(scriptUrl, opts = {}) {
2998
+ const realpath = opts.realpath ?? realpathSync.native;
2999
+ const readFile9 = opts.readFile ?? ((p) => readFileSync(p, "utf-8"));
3000
+ const ua = opts.envUserAgent !== void 0 ? opts.envUserAgent : process.env.npm_config_user_agent ?? "";
3001
+ const resolved = resolveScriptPath(scriptUrl, realpath);
3002
+ if (resolved === null) {
3003
+ return "unknown";
3004
+ }
3005
+ const norm = normalizeSlashes(resolved);
3006
+ for (const pat of NPX_PATTERNS) {
3007
+ if (pat.re.test(norm)) return "npx";
3008
+ }
3009
+ if (ua.includes("npx/")) {
3010
+ return "npx";
3011
+ }
3012
+ if (GLOBAL_PATTERN.test(norm)) {
3013
+ return "global";
3014
+ }
3015
+ let dir = dirname3(resolved);
3016
+ for (let depth = 0; depth < 8; depth++) {
3017
+ const pkgJsonPath = join2(dir, "package.json");
3018
+ let raw;
3019
+ try {
3020
+ raw = readFile9(pkgJsonPath);
3021
+ } catch {
3022
+ const parent2 = dirname3(dir);
3023
+ if (parent2 === dir) break;
3024
+ dir = parent2;
3025
+ continue;
3026
+ }
3027
+ try {
3028
+ const pkg = JSON.parse(raw);
3029
+ if (typeof pkg.name === "string" && pkg.name === "syntaur" && !normalizeSlashes(dir).includes("/node_modules/")) {
3030
+ return "local";
3031
+ }
3032
+ } catch {
3033
+ }
3034
+ const parent = dirname3(dir);
3035
+ if (parent === dir) break;
3036
+ dir = parent;
3037
+ }
3038
+ return "unknown";
3039
+ }
3040
+ function extractNpxHash(scriptUrl, opts = {}) {
3041
+ const realpath = opts.realpath ?? realpathSync.native;
3042
+ const resolved = resolveScriptPath(scriptUrl, realpath);
3043
+ if (resolved === null) return null;
3044
+ const norm = normalizeSlashes(resolved);
3045
+ for (const pat of NPX_PATTERNS) {
3046
+ const m = norm.match(pat.re);
3047
+ if (m) return m[1] ?? null;
3048
+ }
3049
+ return null;
3050
+ }
3051
+ function nudgeStampDir() {
3052
+ return resolve13(syntaurRoot(), "npx-handler-nudge");
3053
+ }
3054
+ function sanitizeHash(hash) {
3055
+ return hash.replace(/[^A-Za-z0-9_-]/g, "_") || "_";
3056
+ }
3057
+ function nudgeStampPath(hash) {
3058
+ return join2(nudgeStampDir(), sanitizeHash(hash));
3059
+ }
3060
+ async function hasNudgedHash(hash) {
3061
+ return fileExists(nudgeStampPath(hash));
3062
+ }
3063
+ async function recordNudge(hash) {
3064
+ try {
3065
+ mkdirSync(nudgeStampDir(), { recursive: true });
3066
+ } catch {
3067
+ }
3068
+ try {
3069
+ await writeFileForce(nudgeStampPath(hash), "");
3070
+ } catch {
3071
+ }
3072
+ }
3073
+ function isHandlerNudgeDisabled() {
3074
+ const raw = process.env.SYNTAUR_SKIP_HANDLER_NUDGE;
3075
+ if (raw === void 0) return false;
3076
+ const trimmed = raw.trim();
3077
+ return /^(1|true|yes)$/i.test(trimmed);
3078
+ }
3079
+ function nudgeMessage() {
3080
+ return 'syntaur: running from npx \u2014 the syntaur:// deep-link handler is not registered. Install durably with `npm i -g syntaur` to enable "Open in agent" buttons.';
3081
+ }
3082
+ async function shouldNudgeForNpx(hash) {
3083
+ if (isHandlerNudgeDisabled()) return false;
3084
+ if (hash === null) return false;
3085
+ if (await hasNudgedHash(hash)) return false;
3086
+ return true;
3087
+ }
3088
+ var META_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version", "help"]);
3089
+ async function maybeNudgeForNpxInstall(scriptUrl) {
3090
+ if (detectInstallKind(scriptUrl) !== "npx") return;
3091
+ const args = process.argv.slice(2);
3092
+ if (args.some((a) => META_ARGS.has(a))) return;
3093
+ const hash = extractNpxHash(scriptUrl);
3094
+ if (!await shouldNudgeForNpx(hash)) return;
3095
+ console.error(nudgeMessage());
3096
+ if (hash !== null) {
3097
+ await recordNudge(hash);
3098
+ }
3099
+ }
3100
+ export {
3101
+ LaunchError,
3102
+ OpenUrlError,
3103
+ TerminalNotFoundError,
3104
+ buildFreshArgv,
3105
+ buildSessionArgv,
3106
+ buildTerminalInvocation,
3107
+ detectInstallKind,
3108
+ executeLaunchPlan,
3109
+ extractNpxHash,
3110
+ hasNudgedHash,
3111
+ isHandlerNudgeDisabled,
3112
+ maybeNudgeForNpxInstall,
3113
+ nudgeMessage,
3114
+ nudgeStampDir,
3115
+ nudgeStampPath,
3116
+ parseOpenUrl,
3117
+ pickAgent,
3118
+ recordNudge,
3119
+ resolveLaunchPlan,
3120
+ shouldNudgeForNpx
3121
+ };
3122
+ //# sourceMappingURL=index.js.map