syntaur 0.41.2 → 0.42.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 (68) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-CMGil-NY.js → _basePickBy-Cd0RkcLT.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-DllyUaEJ.js → _baseUniq-DcVRMSTl.js} +1 -1
  4. package/dashboard/dist/assets/{arc-C6fNP_LJ.js → arc-B2m30WX5.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CxXDnbMY.js → architectureDiagram-2XIMDMQ5-CJdPqqWS.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-B8UDJhxg.js → blockDiagram-WCTKOSBZ-BoThc9ue.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-9XDZP3AD.js → c4Diagram-IC4MRINW-DcLdX7Gp.js} +1 -1
  8. package/dashboard/dist/assets/channel-TK3AY7tt.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-D1LR7D9Y.js → chunk-4BX2VUAB-DeyTroVn.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-sumE5d0X.js → chunk-55IACEB6-Pk1kQHZ3.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-C-Iy8wke.js → chunk-FMBD7UC4-DFoS6k4t.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-Clyrcmzt.js → chunk-JSJVCQXG-DN22e0xM.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-BQqetgrP.js → chunk-KX2RTZJC-MNrdiNWF.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-Cw60fnx2.js → chunk-NQ4KR5QH-C0k2CIP7.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-Dv40SU2-.js → chunk-QZHKN3VN-C32xUlPx.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-DFiOufrs.js → chunk-WL4C6EOR-DB7YEwdA.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BcpVoyRF.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BcpVoyRF.js +1 -0
  19. package/dashboard/dist/assets/clone-tv-jxopI.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-DV306SRn.js → cose-bilkent-S5V4N54A-CMH4iDpK.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-DaQ1pWLV.js → dagre-KLK3FWXG-DdHflfb4.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-2fsjMT-T.js → diagram-E7M64L7V-DtScFCCN.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-CoaSyKLw.js → diagram-IFDJBPK2-DqZgC_98.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-C_j6Kd6q.js → diagram-P4PSJMXO-BIjzlVHf.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-CpOdYJWS.js → erDiagram-INFDFZHY-B_v2XAqY.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-KVRjmhbG.js → flowDiagram-PKNHOUZH-BIsbt9TK.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CA_n5ynk.js → ganttDiagram-A5KZAMGK-3wW3k6UM.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-DKkS_iH8.js → gitGraphDiagram-K3NZZRJ6-J5jG9Qum.js} +1 -1
  29. package/dashboard/dist/assets/{graph-C6ehraTW.js → graph-6IHp6W8J.js} +1 -1
  30. package/dashboard/dist/assets/{index-CdHziP5R.css → index-6uihSopA.css} +1 -1
  31. package/dashboard/dist/assets/{index-SW4WrQLg.js → index-BfWuhZd9.js} +59 -59
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-H1Eg4YK9.js → infoDiagram-LFFYTUFH-BeUnIF7J.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DSrc4sub.js → ishikawaDiagram-PHBUUO56-CBMlrqiK.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-Bl_0LgIo.js → journeyDiagram-4ABVD52K-C25hSiks.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Cq2WGyif.js → kanban-definition-K7BYSVSG-DoJVBKAf.js} +1 -1
  36. package/dashboard/dist/assets/{layout-DJv9vite.js → layout-rmqkK4ql.js} +1 -1
  37. package/dashboard/dist/assets/{linear-CAef3hQD.js → linear-Dcvh5pG3.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-B_gAmtAa.js → mermaid.core-BAS-3wuz.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-4aIWu_CK.js → mindmap-definition-YRQLILUH-Dvs67C76.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-1ThATMqf.js → pieDiagram-SKSYHLDU-DsVBwJs_.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BEq2jVyN.js → quadrantDiagram-337W2JSQ-C5dAkCkr.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-DbYJrAQ9.js → requirementDiagram-Z7DCOOCP-DEqcg6A2.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DMr3kn8l.js → sankeyDiagram-WA2Y5GQK-CvrgIFyY.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BR03-l-y.js → sequenceDiagram-2WXFIKYE-Bu6tanJS.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DUj-dVll.js → stateDiagram-RAJIS63D-D16mva7g.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-XMWsEM8j.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DpN8jElm.js → timeline-definition-YZTLITO2-BcLXDlbF.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-CyUTDKiM.js → treemap-KZPCXAKY-BuA9iiXV.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-DRJFiQmT.js → vennDiagram-LZ73GAT5-CYNPHeLe.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DcrZVnQ-.js → xychartDiagram-JWTSCODW-BJ4lD-Yr.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +2412 -420
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +3783 -966
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +33 -0
  57. package/dist/launch/index.js +1949 -70
  58. package/dist/launch/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  61. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  62. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  63. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  64. package/dashboard/dist/assets/channel-OsoeK3Lk.js +0 -1
  65. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BKX6nUBp.js +0 -1
  66. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BKX6nUBp.js +0 -1
  67. package/dashboard/dist/assets/clone-f-TTh9ms.js +0 -1
  68. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Dzzbhq6b.js +0 -1
@@ -1,7 +1,12 @@
1
+ var __defProp = Object.defineProperty;
1
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
2
3
  var __esm = (fn, res) => function __init() {
3
4
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
5
  };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
5
10
 
6
11
  // src/utils/terminal-schema.ts
7
12
  var TERMINAL_CHOICES;
@@ -76,6 +81,27 @@ var init_fs = __esm({
76
81
  });
77
82
 
78
83
  // src/templates/config.ts
84
+ function renderConfig(params) {
85
+ return `---
86
+ version: "1.0"
87
+ defaultProjectDir: ${params.defaultProjectDir}
88
+ onboarding:
89
+ completed: false
90
+ agentDefaults:
91
+ trustLevel: medium
92
+ autoApprove: false
93
+ backup:
94
+ repo: null
95
+ categories: projects, playbooks, todos, servers, config
96
+ lastBackup: null
97
+ lastRestore: null
98
+ ---
99
+
100
+ # Syntaur Configuration
101
+
102
+ Global configuration for the Syntaur CLI.
103
+ `;
104
+ }
79
105
  var init_config = __esm({
80
106
  "src/templates/config.ts"() {
81
107
  "use strict";
@@ -464,6 +490,51 @@ function parseExternalIds(frontmatter) {
464
490
  }
465
491
  return results;
466
492
  }
493
+ function parseStatusHistory(frontmatter) {
494
+ if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
495
+ const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
496
+ if (!headerMatch) return [];
497
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
498
+ const bodyStart = headerStart + headerMatch[0].length + 1;
499
+ const after = frontmatter.slice(bodyStart);
500
+ const bodyLines = [];
501
+ for (const line of after.split("\n")) {
502
+ if (line.length === 0) {
503
+ bodyLines.push(line);
504
+ continue;
505
+ }
506
+ if (line[0] !== " " && line[0] !== " ") break;
507
+ bodyLines.push(line);
508
+ }
509
+ const body = bodyLines.join("\n");
510
+ const results = [];
511
+ const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
512
+ for (const block of itemBlocks) {
513
+ const entry = {};
514
+ for (const line of block.split("\n")) {
515
+ const colonIdx = line.indexOf(":");
516
+ if (colonIdx < 0) continue;
517
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
518
+ if (!key) continue;
519
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
520
+ }
521
+ if (!entry["to"]) continue;
522
+ const result = {
523
+ at: entry["at"] ?? "",
524
+ from: entry["from"] ?? null,
525
+ to: entry["to"],
526
+ command: entry["command"] ?? "",
527
+ by: entry["by"] ?? null
528
+ };
529
+ if (entry["reason"] != null) result.reason = entry["reason"];
530
+ if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
531
+ if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
532
+ if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
533
+ if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
534
+ results.push(result);
535
+ }
536
+ return results;
537
+ }
467
538
  function parseAssignmentFull(fileContent) {
468
539
  const [fm, body] = extractFrontmatter(fileContent);
469
540
  return {
@@ -486,13 +557,40 @@ function parseAssignmentFull(fileContent) {
486
557
  parentBranch: getNestedField(fm, "workspace", "parentBranch")
487
558
  },
488
559
  externalIds: parseExternalIds(fm),
560
+ statusHistory: parseStatusHistory(fm),
489
561
  tags: parseListField(fm, "tags"),
490
562
  archived: getField(fm, "archived") === "true",
491
563
  archivedAt: getField(fm, "archivedAt"),
492
564
  archivedReason: getField(fm, "archivedReason"),
493
565
  created: getField(fm, "created") ?? "",
494
566
  updated: getField(fm, "updated") ?? "",
495
- body
567
+ body,
568
+ phase: getField(fm, "phase"),
569
+ disposition: getField(fm, "disposition"),
570
+ parked: getField(fm, "parked") === "true",
571
+ reviewRequested: getField(fm, "reviewRequested") === "true",
572
+ implementationStarted: getField(fm, "implementationStarted") === "true",
573
+ planApproval: (() => {
574
+ const file = getNestedField(fm, "planApproval", "file");
575
+ const digest = getNestedField(fm, "planApproval", "digest");
576
+ if (!file || !digest) return null;
577
+ return {
578
+ file,
579
+ digest,
580
+ by: getNestedField(fm, "planApproval", "by"),
581
+ at: getNestedField(fm, "planApproval", "at") ?? ""
582
+ };
583
+ })(),
584
+ override: (() => {
585
+ const status = getNestedField(fm, "override", "status");
586
+ if (!status) return null;
587
+ return {
588
+ status,
589
+ source: getNestedField(fm, "override", "source") ?? "human",
590
+ reason: getNestedField(fm, "override", "reason"),
591
+ at: getNestedField(fm, "override", "at") ?? ""
592
+ };
593
+ })()
496
594
  };
497
595
  }
498
596
  function parsePlan(fileContent) {
@@ -675,7 +773,12 @@ function canonicalizeCombo(input) {
675
773
  }
676
774
  return [...ordered, key].join("+");
677
775
  }
678
- var BINDABLE_ACTION_KINDS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
776
+ function isReservedCombo(combo) {
777
+ const c = canonicalizeCombo(combo);
778
+ if (!c) return false;
779
+ return BUILTIN_RESERVED_COMBOS.includes(c);
780
+ }
781
+ var BINDABLE_ACTION_KINDS, BUILTIN_RESERVED_COMBOS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
679
782
  var init_hotkeysCatalog = __esm({
680
783
  "src/utils/hotkeysCatalog.ts"() {
681
784
  "use strict";
@@ -685,6 +788,40 @@ var init_hotkeysCatalog = __esm({
685
788
  "new-todo",
686
789
  "new-assignment"
687
790
  ];
791
+ BUILTIN_RESERVED_COMBOS = [
792
+ "mod+k",
793
+ "mod+shift+k",
794
+ "?",
795
+ "escape",
796
+ "enter",
797
+ "shift+t",
798
+ // g-chord starter + suffixes
799
+ "g",
800
+ "g o",
801
+ "g m",
802
+ "g a",
803
+ "g t",
804
+ "g s",
805
+ "g !",
806
+ "g ,",
807
+ // list-scope navigation
808
+ "j",
809
+ "k",
810
+ "o",
811
+ // ProjectDetail page
812
+ "a",
813
+ "e",
814
+ // AssignmentsPage board
815
+ "/",
816
+ "r",
817
+ // AssignmentDetail page
818
+ "p",
819
+ "h",
820
+ "d",
821
+ "s",
822
+ "[",
823
+ "]"
824
+ ];
688
825
  MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"];
689
826
  DEFAULT_BINDABLE_HOTKEYS = {
690
827
  "new-workspace": canonicalizeCombo("Mod+Shift+Alt+w"),
@@ -808,6 +945,47 @@ var init_workspace_visibility_schema = __esm({
808
945
  });
809
946
 
810
947
  // src/utils/config.ts
948
+ var config_exports = {};
949
+ __export(config_exports, {
950
+ AGENT_ID_PATTERN: () => AGENT_ID_PATTERN,
951
+ AgentConfigError: () => AgentConfigError,
952
+ BUILTIN_AGENTS: () => BUILTIN_AGENTS,
953
+ DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
954
+ DEFAULT_DERIVE_CONFIG: () => DEFAULT_DERIVE_CONFIG,
955
+ DEFAULT_STATUS_COLORS: () => DEFAULT_STATUS_COLORS,
956
+ PROMPT_ARG_POSITIONS: () => PROMPT_ARG_POSITIONS,
957
+ TERMINAL_CHOICES: () => TERMINAL_CHOICES,
958
+ TerminalConfigError: () => TerminalConfigError,
959
+ buildDefaultStatusConfig: () => buildDefaultStatusConfig,
960
+ deleteAgentsConfig: () => deleteAgentsConfig,
961
+ deleteHotkeyBindingsConfig: () => deleteHotkeyBindingsConfig,
962
+ deleteStatusConfig: () => deleteStatusConfig,
963
+ deleteTerminalConfig: () => deleteTerminalConfig,
964
+ deleteThemeConfig: () => deleteThemeConfig,
965
+ deleteWorkspaceVisibilityConfig: () => deleteWorkspaceVisibilityConfig,
966
+ getAgents: () => getAgents,
967
+ getAssignmentTypes: () => getAssignmentTypes,
968
+ getTerminal: () => getTerminal,
969
+ parseAgentCommand: () => parseAgentCommand,
970
+ parseStatusConfig: () => parseStatusConfig,
971
+ parseTerminalConfig: () => parseTerminalConfig,
972
+ readConfig: () => readConfig,
973
+ serializeStatusConfig: () => serializeStatusConfig,
974
+ toTitleCase: () => toTitleCase,
975
+ updateAgentsConfig: () => updateAgentsConfig,
976
+ updateBackupConfig: () => updateBackupConfig,
977
+ updateIntegrationConfig: () => updateIntegrationConfig,
978
+ updateOnboardingConfig: () => updateOnboardingConfig,
979
+ updatePlaybooksConfig: () => updatePlaybooksConfig,
980
+ validateAgentList: () => validateAgentList,
981
+ validateDeriveConfig: () => validateDeriveConfig,
982
+ writeAgentsConfig: () => writeAgentsConfig,
983
+ writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
984
+ writeStatusConfig: () => writeStatusConfig,
985
+ writeTerminalConfig: () => writeTerminalConfig,
986
+ writeThemeConfig: () => writeThemeConfig,
987
+ writeWorkspaceVisibilityConfig: () => writeWorkspaceVisibilityConfig
988
+ });
811
989
  import { readFile as readFile5 } from "fs/promises";
812
990
  import { spawnSync } from "child_process";
813
991
  import { resolve as resolve6, isAbsolute } from "path";
@@ -972,6 +1150,16 @@ function parseStatusConfig(content) {
972
1150
  const statuses = [];
973
1151
  const order = [];
974
1152
  const transitions = [];
1153
+ const phaseLadder = [];
1154
+ const disposition = [];
1155
+ const headline = {};
1156
+ const unquote = (v) => {
1157
+ const t = v.trim();
1158
+ if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
1159
+ return t.slice(1, -1);
1160
+ }
1161
+ return t;
1162
+ };
975
1163
  let currentSection = null;
976
1164
  const lines = remaining.split("\n");
977
1165
  function parseListEntry(lineIdx, baseIndent) {
@@ -1004,6 +1192,9 @@ function parseStatusConfig(content) {
1004
1192
  if (key === "definitions") currentSection = "definitions";
1005
1193
  else if (key === "order") currentSection = "order";
1006
1194
  else if (key === "transitions") currentSection = "transitions";
1195
+ else if (key === "phaseLadder") currentSection = "phaseLadder";
1196
+ else if (key === "disposition") currentSection = "disposition";
1197
+ else if (key === "headline") currentSection = "headline";
1007
1198
  else currentSection = null;
1008
1199
  continue;
1009
1200
  }
@@ -1042,12 +1233,52 @@ function parseStatusConfig(content) {
1042
1233
  lineIdx += consumed - 1;
1043
1234
  continue;
1044
1235
  }
1236
+ if (currentSection === "phaseLadder" && indent >= 4 && trimmed.startsWith("- ")) {
1237
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
1238
+ if (entry["phase"] && entry["when"] !== void 0) {
1239
+ phaseLadder.push({
1240
+ phase: unquote(entry["phase"]),
1241
+ when: unquote(entry["when"]),
1242
+ next: entry["next"] !== void 0 ? unquote(entry["next"]) : void 0
1243
+ });
1244
+ }
1245
+ lineIdx += consumed - 1;
1246
+ continue;
1247
+ }
1248
+ if (currentSection === "disposition" && indent >= 4 && trimmed.startsWith("- ")) {
1249
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
1250
+ if (entry["else"] !== void 0) {
1251
+ disposition.push({ when: null, is: unquote(entry["else"]) });
1252
+ } else if (entry["when"] !== void 0 && entry["is"]) {
1253
+ disposition.push({ when: unquote(entry["when"]), is: unquote(entry["is"]) });
1254
+ }
1255
+ lineIdx += consumed - 1;
1256
+ continue;
1257
+ }
1258
+ if (currentSection === "headline" && indent >= 4 && !trimmed.startsWith("- ")) {
1259
+ const ci = trimmed.indexOf(":");
1260
+ if (ci > 0) {
1261
+ headline[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1));
1262
+ }
1263
+ continue;
1264
+ }
1045
1265
  }
1046
1266
  if (statuses.length === 0) return null;
1267
+ const derive = phaseLadder.length > 0 || disposition.length > 0 || Object.keys(headline).length > 0 ? {
1268
+ phaseLadder: phaseLadder.length > 0 ? phaseLadder : DEFAULT_DERIVE_CONFIG.phaseLadder,
1269
+ disposition: disposition.length > 0 ? disposition : DEFAULT_DERIVE_CONFIG.disposition,
1270
+ headline: {
1271
+ terminal: "passthrough",
1272
+ parked: headline["parked"] ?? DEFAULT_DERIVE_CONFIG.headline.parked,
1273
+ blocked: headline["blocked"] ?? DEFAULT_DERIVE_CONFIG.headline.blocked,
1274
+ active: "phase"
1275
+ }
1276
+ } : null;
1047
1277
  return {
1048
1278
  statuses,
1049
1279
  order: order.length > 0 ? order : statuses.map((s) => s.id),
1050
- transitions
1280
+ transitions,
1281
+ derive
1051
1282
  };
1052
1283
  }
1053
1284
  function toTitleCase(s) {
@@ -1068,6 +1299,137 @@ function buildDefaultStatusConfig() {
1068
1299
  })
1069
1300
  };
1070
1301
  }
1302
+ function serializeStatusConfig(statuses) {
1303
+ const lines = [];
1304
+ lines.push("statuses:");
1305
+ lines.push(" definitions:");
1306
+ for (const s of statuses.statuses) {
1307
+ lines.push(` - id: ${s.id}`);
1308
+ lines.push(` label: ${s.label}`);
1309
+ if (s.description) lines.push(` description: ${s.description}`);
1310
+ if (s.color) lines.push(` color: ${s.color}`);
1311
+ if (s.icon) lines.push(` icon: ${s.icon}`);
1312
+ if (s.terminal) lines.push(` terminal: true`);
1313
+ }
1314
+ lines.push(" order:");
1315
+ for (const id of statuses.order) {
1316
+ lines.push(` - ${id}`);
1317
+ }
1318
+ if (statuses.transitions.length > 0) {
1319
+ lines.push(" transitions:");
1320
+ for (const t of statuses.transitions) {
1321
+ lines.push(` - from: ${t.from}`);
1322
+ lines.push(` command: ${t.command}`);
1323
+ lines.push(` to: ${t.to}`);
1324
+ if (t.label) lines.push(` label: ${t.label}`);
1325
+ if (t.description) lines.push(` description: ${t.description}`);
1326
+ if (t.requiresReason) lines.push(` requiresReason: true`);
1327
+ }
1328
+ }
1329
+ if (statuses.derive) {
1330
+ const d = statuses.derive;
1331
+ lines.push(" phaseLadder:");
1332
+ for (const rung of d.phaseLadder) {
1333
+ lines.push(` - phase: ${rung.phase}`);
1334
+ lines.push(` when: "${rung.when.replace(/"/g, '\\"')}"`);
1335
+ if (rung.next) lines.push(` next: "${rung.next.replace(/"/g, '\\"')}"`);
1336
+ }
1337
+ lines.push(" disposition:");
1338
+ for (const rule of d.disposition) {
1339
+ if (rule.when === null) {
1340
+ lines.push(` - else: ${rule.is}`);
1341
+ } else {
1342
+ lines.push(` - when: "${rule.when.replace(/"/g, '\\"')}"`);
1343
+ lines.push(` is: ${rule.is}`);
1344
+ }
1345
+ }
1346
+ lines.push(" headline:");
1347
+ lines.push(` terminal: passthrough`);
1348
+ lines.push(` parked: ${d.headline.parked}`);
1349
+ lines.push(` blocked: ${d.headline.blocked}`);
1350
+ lines.push(` active: phase`);
1351
+ }
1352
+ return lines.join("\n");
1353
+ }
1354
+ function validateDeriveConfig(derive, statusConfig, validateWhen = () => null) {
1355
+ const problems = [];
1356
+ const ids = new Set(statusConfig.statuses.map((s) => s.id));
1357
+ if (derive.phaseLadder.length === 0) {
1358
+ problems.push("phaseLadder must have at least one rung");
1359
+ }
1360
+ for (const rung of derive.phaseLadder) {
1361
+ if (!ids.has(rung.phase)) {
1362
+ problems.push(`phaseLadder rung "${rung.phase}" is not a defined status id`);
1363
+ }
1364
+ const err = rung.when === "*" ? null : validateWhen(rung.when);
1365
+ if (err) problems.push(`phaseLadder rung "${rung.phase}": invalid condition \u2014 ${err}`);
1366
+ }
1367
+ const VALID_DISPOSITIONS = /* @__PURE__ */ new Set(["active", "blocked", "parked"]);
1368
+ let sawElse = false;
1369
+ for (const rule of derive.disposition) {
1370
+ if (!VALID_DISPOSITIONS.has(rule.is)) {
1371
+ problems.push(
1372
+ `disposition "${rule.is}" is not valid (expected active, blocked, or parked \u2014 terminal is never a rule)`
1373
+ );
1374
+ }
1375
+ if (rule.when === null) sawElse = true;
1376
+ else {
1377
+ const err = validateWhen(rule.when);
1378
+ if (err) problems.push(`disposition rule "${rule.is}": invalid condition \u2014 ${err}`);
1379
+ }
1380
+ }
1381
+ if (!sawElse) problems.push("disposition rules must end with an `else:` arm");
1382
+ for (const key of ["parked", "blocked"]) {
1383
+ if (!ids.has(derive.headline[key])) {
1384
+ problems.push(
1385
+ `headline.${key} \u2192 "${derive.headline[key]}" is not a defined status id (add the definition or run migrate-derive)`
1386
+ );
1387
+ }
1388
+ }
1389
+ return problems;
1390
+ }
1391
+ function serializeIntegrationConfig(integrations) {
1392
+ const lines = [];
1393
+ if (integrations.claudePluginDir) {
1394
+ lines.push(` claudePluginDir: ${integrations.claudePluginDir}`);
1395
+ }
1396
+ if (integrations.codexPluginDir) {
1397
+ lines.push(` codexPluginDir: ${integrations.codexPluginDir}`);
1398
+ }
1399
+ if (integrations.codexMarketplacePath) {
1400
+ lines.push(` codexMarketplacePath: ${integrations.codexMarketplacePath}`);
1401
+ }
1402
+ if (integrations.installedAgents) {
1403
+ for (const [id, rec] of Object.entries(integrations.installedAgents)) {
1404
+ lines.push(` installedAgents.${id}: ${rec.scope}`);
1405
+ }
1406
+ }
1407
+ if (lines.length === 0) {
1408
+ return null;
1409
+ }
1410
+ return ["integrations:", ...lines].join("\n");
1411
+ }
1412
+ function serializeOnboardingConfig(onboarding) {
1413
+ return ["onboarding:", ` completed: ${onboarding.completed ? "true" : "false"}`].join("\n");
1414
+ }
1415
+ function serializeBackupConfig(backup) {
1416
+ const lines = ["backup:"];
1417
+ lines.push(` repo: ${backup.repo ?? "null"}`);
1418
+ lines.push(` categories: ${backup.categories}`);
1419
+ lines.push(` lastBackup: ${backup.lastBackup ?? "null"}`);
1420
+ lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
1421
+ return lines.join("\n");
1422
+ }
1423
+ function serializePlaybooksConfig(playbooks) {
1424
+ if (!playbooks.disabled || playbooks.disabled.length === 0) {
1425
+ return null;
1426
+ }
1427
+ const lines = ["playbooks:", " disabled:"];
1428
+ for (const slug of playbooks.disabled) {
1429
+ lines.push(` - ${slug}`);
1430
+ }
1431
+ return lines.join("\n");
1432
+ }
1071
1433
  function parsePlaybooksConfig(fmBlock) {
1072
1434
  const blockStart = fmBlock.match(/^playbooks:\s*$/m);
1073
1435
  if (!blockStart) {
@@ -1103,6 +1465,37 @@ function parsePlaybooksConfig(fmBlock) {
1103
1465
  }
1104
1466
  return { disabled };
1105
1467
  }
1468
+ async function updatePlaybooksConfig(playbooks) {
1469
+ const configPath = resolve6(syntaurRoot(), "config.md");
1470
+ const current = (await readConfig()).playbooks;
1471
+ const nextPlaybooks = {
1472
+ disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
1473
+ };
1474
+ const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
1475
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1476
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1477
+ if (!fmMatch) {
1478
+ const bodyBlock = playbooksBlock ? `${playbooksBlock}
1479
+ ` : "";
1480
+ const content = `---
1481
+ version: "2.0"
1482
+ defaultProjectDir: ${defaultProjectDir()}
1483
+ ${bodyBlock}---
1484
+ ${existing}`;
1485
+ await writeFileForce(configPath, content);
1486
+ return;
1487
+ }
1488
+ const fmBlock = fmMatch[2];
1489
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1490
+ const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
1491
+ const newFm = playbooksBlock ? `${cleanedFm}
1492
+ ${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
1493
+ const normalizedFm = newFm.replace(/\n+$/, "");
1494
+ const newContent = `---
1495
+ ${normalizedFm}
1496
+ ---${afterFrontmatter}`;
1497
+ await writeFileForce(configPath, newContent);
1498
+ }
1106
1499
  function parseThemeConfig(content) {
1107
1500
  const match = content.match(/^---\n([\s\S]*?)\n---/);
1108
1501
  if (!match) return null;
@@ -1125,6 +1518,58 @@ function parseThemeConfig(content) {
1125
1518
  if (!preset) return null;
1126
1519
  return { preset };
1127
1520
  }
1521
+ function serializeThemeConfig(theme) {
1522
+ return ["theme:", ` preset: ${theme.preset}`].join("\n");
1523
+ }
1524
+ async function writeThemeConfig(theme) {
1525
+ const configPath = resolve6(syntaurRoot(), "config.md");
1526
+ const themeBlock = serializeThemeConfig(theme);
1527
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1528
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1529
+ if (!fmMatch) {
1530
+ const content = `---
1531
+ version: "2.0"
1532
+ defaultProjectDir: ${defaultProjectDir()}
1533
+ ${themeBlock}
1534
+ ---
1535
+ ${existing}`;
1536
+ await writeFileForce(configPath, content);
1537
+ return;
1538
+ }
1539
+ const fmBlock = fmMatch[2];
1540
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1541
+ const cleanedFm = stripTopLevelBlock(fmBlock, "theme");
1542
+ const newFm = `${cleanedFm}
1543
+ ${themeBlock}`.replace(/^\n+/, "");
1544
+ const normalizedFm = newFm.replace(/\n+$/, "");
1545
+ const newContent = `---
1546
+ ${normalizedFm}
1547
+ ---${afterFrontmatter}`;
1548
+ await writeFileForce(configPath, newContent);
1549
+ }
1550
+ async function deleteThemeConfig() {
1551
+ const configPath = resolve6(syntaurRoot(), "config.md");
1552
+ if (!await fileExists(configPath)) return;
1553
+ const existing = await readFile5(configPath, "utf-8");
1554
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1555
+ if (!fmMatch) return;
1556
+ const fmBlock = fmMatch[2];
1557
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1558
+ const cleanedFm = stripTopLevelBlock(fmBlock, "theme");
1559
+ const newContent = `---
1560
+ ${cleanedFm}
1561
+ ---${afterFrontmatter}`;
1562
+ await writeFileForce(configPath, newContent);
1563
+ }
1564
+ function serializeWorkspaceVisibilityConfig(cfg) {
1565
+ const hidden = normalizeHiddenList(cfg.hidden);
1566
+ if (hidden.length === 0) return null;
1567
+ const lines = ["workspaceVisibility:", " hidden:"];
1568
+ for (const name of hidden) {
1569
+ lines.push(` - ${JSON.stringify(name)}`);
1570
+ }
1571
+ return lines.join("\n");
1572
+ }
1128
1573
  function parseWorkspaceVisibilityConfig(fmBlock) {
1129
1574
  const blockStart = fmBlock.match(/^workspaceVisibility:\s*$/m);
1130
1575
  if (!blockStart) {
@@ -1162,6 +1607,93 @@ function parseWorkspaceVisibilityConfig(fmBlock) {
1162
1607
  }
1163
1608
  return { hidden: normalizeHiddenList(hidden) };
1164
1609
  }
1610
+ async function writeWorkspaceVisibilityConfig(cfg) {
1611
+ const configPath = resolve6(syntaurRoot(), "config.md");
1612
+ const block = serializeWorkspaceVisibilityConfig(cfg);
1613
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1614
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1615
+ if (!fmMatch) {
1616
+ const bodyBlock = block ? `${block}
1617
+ ` : "";
1618
+ const content = `---
1619
+ version: "2.0"
1620
+ defaultProjectDir: ${defaultProjectDir()}
1621
+ ${bodyBlock}---
1622
+ ${existing}`;
1623
+ await writeFileForce(configPath, content);
1624
+ return;
1625
+ }
1626
+ const fmBlock = fmMatch[2];
1627
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1628
+ const cleanedFm = stripTopLevelBlock(fmBlock, "workspaceVisibility");
1629
+ const newFm = block ? `${cleanedFm}
1630
+ ${block}`.replace(/^\n+/, "") : cleanedFm;
1631
+ const normalizedFm = newFm.replace(/\n+$/, "");
1632
+ const newContent = `---
1633
+ ${normalizedFm}
1634
+ ---${afterFrontmatter}`;
1635
+ await writeFileForce(configPath, newContent);
1636
+ }
1637
+ async function deleteWorkspaceVisibilityConfig() {
1638
+ const configPath = resolve6(syntaurRoot(), "config.md");
1639
+ if (!await fileExists(configPath)) return;
1640
+ const existing = await readFile5(configPath, "utf-8");
1641
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1642
+ if (!fmMatch) return;
1643
+ const fmBlock = fmMatch[2];
1644
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1645
+ const cleanedFm = stripTopLevelBlock(fmBlock, "workspaceVisibility");
1646
+ const newContent = `---
1647
+ ${cleanedFm}
1648
+ ---${afterFrontmatter}`;
1649
+ await writeFileForce(configPath, newContent);
1650
+ }
1651
+ function stripTopLevelScalar(fmBlock, key) {
1652
+ const lines = fmBlock.split("\n");
1653
+ const keyRegex = new RegExp(`^${key}:\\s*\\S`);
1654
+ const filtered = lines.filter((line) => !keyRegex.test(line));
1655
+ return filtered.join("\n").replace(/\n+$/, "");
1656
+ }
1657
+ async function writeTerminalConfig(terminal) {
1658
+ const configPath = resolve6(syntaurRoot(), "config.md");
1659
+ const terminalLine = `terminal: ${terminal}`;
1660
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1661
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1662
+ if (!fmMatch) {
1663
+ const content = `---
1664
+ version: "2.0"
1665
+ defaultProjectDir: ${defaultProjectDir()}
1666
+ ${terminalLine}
1667
+ ---
1668
+ ${existing}`;
1669
+ await writeFileForce(configPath, content);
1670
+ return;
1671
+ }
1672
+ const fmBlock = fmMatch[2];
1673
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1674
+ const cleanedFm = stripTopLevelScalar(fmBlock, "terminal");
1675
+ const newFm = `${cleanedFm}
1676
+ ${terminalLine}`.replace(/^\n+/, "");
1677
+ const normalizedFm = newFm.replace(/\n+$/, "");
1678
+ const newContent = `---
1679
+ ${normalizedFm}
1680
+ ---${afterFrontmatter}`;
1681
+ await writeFileForce(configPath, newContent);
1682
+ }
1683
+ async function deleteTerminalConfig() {
1684
+ const configPath = resolve6(syntaurRoot(), "config.md");
1685
+ if (!await fileExists(configPath)) return;
1686
+ const existing = await readFile5(configPath, "utf-8");
1687
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1688
+ if (!fmMatch) return;
1689
+ const fmBlock = fmMatch[2];
1690
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1691
+ const cleanedFm = stripTopLevelScalar(fmBlock, "terminal");
1692
+ const newContent = `---
1693
+ ${cleanedFm}
1694
+ ---${afterFrontmatter}`;
1695
+ await writeFileForce(configPath, newContent);
1696
+ }
1165
1697
  function parseHotkeyBindingsConfig(content) {
1166
1698
  const match = content.match(/^---\n([\s\S]*?)\n---/);
1167
1699
  if (!match) return null;
@@ -1194,6 +1726,92 @@ function parseHotkeyBindingsConfig(content) {
1194
1726
  if (Object.keys(bindings).length === 0) return null;
1195
1727
  return { bindings };
1196
1728
  }
1729
+ function serializeHotkeyBindingsConfig(cfg) {
1730
+ const lines = ["hotkeys:", " bindings:"];
1731
+ for (const kind of BINDABLE_ACTION_KINDS) {
1732
+ const value = cfg.bindings[kind];
1733
+ if (!value) continue;
1734
+ lines.push(` ${kind}: "${canonicalizeCombo(value)}"`);
1735
+ }
1736
+ if (lines.length === 2) return "";
1737
+ return lines.join("\n");
1738
+ }
1739
+ async function writeHotkeyBindingsConfig(cfg) {
1740
+ const cleaned = {};
1741
+ for (const kind of BINDABLE_ACTION_KINDS) {
1742
+ const raw = cfg.bindings[kind];
1743
+ if (typeof raw !== "string" || raw.trim() === "") continue;
1744
+ const canonical = canonicalizeCombo(raw);
1745
+ if (!canonical) continue;
1746
+ if (isReservedCombo(canonical)) continue;
1747
+ cleaned[kind] = canonical;
1748
+ }
1749
+ if (Object.keys(cleaned).length === 0) {
1750
+ await deleteHotkeyBindingsConfig();
1751
+ return;
1752
+ }
1753
+ const configPath = resolve6(syntaurRoot(), "config.md");
1754
+ const block = serializeHotkeyBindingsConfig({ bindings: cleaned });
1755
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1756
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1757
+ if (!fmMatch) {
1758
+ const content = `---
1759
+ version: "2.0"
1760
+ defaultProjectDir: ${defaultProjectDir()}
1761
+ ${block}
1762
+ ---
1763
+ ${existing}`;
1764
+ await writeFileForce(configPath, content);
1765
+ return;
1766
+ }
1767
+ const fmBlock = fmMatch[2];
1768
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1769
+ const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
1770
+ const newFm = `${cleanedFm}
1771
+ ${block}`.replace(/^\n+/, "");
1772
+ const normalizedFm = newFm.replace(/\n+$/, "");
1773
+ const newContent = `---
1774
+ ${normalizedFm}
1775
+ ---${afterFrontmatter}`;
1776
+ await writeFileForce(configPath, newContent);
1777
+ }
1778
+ async function deleteHotkeyBindingsConfig() {
1779
+ const configPath = resolve6(syntaurRoot(), "config.md");
1780
+ if (!await fileExists(configPath)) return;
1781
+ const existing = await readFile5(configPath, "utf-8");
1782
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1783
+ if (!fmMatch) return;
1784
+ const fmBlock = fmMatch[2];
1785
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1786
+ const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
1787
+ const newContent = `---
1788
+ ${cleanedFm}
1789
+ ---${afterFrontmatter}`;
1790
+ await writeFileForce(configPath, newContent);
1791
+ }
1792
+ function stripTopLevelBlock(fmBlock, key) {
1793
+ const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
1794
+ if (!blockStart) {
1795
+ return fmBlock.replace(/\n+$/, "");
1796
+ }
1797
+ const startIdx = fmBlock.indexOf(blockStart[0]);
1798
+ const before = fmBlock.slice(0, startIdx);
1799
+ const after = fmBlock.slice(startIdx + blockStart[0].length);
1800
+ const remaining = after.split("\n");
1801
+ let endIdx = 0;
1802
+ for (let i = 0; i < remaining.length; i++) {
1803
+ const line = remaining[i];
1804
+ if (line.trim() === "") {
1805
+ endIdx = i + 1;
1806
+ continue;
1807
+ }
1808
+ if (line.length > 0 && line[0] !== " ") {
1809
+ break;
1810
+ }
1811
+ endIdx = i + 1;
1812
+ }
1813
+ return (before + remaining.slice(endIdx).join("\n")).replace(/\n+$/, "");
1814
+ }
1197
1815
  function parseOptionalAbsolutePath(value, fieldName) {
1198
1816
  if (!value) {
1199
1817
  return null;
@@ -1429,6 +2047,266 @@ function assignAgentField(target, key, rawValue) {
1429
2047
  break;
1430
2048
  }
1431
2049
  }
2050
+ function yamlQuoteScalar(value) {
2051
+ if (/[\r\n]/.test(value)) {
2052
+ throw new AgentConfigError(
2053
+ `value contains newlines, which the agents config serializer does not support: ${JSON.stringify(value)}`
2054
+ );
2055
+ }
2056
+ if (value === "" || /[:#{}[\],&*?|>!%@`"'\\\t]/.test(value) || /^\s|\s$/.test(value)) {
2057
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t");
2058
+ return `"${escaped}"`;
2059
+ }
2060
+ return value;
2061
+ }
2062
+ function serializeAgentsConfig(agents) {
2063
+ const lines = ["agents:"];
2064
+ for (const a of agents) {
2065
+ lines.push(` - id: ${yamlQuoteScalar(a.id)}`);
2066
+ lines.push(` label: ${yamlQuoteScalar(a.label)}`);
2067
+ lines.push(` command: ${yamlQuoteScalar(a.command)}`);
2068
+ if (a.model) {
2069
+ lines.push(` model: ${yamlQuoteScalar(a.model)}`);
2070
+ }
2071
+ if (a.playbook) {
2072
+ lines.push(` playbook: ${yamlQuoteScalar(a.playbook)}`);
2073
+ }
2074
+ if (a.args && a.args.length > 0) {
2075
+ lines.push(` args:`);
2076
+ for (const arg of a.args) {
2077
+ lines.push(` - ${yamlQuoteScalar(arg)}`);
2078
+ }
2079
+ }
2080
+ if (a.promptArgPosition && a.promptArgPosition !== "first") {
2081
+ lines.push(` promptArgPosition: ${a.promptArgPosition}`);
2082
+ }
2083
+ if (a.default) {
2084
+ lines.push(` default: true`);
2085
+ }
2086
+ if (a.resolveFromShellAliases) {
2087
+ lines.push(` resolveFromShellAliases: true`);
2088
+ }
2089
+ if (a.resume) {
2090
+ appendSessionInvocation(lines, "resume", a.resume);
2091
+ }
2092
+ if (a.fork) {
2093
+ appendSessionInvocation(lines, "fork", a.fork);
2094
+ }
2095
+ }
2096
+ return lines.join("\n");
2097
+ }
2098
+ function appendSessionInvocation(lines, key, invocation) {
2099
+ lines.push(` ${key}:`);
2100
+ if (invocation.command !== void 0) {
2101
+ lines.push(` command: ${yamlQuoteScalar(invocation.command)}`);
2102
+ }
2103
+ lines.push(` args:`);
2104
+ for (const arg of invocation.args) {
2105
+ lines.push(` - ${yamlQuoteScalar(arg)}`);
2106
+ }
2107
+ }
2108
+ async function writeAgentsConfig(agents) {
2109
+ validateAgentList(agents);
2110
+ const configPath = resolve6(syntaurRoot(), "config.md");
2111
+ const agentsBlock = serializeAgentsConfig(agents);
2112
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2113
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2114
+ if (!fmMatch) {
2115
+ const content = `---
2116
+ version: "2.0"
2117
+ defaultProjectDir: ${defaultProjectDir()}
2118
+ ${agentsBlock}
2119
+ ---
2120
+ ${existing}`;
2121
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
2122
+ return;
2123
+ }
2124
+ const fmBlock = fmMatch[2];
2125
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2126
+ const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
2127
+ const newFm = `${cleanedFm}
2128
+ ${agentsBlock}`.replace(/^\n+/, "").replace(/\n+$/, "");
2129
+ const newContent = `---
2130
+ ${newFm}
2131
+ ---${afterFrontmatter}`;
2132
+ await writeFileForce(configPath, newContent);
2133
+ }
2134
+ async function deleteAgentsConfig() {
2135
+ const configPath = resolve6(syntaurRoot(), "config.md");
2136
+ if (!await fileExists(configPath)) return;
2137
+ const existing = await readFile5(configPath, "utf-8");
2138
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2139
+ if (!fmMatch) return;
2140
+ const fmBlock = fmMatch[2];
2141
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2142
+ const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
2143
+ const newContent = `---
2144
+ ${cleanedFm}
2145
+ ---${afterFrontmatter}`;
2146
+ await writeFileForce(configPath, newContent);
2147
+ }
2148
+ async function writeStatusConfig(statuses) {
2149
+ const configPath = resolve6(syntaurRoot(), "config.md");
2150
+ const statusBlock = serializeStatusConfig(statuses);
2151
+ if (!await fileExists(configPath)) {
2152
+ const content = `---
2153
+ version: "2.0"
2154
+ defaultProjectDir: ~/projects
2155
+ ${statusBlock}
2156
+ ---
2157
+ `;
2158
+ await writeFileForce(configPath, content);
2159
+ return;
2160
+ }
2161
+ const existing = await readFile5(configPath, "utf-8");
2162
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2163
+ if (!fmMatch) {
2164
+ const content = `---
2165
+ version: "2.0"
2166
+ ${statusBlock}
2167
+ ---
2168
+ ${existing}`;
2169
+ await writeFileForce(configPath, content);
2170
+ return;
2171
+ }
2172
+ const fmBlock = fmMatch[2];
2173
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2174
+ const statusesStart = fmBlock.match(/^statuses:\s*$/m);
2175
+ let cleanedFm;
2176
+ if (statusesStart) {
2177
+ const startIdx = fmBlock.indexOf(statusesStart[0]);
2178
+ const before = fmBlock.slice(0, startIdx);
2179
+ const after = fmBlock.slice(startIdx + statusesStart[0].length);
2180
+ const remaining = after.split("\n");
2181
+ let endIdx = 0;
2182
+ for (let i = 0; i < remaining.length; i++) {
2183
+ const line = remaining[i];
2184
+ if (line.trim() === "") {
2185
+ endIdx = i + 1;
2186
+ continue;
2187
+ }
2188
+ if (line.length > 0 && line[0] !== " ") break;
2189
+ endIdx = i + 1;
2190
+ }
2191
+ cleanedFm = before + remaining.slice(endIdx).join("\n");
2192
+ } else {
2193
+ cleanedFm = fmBlock;
2194
+ }
2195
+ cleanedFm = cleanedFm.replace(/\n+$/, "");
2196
+ const newContent = `---
2197
+ ${cleanedFm}
2198
+ ${statusBlock}
2199
+ ---${afterFrontmatter}`;
2200
+ await writeFileForce(configPath, newContent);
2201
+ }
2202
+ async function deleteStatusConfig() {
2203
+ const configPath = resolve6(syntaurRoot(), "config.md");
2204
+ if (!await fileExists(configPath)) return;
2205
+ const existing = await readFile5(configPath, "utf-8");
2206
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2207
+ if (!fmMatch) return;
2208
+ const fmBlock = fmMatch[2];
2209
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2210
+ const cleanedFm = stripTopLevelBlock(fmBlock, "statuses");
2211
+ const newContent = `---
2212
+ ${cleanedFm}
2213
+ ---${afterFrontmatter}`;
2214
+ await writeFileForce(configPath, newContent);
2215
+ }
2216
+ async function updateIntegrationConfig(integrations) {
2217
+ const configPath = resolve6(syntaurRoot(), "config.md");
2218
+ const nextIntegrations = {
2219
+ ...(await readConfig()).integrations,
2220
+ ...integrations
2221
+ };
2222
+ const integrationBlock = serializeIntegrationConfig(nextIntegrations);
2223
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2224
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2225
+ if (!fmMatch) {
2226
+ const content = `---
2227
+ version: "2.0"
2228
+ defaultProjectDir: ${defaultProjectDir()}
2229
+ ${integrationBlock ?? ""}
2230
+ ---
2231
+ ${existing}`;
2232
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
2233
+ return;
2234
+ }
2235
+ const fmBlock = fmMatch[2];
2236
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2237
+ const cleanedFm = stripTopLevelBlock(fmBlock, "integrations");
2238
+ const newFm = integrationBlock ? `${cleanedFm}
2239
+ ${integrationBlock}`.replace(/^\n+/, "") : cleanedFm;
2240
+ const normalizedFm = newFm.replace(/\n+$/, "");
2241
+ const newContent = `---
2242
+ ${normalizedFm}
2243
+ ---${afterFrontmatter}`;
2244
+ await writeFileForce(configPath, newContent);
2245
+ }
2246
+ async function updateOnboardingConfig(onboarding) {
2247
+ const configPath = resolve6(syntaurRoot(), "config.md");
2248
+ const nextOnboarding = {
2249
+ ...(await readConfig()).onboarding,
2250
+ ...onboarding
2251
+ };
2252
+ const onboardingBlock = serializeOnboardingConfig(nextOnboarding);
2253
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2254
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2255
+ if (!fmMatch) {
2256
+ const content = `---
2257
+ version: "2.0"
2258
+ defaultProjectDir: ${defaultProjectDir()}
2259
+ ${onboardingBlock}
2260
+ ---
2261
+ ${existing}`;
2262
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
2263
+ return;
2264
+ }
2265
+ const fmBlock = fmMatch[2];
2266
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2267
+ const cleanedFm = stripTopLevelBlock(fmBlock, "onboarding");
2268
+ const newFm = `${cleanedFm}
2269
+ ${onboardingBlock}`.replace(/^\n+/, "");
2270
+ const normalizedFm = newFm.replace(/\n+$/, "");
2271
+ const newContent = `---
2272
+ ${normalizedFm}
2273
+ ---${afterFrontmatter}`;
2274
+ await writeFileForce(configPath, newContent);
2275
+ }
2276
+ async function updateBackupConfig(backup) {
2277
+ const configPath = resolve6(syntaurRoot(), "config.md");
2278
+ const current = (await readConfig()).backup;
2279
+ const nextBackup = {
2280
+ repo: current?.repo ?? null,
2281
+ categories: current?.categories ?? "projects, playbooks, todos, servers, config",
2282
+ lastBackup: current?.lastBackup ?? null,
2283
+ lastRestore: current?.lastRestore ?? null,
2284
+ ...backup
2285
+ };
2286
+ const backupBlock = serializeBackupConfig(nextBackup);
2287
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2288
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2289
+ if (!fmMatch) {
2290
+ const content = `---
2291
+ version: "2.0"
2292
+ defaultProjectDir: ${defaultProjectDir()}
2293
+ ${backupBlock}
2294
+ ---
2295
+ ${existing}`;
2296
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
2297
+ return;
2298
+ }
2299
+ const fmBlock = fmMatch[2];
2300
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2301
+ const cleanedFm = stripTopLevelBlock(fmBlock, "backup");
2302
+ const newFm = `${cleanedFm}
2303
+ ${backupBlock}`.replace(/^\n+/, "");
2304
+ const normalizedFm = newFm.replace(/\n+$/, "");
2305
+ const newContent = `---
2306
+ ${normalizedFm}
2307
+ ---${afterFrontmatter}`;
2308
+ await writeFileForce(configPath, newContent);
2309
+ }
1432
2310
  async function readConfig() {
1433
2311
  const configPath = resolve6(syntaurRoot(), "config.md");
1434
2312
  if (!await fileExists(configPath)) {
@@ -1504,6 +2382,9 @@ async function readConfig() {
1504
2382
  workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock)
1505
2383
  };
1506
2384
  }
2385
+ function getAssignmentTypes(config) {
2386
+ return config.types ?? DEFAULT_ASSIGNMENT_TYPES;
2387
+ }
1507
2388
  function getAgents(config) {
1508
2389
  if (config.agents === null) return BUILTIN_AGENTS;
1509
2390
  const builtinById = new Map(BUILTIN_AGENTS.map((a) => [a.id, a]));
@@ -1550,7 +2431,18 @@ function getTerminal(config) {
1550
2431
  }
1551
2432
  return "terminal-app";
1552
2433
  }
1553
- var DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
2434
+ async function updateAgentsConfig(mutation, options = {}) {
2435
+ const config = await readConfig();
2436
+ const previous = config.agents ?? [...BUILTIN_AGENTS];
2437
+ const next = mutation.apply(previous);
2438
+ validateAgentList(next);
2439
+ if (options.dryRun) {
2440
+ return { previous, next, written: false };
2441
+ }
2442
+ await writeAgentsConfig(next);
2443
+ return { previous, next, written: true };
2444
+ }
2445
+ var DEFAULT_DERIVE_CONFIG, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
1554
2446
  var init_config2 = __esm({
1555
2447
  "src/utils/config.ts"() {
1556
2448
  "use strict";
@@ -1564,6 +2456,45 @@ var init_config2 = __esm({
1564
2456
  init_slug();
1565
2457
  init_terminal_schema();
1566
2458
  init_workspace_visibility_schema();
2459
+ DEFAULT_DERIVE_CONFIG = {
2460
+ phaseLadder: [
2461
+ { phase: "draft", when: "*", next: "Fill in the objective and acceptance criteria" },
2462
+ {
2463
+ // planExists-but-not-approved also sits here: the default status set has
2464
+ // no `planning` id. Users who define one add a `planExists:true` rung.
2465
+ phase: "ready_for_planning",
2466
+ when: "hasRealObjective:true AND acRealTotal > 0",
2467
+ next: "Write a plan and get it approved"
2468
+ },
2469
+ { phase: "ready_to_implement", when: "planApproved:true", next: "Start implementing" },
2470
+ {
2471
+ phase: "in_progress",
2472
+ when: "planApproved:true AND implementationStarted:true",
2473
+ next: "Finish acceptance criteria, then request review"
2474
+ },
2475
+ {
2476
+ phase: "review",
2477
+ when: "acAllChecked:true OR reviewRequested:true",
2478
+ next: "Complete, or address review feedback"
2479
+ }
2480
+ ],
2481
+ disposition: [
2482
+ { when: "parked:true", is: "parked" },
2483
+ { when: "blocked:true", is: "blocked" },
2484
+ { when: null, is: "active" }
2485
+ ],
2486
+ headline: { terminal: "passthrough", parked: "parked", blocked: "blocked", active: "phase" }
2487
+ };
2488
+ DEFAULT_ASSIGNMENT_TYPES = {
2489
+ definitions: [
2490
+ { id: "feature", label: "Feature" },
2491
+ { id: "bug", label: "Bug" },
2492
+ { id: "refactor", label: "Refactor" },
2493
+ { id: "research", label: "Research" },
2494
+ { id: "chore", label: "Chore" }
2495
+ ],
2496
+ default: "feature"
2497
+ };
1567
2498
  DEFAULT_CONFIG = {
1568
2499
  version: "2.0",
1569
2500
  defaultProjectDir: defaultProjectDir(),
@@ -1965,9 +2896,875 @@ var init_overviewCopy = __esm({
1965
2896
  }
1966
2897
  });
1967
2898
 
2899
+ // src/lifecycle/facts.ts
2900
+ var facts_exports = {};
2901
+ __export(facts_exports, {
2902
+ areDependenciesSatisfied: () => areDependenciesSatisfied,
2903
+ computeFacts: () => computeFacts,
2904
+ countRealAcceptanceCriteria: () => countRealAcceptanceCriteria,
2905
+ countUnresolvedQuestions: () => countUnresolvedQuestions,
2906
+ hasRealObjective: () => hasRealObjective,
2907
+ isPlanApproved: () => isPlanApproved,
2908
+ latestPlanFile: () => latestPlanFile,
2909
+ planDigest: () => planDigest
2910
+ });
2911
+ import { createHash } from "crypto";
2912
+ import { readdir as readdir6, readFile as readFile9 } from "fs/promises";
2913
+ import { resolve as resolve11 } from "path";
2914
+ function sectionBody(body, heading) {
2915
+ const re = new RegExp(`^##\\s+${heading}\\s*$`, "m");
2916
+ const m = body.match(re);
2917
+ if (!m || m.index === void 0) return null;
2918
+ const start = m.index + m[0].length;
2919
+ const rest = body.slice(start);
2920
+ const next = rest.search(/^##\s+/m);
2921
+ return next >= 0 ? rest.slice(0, next) : rest;
2922
+ }
2923
+ function hasRealObjective(body) {
2924
+ const section = sectionBody(body, "Objective");
2925
+ if (section === null) return false;
2926
+ return section.replace(HTML_COMMENT_RE, "").trim().length > 0;
2927
+ }
2928
+ function countRealAcceptanceCriteria(body) {
2929
+ const section = sectionBody(body, "Acceptance Criteria");
2930
+ if (section === null) return { total: 0, checked: 0 };
2931
+ let total = 0;
2932
+ let checked = 0;
2933
+ for (const line of section.split("\n")) {
2934
+ const m = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
2935
+ if (!m) continue;
2936
+ const content = m[2].replace(HTML_COMMENT_RE, "").trim();
2937
+ if (content.length === 0) continue;
2938
+ total++;
2939
+ if (m[1].toLowerCase() === "x") checked++;
2940
+ }
2941
+ return { total, checked };
2942
+ }
2943
+ async function latestPlanFile(assignmentDir) {
2944
+ let entries;
2945
+ try {
2946
+ entries = await readdir6(assignmentDir);
2947
+ } catch {
2948
+ return null;
2949
+ }
2950
+ let best = null;
2951
+ for (const name of entries) {
2952
+ const m = name.match(PLAN_FILE_RE);
2953
+ if (!m) continue;
2954
+ const version = m[1] ? parseInt(m[1], 10) : 1;
2955
+ if (!best || version > best.version) best = { name, version };
2956
+ }
2957
+ return best?.name ?? null;
2958
+ }
2959
+ function planDigest(content) {
2960
+ return createHash("sha256").update(content, "utf-8").digest("hex");
2961
+ }
2962
+ async function isPlanApproved(assignmentDir, frontmatter) {
2963
+ const approval = frontmatter.planApproval;
2964
+ if (!approval) return false;
2965
+ const latest = await latestPlanFile(assignmentDir);
2966
+ if (!latest || latest !== approval.file) return false;
2967
+ try {
2968
+ const content = await readFile9(resolve11(assignmentDir, latest), "utf-8");
2969
+ return planDigest(content) === approval.digest;
2970
+ } catch {
2971
+ return false;
2972
+ }
2973
+ }
2974
+ async function countUnresolvedQuestions(assignmentDir) {
2975
+ const commentsPath = resolve11(assignmentDir, "comments.md");
2976
+ if (!await fileExists(commentsPath)) return 0;
2977
+ try {
2978
+ const content = await readFile9(commentsPath, "utf-8");
2979
+ let count = 0;
2980
+ for (const block of content.split(/^##\s+/m).slice(1)) {
2981
+ if (/^\*\*Type:\*\*\s*question\s*$/m.test(block) && /^\*\*Resolved:\*\*\s*false\s*$/m.test(block)) {
2982
+ count++;
2983
+ }
2984
+ }
2985
+ return count;
2986
+ } catch {
2987
+ return 0;
2988
+ }
2989
+ }
2990
+ async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses) {
2991
+ if (dependsOn.length === 0 || projectDir === null) return true;
2992
+ for (const depSlug of dependsOn) {
2993
+ const depPath = resolve11(projectDir, "assignments", depSlug, "assignment.md");
2994
+ if (!await fileExists(depPath)) return false;
2995
+ try {
2996
+ const content = await readFile9(depPath, "utf-8");
2997
+ const m = content.match(/^status:\s*(.+)$/m);
2998
+ const status = m ? m[1].trim() : "";
2999
+ if (!terminalStatuses.has(status)) return false;
3000
+ } catch {
3001
+ return false;
3002
+ }
3003
+ }
3004
+ return true;
3005
+ }
3006
+ async function computeFacts(input) {
3007
+ const { assignmentDir, frontmatter, body, projectDir, terminalStatuses } = input;
3008
+ const ac = countRealAcceptanceCriteria(body);
3009
+ const [planFile, planApproved, unresolvedQuestions, depsSatisfied] = await Promise.all([
3010
+ latestPlanFile(assignmentDir),
3011
+ isPlanApproved(assignmentDir, frontmatter),
3012
+ countUnresolvedQuestions(assignmentDir),
3013
+ areDependenciesSatisfied(projectDir, frontmatter.dependsOn, terminalStatuses)
3014
+ ]);
3015
+ return {
3016
+ hasRealObjective: hasRealObjective(body),
3017
+ acRealTotal: ac.total,
3018
+ acRealChecked: ac.checked,
3019
+ acAllChecked: ac.total > 0 && ac.checked === ac.total,
3020
+ planExists: planFile !== null,
3021
+ planApproved,
3022
+ workspaceSet: frontmatter.workspace.repository !== null && frontmatter.workspace.branch !== null,
3023
+ implementationStarted: frontmatter.implementationStarted,
3024
+ depsSatisfied,
3025
+ unresolvedQuestions,
3026
+ blocked: frontmatter.blockedReason !== null,
3027
+ parked: frontmatter.parked,
3028
+ reviewRequested: frontmatter.reviewRequested,
3029
+ pinned: frontmatter.override !== null
3030
+ };
3031
+ }
3032
+ var HTML_COMMENT_RE, PLAN_FILE_RE;
3033
+ var init_facts = __esm({
3034
+ "src/lifecycle/facts.ts"() {
3035
+ "use strict";
3036
+ init_fs();
3037
+ HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
3038
+ PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
3039
+ }
3040
+ });
3041
+
3042
+ // src/utils/query/fields.ts
3043
+ function resolveField(registry, name) {
3044
+ return registry[name.toLowerCase()] ?? null;
3045
+ }
3046
+ function readField(def, fieldName, item) {
3047
+ if (def.get) return def.get(item);
3048
+ return item[fieldName] ?? item[fieldName.toLowerCase()];
3049
+ }
3050
+ var init_fields = __esm({
3051
+ "src/utils/query/fields.ts"() {
3052
+ "use strict";
3053
+ }
3054
+ });
3055
+
3056
+ // src/utils/query/evaluate.ts
3057
+ function localDayBounds(date) {
3058
+ const [y, m, d] = date.split("-").map((n) => parseInt(n, 10));
3059
+ const start = new Date(y, m - 1, d).getTime();
3060
+ const end = new Date(y, m - 1, d + 1).getTime();
3061
+ return [start, end];
3062
+ }
3063
+ function toEpoch(value) {
3064
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
3065
+ if (typeof value === "string" && value.length > 0) {
3066
+ const t = Date.parse(value);
3067
+ return Number.isNaN(t) ? null : t;
3068
+ }
3069
+ return null;
3070
+ }
3071
+ function toNumber(value) {
3072
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
3073
+ if (typeof value === "string" && value.trim() !== "") {
3074
+ const n = Number(value);
3075
+ return Number.isFinite(n) ? n : null;
3076
+ }
3077
+ return null;
3078
+ }
3079
+ function ciEquals(a, b) {
3080
+ return typeof a === "string" && a.toLowerCase() === b.toLowerCase();
3081
+ }
3082
+ function isNone(value) {
3083
+ return value === null || value === void 0 || value === "";
3084
+ }
3085
+ function compileEquality(def, field, value, atomPos) {
3086
+ switch (def.kind) {
3087
+ case "enum":
3088
+ case "string":
3089
+ if (def.noneSentinel && value.raw.toLowerCase() === "none") {
3090
+ return (item) => isNone(readField(def, field, item));
3091
+ }
3092
+ return (item) => ciEquals(readField(def, field, item), value.raw);
3093
+ case "substring":
3094
+ return (item) => {
3095
+ const v = readField(def, field, item);
3096
+ return typeof v === "string" && v.toLowerCase().includes(value.raw.toLowerCase());
3097
+ };
3098
+ case "bool": {
3099
+ const want = value.raw.toLowerCase();
3100
+ if (want !== "true" && want !== "false") {
3101
+ throw new CompileError([
3102
+ { pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true or ${field}:false` }
3103
+ ]);
3104
+ }
3105
+ const expected = want === "true";
3106
+ return (item) => {
3107
+ const v = readField(def, field, item);
3108
+ const b = typeof v === "boolean" ? v : v === "true" ? true : v === "false" || v === null || v === void 0 || v === "" ? false : null;
3109
+ return b !== null && b === expected;
3110
+ };
3111
+ }
3112
+ case "number": {
3113
+ const n = value.num ?? toNumber(value.raw);
3114
+ if (n === null) {
3115
+ throw new CompileError([{ pos: value.pos, message: `Field "${field}" is numeric \u2014 "${value.raw}" is not a number` }]);
3116
+ }
3117
+ return (item) => toNumber(readField(def, field, item)) === n;
3118
+ }
3119
+ case "ordinal":
3120
+ return (item) => ciEquals(readField(def, field, item), value.raw);
3121
+ case "list":
3122
+ return (item) => {
3123
+ const v = readField(def, field, item);
3124
+ return Array.isArray(v) && v.some((el) => ciEquals(el, value.raw));
3125
+ };
3126
+ case "timestamp": {
3127
+ if (value.type === "date") {
3128
+ const [start, end] = localDayBounds(value.raw);
3129
+ return (item) => {
3130
+ const t = toEpoch(readField(def, field, item));
3131
+ return t !== null && t >= start && t < end;
3132
+ };
3133
+ }
3134
+ throw new CompileError([
3135
+ { pos: value.pos, message: `Field "${field}" is a timestamp \u2014 use a comparison (e.g. ${field} > -36h) or an absolute date (${field}:2026-06-01)` }
3136
+ ]);
3137
+ }
3138
+ case "duration":
3139
+ throw new CompileError([
3140
+ { pos: atomPos, message: `Field "${field}" is a duration \u2014 use a comparison (e.g. ${field} > 3d)` }
3141
+ ]);
3142
+ }
3143
+ }
3144
+ function compileComparison(def, field, op, value) {
3145
+ const cmp = (a, b) => {
3146
+ switch (op) {
3147
+ case "<":
3148
+ return a < b;
3149
+ case ">":
3150
+ return a > b;
3151
+ case "<=":
3152
+ return a <= b;
3153
+ case ">=":
3154
+ return a >= b;
3155
+ case "=":
3156
+ return a === b;
3157
+ case "!=":
3158
+ return a !== b;
3159
+ default:
3160
+ return false;
3161
+ }
3162
+ };
3163
+ switch (def.kind) {
3164
+ case "number": {
3165
+ const n = value.num ?? toNumber(value.raw);
3166
+ if (n === null) {
3167
+ throw new CompileError([{ pos: value.pos, message: `"${value.raw}" is not a number (field "${field}")` }]);
3168
+ }
3169
+ return (item) => {
3170
+ const v = toNumber(readField(def, field, item));
3171
+ return v !== null && cmp(v, n);
3172
+ };
3173
+ }
3174
+ case "ordinal": {
3175
+ const order = def.order ?? [];
3176
+ const idx = order.findIndex((o) => o.toLowerCase() === value.raw.toLowerCase());
3177
+ if (idx < 0) {
3178
+ throw new CompileError([
3179
+ { pos: value.pos, message: `"${value.raw}" is not a valid ${field} (expected one of: ${order.join(", ")})` }
3180
+ ]);
3181
+ }
3182
+ return (item) => {
3183
+ const raw = readField(def, field, item);
3184
+ const vIdx = typeof raw === "string" ? order.findIndex((o) => o.toLowerCase() === raw.toLowerCase()) : -1;
3185
+ return vIdx >= 0 && cmp(vIdx, idx);
3186
+ };
3187
+ }
3188
+ case "timestamp": {
3189
+ if (value.type === "duration") {
3190
+ const sign = value.sign === 0 ? -1 : value.sign ?? -1;
3191
+ const offset = sign * (value.num ?? 0);
3192
+ return (item, ctx) => {
3193
+ const t = toEpoch(readField(def, field, item));
3194
+ return t !== null && cmp(t, ctx.now + offset);
3195
+ };
3196
+ }
3197
+ if (value.type === "date") {
3198
+ const [start, end] = localDayBounds(value.raw);
3199
+ return (item) => {
3200
+ const t = toEpoch(readField(def, field, item));
3201
+ if (t === null) return false;
3202
+ switch (op) {
3203
+ case "<":
3204
+ return t < start;
3205
+ case "<=":
3206
+ return t < end;
3207
+ case ">":
3208
+ return t >= end;
3209
+ case ">=":
3210
+ return t >= start;
3211
+ case "=":
3212
+ return t >= start && t < end;
3213
+ case "!=":
3214
+ return t < start || t >= end;
3215
+ default:
3216
+ return false;
3217
+ }
3218
+ };
3219
+ }
3220
+ throw new CompileError([
3221
+ { pos: value.pos, message: `Compare timestamp field "${field}" to a duration (e.g. -36h) or a date (YYYY-MM-DD)` }
3222
+ ]);
3223
+ }
3224
+ case "duration": {
3225
+ if (value.type !== "duration") {
3226
+ throw new CompileError([
3227
+ { pos: value.pos, message: `Compare duration field "${field}" to a duration literal (e.g. 3d)` }
3228
+ ]);
3229
+ }
3230
+ const magnitude = value.num ?? 0;
3231
+ return (item) => {
3232
+ const v = toNumber(readField(def, field, item));
3233
+ return v !== null && cmp(v, magnitude);
3234
+ };
3235
+ }
3236
+ case "enum":
3237
+ case "string":
3238
+ case "substring":
3239
+ case "list": {
3240
+ if (op === "=") {
3241
+ return compileEquality(def, field, value, value.pos);
3242
+ }
3243
+ if (op === "!=") {
3244
+ const eq = compileEquality(def, field, value, value.pos);
3245
+ return (item, ctx) => !eq(item, ctx);
3246
+ }
3247
+ throw new CompileError([
3248
+ { pos: value.pos, message: `Field "${field}" does not support ordering comparisons (use ":" or "=").` }
3249
+ ]);
3250
+ }
3251
+ case "bool": {
3252
+ if (op === "=" || op === "!=") {
3253
+ const eq = compileEquality(def, field, value, value.pos);
3254
+ return op === "=" ? eq : (item, ctx) => !eq(item, ctx);
3255
+ }
3256
+ throw new CompileError([{ pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true / ${field}:false` }]);
3257
+ }
3258
+ }
3259
+ }
3260
+ function compileAtom(atom, registry) {
3261
+ const def = resolveField(registry, atom.field);
3262
+ if (!def) {
3263
+ throw new CompileError([{ pos: atom.pos, message: `Unknown field "${atom.field}"` }]);
3264
+ }
3265
+ if (atom.op === ":") {
3266
+ const preds = atom.values.map((v) => compileEquality(def, atom.field, v, atom.pos));
3267
+ if (preds.length === 1) return preds[0];
3268
+ return (item, ctx) => preds.some((p) => p(item, ctx));
3269
+ }
3270
+ return compileComparison(def, atom.field, atom.op, atom.values[0]);
3271
+ }
3272
+ function compileNode(node, registry) {
3273
+ switch (node.kind) {
3274
+ case "all":
3275
+ return () => true;
3276
+ case "atom":
3277
+ return compileAtom(node, registry);
3278
+ case "not": {
3279
+ const inner = compileNode(node.child, registry);
3280
+ return (item, ctx) => !inner(item, ctx);
3281
+ }
3282
+ case "and": {
3283
+ const preds = node.children.map((c) => compileNode(c, registry));
3284
+ return (item, ctx) => preds.every((p) => p(item, ctx));
3285
+ }
3286
+ case "or": {
3287
+ const preds = node.children.map((c) => compileNode(c, registry));
3288
+ return (item, ctx) => preds.some((p) => p(item, ctx));
3289
+ }
3290
+ }
3291
+ }
3292
+ var CompileError;
3293
+ var init_evaluate = __esm({
3294
+ "src/utils/query/evaluate.ts"() {
3295
+ "use strict";
3296
+ init_fields();
3297
+ CompileError = class extends Error {
3298
+ constructor(errors) {
3299
+ super(errors.map((e) => `${e.message} (at ${e.pos})`).join("; "));
3300
+ this.errors = errors;
3301
+ this.name = "CompileError";
3302
+ }
3303
+ };
3304
+ }
3305
+ });
3306
+
3307
+ // src/utils/query/lexer.ts
3308
+ function lex(input) {
3309
+ const tokens = [];
3310
+ let i = 0;
3311
+ const numberOrDuration = (start, sign) => {
3312
+ let j = i;
3313
+ while (j < input.length && /\d/.test(input[j])) j++;
3314
+ const digits = input.slice(i, j);
3315
+ let unit = "";
3316
+ while (j < input.length && /[a-z]/i.test(input[j])) {
3317
+ unit += input[j];
3318
+ j++;
3319
+ }
3320
+ i = j;
3321
+ if (unit.length > 0) {
3322
+ const ms = DURATION_MS[unit.toLowerCase()];
3323
+ if (ms === void 0) {
3324
+ throw new LexError(start, `Unknown duration unit "${unit}" (expected h, d, w, m, mo, or y)`);
3325
+ }
3326
+ return {
3327
+ type: "DURATION",
3328
+ text: input.slice(start, j),
3329
+ pos: start,
3330
+ num: parseInt(digits, 10) * ms,
3331
+ sign
3332
+ };
3333
+ }
3334
+ if (sign !== 0) {
3335
+ return { type: "NUMBER", text: input.slice(start, j), pos: start, num: sign * parseInt(digits, 10) };
3336
+ }
3337
+ return { type: "NUMBER", text: digits, pos: start, num: parseInt(digits, 10) };
3338
+ };
3339
+ while (i < input.length) {
3340
+ const c = input[i];
3341
+ const start = i;
3342
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
3343
+ i++;
3344
+ continue;
3345
+ }
3346
+ if (c === "(") {
3347
+ tokens.push({ type: "LPAREN", text: c, pos: start });
3348
+ i++;
3349
+ continue;
3350
+ }
3351
+ if (c === ")") {
3352
+ tokens.push({ type: "RPAREN", text: c, pos: start });
3353
+ i++;
3354
+ continue;
3355
+ }
3356
+ if (c === ",") {
3357
+ tokens.push({ type: "COMMA", text: c, pos: start });
3358
+ i++;
3359
+ continue;
3360
+ }
3361
+ if (c === ":") {
3362
+ tokens.push({ type: "COLON", text: c, pos: start });
3363
+ i++;
3364
+ continue;
3365
+ }
3366
+ if (c === "*") {
3367
+ tokens.push({ type: "STAR", text: c, pos: start });
3368
+ i++;
3369
+ continue;
3370
+ }
3371
+ if (c === "<" || c === ">") {
3372
+ if (input[i + 1] === "=") {
3373
+ tokens.push({ type: "OP", text: c + "=", pos: start });
3374
+ i += 2;
3375
+ } else {
3376
+ tokens.push({ type: "OP", text: c, pos: start });
3377
+ i++;
3378
+ }
3379
+ continue;
3380
+ }
3381
+ if (c === "!") {
3382
+ if (input[i + 1] === "=") {
3383
+ tokens.push({ type: "OP", text: "!=", pos: start });
3384
+ i += 2;
3385
+ continue;
3386
+ }
3387
+ throw new LexError(start, `Unexpected "!" (did you mean "!="?)`);
3388
+ }
3389
+ if (c === "=") {
3390
+ i += input[i + 1] === "=" ? 2 : 1;
3391
+ tokens.push({ type: "OP", text: "=", pos: start });
3392
+ continue;
3393
+ }
3394
+ if (c === '"' || c === "'") {
3395
+ const quote = c;
3396
+ let j = i + 1;
3397
+ let out = "";
3398
+ while (j < input.length && input[j] !== quote) {
3399
+ if (input[j] === "\\" && j + 1 < input.length) {
3400
+ out += input[j + 1];
3401
+ j += 2;
3402
+ } else {
3403
+ out += input[j];
3404
+ j++;
3405
+ }
3406
+ }
3407
+ if (j >= input.length) throw new LexError(start, "Unterminated string literal");
3408
+ tokens.push({ type: "STRING", text: out, pos: start });
3409
+ i = j + 1;
3410
+ continue;
3411
+ }
3412
+ if (c === "-" || c === "+") {
3413
+ if (/\d/.test(input[i + 1] ?? "")) {
3414
+ const sign = c === "-" ? -1 : 1;
3415
+ i++;
3416
+ tokens.push(numberOrDuration(start, sign));
3417
+ continue;
3418
+ }
3419
+ if (c === "-") {
3420
+ tokens.push({ type: "MINUS", text: "-", pos: start });
3421
+ i++;
3422
+ continue;
3423
+ }
3424
+ throw new LexError(start, 'Unexpected "+"');
3425
+ }
3426
+ if (/\d/.test(c)) {
3427
+ const dateMatch = input.slice(i).match(DATE_RE);
3428
+ if (dateMatch) {
3429
+ tokens.push({ type: "DATE", text: dateMatch[0], pos: start });
3430
+ i += dateMatch[0].length;
3431
+ continue;
3432
+ }
3433
+ tokens.push(numberOrDuration(start, 0));
3434
+ continue;
3435
+ }
3436
+ if (IDENT_START.test(c)) {
3437
+ let j = i + 1;
3438
+ while (j < input.length && IDENT_CHAR.test(input[j])) j++;
3439
+ const word = input.slice(i, j);
3440
+ const kw = word.toLowerCase();
3441
+ if (kw === "and") tokens.push({ type: "AND", text: word, pos: start });
3442
+ else if (kw === "or") tokens.push({ type: "OR", text: word, pos: start });
3443
+ else if (kw === "not") tokens.push({ type: "NOT", text: word, pos: start });
3444
+ else tokens.push({ type: "IDENT", text: word, pos: start });
3445
+ i = j;
3446
+ continue;
3447
+ }
3448
+ throw new LexError(start, `Unexpected character "${c}"`);
3449
+ }
3450
+ tokens.push({ type: "EOF", text: "", pos: input.length });
3451
+ return tokens;
3452
+ }
3453
+ var LexError, DURATION_MS, IDENT_START, IDENT_CHAR, DATE_RE;
3454
+ var init_lexer = __esm({
3455
+ "src/utils/query/lexer.ts"() {
3456
+ "use strict";
3457
+ LexError = class extends Error {
3458
+ constructor(pos, message) {
3459
+ super(message);
3460
+ this.pos = pos;
3461
+ this.name = "LexError";
3462
+ }
3463
+ };
3464
+ DURATION_MS = {
3465
+ h: 36e5,
3466
+ d: 864e5,
3467
+ w: 7 * 864e5,
3468
+ m: 30 * 864e5,
3469
+ mo: 30 * 864e5,
3470
+ y: 365 * 864e5
3471
+ };
3472
+ IDENT_START = /[A-Za-z_]/;
3473
+ IDENT_CHAR = /[A-Za-z0-9_-]/;
3474
+ DATE_RE = /^\d{4}-\d{2}-\d{2}/;
3475
+ }
3476
+ });
3477
+
3478
+ // src/utils/query/parser.ts
3479
+ function parseQuery(input) {
3480
+ try {
3481
+ const tokens = lex(input);
3482
+ const ast = new Parser(tokens).parseQuery();
3483
+ return { ast, errors: [] };
3484
+ } catch (err) {
3485
+ if (err instanceof LexError || err instanceof ParseError) {
3486
+ return { ast: null, errors: [{ pos: err.pos, message: err.message }] };
3487
+ }
3488
+ throw err;
3489
+ }
3490
+ }
3491
+ var ParseError, VALUE_TOKENS, TERM_START, Parser;
3492
+ var init_parser3 = __esm({
3493
+ "src/utils/query/parser.ts"() {
3494
+ "use strict";
3495
+ init_lexer();
3496
+ ParseError = class extends Error {
3497
+ constructor(pos, message) {
3498
+ super(message);
3499
+ this.pos = pos;
3500
+ this.name = "ParseError";
3501
+ }
3502
+ };
3503
+ VALUE_TOKENS = /* @__PURE__ */ new Set(["IDENT", "STRING", "NUMBER", "DATE", "DURATION"]);
3504
+ TERM_START = /* @__PURE__ */ new Set(["IDENT", "NOT", "MINUS", "LPAREN", "STAR"]);
3505
+ Parser = class {
3506
+ constructor(tokens) {
3507
+ this.tokens = tokens;
3508
+ }
3509
+ pos = 0;
3510
+ peek() {
3511
+ return this.tokens[this.pos];
3512
+ }
3513
+ next() {
3514
+ return this.tokens[this.pos++];
3515
+ }
3516
+ expect(type, what) {
3517
+ const tok = this.peek();
3518
+ if (tok.type !== type) {
3519
+ throw new ParseError(tok.pos, `Expected ${what}, got "${tok.text || tok.type}"`);
3520
+ }
3521
+ return this.next();
3522
+ }
3523
+ parseQuery() {
3524
+ if (this.peek().type === "EOF") return { kind: "all" };
3525
+ const node = this.orExpr();
3526
+ const tok = this.peek();
3527
+ if (tok.type !== "EOF") {
3528
+ throw new ParseError(tok.pos, `Unexpected "${tok.text}" \u2014 unbalanced parentheses or stray token`);
3529
+ }
3530
+ return node;
3531
+ }
3532
+ orExpr() {
3533
+ const children = [this.andExpr()];
3534
+ while (this.peek().type === "OR") {
3535
+ this.next();
3536
+ children.push(this.andExpr());
3537
+ }
3538
+ return children.length === 1 ? children[0] : { kind: "or", children };
3539
+ }
3540
+ andExpr() {
3541
+ const children = [this.unary()];
3542
+ for (; ; ) {
3543
+ const tok = this.peek();
3544
+ if (tok.type === "AND") {
3545
+ this.next();
3546
+ children.push(this.unary());
3547
+ } else if (TERM_START.has(tok.type)) {
3548
+ children.push(this.unary());
3549
+ } else {
3550
+ break;
3551
+ }
3552
+ }
3553
+ return children.length === 1 ? children[0] : { kind: "and", children };
3554
+ }
3555
+ unary() {
3556
+ const tok = this.peek();
3557
+ if (tok.type === "NOT") {
3558
+ this.next();
3559
+ return { kind: "not", child: this.unary() };
3560
+ }
3561
+ if (tok.type === "MINUS") {
3562
+ this.next();
3563
+ const inner = this.peek();
3564
+ if (inner.type !== "IDENT") {
3565
+ throw new ParseError(inner.pos, 'Expected a field atom after "-" negation');
3566
+ }
3567
+ return { kind: "not", child: this.atom() };
3568
+ }
3569
+ return this.primary();
3570
+ }
3571
+ primary() {
3572
+ const tok = this.peek();
3573
+ if (tok.type === "LPAREN") {
3574
+ this.next();
3575
+ const node = this.orExpr();
3576
+ this.expect("RPAREN", '")"');
3577
+ return node;
3578
+ }
3579
+ if (tok.type === "STAR") {
3580
+ this.next();
3581
+ return { kind: "all" };
3582
+ }
3583
+ if (tok.type === "IDENT") {
3584
+ return this.atom();
3585
+ }
3586
+ throw new ParseError(tok.pos, `Expected a field, "(", "*", or NOT \u2014 got "${tok.text || "end of query"}"`);
3587
+ }
3588
+ atom() {
3589
+ const fieldTok = this.expect("IDENT", "a field name");
3590
+ const opTok = this.peek();
3591
+ if (opTok.type === "COLON") {
3592
+ this.next();
3593
+ const values = this.valueOrList();
3594
+ return { kind: "atom", field: fieldTok.text, op: ":", values, pos: fieldTok.pos };
3595
+ }
3596
+ if (opTok.type === "OP") {
3597
+ this.next();
3598
+ const value = this.value();
3599
+ return {
3600
+ kind: "atom",
3601
+ field: fieldTok.text,
3602
+ op: opTok.text,
3603
+ values: [value],
3604
+ pos: fieldTok.pos
3605
+ };
3606
+ }
3607
+ throw new ParseError(
3608
+ opTok.pos,
3609
+ `Expected ":" or a comparison operator after field "${fieldTok.text}"`
3610
+ );
3611
+ }
3612
+ valueOrList() {
3613
+ if (this.peek().type === "LPAREN") {
3614
+ this.next();
3615
+ const values = [this.value()];
3616
+ while (this.peek().type === "COMMA") {
3617
+ this.next();
3618
+ values.push(this.value());
3619
+ }
3620
+ this.expect("RPAREN", '")" to close the value list');
3621
+ return values;
3622
+ }
3623
+ return [this.value()];
3624
+ }
3625
+ value() {
3626
+ const tok = this.peek();
3627
+ if (!VALUE_TOKENS.has(tok.type)) {
3628
+ throw new ParseError(tok.pos, `Expected a value, got "${tok.text || tok.type}"`);
3629
+ }
3630
+ this.next();
3631
+ switch (tok.type) {
3632
+ case "STRING":
3633
+ return { type: "string", raw: tok.text, pos: tok.pos };
3634
+ case "NUMBER":
3635
+ return { type: "number", raw: tok.text, num: tok.num, pos: tok.pos };
3636
+ case "DATE":
3637
+ return { type: "date", raw: tok.text, pos: tok.pos };
3638
+ case "DURATION":
3639
+ return { type: "duration", raw: tok.text, num: tok.num, sign: tok.sign ?? 0, pos: tok.pos };
3640
+ default:
3641
+ return { type: "word", raw: tok.text, pos: tok.pos };
3642
+ }
3643
+ }
3644
+ };
3645
+ }
3646
+ });
3647
+
3648
+ // src/utils/query/index.ts
3649
+ var init_query = __esm({
3650
+ "src/utils/query/index.ts"() {
3651
+ "use strict";
3652
+ init_evaluate();
3653
+ init_fields();
3654
+ init_parser3();
3655
+ init_parser3();
3656
+ init_evaluate();
3657
+ init_fields();
3658
+ }
3659
+ });
3660
+
3661
+ // src/lifecycle/derive.ts
3662
+ var derive_exports = {};
3663
+ __export(derive_exports, {
3664
+ DERIVE_FIELDS: () => DERIVE_FIELDS,
3665
+ deriveDimensions: () => deriveDimensions,
3666
+ validateDeriveCondition: () => validateDeriveCondition
3667
+ });
3668
+ function validateDeriveCondition(when) {
3669
+ if (when === "*") return null;
3670
+ const parsed = parseQuery(when);
3671
+ if (!parsed.ast) return parsed.errors[0]?.message ?? "unparseable condition";
3672
+ try {
3673
+ compileNode(parsed.ast, DERIVE_FIELDS);
3674
+ return null;
3675
+ } catch (err) {
3676
+ if (err instanceof CompileError) return err.errors[0]?.message ?? "invalid condition";
3677
+ throw err;
3678
+ }
3679
+ }
3680
+ function compiledWhen(derive, when) {
3681
+ let cache = conditionCache.get(derive);
3682
+ if (!cache) {
3683
+ cache = /* @__PURE__ */ new Map();
3684
+ conditionCache.set(derive, cache);
3685
+ }
3686
+ let pred = cache.get(when);
3687
+ if (!pred) {
3688
+ if (when === "*") {
3689
+ pred = () => true;
3690
+ } else {
3691
+ const parsed = parseQuery(when);
3692
+ if (!parsed.ast) {
3693
+ throw new CompileError(parsed.errors);
3694
+ }
3695
+ pred = compileNode(parsed.ast, DERIVE_FIELDS);
3696
+ }
3697
+ cache.set(when, pred);
3698
+ }
3699
+ return pred;
3700
+ }
3701
+ function deriveDimensions(input) {
3702
+ const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
3703
+ if (terminalStatuses.has(currentStatus)) return null;
3704
+ const ctx = { now: 0 };
3705
+ const item = facts;
3706
+ let phase = derive.phaseLadder[0]?.phase ?? currentStatus;
3707
+ let nextAction = derive.phaseLadder[0]?.next ?? null;
3708
+ for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {
3709
+ const rung = derive.phaseLadder[i];
3710
+ if (compiledWhen(derive, rung.when)(item, ctx)) {
3711
+ phase = rung.phase;
3712
+ nextAction = rung.next ?? null;
3713
+ break;
3714
+ }
3715
+ }
3716
+ let disposition = "active";
3717
+ for (const rule of derive.disposition) {
3718
+ if (rule.when === null || compiledWhen(derive, rule.when)(item, ctx)) {
3719
+ disposition = rule.is;
3720
+ break;
3721
+ }
3722
+ }
3723
+ let derivedStatus;
3724
+ switch (disposition) {
3725
+ case "parked":
3726
+ derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;
3727
+ break;
3728
+ case "blocked":
3729
+ derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;
3730
+ break;
3731
+ default:
3732
+ derivedStatus = phase;
3733
+ }
3734
+ let status = derivedStatus;
3735
+ if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
3736
+ status = override.status;
3737
+ }
3738
+ return { phase, disposition, derivedStatus, status, nextAction };
3739
+ }
3740
+ var DERIVE_FIELDS, conditionCache;
3741
+ var init_derive = __esm({
3742
+ "src/lifecycle/derive.ts"() {
3743
+ "use strict";
3744
+ init_query();
3745
+ DERIVE_FIELDS = {
3746
+ hasrealobjective: { kind: "bool", get: (i) => i["hasRealObjective"] },
3747
+ acrealtotal: { kind: "number", get: (i) => i["acRealTotal"] },
3748
+ acrealchecked: { kind: "number", get: (i) => i["acRealChecked"] },
3749
+ acallchecked: { kind: "bool", get: (i) => i["acAllChecked"] },
3750
+ planexists: { kind: "bool", get: (i) => i["planExists"] },
3751
+ planapproved: { kind: "bool", get: (i) => i["planApproved"] },
3752
+ workspaceset: { kind: "bool", get: (i) => i["workspaceSet"] },
3753
+ implementationstarted: { kind: "bool", get: (i) => i["implementationStarted"] },
3754
+ depssatisfied: { kind: "bool", get: (i) => i["depsSatisfied"] },
3755
+ unresolvedquestions: { kind: "number", get: (i) => i["unresolvedQuestions"] },
3756
+ blocked: { kind: "bool" },
3757
+ parked: { kind: "bool" },
3758
+ reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
3759
+ pinned: { kind: "bool" }
3760
+ };
3761
+ conditionCache = /* @__PURE__ */ new WeakMap();
3762
+ }
3763
+ });
3764
+
1968
3765
  // src/dashboard/api.ts
1969
- import { readdir as readdir6, readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
1970
- import { resolve as resolve11, dirname as dirname2, basename } from "path";
3766
+ import { readdir as readdir7, readFile as readFile10, writeFile as writeFile3 } from "fs/promises";
3767
+ import { resolve as resolve12, dirname as dirname2, basename } from "path";
1971
3768
  function activeAssignments(items) {
1972
3769
  return items.filter((item) => item.archived !== true);
1973
3770
  }
@@ -1987,15 +3784,15 @@ async function listStandaloneRecords(assignmentsDir) {
1987
3784
  async function computeStandaloneRecords(assignmentsDir) {
1988
3785
  if (!assignmentsDir) return [];
1989
3786
  if (!await fileExists(assignmentsDir)) return [];
1990
- const entries = await readdir6(assignmentsDir, { withFileTypes: true });
3787
+ const entries = await readdir7(assignmentsDir, { withFileTypes: true });
1991
3788
  const records = [];
1992
3789
  for (const entry of entries) {
1993
3790
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
1994
- const assignmentDir = resolve11(assignmentsDir, entry.name);
1995
- const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
3791
+ const assignmentDir = resolve12(assignmentsDir, entry.name);
3792
+ const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
1996
3793
  if (!await fileExists(assignmentMdPath)) continue;
1997
3794
  try {
1998
- const content = await readFile9(assignmentMdPath, "utf-8");
3795
+ const content = await readFile10(assignmentMdPath, "utf-8");
1999
3796
  const record = parseAssignmentFull(content);
2000
3797
  records.push({ assignmentDir, id: entry.name, record });
2001
3798
  } catch {
@@ -2036,7 +3833,8 @@ async function getStatusConfig() {
2036
3833
  order: config.statuses.order,
2037
3834
  transitions: effectiveTransitions,
2038
3835
  transitionTable: buildTransitionTable(effectiveTransitions),
2039
- terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
3836
+ terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"]),
3837
+ derive: config.statuses.derive ?? null
2040
3838
  };
2041
3839
  } else {
2042
3840
  const def = buildDefaultStatusConfig();
@@ -2046,7 +3844,8 @@ async function getStatusConfig() {
2046
3844
  order: def.order,
2047
3845
  transitions: def.transitions,
2048
3846
  transitionTable: DEFAULT_TRANSITION_TABLE,
2049
- terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
3847
+ terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"]),
3848
+ derive: null
2050
3849
  };
2051
3850
  }
2052
3851
  return _cachedConfig;
@@ -2076,23 +3875,23 @@ async function getStandaloneAvailableTransitions(assignment) {
2076
3875
  return actions;
2077
3876
  }
2078
3877
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2079
- const assignmentDir = resolve11(projectsDir, projectSlug, "assignments", assignmentSlug);
2080
- const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
3878
+ const assignmentDir = resolve12(projectsDir, projectSlug, "assignments", assignmentSlug);
3879
+ const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
2081
3880
  if (!await fileExists(assignmentMdPath)) {
2082
3881
  return null;
2083
3882
  }
2084
- const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
3883
+ const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
2085
3884
  const assignment = parseAssignmentFull(assignmentContent);
2086
3885
  let projectWorkspace = null;
2087
- const projectMdPath = resolve11(projectsDir, projectSlug, "project.md");
3886
+ const projectMdPath = resolve12(projectsDir, projectSlug, "project.md");
2088
3887
  if (await fileExists(projectMdPath)) {
2089
- const projectContent = await readFile9(projectMdPath, "utf-8");
3888
+ const projectContent = await readFile10(projectMdPath, "utf-8");
2090
3889
  projectWorkspace = parseProject(projectContent).workspace;
2091
3890
  }
2092
3891
  let plan = null;
2093
- const planPath = resolve11(assignmentDir, "plan.md");
3892
+ const planPath = resolve12(assignmentDir, "plan.md");
2094
3893
  if (await fileExists(planPath)) {
2095
- const planContent = await readFile9(planPath, "utf-8");
3894
+ const planContent = await readFile10(planPath, "utf-8");
2096
3895
  const parsed = parsePlan(planContent);
2097
3896
  plan = {
2098
3897
  status: parsed.status,
@@ -2101,9 +3900,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2101
3900
  };
2102
3901
  }
2103
3902
  let scratchpad = null;
2104
- const scratchpadPath = resolve11(assignmentDir, "scratchpad.md");
3903
+ const scratchpadPath = resolve12(assignmentDir, "scratchpad.md");
2105
3904
  if (await fileExists(scratchpadPath)) {
2106
- const scratchpadContent = await readFile9(scratchpadPath, "utf-8");
3905
+ const scratchpadContent = await readFile10(scratchpadPath, "utf-8");
2107
3906
  const parsed = parseScratchpad(scratchpadContent);
2108
3907
  scratchpad = {
2109
3908
  updated: parsed.updated,
@@ -2111,9 +3910,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2111
3910
  };
2112
3911
  }
2113
3912
  let handoff = null;
2114
- const handoffPath = resolve11(assignmentDir, "handoff.md");
3913
+ const handoffPath = resolve12(assignmentDir, "handoff.md");
2115
3914
  if (await fileExists(handoffPath)) {
2116
- const handoffContent = await readFile9(handoffPath, "utf-8");
3915
+ const handoffContent = await readFile10(handoffPath, "utf-8");
2117
3916
  const parsed = parseHandoff(handoffContent);
2118
3917
  handoff = {
2119
3918
  updated: parsed.updated,
@@ -2122,9 +3921,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2122
3921
  };
2123
3922
  }
2124
3923
  let decisionRecord = null;
2125
- const decisionRecordPath = resolve11(assignmentDir, "decision-record.md");
3924
+ const decisionRecordPath = resolve12(assignmentDir, "decision-record.md");
2126
3925
  if (await fileExists(decisionRecordPath)) {
2127
- const decisionRecordContent = await readFile9(decisionRecordPath, "utf-8");
3926
+ const decisionRecordContent = await readFile10(decisionRecordPath, "utf-8");
2128
3927
  const parsed = parseDecisionRecord(decisionRecordContent);
2129
3928
  decisionRecord = {
2130
3929
  updated: parsed.updated,
@@ -2133,9 +3932,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2133
3932
  };
2134
3933
  }
2135
3934
  let progress = null;
2136
- const progressPath = resolve11(assignmentDir, "progress.md");
3935
+ const progressPath = resolve12(assignmentDir, "progress.md");
2137
3936
  if (await fileExists(progressPath)) {
2138
- const progressContent = await readFile9(progressPath, "utf-8");
3937
+ const progressContent = await readFile10(progressPath, "utf-8");
2139
3938
  const parsed = parseProgress(progressContent);
2140
3939
  progress = {
2141
3940
  updated: parsed.updated,
@@ -2144,9 +3943,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2144
3943
  };
2145
3944
  }
2146
3945
  let comments = null;
2147
- const commentsPath = resolve11(assignmentDir, "comments.md");
3946
+ const commentsPath = resolve12(assignmentDir, "comments.md");
2148
3947
  if (await fileExists(commentsPath)) {
2149
- const commentsContent = await readFile9(commentsPath, "utf-8");
3948
+ const commentsContent = await readFile10(commentsPath, "utf-8");
2150
3949
  const parsed = parseComments(commentsContent);
2151
3950
  comments = {
2152
3951
  updated: parsed.updated,
@@ -2154,6 +3953,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2154
3953
  entries: parsed.entries
2155
3954
  };
2156
3955
  }
3956
+ const { terminalStatuses } = await getStatusConfig();
2157
3957
  const detail = {
2158
3958
  id: assignment.id,
2159
3959
  projectSlug,
@@ -2175,6 +3975,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2175
3975
  archived: assignment.archived,
2176
3976
  archivedAt: assignment.archivedAt,
2177
3977
  archivedReason: assignment.archivedReason,
3978
+ ...deriveStatusVirtuals(assignment, terminalStatuses),
3979
+ override: assignment.override,
3980
+ derived: await buildDerivedDetail(assignment, assignmentDir, resolve12(projectsDir, projectSlug)),
2178
3981
  created: assignment.created,
2179
3982
  updated: assignment.updated,
2180
3983
  body: assignment.body,
@@ -2265,7 +4068,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2265
4068
  slug: a.slug,
2266
4069
  title: a.title,
2267
4070
  projectSlug: rec.summary.slug,
2268
- assignmentDir: resolve11(rec.projectPath, "assignments", a.slug)
4071
+ assignmentDir: resolve12(rec.projectPath, "assignments", a.slug)
2269
4072
  });
2270
4073
  }
2271
4074
  }
@@ -2298,17 +4101,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2298
4101
  }
2299
4102
  async function countMentionsInAssignment(sourceDir, target) {
2300
4103
  const bodies = [];
2301
- const assignmentMd = resolve11(sourceDir, "assignment.md");
4104
+ const assignmentMd = resolve12(sourceDir, "assignment.md");
2302
4105
  if (await fileExists(assignmentMd)) {
2303
- const content = await readFile9(assignmentMd, "utf-8");
4106
+ const content = await readFile10(assignmentMd, "utf-8");
2304
4107
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
2305
4108
  if (todosMatch) bodies.push(todosMatch[1]);
2306
4109
  }
2307
4110
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
2308
- const path = resolve11(sourceDir, filename);
4111
+ const path = resolve12(sourceDir, filename);
2309
4112
  if (await fileExists(path)) {
2310
4113
  try {
2311
- bodies.push(await readFile9(path, "utf-8"));
4114
+ bodies.push(await readFile10(path, "utf-8"));
2312
4115
  } catch {
2313
4116
  }
2314
4117
  }
@@ -2366,46 +4169,47 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
2366
4169
  }
2367
4170
  async function buildStandaloneAssignmentDetail(resolved) {
2368
4171
  const assignmentDir = resolved.assignmentDir;
2369
- const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
4172
+ const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
2370
4173
  if (!await fileExists(assignmentMdPath)) return null;
2371
- const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
4174
+ const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
2372
4175
  const assignment = parseAssignmentFull(assignmentContent);
2373
4176
  let plan = null;
2374
- const planPath = resolve11(assignmentDir, "plan.md");
4177
+ const planPath = resolve12(assignmentDir, "plan.md");
2375
4178
  if (await fileExists(planPath)) {
2376
- const parsed = parsePlan(await readFile9(planPath, "utf-8"));
4179
+ const parsed = parsePlan(await readFile10(planPath, "utf-8"));
2377
4180
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
2378
4181
  }
2379
4182
  let scratchpad = null;
2380
- const scratchpadPath = resolve11(assignmentDir, "scratchpad.md");
4183
+ const scratchpadPath = resolve12(assignmentDir, "scratchpad.md");
2381
4184
  if (await fileExists(scratchpadPath)) {
2382
- const parsed = parseScratchpad(await readFile9(scratchpadPath, "utf-8"));
4185
+ const parsed = parseScratchpad(await readFile10(scratchpadPath, "utf-8"));
2383
4186
  scratchpad = { updated: parsed.updated, body: parsed.body };
2384
4187
  }
2385
4188
  let handoff = null;
2386
- const handoffPath = resolve11(assignmentDir, "handoff.md");
4189
+ const handoffPath = resolve12(assignmentDir, "handoff.md");
2387
4190
  if (await fileExists(handoffPath)) {
2388
- const parsed = parseHandoff(await readFile9(handoffPath, "utf-8"));
4191
+ const parsed = parseHandoff(await readFile10(handoffPath, "utf-8"));
2389
4192
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
2390
4193
  }
2391
4194
  let decisionRecord = null;
2392
- const decisionRecordPath = resolve11(assignmentDir, "decision-record.md");
4195
+ const decisionRecordPath = resolve12(assignmentDir, "decision-record.md");
2393
4196
  if (await fileExists(decisionRecordPath)) {
2394
- const parsed = parseDecisionRecord(await readFile9(decisionRecordPath, "utf-8"));
4197
+ const parsed = parseDecisionRecord(await readFile10(decisionRecordPath, "utf-8"));
2395
4198
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
2396
4199
  }
2397
4200
  let progress = null;
2398
- const progressPath = resolve11(assignmentDir, "progress.md");
4201
+ const progressPath = resolve12(assignmentDir, "progress.md");
2399
4202
  if (await fileExists(progressPath)) {
2400
- const parsed = parseProgress(await readFile9(progressPath, "utf-8"));
4203
+ const parsed = parseProgress(await readFile10(progressPath, "utf-8"));
2401
4204
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2402
4205
  }
2403
4206
  let comments = null;
2404
- const commentsPath = resolve11(assignmentDir, "comments.md");
4207
+ const commentsPath = resolve12(assignmentDir, "comments.md");
2405
4208
  if (await fileExists(commentsPath)) {
2406
- const parsed = parseComments(await readFile9(commentsPath, "utf-8"));
4209
+ const parsed = parseComments(await readFile10(commentsPath, "utf-8"));
2407
4210
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2408
4211
  }
4212
+ const { terminalStatuses } = await getStatusConfig();
2409
4213
  const detail = {
2410
4214
  id: assignment.id,
2411
4215
  projectSlug: null,
@@ -2428,6 +4232,9 @@ async function buildStandaloneAssignmentDetail(resolved) {
2428
4232
  archived: assignment.archived,
2429
4233
  archivedAt: assignment.archivedAt,
2430
4234
  archivedReason: assignment.archivedReason,
4235
+ ...deriveStatusVirtuals(assignment, terminalStatuses),
4236
+ override: assignment.override,
4237
+ derived: await buildDerivedDetail(assignment, assignmentDir, null),
2431
4238
  created: assignment.created,
2432
4239
  updated: assignment.updated,
2433
4240
  body: assignment.body,
@@ -2459,17 +4266,17 @@ async function computeProjectRecords(projectsDir, traces) {
2459
4266
  await migrateLegacyProjectFiles(projectsDir);
2460
4267
  await migrateLegacyArchivedProjects(projectsDir);
2461
4268
  }
2462
- const entries = await readdir6(projectsDir, { withFileTypes: true });
4269
+ const entries = await readdir7(projectsDir, { withFileTypes: true });
2463
4270
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
2464
4271
  const maybeRecords = await Promise.all(
2465
4272
  projectDirs.map(async (entry) => {
2466
- const projectPath = resolve11(projectsDir, entry.name);
2467
- const projectMdPath = resolve11(projectPath, "project.md");
4273
+ const projectPath = resolve12(projectsDir, entry.name);
4274
+ const projectMdPath = resolve12(projectPath, "project.md");
2468
4275
  if (!await fileExists(projectMdPath)) {
2469
4276
  return null;
2470
4277
  }
2471
4278
  const t0 = traces ? performance.now() : 0;
2472
- const projectContent = await readFile9(projectMdPath, "utf-8");
4279
+ const projectContent = await readFile10(projectMdPath, "utf-8");
2473
4280
  const project = parseProject(projectContent);
2474
4281
  if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
2475
4282
  const t1 = traces ? performance.now() : 0;
@@ -2510,20 +4317,20 @@ async function computeProjectRecords(projectsDir, traces) {
2510
4317
  return records;
2511
4318
  }
2512
4319
  async function listAssignmentRecords(projectPath, traces) {
2513
- const assignmentsDir = resolve11(projectPath, "assignments");
4320
+ const assignmentsDir = resolve12(projectPath, "assignments");
2514
4321
  if (!await fileExists(assignmentsDir)) {
2515
4322
  return [];
2516
4323
  }
2517
- const entries = await readdir6(assignmentsDir, { withFileTypes: true });
4324
+ const entries = await readdir7(assignmentsDir, { withFileTypes: true });
2518
4325
  const dirEntries = entries.filter((entry) => entry.isDirectory());
2519
4326
  const maybeRecords = await Promise.all(
2520
4327
  dirEntries.map(async (entry) => {
2521
- const assignmentMd = resolve11(assignmentsDir, entry.name, "assignment.md");
4328
+ const assignmentMd = resolve12(assignmentsDir, entry.name, "assignment.md");
2522
4329
  if (!await fileExists(assignmentMd)) {
2523
4330
  return null;
2524
4331
  }
2525
4332
  const t0 = traces ? performance.now() : 0;
2526
- const content = await readFile9(assignmentMd, "utf-8");
4333
+ const content = await readFile10(assignmentMd, "utf-8");
2527
4334
  const parsed = parseAssignmentFull(content);
2528
4335
  if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
2529
4336
  return parsed;
@@ -2534,9 +4341,9 @@ async function listAssignmentRecords(projectPath, traces) {
2534
4341
  return records;
2535
4342
  }
2536
4343
  async function loadDependencyGraph(projectPath, assignments) {
2537
- const statusPath = resolve11(projectPath, "_status.md");
4344
+ const statusPath = resolve12(projectPath, "_status.md");
2538
4345
  if (await fileExists(statusPath)) {
2539
- const statusContent = await readFile9(statusPath, "utf-8");
4346
+ const statusContent = await readFile10(statusPath, "utf-8");
2540
4347
  const parsed = parseStatus(statusContent);
2541
4348
  const derivedGraph = extractMermaidGraph(parsed.body);
2542
4349
  if (derivedGraph) {
@@ -2586,6 +4393,78 @@ async function buildProjectRollup(projectPath, project, assignments, traces) {
2586
4393
  }
2587
4394
  return { progress, needsAttention, status };
2588
4395
  }
4396
+ function deriveStatusVirtuals(assignment, terminalStatuses) {
4397
+ const hist = assignment.statusHistory ?? [];
4398
+ let completedAt = null;
4399
+ if (terminalStatuses.has(assignment.status)) {
4400
+ for (const entry of hist) {
4401
+ if (entry.to === assignment.status) completedAt = entry.at;
4402
+ }
4403
+ }
4404
+ let statusAge = null;
4405
+ for (let i = hist.length - 1; i >= 0; i--) {
4406
+ const entry = hist[i];
4407
+ if (entry.from !== entry.to || entry.from === null) {
4408
+ const t = Date.parse(entry.at);
4409
+ statusAge = Number.isNaN(t) ? null : Date.now() - t;
4410
+ break;
4411
+ }
4412
+ }
4413
+ let phaseAge = null;
4414
+ for (let i = hist.length - 1; i >= 0; i--) {
4415
+ const entry = hist[i];
4416
+ if (entry.phaseTo !== void 0 && entry.phaseFrom !== entry.phaseTo) {
4417
+ const t = Date.parse(entry.at);
4418
+ phaseAge = Number.isNaN(t) ? null : Date.now() - t;
4419
+ break;
4420
+ }
4421
+ }
4422
+ return {
4423
+ completedAt,
4424
+ statusAge,
4425
+ phaseAge,
4426
+ phase: assignment.phase,
4427
+ disposition: assignment.disposition,
4428
+ pinned: assignment.override !== null
4429
+ };
4430
+ }
4431
+ async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
4432
+ const config = await getStatusConfig();
4433
+ if (config.terminalStatuses.has(assignment.status)) return null;
4434
+ try {
4435
+ const { computeFacts: computeFacts2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
4436
+ const { deriveDimensions: deriveDimensions2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
4437
+ const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
4438
+ const facts = await computeFacts2({
4439
+ assignmentDir,
4440
+ frontmatter: {
4441
+ ...assignment
4442
+ // AssignmentRecord ⊃ the fields computeFacts reads; statusHistory and
4443
+ // derived caches ride along untouched.
4444
+ },
4445
+ body: assignment.body,
4446
+ projectDir,
4447
+ terminalStatuses: config.terminalStatuses
4448
+ });
4449
+ const dims = deriveDimensions2({
4450
+ facts,
4451
+ derive: config.derive ?? DEFAULT_DERIVE_CONFIG2,
4452
+ currentStatus: assignment.status,
4453
+ terminalStatuses: config.terminalStatuses,
4454
+ knownStatusIds: new Set(config.statuses.map((s) => s.id)),
4455
+ override: assignment.override
4456
+ });
4457
+ if (!dims) return null;
4458
+ return {
4459
+ derivedStatus: dims.derivedStatus,
4460
+ nextAction: dims.nextAction,
4461
+ facts
4462
+ };
4463
+ } catch (err) {
4464
+ console.warn(`buildDerivedDetail failed for ${assignmentDir}:`, err);
4465
+ return null;
4466
+ }
4467
+ }
2589
4468
  function buildDependencyGraph(assignments) {
2590
4469
  const edges = [];
2591
4470
  const usedStatuses = /* @__PURE__ */ new Set();
@@ -2616,7 +4495,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
2616
4495
  const config = await getStatusConfig();
2617
4496
  const transitionDefs = getTransitionDefinitions(config);
2618
4497
  const actions = [];
2619
- const projectPath = resolve11(projectsDir, projectSlug);
4498
+ const projectPath = resolve12(projectsDir, projectSlug);
2620
4499
  const traces = options?.traces;
2621
4500
  for (const definition of transitionDefs) {
2622
4501
  const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
@@ -2664,12 +4543,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
2664
4543
  continue;
2665
4544
  }
2666
4545
  }
2667
- const dependencyPath = resolve11(projectPath, "assignments", dependency, "assignment.md");
4546
+ const dependencyPath = resolve12(projectPath, "assignments", dependency, "assignment.md");
2668
4547
  if (!await fileExists(dependencyPath)) {
2669
4548
  unmet.push(`${dependency} (missing)`);
2670
4549
  continue;
2671
4550
  }
2672
- const content = await readFile9(dependencyPath, "utf-8");
4551
+ const content = await readFile10(dependencyPath, "utf-8");
2673
4552
  const parsed = parseAssignmentFull(content);
2674
4553
  if (!terminals.has(parsed.status)) {
2675
4554
  unmet.push(`${dependency} (${parsed.status})`);
@@ -2685,7 +4564,7 @@ function parseTimestamp(timestamp) {
2685
4564
  return Number.isFinite(parsed) ? parsed : 0;
2686
4565
  }
2687
4566
  async function countOpenQuestions(projectPath, assignmentSlug) {
2688
- const commentsPath = resolve11(
4567
+ const commentsPath = resolve12(
2689
4568
  projectPath,
2690
4569
  "assignments",
2691
4570
  assignmentSlug,
@@ -2695,7 +4574,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
2695
4574
  return 0;
2696
4575
  }
2697
4576
  try {
2698
- const content = await readFile9(commentsPath, "utf-8");
4577
+ const content = await readFile10(commentsPath, "utf-8");
2699
4578
  const parsed = parseComments(content);
2700
4579
  return parsed.entries.filter(
2701
4580
  (e) => e.type === "question" && e.resolved !== true
@@ -2951,7 +4830,7 @@ init_api();
2951
4830
  init_agents_schema();
2952
4831
  import { spawn } from "child_process";
2953
4832
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
2954
- import { isAbsolute as isAbsolute3, resolve as resolve12 } from "path";
4833
+ import { isAbsolute as isAbsolute3, resolve as resolve13 } from "path";
2955
4834
 
2956
4835
  // src/launch/cwd.ts
2957
4836
  import { existsSync, statSync } from "fs";
@@ -3327,7 +5206,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
3327
5206
  `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
3328
5207
  );
3329
5208
  }
3330
- await new Promise((resolve14, reject) => {
5209
+ await new Promise((resolve15, reject) => {
3331
5210
  let settled = false;
3332
5211
  let stderr = "";
3333
5212
  const finishOk = () => {
@@ -3337,7 +5216,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
3337
5216
  child.unref();
3338
5217
  } catch {
3339
5218
  }
3340
- resolve14();
5219
+ resolve15();
3341
5220
  };
3342
5221
  const finishErr = (remediation) => {
3343
5222
  if (settled) return;
@@ -3522,7 +5401,7 @@ function appleScriptString(value) {
3522
5401
  init_paths();
3523
5402
  init_fs();
3524
5403
  import { fileURLToPath } from "url";
3525
- import { dirname as dirname3, resolve as resolve13, join as join3 } from "path";
5404
+ import { dirname as dirname3, resolve as resolve14, join as join3 } from "path";
3526
5405
  import { realpathSync, readFileSync, mkdirSync } from "fs";
3527
5406
  var NPX_PATTERNS = [
3528
5407
  { kind: "npm", re: /\/_npx\/([^/]+)\/node_modules(?:\/|$)/ },
@@ -3548,7 +5427,7 @@ function normalizeSlashes(p) {
3548
5427
  }
3549
5428
  function detectInstallKind(scriptUrl, opts = {}) {
3550
5429
  const realpath = opts.realpath ?? realpathSync.native;
3551
- const readFile10 = opts.readFile ?? ((p) => readFileSync(p, "utf-8"));
5430
+ const readFile11 = opts.readFile ?? ((p) => readFileSync(p, "utf-8"));
3552
5431
  const ua = opts.envUserAgent !== void 0 ? opts.envUserAgent : process.env.npm_config_user_agent ?? "";
3553
5432
  const resolved = resolveScriptPath(scriptUrl, realpath);
3554
5433
  if (resolved === null) {
@@ -3569,7 +5448,7 @@ function detectInstallKind(scriptUrl, opts = {}) {
3569
5448
  const pkgJsonPath = join3(dir, "package.json");
3570
5449
  let raw;
3571
5450
  try {
3572
- raw = readFile10(pkgJsonPath);
5451
+ raw = readFile11(pkgJsonPath);
3573
5452
  } catch {
3574
5453
  const parent2 = dirname3(dir);
3575
5454
  if (parent2 === dir) break;
@@ -3601,7 +5480,7 @@ function extractNpxHash(scriptUrl, opts = {}) {
3601
5480
  return null;
3602
5481
  }
3603
5482
  function nudgeStampDir() {
3604
- return resolve13(syntaurRoot(), "npx-handler-nudge");
5483
+ return resolve14(syntaurRoot(), "npx-handler-nudge");
3605
5484
  }
3606
5485
  function sanitizeHash(hash) {
3607
5486
  return hash.replace(/[^A-Za-z0-9_-]/g, "_") || "_";