syntaur 0.41.3 → 0.43.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 (70) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-ZMecMDb-.js → _basePickBy-DzTDKHYU.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-tVAi81J9.js → _baseUniq-DW7YJayj.js} +1 -1
  4. package/dashboard/dist/assets/{arc-BMB5nyvD.js → arc-BkeAbQzV.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-1_ttZgNb.js → architectureDiagram-2XIMDMQ5-M_COTMq5.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BAuowdCy.js → blockDiagram-WCTKOSBZ-BMSQm_8K.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-DgqpYWNQ.js → c4Diagram-IC4MRINW-Bte3s8bA.js} +1 -1
  8. package/dashboard/dist/assets/channel-BrnqMVFz.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-Dul2PJq1.js → chunk-4BX2VUAB-BpEBr9cy.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-BNsItfH6.js → chunk-55IACEB6-C5vYlqEx.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-C3btqaF-.js → chunk-FMBD7UC4-C8wRljiW.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-BjrX5MC8.js → chunk-JSJVCQXG-svArzivt.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-xMrVR3MQ.js → chunk-KX2RTZJC-DPZbN7jl.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-Dd67ojLx.js → chunk-NQ4KR5QH-ZqOPA0Z2.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-Dqombp1k.js → chunk-QZHKN3VN-BoxtsyU8.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-CCYZi5vV.js → chunk-WL4C6EOR-BoTByw-8.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BfXVQbAH.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BfXVQbAH.js +1 -0
  19. package/dashboard/dist/assets/clone-Bm7jyblm.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CYmvV1RQ.js → cose-bilkent-S5V4N54A-CV74D8s_.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-DXFT_Iz5.js → dagre-KLK3FWXG-NXFGSDTv.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-CkCF8e9h.js → diagram-E7M64L7V-CO89mocJ.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-CqIk_zgE.js → diagram-IFDJBPK2-CGOOte10.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-BV-579JX.js → diagram-P4PSJMXO-tRYoQWNN.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-CeFxcSCp.js → erDiagram-INFDFZHY-D88FnNdx.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-D2IGFjb7.js → flowDiagram-PKNHOUZH-C2dCJPYX.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CctYGwR7.js → ganttDiagram-A5KZAMGK-BVsm_TXU.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-zt67Doyo.js → gitGraphDiagram-K3NZZRJ6-DeYEcfwg.js} +1 -1
  29. package/dashboard/dist/assets/{graph-DZiyaEGT.js → graph-iy97f49o.js} +1 -1
  30. package/dashboard/dist/assets/index-BqxbS9Wf.css +1 -0
  31. package/dashboard/dist/assets/index-esLcRMJY.js +566 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-jvCdU_x3.js → infoDiagram-LFFYTUFH-B_KPLd9N.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-B3LJ4cf3.js → ishikawaDiagram-PHBUUO56-D0i4hG75.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CtEednnS.js → journeyDiagram-4ABVD52K-DxAv-6Dt.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-BhPQqjK5.js → kanban-definition-K7BYSVSG-CFV3hwVv.js} +1 -1
  36. package/dashboard/dist/assets/{layout-DBtJEEbD.js → layout-DkzGMO0p.js} +1 -1
  37. package/dashboard/dist/assets/{linear-CSinjjkH.js → linear-Di0t2dWQ.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-Cu6allRd.js → mermaid.core-BWdLQDB3.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-SFkGLxic.js → mindmap-definition-YRQLILUH-DHFtApR_.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CM63Vi0v.js → pieDiagram-SKSYHLDU-CKv_gkpa.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-KNLfFOZV.js → quadrantDiagram-337W2JSQ-D8d0F939.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CUTnDKwW.js → requirementDiagram-Z7DCOOCP-BpIRQ6xS.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-BJh1laae.js → sankeyDiagram-WA2Y5GQK-B6pEFLq9.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DLrWjp9t.js → sequenceDiagram-2WXFIKYE-C_UzJJ9L.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-AYRZyAEg.js → stateDiagram-RAJIS63D-DffNJyzP.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Bb4Jseta.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BG2DbYEn.js → timeline-definition-YZTLITO2-BiMYMTvs.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-D8ZVNAo3.js → treemap-KZPCXAKY-CUEZz4mB.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-BqBHdSLd.js → vennDiagram-LZ73GAT5-Cpol_IHC.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DBgm8PRH.js → xychartDiagram-JWTSCODW-CbnCJEuN.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +2584 -431
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +4043 -999
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +159 -6
  57. package/dist/launch/index.js +2162 -147
  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-CIhg4t-B.js +0 -1
  65. package/dashboard/dist/assets/classDiagram-VBA2DB6C-Bmt2MjbS.js +0 -1
  66. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Bmt2MjbS.js +0 -1
  67. package/dashboard/dist/assets/clone-BfVm3try.js +0 -1
  68. package/dashboard/dist/assets/index-6uihSopA.css +0 -1
  69. package/dashboard/dist/assets/index-GK0h-4Nt.js +0 -566
  70. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BRhe6_kr.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;
@@ -39,6 +44,9 @@ function syntaurRoot() {
39
44
  function defaultProjectDir() {
40
45
  return resolve(syntaurRoot(), "projects");
41
46
  }
47
+ function playbooksDir() {
48
+ return resolve(syntaurRoot(), "playbooks");
49
+ }
42
50
  var init_paths = __esm({
43
51
  "src/utils/paths.ts"() {
44
52
  "use strict";
@@ -76,6 +84,27 @@ var init_fs = __esm({
76
84
  });
77
85
 
78
86
  // src/templates/config.ts
87
+ function renderConfig(params) {
88
+ return `---
89
+ version: "1.0"
90
+ defaultProjectDir: ${params.defaultProjectDir}
91
+ onboarding:
92
+ completed: false
93
+ agentDefaults:
94
+ trustLevel: medium
95
+ autoApprove: false
96
+ backup:
97
+ repo: null
98
+ categories: projects, playbooks, todos, servers, config
99
+ lastBackup: null
100
+ lastRestore: null
101
+ ---
102
+
103
+ # Syntaur Configuration
104
+
105
+ Global configuration for the Syntaur CLI.
106
+ `;
107
+ }
79
108
  var init_config = __esm({
80
109
  "src/templates/config.ts"() {
81
110
  "use strict";
@@ -464,6 +493,51 @@ function parseExternalIds(frontmatter) {
464
493
  }
465
494
  return results;
466
495
  }
496
+ function parseStatusHistory(frontmatter) {
497
+ if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
498
+ const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
499
+ if (!headerMatch) return [];
500
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
501
+ const bodyStart = headerStart + headerMatch[0].length + 1;
502
+ const after = frontmatter.slice(bodyStart);
503
+ const bodyLines = [];
504
+ for (const line of after.split("\n")) {
505
+ if (line.length === 0) {
506
+ bodyLines.push(line);
507
+ continue;
508
+ }
509
+ if (line[0] !== " " && line[0] !== " ") break;
510
+ bodyLines.push(line);
511
+ }
512
+ const body = bodyLines.join("\n");
513
+ const results = [];
514
+ const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
515
+ for (const block of itemBlocks) {
516
+ const entry = {};
517
+ for (const line of block.split("\n")) {
518
+ const colonIdx = line.indexOf(":");
519
+ if (colonIdx < 0) continue;
520
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
521
+ if (!key) continue;
522
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
523
+ }
524
+ if (!entry["to"]) continue;
525
+ const result = {
526
+ at: entry["at"] ?? "",
527
+ from: entry["from"] ?? null,
528
+ to: entry["to"],
529
+ command: entry["command"] ?? "",
530
+ by: entry["by"] ?? null
531
+ };
532
+ if (entry["reason"] != null) result.reason = entry["reason"];
533
+ if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
534
+ if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
535
+ if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
536
+ if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
537
+ results.push(result);
538
+ }
539
+ return results;
540
+ }
467
541
  function parseAssignmentFull(fileContent) {
468
542
  const [fm, body] = extractFrontmatter(fileContent);
469
543
  return {
@@ -486,13 +560,40 @@ function parseAssignmentFull(fileContent) {
486
560
  parentBranch: getNestedField(fm, "workspace", "parentBranch")
487
561
  },
488
562
  externalIds: parseExternalIds(fm),
563
+ statusHistory: parseStatusHistory(fm),
489
564
  tags: parseListField(fm, "tags"),
490
565
  archived: getField(fm, "archived") === "true",
491
566
  archivedAt: getField(fm, "archivedAt"),
492
567
  archivedReason: getField(fm, "archivedReason"),
493
568
  created: getField(fm, "created") ?? "",
494
569
  updated: getField(fm, "updated") ?? "",
495
- body
570
+ body,
571
+ phase: getField(fm, "phase"),
572
+ disposition: getField(fm, "disposition"),
573
+ parked: getField(fm, "parked") === "true",
574
+ reviewRequested: getField(fm, "reviewRequested") === "true",
575
+ implementationStarted: getField(fm, "implementationStarted") === "true",
576
+ planApproval: (() => {
577
+ const file = getNestedField(fm, "planApproval", "file");
578
+ const digest = getNestedField(fm, "planApproval", "digest");
579
+ if (!file || !digest) return null;
580
+ return {
581
+ file,
582
+ digest,
583
+ by: getNestedField(fm, "planApproval", "by"),
584
+ at: getNestedField(fm, "planApproval", "at") ?? ""
585
+ };
586
+ })(),
587
+ override: (() => {
588
+ const status = getNestedField(fm, "override", "status");
589
+ if (!status) return null;
590
+ return {
591
+ status,
592
+ source: getNestedField(fm, "override", "source") ?? "human",
593
+ reason: getNestedField(fm, "override", "reason"),
594
+ at: getNestedField(fm, "override", "at") ?? ""
595
+ };
596
+ })()
496
597
  };
497
598
  }
498
599
  function parsePlan(fileContent) {
@@ -583,6 +684,19 @@ function parseProgress(fileContent) {
583
684
  body
584
685
  };
585
686
  }
687
+ function parsePlaybook(fileContent) {
688
+ const [fm, body] = extractFrontmatter(fileContent);
689
+ return {
690
+ slug: getField(fm, "slug") ?? "",
691
+ name: getField(fm, "name") ?? "",
692
+ description: getField(fm, "description") ?? "",
693
+ whenToUse: getField(fm, "when_to_use") ?? "",
694
+ created: getField(fm, "created") ?? "",
695
+ updated: getField(fm, "updated") ?? "",
696
+ tags: parseListField(fm, "tags"),
697
+ body
698
+ };
699
+ }
586
700
  function extractMermaidGraph(body) {
587
701
  const match = body.match(/```mermaid\n([\s\S]*?)```/);
588
702
  return match ? match[1].trim() : null;
@@ -675,7 +789,12 @@ function canonicalizeCombo(input) {
675
789
  }
676
790
  return [...ordered, key].join("+");
677
791
  }
678
- var BINDABLE_ACTION_KINDS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
792
+ function isReservedCombo(combo) {
793
+ const c = canonicalizeCombo(combo);
794
+ if (!c) return false;
795
+ return BUILTIN_RESERVED_COMBOS.includes(c);
796
+ }
797
+ var BINDABLE_ACTION_KINDS, BUILTIN_RESERVED_COMBOS, MODIFIER_ORDER, DEFAULT_BINDABLE_HOTKEYS;
679
798
  var init_hotkeysCatalog = __esm({
680
799
  "src/utils/hotkeysCatalog.ts"() {
681
800
  "use strict";
@@ -685,6 +804,40 @@ var init_hotkeysCatalog = __esm({
685
804
  "new-todo",
686
805
  "new-assignment"
687
806
  ];
807
+ BUILTIN_RESERVED_COMBOS = [
808
+ "mod+k",
809
+ "mod+shift+k",
810
+ "?",
811
+ "escape",
812
+ "enter",
813
+ "shift+t",
814
+ // g-chord starter + suffixes
815
+ "g",
816
+ "g o",
817
+ "g m",
818
+ "g a",
819
+ "g t",
820
+ "g s",
821
+ "g !",
822
+ "g ,",
823
+ // list-scope navigation
824
+ "j",
825
+ "k",
826
+ "o",
827
+ // ProjectDetail page
828
+ "a",
829
+ "e",
830
+ // AssignmentsPage board
831
+ "/",
832
+ "r",
833
+ // AssignmentDetail page
834
+ "p",
835
+ "h",
836
+ "d",
837
+ "s",
838
+ "[",
839
+ "]"
840
+ ];
688
841
  MODIFIER_ORDER = ["mod", "ctrl", "alt", "shift"];
689
842
  DEFAULT_BINDABLE_HOTKEYS = {
690
843
  "new-workspace": canonicalizeCombo("Mod+Shift+Alt+w"),
@@ -808,6 +961,47 @@ var init_workspace_visibility_schema = __esm({
808
961
  });
809
962
 
810
963
  // src/utils/config.ts
964
+ var config_exports = {};
965
+ __export(config_exports, {
966
+ AGENT_ID_PATTERN: () => AGENT_ID_PATTERN,
967
+ AgentConfigError: () => AgentConfigError,
968
+ BUILTIN_AGENTS: () => BUILTIN_AGENTS,
969
+ DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
970
+ DEFAULT_DERIVE_CONFIG: () => DEFAULT_DERIVE_CONFIG,
971
+ DEFAULT_STATUS_COLORS: () => DEFAULT_STATUS_COLORS,
972
+ PROMPT_ARG_POSITIONS: () => PROMPT_ARG_POSITIONS,
973
+ TERMINAL_CHOICES: () => TERMINAL_CHOICES,
974
+ TerminalConfigError: () => TerminalConfigError,
975
+ buildDefaultStatusConfig: () => buildDefaultStatusConfig,
976
+ deleteAgentsConfig: () => deleteAgentsConfig,
977
+ deleteHotkeyBindingsConfig: () => deleteHotkeyBindingsConfig,
978
+ deleteStatusConfig: () => deleteStatusConfig,
979
+ deleteTerminalConfig: () => deleteTerminalConfig,
980
+ deleteThemeConfig: () => deleteThemeConfig,
981
+ deleteWorkspaceVisibilityConfig: () => deleteWorkspaceVisibilityConfig,
982
+ getAgents: () => getAgents,
983
+ getAssignmentTypes: () => getAssignmentTypes,
984
+ getTerminal: () => getTerminal,
985
+ parseAgentCommand: () => parseAgentCommand,
986
+ parseStatusConfig: () => parseStatusConfig,
987
+ parseTerminalConfig: () => parseTerminalConfig,
988
+ readConfig: () => readConfig,
989
+ serializeStatusConfig: () => serializeStatusConfig,
990
+ toTitleCase: () => toTitleCase,
991
+ updateAgentsConfig: () => updateAgentsConfig,
992
+ updateBackupConfig: () => updateBackupConfig,
993
+ updateIntegrationConfig: () => updateIntegrationConfig,
994
+ updateOnboardingConfig: () => updateOnboardingConfig,
995
+ updatePlaybooksConfig: () => updatePlaybooksConfig,
996
+ validateAgentList: () => validateAgentList,
997
+ validateDeriveConfig: () => validateDeriveConfig,
998
+ writeAgentsConfig: () => writeAgentsConfig,
999
+ writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
1000
+ writeStatusConfig: () => writeStatusConfig,
1001
+ writeTerminalConfig: () => writeTerminalConfig,
1002
+ writeThemeConfig: () => writeThemeConfig,
1003
+ writeWorkspaceVisibilityConfig: () => writeWorkspaceVisibilityConfig
1004
+ });
811
1005
  import { readFile as readFile5 } from "fs/promises";
812
1006
  import { spawnSync } from "child_process";
813
1007
  import { resolve as resolve6, isAbsolute } from "path";
@@ -860,6 +1054,11 @@ function validateAgentList(agents) {
860
1054
  `agent "${agent.id}" has invalid playbook "${agent.playbook}" \u2014 must be a valid playbook slug`
861
1055
  );
862
1056
  }
1057
+ if (agent.launchPrompt !== void 0 && /[\r\n]/.test(agent.launchPrompt)) {
1058
+ throw new AgentConfigError(
1059
+ `agent "${agent.id}" has invalid launchPrompt \u2014 must be a single line (no newlines)`
1060
+ );
1061
+ }
863
1062
  validateSessionInvocation(agent, "resume", agent.resume);
864
1063
  validateSessionInvocation(agent, "fork", agent.fork);
865
1064
  if (agent.default) defaults++;
@@ -972,6 +1171,16 @@ function parseStatusConfig(content) {
972
1171
  const statuses = [];
973
1172
  const order = [];
974
1173
  const transitions = [];
1174
+ const phaseLadder = [];
1175
+ const disposition = [];
1176
+ const headline = {};
1177
+ const unquote = (v) => {
1178
+ const t = v.trim();
1179
+ if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
1180
+ return t.slice(1, -1);
1181
+ }
1182
+ return t;
1183
+ };
975
1184
  let currentSection = null;
976
1185
  const lines = remaining.split("\n");
977
1186
  function parseListEntry(lineIdx, baseIndent) {
@@ -1004,6 +1213,9 @@ function parseStatusConfig(content) {
1004
1213
  if (key === "definitions") currentSection = "definitions";
1005
1214
  else if (key === "order") currentSection = "order";
1006
1215
  else if (key === "transitions") currentSection = "transitions";
1216
+ else if (key === "phaseLadder") currentSection = "phaseLadder";
1217
+ else if (key === "disposition") currentSection = "disposition";
1218
+ else if (key === "headline") currentSection = "headline";
1007
1219
  else currentSection = null;
1008
1220
  continue;
1009
1221
  }
@@ -1042,12 +1254,52 @@ function parseStatusConfig(content) {
1042
1254
  lineIdx += consumed - 1;
1043
1255
  continue;
1044
1256
  }
1257
+ if (currentSection === "phaseLadder" && indent >= 4 && trimmed.startsWith("- ")) {
1258
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
1259
+ if (entry["phase"] && entry["when"] !== void 0) {
1260
+ phaseLadder.push({
1261
+ phase: unquote(entry["phase"]),
1262
+ when: unquote(entry["when"]),
1263
+ next: entry["next"] !== void 0 ? unquote(entry["next"]) : void 0
1264
+ });
1265
+ }
1266
+ lineIdx += consumed - 1;
1267
+ continue;
1268
+ }
1269
+ if (currentSection === "disposition" && indent >= 4 && trimmed.startsWith("- ")) {
1270
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
1271
+ if (entry["else"] !== void 0) {
1272
+ disposition.push({ when: null, is: unquote(entry["else"]) });
1273
+ } else if (entry["when"] !== void 0 && entry["is"]) {
1274
+ disposition.push({ when: unquote(entry["when"]), is: unquote(entry["is"]) });
1275
+ }
1276
+ lineIdx += consumed - 1;
1277
+ continue;
1278
+ }
1279
+ if (currentSection === "headline" && indent >= 4 && !trimmed.startsWith("- ")) {
1280
+ const ci = trimmed.indexOf(":");
1281
+ if (ci > 0) {
1282
+ headline[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1));
1283
+ }
1284
+ continue;
1285
+ }
1045
1286
  }
1046
1287
  if (statuses.length === 0) return null;
1288
+ const derive = phaseLadder.length > 0 || disposition.length > 0 || Object.keys(headline).length > 0 ? {
1289
+ phaseLadder: phaseLadder.length > 0 ? phaseLadder : DEFAULT_DERIVE_CONFIG.phaseLadder,
1290
+ disposition: disposition.length > 0 ? disposition : DEFAULT_DERIVE_CONFIG.disposition,
1291
+ headline: {
1292
+ terminal: "passthrough",
1293
+ parked: headline["parked"] ?? DEFAULT_DERIVE_CONFIG.headline.parked,
1294
+ blocked: headline["blocked"] ?? DEFAULT_DERIVE_CONFIG.headline.blocked,
1295
+ active: "phase"
1296
+ }
1297
+ } : null;
1047
1298
  return {
1048
1299
  statuses,
1049
1300
  order: order.length > 0 ? order : statuses.map((s) => s.id),
1050
- transitions
1301
+ transitions,
1302
+ derive
1051
1303
  };
1052
1304
  }
1053
1305
  function toTitleCase(s) {
@@ -1068,6 +1320,137 @@ function buildDefaultStatusConfig() {
1068
1320
  })
1069
1321
  };
1070
1322
  }
1323
+ function serializeStatusConfig(statuses) {
1324
+ const lines = [];
1325
+ lines.push("statuses:");
1326
+ lines.push(" definitions:");
1327
+ for (const s of statuses.statuses) {
1328
+ lines.push(` - id: ${s.id}`);
1329
+ lines.push(` label: ${s.label}`);
1330
+ if (s.description) lines.push(` description: ${s.description}`);
1331
+ if (s.color) lines.push(` color: ${s.color}`);
1332
+ if (s.icon) lines.push(` icon: ${s.icon}`);
1333
+ if (s.terminal) lines.push(` terminal: true`);
1334
+ }
1335
+ lines.push(" order:");
1336
+ for (const id of statuses.order) {
1337
+ lines.push(` - ${id}`);
1338
+ }
1339
+ if (statuses.transitions.length > 0) {
1340
+ lines.push(" transitions:");
1341
+ for (const t of statuses.transitions) {
1342
+ lines.push(` - from: ${t.from}`);
1343
+ lines.push(` command: ${t.command}`);
1344
+ lines.push(` to: ${t.to}`);
1345
+ if (t.label) lines.push(` label: ${t.label}`);
1346
+ if (t.description) lines.push(` description: ${t.description}`);
1347
+ if (t.requiresReason) lines.push(` requiresReason: true`);
1348
+ }
1349
+ }
1350
+ if (statuses.derive) {
1351
+ const d = statuses.derive;
1352
+ lines.push(" phaseLadder:");
1353
+ for (const rung of d.phaseLadder) {
1354
+ lines.push(` - phase: ${rung.phase}`);
1355
+ lines.push(` when: "${rung.when.replace(/"/g, '\\"')}"`);
1356
+ if (rung.next) lines.push(` next: "${rung.next.replace(/"/g, '\\"')}"`);
1357
+ }
1358
+ lines.push(" disposition:");
1359
+ for (const rule of d.disposition) {
1360
+ if (rule.when === null) {
1361
+ lines.push(` - else: ${rule.is}`);
1362
+ } else {
1363
+ lines.push(` - when: "${rule.when.replace(/"/g, '\\"')}"`);
1364
+ lines.push(` is: ${rule.is}`);
1365
+ }
1366
+ }
1367
+ lines.push(" headline:");
1368
+ lines.push(` terminal: passthrough`);
1369
+ lines.push(` parked: ${d.headline.parked}`);
1370
+ lines.push(` blocked: ${d.headline.blocked}`);
1371
+ lines.push(` active: phase`);
1372
+ }
1373
+ return lines.join("\n");
1374
+ }
1375
+ function validateDeriveConfig(derive, statusConfig, validateWhen = () => null) {
1376
+ const problems = [];
1377
+ const ids = new Set(statusConfig.statuses.map((s) => s.id));
1378
+ if (derive.phaseLadder.length === 0) {
1379
+ problems.push("phaseLadder must have at least one rung");
1380
+ }
1381
+ for (const rung of derive.phaseLadder) {
1382
+ if (!ids.has(rung.phase)) {
1383
+ problems.push(`phaseLadder rung "${rung.phase}" is not a defined status id`);
1384
+ }
1385
+ const err = rung.when === "*" ? null : validateWhen(rung.when);
1386
+ if (err) problems.push(`phaseLadder rung "${rung.phase}": invalid condition \u2014 ${err}`);
1387
+ }
1388
+ const VALID_DISPOSITIONS = /* @__PURE__ */ new Set(["active", "blocked", "parked"]);
1389
+ let sawElse = false;
1390
+ for (const rule of derive.disposition) {
1391
+ if (!VALID_DISPOSITIONS.has(rule.is)) {
1392
+ problems.push(
1393
+ `disposition "${rule.is}" is not valid (expected active, blocked, or parked \u2014 terminal is never a rule)`
1394
+ );
1395
+ }
1396
+ if (rule.when === null) sawElse = true;
1397
+ else {
1398
+ const err = validateWhen(rule.when);
1399
+ if (err) problems.push(`disposition rule "${rule.is}": invalid condition \u2014 ${err}`);
1400
+ }
1401
+ }
1402
+ if (!sawElse) problems.push("disposition rules must end with an `else:` arm");
1403
+ for (const key of ["parked", "blocked"]) {
1404
+ if (!ids.has(derive.headline[key])) {
1405
+ problems.push(
1406
+ `headline.${key} \u2192 "${derive.headline[key]}" is not a defined status id (add the definition or run migrate-derive)`
1407
+ );
1408
+ }
1409
+ }
1410
+ return problems;
1411
+ }
1412
+ function serializeIntegrationConfig(integrations) {
1413
+ const lines = [];
1414
+ if (integrations.claudePluginDir) {
1415
+ lines.push(` claudePluginDir: ${integrations.claudePluginDir}`);
1416
+ }
1417
+ if (integrations.codexPluginDir) {
1418
+ lines.push(` codexPluginDir: ${integrations.codexPluginDir}`);
1419
+ }
1420
+ if (integrations.codexMarketplacePath) {
1421
+ lines.push(` codexMarketplacePath: ${integrations.codexMarketplacePath}`);
1422
+ }
1423
+ if (integrations.installedAgents) {
1424
+ for (const [id, rec] of Object.entries(integrations.installedAgents)) {
1425
+ lines.push(` installedAgents.${id}: ${rec.scope}`);
1426
+ }
1427
+ }
1428
+ if (lines.length === 0) {
1429
+ return null;
1430
+ }
1431
+ return ["integrations:", ...lines].join("\n");
1432
+ }
1433
+ function serializeOnboardingConfig(onboarding) {
1434
+ return ["onboarding:", ` completed: ${onboarding.completed ? "true" : "false"}`].join("\n");
1435
+ }
1436
+ function serializeBackupConfig(backup) {
1437
+ const lines = ["backup:"];
1438
+ lines.push(` repo: ${backup.repo ?? "null"}`);
1439
+ lines.push(` categories: ${backup.categories}`);
1440
+ lines.push(` lastBackup: ${backup.lastBackup ?? "null"}`);
1441
+ lines.push(` lastRestore: ${backup.lastRestore ?? "null"}`);
1442
+ return lines.join("\n");
1443
+ }
1444
+ function serializePlaybooksConfig(playbooks) {
1445
+ if (!playbooks.disabled || playbooks.disabled.length === 0) {
1446
+ return null;
1447
+ }
1448
+ const lines = ["playbooks:", " disabled:"];
1449
+ for (const slug of playbooks.disabled) {
1450
+ lines.push(` - ${slug}`);
1451
+ }
1452
+ return lines.join("\n");
1453
+ }
1071
1454
  function parsePlaybooksConfig(fmBlock) {
1072
1455
  const blockStart = fmBlock.match(/^playbooks:\s*$/m);
1073
1456
  if (!blockStart) {
@@ -1103,6 +1486,37 @@ function parsePlaybooksConfig(fmBlock) {
1103
1486
  }
1104
1487
  return { disabled };
1105
1488
  }
1489
+ async function updatePlaybooksConfig(playbooks) {
1490
+ const configPath = resolve6(syntaurRoot(), "config.md");
1491
+ const current = (await readConfig()).playbooks;
1492
+ const nextPlaybooks = {
1493
+ disabled: Array.from(new Set(playbooks.disabled ?? current.disabled))
1494
+ };
1495
+ const playbooksBlock = serializePlaybooksConfig(nextPlaybooks);
1496
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1497
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1498
+ if (!fmMatch) {
1499
+ const bodyBlock = playbooksBlock ? `${playbooksBlock}
1500
+ ` : "";
1501
+ const content = `---
1502
+ version: "2.0"
1503
+ defaultProjectDir: ${defaultProjectDir()}
1504
+ ${bodyBlock}---
1505
+ ${existing}`;
1506
+ await writeFileForce(configPath, content);
1507
+ return;
1508
+ }
1509
+ const fmBlock = fmMatch[2];
1510
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1511
+ const cleanedFm = stripTopLevelBlock(fmBlock, "playbooks");
1512
+ const newFm = playbooksBlock ? `${cleanedFm}
1513
+ ${playbooksBlock}`.replace(/^\n+/, "") : cleanedFm;
1514
+ const normalizedFm = newFm.replace(/\n+$/, "");
1515
+ const newContent = `---
1516
+ ${normalizedFm}
1517
+ ---${afterFrontmatter}`;
1518
+ await writeFileForce(configPath, newContent);
1519
+ }
1106
1520
  function parseThemeConfig(content) {
1107
1521
  const match = content.match(/^---\n([\s\S]*?)\n---/);
1108
1522
  if (!match) return null;
@@ -1125,6 +1539,58 @@ function parseThemeConfig(content) {
1125
1539
  if (!preset) return null;
1126
1540
  return { preset };
1127
1541
  }
1542
+ function serializeThemeConfig(theme) {
1543
+ return ["theme:", ` preset: ${theme.preset}`].join("\n");
1544
+ }
1545
+ async function writeThemeConfig(theme) {
1546
+ const configPath = resolve6(syntaurRoot(), "config.md");
1547
+ const themeBlock = serializeThemeConfig(theme);
1548
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1549
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1550
+ if (!fmMatch) {
1551
+ const content = `---
1552
+ version: "2.0"
1553
+ defaultProjectDir: ${defaultProjectDir()}
1554
+ ${themeBlock}
1555
+ ---
1556
+ ${existing}`;
1557
+ await writeFileForce(configPath, content);
1558
+ return;
1559
+ }
1560
+ const fmBlock = fmMatch[2];
1561
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1562
+ const cleanedFm = stripTopLevelBlock(fmBlock, "theme");
1563
+ const newFm = `${cleanedFm}
1564
+ ${themeBlock}`.replace(/^\n+/, "");
1565
+ const normalizedFm = newFm.replace(/\n+$/, "");
1566
+ const newContent = `---
1567
+ ${normalizedFm}
1568
+ ---${afterFrontmatter}`;
1569
+ await writeFileForce(configPath, newContent);
1570
+ }
1571
+ async function deleteThemeConfig() {
1572
+ const configPath = resolve6(syntaurRoot(), "config.md");
1573
+ if (!await fileExists(configPath)) return;
1574
+ const existing = await readFile5(configPath, "utf-8");
1575
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1576
+ if (!fmMatch) return;
1577
+ const fmBlock = fmMatch[2];
1578
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1579
+ const cleanedFm = stripTopLevelBlock(fmBlock, "theme");
1580
+ const newContent = `---
1581
+ ${cleanedFm}
1582
+ ---${afterFrontmatter}`;
1583
+ await writeFileForce(configPath, newContent);
1584
+ }
1585
+ function serializeWorkspaceVisibilityConfig(cfg) {
1586
+ const hidden = normalizeHiddenList(cfg.hidden);
1587
+ if (hidden.length === 0) return null;
1588
+ const lines = ["workspaceVisibility:", " hidden:"];
1589
+ for (const name of hidden) {
1590
+ lines.push(` - ${JSON.stringify(name)}`);
1591
+ }
1592
+ return lines.join("\n");
1593
+ }
1128
1594
  function parseWorkspaceVisibilityConfig(fmBlock) {
1129
1595
  const blockStart = fmBlock.match(/^workspaceVisibility:\s*$/m);
1130
1596
  if (!blockStart) {
@@ -1162,6 +1628,93 @@ function parseWorkspaceVisibilityConfig(fmBlock) {
1162
1628
  }
1163
1629
  return { hidden: normalizeHiddenList(hidden) };
1164
1630
  }
1631
+ async function writeWorkspaceVisibilityConfig(cfg) {
1632
+ const configPath = resolve6(syntaurRoot(), "config.md");
1633
+ const block = serializeWorkspaceVisibilityConfig(cfg);
1634
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1635
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1636
+ if (!fmMatch) {
1637
+ const bodyBlock = block ? `${block}
1638
+ ` : "";
1639
+ const content = `---
1640
+ version: "2.0"
1641
+ defaultProjectDir: ${defaultProjectDir()}
1642
+ ${bodyBlock}---
1643
+ ${existing}`;
1644
+ await writeFileForce(configPath, content);
1645
+ return;
1646
+ }
1647
+ const fmBlock = fmMatch[2];
1648
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1649
+ const cleanedFm = stripTopLevelBlock(fmBlock, "workspaceVisibility");
1650
+ const newFm = block ? `${cleanedFm}
1651
+ ${block}`.replace(/^\n+/, "") : cleanedFm;
1652
+ const normalizedFm = newFm.replace(/\n+$/, "");
1653
+ const newContent = `---
1654
+ ${normalizedFm}
1655
+ ---${afterFrontmatter}`;
1656
+ await writeFileForce(configPath, newContent);
1657
+ }
1658
+ async function deleteWorkspaceVisibilityConfig() {
1659
+ const configPath = resolve6(syntaurRoot(), "config.md");
1660
+ if (!await fileExists(configPath)) return;
1661
+ const existing = await readFile5(configPath, "utf-8");
1662
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1663
+ if (!fmMatch) return;
1664
+ const fmBlock = fmMatch[2];
1665
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1666
+ const cleanedFm = stripTopLevelBlock(fmBlock, "workspaceVisibility");
1667
+ const newContent = `---
1668
+ ${cleanedFm}
1669
+ ---${afterFrontmatter}`;
1670
+ await writeFileForce(configPath, newContent);
1671
+ }
1672
+ function stripTopLevelScalar(fmBlock, key) {
1673
+ const lines = fmBlock.split("\n");
1674
+ const keyRegex = new RegExp(`^${key}:\\s*\\S`);
1675
+ const filtered = lines.filter((line) => !keyRegex.test(line));
1676
+ return filtered.join("\n").replace(/\n+$/, "");
1677
+ }
1678
+ async function writeTerminalConfig(terminal) {
1679
+ const configPath = resolve6(syntaurRoot(), "config.md");
1680
+ const terminalLine = `terminal: ${terminal}`;
1681
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1682
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1683
+ if (!fmMatch) {
1684
+ const content = `---
1685
+ version: "2.0"
1686
+ defaultProjectDir: ${defaultProjectDir()}
1687
+ ${terminalLine}
1688
+ ---
1689
+ ${existing}`;
1690
+ await writeFileForce(configPath, content);
1691
+ return;
1692
+ }
1693
+ const fmBlock = fmMatch[2];
1694
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1695
+ const cleanedFm = stripTopLevelScalar(fmBlock, "terminal");
1696
+ const newFm = `${cleanedFm}
1697
+ ${terminalLine}`.replace(/^\n+/, "");
1698
+ const normalizedFm = newFm.replace(/\n+$/, "");
1699
+ const newContent = `---
1700
+ ${normalizedFm}
1701
+ ---${afterFrontmatter}`;
1702
+ await writeFileForce(configPath, newContent);
1703
+ }
1704
+ async function deleteTerminalConfig() {
1705
+ const configPath = resolve6(syntaurRoot(), "config.md");
1706
+ if (!await fileExists(configPath)) return;
1707
+ const existing = await readFile5(configPath, "utf-8");
1708
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1709
+ if (!fmMatch) return;
1710
+ const fmBlock = fmMatch[2];
1711
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1712
+ const cleanedFm = stripTopLevelScalar(fmBlock, "terminal");
1713
+ const newContent = `---
1714
+ ${cleanedFm}
1715
+ ---${afterFrontmatter}`;
1716
+ await writeFileForce(configPath, newContent);
1717
+ }
1165
1718
  function parseHotkeyBindingsConfig(content) {
1166
1719
  const match = content.match(/^---\n([\s\S]*?)\n---/);
1167
1720
  if (!match) return null;
@@ -1194,6 +1747,92 @@ function parseHotkeyBindingsConfig(content) {
1194
1747
  if (Object.keys(bindings).length === 0) return null;
1195
1748
  return { bindings };
1196
1749
  }
1750
+ function serializeHotkeyBindingsConfig(cfg) {
1751
+ const lines = ["hotkeys:", " bindings:"];
1752
+ for (const kind of BINDABLE_ACTION_KINDS) {
1753
+ const value = cfg.bindings[kind];
1754
+ if (!value) continue;
1755
+ lines.push(` ${kind}: "${canonicalizeCombo(value)}"`);
1756
+ }
1757
+ if (lines.length === 2) return "";
1758
+ return lines.join("\n");
1759
+ }
1760
+ async function writeHotkeyBindingsConfig(cfg) {
1761
+ const cleaned = {};
1762
+ for (const kind of BINDABLE_ACTION_KINDS) {
1763
+ const raw = cfg.bindings[kind];
1764
+ if (typeof raw !== "string" || raw.trim() === "") continue;
1765
+ const canonical = canonicalizeCombo(raw);
1766
+ if (!canonical) continue;
1767
+ if (isReservedCombo(canonical)) continue;
1768
+ cleaned[kind] = canonical;
1769
+ }
1770
+ if (Object.keys(cleaned).length === 0) {
1771
+ await deleteHotkeyBindingsConfig();
1772
+ return;
1773
+ }
1774
+ const configPath = resolve6(syntaurRoot(), "config.md");
1775
+ const block = serializeHotkeyBindingsConfig({ bindings: cleaned });
1776
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
1777
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1778
+ if (!fmMatch) {
1779
+ const content = `---
1780
+ version: "2.0"
1781
+ defaultProjectDir: ${defaultProjectDir()}
1782
+ ${block}
1783
+ ---
1784
+ ${existing}`;
1785
+ await writeFileForce(configPath, content);
1786
+ return;
1787
+ }
1788
+ const fmBlock = fmMatch[2];
1789
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1790
+ const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
1791
+ const newFm = `${cleanedFm}
1792
+ ${block}`.replace(/^\n+/, "");
1793
+ const normalizedFm = newFm.replace(/\n+$/, "");
1794
+ const newContent = `---
1795
+ ${normalizedFm}
1796
+ ---${afterFrontmatter}`;
1797
+ await writeFileForce(configPath, newContent);
1798
+ }
1799
+ async function deleteHotkeyBindingsConfig() {
1800
+ const configPath = resolve6(syntaurRoot(), "config.md");
1801
+ if (!await fileExists(configPath)) return;
1802
+ const existing = await readFile5(configPath, "utf-8");
1803
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
1804
+ if (!fmMatch) return;
1805
+ const fmBlock = fmMatch[2];
1806
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
1807
+ const cleanedFm = stripTopLevelBlock(fmBlock, "hotkeys");
1808
+ const newContent = `---
1809
+ ${cleanedFm}
1810
+ ---${afterFrontmatter}`;
1811
+ await writeFileForce(configPath, newContent);
1812
+ }
1813
+ function stripTopLevelBlock(fmBlock, key) {
1814
+ const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
1815
+ if (!blockStart) {
1816
+ return fmBlock.replace(/\n+$/, "");
1817
+ }
1818
+ const startIdx = fmBlock.indexOf(blockStart[0]);
1819
+ const before = fmBlock.slice(0, startIdx);
1820
+ const after = fmBlock.slice(startIdx + blockStart[0].length);
1821
+ const remaining = after.split("\n");
1822
+ let endIdx = 0;
1823
+ for (let i = 0; i < remaining.length; i++) {
1824
+ const line = remaining[i];
1825
+ if (line.trim() === "") {
1826
+ endIdx = i + 1;
1827
+ continue;
1828
+ }
1829
+ if (line.length > 0 && line[0] !== " ") {
1830
+ break;
1831
+ }
1832
+ endIdx = i + 1;
1833
+ }
1834
+ return (before + remaining.slice(endIdx).join("\n")).replace(/\n+$/, "");
1835
+ }
1197
1836
  function parseOptionalAbsolutePath(value, fieldName) {
1198
1837
  if (!value) {
1199
1838
  return null;
@@ -1239,6 +1878,7 @@ function parseAgentsConfig(content) {
1239
1878
  ...current.resolveFromShellAliases ? { resolveFromShellAliases: true } : {},
1240
1879
  ...current.model ? { model: current.model } : {},
1241
1880
  ...current.playbook ? { playbook: current.playbook } : {},
1881
+ ...current.launchPrompt ? { launchPrompt: current.launchPrompt } : {},
1242
1882
  ...current.resume ? { resume: current.resume } : {},
1243
1883
  ...current.fork ? { fork: current.fork } : {}
1244
1884
  });
@@ -1427,7 +2067,273 @@ function assignAgentField(target, key, rawValue) {
1427
2067
  case "playbook":
1428
2068
  target.playbook = value;
1429
2069
  break;
2070
+ case "launchPrompt":
2071
+ target.launchPrompt = value;
2072
+ break;
2073
+ }
2074
+ }
2075
+ function yamlQuoteScalar(value) {
2076
+ if (/[\r\n]/.test(value)) {
2077
+ throw new AgentConfigError(
2078
+ `value contains newlines, which the agents config serializer does not support: ${JSON.stringify(value)}`
2079
+ );
2080
+ }
2081
+ if (value === "" || /[:#{}[\],&*?|>!%@`"'\\\t]/.test(value) || /^\s|\s$/.test(value)) {
2082
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t");
2083
+ return `"${escaped}"`;
2084
+ }
2085
+ return value;
2086
+ }
2087
+ function serializeAgentsConfig(agents) {
2088
+ const lines = ["agents:"];
2089
+ for (const a of agents) {
2090
+ lines.push(` - id: ${yamlQuoteScalar(a.id)}`);
2091
+ lines.push(` label: ${yamlQuoteScalar(a.label)}`);
2092
+ lines.push(` command: ${yamlQuoteScalar(a.command)}`);
2093
+ if (a.model) {
2094
+ lines.push(` model: ${yamlQuoteScalar(a.model)}`);
2095
+ }
2096
+ if (a.playbook) {
2097
+ lines.push(` playbook: ${yamlQuoteScalar(a.playbook)}`);
2098
+ }
2099
+ if (a.launchPrompt) {
2100
+ lines.push(` launchPrompt: ${yamlQuoteScalar(a.launchPrompt)}`);
2101
+ }
2102
+ if (a.args && a.args.length > 0) {
2103
+ lines.push(` args:`);
2104
+ for (const arg of a.args) {
2105
+ lines.push(` - ${yamlQuoteScalar(arg)}`);
2106
+ }
2107
+ }
2108
+ if (a.promptArgPosition && a.promptArgPosition !== "first") {
2109
+ lines.push(` promptArgPosition: ${a.promptArgPosition}`);
2110
+ }
2111
+ if (a.default) {
2112
+ lines.push(` default: true`);
2113
+ }
2114
+ if (a.resolveFromShellAliases) {
2115
+ lines.push(` resolveFromShellAliases: true`);
2116
+ }
2117
+ if (a.resume) {
2118
+ appendSessionInvocation(lines, "resume", a.resume);
2119
+ }
2120
+ if (a.fork) {
2121
+ appendSessionInvocation(lines, "fork", a.fork);
2122
+ }
2123
+ }
2124
+ return lines.join("\n");
2125
+ }
2126
+ function appendSessionInvocation(lines, key, invocation) {
2127
+ lines.push(` ${key}:`);
2128
+ if (invocation.command !== void 0) {
2129
+ lines.push(` command: ${yamlQuoteScalar(invocation.command)}`);
2130
+ }
2131
+ lines.push(` args:`);
2132
+ for (const arg of invocation.args) {
2133
+ lines.push(` - ${yamlQuoteScalar(arg)}`);
2134
+ }
2135
+ }
2136
+ async function writeAgentsConfig(agents) {
2137
+ validateAgentList(agents);
2138
+ const configPath = resolve6(syntaurRoot(), "config.md");
2139
+ const agentsBlock = serializeAgentsConfig(agents);
2140
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2141
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2142
+ if (!fmMatch) {
2143
+ const content = `---
2144
+ version: "2.0"
2145
+ defaultProjectDir: ${defaultProjectDir()}
2146
+ ${agentsBlock}
2147
+ ---
2148
+ ${existing}`;
2149
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
2150
+ return;
2151
+ }
2152
+ const fmBlock = fmMatch[2];
2153
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2154
+ const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
2155
+ const newFm = `${cleanedFm}
2156
+ ${agentsBlock}`.replace(/^\n+/, "").replace(/\n+$/, "");
2157
+ const newContent = `---
2158
+ ${newFm}
2159
+ ---${afterFrontmatter}`;
2160
+ await writeFileForce(configPath, newContent);
2161
+ }
2162
+ async function deleteAgentsConfig() {
2163
+ const configPath = resolve6(syntaurRoot(), "config.md");
2164
+ if (!await fileExists(configPath)) return;
2165
+ const existing = await readFile5(configPath, "utf-8");
2166
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2167
+ if (!fmMatch) return;
2168
+ const fmBlock = fmMatch[2];
2169
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2170
+ const cleanedFm = stripTopLevelBlock(fmBlock, "agents");
2171
+ const newContent = `---
2172
+ ${cleanedFm}
2173
+ ---${afterFrontmatter}`;
2174
+ await writeFileForce(configPath, newContent);
2175
+ }
2176
+ async function writeStatusConfig(statuses) {
2177
+ const configPath = resolve6(syntaurRoot(), "config.md");
2178
+ const statusBlock = serializeStatusConfig(statuses);
2179
+ if (!await fileExists(configPath)) {
2180
+ const content = `---
2181
+ version: "2.0"
2182
+ defaultProjectDir: ~/projects
2183
+ ${statusBlock}
2184
+ ---
2185
+ `;
2186
+ await writeFileForce(configPath, content);
2187
+ return;
2188
+ }
2189
+ const existing = await readFile5(configPath, "utf-8");
2190
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2191
+ if (!fmMatch) {
2192
+ const content = `---
2193
+ version: "2.0"
2194
+ ${statusBlock}
2195
+ ---
2196
+ ${existing}`;
2197
+ await writeFileForce(configPath, content);
2198
+ return;
1430
2199
  }
2200
+ const fmBlock = fmMatch[2];
2201
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2202
+ const statusesStart = fmBlock.match(/^statuses:\s*$/m);
2203
+ let cleanedFm;
2204
+ if (statusesStart) {
2205
+ const startIdx = fmBlock.indexOf(statusesStart[0]);
2206
+ const before = fmBlock.slice(0, startIdx);
2207
+ const after = fmBlock.slice(startIdx + statusesStart[0].length);
2208
+ const remaining = after.split("\n");
2209
+ let endIdx = 0;
2210
+ for (let i = 0; i < remaining.length; i++) {
2211
+ const line = remaining[i];
2212
+ if (line.trim() === "") {
2213
+ endIdx = i + 1;
2214
+ continue;
2215
+ }
2216
+ if (line.length > 0 && line[0] !== " ") break;
2217
+ endIdx = i + 1;
2218
+ }
2219
+ cleanedFm = before + remaining.slice(endIdx).join("\n");
2220
+ } else {
2221
+ cleanedFm = fmBlock;
2222
+ }
2223
+ cleanedFm = cleanedFm.replace(/\n+$/, "");
2224
+ const newContent = `---
2225
+ ${cleanedFm}
2226
+ ${statusBlock}
2227
+ ---${afterFrontmatter}`;
2228
+ await writeFileForce(configPath, newContent);
2229
+ }
2230
+ async function deleteStatusConfig() {
2231
+ const configPath = resolve6(syntaurRoot(), "config.md");
2232
+ if (!await fileExists(configPath)) return;
2233
+ const existing = await readFile5(configPath, "utf-8");
2234
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2235
+ if (!fmMatch) return;
2236
+ const fmBlock = fmMatch[2];
2237
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2238
+ const cleanedFm = stripTopLevelBlock(fmBlock, "statuses");
2239
+ const newContent = `---
2240
+ ${cleanedFm}
2241
+ ---${afterFrontmatter}`;
2242
+ await writeFileForce(configPath, newContent);
2243
+ }
2244
+ async function updateIntegrationConfig(integrations) {
2245
+ const configPath = resolve6(syntaurRoot(), "config.md");
2246
+ const nextIntegrations = {
2247
+ ...(await readConfig()).integrations,
2248
+ ...integrations
2249
+ };
2250
+ const integrationBlock = serializeIntegrationConfig(nextIntegrations);
2251
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2252
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2253
+ if (!fmMatch) {
2254
+ const content = `---
2255
+ version: "2.0"
2256
+ defaultProjectDir: ${defaultProjectDir()}
2257
+ ${integrationBlock ?? ""}
2258
+ ---
2259
+ ${existing}`;
2260
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
2261
+ return;
2262
+ }
2263
+ const fmBlock = fmMatch[2];
2264
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2265
+ const cleanedFm = stripTopLevelBlock(fmBlock, "integrations");
2266
+ const newFm = integrationBlock ? `${cleanedFm}
2267
+ ${integrationBlock}`.replace(/^\n+/, "") : cleanedFm;
2268
+ const normalizedFm = newFm.replace(/\n+$/, "");
2269
+ const newContent = `---
2270
+ ${normalizedFm}
2271
+ ---${afterFrontmatter}`;
2272
+ await writeFileForce(configPath, newContent);
2273
+ }
2274
+ async function updateOnboardingConfig(onboarding) {
2275
+ const configPath = resolve6(syntaurRoot(), "config.md");
2276
+ const nextOnboarding = {
2277
+ ...(await readConfig()).onboarding,
2278
+ ...onboarding
2279
+ };
2280
+ const onboardingBlock = serializeOnboardingConfig(nextOnboarding);
2281
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2282
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2283
+ if (!fmMatch) {
2284
+ const content = `---
2285
+ version: "2.0"
2286
+ defaultProjectDir: ${defaultProjectDir()}
2287
+ ${onboardingBlock}
2288
+ ---
2289
+ ${existing}`;
2290
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
2291
+ return;
2292
+ }
2293
+ const fmBlock = fmMatch[2];
2294
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2295
+ const cleanedFm = stripTopLevelBlock(fmBlock, "onboarding");
2296
+ const newFm = `${cleanedFm}
2297
+ ${onboardingBlock}`.replace(/^\n+/, "");
2298
+ const normalizedFm = newFm.replace(/\n+$/, "");
2299
+ const newContent = `---
2300
+ ${normalizedFm}
2301
+ ---${afterFrontmatter}`;
2302
+ await writeFileForce(configPath, newContent);
2303
+ }
2304
+ async function updateBackupConfig(backup) {
2305
+ const configPath = resolve6(syntaurRoot(), "config.md");
2306
+ const current = (await readConfig()).backup;
2307
+ const nextBackup = {
2308
+ repo: current?.repo ?? null,
2309
+ categories: current?.categories ?? "projects, playbooks, todos, servers, config",
2310
+ lastBackup: current?.lastBackup ?? null,
2311
+ lastRestore: current?.lastRestore ?? null,
2312
+ ...backup
2313
+ };
2314
+ const backupBlock = serializeBackupConfig(nextBackup);
2315
+ const existing = await fileExists(configPath) ? await readFile5(configPath, "utf-8") : renderConfig({ defaultProjectDir: defaultProjectDir() });
2316
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
2317
+ if (!fmMatch) {
2318
+ const content = `---
2319
+ version: "2.0"
2320
+ defaultProjectDir: ${defaultProjectDir()}
2321
+ ${backupBlock}
2322
+ ---
2323
+ ${existing}`;
2324
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
2325
+ return;
2326
+ }
2327
+ const fmBlock = fmMatch[2];
2328
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
2329
+ const cleanedFm = stripTopLevelBlock(fmBlock, "backup");
2330
+ const newFm = `${cleanedFm}
2331
+ ${backupBlock}`.replace(/^\n+/, "");
2332
+ const normalizedFm = newFm.replace(/\n+$/, "");
2333
+ const newContent = `---
2334
+ ${normalizedFm}
2335
+ ---${afterFrontmatter}`;
2336
+ await writeFileForce(configPath, newContent);
1431
2337
  }
1432
2338
  async function readConfig() {
1433
2339
  const configPath = resolve6(syntaurRoot(), "config.md");
@@ -1504,6 +2410,9 @@ async function readConfig() {
1504
2410
  workspaceVisibility: parseWorkspaceVisibilityConfig(fmBlock)
1505
2411
  };
1506
2412
  }
2413
+ function getAssignmentTypes(config) {
2414
+ return config.types ?? DEFAULT_ASSIGNMENT_TYPES;
2415
+ }
1507
2416
  function getAgents(config) {
1508
2417
  if (config.agents === null) return BUILTIN_AGENTS;
1509
2418
  const builtinById = new Map(BUILTIN_AGENTS.map((a) => [a.id, a]));
@@ -1550,7 +2459,18 @@ function getTerminal(config) {
1550
2459
  }
1551
2460
  return "terminal-app";
1552
2461
  }
1553
- var DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
2462
+ async function updateAgentsConfig(mutation, options = {}) {
2463
+ const config = await readConfig();
2464
+ const previous = config.agents ?? [...BUILTIN_AGENTS];
2465
+ const next = mutation.apply(previous);
2466
+ validateAgentList(next);
2467
+ if (options.dryRun) {
2468
+ return { previous, next, written: false };
2469
+ }
2470
+ await writeAgentsConfig(next);
2471
+ return { previous, next, written: true };
2472
+ }
2473
+ var DEFAULT_DERIVE_CONFIG, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
1554
2474
  var init_config2 = __esm({
1555
2475
  "src/utils/config.ts"() {
1556
2476
  "use strict";
@@ -1564,6 +2484,45 @@ var init_config2 = __esm({
1564
2484
  init_slug();
1565
2485
  init_terminal_schema();
1566
2486
  init_workspace_visibility_schema();
2487
+ DEFAULT_DERIVE_CONFIG = {
2488
+ phaseLadder: [
2489
+ { phase: "draft", when: "*", next: "Fill in the objective and acceptance criteria" },
2490
+ {
2491
+ // planExists-but-not-approved also sits here: the default status set has
2492
+ // no `planning` id. Users who define one add a `planExists:true` rung.
2493
+ phase: "ready_for_planning",
2494
+ when: "hasRealObjective:true AND acRealTotal > 0",
2495
+ next: "Write a plan and get it approved"
2496
+ },
2497
+ { phase: "ready_to_implement", when: "planApproved:true", next: "Start implementing" },
2498
+ {
2499
+ phase: "in_progress",
2500
+ when: "planApproved:true AND implementationStarted:true",
2501
+ next: "Finish acceptance criteria, then request review"
2502
+ },
2503
+ {
2504
+ phase: "review",
2505
+ when: "acAllChecked:true OR reviewRequested:true",
2506
+ next: "Complete, or address review feedback"
2507
+ }
2508
+ ],
2509
+ disposition: [
2510
+ { when: "parked:true", is: "parked" },
2511
+ { when: "blocked:true", is: "blocked" },
2512
+ { when: null, is: "active" }
2513
+ ],
2514
+ headline: { terminal: "passthrough", parked: "parked", blocked: "blocked", active: "phase" }
2515
+ };
2516
+ DEFAULT_ASSIGNMENT_TYPES = {
2517
+ definitions: [
2518
+ { id: "feature", label: "Feature" },
2519
+ { id: "bug", label: "Bug" },
2520
+ { id: "refactor", label: "Refactor" },
2521
+ { id: "research", label: "Research" },
2522
+ { id: "chore", label: "Chore" }
2523
+ ],
2524
+ default: "feature"
2525
+ };
1567
2526
  DEFAULT_CONFIG = {
1568
2527
  version: "2.0",
1569
2528
  defaultProjectDir: defaultProjectDir(),
@@ -1613,7 +2572,8 @@ var init_config2 = __esm({
1613
2572
  "default",
1614
2573
  "resolveFromShellAliases",
1615
2574
  "model",
1616
- "playbook"
2575
+ "playbook",
2576
+ "launchPrompt"
1617
2577
  ]);
1618
2578
  migratedConfigPaths = /* @__PURE__ */ new Set();
1619
2579
  TerminalConfigError = class extends Error {
@@ -1701,6 +2661,22 @@ var init_assignment_resolver = __esm({
1701
2661
  // src/utils/playbooks.ts
1702
2662
  import { resolve as resolve8 } from "path";
1703
2663
  import { readdir as readdir4, readFile as readFile7, unlink } from "fs/promises";
2664
+ function isVisiblePlaybookFile(name, isFile) {
2665
+ return isFile && name.endsWith(".md") && !name.startsWith("_") && name !== "manifest.md";
2666
+ }
2667
+ async function listPlaybookSlugs(playbooksDir2) {
2668
+ const slugs = /* @__PURE__ */ new Set();
2669
+ if (!await fileExists(playbooksDir2)) return slugs;
2670
+ const entries = await readdir4(playbooksDir2, { withFileTypes: true });
2671
+ for (const entry of entries) {
2672
+ if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
2673
+ const filePath = resolve8(playbooksDir2, entry.name);
2674
+ const raw = await readFile7(filePath, "utf-8");
2675
+ const parsed = parsePlaybook(raw);
2676
+ slugs.add(parsed.slug || entry.name.replace(/\.md$/, ""));
2677
+ }
2678
+ return slugs;
2679
+ }
1704
2680
  var init_playbooks = __esm({
1705
2681
  "src/utils/playbooks.ts"() {
1706
2682
  "use strict";
@@ -1965,62 +2941,928 @@ var init_overviewCopy = __esm({
1965
2941
  }
1966
2942
  });
1967
2943
 
1968
- // 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";
1971
- function activeAssignments(items) {
1972
- return items.filter((item) => item.archived !== true);
2944
+ // src/lifecycle/facts.ts
2945
+ var facts_exports = {};
2946
+ __export(facts_exports, {
2947
+ areDependenciesSatisfied: () => areDependenciesSatisfied,
2948
+ computeFacts: () => computeFacts,
2949
+ countRealAcceptanceCriteria: () => countRealAcceptanceCriteria,
2950
+ countUnresolvedQuestions: () => countUnresolvedQuestions,
2951
+ hasRealObjective: () => hasRealObjective,
2952
+ isPlanApproved: () => isPlanApproved,
2953
+ latestPlanFile: () => latestPlanFile,
2954
+ planDigest: () => planDigest
2955
+ });
2956
+ import { createHash } from "crypto";
2957
+ import { readdir as readdir6, readFile as readFile9 } from "fs/promises";
2958
+ import { resolve as resolve11 } from "path";
2959
+ function sectionBody(body, heading) {
2960
+ const re = new RegExp(`^##\\s+${heading}\\s*$`, "m");
2961
+ const m = body.match(re);
2962
+ if (!m || m.index === void 0) return null;
2963
+ const start = m.index + m[0].length;
2964
+ const rest = body.slice(start);
2965
+ const next = rest.search(/^##\s+/m);
2966
+ return next >= 0 ? rest.slice(0, next) : rest;
1973
2967
  }
1974
- function accumulatePhase(traces, label, ms) {
1975
- if (!traces) return;
1976
- traces.subPhases.set(label, (traces.subPhases.get(label) ?? 0) + ms);
2968
+ function hasRealObjective(body) {
2969
+ const section = sectionBody(body, "Objective");
2970
+ if (section === null) return false;
2971
+ return section.replace(HTML_COMMENT_RE, "").trim().length > 0;
1977
2972
  }
1978
- async function listStandaloneRecords(assignmentsDir) {
1979
- const key = assignmentsDir ?? "";
1980
- const cached = standaloneRecordsCache.get(key);
1981
- if (cached) return cached;
1982
- const promise = computeStandaloneRecords(assignmentsDir);
1983
- standaloneRecordsCache.set(key, promise);
1984
- promise.catch(() => standaloneRecordsCache.delete(key));
1985
- return promise;
2973
+ function countRealAcceptanceCriteria(body) {
2974
+ const section = sectionBody(body, "Acceptance Criteria");
2975
+ if (section === null) return { total: 0, checked: 0 };
2976
+ let total = 0;
2977
+ let checked = 0;
2978
+ for (const line of section.split("\n")) {
2979
+ const m = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
2980
+ if (!m) continue;
2981
+ const content = m[2].replace(HTML_COMMENT_RE, "").trim();
2982
+ if (content.length === 0) continue;
2983
+ total++;
2984
+ if (m[1].toLowerCase() === "x") checked++;
2985
+ }
2986
+ return { total, checked };
1986
2987
  }
1987
- async function computeStandaloneRecords(assignmentsDir) {
1988
- if (!assignmentsDir) return [];
1989
- if (!await fileExists(assignmentsDir)) return [];
1990
- const entries = await readdir6(assignmentsDir, { withFileTypes: true });
1991
- const records = [];
1992
- for (const entry of entries) {
1993
- if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
1994
- const assignmentDir = resolve11(assignmentsDir, entry.name);
1995
- const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
1996
- if (!await fileExists(assignmentMdPath)) continue;
2988
+ async function latestPlanFile(assignmentDir) {
2989
+ let entries;
2990
+ try {
2991
+ entries = await readdir6(assignmentDir);
2992
+ } catch {
2993
+ return null;
2994
+ }
2995
+ let best = null;
2996
+ for (const name of entries) {
2997
+ const m = name.match(PLAN_FILE_RE);
2998
+ if (!m) continue;
2999
+ const version = m[1] ? parseInt(m[1], 10) : 1;
3000
+ if (!best || version > best.version) best = { name, version };
3001
+ }
3002
+ return best?.name ?? null;
3003
+ }
3004
+ function planDigest(content) {
3005
+ return createHash("sha256").update(content, "utf-8").digest("hex");
3006
+ }
3007
+ async function isPlanApproved(assignmentDir, frontmatter) {
3008
+ const approval = frontmatter.planApproval;
3009
+ if (!approval) return false;
3010
+ const latest = await latestPlanFile(assignmentDir);
3011
+ if (!latest || latest !== approval.file) return false;
3012
+ try {
3013
+ const content = await readFile9(resolve11(assignmentDir, latest), "utf-8");
3014
+ return planDigest(content) === approval.digest;
3015
+ } catch {
3016
+ return false;
3017
+ }
3018
+ }
3019
+ async function countUnresolvedQuestions(assignmentDir) {
3020
+ const commentsPath = resolve11(assignmentDir, "comments.md");
3021
+ if (!await fileExists(commentsPath)) return 0;
3022
+ try {
3023
+ const content = await readFile9(commentsPath, "utf-8");
3024
+ let count = 0;
3025
+ for (const block of content.split(/^##\s+/m).slice(1)) {
3026
+ if (/^\*\*Type:\*\*\s*question\s*$/m.test(block) && /^\*\*Resolved:\*\*\s*false\s*$/m.test(block)) {
3027
+ count++;
3028
+ }
3029
+ }
3030
+ return count;
3031
+ } catch {
3032
+ return 0;
3033
+ }
3034
+ }
3035
+ async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses) {
3036
+ if (dependsOn.length === 0 || projectDir === null) return true;
3037
+ for (const depSlug of dependsOn) {
3038
+ const depPath = resolve11(projectDir, "assignments", depSlug, "assignment.md");
3039
+ if (!await fileExists(depPath)) return false;
1997
3040
  try {
1998
- const content = await readFile9(assignmentMdPath, "utf-8");
1999
- const record = parseAssignmentFull(content);
2000
- records.push({ assignmentDir, id: entry.name, record });
3041
+ const content = await readFile9(depPath, "utf-8");
3042
+ const m = content.match(/^status:\s*(.+)$/m);
3043
+ const status = m ? m[1].trim() : "";
3044
+ if (!terminalStatuses.has(status)) return false;
2001
3045
  } catch {
3046
+ return false;
2002
3047
  }
2003
3048
  }
2004
- records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));
2005
- return records;
3049
+ return true;
2006
3050
  }
2007
- function getTransitionDefinitions(config) {
2008
- if (!config.custom) return DEFAULT_TRANSITION_DEFINITIONS;
2009
- const seen = /* @__PURE__ */ new Set();
2010
- return config.transitions.filter((t) => {
2011
- if (seen.has(t.command)) return false;
2012
- seen.add(t.command);
2013
- return true;
2014
- }).map((t) => ({
2015
- command: t.command,
2016
- label: t.label ?? toTitleCase(t.command),
2017
- description: t.description ?? `Transition via ${t.command}.`,
2018
- requiresReason: t.requiresReason ?? false
2019
- }));
3051
+ async function computeFacts(input) {
3052
+ const { assignmentDir, frontmatter, body, projectDir, terminalStatuses } = input;
3053
+ const ac = countRealAcceptanceCriteria(body);
3054
+ const [planFile, planApproved, unresolvedQuestions, depsSatisfied] = await Promise.all([
3055
+ latestPlanFile(assignmentDir),
3056
+ isPlanApproved(assignmentDir, frontmatter),
3057
+ countUnresolvedQuestions(assignmentDir),
3058
+ areDependenciesSatisfied(projectDir, frontmatter.dependsOn, terminalStatuses)
3059
+ ]);
3060
+ return {
3061
+ hasRealObjective: hasRealObjective(body),
3062
+ acRealTotal: ac.total,
3063
+ acRealChecked: ac.checked,
3064
+ acAllChecked: ac.total > 0 && ac.checked === ac.total,
3065
+ planExists: planFile !== null,
3066
+ planApproved,
3067
+ workspaceSet: frontmatter.workspace.repository !== null && frontmatter.workspace.branch !== null,
3068
+ implementationStarted: frontmatter.implementationStarted,
3069
+ depsSatisfied,
3070
+ unresolvedQuestions,
3071
+ blocked: frontmatter.blockedReason !== null,
3072
+ parked: frontmatter.parked,
3073
+ reviewRequested: frontmatter.reviewRequested,
3074
+ pinned: frontmatter.override !== null
3075
+ };
2020
3076
  }
2021
- async function getStatusConfig() {
2022
- if (_cachedConfig) return _cachedConfig;
2023
- const config = await readConfig();
3077
+ var HTML_COMMENT_RE, PLAN_FILE_RE;
3078
+ var init_facts = __esm({
3079
+ "src/lifecycle/facts.ts"() {
3080
+ "use strict";
3081
+ init_fs();
3082
+ HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
3083
+ PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
3084
+ }
3085
+ });
3086
+
3087
+ // src/utils/query/fields.ts
3088
+ function resolveField(registry, name) {
3089
+ return registry[name.toLowerCase()] ?? null;
3090
+ }
3091
+ function readField(def, fieldName, item) {
3092
+ if (def.get) return def.get(item);
3093
+ return item[fieldName] ?? item[fieldName.toLowerCase()];
3094
+ }
3095
+ var init_fields = __esm({
3096
+ "src/utils/query/fields.ts"() {
3097
+ "use strict";
3098
+ }
3099
+ });
3100
+
3101
+ // src/utils/query/evaluate.ts
3102
+ function localDayBounds(date) {
3103
+ const [y, m, d] = date.split("-").map((n) => parseInt(n, 10));
3104
+ const start = new Date(y, m - 1, d).getTime();
3105
+ const end = new Date(y, m - 1, d + 1).getTime();
3106
+ return [start, end];
3107
+ }
3108
+ function toEpoch(value) {
3109
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
3110
+ if (typeof value === "string" && value.length > 0) {
3111
+ const t = Date.parse(value);
3112
+ return Number.isNaN(t) ? null : t;
3113
+ }
3114
+ return null;
3115
+ }
3116
+ function toNumber(value) {
3117
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
3118
+ if (typeof value === "string" && value.trim() !== "") {
3119
+ const n = Number(value);
3120
+ return Number.isFinite(n) ? n : null;
3121
+ }
3122
+ return null;
3123
+ }
3124
+ function ciEquals(a, b) {
3125
+ return typeof a === "string" && a.toLowerCase() === b.toLowerCase();
3126
+ }
3127
+ function isNone(value) {
3128
+ return value === null || value === void 0 || value === "";
3129
+ }
3130
+ function compileEquality(def, field, value, atomPos) {
3131
+ switch (def.kind) {
3132
+ case "enum":
3133
+ case "string":
3134
+ if (def.noneSentinel && value.raw.toLowerCase() === "none") {
3135
+ return (item) => isNone(readField(def, field, item));
3136
+ }
3137
+ return (item) => ciEquals(readField(def, field, item), value.raw);
3138
+ case "substring":
3139
+ return (item) => {
3140
+ const v = readField(def, field, item);
3141
+ return typeof v === "string" && v.toLowerCase().includes(value.raw.toLowerCase());
3142
+ };
3143
+ case "bool": {
3144
+ const want = value.raw.toLowerCase();
3145
+ if (want !== "true" && want !== "false") {
3146
+ throw new CompileError([
3147
+ { pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true or ${field}:false` }
3148
+ ]);
3149
+ }
3150
+ const expected = want === "true";
3151
+ return (item) => {
3152
+ const v = readField(def, field, item);
3153
+ const b = typeof v === "boolean" ? v : v === "true" ? true : v === "false" || v === null || v === void 0 || v === "" ? false : null;
3154
+ return b !== null && b === expected;
3155
+ };
3156
+ }
3157
+ case "number": {
3158
+ const n = value.num ?? toNumber(value.raw);
3159
+ if (n === null) {
3160
+ throw new CompileError([{ pos: value.pos, message: `Field "${field}" is numeric \u2014 "${value.raw}" is not a number` }]);
3161
+ }
3162
+ return (item) => toNumber(readField(def, field, item)) === n;
3163
+ }
3164
+ case "ordinal":
3165
+ return (item) => ciEquals(readField(def, field, item), value.raw);
3166
+ case "list":
3167
+ return (item) => {
3168
+ const v = readField(def, field, item);
3169
+ return Array.isArray(v) && v.some((el) => ciEquals(el, value.raw));
3170
+ };
3171
+ case "timestamp": {
3172
+ if (value.type === "date") {
3173
+ const [start, end] = localDayBounds(value.raw);
3174
+ return (item) => {
3175
+ const t = toEpoch(readField(def, field, item));
3176
+ return t !== null && t >= start && t < end;
3177
+ };
3178
+ }
3179
+ throw new CompileError([
3180
+ { 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)` }
3181
+ ]);
3182
+ }
3183
+ case "duration":
3184
+ throw new CompileError([
3185
+ { pos: atomPos, message: `Field "${field}" is a duration \u2014 use a comparison (e.g. ${field} > 3d)` }
3186
+ ]);
3187
+ }
3188
+ }
3189
+ function compileComparison(def, field, op, value) {
3190
+ const cmp = (a, b) => {
3191
+ switch (op) {
3192
+ case "<":
3193
+ return a < b;
3194
+ case ">":
3195
+ return a > b;
3196
+ case "<=":
3197
+ return a <= b;
3198
+ case ">=":
3199
+ return a >= b;
3200
+ case "=":
3201
+ return a === b;
3202
+ case "!=":
3203
+ return a !== b;
3204
+ default:
3205
+ return false;
3206
+ }
3207
+ };
3208
+ switch (def.kind) {
3209
+ case "number": {
3210
+ const n = value.num ?? toNumber(value.raw);
3211
+ if (n === null) {
3212
+ throw new CompileError([{ pos: value.pos, message: `"${value.raw}" is not a number (field "${field}")` }]);
3213
+ }
3214
+ return (item) => {
3215
+ const v = toNumber(readField(def, field, item));
3216
+ return v !== null && cmp(v, n);
3217
+ };
3218
+ }
3219
+ case "ordinal": {
3220
+ const order = def.order ?? [];
3221
+ const idx = order.findIndex((o) => o.toLowerCase() === value.raw.toLowerCase());
3222
+ if (idx < 0) {
3223
+ throw new CompileError([
3224
+ { pos: value.pos, message: `"${value.raw}" is not a valid ${field} (expected one of: ${order.join(", ")})` }
3225
+ ]);
3226
+ }
3227
+ return (item) => {
3228
+ const raw = readField(def, field, item);
3229
+ const vIdx = typeof raw === "string" ? order.findIndex((o) => o.toLowerCase() === raw.toLowerCase()) : -1;
3230
+ return vIdx >= 0 && cmp(vIdx, idx);
3231
+ };
3232
+ }
3233
+ case "timestamp": {
3234
+ if (value.type === "duration") {
3235
+ const sign = value.sign === 0 ? -1 : value.sign ?? -1;
3236
+ const offset = sign * (value.num ?? 0);
3237
+ return (item, ctx) => {
3238
+ const t = toEpoch(readField(def, field, item));
3239
+ return t !== null && cmp(t, ctx.now + offset);
3240
+ };
3241
+ }
3242
+ if (value.type === "date") {
3243
+ const [start, end] = localDayBounds(value.raw);
3244
+ return (item) => {
3245
+ const t = toEpoch(readField(def, field, item));
3246
+ if (t === null) return false;
3247
+ switch (op) {
3248
+ case "<":
3249
+ return t < start;
3250
+ case "<=":
3251
+ return t < end;
3252
+ case ">":
3253
+ return t >= end;
3254
+ case ">=":
3255
+ return t >= start;
3256
+ case "=":
3257
+ return t >= start && t < end;
3258
+ case "!=":
3259
+ return t < start || t >= end;
3260
+ default:
3261
+ return false;
3262
+ }
3263
+ };
3264
+ }
3265
+ throw new CompileError([
3266
+ { pos: value.pos, message: `Compare timestamp field "${field}" to a duration (e.g. -36h) or a date (YYYY-MM-DD)` }
3267
+ ]);
3268
+ }
3269
+ case "duration": {
3270
+ if (value.type !== "duration") {
3271
+ throw new CompileError([
3272
+ { pos: value.pos, message: `Compare duration field "${field}" to a duration literal (e.g. 3d)` }
3273
+ ]);
3274
+ }
3275
+ const magnitude = value.num ?? 0;
3276
+ return (item) => {
3277
+ const v = toNumber(readField(def, field, item));
3278
+ return v !== null && cmp(v, magnitude);
3279
+ };
3280
+ }
3281
+ case "enum":
3282
+ case "string":
3283
+ case "substring":
3284
+ case "list": {
3285
+ if (op === "=") {
3286
+ return compileEquality(def, field, value, value.pos);
3287
+ }
3288
+ if (op === "!=") {
3289
+ const eq = compileEquality(def, field, value, value.pos);
3290
+ return (item, ctx) => !eq(item, ctx);
3291
+ }
3292
+ throw new CompileError([
3293
+ { pos: value.pos, message: `Field "${field}" does not support ordering comparisons (use ":" or "=").` }
3294
+ ]);
3295
+ }
3296
+ case "bool": {
3297
+ if (op === "=" || op === "!=") {
3298
+ const eq = compileEquality(def, field, value, value.pos);
3299
+ return op === "=" ? eq : (item, ctx) => !eq(item, ctx);
3300
+ }
3301
+ throw new CompileError([{ pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true / ${field}:false` }]);
3302
+ }
3303
+ }
3304
+ }
3305
+ function compileAtom(atom, registry) {
3306
+ const def = resolveField(registry, atom.field);
3307
+ if (!def) {
3308
+ throw new CompileError([{ pos: atom.pos, message: `Unknown field "${atom.field}"` }]);
3309
+ }
3310
+ if (atom.op === ":") {
3311
+ const preds = atom.values.map((v) => compileEquality(def, atom.field, v, atom.pos));
3312
+ if (preds.length === 1) return preds[0];
3313
+ return (item, ctx) => preds.some((p) => p(item, ctx));
3314
+ }
3315
+ return compileComparison(def, atom.field, atom.op, atom.values[0]);
3316
+ }
3317
+ function compileNode(node, registry) {
3318
+ switch (node.kind) {
3319
+ case "all":
3320
+ return () => true;
3321
+ case "atom":
3322
+ return compileAtom(node, registry);
3323
+ case "not": {
3324
+ const inner = compileNode(node.child, registry);
3325
+ return (item, ctx) => !inner(item, ctx);
3326
+ }
3327
+ case "and": {
3328
+ const preds = node.children.map((c) => compileNode(c, registry));
3329
+ return (item, ctx) => preds.every((p) => p(item, ctx));
3330
+ }
3331
+ case "or": {
3332
+ const preds = node.children.map((c) => compileNode(c, registry));
3333
+ return (item, ctx) => preds.some((p) => p(item, ctx));
3334
+ }
3335
+ }
3336
+ }
3337
+ var CompileError;
3338
+ var init_evaluate = __esm({
3339
+ "src/utils/query/evaluate.ts"() {
3340
+ "use strict";
3341
+ init_fields();
3342
+ CompileError = class extends Error {
3343
+ constructor(errors) {
3344
+ super(errors.map((e) => `${e.message} (at ${e.pos})`).join("; "));
3345
+ this.errors = errors;
3346
+ this.name = "CompileError";
3347
+ }
3348
+ };
3349
+ }
3350
+ });
3351
+
3352
+ // src/utils/query/lexer.ts
3353
+ function lex(input) {
3354
+ const tokens = [];
3355
+ let i = 0;
3356
+ const numberOrDuration = (start, sign) => {
3357
+ let j = i;
3358
+ while (j < input.length && /\d/.test(input[j])) j++;
3359
+ const digits = input.slice(i, j);
3360
+ let unit = "";
3361
+ while (j < input.length && /[a-z]/i.test(input[j])) {
3362
+ unit += input[j];
3363
+ j++;
3364
+ }
3365
+ i = j;
3366
+ if (unit.length > 0) {
3367
+ const ms = DURATION_MS[unit.toLowerCase()];
3368
+ if (ms === void 0) {
3369
+ throw new LexError(start, `Unknown duration unit "${unit}" (expected h, d, w, m, mo, or y)`);
3370
+ }
3371
+ return {
3372
+ type: "DURATION",
3373
+ text: input.slice(start, j),
3374
+ pos: start,
3375
+ num: parseInt(digits, 10) * ms,
3376
+ sign
3377
+ };
3378
+ }
3379
+ if (sign !== 0) {
3380
+ return { type: "NUMBER", text: input.slice(start, j), pos: start, num: sign * parseInt(digits, 10) };
3381
+ }
3382
+ return { type: "NUMBER", text: digits, pos: start, num: parseInt(digits, 10) };
3383
+ };
3384
+ while (i < input.length) {
3385
+ const c = input[i];
3386
+ const start = i;
3387
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
3388
+ i++;
3389
+ continue;
3390
+ }
3391
+ if (c === "(") {
3392
+ tokens.push({ type: "LPAREN", text: c, pos: start });
3393
+ i++;
3394
+ continue;
3395
+ }
3396
+ if (c === ")") {
3397
+ tokens.push({ type: "RPAREN", text: c, pos: start });
3398
+ i++;
3399
+ continue;
3400
+ }
3401
+ if (c === ",") {
3402
+ tokens.push({ type: "COMMA", text: c, pos: start });
3403
+ i++;
3404
+ continue;
3405
+ }
3406
+ if (c === ":") {
3407
+ tokens.push({ type: "COLON", text: c, pos: start });
3408
+ i++;
3409
+ continue;
3410
+ }
3411
+ if (c === "*") {
3412
+ tokens.push({ type: "STAR", text: c, pos: start });
3413
+ i++;
3414
+ continue;
3415
+ }
3416
+ if (c === "<" || c === ">") {
3417
+ if (input[i + 1] === "=") {
3418
+ tokens.push({ type: "OP", text: c + "=", pos: start });
3419
+ i += 2;
3420
+ } else {
3421
+ tokens.push({ type: "OP", text: c, pos: start });
3422
+ i++;
3423
+ }
3424
+ continue;
3425
+ }
3426
+ if (c === "!") {
3427
+ if (input[i + 1] === "=") {
3428
+ tokens.push({ type: "OP", text: "!=", pos: start });
3429
+ i += 2;
3430
+ continue;
3431
+ }
3432
+ throw new LexError(start, `Unexpected "!" (did you mean "!="?)`);
3433
+ }
3434
+ if (c === "=") {
3435
+ i += input[i + 1] === "=" ? 2 : 1;
3436
+ tokens.push({ type: "OP", text: "=", pos: start });
3437
+ continue;
3438
+ }
3439
+ if (c === '"' || c === "'") {
3440
+ const quote = c;
3441
+ let j = i + 1;
3442
+ let out = "";
3443
+ while (j < input.length && input[j] !== quote) {
3444
+ if (input[j] === "\\" && j + 1 < input.length) {
3445
+ out += input[j + 1];
3446
+ j += 2;
3447
+ } else {
3448
+ out += input[j];
3449
+ j++;
3450
+ }
3451
+ }
3452
+ if (j >= input.length) throw new LexError(start, "Unterminated string literal");
3453
+ tokens.push({ type: "STRING", text: out, pos: start });
3454
+ i = j + 1;
3455
+ continue;
3456
+ }
3457
+ if (c === "-" || c === "+") {
3458
+ if (/\d/.test(input[i + 1] ?? "")) {
3459
+ const sign = c === "-" ? -1 : 1;
3460
+ i++;
3461
+ tokens.push(numberOrDuration(start, sign));
3462
+ continue;
3463
+ }
3464
+ if (c === "-") {
3465
+ tokens.push({ type: "MINUS", text: "-", pos: start });
3466
+ i++;
3467
+ continue;
3468
+ }
3469
+ throw new LexError(start, 'Unexpected "+"');
3470
+ }
3471
+ if (/\d/.test(c)) {
3472
+ const dateMatch = input.slice(i).match(DATE_RE);
3473
+ if (dateMatch) {
3474
+ tokens.push({ type: "DATE", text: dateMatch[0], pos: start });
3475
+ i += dateMatch[0].length;
3476
+ continue;
3477
+ }
3478
+ tokens.push(numberOrDuration(start, 0));
3479
+ continue;
3480
+ }
3481
+ if (IDENT_START.test(c)) {
3482
+ let j = i + 1;
3483
+ while (j < input.length && IDENT_CHAR.test(input[j])) j++;
3484
+ const word = input.slice(i, j);
3485
+ const kw = word.toLowerCase();
3486
+ if (kw === "and") tokens.push({ type: "AND", text: word, pos: start });
3487
+ else if (kw === "or") tokens.push({ type: "OR", text: word, pos: start });
3488
+ else if (kw === "not") tokens.push({ type: "NOT", text: word, pos: start });
3489
+ else tokens.push({ type: "IDENT", text: word, pos: start });
3490
+ i = j;
3491
+ continue;
3492
+ }
3493
+ throw new LexError(start, `Unexpected character "${c}"`);
3494
+ }
3495
+ tokens.push({ type: "EOF", text: "", pos: input.length });
3496
+ return tokens;
3497
+ }
3498
+ var LexError, DURATION_MS, IDENT_START, IDENT_CHAR, DATE_RE;
3499
+ var init_lexer = __esm({
3500
+ "src/utils/query/lexer.ts"() {
3501
+ "use strict";
3502
+ LexError = class extends Error {
3503
+ constructor(pos, message) {
3504
+ super(message);
3505
+ this.pos = pos;
3506
+ this.name = "LexError";
3507
+ }
3508
+ };
3509
+ DURATION_MS = {
3510
+ h: 36e5,
3511
+ d: 864e5,
3512
+ w: 7 * 864e5,
3513
+ m: 30 * 864e5,
3514
+ mo: 30 * 864e5,
3515
+ y: 365 * 864e5
3516
+ };
3517
+ IDENT_START = /[A-Za-z_]/;
3518
+ IDENT_CHAR = /[A-Za-z0-9_-]/;
3519
+ DATE_RE = /^\d{4}-\d{2}-\d{2}/;
3520
+ }
3521
+ });
3522
+
3523
+ // src/utils/query/parser.ts
3524
+ function parseQuery(input) {
3525
+ try {
3526
+ const tokens = lex(input);
3527
+ const ast = new Parser(tokens).parseQuery();
3528
+ return { ast, errors: [] };
3529
+ } catch (err) {
3530
+ if (err instanceof LexError || err instanceof ParseError) {
3531
+ return { ast: null, errors: [{ pos: err.pos, message: err.message }] };
3532
+ }
3533
+ throw err;
3534
+ }
3535
+ }
3536
+ var ParseError, VALUE_TOKENS, TERM_START, Parser;
3537
+ var init_parser3 = __esm({
3538
+ "src/utils/query/parser.ts"() {
3539
+ "use strict";
3540
+ init_lexer();
3541
+ ParseError = class extends Error {
3542
+ constructor(pos, message) {
3543
+ super(message);
3544
+ this.pos = pos;
3545
+ this.name = "ParseError";
3546
+ }
3547
+ };
3548
+ VALUE_TOKENS = /* @__PURE__ */ new Set(["IDENT", "STRING", "NUMBER", "DATE", "DURATION"]);
3549
+ TERM_START = /* @__PURE__ */ new Set(["IDENT", "NOT", "MINUS", "LPAREN", "STAR"]);
3550
+ Parser = class {
3551
+ constructor(tokens) {
3552
+ this.tokens = tokens;
3553
+ }
3554
+ pos = 0;
3555
+ peek() {
3556
+ return this.tokens[this.pos];
3557
+ }
3558
+ next() {
3559
+ return this.tokens[this.pos++];
3560
+ }
3561
+ expect(type, what) {
3562
+ const tok = this.peek();
3563
+ if (tok.type !== type) {
3564
+ throw new ParseError(tok.pos, `Expected ${what}, got "${tok.text || tok.type}"`);
3565
+ }
3566
+ return this.next();
3567
+ }
3568
+ parseQuery() {
3569
+ if (this.peek().type === "EOF") return { kind: "all" };
3570
+ const node = this.orExpr();
3571
+ const tok = this.peek();
3572
+ if (tok.type !== "EOF") {
3573
+ throw new ParseError(tok.pos, `Unexpected "${tok.text}" \u2014 unbalanced parentheses or stray token`);
3574
+ }
3575
+ return node;
3576
+ }
3577
+ orExpr() {
3578
+ const children = [this.andExpr()];
3579
+ while (this.peek().type === "OR") {
3580
+ this.next();
3581
+ children.push(this.andExpr());
3582
+ }
3583
+ return children.length === 1 ? children[0] : { kind: "or", children };
3584
+ }
3585
+ andExpr() {
3586
+ const children = [this.unary()];
3587
+ for (; ; ) {
3588
+ const tok = this.peek();
3589
+ if (tok.type === "AND") {
3590
+ this.next();
3591
+ children.push(this.unary());
3592
+ } else if (TERM_START.has(tok.type)) {
3593
+ children.push(this.unary());
3594
+ } else {
3595
+ break;
3596
+ }
3597
+ }
3598
+ return children.length === 1 ? children[0] : { kind: "and", children };
3599
+ }
3600
+ unary() {
3601
+ const tok = this.peek();
3602
+ if (tok.type === "NOT") {
3603
+ this.next();
3604
+ return { kind: "not", child: this.unary() };
3605
+ }
3606
+ if (tok.type === "MINUS") {
3607
+ this.next();
3608
+ const inner = this.peek();
3609
+ if (inner.type !== "IDENT") {
3610
+ throw new ParseError(inner.pos, 'Expected a field atom after "-" negation');
3611
+ }
3612
+ return { kind: "not", child: this.atom() };
3613
+ }
3614
+ return this.primary();
3615
+ }
3616
+ primary() {
3617
+ const tok = this.peek();
3618
+ if (tok.type === "LPAREN") {
3619
+ this.next();
3620
+ const node = this.orExpr();
3621
+ this.expect("RPAREN", '")"');
3622
+ return node;
3623
+ }
3624
+ if (tok.type === "STAR") {
3625
+ this.next();
3626
+ return { kind: "all" };
3627
+ }
3628
+ if (tok.type === "IDENT") {
3629
+ return this.atom();
3630
+ }
3631
+ throw new ParseError(tok.pos, `Expected a field, "(", "*", or NOT \u2014 got "${tok.text || "end of query"}"`);
3632
+ }
3633
+ atom() {
3634
+ const fieldTok = this.expect("IDENT", "a field name");
3635
+ const opTok = this.peek();
3636
+ if (opTok.type === "COLON") {
3637
+ this.next();
3638
+ const values = this.valueOrList();
3639
+ return { kind: "atom", field: fieldTok.text, op: ":", values, pos: fieldTok.pos };
3640
+ }
3641
+ if (opTok.type === "OP") {
3642
+ this.next();
3643
+ const value = this.value();
3644
+ return {
3645
+ kind: "atom",
3646
+ field: fieldTok.text,
3647
+ op: opTok.text,
3648
+ values: [value],
3649
+ pos: fieldTok.pos
3650
+ };
3651
+ }
3652
+ throw new ParseError(
3653
+ opTok.pos,
3654
+ `Expected ":" or a comparison operator after field "${fieldTok.text}"`
3655
+ );
3656
+ }
3657
+ valueOrList() {
3658
+ if (this.peek().type === "LPAREN") {
3659
+ this.next();
3660
+ const values = [this.value()];
3661
+ while (this.peek().type === "COMMA") {
3662
+ this.next();
3663
+ values.push(this.value());
3664
+ }
3665
+ this.expect("RPAREN", '")" to close the value list');
3666
+ return values;
3667
+ }
3668
+ return [this.value()];
3669
+ }
3670
+ value() {
3671
+ const tok = this.peek();
3672
+ if (!VALUE_TOKENS.has(tok.type)) {
3673
+ throw new ParseError(tok.pos, `Expected a value, got "${tok.text || tok.type}"`);
3674
+ }
3675
+ this.next();
3676
+ switch (tok.type) {
3677
+ case "STRING":
3678
+ return { type: "string", raw: tok.text, pos: tok.pos };
3679
+ case "NUMBER":
3680
+ return { type: "number", raw: tok.text, num: tok.num, pos: tok.pos };
3681
+ case "DATE":
3682
+ return { type: "date", raw: tok.text, pos: tok.pos };
3683
+ case "DURATION":
3684
+ return { type: "duration", raw: tok.text, num: tok.num, sign: tok.sign ?? 0, pos: tok.pos };
3685
+ default:
3686
+ return { type: "word", raw: tok.text, pos: tok.pos };
3687
+ }
3688
+ }
3689
+ };
3690
+ }
3691
+ });
3692
+
3693
+ // src/utils/query/index.ts
3694
+ var init_query = __esm({
3695
+ "src/utils/query/index.ts"() {
3696
+ "use strict";
3697
+ init_evaluate();
3698
+ init_fields();
3699
+ init_parser3();
3700
+ init_parser3();
3701
+ init_evaluate();
3702
+ init_fields();
3703
+ }
3704
+ });
3705
+
3706
+ // src/lifecycle/derive.ts
3707
+ var derive_exports = {};
3708
+ __export(derive_exports, {
3709
+ DERIVE_FIELDS: () => DERIVE_FIELDS,
3710
+ deriveDimensions: () => deriveDimensions,
3711
+ validateDeriveCondition: () => validateDeriveCondition
3712
+ });
3713
+ function validateDeriveCondition(when) {
3714
+ if (when === "*") return null;
3715
+ const parsed = parseQuery(when);
3716
+ if (!parsed.ast) return parsed.errors[0]?.message ?? "unparseable condition";
3717
+ try {
3718
+ compileNode(parsed.ast, DERIVE_FIELDS);
3719
+ return null;
3720
+ } catch (err) {
3721
+ if (err instanceof CompileError) return err.errors[0]?.message ?? "invalid condition";
3722
+ throw err;
3723
+ }
3724
+ }
3725
+ function compiledWhen(derive, when) {
3726
+ let cache = conditionCache.get(derive);
3727
+ if (!cache) {
3728
+ cache = /* @__PURE__ */ new Map();
3729
+ conditionCache.set(derive, cache);
3730
+ }
3731
+ let pred = cache.get(when);
3732
+ if (!pred) {
3733
+ if (when === "*") {
3734
+ pred = () => true;
3735
+ } else {
3736
+ const parsed = parseQuery(when);
3737
+ if (!parsed.ast) {
3738
+ throw new CompileError(parsed.errors);
3739
+ }
3740
+ pred = compileNode(parsed.ast, DERIVE_FIELDS);
3741
+ }
3742
+ cache.set(when, pred);
3743
+ }
3744
+ return pred;
3745
+ }
3746
+ function deriveDimensions(input) {
3747
+ const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
3748
+ if (terminalStatuses.has(currentStatus)) return null;
3749
+ const ctx = { now: 0 };
3750
+ const item = facts;
3751
+ let phase = derive.phaseLadder[0]?.phase ?? currentStatus;
3752
+ let nextAction = derive.phaseLadder[0]?.next ?? null;
3753
+ for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {
3754
+ const rung = derive.phaseLadder[i];
3755
+ if (compiledWhen(derive, rung.when)(item, ctx)) {
3756
+ phase = rung.phase;
3757
+ nextAction = rung.next ?? null;
3758
+ break;
3759
+ }
3760
+ }
3761
+ let disposition = "active";
3762
+ for (const rule of derive.disposition) {
3763
+ if (rule.when === null || compiledWhen(derive, rule.when)(item, ctx)) {
3764
+ disposition = rule.is;
3765
+ break;
3766
+ }
3767
+ }
3768
+ let derivedStatus;
3769
+ switch (disposition) {
3770
+ case "parked":
3771
+ derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;
3772
+ break;
3773
+ case "blocked":
3774
+ derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;
3775
+ break;
3776
+ default:
3777
+ derivedStatus = phase;
3778
+ }
3779
+ let status = derivedStatus;
3780
+ if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
3781
+ status = override.status;
3782
+ }
3783
+ return { phase, disposition, derivedStatus, status, nextAction };
3784
+ }
3785
+ var DERIVE_FIELDS, conditionCache;
3786
+ var init_derive = __esm({
3787
+ "src/lifecycle/derive.ts"() {
3788
+ "use strict";
3789
+ init_query();
3790
+ DERIVE_FIELDS = {
3791
+ hasrealobjective: { kind: "bool", get: (i) => i["hasRealObjective"] },
3792
+ acrealtotal: { kind: "number", get: (i) => i["acRealTotal"] },
3793
+ acrealchecked: { kind: "number", get: (i) => i["acRealChecked"] },
3794
+ acallchecked: { kind: "bool", get: (i) => i["acAllChecked"] },
3795
+ planexists: { kind: "bool", get: (i) => i["planExists"] },
3796
+ planapproved: { kind: "bool", get: (i) => i["planApproved"] },
3797
+ workspaceset: { kind: "bool", get: (i) => i["workspaceSet"] },
3798
+ implementationstarted: { kind: "bool", get: (i) => i["implementationStarted"] },
3799
+ depssatisfied: { kind: "bool", get: (i) => i["depsSatisfied"] },
3800
+ unresolvedquestions: { kind: "number", get: (i) => i["unresolvedQuestions"] },
3801
+ blocked: { kind: "bool" },
3802
+ parked: { kind: "bool" },
3803
+ reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
3804
+ pinned: { kind: "bool" }
3805
+ };
3806
+ conditionCache = /* @__PURE__ */ new WeakMap();
3807
+ }
3808
+ });
3809
+
3810
+ // src/dashboard/api.ts
3811
+ import { readdir as readdir7, readFile as readFile10, writeFile as writeFile3 } from "fs/promises";
3812
+ import { resolve as resolve12, dirname as dirname2, basename } from "path";
3813
+ function activeAssignments(items) {
3814
+ return items.filter((item) => item.archived !== true);
3815
+ }
3816
+ function accumulatePhase(traces, label, ms) {
3817
+ if (!traces) return;
3818
+ traces.subPhases.set(label, (traces.subPhases.get(label) ?? 0) + ms);
3819
+ }
3820
+ async function listStandaloneRecords(assignmentsDir) {
3821
+ const key = assignmentsDir ?? "";
3822
+ const cached = standaloneRecordsCache.get(key);
3823
+ if (cached) return cached;
3824
+ const promise = computeStandaloneRecords(assignmentsDir);
3825
+ standaloneRecordsCache.set(key, promise);
3826
+ promise.catch(() => standaloneRecordsCache.delete(key));
3827
+ return promise;
3828
+ }
3829
+ async function computeStandaloneRecords(assignmentsDir) {
3830
+ if (!assignmentsDir) return [];
3831
+ if (!await fileExists(assignmentsDir)) return [];
3832
+ const entries = await readdir7(assignmentsDir, { withFileTypes: true });
3833
+ const records = [];
3834
+ for (const entry of entries) {
3835
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
3836
+ const assignmentDir = resolve12(assignmentsDir, entry.name);
3837
+ const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
3838
+ if (!await fileExists(assignmentMdPath)) continue;
3839
+ try {
3840
+ const content = await readFile10(assignmentMdPath, "utf-8");
3841
+ const record = parseAssignmentFull(content);
3842
+ records.push({ assignmentDir, id: entry.name, record });
3843
+ } catch {
3844
+ }
3845
+ }
3846
+ records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));
3847
+ return records;
3848
+ }
3849
+ function getTransitionDefinitions(config) {
3850
+ if (!config.custom) return DEFAULT_TRANSITION_DEFINITIONS;
3851
+ const seen = /* @__PURE__ */ new Set();
3852
+ return config.transitions.filter((t) => {
3853
+ if (seen.has(t.command)) return false;
3854
+ seen.add(t.command);
3855
+ return true;
3856
+ }).map((t) => ({
3857
+ command: t.command,
3858
+ label: t.label ?? toTitleCase(t.command),
3859
+ description: t.description ?? `Transition via ${t.command}.`,
3860
+ requiresReason: t.requiresReason ?? false
3861
+ }));
3862
+ }
3863
+ async function getStatusConfig() {
3864
+ if (_cachedConfig) return _cachedConfig;
3865
+ const config = await readConfig();
2024
3866
  if (config.statuses) {
2025
3867
  const terminalSet = new Set(
2026
3868
  config.statuses.statuses.filter((s) => s.terminal).map((s) => s.id)
@@ -2036,7 +3878,8 @@ async function getStatusConfig() {
2036
3878
  order: config.statuses.order,
2037
3879
  transitions: effectiveTransitions,
2038
3880
  transitionTable: buildTransitionTable(effectiveTransitions),
2039
- terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
3881
+ terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"]),
3882
+ derive: config.statuses.derive ?? null
2040
3883
  };
2041
3884
  } else {
2042
3885
  const def = buildDefaultStatusConfig();
@@ -2046,7 +3889,8 @@ async function getStatusConfig() {
2046
3889
  order: def.order,
2047
3890
  transitions: def.transitions,
2048
3891
  transitionTable: DEFAULT_TRANSITION_TABLE,
2049
- terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
3892
+ terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"]),
3893
+ derive: null
2050
3894
  };
2051
3895
  }
2052
3896
  return _cachedConfig;
@@ -2076,23 +3920,23 @@ async function getStandaloneAvailableTransitions(assignment) {
2076
3920
  return actions;
2077
3921
  }
2078
3922
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2079
- const assignmentDir = resolve11(projectsDir, projectSlug, "assignments", assignmentSlug);
2080
- const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
3923
+ const assignmentDir = resolve12(projectsDir, projectSlug, "assignments", assignmentSlug);
3924
+ const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
2081
3925
  if (!await fileExists(assignmentMdPath)) {
2082
3926
  return null;
2083
3927
  }
2084
- const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
3928
+ const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
2085
3929
  const assignment = parseAssignmentFull(assignmentContent);
2086
3930
  let projectWorkspace = null;
2087
- const projectMdPath = resolve11(projectsDir, projectSlug, "project.md");
3931
+ const projectMdPath = resolve12(projectsDir, projectSlug, "project.md");
2088
3932
  if (await fileExists(projectMdPath)) {
2089
- const projectContent = await readFile9(projectMdPath, "utf-8");
3933
+ const projectContent = await readFile10(projectMdPath, "utf-8");
2090
3934
  projectWorkspace = parseProject(projectContent).workspace;
2091
3935
  }
2092
3936
  let plan = null;
2093
- const planPath = resolve11(assignmentDir, "plan.md");
3937
+ const planPath = resolve12(assignmentDir, "plan.md");
2094
3938
  if (await fileExists(planPath)) {
2095
- const planContent = await readFile9(planPath, "utf-8");
3939
+ const planContent = await readFile10(planPath, "utf-8");
2096
3940
  const parsed = parsePlan(planContent);
2097
3941
  plan = {
2098
3942
  status: parsed.status,
@@ -2101,9 +3945,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2101
3945
  };
2102
3946
  }
2103
3947
  let scratchpad = null;
2104
- const scratchpadPath = resolve11(assignmentDir, "scratchpad.md");
3948
+ const scratchpadPath = resolve12(assignmentDir, "scratchpad.md");
2105
3949
  if (await fileExists(scratchpadPath)) {
2106
- const scratchpadContent = await readFile9(scratchpadPath, "utf-8");
3950
+ const scratchpadContent = await readFile10(scratchpadPath, "utf-8");
2107
3951
  const parsed = parseScratchpad(scratchpadContent);
2108
3952
  scratchpad = {
2109
3953
  updated: parsed.updated,
@@ -2111,9 +3955,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2111
3955
  };
2112
3956
  }
2113
3957
  let handoff = null;
2114
- const handoffPath = resolve11(assignmentDir, "handoff.md");
3958
+ const handoffPath = resolve12(assignmentDir, "handoff.md");
2115
3959
  if (await fileExists(handoffPath)) {
2116
- const handoffContent = await readFile9(handoffPath, "utf-8");
3960
+ const handoffContent = await readFile10(handoffPath, "utf-8");
2117
3961
  const parsed = parseHandoff(handoffContent);
2118
3962
  handoff = {
2119
3963
  updated: parsed.updated,
@@ -2122,9 +3966,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2122
3966
  };
2123
3967
  }
2124
3968
  let decisionRecord = null;
2125
- const decisionRecordPath = resolve11(assignmentDir, "decision-record.md");
3969
+ const decisionRecordPath = resolve12(assignmentDir, "decision-record.md");
2126
3970
  if (await fileExists(decisionRecordPath)) {
2127
- const decisionRecordContent = await readFile9(decisionRecordPath, "utf-8");
3971
+ const decisionRecordContent = await readFile10(decisionRecordPath, "utf-8");
2128
3972
  const parsed = parseDecisionRecord(decisionRecordContent);
2129
3973
  decisionRecord = {
2130
3974
  updated: parsed.updated,
@@ -2133,9 +3977,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2133
3977
  };
2134
3978
  }
2135
3979
  let progress = null;
2136
- const progressPath = resolve11(assignmentDir, "progress.md");
3980
+ const progressPath = resolve12(assignmentDir, "progress.md");
2137
3981
  if (await fileExists(progressPath)) {
2138
- const progressContent = await readFile9(progressPath, "utf-8");
3982
+ const progressContent = await readFile10(progressPath, "utf-8");
2139
3983
  const parsed = parseProgress(progressContent);
2140
3984
  progress = {
2141
3985
  updated: parsed.updated,
@@ -2144,9 +3988,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2144
3988
  };
2145
3989
  }
2146
3990
  let comments = null;
2147
- const commentsPath = resolve11(assignmentDir, "comments.md");
3991
+ const commentsPath = resolve12(assignmentDir, "comments.md");
2148
3992
  if (await fileExists(commentsPath)) {
2149
- const commentsContent = await readFile9(commentsPath, "utf-8");
3993
+ const commentsContent = await readFile10(commentsPath, "utf-8");
2150
3994
  const parsed = parseComments(commentsContent);
2151
3995
  comments = {
2152
3996
  updated: parsed.updated,
@@ -2154,6 +3998,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2154
3998
  entries: parsed.entries
2155
3999
  };
2156
4000
  }
4001
+ const { terminalStatuses } = await getStatusConfig();
2157
4002
  const detail = {
2158
4003
  id: assignment.id,
2159
4004
  projectSlug,
@@ -2175,6 +4020,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
2175
4020
  archived: assignment.archived,
2176
4021
  archivedAt: assignment.archivedAt,
2177
4022
  archivedReason: assignment.archivedReason,
4023
+ ...deriveStatusVirtuals(assignment, terminalStatuses),
4024
+ override: assignment.override,
4025
+ derived: await buildDerivedDetail(assignment, assignmentDir, resolve12(projectsDir, projectSlug)),
2178
4026
  created: assignment.created,
2179
4027
  updated: assignment.updated,
2180
4028
  body: assignment.body,
@@ -2265,7 +4113,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2265
4113
  slug: a.slug,
2266
4114
  title: a.title,
2267
4115
  projectSlug: rec.summary.slug,
2268
- assignmentDir: resolve11(rec.projectPath, "assignments", a.slug)
4116
+ assignmentDir: resolve12(rec.projectPath, "assignments", a.slug)
2269
4117
  });
2270
4118
  }
2271
4119
  }
@@ -2298,17 +4146,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
2298
4146
  }
2299
4147
  async function countMentionsInAssignment(sourceDir, target) {
2300
4148
  const bodies = [];
2301
- const assignmentMd = resolve11(sourceDir, "assignment.md");
4149
+ const assignmentMd = resolve12(sourceDir, "assignment.md");
2302
4150
  if (await fileExists(assignmentMd)) {
2303
- const content = await readFile9(assignmentMd, "utf-8");
4151
+ const content = await readFile10(assignmentMd, "utf-8");
2304
4152
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
2305
4153
  if (todosMatch) bodies.push(todosMatch[1]);
2306
4154
  }
2307
4155
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
2308
- const path = resolve11(sourceDir, filename);
4156
+ const path = resolve12(sourceDir, filename);
2309
4157
  if (await fileExists(path)) {
2310
4158
  try {
2311
- bodies.push(await readFile9(path, "utf-8"));
4159
+ bodies.push(await readFile10(path, "utf-8"));
2312
4160
  } catch {
2313
4161
  }
2314
4162
  }
@@ -2366,46 +4214,47 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
2366
4214
  }
2367
4215
  async function buildStandaloneAssignmentDetail(resolved) {
2368
4216
  const assignmentDir = resolved.assignmentDir;
2369
- const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
4217
+ const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
2370
4218
  if (!await fileExists(assignmentMdPath)) return null;
2371
- const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
4219
+ const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
2372
4220
  const assignment = parseAssignmentFull(assignmentContent);
2373
4221
  let plan = null;
2374
- const planPath = resolve11(assignmentDir, "plan.md");
4222
+ const planPath = resolve12(assignmentDir, "plan.md");
2375
4223
  if (await fileExists(planPath)) {
2376
- const parsed = parsePlan(await readFile9(planPath, "utf-8"));
4224
+ const parsed = parsePlan(await readFile10(planPath, "utf-8"));
2377
4225
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
2378
4226
  }
2379
4227
  let scratchpad = null;
2380
- const scratchpadPath = resolve11(assignmentDir, "scratchpad.md");
4228
+ const scratchpadPath = resolve12(assignmentDir, "scratchpad.md");
2381
4229
  if (await fileExists(scratchpadPath)) {
2382
- const parsed = parseScratchpad(await readFile9(scratchpadPath, "utf-8"));
4230
+ const parsed = parseScratchpad(await readFile10(scratchpadPath, "utf-8"));
2383
4231
  scratchpad = { updated: parsed.updated, body: parsed.body };
2384
4232
  }
2385
4233
  let handoff = null;
2386
- const handoffPath = resolve11(assignmentDir, "handoff.md");
4234
+ const handoffPath = resolve12(assignmentDir, "handoff.md");
2387
4235
  if (await fileExists(handoffPath)) {
2388
- const parsed = parseHandoff(await readFile9(handoffPath, "utf-8"));
4236
+ const parsed = parseHandoff(await readFile10(handoffPath, "utf-8"));
2389
4237
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
2390
4238
  }
2391
4239
  let decisionRecord = null;
2392
- const decisionRecordPath = resolve11(assignmentDir, "decision-record.md");
4240
+ const decisionRecordPath = resolve12(assignmentDir, "decision-record.md");
2393
4241
  if (await fileExists(decisionRecordPath)) {
2394
- const parsed = parseDecisionRecord(await readFile9(decisionRecordPath, "utf-8"));
4242
+ const parsed = parseDecisionRecord(await readFile10(decisionRecordPath, "utf-8"));
2395
4243
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
2396
4244
  }
2397
4245
  let progress = null;
2398
- const progressPath = resolve11(assignmentDir, "progress.md");
4246
+ const progressPath = resolve12(assignmentDir, "progress.md");
2399
4247
  if (await fileExists(progressPath)) {
2400
- const parsed = parseProgress(await readFile9(progressPath, "utf-8"));
4248
+ const parsed = parseProgress(await readFile10(progressPath, "utf-8"));
2401
4249
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2402
4250
  }
2403
4251
  let comments = null;
2404
- const commentsPath = resolve11(assignmentDir, "comments.md");
4252
+ const commentsPath = resolve12(assignmentDir, "comments.md");
2405
4253
  if (await fileExists(commentsPath)) {
2406
- const parsed = parseComments(await readFile9(commentsPath, "utf-8"));
4254
+ const parsed = parseComments(await readFile10(commentsPath, "utf-8"));
2407
4255
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2408
4256
  }
4257
+ const { terminalStatuses } = await getStatusConfig();
2409
4258
  const detail = {
2410
4259
  id: assignment.id,
2411
4260
  projectSlug: null,
@@ -2428,6 +4277,9 @@ async function buildStandaloneAssignmentDetail(resolved) {
2428
4277
  archived: assignment.archived,
2429
4278
  archivedAt: assignment.archivedAt,
2430
4279
  archivedReason: assignment.archivedReason,
4280
+ ...deriveStatusVirtuals(assignment, terminalStatuses),
4281
+ override: assignment.override,
4282
+ derived: await buildDerivedDetail(assignment, assignmentDir, null),
2431
4283
  created: assignment.created,
2432
4284
  updated: assignment.updated,
2433
4285
  body: assignment.body,
@@ -2459,17 +4311,17 @@ async function computeProjectRecords(projectsDir, traces) {
2459
4311
  await migrateLegacyProjectFiles(projectsDir);
2460
4312
  await migrateLegacyArchivedProjects(projectsDir);
2461
4313
  }
2462
- const entries = await readdir6(projectsDir, { withFileTypes: true });
4314
+ const entries = await readdir7(projectsDir, { withFileTypes: true });
2463
4315
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
2464
4316
  const maybeRecords = await Promise.all(
2465
4317
  projectDirs.map(async (entry) => {
2466
- const projectPath = resolve11(projectsDir, entry.name);
2467
- const projectMdPath = resolve11(projectPath, "project.md");
4318
+ const projectPath = resolve12(projectsDir, entry.name);
4319
+ const projectMdPath = resolve12(projectPath, "project.md");
2468
4320
  if (!await fileExists(projectMdPath)) {
2469
4321
  return null;
2470
4322
  }
2471
4323
  const t0 = traces ? performance.now() : 0;
2472
- const projectContent = await readFile9(projectMdPath, "utf-8");
4324
+ const projectContent = await readFile10(projectMdPath, "utf-8");
2473
4325
  const project = parseProject(projectContent);
2474
4326
  if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
2475
4327
  const t1 = traces ? performance.now() : 0;
@@ -2510,20 +4362,20 @@ async function computeProjectRecords(projectsDir, traces) {
2510
4362
  return records;
2511
4363
  }
2512
4364
  async function listAssignmentRecords(projectPath, traces) {
2513
- const assignmentsDir = resolve11(projectPath, "assignments");
4365
+ const assignmentsDir = resolve12(projectPath, "assignments");
2514
4366
  if (!await fileExists(assignmentsDir)) {
2515
4367
  return [];
2516
4368
  }
2517
- const entries = await readdir6(assignmentsDir, { withFileTypes: true });
4369
+ const entries = await readdir7(assignmentsDir, { withFileTypes: true });
2518
4370
  const dirEntries = entries.filter((entry) => entry.isDirectory());
2519
4371
  const maybeRecords = await Promise.all(
2520
4372
  dirEntries.map(async (entry) => {
2521
- const assignmentMd = resolve11(assignmentsDir, entry.name, "assignment.md");
4373
+ const assignmentMd = resolve12(assignmentsDir, entry.name, "assignment.md");
2522
4374
  if (!await fileExists(assignmentMd)) {
2523
4375
  return null;
2524
4376
  }
2525
4377
  const t0 = traces ? performance.now() : 0;
2526
- const content = await readFile9(assignmentMd, "utf-8");
4378
+ const content = await readFile10(assignmentMd, "utf-8");
2527
4379
  const parsed = parseAssignmentFull(content);
2528
4380
  if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
2529
4381
  return parsed;
@@ -2534,9 +4386,9 @@ async function listAssignmentRecords(projectPath, traces) {
2534
4386
  return records;
2535
4387
  }
2536
4388
  async function loadDependencyGraph(projectPath, assignments) {
2537
- const statusPath = resolve11(projectPath, "_status.md");
4389
+ const statusPath = resolve12(projectPath, "_status.md");
2538
4390
  if (await fileExists(statusPath)) {
2539
- const statusContent = await readFile9(statusPath, "utf-8");
4391
+ const statusContent = await readFile10(statusPath, "utf-8");
2540
4392
  const parsed = parseStatus(statusContent);
2541
4393
  const derivedGraph = extractMermaidGraph(parsed.body);
2542
4394
  if (derivedGraph) {
@@ -2586,6 +4438,78 @@ async function buildProjectRollup(projectPath, project, assignments, traces) {
2586
4438
  }
2587
4439
  return { progress, needsAttention, status };
2588
4440
  }
4441
+ function deriveStatusVirtuals(assignment, terminalStatuses) {
4442
+ const hist = assignment.statusHistory ?? [];
4443
+ let completedAt = null;
4444
+ if (terminalStatuses.has(assignment.status)) {
4445
+ for (const entry of hist) {
4446
+ if (entry.to === assignment.status) completedAt = entry.at;
4447
+ }
4448
+ }
4449
+ let statusAge = null;
4450
+ for (let i = hist.length - 1; i >= 0; i--) {
4451
+ const entry = hist[i];
4452
+ if (entry.from !== entry.to || entry.from === null) {
4453
+ const t = Date.parse(entry.at);
4454
+ statusAge = Number.isNaN(t) ? null : Date.now() - t;
4455
+ break;
4456
+ }
4457
+ }
4458
+ let phaseAge = null;
4459
+ for (let i = hist.length - 1; i >= 0; i--) {
4460
+ const entry = hist[i];
4461
+ if (entry.phaseTo !== void 0 && entry.phaseFrom !== entry.phaseTo) {
4462
+ const t = Date.parse(entry.at);
4463
+ phaseAge = Number.isNaN(t) ? null : Date.now() - t;
4464
+ break;
4465
+ }
4466
+ }
4467
+ return {
4468
+ completedAt,
4469
+ statusAge,
4470
+ phaseAge,
4471
+ phase: assignment.phase,
4472
+ disposition: assignment.disposition,
4473
+ pinned: assignment.override !== null
4474
+ };
4475
+ }
4476
+ async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
4477
+ const config = await getStatusConfig();
4478
+ if (config.terminalStatuses.has(assignment.status)) return null;
4479
+ try {
4480
+ const { computeFacts: computeFacts2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
4481
+ const { deriveDimensions: deriveDimensions2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
4482
+ const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
4483
+ const facts = await computeFacts2({
4484
+ assignmentDir,
4485
+ frontmatter: {
4486
+ ...assignment
4487
+ // AssignmentRecord ⊃ the fields computeFacts reads; statusHistory and
4488
+ // derived caches ride along untouched.
4489
+ },
4490
+ body: assignment.body,
4491
+ projectDir,
4492
+ terminalStatuses: config.terminalStatuses
4493
+ });
4494
+ const dims = deriveDimensions2({
4495
+ facts,
4496
+ derive: config.derive ?? DEFAULT_DERIVE_CONFIG2,
4497
+ currentStatus: assignment.status,
4498
+ terminalStatuses: config.terminalStatuses,
4499
+ knownStatusIds: new Set(config.statuses.map((s) => s.id)),
4500
+ override: assignment.override
4501
+ });
4502
+ if (!dims) return null;
4503
+ return {
4504
+ derivedStatus: dims.derivedStatus,
4505
+ nextAction: dims.nextAction,
4506
+ facts
4507
+ };
4508
+ } catch (err) {
4509
+ console.warn(`buildDerivedDetail failed for ${assignmentDir}:`, err);
4510
+ return null;
4511
+ }
4512
+ }
2589
4513
  function buildDependencyGraph(assignments) {
2590
4514
  const edges = [];
2591
4515
  const usedStatuses = /* @__PURE__ */ new Set();
@@ -2616,7 +4540,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
2616
4540
  const config = await getStatusConfig();
2617
4541
  const transitionDefs = getTransitionDefinitions(config);
2618
4542
  const actions = [];
2619
- const projectPath = resolve11(projectsDir, projectSlug);
4543
+ const projectPath = resolve12(projectsDir, projectSlug);
2620
4544
  const traces = options?.traces;
2621
4545
  for (const definition of transitionDefs) {
2622
4546
  const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
@@ -2664,12 +4588,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
2664
4588
  continue;
2665
4589
  }
2666
4590
  }
2667
- const dependencyPath = resolve11(projectPath, "assignments", dependency, "assignment.md");
4591
+ const dependencyPath = resolve12(projectPath, "assignments", dependency, "assignment.md");
2668
4592
  if (!await fileExists(dependencyPath)) {
2669
4593
  unmet.push(`${dependency} (missing)`);
2670
4594
  continue;
2671
4595
  }
2672
- const content = await readFile9(dependencyPath, "utf-8");
4596
+ const content = await readFile10(dependencyPath, "utf-8");
2673
4597
  const parsed = parseAssignmentFull(content);
2674
4598
  if (!terminals.has(parsed.status)) {
2675
4599
  unmet.push(`${dependency} (${parsed.status})`);
@@ -2685,7 +4609,7 @@ function parseTimestamp(timestamp) {
2685
4609
  return Number.isFinite(parsed) ? parsed : 0;
2686
4610
  }
2687
4611
  async function countOpenQuestions(projectPath, assignmentSlug) {
2688
- const commentsPath = resolve11(
4612
+ const commentsPath = resolve12(
2689
4613
  projectPath,
2690
4614
  "assignments",
2691
4615
  assignmentSlug,
@@ -2695,7 +4619,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
2695
4619
  return 0;
2696
4620
  }
2697
4621
  try {
2698
- const content = await readFile9(commentsPath, "utf-8");
4622
+ const content = await readFile10(commentsPath, "utf-8");
2699
4623
  const parsed = parseComments(content);
2700
4624
  return parsed.entries.filter(
2701
4625
  (e) => e.type === "question" && e.resolved !== true
@@ -2809,6 +4733,7 @@ var init_api = __esm({
2809
4733
 
2810
4734
  // src/launch/url.ts
2811
4735
  init_terminal_schema();
4736
+ var MAX_OPEN_PROMPT_LENGTH = 2e3;
2812
4737
  var OpenUrlError = class extends Error {
2813
4738
  code;
2814
4739
  constructor(code, message) {
@@ -2889,6 +4814,30 @@ function parseOpenUrl(input) {
2889
4814
  if (agentVals.length === 1 && agentVals[0].trim() !== "") {
2890
4815
  agent = agentVals[0];
2891
4816
  }
4817
+ const promptVals = url.searchParams.getAll("prompt");
4818
+ if (promptVals.length > 1) {
4819
+ throw new OpenUrlError(
4820
+ "duplicate-param",
4821
+ "URL has more than one `prompt` query param"
4822
+ );
4823
+ }
4824
+ let prompt;
4825
+ if (promptVals.length === 1) {
4826
+ const value = promptVals[0];
4827
+ if (/[\r\n]/.test(value)) {
4828
+ throw new OpenUrlError(
4829
+ "invalid-prompt",
4830
+ "`prompt` query param must be a single line (no newlines)"
4831
+ );
4832
+ }
4833
+ if (value.length > MAX_OPEN_PROMPT_LENGTH) {
4834
+ throw new OpenUrlError(
4835
+ "invalid-prompt",
4836
+ `\`prompt\` query param exceeds ${MAX_OPEN_PROMPT_LENGTH} characters`
4837
+ );
4838
+ }
4839
+ prompt = value;
4840
+ }
2892
4841
  if (assignmentVals.length === 1) {
2893
4842
  const id = assignmentVals[0];
2894
4843
  if (id.trim() === "") {
@@ -2901,7 +4850,9 @@ function parseOpenUrl(input) {
2901
4850
  kind: "assignment",
2902
4851
  id,
2903
4852
  ...terminal ? { terminal } : {},
2904
- ...agent ? { agent } : {}
4853
+ ...agent ? { agent } : {},
4854
+ // assignment-only; keep '' (presence-significant) — hence !== undefined.
4855
+ ...prompt !== void 0 ? { prompt } : {}
2905
4856
  };
2906
4857
  }
2907
4858
  if (sessionVals.length === 1) {
@@ -2946,12 +4897,73 @@ init_config2();
2946
4897
  init_assignment_resolver();
2947
4898
  init_api();
2948
4899
 
2949
- // src/tui/launch.ts
2950
- init_api();
2951
- init_agents_schema();
2952
- import { spawn } from "child_process";
2953
- import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
2954
- import { isAbsolute as isAbsolute3, resolve as resolve12 } from "path";
4900
+ // src/launch/launch-prompt.ts
4901
+ init_slug();
4902
+ function bareGrabSeed(params) {
4903
+ if (params.projectSlug) {
4904
+ return `/grab-assignment ${params.projectSlug} ${params.assignmentSlug}`;
4905
+ }
4906
+ if (params.id) {
4907
+ return `/grab-assignment --id ${params.id}`;
4908
+ }
4909
+ return `/grab-assignment ${params.assignmentSlug}`;
4910
+ }
4911
+ function runPlaybookClause(slug) {
4912
+ return `the \`${slug}\` playbook via the /run-playbook skill`;
4913
+ }
4914
+ function assignmentPointer(id, assignmentDir) {
4915
+ const subject = id ? `This session is Syntaur assignment ${id}, with records at ${assignmentDir}.` : `This session's Syntaur assignment records are at ${assignmentDir}.`;
4916
+ return `${subject} Claim and bind it with the /grab-assignment skill if available; otherwise read assignment.md, plan*.md, and progress.md in that directory for full context.`;
4917
+ }
4918
+ var TOKEN_RE = /(^|\s)@([A-Za-z0-9_-]+)/g;
4919
+ function resolveTemplate(template, ctx) {
4920
+ const warnings = [];
4921
+ const prompt = template.replace(TOKEN_RE, (_match, boundary, token) => {
4922
+ if (token === "assignment") {
4923
+ return boundary + assignmentPointer(ctx.id, ctx.assignmentDir);
4924
+ }
4925
+ if (!isValidSlug(token)) {
4926
+ warnings.push(`launchPrompt: "@${token}" is not a valid playbook token \u2014 left as literal text.`);
4927
+ return boundary + "@" + token;
4928
+ }
4929
+ if (ctx.knownPlaybookSlugs !== void 0 && !ctx.knownPlaybookSlugs.has(token)) {
4930
+ warnings.push(`launchPrompt: playbook "${token}" (from "@${token}") is not installed \u2014 left as literal text.`);
4931
+ return boundary + "@" + token;
4932
+ }
4933
+ return boundary + runPlaybookClause(token);
4934
+ });
4935
+ return { prompt, warnings };
4936
+ }
4937
+ function resolveLaunchPrompt(input) {
4938
+ const { template, playbook, id, assignmentDir, projectSlug, assignmentSlug, knownPlaybookSlugs } = input;
4939
+ if (template && template.trim()) {
4940
+ return resolveTemplate(template, { id, assignmentDir, knownPlaybookSlugs });
4941
+ }
4942
+ const pb = playbook?.trim();
4943
+ if (pb) {
4944
+ const pointer = assignmentPointer(id, assignmentDir);
4945
+ return { prompt: `${pointer} Run ${runPlaybookClause(pb)} end-to-end.`, warnings: [] };
4946
+ }
4947
+ return { prompt: bareGrabSeed({ projectSlug, assignmentSlug, id }), warnings: [] };
4948
+ }
4949
+ function effectiveLaunchTemplate(input) {
4950
+ if (input.launchPrompt && input.launchPrompt.trim()) {
4951
+ return input.launchPrompt;
4952
+ }
4953
+ const pb = input.playbook?.trim();
4954
+ if (pb) {
4955
+ return `@assignment Run ${runPlaybookClause(pb)} end-to-end.`;
4956
+ }
4957
+ return bareGrabSeed({
4958
+ projectSlug: input.projectSlug,
4959
+ assignmentSlug: input.assignmentSlug,
4960
+ id: input.id
4961
+ });
4962
+ }
4963
+
4964
+ // src/launch/plan.ts
4965
+ init_paths();
4966
+ init_playbooks();
2955
4967
 
2956
4968
  // src/launch/cwd.ts
2957
4969
  import { existsSync, statSync } from "fs";
@@ -2994,21 +5006,21 @@ function formatFallbackCwdWarning(opts) {
2994
5006
  return `syntaur: ${fields} not set for ${opts.assignmentSlug} \u2014 launching in ${opts.workspaceDir}`;
2995
5007
  }
2996
5008
 
5009
+ // src/launch/plan.ts
5010
+ init_agent_sessions();
5011
+
5012
+ // src/launch/argv.ts
5013
+ init_agents_schema();
5014
+ import { isAbsolute as isAbsolute4 } from "path";
5015
+
2997
5016
  // src/tui/launch.ts
2998
- var INITIAL_PROMPT = (params) => {
2999
- const playbook = params.playbook?.trim();
3000
- if (!playbook) {
3001
- if (params.projectSlug) {
3002
- return `/grab-assignment ${params.projectSlug} ${params.assignmentSlug}`;
3003
- }
3004
- if (params.id) {
3005
- return `/grab-assignment --id ${params.id}`;
3006
- }
3007
- return `/grab-assignment ${params.assignmentSlug}`;
3008
- }
3009
- const grabClause = params.projectSlug ? `the assignment \`${params.projectSlug}/${params.assignmentSlug}\` using the /grab-assignment skill` : params.id ? `the assignment id \`${params.id}\` using /grab-assignment --id ${params.id}` : `the assignment \`${params.assignmentSlug}\` using the /grab-assignment skill`;
3010
- return `Grab ${grabClause}, then load and run the \`${playbook}\` playbook using the /run-playbook skill and carry it out end-to-end.`;
3011
- };
5017
+ init_api();
5018
+ init_agents_schema();
5019
+ import { spawn } from "child_process";
5020
+ import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
5021
+ import { isAbsolute as isAbsolute3, resolve as resolve13 } from "path";
5022
+ init_paths();
5023
+ init_playbooks();
3012
5024
  function shellQuote(arg) {
3013
5025
  if (arg === "") return "''";
3014
5026
  return `'${arg.replace(/'/g, `'\\''`)}'`;
@@ -3037,12 +5049,7 @@ function buildAgentArgv(agent, prompt, env = process.env) {
3037
5049
  };
3038
5050
  }
3039
5051
 
3040
- // src/launch/plan.ts
3041
- init_agent_sessions();
3042
-
3043
5052
  // src/launch/argv.ts
3044
- init_agents_schema();
3045
- import { isAbsolute as isAbsolute4 } from "path";
3046
5053
  var buildFreshArgv = buildAgentArgv;
3047
5054
  function buildSessionArgv(agent, sessionId, mode, env = process.env) {
3048
5055
  const invocation = agent[mode];
@@ -3150,15 +5157,18 @@ async function resolveAssignmentPlan(input, terminal) {
3150
5157
  } else {
3151
5158
  agent = pickAgent(input.config);
3152
5159
  }
3153
- const { argv, shellFallbackWarning } = buildFreshArgv(
3154
- agent,
3155
- INITIAL_PROMPT({
3156
- projectSlug: resolved.projectSlug,
3157
- assignmentSlug: resolved.assignmentSlug,
3158
- id: resolved.id,
3159
- playbook: agent.playbook
3160
- })
3161
- );
5160
+ const knownPlaybookSlugs = await listPlaybookSlugs(playbooksDir());
5161
+ const template = input.promptOverride !== void 0 ? input.promptOverride : agent.launchPrompt;
5162
+ const { prompt, warnings: promptWarnings } = resolveLaunchPrompt({
5163
+ template,
5164
+ playbook: agent.playbook,
5165
+ id: resolved.id,
5166
+ assignmentDir: resolved.assignmentDir,
5167
+ projectSlug: resolved.projectSlug,
5168
+ assignmentSlug: resolved.assignmentSlug,
5169
+ knownPlaybookSlugs
5170
+ });
5171
+ const { argv, shellFallbackWarning } = buildFreshArgv(agent, prompt);
3162
5172
  return {
3163
5173
  terminal,
3164
5174
  cwd,
@@ -3166,7 +5176,8 @@ async function resolveAssignmentPlan(input, terminal) {
3166
5176
  env: process.env,
3167
5177
  agentId: agent.id,
3168
5178
  fallbackWarning,
3169
- shellFallbackWarning
5179
+ shellFallbackWarning,
5180
+ promptWarnings
3170
5181
  };
3171
5182
  }
3172
5183
  async function resolveSessionPlan(input, terminal) {
@@ -3327,7 +5338,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
3327
5338
  `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
3328
5339
  );
3329
5340
  }
3330
- await new Promise((resolve14, reject) => {
5341
+ await new Promise((resolve15, reject) => {
3331
5342
  let settled = false;
3332
5343
  let stderr = "";
3333
5344
  const finishOk = () => {
@@ -3337,7 +5348,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
3337
5348
  child.unref();
3338
5349
  } catch {
3339
5350
  }
3340
- resolve14();
5351
+ resolve15();
3341
5352
  };
3342
5353
  const finishErr = (remediation) => {
3343
5354
  if (settled) return;
@@ -3522,7 +5533,7 @@ function appleScriptString(value) {
3522
5533
  init_paths();
3523
5534
  init_fs();
3524
5535
  import { fileURLToPath } from "url";
3525
- import { dirname as dirname3, resolve as resolve13, join as join3 } from "path";
5536
+ import { dirname as dirname3, resolve as resolve14, join as join3 } from "path";
3526
5537
  import { realpathSync, readFileSync, mkdirSync } from "fs";
3527
5538
  var NPX_PATTERNS = [
3528
5539
  { kind: "npm", re: /\/_npx\/([^/]+)\/node_modules(?:\/|$)/ },
@@ -3548,7 +5559,7 @@ function normalizeSlashes(p) {
3548
5559
  }
3549
5560
  function detectInstallKind(scriptUrl, opts = {}) {
3550
5561
  const realpath = opts.realpath ?? realpathSync.native;
3551
- const readFile10 = opts.readFile ?? ((p) => readFileSync(p, "utf-8"));
5562
+ const readFile11 = opts.readFile ?? ((p) => readFileSync(p, "utf-8"));
3552
5563
  const ua = opts.envUserAgent !== void 0 ? opts.envUserAgent : process.env.npm_config_user_agent ?? "";
3553
5564
  const resolved = resolveScriptPath(scriptUrl, realpath);
3554
5565
  if (resolved === null) {
@@ -3569,7 +5580,7 @@ function detectInstallKind(scriptUrl, opts = {}) {
3569
5580
  const pkgJsonPath = join3(dir, "package.json");
3570
5581
  let raw;
3571
5582
  try {
3572
- raw = readFile10(pkgJsonPath);
5583
+ raw = readFile11(pkgJsonPath);
3573
5584
  } catch {
3574
5585
  const parent2 = dirname3(dir);
3575
5586
  if (parent2 === dir) break;
@@ -3601,7 +5612,7 @@ function extractNpxHash(scriptUrl, opts = {}) {
3601
5612
  return null;
3602
5613
  }
3603
5614
  function nudgeStampDir() {
3604
- return resolve13(syntaurRoot(), "npx-handler-nudge");
5615
+ return resolve14(syntaurRoot(), "npx-handler-nudge");
3605
5616
  }
3606
5617
  function sanitizeHash(hash) {
3607
5618
  return hash.replace(/[^A-Za-z0-9_-]/g, "_") || "_";
@@ -3653,11 +5664,13 @@ export {
3653
5664
  LaunchError,
3654
5665
  OpenUrlError,
3655
5666
  TerminalNotFoundError,
5667
+ bareGrabSeed,
3656
5668
  buildFreshArgv,
3657
5669
  buildSessionArgv,
3658
5670
  buildShellCommandLine,
3659
5671
  buildTerminalInvocation,
3660
5672
  detectInstallKind,
5673
+ effectiveLaunchTemplate,
3661
5674
  executeLaunchPlan,
3662
5675
  extractNpxHash,
3663
5676
  hasNudgedHash,
@@ -3670,6 +5683,8 @@ export {
3670
5683
  pickAgent,
3671
5684
  recordNudge,
3672
5685
  resolveLaunchPlan,
5686
+ resolveLaunchPrompt,
5687
+ runPlaybookClause,
3673
5688
  shouldNudgeForNpx
3674
5689
  };
3675
5690
  //# sourceMappingURL=index.js.map