syntaur 0.27.0 → 0.32.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 (79) hide show
  1. package/dashboard/dist/assets/{_basePickBy-DPBuiT9A.js → _basePickBy-CV3s3ZBR.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-B5Q4dkW3.js → _baseUniq-BTBb-kpx.js} +1 -1
  3. package/dashboard/dist/assets/{arc-Bp71QC_v.js → arc-DroCaru_.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CWHBISZ5.js → architectureDiagram-2XIMDMQ5-hL5g0oNx.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-D0txIHgi.js → blockDiagram-WCTKOSBZ-H-mOZOGQ.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-D_Hpnc38.js → c4Diagram-IC4MRINW-C7JTywql.js} +1 -1
  7. package/dashboard/dist/assets/channel-X6-lrXLw.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-D0A_A8qn.js → chunk-4BX2VUAB-C5NgL7Ud.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-DuK8QvrD.js → chunk-55IACEB6-CpqlZIZp.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-B5WfIDS6.js → chunk-FMBD7UC4-DOEJuCgN.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-D3jB_ZJP.js → chunk-JSJVCQXG-DTvwZQeC.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-DtxN1mOD.js → chunk-KX2RTZJC-DtM5R3Ro.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-4fQpgivN.js → chunk-NQ4KR5QH-BRf7XeyG.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-BOf9TZCT.js → chunk-QZHKN3VN-CkRK4_hm.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-D9HeEPWL.js → chunk-WL4C6EOR-D40xff82.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-DfkR91Os.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-DfkR91Os.js +1 -0
  18. package/dashboard/dist/assets/clone--OUSRwbL.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CpzWcyB7.js → cose-bilkent-S5V4N54A-BzAx8dWI.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-CC9-omFF.js → dagre-KLK3FWXG-DqBzOGFn.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-q_F9KKPz.js → diagram-E7M64L7V-BU3Nv4BP.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-CbYvNpQB.js → diagram-IFDJBPK2-9173qxjV.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-q8XUUKRC.js → diagram-P4PSJMXO-CDO7XNao.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-Q-oL35fO.js → erDiagram-INFDFZHY-DkO9AjCM.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-Cptj-2yF.js → flowDiagram-PKNHOUZH-9Fjhsq_p.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-BYmgXBad.js → ganttDiagram-A5KZAMGK-FL3oGbo9.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-DHF3w-Cn.js → gitGraphDiagram-K3NZZRJ6-FS9HpxFJ.js} +1 -1
  28. package/dashboard/dist/assets/{graph-Br4uG9xg.js → graph-COu71lol.js} +1 -1
  29. package/dashboard/dist/assets/index-BohN_jjP.css +1 -0
  30. package/dashboard/dist/assets/{index-dyJ_mu3x.js → index-D1FxzsMS.js} +84 -83
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Ckb3YLUI.js → infoDiagram-LFFYTUFH-Cb7nqRMJ.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DSXXm4hL.js → ishikawaDiagram-PHBUUO56-DU44jQ_t.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-D4JJ4wn_.js → journeyDiagram-4ABVD52K-Dvf5wmhX.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-DZeWPcIi.js → kanban-definition-K7BYSVSG-NNnzIiBX.js} +1 -1
  35. package/dashboard/dist/assets/{layout-DU5mcBKh.js → layout-BWL1q6XW.js} +1 -1
  36. package/dashboard/dist/assets/{linear-h7AvdT63.js → linear-BpjXUE-L.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-DIOnVuDB.js → mermaid.core-C1YuKa7V.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-BVSORv6W.js → mindmap-definition-YRQLILUH-BUS-7SSM.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-BEdO084J.js → pieDiagram-SKSYHLDU-CId0D08y.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-3Dc5mQ7q.js → quadrantDiagram-337W2JSQ-lrdlvDFz.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-eu-8doSY.js → requirementDiagram-Z7DCOOCP-CijOr4BN.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-jA292hzv.js → sankeyDiagram-WA2Y5GQK-Bz63rCum.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-et31a6Tg.js → sequenceDiagram-2WXFIKYE-0ojwsRXQ.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D6MtTWaR.js → stateDiagram-RAJIS63D-DvTark-k.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-CiT1CTy0.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-Oa_SYaCP.js → timeline-definition-YZTLITO2-B4EQprf1.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-vrIbKmuv.js → treemap-KZPCXAKY-ht_xOxVL.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-B3UlkEHW.js → vennDiagram-LZ73GAT5-C4DqZkvq.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BLiVVy6A.js → xychartDiagram-JWTSCODW-eoymvv9D.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +449 -124
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +5766 -3927
  54. package/dist/index.js.map +1 -1
  55. package/dist/launch/index.js +1191 -1181
  56. package/dist/launch/index.js.map +1 -1
  57. package/package.json +2 -1
  58. package/platforms/README.md +21 -0
  59. package/platforms/claude-code/skills/clear-assignment/SKILL.md +2 -2
  60. package/platforms/claude-code/skills/log-progress/SKILL.md +29 -48
  61. package/platforms/claude-code/skills/plan-assignment/SKILL.md +20 -1
  62. package/platforms/claude-code/skills/save-session-summary/SKILL.md +28 -29
  63. package/platforms/claude-code/skills/set-workspace/SKILL.md +25 -41
  64. package/platforms/codex/skills/clear-assignment/SKILL.md +2 -2
  65. package/platforms/codex/skills/log-progress/SKILL.md +29 -48
  66. package/platforms/codex/skills/plan-assignment/SKILL.md +20 -1
  67. package/platforms/codex/skills/save-session-summary/SKILL.md +28 -29
  68. package/platforms/codex/skills/set-workspace/SKILL.md +25 -41
  69. package/skills/clear-assignment/SKILL.md +2 -2
  70. package/skills/log-progress/SKILL.md +29 -48
  71. package/skills/plan-assignment/SKILL.md +20 -1
  72. package/skills/save-session-summary/SKILL.md +28 -29
  73. package/skills/set-workspace/SKILL.md +25 -41
  74. package/dashboard/dist/assets/channel-D41AslDq.js +0 -1
  75. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BnKy62Yt.js +0 -1
  76. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BnKy62Yt.js +0 -1
  77. package/dashboard/dist/assets/clone-Cz7h9axV.js +0 -1
  78. package/dashboard/dist/assets/index-Ds1-e_jv.css +0 -1
  79. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-sYL-A3ib.js +0 -1
@@ -256,1147 +256,1301 @@ var init_fs_migration = __esm({
256
256
  }
257
257
  });
258
258
 
259
- // src/utils/hotkeysCatalog.ts
260
- function isBindableActionKind(value) {
261
- return typeof value === "string" && BINDABLE_ACTION_KINDS.includes(value);
262
- }
263
- function canonicalizeCombo(input) {
264
- if (typeof input !== "string") return "";
265
- const trimmed = input.trim();
266
- if (!trimmed) return "";
267
- if (/\s/.test(trimmed) && !trimmed.includes("+")) {
268
- return trimmed.split(/\s+/).map(canonicalizeCombo).filter((part) => part.length > 0).join(" ");
269
- }
270
- const parts = trimmed.split("+").map((p) => p.trim()).filter((p) => p.length > 0);
271
- if (parts.length === 0) return "";
272
- if (parts.length === 1) {
273
- return parts[0].toLowerCase();
274
- }
275
- const key = parts[parts.length - 1].toLowerCase();
276
- const mods = parts.slice(0, -1).map((m) => m.toLowerCase());
277
- const seen = /* @__PURE__ */ new Set();
278
- const ordered = [];
279
- for (const m of MODIFIER_ORDER) {
280
- if (mods.includes(m) && !seen.has(m)) {
281
- ordered.push(m);
282
- seen.add(m);
283
- }
284
- }
285
- for (const m of mods) {
286
- if (!seen.has(m)) {
287
- ordered.push(m);
288
- seen.add(m);
289
- }
290
- }
291
- return [...ordered, key].join("+");
292
- }
293
- var BINDABLE_ACTION_KINDS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
294
- var init_hotkeysCatalog = __esm({
295
- "src/utils/hotkeysCatalog.ts"() {
259
+ // src/lifecycle/types.ts
260
+ var DEFAULT_STATUSES;
261
+ var init_types = __esm({
262
+ "src/lifecycle/types.ts"() {
296
263
  "use strict";
297
- BINDABLE_ACTION_KINDS = [
298
- "new-workspace",
299
- "new-project",
300
- "new-todo",
301
- "new-assignment"
264
+ DEFAULT_STATUSES = [
265
+ "draft",
266
+ "pending",
267
+ "ready_for_planning",
268
+ "ready_to_implement",
269
+ "in_progress",
270
+ "blocked",
271
+ "review",
272
+ "completed",
273
+ "failed"
302
274
  ];
303
- MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"];
304
- DEFAULT_BINDABLE_HOTKEYS = {
305
- "new-workspace": canonicalizeCombo("Mod+Shift+Alt+w"),
306
- "new-project": canonicalizeCombo("Mod+Shift+Alt+p"),
307
- "new-todo": canonicalizeCombo("Mod+Shift+Alt+t"),
308
- "new-assignment": canonicalizeCombo("Mod+Shift+Alt+a")
309
- };
310
275
  }
311
276
  });
312
277
 
313
- // src/utils/agents-schema.ts
314
- var BUILTIN_AGENTS, AGENT_ID_PATTERN, PROMPT_ARG_POSITIONS;
315
- var init_agents_schema = __esm({
316
- "src/utils/agents-schema.ts"() {
317
- "use strict";
318
- BUILTIN_AGENTS = [
319
- {
320
- id: "claude",
321
- label: "Claude",
322
- command: "claude",
323
- default: true,
324
- resume: { args: ["--resume", "{id}"] },
325
- fork: { args: ["--resume", "{id}", "--fork-session"] }
326
- },
327
- {
328
- id: "codex",
329
- label: "Codex",
330
- command: "codex",
331
- resume: { args: ["resume", "{id}"] },
332
- fork: { args: ["fork", "{id}"] }
333
- }
334
- ];
335
- AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
336
- PROMPT_ARG_POSITIONS = ["first", "last", "none"];
278
+ // src/lifecycle/state-machine.ts
279
+ function buildTransitionTable(transitions) {
280
+ const table = /* @__PURE__ */ new Map();
281
+ for (const t of transitions) {
282
+ table.set(`${t.from}:${t.command}`, t.to);
337
283
  }
338
- });
339
-
340
- // src/utils/workspace-visibility-schema.ts
341
- function normalizeHiddenList(input) {
342
- if (!Array.isArray(input)) return [];
343
- const seen = /* @__PURE__ */ new Set();
344
- const out = [];
345
- for (const raw of input) {
346
- if (typeof raw !== "string") continue;
347
- const name = raw.trim();
348
- if (name.length === 0) continue;
349
- if (name.length > MAX_WORKSPACE_NAME_LENGTH) continue;
350
- if (/[\r\n]/.test(name)) continue;
351
- if (seen.has(name)) continue;
352
- seen.add(name);
353
- out.push(name);
284
+ return table;
285
+ }
286
+ function getTargetStatus(_from, command, table) {
287
+ if (!table) {
288
+ return DEFAULT_COMMAND_TARGETS.get(command) ?? null;
354
289
  }
355
- return out;
290
+ return table.get(command) ?? table.get(`${_from}:${command}`) ?? null;
356
291
  }
357
- var MAX_WORKSPACE_NAME_LENGTH;
358
- var init_workspace_visibility_schema = __esm({
359
- "src/utils/workspace-visibility-schema.ts"() {
292
+ var DEFAULT_COMMAND_TARGETS, DEFAULT_TRANSITION_TABLE;
293
+ var init_state_machine = __esm({
294
+ "src/lifecycle/state-machine.ts"() {
360
295
  "use strict";
361
- MAX_WORKSPACE_NAME_LENGTH = 256;
296
+ init_types();
297
+ DEFAULT_COMMAND_TARGETS = /* @__PURE__ */ new Map([
298
+ ["start", "in_progress"],
299
+ ["shape", "ready_for_planning"],
300
+ ["plan-ready", "ready_to_implement"],
301
+ ["implement", "in_progress"],
302
+ ["block", "blocked"],
303
+ ["unblock", "in_progress"],
304
+ ["review", "review"],
305
+ ["complete", "completed"],
306
+ ["fail", "failed"],
307
+ ["reopen", "in_progress"]
308
+ ]);
309
+ DEFAULT_TRANSITION_TABLE = /* @__PURE__ */ new Map([
310
+ ["pending:start", "in_progress"],
311
+ ["pending:block", "blocked"],
312
+ ["draft:shape", "ready_for_planning"],
313
+ ["draft:start", "in_progress"],
314
+ ["ready_for_planning:plan-ready", "ready_to_implement"],
315
+ ["ready_for_planning:start", "in_progress"],
316
+ ["ready_to_implement:implement", "in_progress"],
317
+ ["in_progress:block", "blocked"],
318
+ ["in_progress:review", "review"],
319
+ ["in_progress:complete", "completed"],
320
+ ["in_progress:fail", "failed"],
321
+ ["blocked:unblock", "in_progress"],
322
+ ["review:start", "in_progress"],
323
+ ["review:complete", "completed"],
324
+ ["review:fail", "failed"],
325
+ ["completed:reopen", "in_progress"],
326
+ ["failed:reopen", "in_progress"]
327
+ ]);
362
328
  }
363
329
  });
364
330
 
365
- // src/utils/config.ts
366
- import { readFile as readFile3 } from "fs/promises";
367
- import { spawnSync } from "child_process";
368
- import { resolve as resolve3, isAbsolute } from "path";
369
- function parseAgentCommand(value, agentId) {
370
- if (typeof value !== "string" || value.trim() === "") {
371
- throw new AgentConfigError(
372
- `agent${agentId ? ` "${agentId}"` : ""} has empty command`
373
- );
331
+ // src/lifecycle/frontmatter.ts
332
+ var init_frontmatter = __esm({
333
+ "src/lifecycle/frontmatter.ts"() {
334
+ "use strict";
374
335
  }
375
- const expanded = expandHome(value.trim());
376
- if (isAbsolute(expanded)) {
377
- return resolve3(expanded);
336
+ });
337
+
338
+ // src/dashboard/parser.ts
339
+ function extractFrontmatter(fileContent) {
340
+ const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
341
+ if (!match) {
342
+ return ["", fileContent];
378
343
  }
379
- if (expanded.includes("/")) {
380
- throw new AgentConfigError(
381
- `agent${agentId ? ` "${agentId}"` : ""} command "${value}" is a relative path \u2014 use an absolute path or a bare binary name`
382
- );
344
+ const frontmatterBlock = match[1];
345
+ const body = fileContent.slice(match[0].length).trim();
346
+ return [frontmatterBlock, body];
347
+ }
348
+ function parseSimpleValue(raw) {
349
+ const trimmed = raw.trim();
350
+ if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
351
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
352
+ return trimmed.slice(1, -1);
383
353
  }
384
- return expanded;
354
+ return trimmed;
385
355
  }
386
- function validateAgentList(agents) {
387
- const seen = /* @__PURE__ */ new Set();
388
- let defaults = 0;
389
- for (const agent of agents) {
390
- if (!AGENT_ID_PATTERN.test(agent.id)) {
391
- throw new AgentConfigError(
392
- `agent id "${agent.id}" is invalid \u2014 must match /^[a-z0-9][a-z0-9_-]*$/`
393
- );
394
- }
395
- if (seen.has(agent.id)) {
396
- throw new AgentConfigError(`duplicate agent id "${agent.id}"`);
397
- }
398
- seen.add(agent.id);
399
- if (!agent.label || agent.label.trim() === "") {
400
- throw new AgentConfigError(`agent "${agent.id}" has empty label`);
401
- }
402
- parseAgentCommand(agent.command, agent.id);
403
- if (agent.promptArgPosition !== void 0 && !PROMPT_ARG_POSITIONS.includes(agent.promptArgPosition)) {
404
- throw new AgentConfigError(
405
- `agent "${agent.id}" has invalid promptArgPosition "${agent.promptArgPosition}" \u2014 expected first|last|none`
406
- );
356
+ function getField(frontmatter, key) {
357
+ const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
358
+ if (!match) return null;
359
+ return parseSimpleValue(match[1]);
360
+ }
361
+ function getNestedField(frontmatter, parent, key) {
362
+ const parentRegex = new RegExp(`^${parent}:\\s*\\n((?:\\s+.*\\n?)*)`, "m");
363
+ const parentMatch = frontmatter.match(parentRegex);
364
+ if (!parentMatch) return null;
365
+ const block = parentMatch[1];
366
+ const fieldMatch = block.match(new RegExp(`^\\s+${key}:\\s*(.*)$`, "m"));
367
+ if (!fieldMatch) return null;
368
+ return parseSimpleValue(fieldMatch[1]);
369
+ }
370
+ function parseListField(frontmatter, fieldName) {
371
+ const inlineMatch = frontmatter.match(new RegExp(`^${fieldName}:\\s*\\[\\s*\\]`, "m"));
372
+ if (inlineMatch) return [];
373
+ const results = [];
374
+ const blockMatch = frontmatter.match(
375
+ new RegExp(`^${fieldName}:\\s*\\n((?:\\s+-\\s+.*\\n?)*)`, "m")
376
+ );
377
+ if (blockMatch) {
378
+ let item;
379
+ const regex = /^\s+-\s+(.+)$/gm;
380
+ while ((item = regex.exec(blockMatch[1])) !== null) {
381
+ results.push(item[1].trim());
407
382
  }
408
- validateSessionInvocation(agent, "resume", agent.resume);
409
- validateSessionInvocation(agent, "fork", agent.fork);
410
- if (agent.default) defaults++;
411
- }
412
- if (defaults > 1) {
413
- throw new AgentConfigError(
414
- `more than one agent is marked default: true (only one is allowed)`
415
- );
416
383
  }
384
+ return results;
417
385
  }
418
- function validateSessionInvocation(agent, mode, invocation) {
419
- if (invocation === void 0) return;
420
- if (!Array.isArray(invocation.args)) {
421
- throw new AgentConfigError(
422
- `agent "${agent.id}" ${mode}.args must be an array of strings`
423
- );
424
- }
425
- for (const a of invocation.args) {
426
- if (typeof a !== "string") {
427
- throw new AgentConfigError(
428
- `agent "${agent.id}" ${mode}.args must contain only strings`
429
- );
430
- }
431
- }
432
- if (invocation.command !== void 0 && (typeof invocation.command !== "string" || invocation.command.trim() === "")) {
433
- throw new AgentConfigError(
434
- `agent "${agent.id}" ${mode}.command must be a non-empty string when present`
435
- );
386
+ function unquoteYamlString(value) {
387
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
388
+ return value.slice(1, -1);
436
389
  }
390
+ return value;
437
391
  }
438
- function cloneDefaultConfig() {
392
+ function parseProject(fileContent) {
393
+ const [fm, body] = extractFrontmatter(fileContent);
394
+ const slug = getField(fm, "slug") ?? getField(fm, "mission") ?? "";
439
395
  return {
440
- ...DEFAULT_CONFIG,
441
- onboarding: { ...DEFAULT_CONFIG.onboarding },
442
- agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
443
- integrations: { ...DEFAULT_CONFIG.integrations },
444
- backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
445
- statuses: DEFAULT_CONFIG.statuses ? {
446
- statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
447
- order: [...DEFAULT_CONFIG.statuses.order],
448
- transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
449
- } : null,
450
- types: DEFAULT_CONFIG.types ? {
451
- definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
452
- default: DEFAULT_CONFIG.types.default
453
- } : null,
454
- agents: DEFAULT_CONFIG.agents ? DEFAULT_CONFIG.agents.map((a) => ({
455
- ...a,
456
- ...a.args ? { args: [...a.args] } : {},
457
- ...a.resume ? { resume: { ...a.resume, args: [...a.resume.args] } } : {},
458
- ...a.fork ? { fork: { ...a.fork, args: [...a.fork.args] } } : {}
459
- })) : null,
460
- playbooks: {
461
- disabled: [...DEFAULT_CONFIG.playbooks.disabled]
462
- },
463
- theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null,
464
- hotkeys: DEFAULT_CONFIG.hotkeys ? { bindings: { ...DEFAULT_CONFIG.hotkeys.bindings } } : null,
465
- terminal: DEFAULT_CONFIG.terminal,
466
- workspaceVisibility: {
467
- hidden: [...DEFAULT_CONFIG.workspaceVisibility.hidden]
468
- }
396
+ id: getField(fm, "id") ?? "",
397
+ slug,
398
+ title: getField(fm, "title") ?? "",
399
+ archived: getField(fm, "archived") === "true",
400
+ archivedAt: getField(fm, "archivedAt"),
401
+ archivedReason: getField(fm, "archivedReason"),
402
+ statusOverride: getField(fm, "statusOverride"),
403
+ created: getField(fm, "created") ?? "",
404
+ updated: getField(fm, "updated") ?? "",
405
+ tags: parseListField(fm, "tags"),
406
+ workspace: getField(fm, "workspace"),
407
+ repositories: parseListField(fm, "repositories").map(unquoteYamlString),
408
+ externalIds: parseExternalIds(fm),
409
+ body
469
410
  };
470
411
  }
471
- function parseFrontmatter(content) {
472
- const match = content.match(/^---\n([\s\S]*?)\n---/);
473
- if (!match) return {};
474
- const result = {};
475
- const lines = match[1].split("\n");
476
- let currentParent = null;
477
- for (const line of lines) {
478
- if (line.trim() === "") continue;
479
- const indent = line.length - line.trimStart().length;
480
- const colonIndex = line.indexOf(":");
481
- if (colonIndex < 0) continue;
482
- const key = line.slice(0, colonIndex).trim();
483
- const value = line.slice(colonIndex + 1).trim();
484
- if (indent === 0) {
485
- if (value === "" || value === void 0) {
486
- currentParent = key;
487
- } else {
488
- currentParent = null;
489
- result[key] = value.replace(/^["']|["']$/g, "");
412
+ function parseStatus(fileContent) {
413
+ const [fm, body] = extractFrontmatter(fileContent);
414
+ const progress = { total: 0 };
415
+ const progressMatch = fm.match(/^progress:\s*\n((?:\s+.*\n?)*)/m);
416
+ if (progressMatch) {
417
+ const lines = progressMatch[1].split("\n");
418
+ for (const line of lines) {
419
+ const kv = line.match(/^\s+(\w+):\s*(\d+)/);
420
+ if (kv) {
421
+ progress[kv[1]] = parseInt(kv[2], 10);
490
422
  }
491
- } else if (indent > 0 && currentParent) {
492
- result[`${currentParent}.${key}`] = value.replace(/^["']|["']$/g, "");
493
423
  }
494
424
  }
495
- return result;
496
- }
497
- function parseInstalledAgents(fm) {
498
- const prefix = "integrations.installedAgents.";
499
- const installedAgents = {};
500
- for (const [key, value] of Object.entries(fm)) {
501
- if (!key.startsWith(prefix)) continue;
502
- const id = key.slice(prefix.length);
503
- if (!id) continue;
504
- const scope = value === "project" ? "project" : "global";
505
- installedAgents[id] = { scope };
506
- }
507
- return Object.keys(installedAgents).length > 0 ? { installedAgents } : {};
425
+ return {
426
+ project: getField(fm, "project") ?? "",
427
+ status: getField(fm, "status") ?? "pending",
428
+ progress,
429
+ needsAttention: {
430
+ blockedCount: parseInt(getNestedField(fm, "needsAttention", "blockedCount") ?? "0", 10),
431
+ failedCount: parseInt(getNestedField(fm, "needsAttention", "failedCount") ?? "0", 10),
432
+ openQuestions: parseInt(getNestedField(fm, "needsAttention", "openQuestions") ?? "0", 10)
433
+ },
434
+ body
435
+ };
508
436
  }
509
- function parseStatusConfig(content) {
510
- const match = content.match(/^---\n([\s\S]*?)\n---/);
511
- if (!match) return null;
512
- const fmBlock = match[1];
513
- const statusesStart = fmBlock.match(/^statuses:\s*$/m);
514
- if (!statusesStart) return null;
515
- const startIdx = fmBlock.indexOf(statusesStart[0]) + statusesStart[0].length;
516
- const remaining = fmBlock.slice(startIdx);
517
- const statuses = [];
518
- const order = [];
519
- const transitions = [];
520
- let currentSection = null;
521
- const lines = remaining.split("\n");
522
- function parseListEntry(lineIdx, baseIndent) {
437
+ function parseExternalIds(frontmatter) {
438
+ const inlineMatch = frontmatter.match(/^externalIds:\s*\[\s*\]/m);
439
+ if (inlineMatch) return [];
440
+ const results = [];
441
+ const blockMatch = frontmatter.match(
442
+ /^externalIds:\s*\n((?:\s+-\s+[\s\S]*?)(?=^\w|\n---))/m
443
+ );
444
+ if (!blockMatch) return [];
445
+ const itemBlocks = blockMatch[1].split(/\n\s+-\s+/).filter(Boolean);
446
+ for (const block of itemBlocks) {
447
+ const lines = block.split("\n");
523
448
  const entry = {};
524
- const firstLine = lines[lineIdx].trimStart().slice(2).trim();
525
- const colonIdx = firstLine.indexOf(":");
526
- if (colonIdx > 0) {
527
- entry[firstLine.slice(0, colonIdx).trim()] = firstLine.slice(colonIdx + 1).trim();
528
- }
529
- let consumed = 1;
530
- for (let i = lineIdx + 1; i < lines.length; i++) {
531
- const next = lines[i];
532
- const nextTrimmed = next.trimStart();
533
- const nextIndent = next.length - nextTrimmed.length;
534
- if (nextIndent <= baseIndent || nextTrimmed.startsWith("- ")) break;
535
- const ci = nextTrimmed.indexOf(":");
536
- if (ci > 0) {
537
- entry[nextTrimmed.slice(0, ci).trim()] = nextTrimmed.slice(ci + 1).trim();
538
- }
539
- consumed++;
540
- }
541
- return { entry, consumed };
542
- }
543
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
544
- const line = lines[lineIdx];
545
- const trimmed = line.trimStart();
546
- const indent = line.length - trimmed.length;
547
- if (indent === 2 && trimmed.endsWith(":")) {
548
- const key = trimmed.slice(0, -1).trim();
549
- if (key === "definitions") currentSection = "definitions";
550
- else if (key === "order") currentSection = "order";
551
- else if (key === "transitions") currentSection = "transitions";
552
- else currentSection = null;
553
- continue;
554
- }
555
- if (indent === 0 && trimmed.includes(":")) break;
556
- if (currentSection === "order" && indent >= 4 && trimmed.startsWith("- ")) {
557
- order.push(trimmed.slice(2).trim());
558
- continue;
559
- }
560
- if (currentSection === "definitions" && indent >= 4 && trimmed.startsWith("- ")) {
561
- const { entry, consumed } = parseListEntry(lineIdx, indent);
562
- if (entry["id"]) {
563
- statuses.push({
564
- id: entry["id"],
565
- label: entry["label"] ?? entry["id"],
566
- description: entry["description"],
567
- color: entry["color"],
568
- icon: entry["icon"],
569
- terminal: entry["terminal"] === "true"
570
- });
571
- }
572
- lineIdx += consumed - 1;
573
- continue;
449
+ for (const line of lines) {
450
+ const colonIdx = line.indexOf(":");
451
+ if (colonIdx < 0) continue;
452
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
453
+ if (!key) continue;
454
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
574
455
  }
575
- if (currentSection === "transitions" && indent >= 4 && trimmed.startsWith("- ")) {
576
- const { entry, consumed } = parseListEntry(lineIdx, indent);
577
- if (entry["from"] && entry["command"] && entry["to"]) {
578
- transitions.push({
579
- from: entry["from"],
580
- command: entry["command"],
581
- to: entry["to"],
582
- label: entry["label"],
583
- description: entry["description"],
584
- requiresReason: entry["requiresReason"] === "true"
585
- });
586
- }
587
- lineIdx += consumed - 1;
588
- continue;
456
+ if (entry["system"] && entry["id"]) {
457
+ results.push({
458
+ system: entry["system"],
459
+ id: entry["id"],
460
+ url: entry["url"] || null
461
+ });
589
462
  }
590
463
  }
591
- if (statuses.length === 0) return null;
464
+ return results;
465
+ }
466
+ function parseAssignmentFull(fileContent) {
467
+ const [fm, body] = extractFrontmatter(fileContent);
592
468
  return {
593
- statuses,
594
- order: order.length > 0 ? order : statuses.map((s) => s.id),
595
- transitions
469
+ id: getField(fm, "id") ?? "",
470
+ slug: getField(fm, "slug") ?? "",
471
+ title: getField(fm, "title") ?? "",
472
+ project: getField(fm, "project"),
473
+ workspaceGroup: getField(fm, "workspaceGroup"),
474
+ type: getField(fm, "type"),
475
+ status: getField(fm, "status") ?? "pending",
476
+ priority: getField(fm, "priority") ?? "medium",
477
+ assignee: getField(fm, "assignee"),
478
+ dependsOn: parseListField(fm, "dependsOn"),
479
+ links: parseListField(fm, "links"),
480
+ blockedReason: getField(fm, "blockedReason"),
481
+ workspace: {
482
+ repository: getNestedField(fm, "workspace", "repository"),
483
+ worktreePath: getNestedField(fm, "workspace", "worktreePath"),
484
+ branch: getNestedField(fm, "workspace", "branch"),
485
+ parentBranch: getNestedField(fm, "workspace", "parentBranch")
486
+ },
487
+ externalIds: parseExternalIds(fm),
488
+ tags: parseListField(fm, "tags"),
489
+ archived: getField(fm, "archived") === "true",
490
+ archivedAt: getField(fm, "archivedAt"),
491
+ archivedReason: getField(fm, "archivedReason"),
492
+ created: getField(fm, "created") ?? "",
493
+ updated: getField(fm, "updated") ?? "",
494
+ body
596
495
  };
597
496
  }
598
- function parsePlaybooksConfig(fmBlock) {
599
- const blockStart = fmBlock.match(/^playbooks:\s*$/m);
600
- if (!blockStart) {
601
- return { disabled: [] };
602
- }
603
- const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
604
- const remaining = fmBlock.slice(startIdx).split("\n");
605
- const disabled = [];
606
- let currentSection = null;
607
- for (const line of remaining) {
608
- const trimmed = line.trimStart();
609
- const indent = line.length - trimmed.length;
610
- if (indent === 0 && trimmed.length > 0) break;
611
- if (trimmed === "") continue;
612
- if (indent === 2 && trimmed.startsWith("disabled:")) {
613
- currentSection = "disabled";
614
- const afterColon = trimmed.slice("disabled:".length).trim();
615
- if (afterColon === "[]" || afterColon === "") {
616
- continue;
617
- }
618
- continue;
497
+ function parsePlan(fileContent) {
498
+ const [fm, body] = extractFrontmatter(fileContent);
499
+ return {
500
+ assignment: getField(fm, "assignment") ?? "",
501
+ status: getField(fm, "status") ?? "",
502
+ created: getField(fm, "created") ?? "",
503
+ updated: getField(fm, "updated") ?? "",
504
+ body
505
+ };
506
+ }
507
+ function parseScratchpad(fileContent) {
508
+ const [fm, body] = extractFrontmatter(fileContent);
509
+ return {
510
+ assignment: getField(fm, "assignment") ?? "",
511
+ updated: getField(fm, "updated") ?? "",
512
+ body
513
+ };
514
+ }
515
+ function parseHandoff(fileContent) {
516
+ const [fm, body] = extractFrontmatter(fileContent);
517
+ return {
518
+ assignment: getField(fm, "assignment") ?? "",
519
+ handoffCount: parseInt(getField(fm, "handoffCount") ?? "0", 10),
520
+ updated: getField(fm, "updated") ?? "",
521
+ body
522
+ };
523
+ }
524
+ function parseDecisionRecord(fileContent) {
525
+ const [fm, body] = extractFrontmatter(fileContent);
526
+ return {
527
+ assignment: getField(fm, "assignment") ?? "",
528
+ decisionCount: parseInt(getField(fm, "decisionCount") ?? "0", 10),
529
+ updated: getField(fm, "updated") ?? "",
530
+ body
531
+ };
532
+ }
533
+ function parseComments(fileContent) {
534
+ const [fm, body] = extractFrontmatter(fileContent);
535
+ const entries = [];
536
+ const sections = body.split(/^## /m).slice(1);
537
+ for (const section of sections) {
538
+ const newlineIdx = section.indexOf("\n");
539
+ if (newlineIdx === -1) continue;
540
+ const id = section.slice(0, newlineIdx).trim();
541
+ const rest = section.slice(newlineIdx + 1);
542
+ const headerMatch = rest.match(
543
+ /^\s*\*\*Recorded:\*\*\s*(.*)\n\*\*Author:\*\*\s*(.*)\n\*\*Type:\*\*\s*(question|note|feedback)(?:\n\*\*Reply to:\*\*\s*(.*))?(?:\n\*\*Resolved:\*\*\s*(true|false))?\n+([\s\S]*)$/
544
+ );
545
+ if (!headerMatch) continue;
546
+ const [, timestamp, author, type, replyTo, resolvedStr, entryBody] = headerMatch;
547
+ const entry = {
548
+ id,
549
+ timestamp: timestamp.trim(),
550
+ author: author.trim(),
551
+ type,
552
+ body: entryBody.trim()
553
+ };
554
+ if (replyTo) entry.replyTo = replyTo.trim();
555
+ if (resolvedStr) entry.resolved = resolvedStr === "true";
556
+ entries.push(entry);
557
+ }
558
+ return {
559
+ assignment: getField(fm, "assignment") ?? "",
560
+ entryCount: parseInt(getField(fm, "entryCount") ?? "0", 10),
561
+ updated: getField(fm, "updated") ?? "",
562
+ entries,
563
+ body
564
+ };
565
+ }
566
+ function parseProgress(fileContent) {
567
+ const [fm, body] = extractFrontmatter(fileContent);
568
+ const entries = [];
569
+ const sections = body.split(/^## /m).slice(1);
570
+ for (const section of sections) {
571
+ const newlineIdx = section.indexOf("\n");
572
+ if (newlineIdx === -1) continue;
573
+ const timestamp = section.slice(0, newlineIdx).trim();
574
+ const entryBody = section.slice(newlineIdx + 1).trim();
575
+ entries.push({ timestamp, body: entryBody });
576
+ }
577
+ return {
578
+ assignment: getField(fm, "assignment") ?? "",
579
+ entryCount: parseInt(getField(fm, "entryCount") ?? "0", 10),
580
+ updated: getField(fm, "updated") ?? "",
581
+ entries,
582
+ body
583
+ };
584
+ }
585
+ function extractMermaidGraph(body) {
586
+ const match = body.match(/```mermaid\n([\s\S]*?)```/);
587
+ return match ? match[1].trim() : null;
588
+ }
589
+ var init_parser = __esm({
590
+ "src/dashboard/parser.ts"() {
591
+ "use strict";
592
+ }
593
+ });
594
+
595
+ // src/todos/parser.ts
596
+ import { randomBytes } from "crypto";
597
+ import { readFile as readFile3 } from "fs/promises";
598
+ import { resolve as resolve3 } from "path";
599
+ var init_parser2 = __esm({
600
+ "src/todos/parser.ts"() {
601
+ "use strict";
602
+ init_parser();
603
+ init_fs();
604
+ }
605
+ });
606
+
607
+ // src/lifecycle/linked-todos.ts
608
+ import { readdir as readdir2 } from "fs/promises";
609
+ import { resolve as resolve4 } from "path";
610
+ var init_linked_todos = __esm({
611
+ "src/lifecycle/linked-todos.ts"() {
612
+ "use strict";
613
+ init_parser2();
614
+ init_fs();
615
+ }
616
+ });
617
+
618
+ // src/lifecycle/transitions.ts
619
+ import { resolve as resolve5 } from "path";
620
+ import { readFile as readFile4 } from "fs/promises";
621
+ var init_transitions = __esm({
622
+ "src/lifecycle/transitions.ts"() {
623
+ "use strict";
624
+ init_fs();
625
+ init_timestamp();
626
+ init_state_machine();
627
+ init_frontmatter();
628
+ init_linked_todos();
629
+ }
630
+ });
631
+
632
+ // src/lifecycle/index.ts
633
+ var init_lifecycle = __esm({
634
+ "src/lifecycle/index.ts"() {
635
+ "use strict";
636
+ init_types();
637
+ init_state_machine();
638
+ init_frontmatter();
639
+ init_transitions();
640
+ }
641
+ });
642
+
643
+ // src/utils/hotkeysCatalog.ts
644
+ function isBindableActionKind(value) {
645
+ return typeof value === "string" && BINDABLE_ACTION_KINDS.includes(value);
646
+ }
647
+ function canonicalizeCombo(input) {
648
+ if (typeof input !== "string") return "";
649
+ const trimmed = input.trim();
650
+ if (!trimmed) return "";
651
+ if (/\s/.test(trimmed) && !trimmed.includes("+")) {
652
+ return trimmed.split(/\s+/).map(canonicalizeCombo).filter((part) => part.length > 0).join(" ");
653
+ }
654
+ const parts = trimmed.split("+").map((p) => p.trim()).filter((p) => p.length > 0);
655
+ if (parts.length === 0) return "";
656
+ if (parts.length === 1) {
657
+ return parts[0].toLowerCase();
658
+ }
659
+ const key = parts[parts.length - 1].toLowerCase();
660
+ const mods = parts.slice(0, -1).map((m) => m.toLowerCase());
661
+ const seen = /* @__PURE__ */ new Set();
662
+ const ordered = [];
663
+ for (const m of MODIFIER_ORDER) {
664
+ if (mods.includes(m) && !seen.has(m)) {
665
+ ordered.push(m);
666
+ seen.add(m);
619
667
  }
620
- if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
621
- const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
622
- if (raw.length === 0) continue;
623
- if (/\s/.test(raw)) {
624
- console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
625
- continue;
626
- }
627
- disabled.push(raw);
628
- continue;
668
+ }
669
+ for (const m of mods) {
670
+ if (!seen.has(m)) {
671
+ ordered.push(m);
672
+ seen.add(m);
629
673
  }
630
674
  }
631
- return { disabled };
675
+ return [...ordered, key].join("+");
632
676
  }
633
- function parseThemeConfig(content) {
634
- const match = content.match(/^---\n([\s\S]*?)\n---/);
635
- if (!match) return null;
636
- const fmBlock = match[1];
637
- const blockStart = fmBlock.match(/^theme:\s*$/m);
638
- if (!blockStart) return null;
639
- const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
640
- const remaining = fmBlock.slice(startIdx).split("\n");
641
- let preset = null;
642
- for (const line of remaining) {
643
- const trimmed = line.trimStart();
644
- const indent = line.length - trimmed.length;
645
- if (indent === 0 && trimmed.length > 0) break;
646
- if (trimmed === "") continue;
647
- if (indent === 2 && trimmed.startsWith("preset:")) {
648
- const value = trimmed.slice("preset:".length).trim().replace(/^["']|["']$/g, "");
649
- if (value.length > 0) preset = value;
650
- }
677
+ var BINDABLE_ACTION_KINDS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
678
+ var init_hotkeysCatalog = __esm({
679
+ "src/utils/hotkeysCatalog.ts"() {
680
+ "use strict";
681
+ BINDABLE_ACTION_KINDS = [
682
+ "new-workspace",
683
+ "new-project",
684
+ "new-todo",
685
+ "new-assignment"
686
+ ];
687
+ MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"];
688
+ DEFAULT_BINDABLE_HOTKEYS = {
689
+ "new-workspace": canonicalizeCombo("Mod+Shift+Alt+w"),
690
+ "new-project": canonicalizeCombo("Mod+Shift+Alt+p"),
691
+ "new-todo": canonicalizeCombo("Mod+Shift+Alt+t"),
692
+ "new-assignment": canonicalizeCombo("Mod+Shift+Alt+a")
693
+ };
694
+ }
695
+ });
696
+
697
+ // src/utils/agents-schema.ts
698
+ var BUILTIN_AGENTS, AGENT_ID_PATTERN, PROMPT_ARG_POSITIONS;
699
+ var init_agents_schema = __esm({
700
+ "src/utils/agents-schema.ts"() {
701
+ "use strict";
702
+ BUILTIN_AGENTS = [
703
+ {
704
+ id: "claude",
705
+ label: "Claude",
706
+ command: "claude",
707
+ default: true,
708
+ resume: { args: ["--resume", "{id}"] },
709
+ fork: { args: ["--resume", "{id}", "--fork-session"] }
710
+ },
711
+ {
712
+ id: "codex",
713
+ label: "Codex",
714
+ command: "codex",
715
+ resume: { args: ["resume", "{id}"] },
716
+ fork: { args: ["fork", "{id}"] }
717
+ }
718
+ ];
719
+ AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
720
+ PROMPT_ARG_POSITIONS = ["first", "last", "none"];
651
721
  }
652
- if (!preset) return null;
653
- return { preset };
722
+ });
723
+
724
+ // src/utils/workspace-visibility-schema.ts
725
+ function normalizeHiddenList(input) {
726
+ if (!Array.isArray(input)) return [];
727
+ const seen = /* @__PURE__ */ new Set();
728
+ const out = [];
729
+ for (const raw of input) {
730
+ if (typeof raw !== "string") continue;
731
+ const name = raw.trim();
732
+ if (name.length === 0) continue;
733
+ if (name.length > MAX_WORKSPACE_NAME_LENGTH) continue;
734
+ if (/[\r\n]/.test(name)) continue;
735
+ if (seen.has(name)) continue;
736
+ seen.add(name);
737
+ out.push(name);
738
+ }
739
+ return out;
654
740
  }
655
- function parseWorkspaceVisibilityConfig(fmBlock) {
656
- const blockStart = fmBlock.match(/^workspaceVisibility:\s*$/m);
657
- if (!blockStart) {
658
- return { hidden: [] };
741
+ var MAX_WORKSPACE_NAME_LENGTH;
742
+ var init_workspace_visibility_schema = __esm({
743
+ "src/utils/workspace-visibility-schema.ts"() {
744
+ "use strict";
745
+ MAX_WORKSPACE_NAME_LENGTH = 256;
659
746
  }
660
- const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
661
- const remaining = fmBlock.slice(startIdx).split("\n");
662
- const hidden = [];
663
- let currentSection = null;
664
- for (const line of remaining) {
665
- const trimmed = line.trimStart();
666
- const indent = line.length - trimmed.length;
667
- if (indent === 0 && trimmed.length > 0) break;
668
- if (trimmed === "") continue;
669
- if (indent === 2 && trimmed.startsWith("hidden:")) {
670
- currentSection = "hidden";
671
- continue;
672
- }
673
- if (currentSection === "hidden" && indent >= 4 && trimmed.startsWith("- ")) {
674
- const rest = trimmed.slice(2).trim();
675
- if (rest.length === 0) continue;
676
- let name;
677
- if (rest.startsWith('"')) {
678
- try {
679
- name = JSON.parse(rest);
680
- } catch {
681
- name = rest.replace(/^["']|["']$/g, "");
682
- }
683
- } else {
684
- name = rest;
685
- }
686
- hidden.push(name);
687
- continue;
688
- }
747
+ });
748
+
749
+ // src/utils/config.ts
750
+ import { readFile as readFile5 } from "fs/promises";
751
+ import { spawnSync } from "child_process";
752
+ import { resolve as resolve6, isAbsolute } from "path";
753
+ function parseAgentCommand(value, agentId) {
754
+ if (typeof value !== "string" || value.trim() === "") {
755
+ throw new AgentConfigError(
756
+ `agent${agentId ? ` "${agentId}"` : ""} has empty command`
757
+ );
689
758
  }
690
- return { hidden: normalizeHiddenList(hidden) };
759
+ const expanded = expandHome(value.trim());
760
+ if (isAbsolute(expanded)) {
761
+ return resolve6(expanded);
762
+ }
763
+ if (expanded.includes("/")) {
764
+ throw new AgentConfigError(
765
+ `agent${agentId ? ` "${agentId}"` : ""} command "${value}" is a relative path \u2014 use an absolute path or a bare binary name`
766
+ );
767
+ }
768
+ return expanded;
691
769
  }
692
- function parseHotkeyBindingsConfig(content) {
693
- const match = content.match(/^---\n([\s\S]*?)\n---/);
694
- if (!match) return null;
695
- const fmBlock = match[1];
696
- const blockStart = fmBlock.match(/^hotkeys:\s*$/m);
697
- if (!blockStart) return null;
698
- const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
699
- const remaining = fmBlock.slice(startIdx).split("\n");
700
- const bindings = {};
701
- let inBindings = false;
702
- for (const line of remaining) {
703
- const trimmed = line.trimStart();
704
- const indent = line.length - trimmed.length;
705
- if (indent === 0 && trimmed.length > 0) break;
706
- if (trimmed === "") continue;
707
- if (indent === 2 && trimmed === "bindings:") {
708
- inBindings = true;
709
- continue;
770
+ function validateAgentList(agents) {
771
+ const seen = /* @__PURE__ */ new Set();
772
+ let defaults = 0;
773
+ for (const agent of agents) {
774
+ if (!AGENT_ID_PATTERN.test(agent.id)) {
775
+ throw new AgentConfigError(
776
+ `agent id "${agent.id}" is invalid \u2014 must match /^[a-z0-9][a-z0-9_-]*$/`
777
+ );
710
778
  }
711
- if (inBindings && indent === 4) {
712
- const colonIdx = trimmed.indexOf(":");
713
- if (colonIdx <= 0) continue;
714
- const rawKind = trimmed.slice(0, colonIdx).trim();
715
- const rawValue = trimmed.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
716
- if (!isBindableActionKind(rawKind)) continue;
717
- if (rawValue.length === 0) continue;
718
- bindings[rawKind] = canonicalizeCombo(rawValue);
779
+ if (seen.has(agent.id)) {
780
+ throw new AgentConfigError(`duplicate agent id "${agent.id}"`);
781
+ }
782
+ seen.add(agent.id);
783
+ if (!agent.label || agent.label.trim() === "") {
784
+ throw new AgentConfigError(`agent "${agent.id}" has empty label`);
785
+ }
786
+ parseAgentCommand(agent.command, agent.id);
787
+ if (agent.promptArgPosition !== void 0 && !PROMPT_ARG_POSITIONS.includes(agent.promptArgPosition)) {
788
+ throw new AgentConfigError(
789
+ `agent "${agent.id}" has invalid promptArgPosition "${agent.promptArgPosition}" \u2014 expected first|last|none`
790
+ );
719
791
  }
792
+ validateSessionInvocation(agent, "resume", agent.resume);
793
+ validateSessionInvocation(agent, "fork", agent.fork);
794
+ if (agent.default) defaults++;
795
+ }
796
+ if (defaults > 1) {
797
+ throw new AgentConfigError(
798
+ `more than one agent is marked default: true (only one is allowed)`
799
+ );
720
800
  }
721
- if (Object.keys(bindings).length === 0) return null;
722
- return { bindings };
723
801
  }
724
- function parseOptionalAbsolutePath(value, fieldName) {
725
- if (!value) {
726
- return null;
802
+ function validateSessionInvocation(agent, mode, invocation) {
803
+ if (invocation === void 0) return;
804
+ if (!Array.isArray(invocation.args)) {
805
+ throw new AgentConfigError(
806
+ `agent "${agent.id}" ${mode}.args must be an array of strings`
807
+ );
727
808
  }
728
- const expanded = expandHome(String(value));
729
- if (!isAbsolute(expanded)) {
730
- console.warn(
731
- `Warning: config.md ${fieldName} is not an absolute path ("${value}"), ignoring it`
809
+ for (const a of invocation.args) {
810
+ if (typeof a !== "string") {
811
+ throw new AgentConfigError(
812
+ `agent "${agent.id}" ${mode}.args must contain only strings`
813
+ );
814
+ }
815
+ }
816
+ if (invocation.command !== void 0 && (typeof invocation.command !== "string" || invocation.command.trim() === "")) {
817
+ throw new AgentConfigError(
818
+ `agent "${agent.id}" ${mode}.command must be a non-empty string when present`
732
819
  );
733
- return null;
734
820
  }
735
- return resolve3(expanded);
736
821
  }
737
- function parseAgentsConfig(content) {
822
+ function cloneDefaultConfig() {
823
+ return {
824
+ ...DEFAULT_CONFIG,
825
+ onboarding: { ...DEFAULT_CONFIG.onboarding },
826
+ agentDefaults: { ...DEFAULT_CONFIG.agentDefaults },
827
+ integrations: { ...DEFAULT_CONFIG.integrations },
828
+ backup: DEFAULT_CONFIG.backup ? { ...DEFAULT_CONFIG.backup } : null,
829
+ statuses: DEFAULT_CONFIG.statuses ? {
830
+ statuses: DEFAULT_CONFIG.statuses.statuses.map((s) => ({ ...s })),
831
+ order: [...DEFAULT_CONFIG.statuses.order],
832
+ transitions: DEFAULT_CONFIG.statuses.transitions.map((t) => ({ ...t }))
833
+ } : null,
834
+ types: DEFAULT_CONFIG.types ? {
835
+ definitions: DEFAULT_CONFIG.types.definitions.map((d) => ({ ...d })),
836
+ default: DEFAULT_CONFIG.types.default
837
+ } : null,
838
+ agents: DEFAULT_CONFIG.agents ? DEFAULT_CONFIG.agents.map((a) => ({
839
+ ...a,
840
+ ...a.args ? { args: [...a.args] } : {},
841
+ ...a.resume ? { resume: { ...a.resume, args: [...a.resume.args] } } : {},
842
+ ...a.fork ? { fork: { ...a.fork, args: [...a.fork.args] } } : {}
843
+ })) : null,
844
+ playbooks: {
845
+ disabled: [...DEFAULT_CONFIG.playbooks.disabled]
846
+ },
847
+ theme: DEFAULT_CONFIG.theme ? { ...DEFAULT_CONFIG.theme } : null,
848
+ hotkeys: DEFAULT_CONFIG.hotkeys ? { bindings: { ...DEFAULT_CONFIG.hotkeys.bindings } } : null,
849
+ terminal: DEFAULT_CONFIG.terminal,
850
+ workspaceVisibility: {
851
+ hidden: [...DEFAULT_CONFIG.workspaceVisibility.hidden]
852
+ }
853
+ };
854
+ }
855
+ function parseFrontmatter(content) {
738
856
  const match = content.match(/^---\n([\s\S]*?)\n---/);
739
- if (!match) return null;
740
- const fmBlock = match[1];
741
- const agentsStart = fmBlock.match(/^agents:\s*$/m);
742
- if (!agentsStart) return null;
743
- const startIdx = fmBlock.indexOf(agentsStart[0]) + agentsStart[0].length;
744
- const remaining = fmBlock.slice(startIdx);
745
- const lines = remaining.split("\n");
746
- const agents = [];
747
- let current = null;
748
- let argsCapture = null;
749
- let argsBaseIndent = 0;
750
- let nestedKey = null;
751
- let nestedInvocation = null;
752
- let nestedBaseIndent = 0;
753
- function flushCurrent() {
754
- if (!current) return;
755
- if (!current.id || !current.command || !current.label) {
756
- current = null;
757
- return;
857
+ if (!match) return {};
858
+ const result = {};
859
+ const lines = match[1].split("\n");
860
+ let currentParent = null;
861
+ for (const line of lines) {
862
+ if (line.trim() === "") continue;
863
+ const indent = line.length - line.trimStart().length;
864
+ const colonIndex = line.indexOf(":");
865
+ if (colonIndex < 0) continue;
866
+ const key = line.slice(0, colonIndex).trim();
867
+ const value = line.slice(colonIndex + 1).trim();
868
+ if (indent === 0) {
869
+ if (value === "" || value === void 0) {
870
+ currentParent = key;
871
+ } else {
872
+ currentParent = null;
873
+ result[key] = value.replace(/^["']|["']$/g, "");
874
+ }
875
+ } else if (indent > 0 && currentParent) {
876
+ result[`${currentParent}.${key}`] = value.replace(/^["']|["']$/g, "");
758
877
  }
759
- agents.push({
760
- id: current.id,
761
- label: current.label,
762
- command: current.command,
763
- ...current.args && current.args.length > 0 ? { args: current.args } : {},
764
- ...current.promptArgPosition ? { promptArgPosition: current.promptArgPosition } : {},
765
- ...current.default ? { default: true } : {},
766
- ...current.resolveFromShellAliases ? { resolveFromShellAliases: true } : {},
767
- ...current.resume ? { resume: current.resume } : {},
768
- ...current.fork ? { fork: current.fork } : {}
769
- });
770
- current = null;
771
- argsCapture = null;
772
- nestedKey = null;
773
- nestedInvocation = null;
774
878
  }
775
- function closeNestedBlock() {
776
- if (!nestedKey) return;
777
- if (current && (nestedKey === "resume" || nestedKey === "fork") && nestedInvocation) {
778
- if (Array.isArray(nestedInvocation.args)) {
779
- current[nestedKey] = nestedInvocation;
879
+ return result;
880
+ }
881
+ function parseInstalledAgents(fm) {
882
+ const prefix = "integrations.installedAgents.";
883
+ const installedAgents = {};
884
+ for (const [key, value] of Object.entries(fm)) {
885
+ if (!key.startsWith(prefix)) continue;
886
+ const id = key.slice(prefix.length);
887
+ if (!id) continue;
888
+ const scope = value === "project" ? "project" : "global";
889
+ installedAgents[id] = { scope };
890
+ }
891
+ return Object.keys(installedAgents).length > 0 ? { installedAgents } : {};
892
+ }
893
+ function parseStatusConfig(content) {
894
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
895
+ if (!match) return null;
896
+ const fmBlock = match[1];
897
+ const statusesStart = fmBlock.match(/^statuses:\s*$/m);
898
+ if (!statusesStart) return null;
899
+ const startIdx = fmBlock.indexOf(statusesStart[0]) + statusesStart[0].length;
900
+ const remaining = fmBlock.slice(startIdx);
901
+ const statuses = [];
902
+ const order = [];
903
+ const transitions = [];
904
+ let currentSection = null;
905
+ const lines = remaining.split("\n");
906
+ function parseListEntry(lineIdx, baseIndent) {
907
+ const entry = {};
908
+ const firstLine = lines[lineIdx].trimStart().slice(2).trim();
909
+ const colonIdx = firstLine.indexOf(":");
910
+ if (colonIdx > 0) {
911
+ entry[firstLine.slice(0, colonIdx).trim()] = firstLine.slice(colonIdx + 1).trim();
912
+ }
913
+ let consumed = 1;
914
+ for (let i = lineIdx + 1; i < lines.length; i++) {
915
+ const next = lines[i];
916
+ const nextTrimmed = next.trimStart();
917
+ const nextIndent = next.length - nextTrimmed.length;
918
+ if (nextIndent <= baseIndent || nextTrimmed.startsWith("- ")) break;
919
+ const ci = nextTrimmed.indexOf(":");
920
+ if (ci > 0) {
921
+ entry[nextTrimmed.slice(0, ci).trim()] = nextTrimmed.slice(ci + 1).trim();
780
922
  }
923
+ consumed++;
781
924
  }
782
- nestedKey = null;
783
- nestedInvocation = null;
784
- argsCapture = null;
925
+ return { entry, consumed };
785
926
  }
786
- for (let i = 0; i < lines.length; i++) {
787
- const line = lines[i];
927
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
928
+ const line = lines[lineIdx];
788
929
  const trimmed = line.trimStart();
789
930
  const indent = line.length - trimmed.length;
790
- if (indent === 0 && trimmed !== "" && !trimmed.startsWith("#")) {
791
- closeNestedBlock();
792
- break;
793
- }
794
- if (argsCapture) {
795
- if (indent > argsBaseIndent && trimmed.startsWith("- ")) {
796
- argsCapture.push(decodeYamlScalar(trimmed.slice(2).trim()));
797
- continue;
798
- } else {
799
- argsCapture = null;
800
- }
801
- }
802
- if (indent === 2 && trimmed.startsWith("- ")) {
803
- closeNestedBlock();
804
- flushCurrent();
805
- current = {};
806
- const rest = trimmed.slice(2).trim();
807
- const colonIdx = rest.indexOf(":");
808
- if (colonIdx > 0) {
809
- const k = rest.slice(0, colonIdx).trim();
810
- const v = rest.slice(colonIdx + 1).trim();
811
- assignAgentField(current, k, v);
812
- }
931
+ if (indent === 2 && trimmed.endsWith(":")) {
932
+ const key = trimmed.slice(0, -1).trim();
933
+ if (key === "definitions") currentSection = "definitions";
934
+ else if (key === "order") currentSection = "order";
935
+ else if (key === "transitions") currentSection = "transitions";
936
+ else currentSection = null;
813
937
  continue;
814
938
  }
815
- if (!current) continue;
816
- if (nestedKey && indent > nestedBaseIndent) {
817
- const colonIdx = trimmed.indexOf(":");
818
- if (colonIdx <= 0) continue;
819
- const k = trimmed.slice(0, colonIdx).trim();
820
- const v = trimmed.slice(colonIdx + 1).trim();
821
- if (nestedKey === "resume" || nestedKey === "fork") {
822
- if (!nestedInvocation) nestedInvocation = { args: [] };
823
- if (k === "args" && v === "") {
824
- nestedInvocation.args = [];
825
- argsCapture = nestedInvocation.args;
826
- argsBaseIndent = indent;
827
- continue;
828
- }
829
- if (k === "command" && v !== "") {
830
- nestedInvocation.command = decodeYamlScalar(v);
831
- continue;
832
- }
833
- }
939
+ if (indent === 0 && trimmed.includes(":")) break;
940
+ if (currentSection === "order" && indent >= 4 && trimmed.startsWith("- ")) {
941
+ order.push(trimmed.slice(2).trim());
834
942
  continue;
835
943
  }
836
- if (nestedKey && indent <= nestedBaseIndent) {
837
- closeNestedBlock();
838
- }
839
- if (indent >= 4 && current) {
840
- const colonIdx = trimmed.indexOf(":");
841
- if (colonIdx <= 0) continue;
842
- const k = trimmed.slice(0, colonIdx).trim();
843
- const v = trimmed.slice(colonIdx + 1).trim();
844
- if (k === "args" && v === "") {
845
- argsCapture = [];
846
- argsBaseIndent = indent;
847
- current.args = argsCapture;
848
- continue;
849
- }
850
- if ((k === "resume" || k === "fork") && v === "") {
851
- nestedKey = k;
852
- nestedInvocation = { args: [] };
853
- nestedBaseIndent = indent;
854
- continue;
855
- }
856
- if (v === "" && !KNOWN_AGENT_SCALAR_FIELDS.has(k)) {
857
- nestedKey = "__skip__";
858
- nestedInvocation = null;
859
- nestedBaseIndent = indent;
860
- continue;
944
+ if (currentSection === "definitions" && indent >= 4 && trimmed.startsWith("- ")) {
945
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
946
+ if (entry["id"]) {
947
+ statuses.push({
948
+ id: entry["id"],
949
+ label: entry["label"] ?? entry["id"],
950
+ description: entry["description"],
951
+ color: entry["color"],
952
+ icon: entry["icon"],
953
+ terminal: entry["terminal"] === "true"
954
+ });
861
955
  }
862
- assignAgentField(current, k, v);
956
+ lineIdx += consumed - 1;
957
+ continue;
863
958
  }
864
- }
865
- closeNestedBlock();
866
- flushCurrent();
867
- if (agents.length === 0) return [];
868
- return agents;
869
- }
870
- function normalizeAgentsFromConfig(agents) {
871
- if (agents === null) return null;
872
- try {
873
- const normalized = agents.map((agent) => ({
874
- ...agent,
875
- command: parseAgentCommand(agent.command, agent.id)
876
- }));
877
- validateAgentList(normalized);
878
- return normalized;
879
- } catch (err) {
880
- const msg = err instanceof Error ? err.message : String(err);
881
- console.warn(
882
- `Warning: ~/.syntaur/config.md agents block is invalid (${msg}) \u2014 using built-in defaults`
883
- );
884
- return null;
885
- }
886
- }
887
- function decodeYamlScalar(value) {
888
- const trimmed = value.trim();
889
- if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
890
- const body = trimmed.slice(1, -1);
891
- let out = "";
892
- for (let i = 0; i < body.length; i++) {
893
- const ch = body[i];
894
- if (ch === "\\" && i + 1 < body.length) {
895
- const next = body[i + 1];
896
- switch (next) {
897
- case "\\":
898
- out += "\\";
899
- break;
900
- case '"':
901
- out += '"';
902
- break;
903
- case "n":
904
- out += "\n";
905
- break;
906
- case "t":
907
- out += " ";
908
- break;
909
- case "r":
910
- out += "\r";
911
- break;
912
- default:
913
- out += next;
914
- break;
915
- }
916
- i++;
917
- continue;
959
+ if (currentSection === "transitions" && indent >= 4 && trimmed.startsWith("- ")) {
960
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
961
+ if (entry["from"] && entry["command"] && entry["to"]) {
962
+ transitions.push({
963
+ from: entry["from"],
964
+ command: entry["command"],
965
+ to: entry["to"],
966
+ label: entry["label"],
967
+ description: entry["description"],
968
+ requiresReason: entry["requiresReason"] === "true"
969
+ });
918
970
  }
919
- out += ch;
971
+ lineIdx += consumed - 1;
972
+ continue;
920
973
  }
921
- return out;
922
- }
923
- if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
924
- return trimmed.slice(1, -1).replace(/''/g, "'");
925
- }
926
- return trimmed;
927
- }
928
- function assignAgentField(target, key, rawValue) {
929
- const value = decodeYamlScalar(rawValue);
930
- switch (key) {
931
- case "id":
932
- target.id = value;
933
- break;
934
- case "label":
935
- target.label = value;
936
- break;
937
- case "command":
938
- target.command = value;
939
- break;
940
- case "promptArgPosition":
941
- target.promptArgPosition = value;
942
- break;
943
- case "default":
944
- target.default = value === "true";
945
- break;
946
- case "resolveFromShellAliases":
947
- target.resolveFromShellAliases = value === "true";
948
- break;
949
- }
950
- }
951
- async function readConfig() {
952
- const configPath = resolve3(syntaurRoot(), "config.md");
953
- if (!await fileExists(configPath)) {
954
- return cloneDefaultConfig();
955
- }
956
- if (!migratedConfigPaths.has(configPath)) {
957
- migratedConfigPaths.add(configPath);
958
- await migrateLegacyConfig(configPath);
959
- }
960
- const content = await readFile3(configPath, "utf-8");
961
- const fm = parseFrontmatter(content);
962
- if (Object.keys(fm).length === 0) {
963
- console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
964
- return cloneDefaultConfig();
965
- }
966
- let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
967
- if (!isAbsolute(projectDir)) {
968
- console.warn(
969
- `Warning: config.md defaultProjectDir is not an absolute path ("${fm["defaultProjectDir"]}"), using default`
970
- );
971
- projectDir = DEFAULT_CONFIG.defaultProjectDir;
972
974
  }
973
- const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
975
+ if (statuses.length === 0) return null;
974
976
  return {
975
- version: fm["version"] || DEFAULT_CONFIG.version,
976
- defaultProjectDir: projectDir,
977
- onboarding: {
978
- completed: fm["onboarding.completed"] === "true"
979
- },
980
- agentDefaults: {
981
- trustLevel: fm["agentDefaults.trustLevel"] || DEFAULT_CONFIG.agentDefaults.trustLevel,
982
- autoApprove: fm["agentDefaults.autoApprove"] === "true" || DEFAULT_CONFIG.agentDefaults.autoApprove,
983
- autoCreateWorktree: AUTO_CREATE_WORKTREE_VALUES.includes(
984
- fm["agentDefaults.autoCreateWorktree"]
985
- ) ? fm["agentDefaults.autoCreateWorktree"] : DEFAULT_CONFIG.agentDefaults.autoCreateWorktree
986
- },
987
- integrations: {
988
- claudePluginDir: parseOptionalAbsolutePath(
989
- fm["integrations.claudePluginDir"],
990
- "integrations.claudePluginDir"
991
- ),
992
- codexPluginDir: parseOptionalAbsolutePath(
993
- fm["integrations.codexPluginDir"],
994
- "integrations.codexPluginDir"
995
- ),
996
- codexMarketplacePath: parseOptionalAbsolutePath(
997
- fm["integrations.codexMarketplacePath"],
998
- "integrations.codexMarketplacePath"
999
- ),
1000
- ...parseInstalledAgents(fm)
1001
- },
1002
- backup: fm["backup.repo"] || fm["backup.categories"] ? {
1003
- repo: fm["backup.repo"] && fm["backup.repo"] !== "null" ? fm["backup.repo"] : null,
1004
- categories: fm["backup.categories"] || "projects, playbooks, todos, servers, config",
1005
- lastBackup: fm["backup.lastBackup"] && fm["backup.lastBackup"] !== "null" ? fm["backup.lastBackup"] : null,
1006
- lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
1007
- } : null,
1008
- statuses: parseStatusConfig(content),
1009
- types: null,
1010
- agents: normalizeAgentsFromConfig(parseAgentsConfig(content)),
1011
- playbooks: parsePlaybooksConfig(fmBlock),
1012
- theme: parseThemeConfig(content),
1013
- hotkeys: parseHotkeyBindingsConfig(content),
1014
- terminal: (() => {
1015
- try {
1016
- return parseTerminalConfig(fm["terminal"]);
1017
- } catch (err) {
1018
- const msg = err instanceof TerminalConfigError ? err.message : String(err);
1019
- console.warn(`Warning: ${msg} \u2014 falling back to default`);
1020
- return null;
1021
- }
1022
- })(),
1023
- workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock)
977
+ statuses,
978
+ order: order.length > 0 ? order : statuses.map((s) => s.id),
979
+ transitions
1024
980
  };
1025
981
  }
1026
- function getAgents(config) {
1027
- if (config.agents === null) return BUILTIN_AGENTS;
1028
- const builtinById = new Map(BUILTIN_AGENTS.map((a) => [a.id, a]));
1029
- return config.agents.map((agent) => {
1030
- const builtin = builtinById.get(agent.id);
1031
- if (!builtin) return agent;
1032
- const resume = agent.resume ?? builtin.resume;
1033
- const fork = agent.fork ?? builtin.fork;
1034
- if (resume === agent.resume && fork === agent.fork) return agent;
1035
- return {
1036
- ...agent,
1037
- ...resume ? { resume } : {},
1038
- ...fork ? { fork } : {}
1039
- };
1040
- });
982
+ function toTitleCase(s) {
983
+ return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1041
984
  }
1042
- function parseTerminalConfig(value) {
1043
- if (value === void 0 || value === null || value === "") return null;
1044
- if (typeof value !== "string") {
1045
- throw new TerminalConfigError(
1046
- `terminal must be a string \u2014 got ${typeof value}`
1047
- );
1048
- }
1049
- const trimmed = value.trim();
1050
- if (trimmed === "") return null;
1051
- if (!TERMINAL_CHOICES.includes(trimmed)) {
1052
- throw new TerminalConfigError(
1053
- `terminal "${trimmed}" is not a known choice \u2014 expected one of ${TERMINAL_CHOICES.join("|")}`
1054
- );
1055
- }
1056
- return trimmed;
985
+ function buildDefaultStatusConfig() {
986
+ return {
987
+ statuses: DEFAULT_STATUSES.map((id) => ({
988
+ id,
989
+ label: toTitleCase(id),
990
+ color: DEFAULT_STATUS_COLORS[id] ?? "gray",
991
+ terminal: id === "completed" || id === "failed"
992
+ })),
993
+ order: [...DEFAULT_STATUSES],
994
+ transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
995
+ const [from, command] = key.split(":");
996
+ return { from, command, to };
997
+ })
998
+ };
1057
999
  }
1058
- function getTerminal(config) {
1059
- if (config.terminal) return config.terminal;
1060
- if (process.platform === "darwin") return "terminal-app";
1061
- if (process.platform === "linux") {
1062
- const order = ["kitty", "alacritty", "warp"];
1063
- for (const candidate of order) {
1064
- const result = spawnSync("which", [candidate], { encoding: "utf-8" });
1065
- if (result.status === 0 && result.stdout.trim().length > 0) {
1066
- return candidate;
1000
+ function parsePlaybooksConfig(fmBlock) {
1001
+ const blockStart = fmBlock.match(/^playbooks:\s*$/m);
1002
+ if (!blockStart) {
1003
+ return { disabled: [] };
1004
+ }
1005
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
1006
+ const remaining = fmBlock.slice(startIdx).split("\n");
1007
+ const disabled = [];
1008
+ let currentSection = null;
1009
+ for (const line of remaining) {
1010
+ const trimmed = line.trimStart();
1011
+ const indent = line.length - trimmed.length;
1012
+ if (indent === 0 && trimmed.length > 0) break;
1013
+ if (trimmed === "") continue;
1014
+ if (indent === 2 && trimmed.startsWith("disabled:")) {
1015
+ currentSection = "disabled";
1016
+ const afterColon = trimmed.slice("disabled:".length).trim();
1017
+ if (afterColon === "[]" || afterColon === "") {
1018
+ continue;
1067
1019
  }
1020
+ continue;
1068
1021
  }
1069
- }
1070
- return "terminal-app";
1071
- }
1072
- var DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
1073
- var init_config2 = __esm({
1074
- "src/utils/config.ts"() {
1075
- "use strict";
1076
- init_paths();
1077
- init_fs();
1078
- init_config();
1079
- init_fs_migration();
1080
- init_hotkeysCatalog();
1081
- init_agents_schema();
1082
- init_terminal_schema();
1083
- init_workspace_visibility_schema();
1084
- DEFAULT_CONFIG = {
1085
- version: "2.0",
1086
- defaultProjectDir: defaultProjectDir(),
1087
- onboarding: {
1088
- completed: false
1089
- },
1090
- agentDefaults: {
1091
- trustLevel: "medium",
1092
- autoApprove: false,
1093
- autoCreateWorktree: "ask"
1094
- },
1095
- integrations: {
1096
- claudePluginDir: null,
1097
- codexPluginDir: null,
1098
- codexMarketplacePath: null
1099
- },
1100
- backup: null,
1101
- statuses: null,
1102
- types: null,
1103
- agents: null,
1104
- playbooks: {
1105
- disabled: []
1106
- },
1107
- theme: null,
1108
- hotkeys: null,
1109
- terminal: null,
1110
- workspaceVisibility: {
1111
- hidden: []
1022
+ if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
1023
+ const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
1024
+ if (raw.length === 0) continue;
1025
+ if (/\s/.test(raw)) {
1026
+ console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
1027
+ continue;
1112
1028
  }
1113
- };
1114
- AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
1115
- AgentConfigError = class extends Error {
1116
- };
1117
- KNOWN_AGENT_SCALAR_FIELDS = /* @__PURE__ */ new Set([
1118
- "id",
1119
- "label",
1120
- "command",
1121
- "promptArgPosition",
1122
- "default",
1123
- "resolveFromShellAliases"
1124
- ]);
1125
- migratedConfigPaths = /* @__PURE__ */ new Set();
1126
- TerminalConfigError = class extends Error {
1127
- };
1029
+ disabled.push(raw);
1030
+ continue;
1031
+ }
1128
1032
  }
1129
- });
1130
-
1131
- // src/dashboard/parser.ts
1132
- function extractFrontmatter(fileContent) {
1133
- const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
1134
- if (!match) {
1135
- return ["", fileContent];
1033
+ return { disabled };
1034
+ }
1035
+ function parseThemeConfig(content) {
1036
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1037
+ if (!match) return null;
1038
+ const fmBlock = match[1];
1039
+ const blockStart = fmBlock.match(/^theme:\s*$/m);
1040
+ if (!blockStart) return null;
1041
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
1042
+ const remaining = fmBlock.slice(startIdx).split("\n");
1043
+ let preset = null;
1044
+ for (const line of remaining) {
1045
+ const trimmed = line.trimStart();
1046
+ const indent = line.length - trimmed.length;
1047
+ if (indent === 0 && trimmed.length > 0) break;
1048
+ if (trimmed === "") continue;
1049
+ if (indent === 2 && trimmed.startsWith("preset:")) {
1050
+ const value = trimmed.slice("preset:".length).trim().replace(/^["']|["']$/g, "");
1051
+ if (value.length > 0) preset = value;
1052
+ }
1136
1053
  }
1137
- const frontmatterBlock = match[1];
1138
- const body = fileContent.slice(match[0].length).trim();
1139
- return [frontmatterBlock, body];
1054
+ if (!preset) return null;
1055
+ return { preset };
1140
1056
  }
1141
- function parseSimpleValue(raw) {
1142
- const trimmed = raw.trim();
1143
- if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
1144
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
1145
- return trimmed.slice(1, -1);
1057
+ function parseWorkspaceVisibilityConfig(fmBlock) {
1058
+ const blockStart = fmBlock.match(/^workspaceVisibility:\s*$/m);
1059
+ if (!blockStart) {
1060
+ return { hidden: [] };
1061
+ }
1062
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
1063
+ const remaining = fmBlock.slice(startIdx).split("\n");
1064
+ const hidden = [];
1065
+ let currentSection = null;
1066
+ for (const line of remaining) {
1067
+ const trimmed = line.trimStart();
1068
+ const indent = line.length - trimmed.length;
1069
+ if (indent === 0 && trimmed.length > 0) break;
1070
+ if (trimmed === "") continue;
1071
+ if (indent === 2 && trimmed.startsWith("hidden:")) {
1072
+ currentSection = "hidden";
1073
+ continue;
1074
+ }
1075
+ if (currentSection === "hidden" && indent >= 4 && trimmed.startsWith("- ")) {
1076
+ const rest = trimmed.slice(2).trim();
1077
+ if (rest.length === 0) continue;
1078
+ let name;
1079
+ if (rest.startsWith('"')) {
1080
+ try {
1081
+ name = JSON.parse(rest);
1082
+ } catch {
1083
+ name = rest.replace(/^["']|["']$/g, "");
1084
+ }
1085
+ } else {
1086
+ name = rest;
1087
+ }
1088
+ hidden.push(name);
1089
+ continue;
1090
+ }
1146
1091
  }
1147
- return trimmed;
1092
+ return { hidden: normalizeHiddenList(hidden) };
1148
1093
  }
1149
- function getField(frontmatter, key) {
1150
- const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
1094
+ function parseHotkeyBindingsConfig(content) {
1095
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1151
1096
  if (!match) return null;
1152
- return parseSimpleValue(match[1]);
1097
+ const fmBlock = match[1];
1098
+ const blockStart = fmBlock.match(/^hotkeys:\s*$/m);
1099
+ if (!blockStart) return null;
1100
+ const startIdx = fmBlock.indexOf(blockStart[0]) + blockStart[0].length;
1101
+ const remaining = fmBlock.slice(startIdx).split("\n");
1102
+ const bindings = {};
1103
+ let inBindings = false;
1104
+ for (const line of remaining) {
1105
+ const trimmed = line.trimStart();
1106
+ const indent = line.length - trimmed.length;
1107
+ if (indent === 0 && trimmed.length > 0) break;
1108
+ if (trimmed === "") continue;
1109
+ if (indent === 2 && trimmed === "bindings:") {
1110
+ inBindings = true;
1111
+ continue;
1112
+ }
1113
+ if (inBindings && indent === 4) {
1114
+ const colonIdx = trimmed.indexOf(":");
1115
+ if (colonIdx <= 0) continue;
1116
+ const rawKind = trimmed.slice(0, colonIdx).trim();
1117
+ const rawValue = trimmed.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
1118
+ if (!isBindableActionKind(rawKind)) continue;
1119
+ if (rawValue.length === 0) continue;
1120
+ bindings[rawKind] = canonicalizeCombo(rawValue);
1121
+ }
1122
+ }
1123
+ if (Object.keys(bindings).length === 0) return null;
1124
+ return { bindings };
1153
1125
  }
1154
- function getNestedField(frontmatter, parent, key) {
1155
- const parentRegex = new RegExp(`^${parent}:\\s*\\n((?:\\s+.*\\n?)*)`, "m");
1156
- const parentMatch = frontmatter.match(parentRegex);
1157
- if (!parentMatch) return null;
1158
- const block = parentMatch[1];
1159
- const fieldMatch = block.match(new RegExp(`^\\s+${key}:\\s*(.*)$`, "m"));
1160
- if (!fieldMatch) return null;
1161
- return parseSimpleValue(fieldMatch[1]);
1126
+ function parseOptionalAbsolutePath(value, fieldName) {
1127
+ if (!value) {
1128
+ return null;
1129
+ }
1130
+ const expanded = expandHome(String(value));
1131
+ if (!isAbsolute(expanded)) {
1132
+ console.warn(
1133
+ `Warning: config.md ${fieldName} is not an absolute path ("${value}"), ignoring it`
1134
+ );
1135
+ return null;
1136
+ }
1137
+ return resolve6(expanded);
1162
1138
  }
1163
- function parseListField(frontmatter, fieldName) {
1164
- const inlineMatch = frontmatter.match(new RegExp(`^${fieldName}:\\s*\\[\\s*\\]`, "m"));
1165
- if (inlineMatch) return [];
1166
- const results = [];
1167
- const blockMatch = frontmatter.match(
1168
- new RegExp(`^${fieldName}:\\s*\\n((?:\\s+-\\s+.*\\n?)*)`, "m")
1169
- );
1170
- if (blockMatch) {
1171
- let item;
1172
- const regex = /^\s+-\s+(.+)$/gm;
1173
- while ((item = regex.exec(blockMatch[1])) !== null) {
1174
- results.push(item[1].trim());
1139
+ function parseAgentsConfig(content) {
1140
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1141
+ if (!match) return null;
1142
+ const fmBlock = match[1];
1143
+ const agentsStart = fmBlock.match(/^agents:\s*$/m);
1144
+ if (!agentsStart) return null;
1145
+ const startIdx = fmBlock.indexOf(agentsStart[0]) + agentsStart[0].length;
1146
+ const remaining = fmBlock.slice(startIdx);
1147
+ const lines = remaining.split("\n");
1148
+ const agents = [];
1149
+ let current = null;
1150
+ let argsCapture = null;
1151
+ let argsBaseIndent = 0;
1152
+ let nestedKey = null;
1153
+ let nestedInvocation = null;
1154
+ let nestedBaseIndent = 0;
1155
+ function flushCurrent() {
1156
+ if (!current) return;
1157
+ if (!current.id || !current.command || !current.label) {
1158
+ current = null;
1159
+ return;
1175
1160
  }
1161
+ agents.push({
1162
+ id: current.id,
1163
+ label: current.label,
1164
+ command: current.command,
1165
+ ...current.args && current.args.length > 0 ? { args: current.args } : {},
1166
+ ...current.promptArgPosition ? { promptArgPosition: current.promptArgPosition } : {},
1167
+ ...current.default ? { default: true } : {},
1168
+ ...current.resolveFromShellAliases ? { resolveFromShellAliases: true } : {},
1169
+ ...current.resume ? { resume: current.resume } : {},
1170
+ ...current.fork ? { fork: current.fork } : {}
1171
+ });
1172
+ current = null;
1173
+ argsCapture = null;
1174
+ nestedKey = null;
1175
+ nestedInvocation = null;
1176
1176
  }
1177
- return results;
1178
- }
1179
- function unquoteYamlString(value) {
1180
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1181
- return value.slice(1, -1);
1177
+ function closeNestedBlock() {
1178
+ if (!nestedKey) return;
1179
+ if (current && (nestedKey === "resume" || nestedKey === "fork") && nestedInvocation) {
1180
+ if (Array.isArray(nestedInvocation.args)) {
1181
+ current[nestedKey] = nestedInvocation;
1182
+ }
1183
+ }
1184
+ nestedKey = null;
1185
+ nestedInvocation = null;
1186
+ argsCapture = null;
1182
1187
  }
1183
- return value;
1188
+ for (let i = 0; i < lines.length; i++) {
1189
+ const line = lines[i];
1190
+ const trimmed = line.trimStart();
1191
+ const indent = line.length - trimmed.length;
1192
+ if (indent === 0 && trimmed !== "" && !trimmed.startsWith("#")) {
1193
+ closeNestedBlock();
1194
+ break;
1195
+ }
1196
+ if (argsCapture) {
1197
+ if (indent > argsBaseIndent && trimmed.startsWith("- ")) {
1198
+ argsCapture.push(decodeYamlScalar(trimmed.slice(2).trim()));
1199
+ continue;
1200
+ } else {
1201
+ argsCapture = null;
1202
+ }
1203
+ }
1204
+ if (indent === 2 && trimmed.startsWith("- ")) {
1205
+ closeNestedBlock();
1206
+ flushCurrent();
1207
+ current = {};
1208
+ const rest = trimmed.slice(2).trim();
1209
+ const colonIdx = rest.indexOf(":");
1210
+ if (colonIdx > 0) {
1211
+ const k = rest.slice(0, colonIdx).trim();
1212
+ const v = rest.slice(colonIdx + 1).trim();
1213
+ assignAgentField(current, k, v);
1214
+ }
1215
+ continue;
1216
+ }
1217
+ if (!current) continue;
1218
+ if (nestedKey && indent > nestedBaseIndent) {
1219
+ const colonIdx = trimmed.indexOf(":");
1220
+ if (colonIdx <= 0) continue;
1221
+ const k = trimmed.slice(0, colonIdx).trim();
1222
+ const v = trimmed.slice(colonIdx + 1).trim();
1223
+ if (nestedKey === "resume" || nestedKey === "fork") {
1224
+ if (!nestedInvocation) nestedInvocation = { args: [] };
1225
+ if (k === "args" && v === "") {
1226
+ nestedInvocation.args = [];
1227
+ argsCapture = nestedInvocation.args;
1228
+ argsBaseIndent = indent;
1229
+ continue;
1230
+ }
1231
+ if (k === "command" && v !== "") {
1232
+ nestedInvocation.command = decodeYamlScalar(v);
1233
+ continue;
1234
+ }
1235
+ }
1236
+ continue;
1237
+ }
1238
+ if (nestedKey && indent <= nestedBaseIndent) {
1239
+ closeNestedBlock();
1240
+ }
1241
+ if (indent >= 4 && current) {
1242
+ const colonIdx = trimmed.indexOf(":");
1243
+ if (colonIdx <= 0) continue;
1244
+ const k = trimmed.slice(0, colonIdx).trim();
1245
+ const v = trimmed.slice(colonIdx + 1).trim();
1246
+ if (k === "args" && v === "") {
1247
+ argsCapture = [];
1248
+ argsBaseIndent = indent;
1249
+ current.args = argsCapture;
1250
+ continue;
1251
+ }
1252
+ if ((k === "resume" || k === "fork") && v === "") {
1253
+ nestedKey = k;
1254
+ nestedInvocation = { args: [] };
1255
+ nestedBaseIndent = indent;
1256
+ continue;
1257
+ }
1258
+ if (v === "" && !KNOWN_AGENT_SCALAR_FIELDS.has(k)) {
1259
+ nestedKey = "__skip__";
1260
+ nestedInvocation = null;
1261
+ nestedBaseIndent = indent;
1262
+ continue;
1263
+ }
1264
+ assignAgentField(current, k, v);
1265
+ }
1266
+ }
1267
+ closeNestedBlock();
1268
+ flushCurrent();
1269
+ if (agents.length === 0) return [];
1270
+ return agents;
1184
1271
  }
1185
- function parseProject(fileContent) {
1186
- const [fm, body] = extractFrontmatter(fileContent);
1187
- const slug = getField(fm, "slug") ?? getField(fm, "mission") ?? "";
1188
- return {
1189
- id: getField(fm, "id") ?? "",
1190
- slug,
1191
- title: getField(fm, "title") ?? "",
1192
- archived: getField(fm, "archived") === "true",
1193
- archivedAt: getField(fm, "archivedAt"),
1194
- archivedReason: getField(fm, "archivedReason"),
1195
- statusOverride: getField(fm, "statusOverride"),
1196
- created: getField(fm, "created") ?? "",
1197
- updated: getField(fm, "updated") ?? "",
1198
- tags: parseListField(fm, "tags"),
1199
- workspace: getField(fm, "workspace"),
1200
- repositories: parseListField(fm, "repositories").map(unquoteYamlString),
1201
- externalIds: parseExternalIds(fm),
1202
- body
1203
- };
1272
+ function normalizeAgentsFromConfig(agents) {
1273
+ if (agents === null) return null;
1274
+ try {
1275
+ const normalized = agents.map((agent) => ({
1276
+ ...agent,
1277
+ command: parseAgentCommand(agent.command, agent.id)
1278
+ }));
1279
+ validateAgentList(normalized);
1280
+ return normalized;
1281
+ } catch (err) {
1282
+ const msg = err instanceof Error ? err.message : String(err);
1283
+ console.warn(
1284
+ `Warning: ~/.syntaur/config.md agents block is invalid (${msg}) \u2014 using built-in defaults`
1285
+ );
1286
+ return null;
1287
+ }
1204
1288
  }
1205
- function parseStatus(fileContent) {
1206
- const [fm, body] = extractFrontmatter(fileContent);
1207
- const progress = { total: 0 };
1208
- const progressMatch = fm.match(/^progress:\s*\n((?:\s+.*\n?)*)/m);
1209
- if (progressMatch) {
1210
- const lines = progressMatch[1].split("\n");
1211
- for (const line of lines) {
1212
- const kv = line.match(/^\s+(\w+):\s*(\d+)/);
1213
- if (kv) {
1214
- progress[kv[1]] = parseInt(kv[2], 10);
1289
+ function decodeYamlScalar(value) {
1290
+ const trimmed = value.trim();
1291
+ if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
1292
+ const body = trimmed.slice(1, -1);
1293
+ let out = "";
1294
+ for (let i = 0; i < body.length; i++) {
1295
+ const ch = body[i];
1296
+ if (ch === "\\" && i + 1 < body.length) {
1297
+ const next = body[i + 1];
1298
+ switch (next) {
1299
+ case "\\":
1300
+ out += "\\";
1301
+ break;
1302
+ case '"':
1303
+ out += '"';
1304
+ break;
1305
+ case "n":
1306
+ out += "\n";
1307
+ break;
1308
+ case "t":
1309
+ out += " ";
1310
+ break;
1311
+ case "r":
1312
+ out += "\r";
1313
+ break;
1314
+ default:
1315
+ out += next;
1316
+ break;
1317
+ }
1318
+ i++;
1319
+ continue;
1215
1320
  }
1321
+ out += ch;
1216
1322
  }
1323
+ return out;
1217
1324
  }
1218
- return {
1219
- project: getField(fm, "project") ?? "",
1220
- status: getField(fm, "status") ?? "pending",
1221
- progress,
1222
- needsAttention: {
1223
- blockedCount: parseInt(getNestedField(fm, "needsAttention", "blockedCount") ?? "0", 10),
1224
- failedCount: parseInt(getNestedField(fm, "needsAttention", "failedCount") ?? "0", 10),
1225
- openQuestions: parseInt(getNestedField(fm, "needsAttention", "openQuestions") ?? "0", 10)
1226
- },
1227
- body
1228
- };
1325
+ if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
1326
+ return trimmed.slice(1, -1).replace(/''/g, "'");
1327
+ }
1328
+ return trimmed;
1229
1329
  }
1230
- function parseExternalIds(frontmatter) {
1231
- const inlineMatch = frontmatter.match(/^externalIds:\s*\[\s*\]/m);
1232
- if (inlineMatch) return [];
1233
- const results = [];
1234
- const blockMatch = frontmatter.match(
1235
- /^externalIds:\s*\n((?:\s+-\s+[\s\S]*?)(?=^\w|\n---))/m
1236
- );
1237
- if (!blockMatch) return [];
1238
- const itemBlocks = blockMatch[1].split(/\n\s+-\s+/).filter(Boolean);
1239
- for (const block of itemBlocks) {
1240
- const lines = block.split("\n");
1241
- const entry = {};
1242
- for (const line of lines) {
1243
- const colonIdx = line.indexOf(":");
1244
- if (colonIdx < 0) continue;
1245
- const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
1246
- if (!key) continue;
1247
- entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
1248
- }
1249
- if (entry["system"] && entry["id"]) {
1250
- results.push({
1251
- system: entry["system"],
1252
- id: entry["id"],
1253
- url: entry["url"] || null
1254
- });
1255
- }
1330
+ function assignAgentField(target, key, rawValue) {
1331
+ const value = decodeYamlScalar(rawValue);
1332
+ switch (key) {
1333
+ case "id":
1334
+ target.id = value;
1335
+ break;
1336
+ case "label":
1337
+ target.label = value;
1338
+ break;
1339
+ case "command":
1340
+ target.command = value;
1341
+ break;
1342
+ case "promptArgPosition":
1343
+ target.promptArgPosition = value;
1344
+ break;
1345
+ case "default":
1346
+ target.default = value === "true";
1347
+ break;
1348
+ case "resolveFromShellAliases":
1349
+ target.resolveFromShellAliases = value === "true";
1350
+ break;
1256
1351
  }
1257
- return results;
1258
1352
  }
1259
- function parseAssignmentFull(fileContent) {
1260
- const [fm, body] = extractFrontmatter(fileContent);
1353
+ async function readConfig() {
1354
+ const configPath = resolve6(syntaurRoot(), "config.md");
1355
+ if (!await fileExists(configPath)) {
1356
+ return cloneDefaultConfig();
1357
+ }
1358
+ if (!migratedConfigPaths.has(configPath)) {
1359
+ migratedConfigPaths.add(configPath);
1360
+ await migrateLegacyConfig(configPath);
1361
+ }
1362
+ const content = await readFile5(configPath, "utf-8");
1363
+ const fm = parseFrontmatter(content);
1364
+ if (Object.keys(fm).length === 0) {
1365
+ console.warn("Warning: ~/.syntaur/config.md has malformed frontmatter, using defaults");
1366
+ return cloneDefaultConfig();
1367
+ }
1368
+ let projectDir = fm["defaultProjectDir"] ? expandHome(String(fm["defaultProjectDir"])) : DEFAULT_CONFIG.defaultProjectDir;
1369
+ if (!isAbsolute(projectDir)) {
1370
+ console.warn(
1371
+ `Warning: config.md defaultProjectDir is not an absolute path ("${fm["defaultProjectDir"]}"), using default`
1372
+ );
1373
+ projectDir = DEFAULT_CONFIG.defaultProjectDir;
1374
+ }
1375
+ const fmBlock = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
1261
1376
  return {
1262
- id: getField(fm, "id") ?? "",
1263
- slug: getField(fm, "slug") ?? "",
1264
- title: getField(fm, "title") ?? "",
1265
- project: getField(fm, "project"),
1266
- workspaceGroup: getField(fm, "workspaceGroup"),
1267
- type: getField(fm, "type"),
1268
- status: getField(fm, "status") ?? "pending",
1269
- priority: getField(fm, "priority") ?? "medium",
1270
- assignee: getField(fm, "assignee"),
1271
- dependsOn: parseListField(fm, "dependsOn"),
1272
- links: parseListField(fm, "links"),
1273
- blockedReason: getField(fm, "blockedReason"),
1274
- workspace: {
1275
- repository: getNestedField(fm, "workspace", "repository"),
1276
- worktreePath: getNestedField(fm, "workspace", "worktreePath"),
1277
- branch: getNestedField(fm, "workspace", "branch"),
1278
- parentBranch: getNestedField(fm, "workspace", "parentBranch")
1377
+ version: fm["version"] || DEFAULT_CONFIG.version,
1378
+ defaultProjectDir: projectDir,
1379
+ onboarding: {
1380
+ completed: fm["onboarding.completed"] === "true"
1279
1381
  },
1280
- externalIds: parseExternalIds(fm),
1281
- tags: parseListField(fm, "tags"),
1282
- archived: getField(fm, "archived") === "true",
1283
- archivedAt: getField(fm, "archivedAt"),
1284
- archivedReason: getField(fm, "archivedReason"),
1285
- created: getField(fm, "created") ?? "",
1286
- updated: getField(fm, "updated") ?? "",
1287
- body
1288
- };
1289
- }
1290
- function parsePlan(fileContent) {
1291
- const [fm, body] = extractFrontmatter(fileContent);
1292
- return {
1293
- assignment: getField(fm, "assignment") ?? "",
1294
- status: getField(fm, "status") ?? "",
1295
- created: getField(fm, "created") ?? "",
1296
- updated: getField(fm, "updated") ?? "",
1297
- body
1298
- };
1299
- }
1300
- function parseScratchpad(fileContent) {
1301
- const [fm, body] = extractFrontmatter(fileContent);
1302
- return {
1303
- assignment: getField(fm, "assignment") ?? "",
1304
- updated: getField(fm, "updated") ?? "",
1305
- body
1306
- };
1307
- }
1308
- function parseHandoff(fileContent) {
1309
- const [fm, body] = extractFrontmatter(fileContent);
1310
- return {
1311
- assignment: getField(fm, "assignment") ?? "",
1312
- handoffCount: parseInt(getField(fm, "handoffCount") ?? "0", 10),
1313
- updated: getField(fm, "updated") ?? "",
1314
- body
1382
+ agentDefaults: {
1383
+ trustLevel: fm["agentDefaults.trustLevel"] || DEFAULT_CONFIG.agentDefaults.trustLevel,
1384
+ autoApprove: fm["agentDefaults.autoApprove"] === "true" || DEFAULT_CONFIG.agentDefaults.autoApprove,
1385
+ autoCreateWorktree: AUTO_CREATE_WORKTREE_VALUES.includes(
1386
+ fm["agentDefaults.autoCreateWorktree"]
1387
+ ) ? fm["agentDefaults.autoCreateWorktree"] : DEFAULT_CONFIG.agentDefaults.autoCreateWorktree
1388
+ },
1389
+ integrations: {
1390
+ claudePluginDir: parseOptionalAbsolutePath(
1391
+ fm["integrations.claudePluginDir"],
1392
+ "integrations.claudePluginDir"
1393
+ ),
1394
+ codexPluginDir: parseOptionalAbsolutePath(
1395
+ fm["integrations.codexPluginDir"],
1396
+ "integrations.codexPluginDir"
1397
+ ),
1398
+ codexMarketplacePath: parseOptionalAbsolutePath(
1399
+ fm["integrations.codexMarketplacePath"],
1400
+ "integrations.codexMarketplacePath"
1401
+ ),
1402
+ ...parseInstalledAgents(fm)
1403
+ },
1404
+ backup: fm["backup.repo"] || fm["backup.categories"] ? {
1405
+ repo: fm["backup.repo"] && fm["backup.repo"] !== "null" ? fm["backup.repo"] : null,
1406
+ categories: fm["backup.categories"] || "projects, playbooks, todos, servers, config",
1407
+ lastBackup: fm["backup.lastBackup"] && fm["backup.lastBackup"] !== "null" ? fm["backup.lastBackup"] : null,
1408
+ lastRestore: fm["backup.lastRestore"] && fm["backup.lastRestore"] !== "null" ? fm["backup.lastRestore"] : null
1409
+ } : null,
1410
+ statuses: parseStatusConfig(content),
1411
+ types: null,
1412
+ agents: normalizeAgentsFromConfig(parseAgentsConfig(content)),
1413
+ playbooks: parsePlaybooksConfig(fmBlock),
1414
+ theme: parseThemeConfig(content),
1415
+ hotkeys: parseHotkeyBindingsConfig(content),
1416
+ terminal: (() => {
1417
+ try {
1418
+ return parseTerminalConfig(fm["terminal"]);
1419
+ } catch (err) {
1420
+ const msg = err instanceof TerminalConfigError ? err.message : String(err);
1421
+ console.warn(`Warning: ${msg} \u2014 falling back to default`);
1422
+ return null;
1423
+ }
1424
+ })(),
1425
+ workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock)
1315
1426
  };
1316
1427
  }
1317
- function parseDecisionRecord(fileContent) {
1318
- const [fm, body] = extractFrontmatter(fileContent);
1319
- return {
1320
- assignment: getField(fm, "assignment") ?? "",
1321
- decisionCount: parseInt(getField(fm, "decisionCount") ?? "0", 10),
1322
- updated: getField(fm, "updated") ?? "",
1323
- body
1324
- };
1428
+ function getAgents(config) {
1429
+ if (config.agents === null) return BUILTIN_AGENTS;
1430
+ const builtinById = new Map(BUILTIN_AGENTS.map((a) => [a.id, a]));
1431
+ return config.agents.map((agent) => {
1432
+ const builtin = builtinById.get(agent.id);
1433
+ if (!builtin) return agent;
1434
+ const resume = agent.resume ?? builtin.resume;
1435
+ const fork = agent.fork ?? builtin.fork;
1436
+ if (resume === agent.resume && fork === agent.fork) return agent;
1437
+ return {
1438
+ ...agent,
1439
+ ...resume ? { resume } : {},
1440
+ ...fork ? { fork } : {}
1441
+ };
1442
+ });
1325
1443
  }
1326
- function parseComments(fileContent) {
1327
- const [fm, body] = extractFrontmatter(fileContent);
1328
- const entries = [];
1329
- const sections = body.split(/^## /m).slice(1);
1330
- for (const section of sections) {
1331
- const newlineIdx = section.indexOf("\n");
1332
- if (newlineIdx === -1) continue;
1333
- const id = section.slice(0, newlineIdx).trim();
1334
- const rest = section.slice(newlineIdx + 1);
1335
- const headerMatch = rest.match(
1336
- /^\s*\*\*Recorded:\*\*\s*(.*)\n\*\*Author:\*\*\s*(.*)\n\*\*Type:\*\*\s*(question|note|feedback)(?:\n\*\*Reply to:\*\*\s*(.*))?(?:\n\*\*Resolved:\*\*\s*(true|false))?\n+([\s\S]*)$/
1444
+ function parseTerminalConfig(value) {
1445
+ if (value === void 0 || value === null || value === "") return null;
1446
+ if (typeof value !== "string") {
1447
+ throw new TerminalConfigError(
1448
+ `terminal must be a string \u2014 got ${typeof value}`
1449
+ );
1450
+ }
1451
+ const trimmed = value.trim();
1452
+ if (trimmed === "") return null;
1453
+ if (!TERMINAL_CHOICES.includes(trimmed)) {
1454
+ throw new TerminalConfigError(
1455
+ `terminal "${trimmed}" is not a known choice \u2014 expected one of ${TERMINAL_CHOICES.join("|")}`
1337
1456
  );
1338
- if (!headerMatch) continue;
1339
- const [, timestamp, author, type, replyTo, resolvedStr, entryBody] = headerMatch;
1340
- const entry = {
1341
- id,
1342
- timestamp: timestamp.trim(),
1343
- author: author.trim(),
1344
- type,
1345
- body: entryBody.trim()
1346
- };
1347
- if (replyTo) entry.replyTo = replyTo.trim();
1348
- if (resolvedStr) entry.resolved = resolvedStr === "true";
1349
- entries.push(entry);
1350
1457
  }
1351
- return {
1352
- assignment: getField(fm, "assignment") ?? "",
1353
- entryCount: parseInt(getField(fm, "entryCount") ?? "0", 10),
1354
- updated: getField(fm, "updated") ?? "",
1355
- entries,
1356
- body
1357
- };
1458
+ return trimmed;
1358
1459
  }
1359
- function parseProgress(fileContent) {
1360
- const [fm, body] = extractFrontmatter(fileContent);
1361
- const entries = [];
1362
- const sections = body.split(/^## /m).slice(1);
1363
- for (const section of sections) {
1364
- const newlineIdx = section.indexOf("\n");
1365
- if (newlineIdx === -1) continue;
1366
- const timestamp = section.slice(0, newlineIdx).trim();
1367
- const entryBody = section.slice(newlineIdx + 1).trim();
1368
- entries.push({ timestamp, body: entryBody });
1460
+ function getTerminal(config) {
1461
+ if (config.terminal) return config.terminal;
1462
+ if (process.platform === "darwin") return "terminal-app";
1463
+ if (process.platform === "linux") {
1464
+ const order = ["kitty", "alacritty", "warp"];
1465
+ for (const candidate of order) {
1466
+ const result = spawnSync("which", [candidate], { encoding: "utf-8" });
1467
+ if (result.status === 0 && result.stdout.trim().length > 0) {
1468
+ return candidate;
1469
+ }
1470
+ }
1369
1471
  }
1370
- return {
1371
- assignment: getField(fm, "assignment") ?? "",
1372
- entryCount: parseInt(getField(fm, "entryCount") ?? "0", 10),
1373
- updated: getField(fm, "updated") ?? "",
1374
- entries,
1375
- body
1376
- };
1377
- }
1378
- function extractMermaidGraph(body) {
1379
- const match = body.match(/```mermaid\n([\s\S]*?)```/);
1380
- return match ? match[1].trim() : null;
1472
+ return "terminal-app";
1381
1473
  }
1382
- var init_parser = __esm({
1383
- "src/dashboard/parser.ts"() {
1474
+ var DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
1475
+ var init_config2 = __esm({
1476
+ "src/utils/config.ts"() {
1384
1477
  "use strict";
1478
+ init_paths();
1479
+ init_fs();
1480
+ init_config();
1481
+ init_fs_migration();
1482
+ init_lifecycle();
1483
+ init_hotkeysCatalog();
1484
+ init_agents_schema();
1485
+ init_terminal_schema();
1486
+ init_workspace_visibility_schema();
1487
+ DEFAULT_CONFIG = {
1488
+ version: "2.0",
1489
+ defaultProjectDir: defaultProjectDir(),
1490
+ onboarding: {
1491
+ completed: false
1492
+ },
1493
+ agentDefaults: {
1494
+ trustLevel: "medium",
1495
+ autoApprove: false,
1496
+ autoCreateWorktree: "ask"
1497
+ },
1498
+ integrations: {
1499
+ claudePluginDir: null,
1500
+ codexPluginDir: null,
1501
+ codexMarketplacePath: null
1502
+ },
1503
+ backup: null,
1504
+ statuses: null,
1505
+ types: null,
1506
+ agents: null,
1507
+ playbooks: {
1508
+ disabled: []
1509
+ },
1510
+ theme: null,
1511
+ hotkeys: null,
1512
+ terminal: null,
1513
+ workspaceVisibility: {
1514
+ hidden: []
1515
+ }
1516
+ };
1517
+ AUTO_CREATE_WORKTREE_VALUES = ["skip", "ask", "always"];
1518
+ AgentConfigError = class extends Error {
1519
+ };
1520
+ DEFAULT_STATUS_COLORS = {
1521
+ pending: "slate",
1522
+ in_progress: "teal",
1523
+ blocked: "amber",
1524
+ review: "violet",
1525
+ completed: "emerald",
1526
+ failed: "rose"
1527
+ };
1528
+ KNOWN_AGENT_SCALAR_FIELDS = /* @__PURE__ */ new Set([
1529
+ "id",
1530
+ "label",
1531
+ "command",
1532
+ "promptArgPosition",
1533
+ "default",
1534
+ "resolveFromShellAliases"
1535
+ ]);
1536
+ migratedConfigPaths = /* @__PURE__ */ new Set();
1537
+ TerminalConfigError = class extends Error {
1538
+ };
1385
1539
  }
1386
1540
  });
1387
1541
 
1388
1542
  // src/utils/assignment-resolver.ts
1389
- import { resolve as resolve4 } from "path";
1390
- import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
1543
+ import { resolve as resolve7 } from "path";
1544
+ import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
1391
1545
  async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
1392
1546
  let standaloneMatch = null;
1393
1547
  let projectMatch = null;
1394
- const standaloneDir = resolve4(assignmentsDir, id);
1395
- const standalonePath = resolve4(standaloneDir, "assignment.md");
1548
+ const standaloneDir = resolve7(assignmentsDir, id);
1549
+ const standalonePath = resolve7(standaloneDir, "assignment.md");
1396
1550
  if (await fileExists(standalonePath)) {
1397
1551
  let workspaceGroup = null;
1398
1552
  try {
1399
- const content = await readFile4(standalonePath, "utf-8");
1553
+ const content = await readFile6(standalonePath, "utf-8");
1400
1554
  const [fm] = extractFrontmatter(content);
1401
1555
  workspaceGroup = getField(fm, "workspaceGroup");
1402
1556
  } catch {
@@ -1412,24 +1566,24 @@ async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
1412
1566
  }
1413
1567
  if (await fileExists(projectsDir)) {
1414
1568
  try {
1415
- const projects = await readdir2(projectsDir, { withFileTypes: true });
1569
+ const projects = await readdir3(projectsDir, { withFileTypes: true });
1416
1570
  for (const p of projects) {
1417
1571
  if (!p.isDirectory()) continue;
1418
1572
  if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
1419
- const assignmentsPath = resolve4(projectsDir, p.name, "assignments");
1573
+ const assignmentsPath = resolve7(projectsDir, p.name, "assignments");
1420
1574
  if (!await fileExists(assignmentsPath)) continue;
1421
- const entries = await readdir2(assignmentsPath, { withFileTypes: true });
1575
+ const entries = await readdir3(assignmentsPath, { withFileTypes: true });
1422
1576
  for (const a of entries) {
1423
1577
  if (!a.isDirectory()) continue;
1424
- const aPath = resolve4(assignmentsPath, a.name, "assignment.md");
1578
+ const aPath = resolve7(assignmentsPath, a.name, "assignment.md");
1425
1579
  if (!await fileExists(aPath)) continue;
1426
1580
  try {
1427
- const content = await readFile4(aPath, "utf-8");
1581
+ const content = await readFile6(aPath, "utf-8");
1428
1582
  const [fm] = extractFrontmatter(content);
1429
1583
  const fileId = getField(fm, "id");
1430
1584
  if (fileId === id) {
1431
1585
  projectMatch = {
1432
- assignmentDir: resolve4(assignmentsPath, a.name),
1586
+ assignmentDir: resolve7(assignmentsPath, a.name),
1433
1587
  projectSlug: p.name,
1434
1588
  assignmentSlug: a.name,
1435
1589
  id,
@@ -1462,133 +1616,6 @@ var init_assignment_resolver = __esm({
1462
1616
  }
1463
1617
  });
1464
1618
 
1465
- // src/lifecycle/types.ts
1466
- var DEFAULT_STATUSES;
1467
- var init_types = __esm({
1468
- "src/lifecycle/types.ts"() {
1469
- "use strict";
1470
- DEFAULT_STATUSES = [
1471
- "draft",
1472
- "pending",
1473
- "ready_for_planning",
1474
- "ready_to_implement",
1475
- "in_progress",
1476
- "blocked",
1477
- "review",
1478
- "completed",
1479
- "failed"
1480
- ];
1481
- }
1482
- });
1483
-
1484
- // src/lifecycle/state-machine.ts
1485
- function buildTransitionTable(transitions) {
1486
- const table = /* @__PURE__ */ new Map();
1487
- for (const t of transitions) {
1488
- table.set(`${t.from}:${t.command}`, t.to);
1489
- }
1490
- return table;
1491
- }
1492
- function getTargetStatus(_from, command, table) {
1493
- if (!table) {
1494
- return DEFAULT_COMMAND_TARGETS.get(command) ?? null;
1495
- }
1496
- return table.get(command) ?? table.get(`${_from}:${command}`) ?? null;
1497
- }
1498
- var DEFAULT_COMMAND_TARGETS, DEFAULT_TRANSITION_TABLE;
1499
- var init_state_machine = __esm({
1500
- "src/lifecycle/state-machine.ts"() {
1501
- "use strict";
1502
- init_types();
1503
- DEFAULT_COMMAND_TARGETS = /* @__PURE__ */ new Map([
1504
- ["start", "in_progress"],
1505
- ["shape", "ready_for_planning"],
1506
- ["plan-ready", "ready_to_implement"],
1507
- ["implement", "in_progress"],
1508
- ["block", "blocked"],
1509
- ["unblock", "in_progress"],
1510
- ["review", "review"],
1511
- ["complete", "completed"],
1512
- ["fail", "failed"],
1513
- ["reopen", "in_progress"]
1514
- ]);
1515
- DEFAULT_TRANSITION_TABLE = /* @__PURE__ */ new Map([
1516
- ["pending:start", "in_progress"],
1517
- ["pending:block", "blocked"],
1518
- ["draft:shape", "ready_for_planning"],
1519
- ["draft:start", "in_progress"],
1520
- ["ready_for_planning:plan-ready", "ready_to_implement"],
1521
- ["ready_for_planning:start", "in_progress"],
1522
- ["ready_to_implement:implement", "in_progress"],
1523
- ["in_progress:block", "blocked"],
1524
- ["in_progress:review", "review"],
1525
- ["in_progress:complete", "completed"],
1526
- ["in_progress:fail", "failed"],
1527
- ["blocked:unblock", "in_progress"],
1528
- ["review:start", "in_progress"],
1529
- ["review:complete", "completed"],
1530
- ["review:fail", "failed"],
1531
- ["completed:reopen", "in_progress"],
1532
- ["failed:reopen", "in_progress"]
1533
- ]);
1534
- }
1535
- });
1536
-
1537
- // src/lifecycle/frontmatter.ts
1538
- var init_frontmatter = __esm({
1539
- "src/lifecycle/frontmatter.ts"() {
1540
- "use strict";
1541
- }
1542
- });
1543
-
1544
- // src/todos/parser.ts
1545
- import { randomBytes } from "crypto";
1546
- import { readFile as readFile5 } from "fs/promises";
1547
- import { resolve as resolve5 } from "path";
1548
- var init_parser2 = __esm({
1549
- "src/todos/parser.ts"() {
1550
- "use strict";
1551
- init_parser();
1552
- init_fs();
1553
- }
1554
- });
1555
-
1556
- // src/lifecycle/linked-todos.ts
1557
- import { readdir as readdir3 } from "fs/promises";
1558
- import { resolve as resolve6 } from "path";
1559
- var init_linked_todos = __esm({
1560
- "src/lifecycle/linked-todos.ts"() {
1561
- "use strict";
1562
- init_parser2();
1563
- init_fs();
1564
- }
1565
- });
1566
-
1567
- // src/lifecycle/transitions.ts
1568
- import { resolve as resolve7 } from "path";
1569
- import { readFile as readFile6 } from "fs/promises";
1570
- var init_transitions = __esm({
1571
- "src/lifecycle/transitions.ts"() {
1572
- "use strict";
1573
- init_fs();
1574
- init_timestamp();
1575
- init_state_machine();
1576
- init_frontmatter();
1577
- init_linked_todos();
1578
- }
1579
- });
1580
-
1581
- // src/lifecycle/index.ts
1582
- var init_lifecycle = __esm({
1583
- "src/lifecycle/index.ts"() {
1584
- "use strict";
1585
- init_types();
1586
- init_state_machine();
1587
- init_frontmatter();
1588
- init_transitions();
1589
- }
1590
- });
1591
-
1592
1619
  // src/utils/slug.ts
1593
1620
  var init_slug = __esm({
1594
1621
  "src/utils/slug.ts"() {
@@ -1839,7 +1866,8 @@ function rowToSession(row) {
1839
1866
  description: row.description ?? null,
1840
1867
  transcriptPath: row.transcript_path ?? null,
1841
1868
  pid: row.pid ?? null,
1842
- pidStartedAt: row.pid_started_at ?? null
1869
+ pidStartedAt: row.pid_started_at ?? null,
1870
+ originalHeadSha: row.original_head_sha ?? null
1843
1871
  };
1844
1872
  }
1845
1873
  function getSessionById(sessionId) {
@@ -1901,9 +1929,6 @@ async function computeStandaloneRecords(assignmentsDir) {
1901
1929
  records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));
1902
1930
  return records;
1903
1931
  }
1904
- function toTitleCase(s) {
1905
- return s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1906
- }
1907
1932
  function getTransitionDefinitions(config) {
1908
1933
  if (!config.custom) return DEFAULT_TRANSITION_DEFINITIONS;
1909
1934
  const seen = /* @__PURE__ */ new Set();
@@ -1939,19 +1964,12 @@ async function getStatusConfig() {
1939
1964
  terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
1940
1965
  };
1941
1966
  } else {
1967
+ const def = buildDefaultStatusConfig();
1942
1968
  _cachedConfig = {
1943
1969
  custom: false,
1944
- statuses: DEFAULT_STATUSES.map((id) => ({
1945
- id,
1946
- label: toTitleCase(id),
1947
- color: DEFAULT_STATUS_COLORS[id] ?? "gray",
1948
- terminal: id === "completed" || id === "failed"
1949
- })),
1950
- order: [...DEFAULT_STATUSES],
1951
- transitions: Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
1952
- const [from, command] = key.split(":");
1953
- return { from, command, to };
1954
- }),
1970
+ statuses: def.statuses,
1971
+ order: def.order,
1972
+ transitions: def.transitions,
1955
1973
  transitionTable: DEFAULT_TRANSITION_TABLE,
1956
1974
  terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
1957
1975
  };
@@ -2620,7 +2638,7 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
2620
2638
  }
2621
2639
  return latest;
2622
2640
  }
2623
- var STALE_ASSIGNMENT_MS, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
2641
+ var STALE_ASSIGNMENT_MS, projectRecordsCache, standaloneRecordsCache, DEFAULT_TRANSITION_DEFINITIONS, _cachedConfig, REFERENCED_BY_LIMIT, migratedProjectsDirs, DEFAULT_GRAPH_COLORS;
2624
2642
  var init_api = __esm({
2625
2643
  "src/dashboard/api.ts"() {
2626
2644
  "use strict";
@@ -2700,14 +2718,6 @@ var init_api = __esm({
2700
2718
  requiresReason: false
2701
2719
  }
2702
2720
  ];
2703
- DEFAULT_STATUS_COLORS = {
2704
- pending: "slate",
2705
- in_progress: "teal",
2706
- blocked: "amber",
2707
- review: "violet",
2708
- completed: "emerald",
2709
- failed: "rose"
2710
- };
2711
2721
  _cachedConfig = null;
2712
2722
  REFERENCED_BY_LIMIT = 50;
2713
2723
  migratedProjectsDirs = /* @__PURE__ */ new Set();