@runfusion/fusion 0.54.0 → 0.55.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 (135) hide show
  1. package/dist/bin.js +3384 -768
  2. package/dist/client/.vite/manifest.json +283 -251
  3. package/dist/client/assets/AgentDetailView-B_VfFSlt.js +18 -0
  4. package/dist/client/assets/{AgentDetailView-BzSyldiW.css → AgentDetailView-D7UY2Tup.css} +1 -1
  5. package/dist/client/assets/{AgentPermissionPolicyEditor-D2AQ6swg.js → AgentPermissionPolicyEditor-QZhz41AU.js} +1 -1
  6. package/dist/client/assets/{AgentsView-DwEFPKuN.js → AgentsView-hkhJ8SUq.js} +6 -6
  7. package/dist/client/assets/ChatView-B4WnXLYA.js +6 -0
  8. package/dist/client/assets/{CommandCenter-BABqoSkg.js → CommandCenter-B3fAajUO.js} +16 -16
  9. package/dist/client/assets/{DevServerView-DlgOzRdC.js → DevServerView-BR0ngFBw.js} +1 -1
  10. package/dist/client/assets/{DirectoryPicker-BwMSmxYS.js → DirectoryPicker-BiS41Hca.js} +1 -1
  11. package/dist/client/assets/DocumentsView-BnRvIUpa.js +1 -0
  12. package/dist/client/assets/{EvalsView-CQBNrO5n.js → EvalsView-Bvx18QyC.js} +1 -1
  13. package/dist/client/assets/{ExperimentalAgentOnboardingModal-2SDqP2lc.js → ExperimentalAgentOnboardingModal-CKGJcfXI.js} +1 -1
  14. package/dist/client/assets/{GoalsView-BgkX-_SB.js → GoalsView-Bj1yVIiv.js} +1 -1
  15. package/dist/client/assets/{InsightsView-CJY4eZcO.js → InsightsView-DBX8DI60.js} +2 -2
  16. package/dist/client/assets/{MemoryView-5Crtfanz.js → MemoryView-CqkRH2fa.js} +1 -1
  17. package/dist/client/assets/{PiExtensionsManager-COmzskVa.js → PiExtensionsManager-BMMmGWe2.js} +2 -2
  18. package/dist/client/assets/PluginManager-C1bdfN2W.js +1 -0
  19. package/dist/client/assets/{PullRequestView-DAHh0FVM.js → PullRequestView-D0CsXWLl.js} +2 -2
  20. package/dist/client/assets/{ResearchView-cSUka3sd.js → ResearchView-CVgmXAlj.js} +1 -1
  21. package/dist/client/assets/{SecretsView-ChFAfFRY.js → SecretsView-Bv_gw5Ix.js} +1 -1
  22. package/dist/client/assets/{SessionTerminal-D4R3ScvQ.css → SessionTerminal-BvK8A98c.css} +1 -1
  23. package/dist/client/assets/{SessionTerminal-CtQKDaqI.js → SessionTerminal-VLdVV3Ji.js} +1 -1
  24. package/dist/client/assets/{SettingsModal-BzZNPNIh.css → SettingsModal-4t8rV7rh.css} +1 -1
  25. package/dist/client/assets/{SettingsModal-pAz_hOoN.js → SettingsModal-D7YgojbV.js} +1 -1
  26. package/dist/client/assets/SettingsModal-gcrh5EOG.js +21 -0
  27. package/dist/client/assets/SetupWizardModal-DvmTlFP_.js +2 -0
  28. package/dist/client/assets/{ShadcnColorPicker-CsfgWpf6.js → ShadcnColorPicker-DM2fqGwl.js} +1 -1
  29. package/dist/client/assets/{SkillsView-BSUplEYb.js → SkillsView-Bb-JTo99.js} +1 -1
  30. package/dist/client/assets/{TodoView-DhBseLvM.js → TodoView-bV-7deOh.js} +1 -1
  31. package/dist/client/assets/{WorkflowNodeEditor-Bi12CNif.js → WorkflowNodeEditor-Ui1q_fFI.js} +3 -3
  32. package/dist/client/assets/{agentSkills-qVb0x_G3.js → agentSkills-DGFyuKuP.js} +1 -1
  33. package/dist/client/assets/{app-DczBmsP4.js → app--OM-ul6R.js} +3 -3
  34. package/dist/client/assets/{app-BhtYYxin.js → app-BCvqf8qq.js} +3 -3
  35. package/dist/client/assets/app-BZqqEiWR.js +13 -0
  36. package/dist/client/assets/{app-5S4z84u5.js → app-DQeH1UDD.js} +3 -3
  37. package/dist/client/assets/{app-Cts0__Vx.js → app-FiYwRatp.js} +3 -3
  38. package/dist/client/assets/{app-_GFUqcMK.js → app-rGdYcsAA.js} +3 -3
  39. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DEt4eIEQ.js → architectureDiagram-3BPJPVTR-DP7vOyXY.js} +1 -1
  40. package/dist/client/assets/{blockDiagram-GPEHLZMM-CJuX0UM_.js → blockDiagram-GPEHLZMM-Di9v1pV8.js} +1 -1
  41. package/dist/client/assets/{c4Diagram-AAUBKEIU-a46Lm113.js → c4Diagram-AAUBKEIU-Dvb8RSfr.js} +1 -1
  42. package/dist/client/assets/channel-C5g9M3Sk.js +1 -0
  43. package/dist/client/assets/{chunk-2J33WTMH-iBhwyS0c.js → chunk-2J33WTMH-gLcAuGjI.js} +1 -1
  44. package/dist/client/assets/{chunk-4BX2VUAB-C7lS5wER.js → chunk-4BX2VUAB-B-B0wjkx.js} +1 -1
  45. package/dist/client/assets/{chunk-55IACEB6-CQ6YOncn.js → chunk-55IACEB6-XUDeC0Co.js} +1 -1
  46. package/dist/client/assets/{chunk-727SXJPM-CveMaMKK.js → chunk-727SXJPM-1eebiGA2.js} +1 -1
  47. package/dist/client/assets/{chunk-AQP2D5EJ-DErjGUEk.js → chunk-AQP2D5EJ-D9VOB5QS.js} +1 -1
  48. package/dist/client/assets/{chunk-FMBD7UC4-DXUvEFfJ.js → chunk-FMBD7UC4-EFNFEaAz.js} +1 -1
  49. package/dist/client/assets/{chunk-ND2GUHAM-w5RqSj1J.js → chunk-ND2GUHAM-CbeQailD.js} +1 -1
  50. package/dist/client/assets/{chunk-QZHKN3VN-CAi0lJ7V.js → chunk-QZHKN3VN-D55N6EBz.js} +1 -1
  51. package/dist/client/assets/classDiagram-4FO5ZUOK-B-wdlvD6.js +1 -0
  52. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B-wdlvD6.js +1 -0
  53. package/dist/client/assets/{cose-bilkent-S5V4N54A-Ck2h4uV5.js → cose-bilkent-S5V4N54A-B0zbhkX3.js} +1 -1
  54. package/dist/client/assets/createLucideIcon-CshVyKO4.js +21 -0
  55. package/dist/client/assets/{dagre-BM42HDAG-BCRTXNj6.js → dagre-BM42HDAG-BL6peXwl.js} +1 -1
  56. package/dist/client/assets/dashboard-view-Bsdq3OM4.css +1 -0
  57. package/dist/client/assets/dashboard-view-CS1H5SQN.js +1 -0
  58. package/dist/client/assets/dashboard-view-DafOfnxI.js +1 -0
  59. package/dist/client/assets/{dashboard-view-D9rF_PCq.js → dashboard-view-Db3n_W85.js} +1320 -1335
  60. package/dist/client/assets/{diagram-2AECGRRQ-DTTk-DYO.js → diagram-2AECGRRQ-cYKwSHk7.js} +1 -1
  61. package/dist/client/assets/{diagram-5GNKFQAL-jDc9kMi2.js → diagram-5GNKFQAL-DHIwTGuM.js} +1 -1
  62. package/dist/client/assets/{diagram-KO2AKTUF-CcLmzlGr.js → diagram-KO2AKTUF-DfHBJ4aX.js} +1 -1
  63. package/dist/client/assets/{diagram-LMA3HP47-DCW8XKQj.js → diagram-LMA3HP47-pWhYmB6Z.js} +1 -1
  64. package/dist/client/assets/{diagram-OG6HWLK6-wbbKquPs.js → diagram-OG6HWLK6-GzWqbi_m.js} +1 -1
  65. package/dist/client/assets/{erDiagram-TEJ5UH35-Fo933Mo7.js → erDiagram-TEJ5UH35-CCVm5eiR.js} +1 -1
  66. package/dist/client/assets/{flowDiagram-I6XJVG4X-Dfum7HZP.js → flowDiagram-I6XJVG4X-CUTdnU6S.js} +1 -1
  67. package/dist/client/assets/{folder-open-B8vOwFeP.js → folder-open-DOCy9GVC.js} +1 -1
  68. package/dist/client/assets/{ganttDiagram-6RSMTGT7-BzEBCn01.js → ganttDiagram-6RSMTGT7-CW4ENJaC.js} +1 -1
  69. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CgovghHs.js → gitGraphDiagram-PVQCEYII-QXXMfSzp.js} +1 -1
  70. package/dist/client/assets/index-B6ttSchA.js +1336 -0
  71. package/dist/client/assets/index-CPE2Kwra.css +1 -0
  72. package/dist/client/assets/{infoDiagram-5YYISTIA-BzpU4Wys.js → infoDiagram-5YYISTIA-CZyHbj0n.js} +1 -1
  73. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-cpp5pozu.js → ishikawaDiagram-YF4QCWOH-Dea84lqZ.js} +1 -1
  74. package/dist/client/assets/{journeyDiagram-JHISSGLW-BVjcw6oo.js → journeyDiagram-JHISSGLW-Db2iKGU5.js} +1 -1
  75. package/dist/client/assets/{kanban-definition-UN3LZRKU-4unAhBzo.js → kanban-definition-UN3LZRKU-CB9loIqR.js} +1 -1
  76. package/dist/client/assets/loader-circle-CMNcxXXr.js +16 -0
  77. package/dist/client/assets/{mermaid.core-DzIyUj9t.js → mermaid.core-CoQ8aCb0.js} +4 -4
  78. package/dist/client/assets/{mindmap-definition-RKZ34NQL-Be_yfVir.js → mindmap-definition-RKZ34NQL-JPb2wF-K.js} +1 -1
  79. package/dist/client/assets/{pieDiagram-4H26LBE5-Cfnw2aji.js → pieDiagram-4H26LBE5-pdZzOreP.js} +1 -1
  80. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BFlC-_Uo.js → quadrantDiagram-W4KKPZXB-Dh9Ids90.js} +1 -1
  81. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DpVPzCOx.js → requirementDiagram-4Y6WPE33-BVPOSvHU.js} +1 -1
  82. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DM-gKKP0.js → sankeyDiagram-5OEKKPKP-BqSMUpsx.js} +1 -1
  83. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DjhZCZhd.js → sequenceDiagram-3UESZ5HK-B8nepQ6X.js} +1 -1
  84. package/dist/client/assets/{shield-alert-DCfkjjYc.js → shield-alert-CtSaIQNY.js} +1 -1
  85. package/dist/client/assets/{stateDiagram-AJRCARHV-BCA96rdI.js → stateDiagram-AJRCARHV-CQv9RdRp.js} +1 -1
  86. package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-DHds08RS.js → stateDiagram-v2-BHNVJYJU-BZ_mlvie.js} +1 -1
  87. package/dist/client/assets/{timeline-definition-PNZ67QCA-CHW2OQRw.js → timeline-definition-PNZ67QCA-CAjwM-Z5.js} +1 -1
  88. package/dist/client/assets/{upload-EJc5PJPz.js → upload-0_pT1euA.js} +1 -1
  89. package/dist/client/assets/{users-C-e-99Vp.js → users--1B6mnm8.js} +1 -1
  90. package/dist/client/assets/{vennDiagram-CIIHVFJN-shGh4eBY.js → vennDiagram-CIIHVFJN-BO3Um3rH.js} +1 -1
  91. package/dist/client/assets/{wardley-L42UT6IY-l0RGrVhn.js → wardley-L42UT6IY-BzqJcqDA.js} +1 -1
  92. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-C-USmO8A.js → wardleyDiagram-YWT4CUSO-Bctxc6_5.js} +1 -1
  93. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CHX9Umta.js → xychartDiagram-2RQKCTM6-CgDuiMFR.js} +1 -1
  94. package/dist/client/assets/zoom-out-DzDzWQt1.js +21 -0
  95. package/dist/client/index.html +13 -4
  96. package/dist/client/version.json +1 -1
  97. package/dist/droid-cli/package.json +1 -1
  98. package/dist/extension.js +2066 -271
  99. package/dist/pi-claude-cli/package.json +1 -1
  100. package/dist/plugin-sdk/index.d.ts +61 -1
  101. package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
  102. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  103. package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
  104. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +70 -25
  105. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  106. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  107. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  108. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  109. package/dist/plugins/fusion-plugin-linear-import/bundled.js +658 -0
  110. package/dist/plugins/fusion-plugin-linear-import/manifest.json +48 -0
  111. package/dist/plugins/fusion-plugin-linear-import/package.json +11 -0
  112. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  113. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  114. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  115. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  116. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  117. package/package.json +2 -1
  118. package/skill/fusion/SKILL.md +2 -2
  119. package/skill/fusion/references/engine-tools.md +1 -0
  120. package/skill/fusion/references/extension-tools.md +81 -0
  121. package/skill/fusion/references/fusion-capabilities.md +7 -0
  122. package/dist/client/assets/AgentDetailView-BJditQat.js +0 -18
  123. package/dist/client/assets/ChatView-B4bjPXFN.js +0 -6
  124. package/dist/client/assets/DocumentsView-BfaRXFg5.js +0 -1
  125. package/dist/client/assets/PluginManager-CGWXqZMa.js +0 -1
  126. package/dist/client/assets/SettingsModal-BYaotQ2o.js +0 -21
  127. package/dist/client/assets/SetupWizardModal-eHZHi8VP.js +0 -2
  128. package/dist/client/assets/app-B1VYocx2.js +0 -13
  129. package/dist/client/assets/channel-BGI9APMr.js +0 -1
  130. package/dist/client/assets/classDiagram-4FO5ZUOK-g6hCcufy.js +0 -1
  131. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-g6hCcufy.js +0 -1
  132. package/dist/client/assets/dashboard-view-BfLJ60Kd.js +0 -1
  133. package/dist/client/assets/index-BjELA24x.js +0 -1336
  134. package/dist/client/assets/index-DwGW7OJh.css +0 -1
  135. package/dist/client/assets/zoom-out-NWbVkreR.js +0 -41
package/dist/extension.js CHANGED
@@ -564,6 +564,66 @@ var init_mcp_config = __esm({
564
564
  }
565
565
  });
566
566
 
567
+ // ../core/src/gitlab-config.ts
568
+ function resolveGitlabEnabled(input = {}) {
569
+ if (typeof input.project?.gitlabEnabled === "boolean") return input.project.gitlabEnabled;
570
+ if (typeof input.global?.gitlabEnabled === "boolean") return input.global.gitlabEnabled;
571
+ return true;
572
+ }
573
+ function readConfiguredString(value) {
574
+ if (typeof value !== "string") return void 0;
575
+ const trimmed = value.trim();
576
+ return trimmed.length > 0 ? trimmed : void 0;
577
+ }
578
+ function normalizeHttpUrl(value, label) {
579
+ let parsed;
580
+ try {
581
+ parsed = new URL(value);
582
+ } catch {
583
+ throw new Error(`${label} must be a valid absolute http(s) URL`);
584
+ }
585
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
586
+ throw new Error(`${label} must use http:// or https://`);
587
+ }
588
+ if (parsed.username || parsed.password) {
589
+ throw new Error(`${label} must not include username or password userinfo`);
590
+ }
591
+ if (!parsed.hostname) {
592
+ throw new Error(`${label} must include a hostname`);
593
+ }
594
+ parsed.hash = "";
595
+ parsed.search = "";
596
+ parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
597
+ const normalized = parsed.toString().replace(/\/$/u, "");
598
+ return normalized;
599
+ }
600
+ function deriveApiBaseUrl(instanceUrl) {
601
+ const parsed = new URL(instanceUrl);
602
+ const basePath = parsed.pathname.replace(/\/+$/u, "");
603
+ parsed.pathname = `${basePath}/api/v4`.replace(/\/+/gu, "/");
604
+ parsed.search = "";
605
+ parsed.hash = "";
606
+ return parsed.toString().replace(/\/$/u, "");
607
+ }
608
+ function resolveGitlabConfig(input = {}) {
609
+ const enabled = resolveGitlabEnabled(input);
610
+ const projectInstanceUrl = readConfiguredString(input.project?.gitlabInstanceUrl);
611
+ const globalInstanceUrl = readConfiguredString(input.global?.gitlabInstanceUrl);
612
+ const projectApiBaseUrl = readConfiguredString(input.project?.gitlabApiBaseUrl);
613
+ const globalApiBaseUrl = readConfiguredString(input.global?.gitlabApiBaseUrl);
614
+ const instanceUrl = normalizeHttpUrl(projectInstanceUrl ?? globalInstanceUrl ?? DEFAULT_GITLAB_INSTANCE_URL, "GitLab instance URL");
615
+ const apiBaseUrl = projectApiBaseUrl ?? globalApiBaseUrl ? normalizeHttpUrl(projectApiBaseUrl ?? globalApiBaseUrl ?? "", "GitLab API base URL") : instanceUrl === DEFAULT_GITLAB_INSTANCE_URL ? DEFAULT_GITLAB_API_BASE_URL : deriveApiBaseUrl(instanceUrl);
616
+ return { enabled, instanceUrl, apiBaseUrl };
617
+ }
618
+ var DEFAULT_GITLAB_INSTANCE_URL, DEFAULT_GITLAB_API_BASE_URL;
619
+ var init_gitlab_config = __esm({
620
+ "../core/src/gitlab-config.ts"() {
621
+ "use strict";
622
+ DEFAULT_GITLAB_INSTANCE_URL = "https://gitlab.com";
623
+ DEFAULT_GITLAB_API_BASE_URL = "https://gitlab.com/api/v4";
624
+ }
625
+ });
626
+
567
627
  // ../core/src/task-helpers.ts
568
628
  function getPrimaryPrInfo(task) {
569
629
  return task.prInfos?.[0] ?? task.prInfo;
@@ -1017,7 +1077,11 @@ var init_settings_schema = __esm({
1017
1077
  "use strict";
1018
1078
  init_in_review_stall();
1019
1079
  DEFAULT_GLOBAL_SETTINGS = {
1020
- themeMode: "dark",
1080
+ /*
1081
+ FNXC:DashboardTheming 2026-07-03-00:00:
1082
+ Fresh installs must follow the operating system theme until the user explicitly chooses Light, Dark, or System. Keep this global default aligned with dashboard and desktop pre-hydration fallbacks.
1083
+ */
1084
+ themeMode: "system",
1021
1085
  /*
1022
1086
  FNXC:DashboardTheming 2026-06-30-00:00:
1023
1087
  New users and unset installs should start on Shadcn Ember. Existing users who explicitly stored colorTheme "default", "ocean", or another valid theme must remain on that selection, so the ids stay valid and only the absence/default seed changes to "shadcn-ember".
@@ -1090,10 +1154,21 @@ var init_settings_schema = __esm({
1090
1154
  updateCheckFrequency: "daily",
1091
1155
  autoReloadOnVersionChange: true,
1092
1156
  githubTrackingDefaultRepo: void 0,
1157
+ gitlabEnabled: void 0,
1158
+ gitlabInstanceUrl: void 0,
1159
+ gitlabApiBaseUrl: void 0,
1160
+ gitlabAuthToken: void 0,
1161
+ gitlabAuthTokenType: void 0,
1093
1162
  modelOnboardingComplete: void 0,
1094
1163
  useClaudeCli: void 0,
1095
1164
  useDroidCli: void 0,
1096
1165
  useLlamaCpp: void 0,
1166
+ useCursorCli: void 0,
1167
+ /*
1168
+ FNXC:CursorCli 2026-07-02-00:00:
1169
+ Cursor CLI binary overrides are global operator settings because executable locations are machine-local. Blank/undefined preserves PATH auto-detection through cursor-agent and cursor.
1170
+ */
1171
+ cursorCliBinaryPath: void 0,
1097
1172
  // Global baseline lanes for per-role model selection
1098
1173
  executionGlobalProvider: void 0,
1099
1174
  executionGlobalModelId: void 0,
@@ -1443,6 +1518,14 @@ var init_settings_schema = __esm({
1443
1518
  githubTrackingEnabledByDefault: false,
1444
1519
  githubLinkImportedIssuesToTracking: false,
1445
1520
  githubTrackingDefaultRepo: void 0,
1521
+ gitlabEnabled: void 0,
1522
+ gitlabInstanceUrl: void 0,
1523
+ gitlabApiBaseUrl: void 0,
1524
+ gitlabAuthToken: void 0,
1525
+ gitlabAuthTokenType: void 0,
1526
+ gitlabCommentOnDone: false,
1527
+ gitlabCommentTemplate: void 0,
1528
+ gitlabCloseSourceIssueOnDone: false,
1446
1529
  githubTrackingDedupEnabled: true,
1447
1530
  githubAuthMode: "gh-cli",
1448
1531
  githubAuthToken: void 0,
@@ -2085,6 +2168,9 @@ function categorizeToolName(toolName) {
2085
2168
  if (name.startsWith("fn_task_import_github") || name.startsWith("fn_task_browse_github")) {
2086
2169
  return "github";
2087
2170
  }
2171
+ if (name.startsWith("fn_task_import_gitlab") || name.startsWith("fn_task_browse_gitlab")) {
2172
+ return "gitlab";
2173
+ }
2088
2174
  if (name === "fn_web_fetch") return "network";
2089
2175
  if (name.startsWith("fn_secret_")) return "secrets";
2090
2176
  if (name.startsWith("fn_skills_")) return "skills";
@@ -2989,12 +3075,13 @@ function validateMessageMetadata(metadata) {
2989
3075
  throw new Error("metadata.wakeRecipient must be a boolean");
2990
3076
  }
2991
3077
  }
2992
- var THINKING_LEVELS, COLUMNS, DEFAULT_COLUMN, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, warnedLegacyCwdMain, MERGE_ADVANCE_AUTO_SYNC_MODES, MERGE_STRATEGY_OVERLAP_BEHAVIORS, POST_MERGE_AUDIT_MODES, MERGE_AUDIT_AUTO_RECOVERY_MODES, MERGER_MODES, AUTO_RECOVERY_MODES, NOTIFICATION_EVENTS, DOCUMENT_KEY_RE, REPO_OVERRIDE_RE, CheckoutConflictError, DUPLICATE_OF_METADATA_KEY, WorkspaceTaskMergeError, SHARED_STATE_SNAPSHOT_VERSION, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, AGENT_PROVISIONING_APPROVAL_MODES, SECRET_ACCESS_POLICIES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, DEFAULT_HEARTBEAT_PROCEDURE_PATH, DASHBOARD_USER_ID, DASHBOARD_USER_ALIASES;
3078
+ var THINKING_LEVELS, COLUMNS, DEFAULT_COLUMN, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, warnedLegacyCwdMain, MERGE_ADVANCE_AUTO_SYNC_MODES, MERGE_STRATEGY_OVERLAP_BEHAVIORS, POST_MERGE_AUDIT_MODES, MERGE_AUDIT_AUTO_RECOVERY_MODES, MERGER_MODES, AUTO_RECOVERY_MODES, NOTIFICATION_EVENTS, DOCUMENT_KEY_RE, REPO_OVERRIDE_RE, CheckoutConflictError, DUPLICATE_OF_METADATA_KEY, WorkspaceTaskMergeError, SHARED_STATE_SNAPSHOT_VERSION, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, PLANNING_DEEPEN_CHECKPOINT_QUESTION, PLANNING_DEEPEN_CHECKPOINT_ID, PLANNING_DEEPEN_PROCEED_OPTION_ID, PLANNING_DEEPEN_PROCEED_RESPONSE_KEY, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, AGENT_PROVISIONING_APPROVAL_MODES, SECRET_ACCESS_POLICIES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, DEFAULT_HEARTBEAT_PROCEDURE_PATH, DASHBOARD_USER_ID, DASHBOARD_USER_ALIASES;
2993
3079
  var init_types = __esm({
2994
3080
  "../core/src/types.ts"() {
2995
3081
  "use strict";
2996
3082
  init_capacity();
2997
3083
  init_mcp_config();
3084
+ init_gitlab_config();
2998
3085
  init_settings_validation();
2999
3086
  init_settings_schema();
3000
3087
  init_prompt_overrides();
@@ -3200,6 +3287,10 @@ var init_types = __esm({
3200
3287
  done: ["todo", "triage", "archived"],
3201
3288
  archived: ["done"]
3202
3289
  };
3290
+ PLANNING_DEEPEN_CHECKPOINT_QUESTION = "Would you like to go deeper?";
3291
+ PLANNING_DEEPEN_CHECKPOINT_ID = "__planning_deepen_checkpoint__";
3292
+ PLANNING_DEEPEN_PROCEED_OPTION_ID = "__planning_deepen_proceed_to_final__";
3293
+ PLANNING_DEEPEN_PROCEED_RESPONSE_KEY = "__planning_deepen_proceed__";
3203
3294
  AGENT_VALID_TRANSITIONS = {
3204
3295
  idle: ["active"],
3205
3296
  active: ["idle", "running", "paused", "error"],
@@ -3272,6 +3363,12 @@ var init_types = __esm({
3272
3363
  "fn_delegate_task",
3273
3364
  "fn_task_import_github",
3274
3365
  "fn_task_import_github_issue",
3366
+ "fn_task_import_gitlab_project_issues",
3367
+ "fn_task_import_gitlab_group_issues",
3368
+ "fn_task_import_gitlab_merge_requests",
3369
+ "fn_task_browse_gitlab_project_issues",
3370
+ "fn_task_browse_gitlab_group_issues",
3371
+ "fn_task_browse_gitlab_merge_requests",
3275
3372
  "fn_spawn_agent",
3276
3373
  "fn_update_agent_config",
3277
3374
  "fn_task_update",
@@ -3586,6 +3683,20 @@ var init_near_duplicate_canonical = __esm({
3586
3683
  }
3587
3684
  });
3588
3685
 
3686
+ // ../core/src/gitlab-tracking.ts
3687
+ function formatGitLabTrackedItemRef(item) {
3688
+ const marker = item.kind === "merge_request" ? "!" : "#";
3689
+ return `${item.host} ${item.kind} ${marker}${item.iid}`;
3690
+ }
3691
+ function isGitLabTrackingStale(item) {
3692
+ return Boolean(item?.staleAt || item?.staleReason);
3693
+ }
3694
+ var init_gitlab_tracking = __esm({
3695
+ "../core/src/gitlab-tracking.ts"() {
3696
+ "use strict";
3697
+ }
3698
+ });
3699
+
3589
3700
  // ../core/src/frontend-ux-policy.ts
3590
3701
  function applyFrontendUxCriteria(promptMarkdown, fileScopePaths) {
3591
3702
  if (promptMarkdown.includes(FRONTEND_UX_HEADING)) {
@@ -13032,7 +13143,7 @@ var init_task_title_id_drift = __esm({
13032
13143
  });
13033
13144
 
13034
13145
  // ../core/src/db.ts
13035
- import { basename, isAbsolute, join as join2 } from "node:path";
13146
+ import { basename, dirname, isAbsolute, join as join2 } from "node:path";
13036
13147
  import { mkdirSync, existsSync, statSync, renameSync, rmSync } from "node:fs";
13037
13148
  import { spawn, spawnSync } from "node:child_process";
13038
13149
  import { createHash as createHash2, randomUUID } from "node:crypto";
@@ -13302,7 +13413,7 @@ var init_db = __esm({
13302
13413
  DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5e3;
13303
13414
  DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS = 1e3;
13304
13415
  DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS = 50;
13305
- SCHEMA_VERSION = 134;
13416
+ SCHEMA_VERSION = 136;
13306
13417
  TASKS_FTS_AUTOMERGE = 8;
13307
13418
  TASKS_FTS_CRISISMERGE = 16;
13308
13419
  TASKS_FTS_MERGE_PAGES = 16;
@@ -13396,6 +13507,8 @@ CREATE TABLE IF NOT EXISTS tasks (
13396
13507
  prInfos TEXT,
13397
13508
  issueInfo TEXT,
13398
13509
  githubTracking TEXT,
13510
+ -- FNXC:GitLabTracking 2026-07-02-00:00: GitLab item links are a nullable JSON column so project/group issue and merge-request metadata from GitLab.com or self-managed instances round-trip without altering GitHub tracking/source-issue columns.
13511
+ gitlabTracking TEXT,
13399
13512
  sourceIssueProvider TEXT,
13400
13513
  sourceIssueRepository TEXT,
13401
13514
  sourceIssueExternalIssueId TEXT,
@@ -14520,6 +14633,23 @@ CREATE INDEX IF NOT EXISTS idxIncidentsResolvedAt ON incidents(resolvedAt);
14520
14633
  createdAt: "TEXT NOT NULL",
14521
14634
  attachments: "TEXT"
14522
14635
  },
14636
+ chat_token_usage: {
14637
+ id: "TEXT PRIMARY KEY",
14638
+ sourceKind: "TEXT NOT NULL",
14639
+ chatSessionId: "TEXT",
14640
+ roomId: "TEXT",
14641
+ messageId: "TEXT",
14642
+ projectId: "TEXT",
14643
+ agentId: "TEXT",
14644
+ modelProvider: "TEXT",
14645
+ modelId: "TEXT",
14646
+ inputTokens: "INTEGER NOT NULL DEFAULT 0",
14647
+ outputTokens: "INTEGER NOT NULL DEFAULT 0",
14648
+ cachedTokens: "INTEGER NOT NULL DEFAULT 0",
14649
+ cacheWriteTokens: "INTEGER NOT NULL DEFAULT 0",
14650
+ totalTokens: "INTEGER NOT NULL DEFAULT 0",
14651
+ createdAt: "TEXT NOT NULL"
14652
+ },
14523
14653
  runAuditEvents: {
14524
14654
  id: "TEXT PRIMARY KEY",
14525
14655
  timestamp: "TEXT NOT NULL",
@@ -14682,6 +14812,7 @@ CREATE INDEX IF NOT EXISTS idxIncidentsResolvedAt ON incidents(resolvedAt);
14682
14812
  static sharedIntegrityChecks = /* @__PURE__ */ new Map();
14683
14813
  db;
14684
14814
  dbPath;
14815
+ fusionDir;
14685
14816
  inMemory;
14686
14817
  /** Returns the database file path (or ":memory:" for in-memory databases). */
14687
14818
  get path() {
@@ -14702,6 +14833,7 @@ CREATE INDEX IF NOT EXISTS idxIncidentsResolvedAt ON incidents(resolvedAt);
14702
14833
  constructor(fusionDir, options) {
14703
14834
  const inMemory = options?.inMemory === true;
14704
14835
  this.inMemory = inMemory;
14836
+ this.fusionDir = fusionDir;
14705
14837
  this.dbPath = inMemory ? ":memory:" : join2(fusionDir, "fusion.db");
14706
14838
  this.busyTimeoutMs = Math.max(0, options?.busyTimeoutMs ?? DEFAULT_SQLITE_BUSY_TIMEOUT_MS);
14707
14839
  this.lockRecoveryWindowMs = Math.max(0, options?.lockRecoveryWindowMs ?? DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS);
@@ -17576,6 +17708,39 @@ This means a caller passed a .fusion directory where a project root was expected
17576
17708
  this.addColumnIfMissing("workflows", "icon", "TEXT");
17577
17709
  });
17578
17710
  }
17711
+ if (version2 < 135) {
17712
+ this.applyMigration(135, () => {
17713
+ this.addColumnIfMissing("tasks", "gitlabTracking", "TEXT");
17714
+ });
17715
+ }
17716
+ if (version2 < 136) {
17717
+ this.applyMigration(136, () => {
17718
+ this.db.exec(`
17719
+ CREATE TABLE IF NOT EXISTS chat_token_usage (
17720
+ id TEXT PRIMARY KEY,
17721
+ sourceKind TEXT NOT NULL,
17722
+ chatSessionId TEXT,
17723
+ roomId TEXT,
17724
+ messageId TEXT,
17725
+ projectId TEXT,
17726
+ agentId TEXT,
17727
+ modelProvider TEXT,
17728
+ modelId TEXT,
17729
+ inputTokens INTEGER NOT NULL DEFAULT 0,
17730
+ outputTokens INTEGER NOT NULL DEFAULT 0,
17731
+ cachedTokens INTEGER NOT NULL DEFAULT 0,
17732
+ cacheWriteTokens INTEGER NOT NULL DEFAULT 0,
17733
+ totalTokens INTEGER NOT NULL DEFAULT 0,
17734
+ createdAt TEXT NOT NULL
17735
+ );
17736
+ CREATE INDEX IF NOT EXISTS idxChatTokenUsageCreatedAt ON chat_token_usage(createdAt);
17737
+ CREATE INDEX IF NOT EXISTS idxChatTokenUsageProjectCreatedAt ON chat_token_usage(projectId, createdAt);
17738
+ CREATE INDEX IF NOT EXISTS idxChatTokenUsageSessionMessage ON chat_token_usage(chatSessionId, messageId);
17739
+ CREATE INDEX IF NOT EXISTS idxChatTokenUsageRoomMessage ON chat_token_usage(roomId, messageId);
17740
+ CREATE INDEX IF NOT EXISTS idxChatTokenUsageAgentCreatedAt ON chat_token_usage(agentId, createdAt);
17741
+ `);
17742
+ });
17743
+ }
17579
17744
  }
17580
17745
  /**
17581
17746
  * Idempotent schema reconciliation for the PR-entity tables. ensureSchema-
@@ -17950,23 +18115,61 @@ This means a caller passed a .fusion directory where a project root was expected
17950
18115
  const value = this.getMetaValue(_Database.PROJECT_IDENTITY_META_KEY);
17951
18116
  return fromJson(value);
17952
18117
  }
18118
+ parseWorkflowSettingsJson(raw) {
18119
+ if (!raw) return {};
18120
+ try {
18121
+ const parsed = JSON.parse(raw);
18122
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18123
+ } catch {
18124
+ return {};
18125
+ }
18126
+ }
18127
+ reconcileWorkflowSettingsRootDirProjectId(identityId) {
18128
+ if (this.inMemory) return false;
18129
+ const rootDirProjectId = basename(this.fusionDir) === ".fusion" ? dirname(this.fusionDir) : this.fusionDir;
18130
+ if (!rootDirProjectId || rootDirProjectId === identityId) return false;
18131
+ const rows = this.db.prepare('SELECT workflowId, "values" FROM workflow_settings WHERE projectId = ?').all(rootDirProjectId);
18132
+ if (rows.length === 0) return false;
18133
+ let changed = false;
18134
+ const now = (/* @__PURE__ */ new Date()).toISOString();
18135
+ for (const row of rows) {
18136
+ const rootValues = this.parseWorkflowSettingsJson(row.values);
18137
+ if (Object.keys(rootValues).length === 0) continue;
18138
+ const identityRow = this.db.prepare('SELECT "values" FROM workflow_settings WHERE workflowId = ? AND projectId = ?').get(row.workflowId, identityId);
18139
+ const identityValues = this.parseWorkflowSettingsJson(identityRow?.values);
18140
+ const carriesEveryRootKey = Object.keys(rootValues).every((key) => Object.prototype.hasOwnProperty.call(identityValues, key));
18141
+ if (carriesEveryRootKey) continue;
18142
+ const next = { ...rootValues, ...identityValues };
18143
+ this.db.prepare(
18144
+ `INSERT INTO workflow_settings (workflowId, projectId, "values", updatedAt)
18145
+ VALUES (?, ?, ?, ?)
18146
+ ON CONFLICT(workflowId, projectId)
18147
+ DO UPDATE SET "values" = excluded."values", updatedAt = excluded.updatedAt`
18148
+ ).run(row.workflowId, identityId, JSON.stringify(next), now);
18149
+ changed = true;
18150
+ }
18151
+ return changed;
18152
+ }
17953
18153
  setProjectIdentity(identity, options) {
17954
18154
  const stored = this.getProjectIdentity();
17955
18155
  const force = options?.force === true;
17956
- if (stored) {
17957
- if (stored.id === identity.id) {
17958
- return;
17959
- }
17960
- if (!force) {
17961
- throw new ProjectIdentityConflictError({
17962
- storedId: stored.id,
17963
- storedPath: stored.firstSeenPath,
17964
- incomingId: identity.id,
17965
- incomingPath: identity.firstSeenPath
17966
- });
17967
- }
18156
+ if (stored && stored.id !== identity.id && !force) {
18157
+ throw new ProjectIdentityConflictError({
18158
+ storedId: stored.id,
18159
+ storedPath: stored.firstSeenPath,
18160
+ incomingId: identity.id,
18161
+ incomingPath: identity.firstSeenPath
18162
+ });
17968
18163
  }
17969
- this.setMetaValue(_Database.PROJECT_IDENTITY_META_KEY, JSON.stringify(identity));
18164
+ this.transactionImmediate(() => {
18165
+ let changed = false;
18166
+ if (!stored || stored.id !== identity.id || force) {
18167
+ this.setMetaValue(_Database.PROJECT_IDENTITY_META_KEY, JSON.stringify(identity));
18168
+ changed = true;
18169
+ }
18170
+ changed = this.reconcileWorkflowSettingsRootDirProjectId(identity.id) || changed;
18171
+ if (changed) this.bumpLastModified();
18172
+ });
17970
18173
  }
17971
18174
  clearProjectIdentity() {
17972
18175
  this.db.prepare("DELETE FROM __meta WHERE key = ?").run(_Database.PROJECT_IDENTITY_META_KEY);
@@ -18022,7 +18225,7 @@ This means a caller passed a .fusion directory where a project root was expected
18022
18225
  // ../core/src/agent-store.ts
18023
18226
  import { mkdir, readFile, writeFile, readdir, unlink, rename, access, appendFile } from "node:fs/promises";
18024
18227
  import { constants as fsConstants } from "node:fs";
18025
- import { basename as basename2, dirname, join as join3, resolve as resolve2 } from "node:path";
18228
+ import { basename as basename2, dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
18026
18229
  import { randomUUID as randomUUID2, randomBytes, createHash as createHash3 } from "node:crypto";
18027
18230
  import { EventEmitter } from "node:events";
18028
18231
  function formatCurrentTaskLine(taskId, linkedTask) {
@@ -18034,6 +18237,12 @@ function formatCurrentTaskLine(taskId, linkedTask) {
18034
18237
  }
18035
18238
  return `Current Task: ${taskId} (${linkedTask.column})`;
18036
18239
  }
18240
+ function normalizeStoredPermissions(raw) {
18241
+ if (!raw) return void 0;
18242
+ const normalized = normalizePermissions(raw);
18243
+ if (normalized.size === 0) return void 0;
18244
+ return Object.fromEntries([...normalized].map((permission) => [permission, true]));
18245
+ }
18037
18246
  function resolveCreationRuntimeConfig(incoming, metadata) {
18038
18247
  const isEphemeral = isEphemeralAgent({ metadata });
18039
18248
  if (isEphemeral) {
@@ -18159,7 +18368,7 @@ var init_agent_store = __esm({
18159
18368
  const newAbsPath = join3(this.rootDir, "..", newRelPath);
18160
18369
  if (legacyContent !== null) {
18161
18370
  try {
18162
- await mkdir(dirname(newAbsPath), { recursive: true });
18371
+ await mkdir(dirname2(newAbsPath), { recursive: true });
18163
18372
  try {
18164
18373
  await access(newAbsPath, fsConstants.F_OK);
18165
18374
  } catch {
@@ -18391,7 +18600,7 @@ var init_agent_store = __esm({
18391
18600
  const agentId = `agent-${randomUUID2().slice(0, 8)}`;
18392
18601
  const runtimeConfig = resolveCreationRuntimeConfig(input.runtimeConfig, metadata);
18393
18602
  const resolvedHeartbeatProcedurePath = input.heartbeatProcedurePath ?? (ephemeral ? void 0 : getDefaultHeartbeatProcedurePath(agentId, input.name));
18394
- const normalizedPermissionPolicy = ephemeral ? input.permissionPolicy : resolveEffectiveAgentPermissionPolicy(input.permissionPolicy);
18603
+ const normalizedPermissionPolicy = input.permissionPolicy ? normalizeAgentPermissionPolicy(input.permissionPolicy) : void 0;
18395
18604
  const agent = {
18396
18605
  id: agentId,
18397
18606
  name: normalizedName,
@@ -18408,7 +18617,7 @@ var init_agent_store = __esm({
18408
18617
  ...input.imageUrl && { imageUrl: input.imageUrl },
18409
18618
  ...input.reportsTo && { reportsTo: input.reportsTo },
18410
18619
  ...runtimeConfig && { runtimeConfig },
18411
- ...input.permissions && { permissions: input.permissions },
18620
+ ...normalizeStoredPermissions(input.permissions) && { permissions: normalizeStoredPermissions(input.permissions) },
18412
18621
  ...normalizedPermissionPolicy && { permissionPolicy: normalizedPermissionPolicy },
18413
18622
  ...input.instructionsPath && { instructionsPath: input.instructionsPath },
18414
18623
  ...input.instructionsText && { instructionsText: input.instructionsText },
@@ -18795,7 +19004,7 @@ var init_agent_store = __esm({
18795
19004
  ..."reportsTo" in updates && { reportsTo: updates.reportsTo },
18796
19005
  ..."runtimeConfig" in updates && { runtimeConfig: updates.runtimeConfig },
18797
19006
  ..."pauseReason" in updates && { pauseReason: updates.pauseReason },
18798
- ..."permissions" in updates && { permissions: updates.permissions },
19007
+ ..."permissions" in updates && { permissions: normalizeStoredPermissions(updates.permissions) },
18799
19008
  ..."permissionPolicy" in updates && { permissionPolicy: normalizedUpdatedPermissionPolicy },
18800
19009
  ..."lastError" in updates && { lastError: updates.lastError },
18801
19010
  ..."totalInputTokens" in updates && { totalInputTokens: updates.totalInputTokens },
@@ -20191,8 +20400,8 @@ var init_agent_store = __esm({
20191
20400
  reportsTo: data.reportsTo,
20192
20401
  runtimeConfig: data.runtimeConfig,
20193
20402
  pauseReason: data.pauseReason,
20194
- permissions: data.permissions,
20195
- permissionPolicy: isEphemeralAgent(data) ? data.permissionPolicy : resolveEffectiveAgentPermissionPolicy(data.permissionPolicy),
20403
+ permissions: normalizeStoredPermissions(data.permissions),
20404
+ permissionPolicy: data.permissionPolicy ? normalizeAgentPermissionPolicy(data.permissionPolicy) : void 0,
20196
20405
  totalInputTokens: data.totalInputTokens,
20197
20406
  totalOutputTokens: data.totalOutputTokens,
20198
20407
  lastError: data.lastError,
@@ -21809,7 +22018,7 @@ var init_task_priority = __esm({
21809
22018
 
21810
22019
  // ../core/src/global-settings.ts
21811
22020
  import { homedir } from "node:os";
21812
- import { basename as basename3, dirname as dirname2, join as join5, resolve as resolve4 } from "node:path";
22021
+ import { basename as basename3, dirname as dirname3, join as join5, resolve as resolve4 } from "node:path";
21813
22022
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3, rename as rename2, chmod } from "node:fs/promises";
21814
22023
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, realpathSync as realpathSync2, renameSync as renameSync2 } from "node:fs";
21815
22024
  function getHomeDir() {
@@ -21823,7 +22032,7 @@ function resolveGlobalDirForHome(homeDir) {
21823
22032
  const legacyDir = join5(homeDir, ".pi", "fusion");
21824
22033
  if (existsSync3(legacyDir)) {
21825
22034
  try {
21826
- mkdirSync2(dirname2(preferredDir), { recursive: true });
22035
+ mkdirSync2(dirname3(preferredDir), { recursive: true });
21827
22036
  renameSync2(legacyDir, preferredDir);
21828
22037
  return preferredDir;
21829
22038
  } catch {
@@ -21833,7 +22042,7 @@ function resolveGlobalDirForHome(homeDir) {
21833
22042
  const legacyDirOriginal = join5(homeDir, ".pi", "kb");
21834
22043
  if (existsSync3(legacyDirOriginal)) {
21835
22044
  try {
21836
- mkdirSync2(dirname2(preferredDir), { recursive: true });
22045
+ mkdirSync2(dirname3(preferredDir), { recursive: true });
21837
22046
  renameSync2(legacyDirOriginal, preferredDir);
21838
22047
  return preferredDir;
21839
22048
  } catch {
@@ -21851,7 +22060,7 @@ function resolveGlobalDir(dir) {
21851
22060
  }
21852
22061
  if (hasExplicitDir) {
21853
22062
  if (process.env.VITEST !== "true" && process.env.FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR !== "true") {
21854
- const isFusionDirInsideRepo = basename3(dir) === ".fusion" && existsSync3(join5(dirname2(dir), ".git"));
22063
+ const isFusionDirInsideRepo = basename3(dir) === ".fusion" && existsSync3(join5(dirname3(dir), ".git"));
21855
22064
  if (isFusionDirInsideRepo) {
21856
22065
  const homeGlobalDir = resolveGlobalDirForHome(getHomeDir());
21857
22066
  const normalize8 = (p2) => {
@@ -27801,7 +28010,7 @@ var init_plugin_types = __esm({
27801
28010
  // ../core/src/project-root-guard.ts
27802
28011
  import { spawnSync as spawnSync2 } from "node:child_process";
27803
28012
  import { existsSync as existsSync7 } from "node:fs";
27804
- import { dirname as dirname3, join as join9, resolve as resolve5 } from "node:path";
28013
+ import { dirname as dirname4, join as join9, resolve as resolve5 } from "node:path";
27805
28014
  function assertProjectRootDir(rootDir, caller) {
27806
28015
  if (FUSION_DIR_SUFFIX.test(rootDir)) {
27807
28016
  throw new Error(
@@ -27837,7 +28046,7 @@ function assertNotLinkedWorktreeOfExistingProject(rootDir, _caller) {
27837
28046
  if (resolvedCommonDir === resolvedGitDir) {
27838
28047
  return;
27839
28048
  }
27840
- const parentRoot = resolvedCommonDir.endsWith(`${join9("", ".git")}`) ? dirname3(resolvedCommonDir) : resolvedCommonDir;
28049
+ const parentRoot = resolvedCommonDir.endsWith(`${join9("", ".git")}`) ? dirname4(resolvedCommonDir) : resolvedCommonDir;
27841
28050
  if (!existsSync7(join9(parentRoot, ".fusion", "fusion.db"))) {
27842
28051
  return;
27843
28052
  }
@@ -30823,11 +31032,11 @@ var init_eval_store = __esm({
30823
31032
 
30824
31033
  // ../core/src/app-version.ts
30825
31034
  import { readFileSync } from "node:fs";
30826
- import { join as join11, dirname as dirname4 } from "node:path";
31035
+ import { join as join11, dirname as dirname5 } from "node:path";
30827
31036
  import { fileURLToPath as fileURLToPath2 } from "node:url";
30828
31037
  function getAppVersion() {
30829
31038
  if (cachedVersion !== null) return cachedVersion;
30830
- const __dirname2 = dirname4(fileURLToPath2(import.meta.url));
31039
+ const __dirname2 = dirname5(fileURLToPath2(import.meta.url));
30831
31040
  let currentDir = __dirname2;
30832
31041
  for (let i = 0; i < 10; i++) {
30833
31042
  try {
@@ -30839,7 +31048,7 @@ function getAppVersion() {
30839
31048
  }
30840
31049
  } catch {
30841
31050
  }
30842
- const parentDir = dirname4(currentDir);
31051
+ const parentDir = dirname5(currentDir);
30843
31052
  if (parentDir === currentDir) break;
30844
31053
  currentDir = parentDir;
30845
31054
  }
@@ -37566,7 +37775,7 @@ __export(migration_exports, {
37566
37775
  });
37567
37776
  import { existsSync as existsSync11, readFileSync as readFileSync2, realpathSync as realpathSync3 } from "node:fs";
37568
37777
  import { homedir as homedir2, tmpdir } from "node:os";
37569
- import { isAbsolute as isAbsolute3, join as join14, resolve as resolve8, basename as basename6, dirname as dirname5 } from "node:path";
37778
+ import { isAbsolute as isAbsolute3, join as join14, resolve as resolve8, basename as basename6, dirname as dirname6 } from "node:path";
37570
37779
  function getHomeDir2() {
37571
37780
  return process.env.HOME || process.env.USERPROFILE || homedir2();
37572
37781
  }
@@ -37667,7 +37876,7 @@ var init_migration = __esm({
37667
37876
  const visited = /* @__PURE__ */ new Set();
37668
37877
  let current = resolve8(startDir);
37669
37878
  const home = resolve8(getHomeDir2());
37670
- const root = dirname5(current) === current ? current : "/";
37879
+ const root = dirname6(current) === current ? current : "/";
37671
37880
  const systemTmp = resolve8(tmpdir());
37672
37881
  const normalizePath4 = (path6) => {
37673
37882
  try {
@@ -37704,7 +37913,7 @@ var init_migration = __esm({
37704
37913
  if (isStopBoundary) {
37705
37914
  break;
37706
37915
  }
37707
- const parent = dirname5(current);
37916
+ const parent = dirname6(current);
37708
37917
  if (parent === current) break;
37709
37918
  current = parent;
37710
37919
  }
@@ -48807,7 +49016,7 @@ var init_memory_dreams = __esm({
48807
49016
  // ../core/src/memory-backend.ts
48808
49017
  import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir6, access as access3, constants, readdir as readdir4, stat as stat2 } from "node:fs/promises";
48809
49018
  import { existsSync as existsSync13 } from "node:fs";
48810
- import { basename as basename7, dirname as dirname6, isAbsolute as isAbsolute4, join as join18, normalize as normalize3, relative, resolve as resolve9, sep as sep3 } from "node:path";
49019
+ import { basename as basename7, dirname as dirname7, isAbsolute as isAbsolute4, join as join18, normalize as normalize3, relative, resolve as resolve9, sep as sep3 } from "node:path";
48811
49020
  import { createHash as createHash7 } from "node:crypto";
48812
49021
  function shouldSkipBackgroundQmdRefresh() {
48813
49022
  return (process.env.VITEST === "true" || process.env.NODE_ENV === "test") && process.env.FUSION_ENABLE_QMD_REFRESH_IN_TESTS !== "1";
@@ -48973,7 +49182,7 @@ async function readProjectMemoryFileContent(rootDir, path6) {
48973
49182
  }
48974
49183
  async function writeProjectMemoryFile(rootDir, path6, content) {
48975
49184
  const { absPath } = resolveMemoryFilePath(rootDir, path6);
48976
- await mkdir6(dirname6(absPath), { recursive: true });
49185
+ await mkdir6(dirname7(absPath), { recursive: true });
48977
49186
  const tmpPath = `${absPath}.tmp`;
48978
49187
  await writeFile5(tmpPath, content, "utf-8");
48979
49188
  const { rename: rename9 } = await import("node:fs/promises");
@@ -49092,7 +49301,7 @@ async function readAgentMemoryFile(rootDir, agentId, path6) {
49092
49301
  }
49093
49302
  async function writeAgentMemoryFile(rootDir, agentId, path6, content) {
49094
49303
  const { absPath } = await resolveAgentMemoryFilePath(rootDir, agentId, path6);
49095
- await mkdir6(dirname6(absPath), { recursive: true });
49304
+ await mkdir6(dirname7(absPath), { recursive: true });
49096
49305
  const tmpPath = `${absPath}.tmp`;
49097
49306
  await writeFile5(tmpPath, content, "utf-8");
49098
49307
  const { rename: rename9 } = await import("node:fs/promises");
@@ -49238,7 +49447,7 @@ function normalizeQmdSearchResultPath(rootDir, rawPath) {
49238
49447
  }
49239
49448
  const lowerCandidate = candidate.toLowerCase();
49240
49449
  const normalizedBaseName = basename7(candidate).toLowerCase();
49241
- const normalizedDirName = dirname6(lowerCandidate).replace(/\\/g, "/");
49450
+ const normalizedDirName = dirname7(lowerCandidate).replace(/\\/g, "/");
49242
49451
  if (normalizedBaseName === "memory.md" && (normalizedDirName === ".fusion" || normalizedDirName.endsWith("/.fusion"))) {
49243
49452
  return `${MEMORY_WORKSPACE_PATH}/${MEMORY_LONG_TERM_FILENAME}`;
49244
49453
  }
@@ -50387,14 +50596,14 @@ var init_agent_log_constants = __esm({
50387
50596
 
50388
50597
  // ../core/src/agent-log-file-store.ts
50389
50598
  import { appendFileSync, existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync3, readdirSync, renameSync as renameSync3, unlinkSync, writeFileSync } from "node:fs";
50390
- import { dirname as dirname7, join as join19 } from "node:path";
50599
+ import { dirname as dirname8, join as join19 } from "node:path";
50391
50600
  function getAgentLogFilePath(taskDir) {
50392
50601
  return join19(taskDir, AGENT_LOG_FILENAME);
50393
50602
  }
50394
50603
  function appendAgentLogEntriesSync(taskDir, entries) {
50395
50604
  if (entries.length === 0) return [];
50396
50605
  const filePath = getAgentLogFilePath(taskDir);
50397
- mkdirSync6(dirname7(filePath), { recursive: true });
50606
+ mkdirSync6(dirname8(filePath), { recursive: true });
50398
50607
  const startingLineNo = countLineNumbers(filePath);
50399
50608
  const payload = entries.map((entry) => serializeEntry(entry)).join("");
50400
50609
  appendFileSync(filePath, payload, "utf8");
@@ -51831,6 +52040,7 @@ var init_store = __esm({
51831
52040
  defineTaskColumn("prInfos", (task) => toJson(task.prInfos || [])),
51832
52041
  defineTaskColumn("issueInfo", (task) => toJsonNullable(task.issueInfo)),
51833
52042
  defineTaskColumn("githubTracking", (task) => toJsonNullable(task.githubTracking)),
52043
+ defineTaskColumn("gitlabTracking", (task) => toJsonNullable(task.gitlabTracking)),
51834
52044
  defineTaskColumn("sourceIssueProvider", (task) => task.sourceIssue?.provider ?? null),
51835
52045
  defineTaskColumn("sourceIssueRepository", (task) => task.sourceIssue?.repository ?? null),
51836
52046
  defineTaskColumn("sourceIssueExternalIssueId", (task) => task.sourceIssue?.externalIssueId ?? null),
@@ -52624,6 +52834,7 @@ var init_store = __esm({
52624
52834
  })(),
52625
52835
  issueInfo: fromJson(row.issueInfo),
52626
52836
  githubTracking: fromJson(row.githubTracking) ?? void 0,
52837
+ gitlabTracking: fromJson(row.gitlabTracking) ?? void 0,
52627
52838
  sourceIssue: (() => {
52628
52839
  if (row.sourceIssueProvider === null || row.sourceIssueRepository === null || row.sourceIssueExternalIssueId === null || row.sourceIssueNumber === null) {
52629
52840
  return void 0;
@@ -52733,6 +52944,7 @@ var init_store = __esm({
52733
52944
  prInfos: slim ? void 0 : entry.prInfos,
52734
52945
  issueInfo: slim ? void 0 : entry.issueInfo,
52735
52946
  githubTracking: entry.githubTracking,
52947
+ gitlabTracking: entry.gitlabTracking,
52736
52948
  sourceIssue: slim ? void 0 : entry.sourceIssue,
52737
52949
  attachments: slim ? void 0 : entry.attachments,
52738
52950
  comments: entry.comments,
@@ -52846,6 +53058,7 @@ ${recentText}` : void 0
52846
53058
  prInfos: task.prInfos,
52847
53059
  issueInfo: task.issueInfo,
52848
53060
  githubTracking: task.githubTracking,
53061
+ gitlabTracking: task.gitlabTracking,
52849
53062
  sourceIssue: task.sourceIssue,
52850
53063
  attachments: task.attachments,
52851
53064
  comments: task.comments,
@@ -53123,6 +53336,7 @@ ${recentText}` : void 0
53123
53336
  "prInfos",
53124
53337
  "issueInfo",
53125
53338
  "githubTracking",
53339
+ "gitlabTracking",
53126
53340
  "sourceIssueProvider",
53127
53341
  "sourceIssueRepository",
53128
53342
  "sourceIssueExternalIssueId",
@@ -53319,6 +53533,7 @@ ${outcome}`;
53319
53533
  "prInfos",
53320
53534
  "issueInfo",
53321
53535
  "githubTracking",
53536
+ "gitlabTracking",
53322
53537
  "sourceIssueProvider",
53323
53538
  "sourceIssueRepository",
53324
53539
  "sourceIssueExternalIssueId",
@@ -55096,6 +55311,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
55096
55311
  tokenUsage: input.tokenUsage,
55097
55312
  sourceIssue: input.sourceIssue,
55098
55313
  githubTracking: input.githubTracking,
55314
+ gitlabTracking: input.gitlabTracking,
55099
55315
  sourceType: input.source?.sourceType ?? "unknown",
55100
55316
  sourceAgentId: input.source?.sourceAgentId,
55101
55317
  sourceRunId: input.source?.sourceRunId,
@@ -56146,17 +56362,23 @@ ${newTask.description}
56146
56362
  }
56147
56363
  }
56148
56364
  async listTasksForGithubTrackingReconcile(options) {
56365
+ return this.listTasksForProviderTrackingReconcile("githubTracking", options);
56366
+ }
56367
+ async listTasksForGitlabTrackingReconcile(options) {
56368
+ return this.listTasksForProviderTrackingReconcile("gitlabTracking", options);
56369
+ }
56370
+ listTasksForProviderTrackingReconcile(trackingColumn, options) {
56149
56371
  const reconcileScanLimit = 200;
56150
56372
  const offset = Math.max(0, options?.offset ?? 0);
56151
56373
  const limit = Math.max(0, options?.limit ?? reconcileScanLimit);
56152
56374
  const selectClause = this.getTaskSelectClause(true);
56153
56375
  const deletedTotal = this.db.prepare(
56154
- 'SELECT COUNT(*) as count FROM tasks WHERE "deletedAt" IS NOT NULL AND "githubTracking" IS NOT NULL'
56376
+ `SELECT COUNT(*) as count FROM tasks WHERE "deletedAt" IS NOT NULL AND "${trackingColumn}" IS NOT NULL`
56155
56377
  ).get();
56156
56378
  const deletedCount = Number(deletedTotal?.count ?? 0);
56157
56379
  const deletedOffset = Math.min(offset, deletedCount);
56158
56380
  const deletedRows = this.db.prepare(
56159
- `SELECT ${selectClause} FROM tasks WHERE "deletedAt" IS NOT NULL AND "githubTracking" IS NOT NULL ORDER BY updatedAt ASC LIMIT ? OFFSET ?`
56381
+ `SELECT ${selectClause} FROM tasks WHERE "deletedAt" IS NOT NULL AND "${trackingColumn}" IS NOT NULL ORDER BY updatedAt ASC LIMIT ? OFFSET ?`
56160
56382
  ).all(limit, deletedOffset);
56161
56383
  const deletedTasks = deletedRows.map((row) => {
56162
56384
  const task = this.rowToTask(row);
@@ -56167,7 +56389,7 @@ ${newTask.description}
56167
56389
  let archivedTasks = [];
56168
56390
  let archivedCount = 0;
56169
56391
  try {
56170
- const archivedCandidates = this.archiveDb.list().map((entry) => this.archiveEntryToTask(entry, true)).filter((task) => Boolean(task.githubTracking));
56392
+ const archivedCandidates = this.archiveDb.list().map((entry) => this.archiveEntryToTask(entry, true)).filter((task) => Boolean(task[trackingColumn]));
56171
56393
  archivedCount = archivedCandidates.length;
56172
56394
  const archivedOffset = Math.max(0, offset - deletedCount);
56173
56395
  const remainingLimit = Math.max(0, limit - deletedTasks.length);
@@ -58456,6 +58678,38 @@ ${newTask.description}
58456
58678
  }
58457
58679
  task.githubTracking = nextTracking;
58458
58680
  }
58681
+ if (updates.gitlabTracking === null) {
58682
+ task.gitlabTracking = void 0;
58683
+ } else if (updates.gitlabTracking !== void 0) {
58684
+ const previousTracking = task.gitlabTracking;
58685
+ const previousItem = previousTracking?.item;
58686
+ const { item: gitlabItemPatch, ...gitlabTrackingPatch } = updates.gitlabTracking;
58687
+ const nextTracking = {
58688
+ ...previousTracking ?? {},
58689
+ ...gitlabTrackingPatch
58690
+ };
58691
+ if (gitlabItemPatch === null) {
58692
+ if (previousItem) {
58693
+ task.log.push({
58694
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
58695
+ action: "GitLab item unlinked",
58696
+ outcome: `${previousItem.host} ${previousItem.kind} !${previousItem.iid}`,
58697
+ ...runContext ? { runContext } : {}
58698
+ });
58699
+ }
58700
+ nextTracking.item = void 0;
58701
+ nextTracking.unlinkedAt = (/* @__PURE__ */ new Date()).toISOString();
58702
+ } else if (gitlabItemPatch !== void 0) {
58703
+ nextTracking.item = gitlabItemPatch;
58704
+ task.log.push({
58705
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
58706
+ action: "GitLab item linked",
58707
+ outcome: `${gitlabItemPatch.host} ${gitlabItemPatch.kind} !${gitlabItemPatch.iid}`,
58708
+ ...runContext ? { runContext } : {}
58709
+ });
58710
+ }
58711
+ task.gitlabTracking = nextTracking;
58712
+ }
58459
58713
  if (updates.tokenUsage === null) {
58460
58714
  task.tokenUsage = void 0;
58461
58715
  } else if (updates.tokenUsage !== void 0) {
@@ -60191,9 +60445,15 @@ ${newTask.description}
60191
60445
  throw new TaskSelfDeleteError(id);
60192
60446
  }
60193
60447
  this.flushAgentLogBuffer();
60194
- const task = this.readTaskFromDb(id, { includeDeleted: true });
60448
+ let task = this.readTaskFromDb(id, { includeDeleted: true });
60449
+ let restoredColdArchive = false;
60195
60450
  if (!task) {
60196
- throw new Error(`Task ${id} not found`);
60451
+ const archivedEntry = await this.findInArchive(id);
60452
+ if (!archivedEntry) {
60453
+ throw new Error(`Task ${id} not found`);
60454
+ }
60455
+ task = await this.restoreFromArchive(archivedEntry);
60456
+ restoredColdArchive = true;
60197
60457
  }
60198
60458
  if (task.deletedAt) {
60199
60459
  return task;
@@ -60221,9 +60481,15 @@ ${newTask.description}
60221
60481
  rewrittenDependents = this.rewriteDependentsForRemoval(id, dependentIds);
60222
60482
  rewrittenBlockedByResidueDependents = this.rewriteBlockedByResidueDependentsForRemoval(id, new Set(dependentIds));
60223
60483
  rewrittenLineageChildren = this.rewriteLineageChildrenForRemoval(id, lineageChildIds);
60484
+ if (restoredColdArchive) {
60485
+ this.upsertTaskWithFtsRecovery(task);
60486
+ }
60224
60487
  const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
60225
60488
  const allowResurrection = options?.allowResurrection === true ? 1 : 0;
60226
60489
  this.db.prepare(`UPDATE tasks SET "column" = 'archived', deletedAt = ?, allowResurrection = ?, updatedAt = ? WHERE id = ?`).run(deletedAt, allowResurrection, deletedAt, id);
60490
+ task.deletedAt = deletedAt;
60491
+ task.allowResurrection = options?.allowResurrection === true ? true : void 0;
60492
+ task.updatedAt = deletedAt;
60227
60493
  this.recordRunAuditEvent({
60228
60494
  domain: "database",
60229
60495
  mutationType: "task:deleted",
@@ -60247,6 +60513,7 @@ ${newTask.description}
60247
60513
  if (this.agentLogBuffer.length > 0) {
60248
60514
  this.agentLogBuffer = this.agentLogBuffer.filter((entry) => entry.taskId !== id);
60249
60515
  }
60516
+ this.archiveDb.delete(id);
60250
60517
  if (this.isWatching) this.taskCache.delete(id);
60251
60518
  for (const dependentTask of rewrittenDependents) {
60252
60519
  this.emit("task:updated", dependentTask);
@@ -62316,6 +62583,76 @@ ${newTask.description}
62316
62583
  return task;
62317
62584
  });
62318
62585
  }
62586
+ async updateGitLabTracking(id, tracking) {
62587
+ return this.withTaskLock(id, async () => {
62588
+ const dir = this.taskDir(id);
62589
+ const task = await this.readTaskJson(dir);
62590
+ const nextTracking = tracking ?? void 0;
62591
+ const previousTracking = task.gitlabTracking;
62592
+ if (JSON.stringify(previousTracking ?? null) === JSON.stringify(nextTracking ?? null)) {
62593
+ return task;
62594
+ }
62595
+ task.gitlabTracking = nextTracking;
62596
+ task.log.push({
62597
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
62598
+ action: nextTracking?.item ? "GitLab item linked" : "GitLab tracking cleared",
62599
+ outcome: nextTracking?.item ? `${nextTracking.item.host} ${nextTracking.item.kind} !${nextTracking.item.iid}` : void 0
62600
+ });
62601
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
62602
+ await this.atomicWriteTaskJson(dir, task);
62603
+ if (this.isWatching) this.taskCache.set(id, { ...task });
62604
+ this.emit("task:updated", task);
62605
+ return task;
62606
+ });
62607
+ }
62608
+ async linkGitLabItem(id, item) {
62609
+ return this.withTaskLock(id, async () => {
62610
+ const dir = this.taskDir(id);
62611
+ const task = await this.readTaskJson(dir);
62612
+ const previous = task.gitlabTracking ?? {};
62613
+ const nextTracking = { ...previous, item };
62614
+ if (JSON.stringify(previous) === JSON.stringify(nextTracking)) {
62615
+ return task;
62616
+ }
62617
+ task.gitlabTracking = nextTracking;
62618
+ task.log.push({
62619
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
62620
+ action: "GitLab item linked",
62621
+ outcome: `${item.host} ${item.kind} !${item.iid}`
62622
+ });
62623
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
62624
+ await this.atomicWriteTaskJson(dir, task);
62625
+ if (this.isWatching) this.taskCache.set(id, { ...task });
62626
+ this.emit("task:updated", task);
62627
+ return task;
62628
+ });
62629
+ }
62630
+ async unlinkGitLabItem(id) {
62631
+ return this.withTaskLock(id, async () => {
62632
+ const dir = this.taskDir(id);
62633
+ const task = await this.readTaskJson(dir);
62634
+ const previous = task.gitlabTracking;
62635
+ const previousItem = previous?.item;
62636
+ if (!previousItem || !previous) {
62637
+ return task;
62638
+ }
62639
+ task.gitlabTracking = {
62640
+ ...previous,
62641
+ item: void 0,
62642
+ unlinkedAt: (/* @__PURE__ */ new Date()).toISOString()
62643
+ };
62644
+ task.log.push({
62645
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
62646
+ action: "GitLab item unlinked",
62647
+ outcome: `${previousItem.host} ${previousItem.kind} !${previousItem.iid}`
62648
+ });
62649
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
62650
+ await this.atomicWriteTaskJson(dir, task);
62651
+ if (this.isWatching) this.taskCache.set(id, { ...task });
62652
+ this.emit("task:updated", task);
62653
+ return task;
62654
+ });
62655
+ }
62319
62656
  /**
62320
62657
  * Read historical agent log entries for a task from JSONL storage.
62321
62658
  * Returns entries in chronological order (oldest first).
@@ -62801,6 +63138,7 @@ ${newTask.description}
62801
63138
  review: entry.review,
62802
63139
  issueInfo: entry.issueInfo,
62803
63140
  githubTracking: entry.githubTracking,
63141
+ gitlabTracking: entry.gitlabTracking,
62804
63142
  sourceIssue: entry.sourceIssue,
62805
63143
  attachments: entry.attachments,
62806
63144
  log: [...entry.log, { timestamp: (/* @__PURE__ */ new Date()).toISOString(), action: "Task restored from archive" }],
@@ -65495,19 +65833,20 @@ function emptyTotals() {
65495
65833
  cachedTokens: 0,
65496
65834
  cacheWriteTokens: 0,
65497
65835
  totalTokens: 0,
65498
- nTasks: 0
65836
+ nTasks: 0,
65837
+ nChatMessages: 0
65499
65838
  };
65500
65839
  }
65501
65840
  function groupKeyFor(row, groupBy) {
65502
65841
  switch (groupBy) {
65503
65842
  case "model":
65504
- return row.tokenUsageModelId ?? row.modelId;
65843
+ return row.contributionKind === "task" ? row.tokenUsageModelId ?? row.modelId : row.modelId;
65505
65844
  case "provider":
65506
- return row.tokenUsageModelProvider ?? row.modelProvider;
65845
+ return row.contributionKind === "task" ? row.tokenUsageModelProvider ?? row.modelProvider : row.modelProvider;
65507
65846
  case "node":
65508
- return row.checkoutNodeId;
65847
+ return row.contributionKind === "task" ? row.checkoutNodeId : null;
65509
65848
  case "agent":
65510
- return row.assignedAgentId;
65849
+ return row.contributionKind === "task" ? row.assignedAgentId : row.agentId;
65511
65850
  }
65512
65851
  }
65513
65852
  function emptyCostAccumulator() {
@@ -65522,8 +65861,8 @@ function addRowCost(acc, row, now, pricingOverrides) {
65522
65861
  cacheWriteTokens: row.cacheWriteTokens ?? 0
65523
65862
  },
65524
65863
  {
65525
- provider: row.tokenUsageModelProvider ?? row.modelProvider,
65526
- model: row.tokenUsageModelId ?? row.modelId
65864
+ provider: row.contributionKind === "task" ? row.tokenUsageModelProvider ?? row.modelProvider : row.modelProvider,
65865
+ model: row.contributionKind === "task" ? row.tokenUsageModelId ?? row.modelId : row.modelId
65527
65866
  },
65528
65867
  now,
65529
65868
  pricingOverrides
@@ -65544,11 +65883,11 @@ function finalizeCost(acc) {
65544
65883
  };
65545
65884
  }
65546
65885
  function parsePerModelRows(row) {
65547
- if (!row.tokenUsagePerModel) return [];
65886
+ if (!row.tokenUsagePerModel) return { valid: false, rows: [] };
65548
65887
  try {
65549
65888
  const parsed = JSON.parse(row.tokenUsagePerModel);
65550
- if (!Array.isArray(parsed) || parsed.length === 0) return [];
65551
- return parsed.filter((entry) => entry !== null && typeof entry === "object").map((entry) => {
65889
+ if (!Array.isArray(parsed) || parsed.length === 0) return { valid: false, rows: [] };
65890
+ const rows = parsed.filter((entry) => entry !== null && typeof entry === "object").map((entry) => {
65552
65891
  const inputTokens = Number.isFinite(entry.inputTokens) ? Number(entry.inputTokens) : 0;
65553
65892
  const outputTokens = Number.isFinite(entry.outputTokens) ? Number(entry.outputTokens) : 0;
65554
65893
  const cachedTokens = Number.isFinite(entry.cachedTokens) ? Number(entry.cachedTokens) : 0;
@@ -65562,21 +65901,34 @@ function parsePerModelRows(row) {
65562
65901
  cacheWriteTokens,
65563
65902
  totalTokens,
65564
65903
  tokenUsageModelProvider: typeof entry.modelProvider === "string" ? entry.modelProvider : null,
65565
- tokenUsageModelId: typeof entry.modelId === "string" ? entry.modelId : null
65904
+ tokenUsageModelId: typeof entry.modelId === "string" ? entry.modelId : null,
65905
+ tokenUsageLastUsedAt: typeof entry.lastUsedAt === "string" ? entry.lastUsedAt : row.tokenUsageLastUsedAt,
65906
+ contributionKind: "task"
65566
65907
  };
65567
65908
  });
65909
+ return { valid: rows.length > 0, rows };
65568
65910
  } catch {
65569
- return [];
65911
+ return { valid: false, rows: [] };
65570
65912
  }
65571
65913
  }
65572
- function addRow(totals, row) {
65914
+ function isWithinRange(isoTimestamp, from, to2) {
65915
+ return (from === void 0 || isoTimestamp >= from) && (to2 === void 0 || isoTimestamp <= to2);
65916
+ }
65917
+ function addRow(totals, row, ids) {
65573
65918
  totals.inputTokens += row.inputTokens ?? 0;
65574
65919
  totals.outputTokens += row.outputTokens ?? 0;
65575
65920
  totals.cachedTokens += row.cachedTokens ?? 0;
65576
65921
  totals.cacheWriteTokens += row.cacheWriteTokens ?? 0;
65577
65922
  const persistedTotal = row.totalTokens;
65578
65923
  totals.totalTokens += persistedTotal ?? (row.inputTokens ?? 0) + (row.outputTokens ?? 0) + (row.cachedTokens ?? 0) + (row.cacheWriteTokens ?? 0);
65579
- totals.nTasks += 1;
65924
+ if (!ids || !ids.has(row.id)) {
65925
+ if (row.contributionKind === "task") {
65926
+ totals.nTasks += 1;
65927
+ } else {
65928
+ totals.nChatMessages = (totals.nChatMessages ?? 0) + 1;
65929
+ }
65930
+ ids?.add(row.id);
65931
+ }
65580
65932
  }
65581
65933
  function isoWeekBucket(isoTimestamp) {
65582
65934
  const date5 = new Date(isoTimestamp);
@@ -65587,30 +65939,39 @@ function isoWeekBucket(isoTimestamp) {
65587
65939
  const week = Math.ceil(((thursday.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
65588
65940
  return `${thursday.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
65589
65941
  }
65942
+ function contributionTimestamp(row) {
65943
+ return row.contributionKind === "task" ? row.tokenUsageLastUsedAt : row.createdAt;
65944
+ }
65590
65945
  function bucketFor(row, granularity) {
65946
+ const timestamp = contributionTimestamp(row);
65591
65947
  switch (granularity) {
65592
65948
  case "hour":
65593
- return row.tokenUsageLastUsedAt.slice(0, 13);
65949
+ return timestamp.slice(0, 13);
65594
65950
  case "day":
65595
- return row.tokenUsageLastUsedAt.slice(0, 10);
65951
+ return timestamp.slice(0, 10);
65596
65952
  case "week":
65597
- return isoWeekBucket(row.tokenUsageLastUsedAt);
65953
+ return isoWeekBucket(timestamp);
65598
65954
  }
65599
65955
  }
65600
65956
  function aggregateTokenAnalytics(db, query = {}) {
65601
65957
  const clauses = ["tokenUsageLastUsedAt IS NOT NULL"];
65602
65958
  const params = [];
65959
+ const rangeClauses2 = [];
65603
65960
  if (query.from !== void 0) {
65604
- clauses.push("tokenUsageLastUsedAt >= ?");
65961
+ rangeClauses2.push("tokenUsageLastUsedAt >= ?");
65605
65962
  params.push(query.from);
65606
65963
  }
65607
65964
  if (query.to !== void 0) {
65608
- clauses.push("tokenUsageLastUsedAt <= ?");
65965
+ rangeClauses2.push("tokenUsageLastUsedAt <= ?");
65609
65966
  params.push(query.to);
65610
65967
  }
65968
+ if (rangeClauses2.length > 0) {
65969
+ clauses.push(`((${rangeClauses2.join(" AND ")}) OR tokenUsagePerModel IS NOT NULL)`);
65970
+ }
65611
65971
  const where = `WHERE ${clauses.join(" AND ")}`;
65612
65972
  const rows = db.prepare(
65613
65973
  `SELECT
65974
+ id,
65614
65975
  tokenUsageInputTokens AS inputTokens,
65615
65976
  tokenUsageOutputTokens AS outputTokens,
65616
65977
  tokenUsageCachedTokens AS cachedTokens,
@@ -65626,6 +65987,36 @@ function aggregateTokenAnalytics(db, query = {}) {
65626
65987
  tokenUsageLastUsedAt
65627
65988
  FROM tasks ${where}`
65628
65989
  ).all(...params);
65990
+ const chatClauses = [];
65991
+ const chatParams = [];
65992
+ if (query.from !== void 0) {
65993
+ chatClauses.push("createdAt >= ?");
65994
+ chatParams.push(query.from);
65995
+ }
65996
+ if (query.to !== void 0) {
65997
+ chatClauses.push("createdAt <= ?");
65998
+ chatParams.push(query.to);
65999
+ }
66000
+ const chatWhere = chatClauses.length > 0 ? `WHERE ${chatClauses.join(" AND ")}` : "";
66001
+ const chatRows = db.prepare(
66002
+ `SELECT
66003
+ id,
66004
+ sourceKind,
66005
+ chatSessionId,
66006
+ roomId,
66007
+ messageId,
66008
+ projectId,
66009
+ agentId,
66010
+ inputTokens,
66011
+ outputTokens,
66012
+ cachedTokens,
66013
+ cacheWriteTokens,
66014
+ totalTokens,
66015
+ modelProvider,
66016
+ modelId,
66017
+ createdAt
66018
+ FROM chat_token_usage ${chatWhere}`
66019
+ ).all(...chatParams);
65629
66020
  const totals = emptyTotals();
65630
66021
  const totalCost = emptyCostAccumulator();
65631
66022
  const groupMap = /* @__PURE__ */ new Map();
@@ -65636,34 +66027,70 @@ function aggregateTokenAnalytics(db, query = {}) {
65636
66027
  const granularity = query.granularity;
65637
66028
  const now = query.now;
65638
66029
  const pricingOverrides = query.pricingOverrides;
66030
+ const totalContributionIds = /* @__PURE__ */ new Set();
66031
+ const groupContributionIds = /* @__PURE__ */ new Map();
66032
+ const seriesContributionIds = /* @__PURE__ */ new Map();
65639
66033
  for (const row of rows) {
65640
- addRow(totals, row);
65641
- addRowCost(totalCost, row, now, pricingOverrides);
65642
- if (groupBy) {
65643
- const groupRows = groupBy === "model" || groupBy === "provider" ? parsePerModelRows(row) : [];
65644
- const rowsForGroup = groupRows.length > 0 ? groupRows : [row];
65645
- for (const groupRow of rowsForGroup) {
65646
- const key = groupKeyFor(groupRow, groupBy);
66034
+ const taskRow = { ...row, contributionKind: "task" };
66035
+ const perModel = parsePerModelRows(row);
66036
+ const rowInRange = isWithinRange(row.tokenUsageLastUsedAt, query.from, query.to);
66037
+ const contributionRows = perModel.valid ? perModel.rows.filter((bucketRow) => isWithinRange(contributionTimestamp(bucketRow), query.from, query.to)) : rowInRange ? [taskRow] : [];
66038
+ for (const contributionRow of contributionRows) {
66039
+ addRow(totals, contributionRow, totalContributionIds);
66040
+ addRowCost(totalCost, contributionRow, now, pricingOverrides);
66041
+ if (groupBy) {
66042
+ const key = groupKeyFor(contributionRow, groupBy);
65647
66043
  let group = groupMap.get(key);
65648
66044
  if (!group) {
65649
66045
  group = { key, ...emptyTotals(), cost: { usd: null, unavailable: false, stale: false } };
65650
66046
  groupMap.set(key, group);
65651
66047
  groupCostMap.set(key, emptyCostAccumulator());
66048
+ groupContributionIds.set(key, /* @__PURE__ */ new Set());
66049
+ }
66050
+ addRow(group, contributionRow, groupContributionIds.get(key));
66051
+ addRowCost(groupCostMap.get(key), contributionRow, now, pricingOverrides);
66052
+ }
66053
+ if (granularity) {
66054
+ const bucket = bucketFor(contributionRow, granularity);
66055
+ let point = seriesMap.get(bucket);
66056
+ if (!point) {
66057
+ point = { bucket, ...emptyTotals(), cost: { usd: null, unavailable: false, stale: false } };
66058
+ seriesMap.set(bucket, point);
66059
+ seriesCostMap.set(bucket, emptyCostAccumulator());
66060
+ seriesContributionIds.set(bucket, /* @__PURE__ */ new Set());
65652
66061
  }
65653
- addRow(group, groupRow);
65654
- addRowCost(groupCostMap.get(key), groupRow, now, pricingOverrides);
66062
+ addRow(point, contributionRow, seriesContributionIds.get(bucket));
66063
+ addRowCost(seriesCostMap.get(bucket), contributionRow, now, pricingOverrides);
66064
+ }
66065
+ }
66066
+ }
66067
+ for (const row of chatRows) {
66068
+ const contributionRow = { ...row, contributionKind: "chat" };
66069
+ addRow(totals, contributionRow, totalContributionIds);
66070
+ addRowCost(totalCost, contributionRow, now, pricingOverrides);
66071
+ if (groupBy) {
66072
+ const key = groupKeyFor(contributionRow, groupBy);
66073
+ let group = groupMap.get(key);
66074
+ if (!group) {
66075
+ group = { key, ...emptyTotals(), cost: { usd: null, unavailable: false, stale: false } };
66076
+ groupMap.set(key, group);
66077
+ groupCostMap.set(key, emptyCostAccumulator());
66078
+ groupContributionIds.set(key, /* @__PURE__ */ new Set());
65655
66079
  }
66080
+ addRow(group, contributionRow, groupContributionIds.get(key));
66081
+ addRowCost(groupCostMap.get(key), contributionRow, now, pricingOverrides);
65656
66082
  }
65657
66083
  if (granularity) {
65658
- const bucket = bucketFor(row, granularity);
66084
+ const bucket = bucketFor(contributionRow, granularity);
65659
66085
  let point = seriesMap.get(bucket);
65660
66086
  if (!point) {
65661
66087
  point = { bucket, ...emptyTotals(), cost: { usd: null, unavailable: false, stale: false } };
65662
66088
  seriesMap.set(bucket, point);
65663
66089
  seriesCostMap.set(bucket, emptyCostAccumulator());
66090
+ seriesContributionIds.set(bucket, /* @__PURE__ */ new Set());
65664
66091
  }
65665
- addRow(point, row);
65666
- addRowCost(seriesCostMap.get(bucket), row, now, pricingOverrides);
66092
+ addRow(point, contributionRow, seriesContributionIds.get(bucket));
66093
+ addRowCost(seriesCostMap.get(bucket), contributionRow, now, pricingOverrides);
65667
66094
  }
65668
66095
  }
65669
66096
  for (const [key, group] of groupMap) {
@@ -66939,6 +67366,113 @@ var init_github_issue_analytics = __esm({
66939
67366
  }
66940
67367
  });
66941
67368
 
67369
+ // ../core/src/gitlab-issue-analytics.ts
67370
+ function isInRange2(iso, query) {
67371
+ const t = Date.parse(iso);
67372
+ if (!Number.isFinite(t)) return false;
67373
+ if (query.from !== void 0 && t < Date.parse(query.from)) return false;
67374
+ if (query.to !== void 0 && t > Date.parse(query.to)) return false;
67375
+ return true;
67376
+ }
67377
+ function dayKey2(iso) {
67378
+ const t = Date.parse(iso);
67379
+ if (!Number.isFinite(t)) return null;
67380
+ return new Date(t).toISOString().slice(0, 10);
67381
+ }
67382
+ function projectFromItem(item) {
67383
+ const projectPath = typeof item.projectPath === "string" ? item.projectPath.trim() : "";
67384
+ if (projectPath) return projectPath;
67385
+ const groupPath = typeof item.groupPath === "string" ? item.groupPath.trim() : "";
67386
+ if (groupPath) return groupPath;
67387
+ if (typeof item.projectId === "number" && Number.isFinite(item.projectId)) return String(item.projectId);
67388
+ return "(unknown)";
67389
+ }
67390
+ function addDaily2(daily, date5, kind) {
67391
+ const current = daily.get(date5) ?? { filed: 0, fixed: 0 };
67392
+ current[kind] += 1;
67393
+ daily.set(date5, current);
67394
+ }
67395
+ function addProject(byProject, project, kind) {
67396
+ const current = byProject.get(project) ?? { filed: 0, fixed: 0 };
67397
+ current[kind] += 1;
67398
+ byProject.set(project, current);
67399
+ }
67400
+ function aggregateGitlabIssueAnalytics(db, query = {}) {
67401
+ const daily = /* @__PURE__ */ new Map();
67402
+ const byProject = /* @__PURE__ */ new Map();
67403
+ const filedRows = db.prepare(
67404
+ "SELECT gitlabTracking FROM tasks WHERE gitlabTracking IS NOT NULL AND gitlabTracking NOT IN ('', '{}')"
67405
+ ).all();
67406
+ let filed = 0;
67407
+ for (const row of filedRows) {
67408
+ if (!row.gitlabTracking) continue;
67409
+ let parsed;
67410
+ try {
67411
+ parsed = JSON.parse(row.gitlabTracking);
67412
+ } catch {
67413
+ continue;
67414
+ }
67415
+ const item = parsed.item;
67416
+ if (!item || typeof item.iid !== "number" || !Number.isFinite(item.iid)) continue;
67417
+ const createdAt = typeof item.createdAt === "string" ? item.createdAt : void 0;
67418
+ const hasUsableDate = createdAt !== void 0 && dayKey2(createdAt) !== null;
67419
+ if (hasUsableDate && !isInRange2(createdAt, query)) continue;
67420
+ filed += 1;
67421
+ const project = projectFromItem(item);
67422
+ addProject(byProject, project, "filed");
67423
+ if (hasUsableDate && createdAt !== void 0) {
67424
+ const day = dayKey2(createdAt);
67425
+ if (day !== null) addDaily2(daily, day, "filed");
67426
+ }
67427
+ }
67428
+ const fixedRows = db.prepare(
67429
+ `SELECT id, title, sourceIssueRepository, sourceIssueNumber, sourceIssueUrl, sourceIssueClosedAt, updatedAt FROM tasks WHERE sourceIssueProvider = 'gitlab' AND "column" = 'done'`
67430
+ ).all();
67431
+ let fixed = 0;
67432
+ const resolved = [];
67433
+ for (const row of fixedRows) {
67434
+ const hasExactResolvedAt = row.sourceIssueClosedAt !== null;
67435
+ const fixedDate = row.sourceIssueClosedAt ?? row.updatedAt;
67436
+ if (fixedDate === null || !isInRange2(fixedDate, query)) continue;
67437
+ fixed += 1;
67438
+ const project = row.sourceIssueRepository?.trim() || "(unknown)";
67439
+ addProject(byProject, project, "fixed");
67440
+ const day = dayKey2(fixedDate);
67441
+ if (day !== null) addDaily2(daily, day, "fixed");
67442
+ resolved.push({
67443
+ taskId: row.id,
67444
+ taskTitle: row.title ?? "",
67445
+ project,
67446
+ issueNumber: typeof row.sourceIssueNumber === "number" ? row.sourceIssueNumber : null,
67447
+ url: row.sourceIssueUrl?.trim() || null,
67448
+ resolvedAt: fixedDate,
67449
+ resolvedAtExact: hasExactResolvedAt
67450
+ });
67451
+ }
67452
+ resolved.sort((a, b2) => {
67453
+ const byDate = Date.parse(b2.resolvedAt) - Date.parse(a.resolvedAt);
67454
+ return byDate !== 0 ? byDate : a.taskId.localeCompare(b2.taskId);
67455
+ });
67456
+ return {
67457
+ from: query.from ?? null,
67458
+ to: query.to ?? null,
67459
+ filed,
67460
+ fixed,
67461
+ net: filed - fixed,
67462
+ daily: [...daily.entries()].map(([date5, counts]) => ({ date: date5, filed: counts.filed, fixed: counts.fixed })).sort((a, b2) => a.date.localeCompare(b2.date)),
67463
+ byProject: [...byProject.entries()].map(([project, counts]) => ({ project, filed: counts.filed, fixed: counts.fixed })).sort((a, b2) => {
67464
+ const total = b2.filed + b2.fixed - (a.filed + a.fixed);
67465
+ return total !== 0 ? total : a.project.localeCompare(b2.project);
67466
+ }),
67467
+ resolved
67468
+ };
67469
+ }
67470
+ var init_gitlab_issue_analytics = __esm({
67471
+ "../core/src/gitlab-issue-analytics.ts"() {
67472
+ "use strict";
67473
+ }
67474
+ });
67475
+
66942
67476
  // ../core/src/command-center-live.ts
66943
67477
  function composeLiveSnapshot(db, now) {
66944
67478
  const capturedAt = new Date(now ?? Date.now()).toISOString();
@@ -67306,7 +67840,7 @@ var init_daemon_token = __esm({
67306
67840
  // ../core/src/pi-extensions.ts
67307
67841
  import { existsSync as existsSync17, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
67308
67842
  import { homedir as homedir3 } from "node:os";
67309
- import { basename as basename8, dirname as dirname8, isAbsolute as isAbsolute5, join as join21, relative as relative2, resolve as resolve10, sep as sep4, win32 } from "node:path";
67843
+ import { basename as basename8, dirname as dirname9, isAbsolute as isAbsolute5, join as join21, relative as relative2, resolve as resolve10, sep as sep4, win32 } from "node:path";
67310
67844
  import { createRequire as createRequire3 } from "node:module";
67311
67845
  function getSpawnSync() {
67312
67846
  if (!didLoadSpawnSync) {
@@ -67382,7 +67916,7 @@ function getProjectRootFromGitLinkedWorktree(cwd) {
67382
67916
  if (!resolvedCommonDir || !resolvedGitDir || resolvedCommonDir === resolvedGitDir) {
67383
67917
  return null;
67384
67918
  }
67385
- const parentRoot = resolvedCommonDir.endsWith(`${sep4}.git`) ? dirname8(resolvedCommonDir) : resolvedCommonDir;
67919
+ const parentRoot = resolvedCommonDir.endsWith(`${sep4}.git`) ? dirname9(resolvedCommonDir) : resolvedCommonDir;
67386
67920
  return existsSync17(join21(parentRoot, ".fusion")) ? parentRoot : null;
67387
67921
  }
67388
67922
  function resolvePiExtensionProjectRoot(cwd) {
@@ -69097,7 +69631,7 @@ var init_plugin_security_scan = __esm({
69097
69631
  });
69098
69632
 
69099
69633
  // ../core/src/plugin-loader.ts
69100
- import { basename as basename9, dirname as dirname9, extname, isAbsolute as isAbsolute6, join as join23, resolve as resolve11 } from "node:path";
69634
+ import { basename as basename9, dirname as dirname10, extname, isAbsolute as isAbsolute6, join as join23, resolve as resolve11 } from "node:path";
69101
69635
  import { existsSync as existsSync19, readdirSync as readdirSync3, statSync as statSync6 } from "node:fs";
69102
69636
  import { copyFile, readFile as readFile10, stat as stat4 } from "node:fs/promises";
69103
69637
  import { pathToFileURL } from "node:url";
@@ -69377,7 +69911,7 @@ var init_plugin_loader = __esm({
69377
69911
  moduleImportVersion += 1;
69378
69912
  const ext = extname(path6);
69379
69913
  const baseName = basename9(path6, ext);
69380
- const reloadedPath = resolve11(dirname9(path6), `.${baseName}.reload-${moduleImportVersion}${ext}`);
69914
+ const reloadedPath = resolve11(dirname10(path6), `.${baseName}.reload-${moduleImportVersion}${ext}`);
69381
69915
  await copyFile(path6, reloadedPath);
69382
69916
  mod = await import(pathToFileURL(reloadedPath).href);
69383
69917
  } else {
@@ -69869,10 +70403,10 @@ var init_plugin_loader = __esm({
69869
70403
  }
69870
70404
  } catch {
69871
70405
  }
69872
- const entryDir = dirname9(pluginEntryPath);
70406
+ const entryDir = dirname10(pluginEntryPath);
69873
70407
  candidates.add(join23(entryDir, "manifest.json"));
69874
70408
  if (PLUGIN_MANIFEST_PARENT_DIR_NAMES.has(basename9(entryDir))) {
69875
- candidates.add(join23(dirname9(entryDir), "manifest.json"));
70409
+ candidates.add(join23(dirname10(entryDir), "manifest.json"));
69876
70410
  }
69877
70411
  for (const candidate of candidates) {
69878
70412
  try {
@@ -70682,7 +71216,7 @@ var init_backup = __esm({
70682
71216
  // ../core/src/memory-backup.ts
70683
71217
  import { cp as cp2, mkdir as mkdir9, readdir as readdir7, readFile as readFile11, rename as rename6, rm as rm2, stat as stat6, writeFile as writeFile7 } from "node:fs/promises";
70684
71218
  import { existsSync as existsSync21 } from "node:fs";
70685
- import { dirname as dirname10, join as join25, relative as relative3, resolve as resolve12, sep as sep5 } from "node:path";
71219
+ import { dirname as dirname11, join as join25, relative as relative3, resolve as resolve12, sep as sep5 } from "node:path";
70686
71220
  function createMemoryBackupManager(fusionDir, settings) {
70687
71221
  return new MemoryBackupManager(fusionDir, {
70688
71222
  backupDir: sanitizeMemoryBackupDir(fusionDir, canonicalizeMemoryBackupDir(settings?.memoryBackupDir)),
@@ -70964,7 +71498,7 @@ var init_memory_backup = __esm({
70964
71498
  for (const file2 of files) {
70965
71499
  const rel = relative3(sourceDir, file2);
70966
71500
  const destFile = join25(destinationDir, rel);
70967
- await mkdir9(dirname10(destFile), { recursive: true });
71501
+ await mkdir9(dirname11(destFile), { recursive: true });
70968
71502
  if (existsSync21(destFile) && !overwrite) {
70969
71503
  const [srcBuf, dstBuf] = await Promise.all([readFile11(file2), readFile11(destFile)]);
70970
71504
  if (!srcBuf.equals(dstBuf)) {
@@ -72181,7 +72715,7 @@ var init_docker_provisioning = __esm({
72181
72715
  // ../core/src/memory-insights.ts
72182
72716
  import { readFile as readFile14, writeFile as writeFile9, mkdir as mkdir10, unlink as unlink5 } from "node:fs/promises";
72183
72717
  import { existsSync as existsSync22 } from "node:fs";
72184
- import { dirname as dirname11, join as join26 } from "node:path";
72718
+ import { dirname as dirname12, join as join26 } from "node:path";
72185
72719
  async function readWorkingMemory(rootDir) {
72186
72720
  const filePath = join26(rootDir, MEMORY_WORKING_PATH);
72187
72721
  if (!existsSync22(filePath)) {
@@ -72196,7 +72730,7 @@ async function migrateLegacyArtifactIfNeeded(rootDir, canonicalPath, legacyPath)
72196
72730
  return;
72197
72731
  }
72198
72732
  const content = await readFile14(legacyFilePath, "utf-8");
72199
- const canonicalDir = dirname11(canonicalFilePath);
72733
+ const canonicalDir = dirname12(canonicalFilePath);
72200
72734
  if (!existsSync22(canonicalDir)) {
72201
72735
  await mkdir10(canonicalDir, { recursive: true });
72202
72736
  }
@@ -72226,7 +72760,7 @@ async function readInsightsMemory(rootDir) {
72226
72760
  }
72227
72761
  async function writeInsightsMemory(rootDir, content) {
72228
72762
  const filePath = join26(rootDir, MEMORY_INSIGHTS_PATH);
72229
- const dir = dirname11(filePath);
72763
+ const dir = dirname12(filePath);
72230
72764
  if (!existsSync22(dir)) {
72231
72765
  await mkdir10(dir, { recursive: true });
72232
72766
  }
@@ -72235,7 +72769,7 @@ async function writeInsightsMemory(rootDir, content) {
72235
72769
  }
72236
72770
  async function writeWorkingMemory(rootDir, content) {
72237
72771
  const filePath = join26(rootDir, MEMORY_WORKING_PATH);
72238
- const dir = dirname11(filePath);
72772
+ const dir = dirname12(filePath);
72239
72773
  if (!existsSync22(dir)) {
72240
72774
  await mkdir10(dir, { recursive: true });
72241
72775
  }
@@ -72251,7 +72785,7 @@ async function readMemoryAudit(rootDir) {
72251
72785
  }
72252
72786
  async function writeMemoryAudit(rootDir, content) {
72253
72787
  const filePath = join26(rootDir, MEMORY_AUDIT_PATH);
72254
- const dir = dirname11(filePath);
72788
+ const dir = dirname12(filePath);
72255
72789
  if (!existsSync22(dir)) {
72256
72790
  await mkdir10(dir, { recursive: true });
72257
72791
  }
@@ -72280,7 +72814,7 @@ async function readMemoryAuditState(rootDir) {
72280
72814
  }
72281
72815
  async function writeMemoryAuditState(rootDir, state) {
72282
72816
  const filePath = join26(rootDir, MEMORY_AUDIT_STATE_PATH);
72283
- const dir = dirname11(filePath);
72817
+ const dir = dirname12(filePath);
72284
72818
  if (!existsSync22(dir)) {
72285
72819
  await mkdir10(dir, { recursive: true });
72286
72820
  }
@@ -87517,6 +88051,25 @@ var init_chat_store = __esm({
87517
88051
  createdAt: row.createdAt
87518
88052
  };
87519
88053
  }
88054
+ rowToTokenUsage(row) {
88055
+ return {
88056
+ id: row.id,
88057
+ sourceKind: row.sourceKind,
88058
+ chatSessionId: row.chatSessionId ?? null,
88059
+ roomId: row.roomId ?? null,
88060
+ messageId: row.messageId ?? null,
88061
+ projectId: row.projectId ?? null,
88062
+ agentId: row.agentId ?? null,
88063
+ modelProvider: row.modelProvider ?? null,
88064
+ modelId: row.modelId ?? null,
88065
+ inputTokens: row.inputTokens ?? 0,
88066
+ outputTokens: row.outputTokens ?? 0,
88067
+ cachedTokens: row.cachedTokens ?? 0,
88068
+ cacheWriteTokens: row.cacheWriteTokens ?? 0,
88069
+ totalTokens: row.totalTokens ?? 0,
88070
+ createdAt: row.createdAt
88071
+ };
88072
+ }
87520
88073
  normalizeRoomName(name) {
87521
88074
  return name.trim().replace(/^#+/, "").trim();
87522
88075
  }
@@ -88265,6 +88818,62 @@ var init_chat_store = __esm({
88265
88818
  this.emit("chat:room:message:updated", updated);
88266
88819
  return updated;
88267
88820
  }
88821
+ recordTokenUsage(input) {
88822
+ const inputTokens = Math.max(0, Math.trunc(input.inputTokens));
88823
+ const outputTokens = Math.max(0, Math.trunc(input.outputTokens));
88824
+ const cachedTokens = Math.max(0, Math.trunc(input.cachedTokens));
88825
+ const cacheWriteTokens = Math.max(0, Math.trunc(input.cacheWriteTokens));
88826
+ const totalTokens = Math.max(0, Math.trunc(input.totalTokens ?? inputTokens + outputTokens + cachedTokens + cacheWriteTokens));
88827
+ if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && cacheWriteTokens === 0 && totalTokens === 0) {
88828
+ return void 0;
88829
+ }
88830
+ const record2 = {
88831
+ id: `chat-tokens-${randomUUID19().slice(0, 12)}`,
88832
+ sourceKind: input.sourceKind,
88833
+ chatSessionId: input.chatSessionId ?? null,
88834
+ roomId: input.roomId ?? null,
88835
+ messageId: input.messageId ?? null,
88836
+ projectId: input.projectId ?? null,
88837
+ agentId: input.agentId ?? null,
88838
+ modelProvider: input.modelProvider ?? null,
88839
+ modelId: input.modelId ?? null,
88840
+ inputTokens,
88841
+ outputTokens,
88842
+ cachedTokens,
88843
+ cacheWriteTokens,
88844
+ totalTokens,
88845
+ createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
88846
+ };
88847
+ this.db.prepare(`
88848
+ INSERT INTO chat_token_usage (
88849
+ id, sourceKind, chatSessionId, roomId, messageId, projectId, agentId,
88850
+ modelProvider, modelId, inputTokens, outputTokens, cachedTokens,
88851
+ cacheWriteTokens, totalTokens, createdAt
88852
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
88853
+ `).run(
88854
+ record2.id,
88855
+ record2.sourceKind,
88856
+ record2.chatSessionId,
88857
+ record2.roomId,
88858
+ record2.messageId,
88859
+ record2.projectId,
88860
+ record2.agentId,
88861
+ record2.modelProvider,
88862
+ record2.modelId,
88863
+ record2.inputTokens,
88864
+ record2.outputTokens,
88865
+ record2.cachedTokens,
88866
+ record2.cacheWriteTokens,
88867
+ record2.totalTokens,
88868
+ record2.createdAt
88869
+ );
88870
+ this.db.bumpLastModified();
88871
+ return record2;
88872
+ }
88873
+ listTokenUsage() {
88874
+ const rows = this.db.prepare("SELECT * FROM chat_token_usage ORDER BY createdAt ASC").all();
88875
+ return rows.map((row) => this.rowToTokenUsage(row));
88876
+ }
88268
88877
  };
88269
88878
  }
88270
88879
  });
@@ -88947,6 +89556,8 @@ __export(src_exports, {
88947
89556
  DEFAULT_DEPENDENCY_BLOCKED_TODO_MIN_COUNT: () => DEFAULT_DEPENDENCY_BLOCKED_TODO_MIN_COUNT,
88948
89557
  DEFAULT_DEPENDENCY_BLOCKED_TODO_STALE_MS: () => DEFAULT_DEPENDENCY_BLOCKED_TODO_STALE_MS,
88949
89558
  DEFAULT_EXECUTION_MODE: () => DEFAULT_EXECUTION_MODE,
89559
+ DEFAULT_GITLAB_API_BASE_URL: () => DEFAULT_GITLAB_API_BASE_URL,
89560
+ DEFAULT_GITLAB_INSTANCE_URL: () => DEFAULT_GITLAB_INSTANCE_URL,
88950
89561
  DEFAULT_GLOBAL_SETTINGS: () => DEFAULT_GLOBAL_SETTINGS,
88951
89562
  DEFAULT_HEARTBEAT_PROCEDURE_PATH: () => DEFAULT_HEARTBEAT_PROCEDURE_PATH,
88952
89563
  DEFAULT_INSIGHT_SCHEDULE: () => DEFAULT_INSIGHT_SCHEDULE,
@@ -89099,6 +89710,10 @@ __export(src_exports, {
89099
89710
  OTEL_METRIC_PREFIX: () => OTEL_METRIC_PREFIX,
89100
89711
  OccupiedColumnsError: () => OccupiedColumnsError,
89101
89712
  PERMANENT_AGENT_ACTION_CATEGORIES: () => PERMANENT_AGENT_ACTION_CATEGORIES,
89713
+ PLANNING_DEEPEN_CHECKPOINT_ID: () => PLANNING_DEEPEN_CHECKPOINT_ID,
89714
+ PLANNING_DEEPEN_CHECKPOINT_QUESTION: () => PLANNING_DEEPEN_CHECKPOINT_QUESTION,
89715
+ PLANNING_DEEPEN_PROCEED_OPTION_ID: () => PLANNING_DEEPEN_PROCEED_OPTION_ID,
89716
+ PLANNING_DEEPEN_PROCEED_RESPONSE_KEY: () => PLANNING_DEEPEN_PROCEED_RESPONSE_KEY,
89102
89717
  PLAN_REVIEW_GROUP_ID: () => PLAN_REVIEW_GROUP_ID,
89103
89718
  PLAN_REVIEW_MAX_REVISIONS_SETTING_ID: () => PLAN_REVIEW_MAX_REVISIONS_SETTING_ID,
89104
89719
  PLUGIN_TRAIT_ALLOWED_HOOK_POINTS: () => PLUGIN_TRAIT_ALLOWED_HOOK_POINTS,
@@ -89227,6 +89842,7 @@ __export(src_exports, {
89227
89842
  aggregateActivityAnalytics: () => aggregateActivityAnalytics,
89228
89843
  aggregateAgentTokenUsage: () => aggregateAgentTokenUsage,
89229
89844
  aggregateGithubIssueAnalytics: () => aggregateGithubIssueAnalytics,
89845
+ aggregateGitlabIssueAnalytics: () => aggregateGitlabIssueAnalytics,
89230
89846
  aggregateMonitorMetrics: () => aggregateMonitorMetrics,
89231
89847
  aggregatePluginActivations: () => aggregatePluginActivations,
89232
89848
  aggregateProductivityAnalytics: () => aggregateProductivityAnalytics,
@@ -89401,6 +90017,7 @@ __export(src_exports, {
89401
90017
  findWorkflowColumn: () => findWorkflowColumn,
89402
90018
  formatCurrentTaskLine: () => formatCurrentTaskLine,
89403
90019
  formatDistributedTaskId: () => formatDistributedTaskId,
90020
+ formatGitLabTrackedItemRef: () => formatGitLabTrackedItemRef,
89404
90021
  formatPiExtensionSource: () => formatPiExtensionSource,
89405
90022
  formatRoleMismatchReason: () => formatRoleMismatchReason,
89406
90023
  formatTaskListText: () => formatTaskListText,
@@ -89503,6 +90120,7 @@ __export(src_exports, {
89503
90120
  isFinalizeRecord: () => isFinalizeRecord,
89504
90121
  isGhAuthenticated: () => isGhAuthenticated,
89505
90122
  isGhAvailable: () => isGhAvailable,
90123
+ isGitLabTrackingStale: () => isGitLabTrackingStale,
89506
90124
  isGlobalSettingsKey: () => isGlobalSettingsKey,
89507
90125
  isHookRecord: () => isHookRecord,
89508
90126
  isImplementationTask: () => isImplementationTask,
@@ -89690,6 +90308,8 @@ __export(src_exports, {
89690
90308
  resolveEntryPointBranchAssignment: () => resolveEntryPointBranchAssignment,
89691
90309
  resolveEvalSettings: () => resolveEvalSettings,
89692
90310
  resolveExecutionSettingsModel: () => resolveExecutionSettingsModel,
90311
+ resolveGitlabConfig: () => resolveGitlabConfig,
90312
+ resolveGitlabEnabled: () => resolveGitlabEnabled,
89693
90313
  resolveGlobalDir: () => resolveGlobalDir,
89694
90314
  resolveGlobalDirForHome: () => resolveGlobalDirForHome,
89695
90315
  resolveLocalNodeId: () => resolveLocalNodeId,
@@ -89865,6 +90485,7 @@ var init_src = __esm({
89865
90485
  init_redact_secrets();
89866
90486
  init_plan_approval();
89867
90487
  init_near_duplicate_canonical();
90488
+ init_gitlab_tracking();
89868
90489
  init_frontend_ux_policy();
89869
90490
  init_file_scope_classification();
89870
90491
  init_task_list_format();
@@ -89954,6 +90575,7 @@ var init_src = __esm({
89954
90575
  init_team_analytics();
89955
90576
  init_workflow_analytics();
89956
90577
  init_github_issue_analytics();
90578
+ init_gitlab_issue_analytics();
89957
90579
  init_activity_analytics();
89958
90580
  init_command_center_live();
89959
90581
  init_otel_metrics();
@@ -90616,7 +91238,10 @@ var init_gating_classifications = __esm({
90616
91238
  ...PERMANENT_AND_ACTION_TASK_AGENT_TOOLS,
90617
91239
  "fn_delegate_task",
90618
91240
  "fn_task_import_github",
90619
- "fn_task_import_github_issue"
91241
+ "fn_task_import_github_issue",
91242
+ "fn_task_import_gitlab_project_issues",
91243
+ "fn_task_import_gitlab_group_issues",
91244
+ "fn_task_import_gitlab_merge_requests"
90620
91245
  ];
90621
91246
  ACTION_GATE_SHARED_TASK_AGENT_TOOLS = SHARED_TASK_AGENT_TOOLS.filter(
90622
91247
  (tool) => !PROVISIONING_TOOLS.includes(tool)
@@ -90694,6 +91319,12 @@ var init_gating_classifications = __esm({
90694
91319
  "fn_delegate_task",
90695
91320
  "fn_task_import_github",
90696
91321
  "fn_task_import_github_issue",
91322
+ "fn_task_import_gitlab_project_issues",
91323
+ "fn_task_import_gitlab_group_issues",
91324
+ "fn_task_import_gitlab_merge_requests",
91325
+ "fn_task_browse_gitlab_project_issues",
91326
+ "fn_task_browse_gitlab_group_issues",
91327
+ "fn_task_browse_gitlab_merge_requests",
90697
91328
  "fn_research_list",
90698
91329
  "fn_research_get",
90699
91330
  "fn_insight_list",
@@ -90962,7 +91593,7 @@ var init_agent_action_gate = __esm({
90962
91593
  import { existsSync as existsSync25, readFileSync as readFileSync8 } from "node:fs";
90963
91594
  import { homedir as homedir5 } from "node:os";
90964
91595
  import { join as join30 } from "node:path";
90965
- import { AuthStorage } from "@earendil-works/pi-coding-agent";
91596
+ import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
90966
91597
  import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
90967
91598
  function getHomeDir5() {
90968
91599
  return process.env.HOME || process.env.USERPROFILE || homedir5();
@@ -91152,6 +91783,9 @@ function readModelsJsonApiKeys(home = getHomeDir5()) {
91152
91783
  }
91153
91784
  return apiKeys;
91154
91785
  }
91786
+ function createFusionModelRegistry(authStorage, home) {
91787
+ return ModelRegistry.create(authStorage, getModelRegistryModelsPath(home));
91788
+ }
91155
91789
  function createFusionAuthStorage() {
91156
91790
  const primary = AuthStorage.create(getFusionAuthPath());
91157
91791
  let supplementalCredentials = readSupplementalCredentials();
@@ -92892,7 +93526,7 @@ var init_github_provider = __esm({
92892
93526
 
92893
93527
  // ../engine/src/skill-resolver.ts
92894
93528
  import { existsSync as existsSync26, readFileSync as readFileSync9 } from "node:fs";
92895
- import { dirname as dirname12, join as join31, resolve as resolve15 } from "node:path";
93529
+ import { dirname as dirname13, join as join31, resolve as resolve15 } from "node:path";
92896
93530
  function resolveProjectRoot(cwd) {
92897
93531
  const worktreeProjectRoot = getProjectRootFromWorktree(cwd);
92898
93532
  if (worktreeProjectRoot && existsSync26(join31(worktreeProjectRoot, ".fusion"))) {
@@ -92903,7 +93537,7 @@ function resolveProjectRoot(cwd) {
92903
93537
  if (existsSync26(join31(current, ".fusion"))) {
92904
93538
  return current;
92905
93539
  }
92906
- const parent = dirname12(current);
93540
+ const parent = dirname13(current);
92907
93541
  if (parent === current) {
92908
93542
  return resolve15(cwd);
92909
93543
  }
@@ -119000,7 +119634,7 @@ import { existsSync as existsSync27, readFileSync as readFileSync11, realpathSyn
119000
119634
  import { exec as exec2, execFile as execFile4 } from "node:child_process";
119001
119635
  import { promisify as promisify4 } from "node:util";
119002
119636
  import { createRequire as createRequire4 } from "node:module";
119003
- import { basename as basename10, dirname as dirname13, join as join33, relative as relative4, isAbsolute as isAbsolute8, resolve as resolve16 } from "node:path";
119637
+ import { basename as basename10, dirname as dirname14, join as join33, relative as relative4, isAbsolute as isAbsolute8, resolve as resolve16 } from "node:path";
119004
119638
  import {
119005
119639
  createAgentSession,
119006
119640
  createBashTool,
@@ -119016,7 +119650,7 @@ import {
119016
119650
  DefaultResourceLoader,
119017
119651
  DefaultPackageManager,
119018
119652
  discoverAndLoadExtensions,
119019
- ModelRegistry,
119653
+ ModelRegistry as ModelRegistry2,
119020
119654
  SessionManager,
119021
119655
  SettingsManager
119022
119656
  } from "@earendil-works/pi-coding-agent";
@@ -119794,7 +120428,7 @@ function siblingAgentDir(agentDir, siblingRoot) {
119794
120428
  if (basename10(agentDir) !== "agent") {
119795
120429
  return void 0;
119796
120430
  }
119797
- return join33(dirname13(dirname13(agentDir)), siblingRoot, "agent");
120431
+ return join33(dirname14(dirname14(agentDir)), siblingRoot, "agent");
119798
120432
  }
119799
120433
  function createReadOnlyPiSettingsView(cwd, agentDir) {
119800
120434
  const projectRoot = resolvePiExtensionProjectRoot(cwd);
@@ -119838,7 +120472,7 @@ function resolveVendoredClaudeCliEntry() {
119838
120472
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
119839
120473
  const entry = extensions[0];
119840
120474
  if (typeof entry !== "string" || entry.length === 0) return null;
119841
- const path6 = resolve16(dirname13(pkgJsonPath), entry);
120475
+ const path6 = resolve16(dirname14(pkgJsonPath), entry);
119842
120476
  return existsSync27(path6) ? path6 : null;
119843
120477
  } catch {
119844
120478
  return null;
@@ -119853,7 +120487,7 @@ function resolveVendoredDroidCliEntry() {
119853
120487
  if (!Array.isArray(extensions) || extensions.length === 0) return null;
119854
120488
  const entry = extensions[0];
119855
120489
  if (typeof entry !== "string" || entry.length === 0) return null;
119856
- const path6 = resolve16(dirname13(pkgJsonPath), entry);
120490
+ const path6 = resolve16(dirname14(pkgJsonPath), entry);
119857
120491
  return existsSync27(path6) ? path6 : null;
119858
120492
  } catch {
119859
120493
  return null;
@@ -120237,7 +120871,7 @@ async function createFnAgent2(options) {
120237
120871
  logMcpForwardingSkipped({ runtimeId: "pi", provider: options.defaultProvider, skippedCount: requestedMcpServers.length, lane: "createFnAgent" });
120238
120872
  }
120239
120873
  const authStorage = createFusionAuthStorage();
120240
- const modelRegistry = ModelRegistry.create(authStorage, getModelRegistryModelsPath());
120874
+ const modelRegistry = ModelRegistry2.create(authStorage, getModelRegistryModelsPath());
120241
120875
  const resolvedProjectRoot = getProjectRootFromWorktree(options.cwd) ?? resolvePiExtensionProjectRoot(options.cwd);
120242
120876
  await registerExtensionProviders(resolvedProjectRoot, modelRegistry);
120243
120877
  const customProviders = readCustomProviders();
@@ -120499,8 +121133,25 @@ async function createFnAgent2(options) {
120499
121133
  throw error2;
120500
121134
  }
120501
121135
  };
121136
+ const modelDescription = (model) => model ? `${model.provider}/${model.id}` : "unknown model";
121137
+ const configuredFallbackDiffers = Boolean(
121138
+ options.fallbackProvider && options.fallbackModelId && (options.fallbackProvider !== options.defaultProvider || options.fallbackModelId !== options.defaultModelId)
121139
+ );
121140
+ const hasDistinctFallback = Boolean(
121141
+ selectedModel && fallbackModel && (configuredFallbackDiffers || selectedModel.provider !== fallbackModel.provider || selectedModel.id !== fallbackModel.id)
121142
+ );
121143
+ const makeFallbackExhaustedError = (triggerPoint, attempts, underlying) => {
121144
+ const underlyingReason = underlying instanceof Error ? underlying.message : String(underlying);
121145
+ return new ModelFallbackExhaustedError({
121146
+ primaryModel: modelDescription(selectedModel),
121147
+ fallbackModel: hasDistinctFallback ? modelDescription(fallbackModel) : void 0,
121148
+ triggerPoint,
121149
+ attempts,
121150
+ underlyingReason
121151
+ });
121152
+ };
120502
121153
  const emitFallbackUsed = async (triggerPoint) => {
120503
- if (!options.onFallbackModelUsed || !selectedModel || !fallbackModel) {
121154
+ if (!options.onFallbackModelUsed || !selectedModel || !fallbackModel || !hasDistinctFallback) {
120504
121155
  return;
120505
121156
  }
120506
121157
  await options.onFallbackModelUsed({
@@ -120518,13 +121169,17 @@ async function createFnAgent2(options) {
120518
121169
  sessionResult = await createSessionWithModel(selectedModel);
120519
121170
  piLog.log(`Session created successfully (model=${selectedModel ? `${selectedModel.provider}/${selectedModel.id}` : "default"})`);
120520
121171
  } catch (err) {
120521
- if (!fallbackModel || !selectedModel || !isRetryableModelSelectionError(err?.message || "")) {
121172
+ if (!fallbackModel || !selectedModel || !hasDistinctFallback || !isRetryableModelSelectionError(err?.message || "")) {
120522
121173
  piLog.error(`Session creation failed: ${err.message}`);
120523
121174
  throw err;
120524
121175
  }
120525
121176
  piLog.warn(`Primary model failed (${err.message}), trying fallback`);
120526
121177
  usingFallback = true;
120527
- sessionResult = await createSessionWithModel(fallbackModel);
121178
+ try {
121179
+ sessionResult = await createSessionWithModel(fallbackModel);
121180
+ } catch (fallbackErr) {
121181
+ throw makeFallbackExhaustedError("session-creation", 2, fallbackErr);
121182
+ }
120528
121183
  await emitFallbackUsed("session-creation");
120529
121184
  piLog.log("Fallback session created successfully");
120530
121185
  }
@@ -120652,6 +121307,9 @@ async function createFnAgent2(options) {
120652
121307
  if (!fallbackModel || usingFallback || !isRetryableModelSelectionError(errorMessage2)) {
120653
121308
  throw err;
120654
121309
  }
121310
+ if (!hasDistinctFallback) {
121311
+ throw makeFallbackExhaustedError("prompt-time", 1, err);
121312
+ }
120655
121313
  usingFallback = true;
120656
121314
  const fallbackSession = await swapPromptSession(fallbackModel);
120657
121315
  await emitFallbackUsed("prompt-time");
@@ -120699,7 +121357,7 @@ async function createFnAgent2(options) {
120699
121357
  throw fallbackErr;
120700
121358
  }
120701
121359
  }
120702
- throw fallbackErr;
121360
+ throw makeFallbackExhaustedError("prompt-time", 2, fallbackErr);
120703
121361
  }
120704
121362
  }
120705
121363
  };
@@ -120726,7 +121384,7 @@ async function createFnAgent2(options) {
120726
121384
  });
120727
121385
  return { session: promptableSession, sessionFile: promptableSession.sessionFile };
120728
121386
  }
120729
- var execAsync, RTK_ACCEPTED_REWRITE_EXIT_CODES, RTK_EXPECTED_PASSTHROUGH_EXIT_CODES, RTK_EXPECTED_FAIL_OPEN_ERROR_CODES, RTK_REWRITE_MAX_BUFFER_BYTES, hostExtensionPaths, FN_MEMORY_APPEND_TOOL_NAME, FUSION_SHUTDOWN_WRAP_FLAG, promptWithFallbackInFlight, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS, MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS, MAX_COMPACTED_ATTACHMENTS_CHARS, MAX_COMPACTED_EXISTING_SPEC_CHARS, MAX_COMPACTED_TASK_PROMPT_CHARS, MAX_COMPACTED_USER_COMMENTS_CHARS, GATE_BYPASS_TOOL_NAMES;
121387
+ var execAsync, RTK_ACCEPTED_REWRITE_EXIT_CODES, RTK_EXPECTED_PASSTHROUGH_EXIT_CODES, RTK_EXPECTED_FAIL_OPEN_ERROR_CODES, RTK_REWRITE_MAX_BUFFER_BYTES, hostExtensionPaths, FN_MEMORY_APPEND_TOOL_NAME, FUSION_SHUTDOWN_WRAP_FLAG, promptWithFallbackInFlight, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS, MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS, MAX_COMPACTED_ATTACHMENTS_CHARS, MAX_COMPACTED_EXISTING_SPEC_CHARS, MAX_COMPACTED_TASK_PROMPT_CHARS, MAX_COMPACTED_USER_COMMENTS_CHARS, ModelFallbackExhaustedError, GATE_BYPASS_TOOL_NAMES;
120730
121388
  var init_pi = __esm({
120731
121389
  "../engine/src/pi.ts"() {
120732
121390
  "use strict";
@@ -120766,6 +121424,25 @@ var init_pi = __esm({
120766
121424
  MAX_COMPACTED_EXISTING_SPEC_CHARS = 4e3;
120767
121425
  MAX_COMPACTED_TASK_PROMPT_CHARS = MAX_COMPACTED_EXISTING_SPEC_CHARS;
120768
121426
  MAX_COMPACTED_USER_COMMENTS_CHARS = 2e3;
121427
+ ModelFallbackExhaustedError = class extends Error {
121428
+ primaryModel;
121429
+ fallbackModel;
121430
+ triggerPoint;
121431
+ attempts;
121432
+ underlyingReason;
121433
+ constructor(input) {
121434
+ const fallbackClause = input.fallbackModel ? `, fallback ${input.fallbackModel}` : ", no fallback configured";
121435
+ super(
121436
+ `Unable to select a usable model after ${input.attempts} attempt${input.attempts === 1 ? "" : "s"} (primary ${input.primaryModel}${fallbackClause}, trigger: ${input.triggerPoint}): ${input.underlyingReason}`
121437
+ );
121438
+ this.name = "ModelFallbackExhaustedError";
121439
+ this.primaryModel = input.primaryModel;
121440
+ this.fallbackModel = input.fallbackModel;
121441
+ this.triggerPoint = input.triggerPoint;
121442
+ this.attempts = input.attempts;
121443
+ this.underlyingReason = input.underlyingReason;
121444
+ }
121445
+ };
120769
121446
  GATE_BYPASS_TOOL_NAMES = /* @__PURE__ */ new Set([
120770
121447
  "fn_heartbeat_done",
120771
121448
  "fn_send_message",
@@ -122237,11 +122914,17 @@ function normalize6(value) {
122237
122914
  }
122238
122915
  return value.trim().replace(/^refs\/heads\//, "").replace(/^refs\/remotes\/origin\//, "").replace(/^origin\//, "");
122239
122916
  }
122240
- function warnFallback(rootDir, logger3) {
122917
+ function warnFallback(rootDir, logger3, remotes = []) {
122241
122918
  if (warnedFallbackRootDirs.has(rootDir)) {
122242
122919
  return;
122243
122920
  }
122244
122921
  warnedFallbackRootDirs.add(rootDir);
122922
+ if (remotes.length > 0) {
122923
+ const remoteList = remotes.join(", ");
122924
+ const originState = remotes.includes("origin") ? "origin/HEAD is unset" : "origin is absent";
122925
+ logger3.warn(`[integration-branch] falling back to 'main' \u2014 auto-detect checks origin/HEAD, but ${originState}; found remote ${remoteList}. Add an origin alias or set integrationBranch manually.`);
122926
+ return;
122927
+ }
122245
122928
  logger3.warn("[integration-branch] falling back to 'main' \u2014 origin/HEAD unset and no project override");
122246
122929
  }
122247
122930
  function resolveFromSettings(settings) {
@@ -122278,6 +122961,36 @@ function resolveFromOriginHeadSync(rootDir) {
122278
122961
  return "";
122279
122962
  }
122280
122963
  }
122964
+ function parseRemotes(stdout) {
122965
+ return [...new Set(stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean))];
122966
+ }
122967
+ async function listGitRemotes(rootDir) {
122968
+ try {
122969
+ const { stdout } = await execAsync2("git remote", {
122970
+ cwd: rootDir,
122971
+ encoding: "utf8",
122972
+ timeout: 5e3,
122973
+ maxBuffer: 1024 * 1024
122974
+ });
122975
+ return parseRemotes(stdout);
122976
+ } catch {
122977
+ return [];
122978
+ }
122979
+ }
122980
+ function listGitRemotesSync(rootDir) {
122981
+ try {
122982
+ const stdout = execSync("git remote", {
122983
+ cwd: rootDir,
122984
+ encoding: "utf8",
122985
+ timeout: 5e3,
122986
+ maxBuffer: 1024 * 1024,
122987
+ stdio: ["ignore", "pipe", "ignore"]
122988
+ });
122989
+ return parseRemotes(stdout);
122990
+ } catch {
122991
+ return [];
122992
+ }
122993
+ }
122281
122994
  async function resolveIntegrationBranch(rootDir, settings, opts = {}) {
122282
122995
  const logger3 = opts.logger ?? console;
122283
122996
  const fromSettings = resolveFromSettings(settings);
@@ -122288,7 +123001,8 @@ async function resolveIntegrationBranch(rootDir, settings, opts = {}) {
122288
123001
  if (fromOrigin.length > 0) {
122289
123002
  return fromOrigin;
122290
123003
  }
122291
- warnFallback(rootDir, logger3);
123004
+ const remotes = await listGitRemotes(rootDir);
123005
+ warnFallback(rootDir, logger3, remotes);
122292
123006
  return INTEGRATION_BRANCH_FALLBACK;
122293
123007
  }
122294
123008
  function resolveIntegrationBranchSync(rootDir, settings, opts = {}) {
@@ -122301,7 +123015,8 @@ function resolveIntegrationBranchSync(rootDir, settings, opts = {}) {
122301
123015
  if (fromOrigin.length > 0) {
122302
123016
  return fromOrigin;
122303
123017
  }
122304
- warnFallback(rootDir, logger3);
123018
+ const remotes = listGitRemotesSync(rootDir);
123019
+ warnFallback(rootDir, logger3, remotes);
122305
123020
  return INTEGRATION_BRANCH_FALLBACK;
122306
123021
  }
122307
123022
  function __resetIntegrationBranchCacheForTests() {
@@ -123543,7 +124258,7 @@ var init_active_session_registry = __esm({
123543
124258
  // ../engine/src/worktree-hooks.ts
123544
124259
  import { exec as exec6 } from "node:child_process";
123545
124260
  import * as fs4 from "node:fs/promises";
123546
- import { dirname as dirname14, resolve as resolve18 } from "node:path";
124261
+ import { dirname as dirname15, resolve as resolve18 } from "node:path";
123547
124262
  import { promisify as promisify8 } from "node:util";
123548
124263
  function toShellCasePattern(pattern) {
123549
124264
  return pattern.replace(/^\^/, "").replace(/\$$/, "").replace(/\\d\+/g, "[0-9]*").replace(/\[a-z0-9-\]\+/g, "[a-z0-9-]*");
@@ -123733,7 +124448,7 @@ git interpret-trailers --in-place --if-exists doNothing --trailer "$TRAILE
123733
124448
  `;
123734
124449
  }
123735
124450
  async function writeFileAtomic(targetPath, content, mode) {
123736
- await fs4.mkdir(dirname14(targetPath), { recursive: true });
124451
+ await fs4.mkdir(dirname15(targetPath), { recursive: true });
123737
124452
  const tmpPath = `${targetPath}.tmp`;
123738
124453
  const current = await fs4.readFile(targetPath, "utf-8").catch(() => null);
123739
124454
  if (current === content) return;
@@ -123889,7 +124604,7 @@ var init_worktree_prune = __esm({
123889
124604
  // ../engine/src/worktree-stale-lock.ts
123890
124605
  import { exec as exec8 } from "node:child_process";
123891
124606
  import { readFile as readFile16, stat as stat7, unlink as unlink6 } from "node:fs/promises";
123892
- import { dirname as dirname15, resolve as resolve19 } from "node:path";
124607
+ import { dirname as dirname16, resolve as resolve19 } from "node:path";
123893
124608
  import { promisify as promisify10 } from "node:util";
123894
124609
  function parseIndexLockPath(stderr) {
123895
124610
  const match = /unable to create ['"]([^'"]*index\.lock)['"]:\s*File exists/i.exec(stderr);
@@ -123911,15 +124626,15 @@ async function resolveOwningWorktreePath(input) {
123911
124626
  }
123912
124627
  const worktreeName = parseWorktreeNameFromLockPath(input.lockPath);
123913
124628
  if (!worktreeName) return void 0;
123914
- const gitdirFile = resolve19(dirname15(input.lockPath), "gitdir");
124629
+ const gitdirFile = resolve19(dirname16(input.lockPath), "gitdir");
123915
124630
  try {
123916
124631
  const gitdirRef = (await readFile16(gitdirFile, "utf-8")).trim();
123917
124632
  if (gitdirRef) {
123918
- const resolvedGitdirRef = resolve19(dirname15(gitdirFile), gitdirRef);
124633
+ const resolvedGitdirRef = resolve19(dirname16(gitdirFile), gitdirRef);
123919
124634
  if (resolvedGitdirRef.endsWith("/.git") || resolvedGitdirRef.endsWith("\\.git")) {
123920
- return dirname15(resolvedGitdirRef);
124635
+ return dirname16(resolvedGitdirRef);
123921
124636
  }
123922
- return dirname15(resolvedGitdirRef);
124637
+ return dirname16(resolvedGitdirRef);
123923
124638
  }
123924
124639
  } catch {
123925
124640
  }
@@ -125207,7 +125922,7 @@ __export(worktree_pool_exports, {
125207
125922
  import { exec as exec11 } from "node:child_process";
125208
125923
  import { promisify as promisify13 } from "node:util";
125209
125924
  import { existsSync as existsSync31, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync3, realpathSync as realpathSync6 } from "node:fs";
125210
- import { basename as basename12, dirname as dirname16, join as join35, relative as relative6, resolve as resolve22, isAbsolute as isAbsolute9 } from "node:path";
125925
+ import { basename as basename12, dirname as dirname17, join as join35, relative as relative6, resolve as resolve22, isAbsolute as isAbsolute9 } from "node:path";
125211
125926
  async function getWorktrunkBinary(settings) {
125212
125927
  const cacheKey = `${process.env.HOME ?? ""}::${settings.binaryPath ?? ""}`;
125213
125928
  const cached2 = _worktrunkBinaryCache.get(cacheKey);
@@ -125498,7 +126213,7 @@ function dotGitPointerIsDangling(dotGitPath) {
125498
126213
  const match = /^gitdir:\s*(.+)$/.exec(raw);
125499
126214
  if (!match) return false;
125500
126215
  const target = match[1].trim();
125501
- const resolved = isAbsolute9(target) ? target : resolve22(dirname16(dotGitPath), target);
126216
+ const resolved = isAbsolute9(target) ? target : resolve22(dirname17(dotGitPath), target);
125502
126217
  return !existsSync31(resolved);
125503
126218
  } catch {
125504
126219
  return false;
@@ -126399,7 +127114,7 @@ var init_worktrunk_failure_handler = __esm({
126399
127114
  // ../engine/src/worktree-copy-files.ts
126400
127115
  import { constants as fsConstants2 } from "node:fs";
126401
127116
  import { access as access5, copyFile as copyFile2, lstat, mkdir as mkdir13 } from "node:fs/promises";
126402
- import { dirname as dirname17, isAbsolute as isAbsolute11, relative as relative8, resolve as resolve24 } from "node:path";
127117
+ import { dirname as dirname18, isAbsolute as isAbsolute11, relative as relative8, resolve as resolve24 } from "node:path";
126403
127118
  function isInsideRoot(rootDir, candidate) {
126404
127119
  const rel = relative8(rootDir, candidate);
126405
127120
  return rel === "" || !rel.startsWith("..") && !isAbsolute11(rel);
@@ -126480,7 +127195,7 @@ async function copyConfiguredWorktreeFiles(options) {
126480
127195
  continue;
126481
127196
  }
126482
127197
  try {
126483
- await mkdir13(dirname17(destinationPath), { recursive: true });
127198
+ await mkdir13(dirname18(destinationPath), { recursive: true });
126484
127199
  await copyFile2(sourcePath, destinationPath);
126485
127200
  const result = { path: trimmed, sourcePath, destinationPath, outcome: "copied" };
126486
127201
  logger3?.log?.(`${taskId ?? "worktree"}: copied configured worktree file ${trimmed}`);
@@ -132327,7 +133042,7 @@ import {
132327
133042
  rmSync as rmSync4,
132328
133043
  writeFileSync as writeFileSync3
132329
133044
  } from "node:fs";
132330
- import { dirname as dirname18 } from "node:path";
133045
+ import { dirname as dirname19 } from "node:path";
132331
133046
  function sanitizeState(parsed) {
132332
133047
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
132333
133048
  return {};
@@ -132420,7 +133135,7 @@ var init_oauth_alert_state = __esm({
132420
133135
  }
132421
133136
  writeState(state) {
132422
133137
  const sanitized = sanitizeState(state);
132423
- const dir = dirname18(this.statePath);
133138
+ const dir = dirname19(this.statePath);
132424
133139
  this.fs.mkdirSync(dir, { recursive: true });
132425
133140
  const tempPath = `${this.statePath}.${process.pid}.${this.clock()}.tmp`;
132426
133141
  const body = `${JSON.stringify(sanitized, null, 2)}
@@ -134940,7 +135655,7 @@ var init_agent_memory_index = __esm({
134940
135655
  // ../engine/src/agent-instructions.ts
134941
135656
  import { readFile as readFile20, writeFile as writeFile14, mkdir as mkdir16, access as access7 } from "node:fs/promises";
134942
135657
  import { constants as fsConstants3 } from "node:fs";
134943
- import { isAbsolute as isAbsolute12, resolve as resolve26, relative as relative10, normalize as normalize7, sep as sep6, dirname as dirname19 } from "node:path";
135658
+ import { isAbsolute as isAbsolute12, resolve as resolve26, relative as relative10, normalize as normalize7, sep as sep6, dirname as dirname20 } from "node:path";
134944
135659
  function trimAndClamp(value, maxLength, label, agentId) {
134945
135660
  const trimmed = value.trim();
134946
135661
  if (!trimmed) {
@@ -135029,7 +135744,7 @@ async function ensureDefaultHeartbeatProcedureFile(rootDir, procedurePathRel, de
135029
135744
  } catch {
135030
135745
  }
135031
135746
  try {
135032
- await mkdir16(dirname19(filePath), { recursive: true });
135747
+ await mkdir16(dirname20(filePath), { recursive: true });
135033
135748
  await writeFile14(filePath, defaultContent, "utf-8");
135034
135749
  log17.log(`Seeded default heartbeat procedure file at ${filePath}`);
135035
135750
  return filePath;
@@ -140211,7 +140926,7 @@ var init_bubblewrap_detect = __esm({
140211
140926
  });
140212
140927
 
140213
140928
  // ../engine/src/sandbox/bubblewrap-policy.ts
140214
- import { dirname as dirname20, resolve as resolve27 } from "node:path";
140929
+ import { dirname as dirname21, resolve as resolve27 } from "node:path";
140215
140930
  function shouldPassEnv(key) {
140216
140931
  return key === "PATH" || key === "HOME" || key === "USER" || key === "LANG" || key.startsWith("LC_") || key.startsWith("NODE_") || key.startsWith("npm_") || key.startsWith("PNPM_") || key === "CI" || key.startsWith("FUSION_");
140217
140932
  }
@@ -140250,7 +140965,7 @@ function policyToBwrapArgs(policy, ctx) {
140250
140965
  if (!pathExists(path6)) continue;
140251
140966
  args.push("--bind", path6, path6);
140252
140967
  }
140253
- const nodeInstallDir = dirname20(ctx.nodeBinPath);
140968
+ const nodeInstallDir = dirname21(ctx.nodeBinPath);
140254
140969
  const readonlyPaths = uniq([
140255
140970
  ...(policy.allowedReadPaths ?? []).length ? policy.allowedReadPaths ?? [] : [ctx.repoRootPath],
140256
140971
  ...ctx.repoRootPath !== ctx.worktreePath ? [ctx.repoRootPath] : [],
@@ -141014,7 +141729,7 @@ var init_sandbox_exec_profile_templates = __esm({
141014
141729
  });
141015
141730
 
141016
141731
  // ../engine/src/sandbox/sandbox-exec-policy.ts
141017
- import { dirname as dirname21, resolve as resolve28 } from "node:path";
141732
+ import { dirname as dirname22, resolve as resolve28 } from "node:path";
141018
141733
  function uniq2(paths) {
141019
141734
  return [...new Set(paths.map((p2) => resolve28(p2)))];
141020
141735
  }
@@ -141054,7 +141769,7 @@ function policyToSbplProfile(policy, ctx) {
141054
141769
  throw new SandboxPolicyError2("Port 4040 is reserved and cannot be allowed in sandbox policy.");
141055
141770
  }
141056
141771
  const tmpDir = resolve28(ctx.tmpDirOverride ?? "/private/tmp");
141057
- const nodeDir = dirname21(ctx.nodeBinPath);
141772
+ const nodeDir = dirname22(ctx.nodeBinPath);
141058
141773
  const writePaths = uniq2([ctx.worktreePath, ctx.pnpmStorePath, ...policy.allowedWritePaths ?? []]);
141059
141774
  ensureNoFusionWrites(writePaths, ctx.repoRootPath);
141060
141775
  const readPaths = uniq2([
@@ -141775,6 +142490,22 @@ function throwIfDependencySyncAborted(signal, taskId) {
141775
142490
  err.name = "AbortError";
141776
142491
  throw err;
141777
142492
  }
142493
+ function extractCommandErrorDetails(error2) {
142494
+ const maybeCommandError = error2;
142495
+ return String(maybeCommandError.stderr || maybeCommandError.stdout || maybeCommandError.message || error2);
142496
+ }
142497
+ function isOutdatedLockfileError(details) {
142498
+ return /ERR_PNPM_OUTDATED_LOCKFILE|OUTDATED_LOCKFILE|frozen-lockfile|lockfile is frozen|lockfile had changes, but lockfile is frozen|lockfile needs to be updated|Your lockfile needs to be updated/i.test(
142499
+ details
142500
+ );
142501
+ }
142502
+ function buildNonFrozenRetryCommand(installCommand) {
142503
+ if (!installCommand.includes("--frozen-lockfile")) return null;
142504
+ if (/^\s*pnpm\b/.test(installCommand)) {
142505
+ return installCommand.replace(/--frozen-lockfile/g, "--no-frozen-lockfile");
142506
+ }
142507
+ return installCommand.replace(/\s*--frozen-lockfile/g, "").trim();
142508
+ }
141778
142509
  async function installWorktreeDependencies(options) {
141779
142510
  const { cwd, settings, taskId, signal, log: log27, logger: logger3, context = "merge worktree dependency sync" } = options;
141780
142511
  const startedAt = Date.now();
@@ -141782,7 +142513,7 @@ async function installWorktreeDependencies(options) {
141782
142513
  const installCommand = getDependencySyncCommand(cwd, settings);
141783
142514
  const configured = configuredCommand !== null;
141784
142515
  if (!installCommand) {
141785
- return { installCommand: null, configured: false, skipped: true, skipReason: "no-command", durationMs: Date.now() - startedAt };
142516
+ return { installCommand: null, configured: false, skipped: true, skipReason: "no-command", healed: false, durationMs: Date.now() - startedAt };
141786
142517
  }
141787
142518
  const shouldUseInstallMarker = !configured;
141788
142519
  const lockHash = shouldUseInstallMarker ? computeLockfileHash(cwd) : null;
@@ -141794,27 +142525,45 @@ async function installWorktreeDependencies(options) {
141794
142525
  configured,
141795
142526
  skipped: true,
141796
142527
  skipReason: "lockfile-marker-match",
142528
+ healed: false,
141797
142529
  durationMs: Date.now() - startedAt
141798
142530
  };
141799
142531
  }
141800
142532
  throwIfDependencySyncAborted(signal, taskId);
141801
142533
  logger3?.log?.(`${taskId}: syncing dependencies ${context}`);
141802
142534
  await log27?.(`Syncing dependencies ${context}: ${installCommand}`);
142535
+ const runInstall = (command) => execAsync21(command, {
142536
+ cwd,
142537
+ encoding: "utf-8",
142538
+ maxBuffer: 10 * 1024 * 1024,
142539
+ timeout: INSTALL_TIMEOUT_MS
142540
+ });
141803
142541
  try {
141804
- await execAsync21(installCommand, {
141805
- cwd,
141806
- encoding: "utf-8",
141807
- maxBuffer: 10 * 1024 * 1024,
141808
- timeout: INSTALL_TIMEOUT_MS
141809
- });
142542
+ await runInstall(installCommand);
141810
142543
  throwIfDependencySyncAborted(signal, taskId);
141811
142544
  if (lockHash) writeInstallMarker(cwd, lockHash);
141812
- return { installCommand, configured, skipped: false, durationMs: Date.now() - startedAt };
142545
+ return { installCommand, configured, skipped: false, healed: false, durationMs: Date.now() - startedAt };
141813
142546
  } catch (error2) {
141814
142547
  throwIfDependencySyncAborted(signal, taskId);
141815
- const maybeCommandError = error2;
141816
- const details = maybeCommandError.stderr || maybeCommandError.stdout || maybeCommandError.message || String(error2);
141817
- throw new Error(`Dependency sync failed for ${taskId}: ${String(details)}`.trim());
142548
+ const details = extractCommandErrorDetails(error2);
142549
+ const retryCommand = configured ? null : buildNonFrozenRetryCommand(installCommand);
142550
+ if (retryCommand && isOutdatedLockfileError(details)) {
142551
+ logger3?.log?.(`${taskId}: lockfile out of date; retrying dependency sync without frozen lockfile`);
142552
+ await log27?.(`Dependency sync hit an outdated lockfile; retrying without frozen lockfile: ${retryCommand}`);
142553
+ try {
142554
+ await runInstall(retryCommand);
142555
+ } catch (retryError) {
142556
+ throwIfDependencySyncAborted(signal, taskId);
142557
+ throw new Error(
142558
+ `Dependency sync failed for ${taskId} (after non-frozen retry): ${extractCommandErrorDetails(retryError)}`.trim()
142559
+ );
142560
+ }
142561
+ throwIfDependencySyncAborted(signal, taskId);
142562
+ const healedHash = computeLockfileHash(cwd);
142563
+ if (healedHash) writeInstallMarker(cwd, healedHash);
142564
+ return { installCommand, configured, skipped: false, healed: true, healedCommand: retryCommand, durationMs: Date.now() - startedAt };
142565
+ }
142566
+ throw new Error(`Dependency sync failed for ${taskId}: ${details}`.trim());
141818
142567
  }
141819
142568
  }
141820
142569
  var execAsync21, INSTALL_MARKER_RELPATH, LOCKFILE_CANDIDATES, INSTALL_TIMEOUT_MS;
@@ -143954,7 +144703,7 @@ var init_merger_ref_update_advance = __esm({
143954
144703
  import { execFile as execFile14, spawn as spawn7 } from "node:child_process";
143955
144704
  import { promisify as promisify36 } from "node:util";
143956
144705
  import { copyFileSync, existsSync as existsSync41, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
143957
- import { dirname as dirname22, join as join45 } from "node:path";
144706
+ import { dirname as dirname23, join as join45 } from "node:path";
143958
144707
  import { tmpdir as tmpdir4 } from "node:os";
143959
144708
  async function runGit4(args, cwd, timeoutMs) {
143960
144709
  const result = await execFileAsync9("git", ["-c", "core.quotePath=false", ...args], {
@@ -144065,7 +144814,7 @@ async function syncWorktreeToHead(input) {
144065
144814
  const src = join45(worktreePath, rel);
144066
144815
  const dst = join45(untrackedDir, rel);
144067
144816
  try {
144068
- mkdirSync10(dirname22(dst), { recursive: true });
144817
+ mkdirSync10(dirname23(dst), { recursive: true });
144069
144818
  copyFileSync(src, dst);
144070
144819
  } catch {
144071
144820
  }
@@ -144134,7 +144883,7 @@ async function syncWorktreeToHead(input) {
144134
144883
  }
144135
144884
  const dst = join45(worktreePath, rel);
144136
144885
  try {
144137
- mkdirSync10(dirname22(dst), { recursive: true });
144886
+ mkdirSync10(dirname23(dst), { recursive: true });
144138
144887
  const data = readFileSync15(src);
144139
144888
  writeFileSync5(dst, data);
144140
144889
  restored.push(rel);
@@ -144484,7 +145233,7 @@ import * as childProcess3 from "node:child_process";
144484
145233
  import { promisify as promisify38 } from "node:util";
144485
145234
  import { existsSync as existsSync42, readFileSync as readFileSync16, readdirSync as readdirSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2, renameSync as renameSync5 } from "node:fs";
144486
145235
  import { createHash as createHash12 } from "node:crypto";
144487
- import { join as join47, dirname as dirname23, basename as basename14 } from "node:path";
145236
+ import { join as join47, dirname as dirname24, basename as basename14 } from "node:path";
144488
145237
  import { hostname as hostname3 } from "node:os";
144489
145238
  import { Type as Type3 } from "typebox";
144490
145239
  function mergerCommitEnv() {
@@ -145075,7 +145824,7 @@ function deriveFileScopedPnpmTestCommand(rootDir, baseBranch, branch) {
145075
145824
  resolvedTests.add(file2);
145076
145825
  continue;
145077
145826
  }
145078
- const dir = dirname23(file2);
145827
+ const dir = dirname24(file2);
145079
145828
  const stem = basename14(file2).replace(/\.(ts|tsx|js|jsx)$/, "");
145080
145829
  if (!stem) continue;
145081
145830
  const candidates = [
@@ -153327,8 +154076,8 @@ function optionalStepRevisionLogOutcome(details, key) {
153327
154076
  return `${details}
153328
154077
  ${OPTIONAL_STEP_REVISION_KEY_MARKER} ${key}`;
153329
154078
  }
153330
- function extractTaskIdFromTempMergeDir(dirname30) {
153331
- const match = /^fusion-ai-merge-(fn-\d+)-[a-z0-9]+$/i.exec(dirname30);
154079
+ function extractTaskIdFromTempMergeDir(dirname31) {
154080
+ const match = /^fusion-ai-merge-(fn-\d+)-[a-z0-9]+$/i.exec(dirname31);
153332
154081
  return match?.[1]?.toUpperCase() ?? null;
153333
154082
  }
153334
154083
  function resolveRepoLocalAiMergeRoot(rootDir, settings) {
@@ -164847,6 +165596,24 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
164847
165596
  task.id,
164848
165597
  errorMessage2
164849
165598
  );
165599
+ } else if (err instanceof ModelFallbackExhaustedError) {
165600
+ const failureMessage = `Triage failed: unable to select a usable model after ${err.attempts} attempt${err.attempts === 1 ? "" : "s"}. ${err.message}`;
165601
+ planLog.error(`\u2717 ${task.id} planner model fallback exhausted: ${failureMessage}`);
165602
+ await this.store.logEntry(task.id, failureMessage).catch((logErr) => {
165603
+ const msg = logErr instanceof Error ? logErr.message : String(logErr);
165604
+ planLog.warn(`${task.id}: failed to log planner fallback exhaustion: ${msg}`);
165605
+ });
165606
+ await this.store.updateTask(task.id, {
165607
+ status: "failed",
165608
+ error: failureMessage,
165609
+ recoveryRetryCount: null,
165610
+ nextRecoveryAt: null
165611
+ }).catch((updateErr) => {
165612
+ const msg = updateErr instanceof Error ? updateErr.message : String(updateErr);
165613
+ planLog.warn(`${task.id}: failed to persist planner fallback exhaustion: ${msg}`);
165614
+ });
165615
+ this.options.onSpecifyError?.(task, err);
165616
+ return;
164850
165617
  } else if (isTransientError(errorMessage2)) {
164851
165618
  const decision = computeRecoveryDecision({
164852
165619
  recoveryRetryCount: task.recoveryRetryCount,
@@ -170241,7 +171008,7 @@ var init_task_session = __esm({
170241
171008
  // ../engine/src/pty-native.ts
170242
171009
  import * as fs6 from "node:fs";
170243
171010
  import { createRequire as createRequire6 } from "node:module";
170244
- import { join as join53, dirname as dirname24 } from "node:path";
171011
+ import { join as join53, dirname as dirname25 } from "node:path";
170245
171012
  function getNativePrebuildName() {
170246
171013
  const platform4 = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "win32" : "unknown";
170247
171014
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
@@ -170250,7 +171017,7 @@ function getNativePrebuildName() {
170250
171017
  function findInstalledNodePtyNativeDir() {
170251
171018
  try {
170252
171019
  const packageJsonPath = require3.resolve("node-pty/package.json");
170253
- const pkgRoot = dirname24(packageJsonPath);
171020
+ const pkgRoot = dirname25(packageJsonPath);
170254
171021
  const releaseDir = join53(pkgRoot, "build", "Release");
170255
171022
  if (fs6.existsSync(join53(releaseDir, "pty.node"))) {
170256
171023
  return releaseDir;
@@ -170272,7 +171039,7 @@ function findStagedNativeDir() {
170272
171039
  return envPath;
170273
171040
  }
170274
171041
  }
170275
- const execDir = dirname24(process.execPath);
171042
+ const execDir = dirname25(process.execPath);
170276
171043
  const nextToBinary = join53(execDir, "runtime", prebuildName);
170277
171044
  if (fs6.existsSync(join53(nextToBinary, "pty.node"))) {
170278
171045
  return nextToBinary;
@@ -172296,7 +173063,7 @@ import { delimiter, isAbsolute as isAbsolute16, join as join55, relative as rela
172296
173063
  import { existsSync as existsSync48, lstatSync as lstatSync2, realpathSync as realpathSync9 } from "node:fs";
172297
173064
  import { readFile as readFile24, rm as rm9, writeFile as writeFile19 } from "node:fs/promises";
172298
173065
  import { Type as Type6 } from "@earendil-works/pi-ai";
172299
- import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
173066
+ import { ModelRegistry as ModelRegistry3, SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
172300
173067
  function isAgentBrowserNotFoundError(error2) {
172301
173068
  const err = error2;
172302
173069
  const code = typeof err?.code === "string" || typeof err?.code === "number" ? String(err.code) : void 0;
@@ -174063,20 +174830,30 @@ The tool prevents your session from being killed by the inactivity watchdog duri
174063
174830
  /** Set of ephemeral spawned agent IDs with in-flight cleanup (prevents duplicate deletion attempts). */
174064
174831
  pendingEphemeralDeletions = /* @__PURE__ */ new Set();
174065
174832
  workspaceConfig = void 0;
174833
+ /*
174834
+ FNXC:WorkflowLifecycle 2026-07-01-16:20:
174835
+ Breadcrumb task-log writes on the abort/pause/finalize paths are best-effort diagnostics and must NEVER break control flow. FN-7335 wired store.logEntry() straight into the SYNCHRONOUS markPausedAborted() as `void this.store.logEntry(...).catch(...)`; when store.logEntry is absent/throws synchronously (undefined method, store closed mid-abort, corrupted pager) the call throws a TypeError BEFORE the promise exists, so the trailing .catch() never runs and the exception unwinds out of markPausedAborted — aborting hard-cancel/pause and stranding the in-review handoff. Route every breadcrumb write through safeLogEntry() so both synchronous throws and async rejections are swallowed into a warn.
174836
+ */
174837
+ safeLogEntry(taskId, message) {
174838
+ try {
174839
+ const result = this.store.logEntry(taskId, message, void 0, this.getRunContextFor(taskId));
174840
+ void Promise.resolve(result).catch((error2) => {
174841
+ executorLog.warn(`${taskId}: failed to write task-log breadcrumb: ${error2 instanceof Error ? error2.message : String(error2)}`);
174842
+ });
174843
+ } catch (error2) {
174844
+ executorLog.warn(`${taskId}: failed to write task-log breadcrumb: ${error2 instanceof Error ? error2.message : String(error2)}`);
174845
+ }
174846
+ }
174066
174847
  markPausedAborted(taskId, provenance = "hard-cancel", source = "unspecified") {
174067
174848
  const previousProvenance = this.pausedAbortProvenance.get(taskId);
174068
174849
  const alreadyMarked = this.pausedAborted.has(taskId);
174069
174850
  this.pausedAborted.add(taskId);
174070
174851
  this.pausedAbortProvenance.set(taskId, provenance);
174071
174852
  if (!alreadyMarked || previousProvenance !== provenance) {
174072
- void this.store.logEntry(
174853
+ this.safeLogEntry(
174073
174854
  taskId,
174074
- `Pause abort marked: provenance=${provenance} source=${source}${previousProvenance && previousProvenance !== provenance ? ` previous=${previousProvenance}` : ""}`,
174075
- void 0,
174076
- this.getRunContextFor(taskId)
174077
- ).catch((error2) => {
174078
- executorLog.warn(`${taskId}: failed to log pause-abort marker: ${error2 instanceof Error ? error2.message : String(error2)}`);
174079
- });
174855
+ `Pause abort marked: provenance=${provenance} source=${source}${previousProvenance && previousProvenance !== provenance ? ` previous=${previousProvenance}` : ""}`
174856
+ );
174080
174857
  }
174081
174858
  }
174082
174859
  /**
@@ -174403,7 +175180,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
174403
175180
  get modelRegistry() {
174404
175181
  if (!this._modelRegistry) {
174405
175182
  const authStorage = createFusionAuthStorage();
174406
- this._modelRegistry = ModelRegistry2.create(authStorage, getModelRegistryModelsPath());
175183
+ this._modelRegistry = ModelRegistry3.create(authStorage, getModelRegistryModelsPath());
174407
175184
  this._modelRegistry.refresh();
174408
175185
  }
174409
175186
  return this._modelRegistry;
@@ -174415,22 +175192,22 @@ The tool prevents your session from being killed by the inactivity watchdog duri
174415
175192
  return this._approvalRequestStore;
174416
175193
  }
174417
175194
  buildActionGateContext(taskId, agent, projectDefaultPolicy) {
174418
- if (!agent || isEphemeralAgent(agent)) {
174419
- return void 0;
174420
- }
174421
- const policy = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy);
175195
+ const actorId = agent?.id ?? `executor-${taskId ?? "unknown"}`;
175196
+ const actorName = agent?.name ?? `Task worker ${taskId ?? "unknown"}`;
175197
+ const isEphemeral = !agent || isEphemeralAgent(agent);
175198
+ const policy = resolveEffectiveAgentPermissionPolicy(agent?.permissionPolicy, projectDefaultPolicy);
174422
175199
  return {
174423
- agentId: agent.id,
174424
- agentName: agent.name,
174425
- isEphemeral: false,
175200
+ agentId: actorId,
175201
+ agentName: actorName,
175202
+ isEphemeral,
174426
175203
  taskId,
174427
175204
  runId: taskId ? this.getRunContextFor(taskId)?.runId : void 0,
174428
175205
  permissionPolicy: policy,
174429
175206
  createApprovalRequest: async (decision, args) => this.approvalRequestStore.create({
174430
175207
  requester: {
174431
- actorId: agent.id,
175208
+ actorId,
174432
175209
  actorType: "agent",
174433
- actorName: agent.name
175210
+ actorName
174434
175211
  },
174435
175212
  taskId,
174436
175213
  runId: taskId ? this.getRunContextFor(taskId)?.runId : void 0,
@@ -174449,16 +175226,16 @@ The tool prevents your session from being killed by the inactivity watchdog duri
174449
175226
  }
174450
175227
  }),
174451
175228
  findApprovalByDedupeKey: async (dedupeKey) => {
174452
- const latest = this.approvalRequestStore.findLatestByDedupeKey({ requesterActorId: agent.id, taskId, dedupeKey });
175229
+ const latest = this.approvalRequestStore.findLatestByDedupeKey({ requesterActorId: actorId, taskId, dedupeKey });
174453
175230
  return latest ? { id: latest.id, status: latest.status } : null;
174454
175231
  },
174455
175232
  findPendingApprovalByDedupeKey: async (dedupeKey) => {
174456
- const latest = this.approvalRequestStore.findLatestByDedupeKey({ requesterActorId: agent.id, taskId, dedupeKey });
175233
+ const latest = this.approvalRequestStore.findLatestByDedupeKey({ requesterActorId: actorId, taskId, dedupeKey });
174457
175234
  return latest?.status === "pending" ? { id: latest.id } : null;
174458
175235
  },
174459
175236
  pauseForApproval: async ({ approvalRequestId, decision }) => {
174460
175237
  if (taskId) {
174461
- await this.store.pauseTask(taskId, true, this.getRunContextFor(taskId), { pausedByAgentId: agent.id });
175238
+ await this.store.pauseTask(taskId, true, this.getRunContextFor(taskId), { pausedByAgentId: actorId });
174462
175239
  await this.store.logEntry(
174463
175240
  taskId,
174464
175241
  `Approval required for ${decision.toolName}. Request ${approvalRequestId} created; task and agent paused awaiting decision.`,
@@ -174466,55 +175243,54 @@ The tool prevents your session from being killed by the inactivity watchdog duri
174466
175243
  this.getRunContextFor(taskId)
174467
175244
  );
174468
175245
  }
174469
- if (this.options.agentStore) {
175246
+ if (agent && this.options.agentStore) {
174470
175247
  await this.options.agentStore.updateAgentState(agent.id, "paused");
174471
175248
  await this.options.agentStore.updateAgent(agent.id, { pauseReason: "awaiting-approval" });
174472
175249
  }
174473
175250
  },
174474
175251
  markApprovalCompleted: async (approvalRequestId) => {
174475
175252
  await this.approvalRequestStore.markCompleted(approvalRequestId, {
174476
- actor: { actorId: agent.id, actorType: "agent", actorName: agent.name },
175253
+ actor: { actorId, actorType: "agent", actorName },
174477
175254
  note: "Tool executed after approval"
174478
175255
  });
174479
175256
  }
174480
175257
  };
174481
175258
  }
174482
175259
  buildPermanentAgentGatingContext(taskId, agent, projectDefaultPolicy) {
174483
- if (!agent || isEphemeralAgent(agent)) {
174484
- return void 0;
174485
- }
175260
+ const actorId = agent?.id ?? `executor-${taskId ?? "unknown"}`;
175261
+ const actorName = agent?.name ?? `Task worker ${taskId ?? "unknown"}`;
174486
175262
  return {
174487
- permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy),
175263
+ permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent?.permissionPolicy, projectDefaultPolicy),
174488
175264
  requester: {
174489
- actorId: agent.id,
175265
+ actorId,
174490
175266
  actorType: "agent",
174491
- actorName: agent.name
175267
+ actorName
174492
175268
  },
174493
175269
  taskId,
174494
175270
  runId: taskId ? this.getRunContextFor(taskId)?.runId : void 0,
174495
175271
  createApprovalRequest: async ({ category, toolName, args }) => this.approvalRequestStore.create({
174496
175272
  requester: {
174497
- actorId: agent.id,
175273
+ actorId,
174498
175274
  actorType: "agent",
174499
- actorName: agent.name
175275
+ actorName
174500
175276
  },
174501
175277
  taskId,
174502
175278
  runId: taskId ? this.getRunContextFor(taskId)?.runId : void 0,
174503
175279
  targetAction: {
174504
175280
  category,
174505
175281
  action: toolName,
174506
- summary: `Permanent-agent gated action for ${toolName}`,
175282
+ summary: `Agent gated action for ${toolName}`,
174507
175283
  resourceType: "tool",
174508
175284
  resourceId: toolName,
174509
175285
  context: {
174510
175286
  toolName,
174511
175287
  toolArgs: args,
174512
- source: "permanent-agent-gating"
175288
+ source: "agent-gating"
174513
175289
  }
174514
175290
  }
174515
175291
  }),
174516
175292
  findPendingApprovalRequest: async (dedupeKey) => {
174517
- const pending = this.approvalRequestStore.list({ status: "pending", requesterActorId: agent.id, taskId, limit: 100 });
175293
+ const pending = this.approvalRequestStore.list({ status: "pending", requesterActorId: actorId, taskId, limit: 100 });
174518
175294
  return pending.find((request2) => request2.targetAction.context?.approvalDedupeKey === dedupeKey) ?? null;
174519
175295
  }
174520
175296
  };
@@ -174763,14 +175539,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
174763
175539
  this.stuckAborted.delete(taskId);
174764
175540
  if (hadActiveSurface) {
174765
175541
  executorLog.log(`${taskId}: awaited abort of in-flight work \u2014 ${reason}`);
174766
- await this.store.logEntry(
175542
+ this.safeLogEntry(
174767
175543
  taskId,
174768
- `Pause abort cleanup completed: reason=${reason}; surfaces=${abortedSurfaces.join(", ") || "none"}`,
174769
- void 0,
174770
- this.getRunContextFor(taskId)
174771
- ).catch((error2) => {
174772
- executorLog.warn(`${taskId}: failed to log pause-abort cleanup: ${error2 instanceof Error ? error2.message : String(error2)}`);
174773
- });
175544
+ `Pause abort cleanup completed: reason=${reason}; surfaces=${abortedSurfaces.join(", ") || "none"}`
175545
+ );
174774
175546
  }
174775
175547
  }
174776
175548
  async abortAllInFlight(reason) {
@@ -178511,11 +179283,9 @@ ${prompt}`;
178511
179283
  if (pausedAborted || live.paused || live.userPaused || abortProvenance) {
178512
179284
  const failedNodeForLog = result.visitedNodeIds[result.visitedNodeIds.length - 1] ?? "unknown";
178513
179285
  const failureValueForLog = this.graphFailureValue(result) ?? "none";
178514
- await this.store.logEntry(
179286
+ this.safeLogEntry(
178515
179287
  task.id,
178516
- `Pause abort classified: provenance=${abortProvenance ?? "unknown"}; node=${failedNodeForLog}; interrupted=${result.interruptedNodeId ?? "none"}; abortKind=${result.interruptedAbortKind ?? "none"}; column=${live.column}; status=${live.status ?? "none"}; paused=${live.paused === true}; userPaused=${live.userPaused === true}; value=${failureValueForLog}; genuine=${genuinePauseAbort}; mergeSeam=${mergeSeamAborted}; completionSuppressed=${suppressFinalizedCompletionAbort}`,
178517
- void 0,
178518
- this.getRunContextFor(task.id)
179288
+ `Pause abort classified: provenance=${abortProvenance ?? "unknown"}; node=${failedNodeForLog}; interrupted=${result.interruptedNodeId ?? "none"}; abortKind=${result.interruptedAbortKind ?? "none"}; column=${live.column}; status=${live.status ?? "none"}; paused=${live.paused === true}; userPaused=${live.userPaused === true}; value=${failureValueForLog}; genuine=${genuinePauseAbort}; mergeSeam=${mergeSeamAborted}; completionSuppressed=${suppressFinalizedCompletionAbort}`
178519
179289
  );
178520
179290
  }
178521
179291
  if (genuinePauseAbort && await this.isReentrantPausedAbortedInFlightNode(live, result, abortProvenance, pausedAborted, this.userCanceledTaskIds.has(task.id))) {
@@ -189152,6 +189922,10 @@ async function landOneRepo(repoRootDir, branch, integrationBranch, ctx) {
189152
189922
  configured: depsSyncResult.configured,
189153
189923
  skipped: depsSyncResult.skipped,
189154
189924
  skipReason: depsSyncResult.skipReason,
189925
+ // FNXC:AIMerge 2026-07-02-14:05 (lockfile auto-heal): record when an outdated frozen lockfile
189926
+ // was recovered by a non-frozen retry so operators can see deps drifted without failing merge.
189927
+ healed: depsSyncResult.healed,
189928
+ healedCommand: depsSyncResult.healedCommand,
189155
189929
  durationMs: depsSyncResult.durationMs
189156
189930
  }
189157
189931
  });
@@ -190089,7 +190863,30 @@ var init_workflow_authoritative_driver = __esm({
190089
190863
  },
190090
190864
  primitives: this.deps.executor.createAuthoritativeWorkflowPrimitives?.(settings) ?? primitivesFromLegacySeams(seams),
190091
190865
  seams,
190866
+ /*
190867
+ FNXC:WorkflowInterpreterCutover 2026-07-01-21:05:
190868
+ BUILTIN_CODING_WORKFLOW_IR graduated to carry default-on/always-on custom
190869
+ lifecycle nodes: the plan-review/code-review/browser-verification/post-merge
190870
+ optional groups (bypassed at their group node when a task disables them via
190871
+ enabledWorkflowSteps) and an ALWAYS-ON `completion-summary` prompt node
190872
+ (kind:"prompt", summaryTarget:"task") that records the pre-review summary.
190873
+ The transitional authoritative driver validates SEAM-LEVEL parity
190874
+ (planning/execute/review/merge/schedule) against the legacy executor; it does
190875
+ not reproduce custom-node side effects. Previously runCustomNode threw on any
190876
+ custom node, so the always-on completion-summary aborted every clean run
190877
+ before it could reach review→merge. Pass built-in auxiliary custom nodes
190878
+ (task-summary nodes and bypassable optional groups) through as success so the
190879
+ seam pipeline completes, mirroring the main executor's completion path for
190880
+ parity purposes. Genuinely unexpected custom nodes still fail loudly.
190881
+ */
190092
190882
  runCustomNode: async (node) => {
190883
+ const isBuiltinAuxiliaryNode = node.config?.summaryTarget === "task" || node.kind === "optional-group";
190884
+ if (isBuiltinAuxiliaryNode) {
190885
+ executorLog.log(
190886
+ `[workflow-authoritative] ${task.id}: passing auxiliary custom node '${node.id}' through as success (seam-parity run)`
190887
+ );
190888
+ return { outcome: "success" };
190889
+ }
190093
190890
  throw new Error(`unexpected custom node in builtin authoritative workflow: ${node.id}`);
190094
190891
  },
190095
190892
  onEvent: (event) => {
@@ -192512,14 +193309,11 @@ Critical: a heartbeat without observable progress (or an explicit no-op reason)
192512
193309
  return this.approvalRequestStore;
192513
193310
  }
192514
193311
  buildActionGateContext(agent, taskId, runId, projectDefaultPolicy) {
192515
- if (isEphemeralAgent(agent)) {
192516
- return void 0;
192517
- }
192518
193312
  const policy = resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy);
192519
193313
  return {
192520
193314
  agentId: agent.id,
192521
193315
  agentName: agent.name,
192522
- isEphemeral: false,
193316
+ isEphemeral: isEphemeralAgent(agent),
192523
193317
  taskId,
192524
193318
  runId,
192525
193319
  permissionPolicy: policy,
@@ -192564,9 +193358,6 @@ Critical: a heartbeat without observable progress (or an explicit no-op reason)
192564
193358
  };
192565
193359
  }
192566
193360
  buildPermanentAgentGatingContext(agent, taskId, runId, projectDefaultPolicy) {
192567
- if (isEphemeralAgent(agent)) {
192568
- return void 0;
192569
- }
192570
193361
  return {
192571
193362
  permissionPolicy: resolveEffectiveAgentPermissionPolicy(agent.permissionPolicy, projectDefaultPolicy),
192572
193363
  requester: { actorId: agent.id, actorType: "agent", actorName: agent.name },
@@ -192579,13 +193370,13 @@ Critical: a heartbeat without observable progress (or an explicit no-op reason)
192579
193370
  targetAction: {
192580
193371
  category,
192581
193372
  action: toolName,
192582
- summary: `Permanent-agent gated action for ${toolName}`,
193373
+ summary: `Agent gated action for ${toolName}`,
192583
193374
  resourceType: "tool",
192584
193375
  resourceId: toolName,
192585
193376
  context: {
192586
193377
  toolName,
192587
193378
  toolArgs: args,
192588
- source: "permanent-agent-gating"
193379
+ source: "agent-gating"
192589
193380
  }
192590
193381
  }
192591
193382
  }),
@@ -203987,7 +204778,7 @@ __export(child_process_runtime_exports, {
203987
204778
  import { EventEmitter as EventEmitter23 } from "node:events";
203988
204779
  import { fork } from "node:child_process";
203989
204780
  import { fileURLToPath as fileURLToPath3 } from "node:url";
203990
- import { dirname as dirname25, join as join58 } from "node:path";
204781
+ import { dirname as dirname26, join as join58 } from "node:path";
203991
204782
  var HealthMonitor, ChildProcessRuntime;
203992
204783
  var init_child_process_runtime = __esm({
203993
204784
  "../engine/src/runtimes/child-process-runtime.ts"() {
@@ -204147,7 +204938,7 @@ var init_child_process_runtime = __esm({
204147
204938
  */
204148
204939
  getWorkerPath() {
204149
204940
  const isCompiled = !import.meta.url.endsWith(".ts");
204150
- const currentDir = dirname25(fileURLToPath3(import.meta.url));
204941
+ const currentDir = dirname26(fileURLToPath3(import.meta.url));
204151
204942
  const workerFile = isCompiled ? "child-process-worker.js" : "child-process-worker.ts";
204152
204943
  return join58(currentDir, workerFile);
204153
204944
  }
@@ -212450,6 +213241,7 @@ __export(src_exports2, {
212450
213241
  createDefaultNodeHandlers: () => createDefaultNodeHandlers,
212451
213242
  createFnAgent: () => createFnAgent2,
212452
213243
  createFusionAuthStorage: () => createFusionAuthStorage,
213244
+ createFusionModelRegistry: () => createFusionModelRegistry,
212453
213245
  createInteractiveAiSessionWith: () => createInteractiveAiSessionWith,
212454
213246
  createLogger: () => createLogger2,
212455
213247
  createNoopLegacySeams: () => createNoopLegacySeams,
@@ -213139,6 +213931,13 @@ var init_planning_board_tools = __esm({
213139
213931
  // ../dashboard/src/planning.ts
213140
213932
  import { randomUUID as randomUUID23 } from "node:crypto";
213141
213933
  import { EventEmitter as EventEmitter28 } from "node:events";
213934
+ async function resolvePlanningMcpServers(store) {
213935
+ const resolved = await resolveMcpServersForStore(store);
213936
+ if (!resolved || !Array.isArray(resolved.servers)) {
213937
+ return [];
213938
+ }
213939
+ return resolved.servers;
213940
+ }
213142
213941
  function ensureEngineReady() {
213143
213942
  return Promise.resolve();
213144
213943
  }
@@ -213207,6 +214006,68 @@ function normalizePlanningSummaryPayload(summaryInput, fallback) {
213207
214006
  keyDeliverables: normalizeStringArray(summary.keyDeliverables)
213208
214007
  };
213209
214008
  }
214009
+ function slugifyCheckpointTheme(label) {
214010
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "topic";
214011
+ }
214012
+ function collectCheckpointThemeText(history, summary) {
214013
+ return [
214014
+ summary.title,
214015
+ summary.description,
214016
+ summary.keyDeliverables.join("\n"),
214017
+ summary.suggestedDependencies.join("\n"),
214018
+ ...history.flatMap((entry) => [
214019
+ entry.question.question,
214020
+ entry.question.description ?? "",
214021
+ JSON.stringify(entry.response)
214022
+ ])
214023
+ ].join("\n");
214024
+ }
214025
+ function buildDeepeningCheckpointOptions(history, summary) {
214026
+ const text = collectCheckpointThemeText(history, summary);
214027
+ const matched = CHECKPOINT_THEME_CANDIDATES.filter(
214028
+ (candidate) => candidate.patterns.some((pattern) => pattern.test(text))
214029
+ );
214030
+ const selected = matched.length > 0 ? matched : CHECKPOINT_THEME_CANDIDATES.filter((candidate) => FALLBACK_CHECKPOINT_THEME_OPTIONS.includes(candidate.id));
214031
+ const seen = /* @__PURE__ */ new Set();
214032
+ const options = selected.flatMap((candidate) => {
214033
+ const id = `theme-${slugifyCheckpointTheme(candidate.id)}`;
214034
+ if (seen.has(id) || candidate.label === "Proceed to final plan") {
214035
+ return [];
214036
+ }
214037
+ seen.add(id);
214038
+ return [{ id, label: candidate.label, description: candidate.description }];
214039
+ });
214040
+ return [
214041
+ { id: PLANNING_DEEPEN_PROCEED_OPTION_ID, label: "Proceed to final plan", description: "The plan is detailed enough; show the final editable summary." },
214042
+ ...options
214043
+ ];
214044
+ }
214045
+ function buildDeepeningCheckpointQuestion(history, summary) {
214046
+ return {
214047
+ id: PLANNING_DEEPEN_CHECKPOINT_ID,
214048
+ type: "multi_select",
214049
+ question: PLANNING_DEEPEN_CHECKPOINT_QUESTION,
214050
+ description: "Select any areas you want to explore further, write an unlisted topic, or proceed to the final plan.",
214051
+ options: buildDeepeningCheckpointOptions(history, summary)
214052
+ };
214053
+ }
214054
+ function classifyDeepeningCheckpointResponse(question, responses) {
214055
+ const rawSelected = responses[question.id];
214056
+ const selectedIds = Array.isArray(rawSelected) ? rawSelected.filter((id) => typeof id === "string") : [];
214057
+ const customTopic = typeof responses._other === "string" && responses._other.trim().length > 0 ? responses._other.trim() : void 0;
214058
+ const proceed = responses[PLANNING_DEEPEN_PROCEED_RESPONSE_KEY] === true || selectedIds.includes(PLANNING_DEEPEN_PROCEED_OPTION_ID);
214059
+ const themeIds = selectedIds.filter((id) => id !== PLANNING_DEEPEN_PROCEED_OPTION_ID);
214060
+ const selectedThemeLabels = themeIds.map((id) => question.options?.find((option) => option.id === id)?.label || id);
214061
+ return {
214062
+ proceed,
214063
+ selectedThemeIds: themeIds,
214064
+ selectedThemeLabels,
214065
+ ...customTopic ? { customTopic } : {}
214066
+ };
214067
+ }
214068
+ function isDeepeningCheckpointQuestion(question) {
214069
+ return question?.id === PLANNING_DEEPEN_CHECKPOINT_ID && question.question === PLANNING_DEEPEN_CHECKPOINT_QUESTION;
214070
+ }
213210
214071
  function safeParseJson(text, fallback, options) {
213211
214072
  if (!text) {
213212
214073
  return fallback;
@@ -213260,7 +214121,8 @@ function persistSession(session, status, error2) {
213260
214121
  initialPlan: session.initialPlan,
213261
214122
  ...session.draftModelProvider ? { modelProvider: session.draftModelProvider } : {},
213262
214123
  ...session.draftModelId ? { modelId: session.draftModelId } : {},
213263
- ...session.draftSummarizedFor ? { summarizedFor: session.draftSummarizedFor } : {}
214124
+ ...session.draftSummarizedFor ? { summarizedFor: session.draftSummarizedFor } : {},
214125
+ ...session.pendingSummary ? { pendingSummary: session.pendingSummary } : {}
213264
214126
  }),
213265
214127
  conversationHistory: JSON.stringify(session.history),
213266
214128
  currentQuestion: session.currentQuestion ? JSON.stringify(session.currentQuestion) : null,
@@ -213317,6 +214179,7 @@ function buildSessionFromRow(row) {
213317
214179
  }),
213318
214180
  { title: row.title, description: row.title }
213319
214181
  ) : void 0,
214182
+ pendingSummary: payload.pendingSummary ? normalizePlanningSummaryPayload(payload.pendingSummary, { title: row.title, description: row.title }) : void 0,
213320
214183
  thinkingOutput: row.thinkingOutput,
213321
214184
  lastGeneratedThinking: row.thinkingOutput || "",
213322
214185
  error: row.error ?? void 0,
@@ -213424,7 +214287,7 @@ ${depthPromptSuffix}` : baseSystemPrompt;
213424
214287
  builtinToolsAllowlist: [...PLANNING_BUILTIN_WEB_TOOLS],
213425
214288
  // FNXC:McpConfig 2026-06-25-22:31: Planning/chat session creation resolves trusted MCP servers through the dashboard-scoped store and forwards only the materialized in-memory set to the engine runtime guard.
213426
214289
  // FNXC:McpConfig 2026-06-29-00:00: Planning sessions are intentionally read-only but still need configured MCP documentation/context tools; opt in at the session boundary while preserving engine-side namespacing, filtering, wrappers, and disposal.
213427
- mcpServers: (await resolveMcpServersForStore(store)).servers,
214290
+ mcpServers: await resolvePlanningMcpServers(store),
213428
214291
  allowMcpToolsInReadonly: true,
213429
214292
  customTools: [
213430
214293
  ...createPlanningBoardTools(store),
@@ -213537,14 +214400,7 @@ async function getFirstQuestionFromAgent(session, message) {
213537
214400
  title: session.title || session.initialPlan,
213538
214401
  description: session.initialPlan
213539
214402
  });
213540
- session.summary = summary;
213541
- persistSession(session, "complete");
213542
- return {
213543
- id: "q-direct-summary",
213544
- type: "confirm",
213545
- question: `The AI has generated a plan: "${summary.title}". Proceed with this?`,
213546
- description: summary.description
213547
- };
214403
+ return setPendingSummaryCheckpoint(session, summary);
213548
214404
  }
213549
214405
  return parsed.data;
213550
214406
  }
@@ -213564,7 +214420,7 @@ ${depthPromptSuffix}` : baseSystemPrompt;
213564
214420
  builtinToolsAllowlist: [...PLANNING_BUILTIN_WEB_TOOLS],
213565
214421
  // FNXC:McpConfig 2026-06-25-22:31: Streaming planning uses the same dashboard-scoped MCP resolution seam as non-streaming planning so no planning lane silently drops enabled servers.
213566
214422
  // FNXC:McpConfig 2026-06-29-00:00: Streaming planning uses the explicit read-only MCP opt-in; non-planning read-only lanes remain denied unless they set the same reviewed policy flag.
213567
- mcpServers: (await resolveMcpServersForStore(store)).servers,
214423
+ mcpServers: await resolvePlanningMcpServers(store),
213568
214424
  allowMcpToolsInReadonly: true,
213569
214425
  customTools: [
213570
214426
  ...createPlanningBoardTools(store),
@@ -213770,6 +214626,54 @@ async function runGenerationWithTimeout(session, operation) {
213770
214626
  }
213771
214627
  }
213772
214628
  }
214629
+ function finalizePendingSummary(session) {
214630
+ const summary = normalizePlanningSummaryPayload(session.pendingSummary ?? session.summary, {
214631
+ title: session.title || session.initialPlan,
214632
+ description: session.initialPlan
214633
+ });
214634
+ session.summary = summary;
214635
+ session.pendingSummary = void 0;
214636
+ session.currentQuestion = void 0;
214637
+ session.error = void 0;
214638
+ session.updatedAt = /* @__PURE__ */ new Date();
214639
+ persistSession(session, "complete");
214640
+ planningStreamManager.broadcast(session.id, {
214641
+ type: "summary",
214642
+ data: summary
214643
+ });
214644
+ planningStreamManager.broadcast(session.id, { type: "complete" });
214645
+ return summary;
214646
+ }
214647
+ function setPendingSummaryCheckpoint(session, summary) {
214648
+ const checkpoint = buildDeepeningCheckpointQuestion(session.history, summary);
214649
+ session.pendingSummary = summary;
214650
+ session.summary = void 0;
214651
+ session.currentQuestion = checkpoint;
214652
+ session.error = void 0;
214653
+ session.lastGeneratedThinking = session.thinkingOutput;
214654
+ session.updatedAt = /* @__PURE__ */ new Date();
214655
+ persistSession(session, "awaiting_input");
214656
+ void maybeNotifyPlanningAwaitingInput(session, checkpoint);
214657
+ planningStreamManager.broadcast(session.id, {
214658
+ type: "question",
214659
+ data: checkpoint
214660
+ });
214661
+ return checkpoint;
214662
+ }
214663
+ function formatDeepeningRequestForAgent(decision, pendingSummary) {
214664
+ const requestedTopics = [
214665
+ ...decision.selectedThemeLabels,
214666
+ ...decision.customTopic ? [decision.customTopic] : []
214667
+ ];
214668
+ return [
214669
+ "The user chose to go deeper before accepting the final planning summary.",
214670
+ "Continue the planning interview and explore these specific topics in more detail before producing another completion summary:",
214671
+ requestedTopics.map((topic) => `- ${topic}`).join("\n"),
214672
+ "Do not skip directly to the final summary unless the additional details are addressed; when you next complete, return the normal JSON complete payload.",
214673
+ "Pending summary that was withheld from the user:",
214674
+ JSON.stringify(pendingSummary)
214675
+ ].join("\n\n");
214676
+ }
213773
214677
  async function continueAgentConversation(session, message) {
213774
214678
  if (!session.agent) {
213775
214679
  throw new InvalidSessionStateError("AI agent not initialized");
@@ -213862,6 +214766,7 @@ async function continueAgentConversation(session, message) {
213862
214766
  if (parsed.type === "question") {
213863
214767
  session.currentQuestion = parsed.data;
213864
214768
  session.summary = void 0;
214769
+ session.pendingSummary = void 0;
213865
214770
  session.error = void 0;
213866
214771
  session.lastGeneratedThinking = session.thinkingOutput;
213867
214772
  session.updatedAt = /* @__PURE__ */ new Date();
@@ -213876,16 +214781,7 @@ async function continueAgentConversation(session, message) {
213876
214781
  title: session.title || session.initialPlan,
213877
214782
  description: session.initialPlan
213878
214783
  });
213879
- session.summary = summary;
213880
- session.currentQuestion = void 0;
213881
- session.error = void 0;
213882
- session.updatedAt = /* @__PURE__ */ new Date();
213883
- persistSession(session, "complete");
213884
- planningStreamManager.broadcast(session.id, {
213885
- type: "summary",
213886
- data: summary
213887
- });
213888
- planningStreamManager.broadcast(session.id, { type: "complete" });
214784
+ setPendingSummaryCheckpoint(session, summary);
213889
214785
  }
213890
214786
  });
213891
214787
  } catch (err) {
@@ -214091,6 +214987,7 @@ async function submitResponse(sessionId, responses, rootDir, promptOverrides, st
214091
214987
  throw new InvalidSessionStateError("No active question in session");
214092
214988
  }
214093
214989
  session.error = void 0;
214990
+ session.pendingSummary = void 0;
214094
214991
  persistSession(session, "generating");
214095
214992
  await ensureSessionAgent(session, rootDir, session.history, promptOverrides, store);
214096
214993
  const refineMessage = formatRefineRequestForAgent(session.summary);
@@ -214104,12 +215001,35 @@ async function submitResponse(sessionId, responses, rootDir, promptOverrides, st
214104
215001
  };
214105
215002
  session.error = void 0;
214106
215003
  session.history.push(historyEntry);
214107
- persistSession(session, "generating");
214108
- if (!session.agent) {
214109
- await ensureSessionAgent(session, rootDir, session.history.slice(0, -1), promptOverrides, store);
215004
+ if (isDeepeningCheckpointQuestion(currentQuestion)) {
215005
+ const pendingSummary = session.pendingSummary;
215006
+ if (!pendingSummary) {
215007
+ throw new InvalidSessionStateError("Planning checkpoint is missing its pending summary");
215008
+ }
215009
+ const decision = classifyDeepeningCheckpointResponse(currentQuestion, responses);
215010
+ if (decision.proceed) {
215011
+ finalizePendingSummary(session);
215012
+ } else {
215013
+ const hasDeepeningTopic = decision.selectedThemeLabels.length > 0 || Boolean(decision.customTopic);
215014
+ if (!hasDeepeningTopic) {
215015
+ session.history.pop();
215016
+ throw new InvalidSessionStateError("Select a topic to explore or proceed to the final plan");
215017
+ }
215018
+ session.pendingSummary = void 0;
215019
+ persistSession(session, "generating");
215020
+ if (!session.agent) {
215021
+ await ensureSessionAgent(session, rootDir, session.history.slice(0, -1), promptOverrides, store);
215022
+ }
215023
+ await continueAgentConversation(session, formatDeepeningRequestForAgent(decision, pendingSummary));
215024
+ }
215025
+ } else {
215026
+ persistSession(session, "generating");
215027
+ if (!session.agent) {
215028
+ await ensureSessionAgent(session, rootDir, session.history.slice(0, -1), promptOverrides, store);
215029
+ }
215030
+ const message = formatResponseForAgent(currentQuestion, responses);
215031
+ await continueAgentConversation(session, message);
214110
215032
  }
214111
- const message = formatResponseForAgent(currentQuestion, responses);
214112
- await continueAgentConversation(session, message);
214113
215033
  }
214114
215034
  if (session.summary) {
214115
215035
  return { type: "complete", data: session.summary };
@@ -214266,7 +215186,7 @@ function getSession(sessionId) {
214266
215186
  return void 0;
214267
215187
  }
214268
215188
  }
214269
- var PLANNING_NO_AMBIENT_TASK_ID, PLANNING_BUILTIN_WEB_TOOLS, createFnAgent4, planningNtfyHelpers, diagnostics2, PLANNING_SYSTEM_PROMPT, SESSION_TTL_MS, CLEANUP_INTERVAL_MS2, MAX_SESSIONS_PER_IP_PER_HOUR, RATE_LIMIT_WINDOW_MS2, GENERATION_TIMEOUT_MS, GENERATION_LOOP_REPEAT_LIMIT, PLANNING_STUCK_ERROR_MESSAGE, PLANNING_LOOP_ERROR_MESSAGE, PLANNING_USER_STOP_ERROR_MESSAGE, PLANNING_DEPTH_PROMPT_SUFFIX, sessions, rateLimits2, activeGenerations, _aiSessionStore, cleanupInterval2, PlanningStreamManager, planningStreamManager, MAX_PARSE_RETRIES2, RateLimitError2, SessionNotFoundError, InvalidSessionStateError, GenerationInProgressError;
215189
+ var PLANNING_NO_AMBIENT_TASK_ID, PLANNING_BUILTIN_WEB_TOOLS, createFnAgent4, planningNtfyHelpers, diagnostics2, PLANNING_SYSTEM_PROMPT, SESSION_TTL_MS, CLEANUP_INTERVAL_MS2, MAX_SESSIONS_PER_IP_PER_HOUR, RATE_LIMIT_WINDOW_MS2, GENERATION_TIMEOUT_MS, GENERATION_LOOP_REPEAT_LIMIT, PLANNING_STUCK_ERROR_MESSAGE, PLANNING_LOOP_ERROR_MESSAGE, PLANNING_USER_STOP_ERROR_MESSAGE, PLANNING_DEPTH_PROMPT_SUFFIX, sessions, rateLimits2, activeGenerations, _aiSessionStore, CHECKPOINT_THEME_CANDIDATES, FALLBACK_CHECKPOINT_THEME_OPTIONS, cleanupInterval2, PlanningStreamManager, planningStreamManager, MAX_PARSE_RETRIES2, RateLimitError2, SessionNotFoundError, InvalidSessionStateError, GenerationInProgressError;
214270
215190
  var init_planning = __esm({
214271
215191
  "../dashboard/src/planning.ts"() {
214272
215192
  "use strict";
@@ -214360,6 +215280,15 @@ For completion:
214360
215280
  sessions = /* @__PURE__ */ new Map();
214361
215281
  rateLimits2 = /* @__PURE__ */ new Map();
214362
215282
  activeGenerations = /* @__PURE__ */ new Map();
215283
+ CHECKPOINT_THEME_CANDIDATES = [
215284
+ { id: "scope", label: "Scope and non-goals", description: "Clarify boundaries, trade-offs, and what should stay out of this task.", patterns: [/\bscope\b/i, /non[- ]?goal/i, /boundary/i, /trade[- ]?off/i] },
215285
+ { id: "edge-cases", label: "Edge cases and data states", description: "Explore empty, duplicate, malformed, missing, or unusual states before implementation.", patterns: [/edge case/i, /empty/i, /undefined/i, /duplicate/i, /malformed/i, /data state/i] },
215286
+ { id: "ux", label: "UX and interaction details", description: "Tighten user-facing copy, responsive behavior, accessibility, and interaction flow.", patterns: [/\bux\b/i, /user/i, /mobile/i, /responsive/i, /accessibility/i, /keyboard/i, /button/i] },
215287
+ { id: "dependencies", label: "Dependencies and integrations", description: "Identify prerequisite tasks, third-party systems, and integration constraints.", patterns: [/dependenc/i, /integration/i, /api\b/i, /external/i, /provider/i, /service/i] },
215288
+ { id: "testing", label: "Testing and verification", description: "Deepen acceptance criteria, regression coverage, and validation commands.", patterns: [/test/i, /verify/i, /validation/i, /acceptance/i, /regression/i] },
215289
+ { id: "rollout", label: "Rollout and operations", description: "Discuss migration, release, observability, documentation, or support considerations.", patterns: [/rollout/i, /migration/i, /release/i, /observability/i, /docs?\b/i, /operator/i] }
215290
+ ];
215291
+ FALLBACK_CHECKPOINT_THEME_OPTIONS = ["scope", "edge-cases", "ux", "testing"];
214363
215292
  cleanupInterval2 = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS2);
214364
215293
  cleanupInterval2.unref?.();
214365
215294
  registerBeforeExitCleanup(() => {
@@ -215229,7 +216158,7 @@ import {
215229
216158
  unlinkSync as unlinkSync3
215230
216159
  } from "node:fs";
215231
216160
  import { homedir as homedir8 } from "node:os";
215232
- import { basename as basename16, dirname as dirname26, join as join60, resolve as resolve31 } from "node:path";
216161
+ import { basename as basename16, dirname as dirname27, join as join60, resolve as resolve31 } from "node:path";
215233
216162
  import { fileURLToPath as fileURLToPath4 } from "node:url";
215234
216163
  function resolveHermesHome(profile) {
215235
216164
  const base = process.env.HERMES_HOME ?? join60(homedir8(), ".hermes");
@@ -215238,7 +216167,7 @@ function resolveHermesHome(profile) {
215238
216167
  }
215239
216168
  function getFusionSkillSourceCandidates(moduleUrl = import.meta.url) {
215240
216169
  const here = fileURLToPath4(moduleUrl);
215241
- const moduleDir = dirname26(here);
216170
+ const moduleDir = dirname27(here);
215242
216171
  return [
215243
216172
  resolve31(moduleDir, "..", "..", "..", "..", "packages", "cli", "skill", FUSION_SKILL_NAME),
215244
216173
  resolve31(moduleDir, "..", "..", "..", "skill", FUSION_SKILL_NAME),
@@ -215265,16 +216194,16 @@ function installFusionSkillIntoHermesHome(options = {}) {
215265
216194
  };
215266
216195
  }
215267
216196
  try {
215268
- mkdirSync14(dirname26(targetDir), { recursive: true });
216197
+ mkdirSync14(dirname27(targetDir), { recursive: true });
215269
216198
  let replaced = false;
215270
216199
  if (existsSync51(targetDir) || isBrokenSymlink(targetDir)) {
215271
216200
  const stat12 = lstatSync3(targetDir);
215272
216201
  if (stat12.isSymbolicLink()) {
215273
216202
  const currentTarget = safeReadlink(targetDir);
215274
- if (currentTarget && resolve31(dirname26(targetDir), currentTarget) === resolve31(sourceDir)) {
216203
+ if (currentTarget && resolve31(dirname27(targetDir), currentTarget) === resolve31(sourceDir)) {
215275
216204
  return { outcome: "already-installed", sourceDir, targetDir };
215276
216205
  }
215277
- if (!looksLikeFusionSkillTarget(resolve31(dirname26(targetDir), currentTarget ?? ""))) {
216206
+ if (!looksLikeFusionSkillTarget(resolve31(dirname27(targetDir), currentTarget ?? ""))) {
215278
216207
  return {
215279
216208
  outcome: "skipped",
215280
216209
  sourceDir,
@@ -215741,7 +216670,7 @@ import { writeFileSync as writeFileSync10 } from "node:fs";
215741
216670
  import { tmpdir as tmpdir10 } from "node:os";
215742
216671
  import { join as join61 } from "node:path";
215743
216672
  import { fileURLToPath as fileURLToPath5 } from "node:url";
215744
- import { dirname as dirname27 } from "node:path";
216673
+ import { dirname as dirname28 } from "node:path";
215745
216674
  function toolsToMcpToolDefs(tools) {
215746
216675
  if (!Array.isArray(tools)) return [];
215747
216676
  return tools.filter((tool) => tool && typeof tool.name === "string" && !BUILT_IN_TOOL_NAMES.has(tool.name)).map((tool) => ({
@@ -215755,7 +216684,7 @@ function writeOpenClawMcpBridgeFiles(toolDefs, cacheKey) {
215755
216684
  const schemaPath = join61(tmpdir10(), `openclaw-runtime-mcp-schemas-${suffix}.json`);
215756
216685
  writeFileSync10(schemaPath, JSON.stringify(toolDefs));
215757
216686
  const __filename = fileURLToPath5(import.meta.url);
215758
- const __dirname2 = dirname27(__filename);
216687
+ const __dirname2 = dirname28(__filename);
215759
216688
  const serverPath = join61(__dirname2, "mcp-schema-server.cjs");
215760
216689
  const serverConfig = {
215761
216690
  command: "node",
@@ -219575,6 +220504,430 @@ var init_github_source_issue_close = __esm({
219575
220504
  }
219576
220505
  });
219577
220506
 
220507
+ // ../dashboard/src/gitlab.ts
220508
+ function clampLimit(limit) {
220509
+ if (typeof limit !== "number" || !Number.isFinite(limit)) return DEFAULT_LIMIT3;
220510
+ return Math.max(1, Math.min(MAX_LIMIT, Math.floor(limit)));
220511
+ }
220512
+ function encodeGitLabPathId(id) {
220513
+ return encodeURIComponent(String(id).trim());
220514
+ }
220515
+ function appendListParams(params, options) {
220516
+ const state = typeof options.state === "string" && options.state.trim() ? options.state.trim() : "opened";
220517
+ params.set("state", state);
220518
+ if (options.labels && options.labels.length > 0) {
220519
+ params.set("labels", options.labels.map((label) => label.trim()).filter(Boolean).join(","));
220520
+ }
220521
+ }
220522
+ function asRecord2(value) {
220523
+ return value && typeof value === "object" ? value : {};
220524
+ }
220525
+ function normalizeAuthor(author) {
220526
+ const raw = asRecord2(author);
220527
+ if (Object.keys(raw).length === 0) return null;
220528
+ return {
220529
+ ...typeof raw.id === "number" ? { id: raw.id } : {},
220530
+ ...typeof raw.username === "string" ? { username: raw.username } : {},
220531
+ ...typeof raw.name === "string" ? { name: raw.name } : {},
220532
+ ...typeof raw.avatar_url === "string" ? { avatarUrl: raw.avatar_url } : {},
220533
+ ...typeof raw.web_url === "string" ? { webUrl: raw.web_url } : {}
220534
+ };
220535
+ }
220536
+ function normalizeLabels(labels) {
220537
+ return Array.isArray(labels) ? labels.filter((label) => typeof label === "string") : [];
220538
+ }
220539
+ function readProjectPath(input) {
220540
+ const raw = asRecord2(input);
220541
+ if (typeof raw.project_path_with_namespace === "string") return raw.project_path_with_namespace;
220542
+ const references = asRecord2(raw.references);
220543
+ if (typeof references.full === "string") {
220544
+ const full = references.full;
220545
+ const webUrl = typeof raw.web_url === "string" ? raw.web_url : "";
220546
+ const marker = webUrl.includes("/-/merge_requests/") ? "!" : "#";
220547
+ const idx = full.lastIndexOf(marker);
220548
+ return idx > 0 ? full.slice(0, idx) : void 0;
220549
+ }
220550
+ if (typeof raw?.web_url === "string") {
220551
+ const marker = raw.web_url.includes("/-/merge_requests/") ? "/-/merge_requests/" : "/-/issues/";
220552
+ try {
220553
+ const url2 = new URL(raw.web_url);
220554
+ const idx = url2.pathname.indexOf(marker);
220555
+ return idx > 0 ? decodeURIComponent(url2.pathname.slice(1, idx)) : void 0;
220556
+ } catch {
220557
+ return void 0;
220558
+ }
220559
+ }
220560
+ return void 0;
220561
+ }
220562
+ function normalizeIssue(input, resourceKind, extras = {}) {
220563
+ const raw = asRecord2(input);
220564
+ return {
220565
+ resourceKind,
220566
+ ...typeof raw.id === "number" ? { id: raw.id } : {},
220567
+ iid: Number(raw.iid),
220568
+ ...typeof raw.project_id === "number" ? { projectId: raw.project_id } : {},
220569
+ ...readProjectPath(raw) ? { projectPath: readProjectPath(raw) } : {},
220570
+ title: String(raw.title ?? ""),
220571
+ description: typeof raw.description === "string" ? raw.description : null,
220572
+ webUrl: String(raw.web_url ?? ""),
220573
+ state: String(raw.state ?? "unknown"),
220574
+ author: normalizeAuthor(raw.author),
220575
+ labels: normalizeLabels(raw.labels),
220576
+ ...typeof raw.created_at === "string" ? { createdAt: raw.created_at } : {},
220577
+ ...typeof raw.updated_at === "string" ? { updatedAt: raw.updated_at } : {},
220578
+ ...typeof raw.closed_at === "string" ? { closedAt: raw.closed_at } : {},
220579
+ ...typeof raw.user_notes_count === "number" ? { commentsCount: raw.user_notes_count } : {},
220580
+ ...extras
220581
+ };
220582
+ }
220583
+ function normalizeMergeRequest(input, extras = {}) {
220584
+ const raw = asRecord2(input);
220585
+ return {
220586
+ resourceKind: "merge_request",
220587
+ ...typeof raw.id === "number" ? { id: raw.id } : {},
220588
+ iid: Number(raw.iid),
220589
+ ...typeof raw.project_id === "number" ? { projectId: raw.project_id } : {},
220590
+ ...readProjectPath(raw) ? { projectPath: readProjectPath(raw) } : {},
220591
+ title: String(raw.title ?? ""),
220592
+ description: typeof raw.description === "string" ? raw.description : null,
220593
+ webUrl: String(raw.web_url ?? ""),
220594
+ state: String(raw.state ?? "unknown"),
220595
+ author: normalizeAuthor(raw.author),
220596
+ labels: normalizeLabels(raw.labels),
220597
+ ...typeof raw.created_at === "string" ? { createdAt: raw.created_at } : {},
220598
+ ...typeof raw.updated_at === "string" ? { updatedAt: raw.updated_at } : {},
220599
+ ...typeof raw.closed_at === "string" ? { closedAt: raw.closed_at } : {},
220600
+ ...typeof raw.merged_at === "string" ? { mergedAt: raw.merged_at } : {},
220601
+ ...typeof raw.user_notes_count === "number" ? { commentsCount: raw.user_notes_count } : {},
220602
+ ...typeof raw.source_branch === "string" ? { sourceBranch: raw.source_branch } : {},
220603
+ ...typeof raw.target_branch === "string" ? { targetBranch: raw.target_branch } : {},
220604
+ ...typeof raw.draft === "boolean" ? { draft: raw.draft } : {},
220605
+ ...extras
220606
+ };
220607
+ }
220608
+ function mapGitLabError(status) {
220609
+ switch (status) {
220610
+ case 401:
220611
+ return "GitLab authentication failed. Check gitlabAuthToken and required read_api scope.";
220612
+ case 403:
220613
+ return "GitLab authorization failed. Check token scope and project/group access.";
220614
+ case 404:
220615
+ return "GitLab project, group, issue, or merge request was not found.";
220616
+ case 429:
220617
+ return "GitLab rate limit exceeded. Try again later.";
220618
+ default:
220619
+ return status >= 500 ? "GitLab is unavailable. Try again later." : `GitLab API request failed with status ${status}.`;
220620
+ }
220621
+ }
220622
+ function projectIdentity(item) {
220623
+ return item.projectPath ?? (item.projectId !== void 0 ? String(item.projectId) : "unknown-project");
220624
+ }
220625
+ function buildGitLabTaskProvenance(args) {
220626
+ const { auth: auth2, resourceType, item } = args;
220627
+ const groupPathFromInput = typeof args.groupInput === "string" ? args.groupInput : void 0;
220628
+ const groupIdFromInput = typeof args.groupInput === "number" ? args.groupInput : void 0;
220629
+ const repository = projectIdentity(item);
220630
+ const externalIssueId = resourceType === "merge_request" ? `gitlab:mr:${item.projectId ?? repository}:${item.id ?? item.iid}` : String(item.id ?? `${item.projectId ?? repository}:${item.iid}`);
220631
+ const url2 = new URL(item.webUrl);
220632
+ return {
220633
+ sourceIssue: {
220634
+ provider: "gitlab",
220635
+ repository,
220636
+ externalIssueId,
220637
+ issueNumber: item.iid,
220638
+ url: item.webUrl
220639
+ },
220640
+ gitlabTracking: {
220641
+ item: {
220642
+ kind: resourceType,
220643
+ url: item.webUrl,
220644
+ instanceUrl: auth2.webBaseUrl,
220645
+ host: url2.host,
220646
+ iid: item.iid,
220647
+ ...typeof item.id === "number" ? { id: item.id } : {},
220648
+ ...typeof item.projectId === "number" ? { projectId: item.projectId } : {},
220649
+ ...typeof item.projectPath === "string" ? { projectPath: item.projectPath } : {},
220650
+ ..."groupId" in item && item.groupId !== void 0 ? { groupId: item.groupId } : groupIdFromInput !== void 0 ? { groupId: groupIdFromInput } : {},
220651
+ ..."groupPath" in item && item.groupPath !== void 0 ? { groupPath: item.groupPath } : groupPathFromInput !== void 0 ? { groupPath: groupPathFromInput } : {},
220652
+ title: item.title,
220653
+ state: item.state,
220654
+ createdAt: item.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
220655
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
220656
+ ...item.updatedAt ? { lastSyncedAt: item.updatedAt } : {}
220657
+ }
220658
+ },
220659
+ sourceMetadata: {
220660
+ provider: "gitlab",
220661
+ resourceType,
220662
+ instanceUrl: auth2.webBaseUrl,
220663
+ apiBaseUrl: auth2.apiBaseUrl,
220664
+ projectId: item.projectId,
220665
+ projectPath: item.projectPath,
220666
+ groupId: "groupId" in item && item.groupId !== void 0 ? item.groupId : groupIdFromInput,
220667
+ groupPath: "groupPath" in item && item.groupPath !== void 0 ? item.groupPath : groupPathFromInput,
220668
+ projectInput: args.projectInput,
220669
+ groupInput: args.groupInput,
220670
+ iid: item.iid,
220671
+ webUrl: item.webUrl,
220672
+ ...resourceType === "merge_request" ? {
220673
+ mergeRequestId: item.id,
220674
+ mergeRequestIid: item.iid,
220675
+ sourceBranch: item.sourceBranch,
220676
+ targetBranch: item.targetBranch,
220677
+ draft: item.draft
220678
+ } : {
220679
+ issueId: item.id,
220680
+ issueIid: item.iid
220681
+ }
220682
+ }
220683
+ };
220684
+ }
220685
+ function isGitLabAlreadyImported(task, provenance) {
220686
+ const sourceUrl = provenance.sourceIssue.url;
220687
+ if (sourceUrl && task.description?.includes(sourceUrl)) return true;
220688
+ if (task.sourceIssue?.provider === "gitlab" && task.sourceIssue.externalIssueId === provenance.sourceIssue.externalIssueId) return true;
220689
+ const metadata = task.source?.sourceMetadata;
220690
+ if (task.source?.sourceType === "gitlab_import" && metadata && typeof metadata === "object") {
220691
+ const meta3 = metadata;
220692
+ return meta3.resourceType === provenance.sourceMetadata.resourceType && meta3.iid === provenance.sourceMetadata.iid && (meta3.projectId === provenance.sourceMetadata.projectId || meta3.projectPath === provenance.sourceMetadata.projectPath) && (meta3.webUrl === sourceUrl || sourceUrl === void 0);
220693
+ }
220694
+ return false;
220695
+ }
220696
+ function buildGitLabTaskDescription(item) {
220697
+ const body = item.description?.trim() || "(no description)";
220698
+ return `${body}
220699
+
220700
+ Source: ${item.webUrl}`;
220701
+ }
220702
+ var GitLabApiError, DEFAULT_LIMIT3, MAX_LIMIT, PAGE_SIZE, GitLabClient;
220703
+ var init_gitlab = __esm({
220704
+ "../dashboard/src/gitlab.ts"() {
220705
+ "use strict";
220706
+ GitLabApiError = class extends Error {
220707
+ constructor(status, message) {
220708
+ super(message);
220709
+ this.status = status;
220710
+ this.name = "GitLabApiError";
220711
+ }
220712
+ };
220713
+ DEFAULT_LIMIT3 = 30;
220714
+ MAX_LIMIT = 100;
220715
+ PAGE_SIZE = 100;
220716
+ GitLabClient = class {
220717
+ constructor(auth2, fetchImpl = fetch) {
220718
+ this.auth = auth2;
220719
+ this.fetchImpl = fetchImpl;
220720
+ }
220721
+ async request(path6, init) {
220722
+ const url2 = `${this.auth.apiBaseUrl.replace(/\/+$/u, "")}/${path6.replace(/^\/+/, "")}`;
220723
+ const response = await this.fetchImpl(url2, {
220724
+ ...init,
220725
+ headers: {
220726
+ Accept: "application/json",
220727
+ [this.auth.headerName]: this.auth.token,
220728
+ ...init?.body ? { "Content-Type": "application/json" } : {},
220729
+ ...init?.headers ?? {}
220730
+ }
220731
+ });
220732
+ if (!response.ok) {
220733
+ throw new GitLabApiError(response.status, mapGitLabError(response.status));
220734
+ }
220735
+ if (response.status === 204) return void 0;
220736
+ return await response.json();
220737
+ }
220738
+ async listPaginated(path6, options, normalize8) {
220739
+ const limit = clampLimit(options.limit);
220740
+ const out = [];
220741
+ let page = 1;
220742
+ while (out.length < limit && page <= Math.ceil(limit / PAGE_SIZE) + 1) {
220743
+ const params = new URLSearchParams();
220744
+ params.set("per_page", String(Math.min(PAGE_SIZE, limit - out.length)));
220745
+ params.set("page", String(page));
220746
+ appendListParams(params, options);
220747
+ const rows = await this.request(`${path6}?${params.toString()}`);
220748
+ if (!Array.isArray(rows) || rows.length === 0) break;
220749
+ out.push(...rows.map(normalize8));
220750
+ if (rows.length < Number(params.get("per_page"))) break;
220751
+ page += 1;
220752
+ }
220753
+ return out.slice(0, limit);
220754
+ }
220755
+ listProjectIssues(project, options = {}) {
220756
+ return this.listPaginated(`projects/${encodeGitLabPathId(project)}/issues`, options, (raw) => normalizeIssue(raw, "project_issue"));
220757
+ }
220758
+ listGroupIssues(group, options = {}) {
220759
+ return this.listPaginated(`groups/${encodeGitLabPathId(group)}/issues`, options, (raw) => normalizeIssue(raw, "group_issue", {
220760
+ groupPath: typeof group === "string" ? group : void 0,
220761
+ groupId: typeof group === "number" ? group : void 0
220762
+ }));
220763
+ }
220764
+ listMergeRequests(project, options = {}) {
220765
+ return this.listPaginated(`projects/${encodeGitLabPathId(project)}/merge_requests`, options, normalizeMergeRequest);
220766
+ }
220767
+ async getProjectIssue(project, iid) {
220768
+ return normalizeIssue(await this.request(`projects/${encodeGitLabPathId(project)}/issues/${iid}`), "project_issue");
220769
+ }
220770
+ async getMergeRequest(project, iid) {
220771
+ return normalizeMergeRequest(await this.request(`projects/${encodeGitLabPathId(project)}/merge_requests/${iid}`));
220772
+ }
220773
+ /*
220774
+ FNXC:GitLabLifecycle 2026-07-02-00:00:
220775
+ GitLab lifecycle side effects must use REST notes and state_event APIs for GitLab.com and self-managed instances. Keep project identifiers URL-encoded, token auth header-based, and never introduce a local GitLab CLI dependency.
220776
+ */
220777
+ async commentOnProjectIssue(project, iid, body) {
220778
+ await this.request(`projects/${encodeGitLabPathId(project)}/issues/${iid}/notes`, {
220779
+ method: "POST",
220780
+ body: JSON.stringify({ body })
220781
+ });
220782
+ }
220783
+ async commentOnMergeRequest(project, iid, body) {
220784
+ await this.request(`projects/${encodeGitLabPathId(project)}/merge_requests/${iid}/notes`, {
220785
+ method: "POST",
220786
+ body: JSON.stringify({ body })
220787
+ });
220788
+ }
220789
+ async setProjectIssueState(project, iid, state) {
220790
+ const params = new URLSearchParams({ state_event: state === "closed" ? "close" : "reopen" });
220791
+ return normalizeIssue(await this.request(`projects/${encodeGitLabPathId(project)}/issues/${iid}?${params.toString()}`, { method: "PUT" }), "project_issue");
220792
+ }
220793
+ async setMergeRequestState(project, iid, state) {
220794
+ const params = new URLSearchParams({ state_event: state === "closed" ? "close" : "reopen" });
220795
+ return normalizeMergeRequest(await this.request(`projects/${encodeGitLabPathId(project)}/merge_requests/${iid}?${params.toString()}`, { method: "PUT" }));
220796
+ }
220797
+ };
220798
+ }
220799
+ });
220800
+
220801
+ // ../dashboard/src/gitlab-auth.ts
220802
+ function pickString2(source, key) {
220803
+ const value = source?.[key];
220804
+ return typeof value === "string" ? value : void 0;
220805
+ }
220806
+ function readConfiguredString2(value) {
220807
+ if (typeof value !== "string") return void 0;
220808
+ const trimmed = value.trim();
220809
+ return trimmed.length > 0 ? trimmed : void 0;
220810
+ }
220811
+ function normalizeTokenType(value) {
220812
+ if (value === void 0 || value === null || value === "") return void 0;
220813
+ if (typeof value !== "string") return "invalid";
220814
+ const trimmed = value.trim();
220815
+ if (trimmed === "") return void 0;
220816
+ return GITLAB_AUTH_TOKEN_TYPES.includes(trimmed) ? trimmed : "invalid";
220817
+ }
220818
+ function firstConfiguredTokenType(...values) {
220819
+ for (const value of values) {
220820
+ const normalized = normalizeTokenType(value);
220821
+ if (normalized !== void 0) return normalized;
220822
+ }
220823
+ return void 0;
220824
+ }
220825
+ function resolveGitlabAuth(deps = {}) {
220826
+ if (!resolveGitlabEnabled({ project: deps.projectSettings ?? void 0, global: deps.globalSettings })) {
220827
+ return { ok: false, reason: "disabled", message: "GitLab integration is disabled in Settings." };
220828
+ }
220829
+ let config2;
220830
+ try {
220831
+ config2 = resolveGitlabConfig({
220832
+ project: deps.projectSettings ?? void 0,
220833
+ global: deps.globalSettings
220834
+ });
220835
+ } catch (error2) {
220836
+ return {
220837
+ ok: false,
220838
+ reason: "invalid_config",
220839
+ message: error2 instanceof Error ? error2.message : "Invalid GitLab configuration."
220840
+ };
220841
+ }
220842
+ const project = deps.projectSettings;
220843
+ const global2 = deps.globalSettings;
220844
+ const env = deps.env ?? process.env;
220845
+ const token = readConfiguredString2(project?.gitlabAuthToken) ?? readConfiguredString2(global2?.gitlabAuthToken) ?? readConfiguredString2(pickString2(global2, "projectGitlabAuthToken")) ?? readConfiguredString2(env.GITLAB_TOKEN) ?? "";
220846
+ const tokenType = firstConfiguredTokenType(
220847
+ project?.gitlabAuthTokenType,
220848
+ global2?.gitlabAuthTokenType,
220849
+ pickString2(global2, "projectGitlabAuthTokenType")
220850
+ );
220851
+ if (tokenType === "invalid") {
220852
+ return {
220853
+ ok: false,
220854
+ reason: "invalid_token_type",
220855
+ message: 'Invalid gitlabAuthTokenType. Expected "personal", "project", or "group".'
220856
+ };
220857
+ }
220858
+ if (!token) {
220859
+ return {
220860
+ ok: false,
220861
+ reason: "token_missing",
220862
+ message: "GitLab auth requires gitlabAuthToken or GITLAB_TOKEN."
220863
+ };
220864
+ }
220865
+ return {
220866
+ ok: true,
220867
+ auth: {
220868
+ apiBaseUrl: config2.apiBaseUrl,
220869
+ webBaseUrl: config2.instanceUrl,
220870
+ token,
220871
+ tokenType: tokenType ?? "personal",
220872
+ headerName: GITLAB_AUTH_HEADER_NAME
220873
+ }
220874
+ };
220875
+ }
220876
+ var GITLAB_AUTH_HEADER_NAME, GITLAB_AUTH_TOKEN_TYPES;
220877
+ var init_gitlab_auth = __esm({
220878
+ "../dashboard/src/gitlab-auth.ts"() {
220879
+ "use strict";
220880
+ init_src();
220881
+ GITLAB_AUTH_HEADER_NAME = "PRIVATE-TOKEN";
220882
+ GITLAB_AUTH_TOKEN_TYPES = ["personal", "project", "group"];
220883
+ }
220884
+ });
220885
+
220886
+ // ../dashboard/src/gitlab-lifecycle.ts
220887
+ var init_gitlab_lifecycle = __esm({
220888
+ "../dashboard/src/gitlab-lifecycle.ts"() {
220889
+ "use strict";
220890
+ init_gitlab();
220891
+ init_gitlab_auth();
220892
+ }
220893
+ });
220894
+
220895
+ // ../dashboard/src/gitlab-issue-comment.ts
220896
+ var init_gitlab_issue_comment = __esm({
220897
+ "../dashboard/src/gitlab-issue-comment.ts"() {
220898
+ "use strict";
220899
+ init_gitlab_lifecycle();
220900
+ }
220901
+ });
220902
+
220903
+ // ../dashboard/src/gitlab-tracking-comments.ts
220904
+ var init_gitlab_tracking_comments = __esm({
220905
+ "../dashboard/src/gitlab-tracking-comments.ts"() {
220906
+ "use strict";
220907
+ init_gitlab_lifecycle();
220908
+ }
220909
+ });
220910
+
220911
+ // ../dashboard/src/gitlab-tracking-state.ts
220912
+ var init_gitlab_tracking_state = __esm({
220913
+ "../dashboard/src/gitlab-tracking-state.ts"() {
220914
+ "use strict";
220915
+ init_gitlab();
220916
+ init_gitlab_lifecycle();
220917
+ init_github_tracking_state();
220918
+ }
220919
+ });
220920
+
220921
+ // ../dashboard/src/gitlab-source-issue-close.ts
220922
+ var init_gitlab_source_issue_close = __esm({
220923
+ "../dashboard/src/gitlab-source-issue-close.ts"() {
220924
+ "use strict";
220925
+ init_gitlab_lifecycle();
220926
+ init_gitlab_tracking_state();
220927
+ init_github_tracking_state();
220928
+ }
220929
+ });
220930
+
219578
220931
  // ../dashboard/src/knowledge-index.ts
219579
220932
  var init_knowledge_index = __esm({
219580
220933
  "../dashboard/src/knowledge-index.ts"() {
@@ -220244,6 +221597,10 @@ var init_register_git_github = __esm({
220244
221597
  init_github_tracking_state();
220245
221598
  init_github_tracking_reconciler();
220246
221599
  init_github_source_issue_close();
221600
+ init_gitlab_issue_comment();
221601
+ init_gitlab_tracking_comments();
221602
+ init_gitlab_tracking_state();
221603
+ init_gitlab_source_issue_close();
220247
221604
  init_knowledge_index_refresh();
220248
221605
  init_github_poll();
220249
221606
  init_project_store_resolver();
@@ -225194,6 +226551,25 @@ var init_register_messaging_scripts = __esm({
225194
226551
  }
225195
226552
  });
225196
226553
 
226554
+ // ../dashboard/src/gitlab-source-issue-reconciler.ts
226555
+ var init_gitlab_source_issue_reconciler = __esm({
226556
+ "../dashboard/src/gitlab-source-issue-reconciler.ts"() {
226557
+ "use strict";
226558
+ init_gitlab_lifecycle();
226559
+ }
226560
+ });
226561
+
226562
+ // ../dashboard/src/routes/register-gitlab.ts
226563
+ var init_register_gitlab = __esm({
226564
+ "../dashboard/src/routes/register-gitlab.ts"() {
226565
+ "use strict";
226566
+ init_api_error();
226567
+ init_gitlab_auth();
226568
+ init_gitlab();
226569
+ init_gitlab_source_issue_reconciler();
226570
+ }
226571
+ });
226572
+
225197
226573
  // ../dashboard/src/terminal.ts
225198
226574
  import { spawn as spawn14 } from "node:child_process";
225199
226575
  import { randomUUID as randomUUID26 } from "node:crypto";
@@ -225667,7 +227043,7 @@ var init_register_secrets_sync_inbound_routes = __esm({
225667
227043
  });
225668
227044
 
225669
227045
  // ../dashboard/src/routes/register-agent-core-routes.ts
225670
- var MAX_AVATAR_BYTES;
227046
+ var MAX_AVATAR_BYTES, VALID_AGENT_PERMISSION_KEYS;
225671
227047
  var init_register_agent_core_routes = __esm({
225672
227048
  "../dashboard/src/routes/register-agent-core-routes.ts"() {
225673
227049
  "use strict";
@@ -225675,6 +227051,7 @@ var init_register_agent_core_routes = __esm({
225675
227051
  init_api_error();
225676
227052
  init_src4();
225677
227053
  MAX_AVATAR_BYTES = 2 * 1024 * 1024;
227054
+ VALID_AGENT_PERMISSION_KEYS = new Set(AGENT_PERMISSIONS);
225678
227055
  }
225679
227056
  });
225680
227057
 
@@ -225896,6 +227273,14 @@ var init_pagerduty = __esm({
225896
227273
  }
225897
227274
  });
225898
227275
 
227276
+ // ../dashboard/src/signal-sources/gitlab.ts
227277
+ var init_gitlab2 = __esm({
227278
+ "../dashboard/src/signal-sources/gitlab.ts"() {
227279
+ "use strict";
227280
+ init_signal_source();
227281
+ }
227282
+ });
227283
+
225899
227284
  // ../dashboard/src/routes/register-signal-routes.ts
225900
227285
  var init_register_signal_routes = __esm({
225901
227286
  "../dashboard/src/routes/register-signal-routes.ts"() {
@@ -225907,6 +227292,7 @@ var init_register_signal_routes = __esm({
225907
227292
  init_sentry();
225908
227293
  init_datadog();
225909
227294
  init_pagerduty();
227295
+ init_gitlab2();
225910
227296
  }
225911
227297
  });
225912
227298
 
@@ -225979,17 +227365,32 @@ var init_droid_cli_probe = __esm({
225979
227365
 
225980
227366
  // ../../plugins/fusion-plugin-cursor-runtime/src/cli-spawn.ts
225981
227367
  import { spawn as spawn15 } from "node:child_process";
227368
+ function formatSpawnError(error2) {
227369
+ const code = typeof error2.code === "string" ? `${error2.code}: ` : "";
227370
+ return `spawn error: ${code}${error2.message}`.trim();
227371
+ }
225982
227372
  async function runCursorCommand(binary, args, timeoutMs) {
225983
227373
  return new Promise((resolve34) => {
225984
- const child = spawn15(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
225985
227374
  let stdout = "";
225986
227375
  let stderr = "";
225987
- const timer = setTimeout(() => {
227376
+ let settled = false;
227377
+ let timer;
227378
+ const finish = (result) => {
227379
+ if (settled) return;
227380
+ settled = true;
227381
+ if (timer) clearTimeout(timer);
227382
+ resolve34(result);
227383
+ };
227384
+ const child = spawn15(binary, args, {
227385
+ stdio: ["ignore", "pipe", "pipe"],
227386
+ shell: process.platform === "win32"
227387
+ });
227388
+ timer = setTimeout(() => {
225988
227389
  try {
225989
227390
  child.kill("SIGKILL");
225990
227391
  } catch {
225991
227392
  }
225992
- resolve34({ code: 124, stdout, stderr });
227393
+ finish({ code: 124, stdout, stderr });
225993
227394
  }, timeoutMs);
225994
227395
  child.stdout?.on("data", (c) => {
225995
227396
  stdout += c.toString("utf-8");
@@ -225997,13 +227398,14 @@ async function runCursorCommand(binary, args, timeoutMs) {
225997
227398
  child.stderr?.on("data", (c) => {
225998
227399
  stderr += c.toString("utf-8");
225999
227400
  });
226000
- child.once("error", () => {
226001
- clearTimeout(timer);
226002
- resolve34({ code: 127, stdout, stderr });
227401
+ child.once("error", (error2) => {
227402
+ const diagnostic = formatSpawnError(error2);
227403
+ stderr = stderr ? `${stderr}
227404
+ ${diagnostic}` : diagnostic;
227405
+ finish({ code: 127, stdout, stderr });
226003
227406
  });
226004
227407
  child.once("close", (code) => {
226005
- clearTimeout(timer);
226006
- resolve34({ code, stdout, stderr });
227408
+ finish({ code, stdout, stderr });
226007
227409
  });
226008
227410
  });
226009
227411
  }
@@ -226014,20 +227416,40 @@ var init_cli_spawn2 = __esm({
226014
227416
  });
226015
227417
 
226016
227418
  // ../../plugins/fusion-plugin-cursor-runtime/src/probe.ts
227419
+ function buildCandidates(binaryPath) {
227420
+ const configuredBinaryPath = binaryPath?.trim() || void 0;
227421
+ const ordered = configuredBinaryPath ? [configuredBinaryPath, ...CANDIDATES2] : [...CANDIDATES2];
227422
+ return { candidates: Array.from(new Set(ordered)), configuredBinaryPath };
227423
+ }
227424
+ function summarizeFailure(binary, stdout, stderr) {
227425
+ const detail = `${stderr || stdout}`.replace(/\s+/g, " ").trim();
227426
+ if (!detail) return void 0;
227427
+ const truncated = detail.length > MAX_FAILURE_DETAIL_LENGTH ? `${detail.slice(0, MAX_FAILURE_DETAIL_LENGTH - 1)}\u2026` : detail;
227428
+ return `${binary}: ${truncated}`;
227429
+ }
226017
227430
  async function probeCursorBinary(options) {
226018
227431
  const startedAt = Date.now();
226019
227432
  const timeoutMs = options?.timeoutMs ?? 3e3;
226020
- const candidates = options?.binaryPath ? [options.binaryPath] : [...CANDIDATES2];
227433
+ const { candidates, configuredBinaryPath } = buildCandidates(options?.binaryPath);
227434
+ const failureDetails = [];
226021
227435
  for (const binary of candidates) {
226022
227436
  const version2 = await runCursorCommand(binary, ["--version"], timeoutMs);
227437
+ const failureDetail = summarizeFailure(binary, version2.stdout, version2.stderr);
227438
+ if (failureDetail) failureDetails.push(failureDetail);
227439
+ const common = {
227440
+ binaryName: binary,
227441
+ binaryPath: binary,
227442
+ configuredBinaryPath,
227443
+ usingConfiguredBinaryPath: configuredBinaryPath === binary,
227444
+ diagnostics: failureDetails.length > 0 ? [...failureDetails] : void 0,
227445
+ probeDurationMs: Date.now() - startedAt
227446
+ };
226023
227447
  if (version2.code === 0) {
226024
227448
  return {
226025
227449
  available: true,
226026
227450
  authenticated: true,
226027
- binaryName: binary,
226028
- binaryPath: binary,
226029
- version: version2.stdout.trim() || void 0,
226030
- probeDurationMs: Date.now() - startedAt
227451
+ ...common,
227452
+ version: version2.stdout.trim() || void 0
226031
227453
  };
226032
227454
  }
226033
227455
  const combined = `${version2.stdout}
@@ -226036,36 +227458,37 @@ ${version2.stderr}`.toLowerCase();
226036
227458
  return {
226037
227459
  available: true,
226038
227460
  authenticated: false,
226039
- binaryName: binary,
226040
- binaryPath: binary,
226041
- reason: "macOS login keychain is locked",
226042
- probeDurationMs: Date.now() - startedAt
227461
+ ...common,
227462
+ reason: "macOS login keychain is locked"
226043
227463
  };
226044
227464
  }
226045
227465
  if (combined.includes("no cursor ide installation found")) {
226046
227466
  return {
226047
227467
  available: true,
226048
227468
  authenticated: false,
226049
- binaryName: binary,
226050
- binaryPath: binary,
226051
- reason: "Cursor IDE installation not found",
226052
- probeDurationMs: Date.now() - startedAt
227469
+ ...common,
227470
+ reason: "Cursor IDE installation not found"
226053
227471
  };
226054
227472
  }
226055
227473
  }
227474
+ const baseReason = configuredBinaryPath ? `Configured Cursor CLI binary '${configuredBinaryPath}' failed; PATH fallback cursor-agent/cursor also failed` : "cursor-agent/cursor not found on PATH";
226056
227475
  return {
226057
227476
  available: false,
226058
227477
  authenticated: false,
226059
- reason: "cursor-agent/cursor not found on PATH",
227478
+ configuredBinaryPath,
227479
+ usingConfiguredBinaryPath: false,
227480
+ diagnostics: failureDetails.length > 0 ? failureDetails : void 0,
227481
+ reason: failureDetails.length > 0 ? `${baseReason} (${failureDetails.join("; ")})` : baseReason,
226060
227482
  probeDurationMs: Date.now() - startedAt
226061
227483
  };
226062
227484
  }
226063
- var CANDIDATES2;
227485
+ var CANDIDATES2, MAX_FAILURE_DETAIL_LENGTH;
226064
227486
  var init_probe4 = __esm({
226065
227487
  "../../plugins/fusion-plugin-cursor-runtime/src/probe.ts"() {
226066
227488
  "use strict";
226067
227489
  init_cli_spawn2();
226068
227490
  CANDIDATES2 = ["cursor-agent", "cursor"];
227491
+ MAX_FAILURE_DETAIL_LENGTH = 180;
226069
227492
  }
226070
227493
  });
226071
227494
 
@@ -226109,12 +227532,20 @@ var init_process_manager = __esm({
226109
227532
  });
226110
227533
 
226111
227534
  // ../../plugins/fusion-plugin-cursor-runtime/src/provider.ts
226112
- async function discoverCursorProviderModels() {
226113
- const probe = await probeCursorBinary();
227535
+ function normalizeDiscoveryOptions(options) {
227536
+ if (!options || typeof options !== "object") return {};
227537
+ const record2 = options;
227538
+ return {
227539
+ binaryPath: typeof record2.binaryPath === "string" ? record2.binaryPath : void 0,
227540
+ timeoutMs: typeof record2.timeoutMs === "number" ? record2.timeoutMs : void 0
227541
+ };
227542
+ }
227543
+ async function discoverCursorProviderModels(options) {
227544
+ const probe = await probeCursorBinary(normalizeDiscoveryOptions(options));
226114
227545
  if (!probe.available || !probe.binaryName) {
226115
227546
  return { models: [], source: "probe", fallbackUsed: true, reason: probe.reason ?? "binary unavailable" };
226116
227547
  }
226117
- const result = await discoverCursorModels(probe.binaryName);
227548
+ const result = await discoverCursorModels(probe.binaryPath ?? probe.binaryName);
226118
227549
  return {
226119
227550
  models: result.models.map((id) => ({ id, label: id })),
226120
227551
  source: result.source,
@@ -229072,6 +230503,7 @@ var init_routes = __esm({
229072
230503
  init_register_secrets_routes();
229073
230504
  init_register_messaging_scripts();
229074
230505
  init_register_git_github();
230506
+ init_register_gitlab();
229075
230507
  init_register_files_terminal_workspaces();
229076
230508
  init_register_agents_projects_nodes();
229077
230509
  init_register_project_routes();
@@ -229356,7 +230788,7 @@ var init_otel_exporter = __esm({
229356
230788
 
229357
230789
  // ../dashboard/src/server.ts
229358
230790
  import express2 from "express";
229359
- import { join as join64, dirname as dirname28 } from "node:path";
230791
+ import { join as join64, dirname as dirname29 } from "node:path";
229360
230792
  import { fileURLToPath as fileURLToPath6 } from "node:url";
229361
230793
  import { WebSocketServer as WebSocketServer2 } from "ws";
229362
230794
  function clearAiSessionCleanupInterval() {
@@ -229402,7 +230834,7 @@ var init_server2 = __esm({
229402
230834
  init_reliability_metrics();
229403
230835
  init_view_chunk_manifest();
229404
230836
  init_otel_exporter();
229405
- __dirname = dirname28(fileURLToPath6(import.meta.url));
230837
+ __dirname = dirname29(fileURLToPath6(import.meta.url));
229406
230838
  MIN_AI_SESSION_TTL_MS = 10 * 60 * 1e3;
229407
230839
  MAX_AI_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
229408
230840
  MIN_AI_SESSION_CLEANUP_INTERVAL_MS = 60 * 1e3;
@@ -229470,6 +230902,8 @@ var init_src11 = __esm({
229470
230902
  init_github_tracking2();
229471
230903
  init_github_tracking_dedup();
229472
230904
  init_github_tracking_hook();
230905
+ init_gitlab();
230906
+ init_gitlab_auth();
229473
230907
  init_github_auth();
229474
230908
  init_rate_limit();
229475
230909
  init_github_poll();
@@ -229493,7 +230927,7 @@ var init_src11 = __esm({
229493
230927
  });
229494
230928
 
229495
230929
  // src/project-context.ts
229496
- import { resolve as resolve32, dirname as dirname29, basename as basename17 } from "node:path";
230930
+ import { resolve as resolve32, dirname as dirname30, basename as basename17 } from "node:path";
229497
230931
  async function resolveProject(projectNameFlag, cwd = process.cwd(), globalDir) {
229498
230932
  const central = new CentralCore(globalDir);
229499
230933
  await central.init();
@@ -229586,7 +231020,7 @@ async function detectProjectFromCwd(cwd, central) {
229586
231020
  };
229587
231021
  }
229588
231022
  }
229589
- const parentDir = dirname29(currentDir);
231023
+ const parentDir = dirname30(currentDir);
229590
231024
  if (parentDir === currentDir) {
229591
231025
  break;
229592
231026
  }
@@ -229818,6 +231252,7 @@ __export(task_exports, {
229818
231252
  runTaskDeps: () => runTaskDeps,
229819
231253
  runTaskDuplicate: () => runTaskDuplicate,
229820
231254
  runTaskImportFromGitHub: () => runTaskImportFromGitHub,
231255
+ runTaskImportFromGitLab: () => runTaskImportFromGitLab,
229821
231256
  runTaskImportGitHubInteractive: () => runTaskImportGitHubInteractive,
229822
231257
  runTaskList: () => runTaskList,
229823
231258
  runTaskLog: () => runTaskLog,
@@ -229889,6 +231324,8 @@ function formatTaskSource(task) {
229889
231324
  const issueUrl = getGitHubIssueUrl(task.sourceMetadata);
229890
231325
  return issueUrl ? `GitHub Import (${issueUrl})` : "GitHub Import";
229891
231326
  }
231327
+ case "gitlab_import":
231328
+ return "GitLab Import";
229892
231329
  case "research": {
229893
231330
  const context = getResearchSourceContext(task.sourceMetadata);
229894
231331
  return context ? `Research (${context})` : "Research";
@@ -230939,6 +232376,55 @@ Source: ${issue2.html_url}`;
230939
232376
  console.log(` \u2713 Imported ${created} tasks from ${owner}/${repo}${skipped > 0 ? ` (${skipped} skipped)` : ""}`);
230940
232377
  console.log();
230941
232378
  }
232379
+ async function createGitLabClientForStore(store) {
232380
+ const projectSettings = await store.getSettings();
232381
+ const globalSettings = await store.getGlobalSettingsStore().getSettings();
232382
+ const auth2 = resolveGitlabAuth({ projectSettings, globalSettings });
232383
+ if (!auth2.ok) throw new Error(auth2.message);
232384
+ return new GitLabClient(auth2.auth);
232385
+ }
232386
+ async function runTaskImportFromGitLab(target, options = {}, projectName) {
232387
+ const resource = options.resource ?? "project-issues";
232388
+ const store = await getStore(projectName);
232389
+ const client = await createGitLabClientForStore(store);
232390
+ console.log(`
232391
+ Importing GitLab ${resource} from ${target}...
232392
+ `);
232393
+ const labelArray = options.labels ?? [];
232394
+ const items = resource === "project-issues" ? await client.listProjectIssues(target, { limit: options.limit, labels: labelArray }) : resource === "group-issues" ? await client.listGroupIssues(target, { limit: options.limit, labels: labelArray }) : await client.listMergeRequests(target, { limit: options.limit, labels: labelArray });
232395
+ if (items.length === 0) {
232396
+ console.log(" No GitLab resources found.\n");
232397
+ return;
232398
+ }
232399
+ const existingTasks = await store.listTasks({ slim: false, includeArchived: false });
232400
+ let created = 0;
232401
+ let skipped = 0;
232402
+ for (const item of items) {
232403
+ const provenance = buildGitLabTaskProvenance({ auth: client.auth, resourceType: resource === "merge-requests" ? "merge_request" : resource === "group-issues" ? "group_issue" : "project_issue", item, projectInput: resource !== "group-issues" ? target : void 0, groupInput: resource === "group-issues" ? target : void 0 });
232404
+ if (existingTasks.some((task2) => isGitLabAlreadyImported(task2, provenance))) {
232405
+ console.log(` \u2192 Skipping #${item.iid}: already imported`);
232406
+ skipped += 1;
232407
+ continue;
232408
+ }
232409
+ const title = resource === "merge-requests" ? `Review MR !${item.iid}: ${item.title.slice(0, 180)}` : item.title.slice(0, 200);
232410
+ const task = await store.createTask({
232411
+ title: title || void 0,
232412
+ description: buildGitLabTaskDescription(item),
232413
+ column: "triage",
232414
+ dependencies: [],
232415
+ sourceIssue: provenance.sourceIssue,
232416
+ gitlabTracking: provenance.gitlabTracking,
232417
+ source: { sourceType: "gitlab_import", sourceMetadata: provenance.sourceMetadata }
232418
+ });
232419
+ await store.logEntry(task.id, resource === "merge-requests" ? "Imported merge request from GitLab" : "Imported from GitLab", item.webUrl);
232420
+ existingTasks.push(task);
232421
+ created += 1;
232422
+ console.log(` \u2713 Created ${task.id}: ${task.title}`);
232423
+ }
232424
+ console.log(`
232425
+ \u2713 Imported ${created} GitLab tasks${skipped > 0 ? ` (${skipped} skipped)` : ""}
232426
+ `);
232427
+ }
230942
232428
  async function runTaskComment(id, message, author = "user", projectName) {
230943
232429
  const store = await getStore(projectName);
230944
232430
  let text = message;
@@ -232837,6 +234323,107 @@ ${sourceUrl}`
232837
234323
  };
232838
234324
  }
232839
234325
  });
234326
+ async function createGitLabClient(ctx) {
234327
+ const store = await getStore2(ctx.cwd);
234328
+ const projectSettings = await store.getSettings();
234329
+ const globalSettings = await store.getGlobalSettingsStore().getSettings();
234330
+ const auth2 = resolveGitlabAuth({ projectSettings, globalSettings });
234331
+ if (!auth2.ok) throw new Error(auth2.message);
234332
+ return { store, client: new GitLabClient(auth2.auth) };
234333
+ }
234334
+ async function importGitLabItems(ctx, resourceType, target, items) {
234335
+ const { store, client } = await createGitLabClient(ctx);
234336
+ const existingTasks = await store.listTasks({ slim: false, includeArchived: false });
234337
+ const createdTasks = [];
234338
+ for (const item of items) {
234339
+ const provenance = buildGitLabTaskProvenance({ auth: client.auth, resourceType, item, projectInput: resourceType !== "group_issue" ? target : void 0, groupInput: resourceType === "group_issue" ? target : void 0 });
234340
+ if (existingTasks.some((task2) => isGitLabAlreadyImported(task2, provenance))) continue;
234341
+ const title = resourceType === "merge_request" ? `Review MR !${item.iid}: ${item.title.slice(0, 180)}` : item.title.slice(0, 200);
234342
+ const task = await store.createTask({ title: title || void 0, description: buildGitLabTaskDescription(item), column: "triage", dependencies: [], sourceIssue: provenance.sourceIssue, gitlabTracking: provenance.gitlabTracking, source: { sourceType: "gitlab_import", sourceMetadata: provenance.sourceMetadata } });
234343
+ await store.logEntry(task.id, resourceType === "merge_request" ? "Imported merge request from GitLab" : "Imported from GitLab", item.webUrl);
234344
+ existingTasks.push(task);
234345
+ createdTasks.push({ id: task.id, title: task.title || item.title });
234346
+ }
234347
+ return createdTasks;
234348
+ }
234349
+ pi2.registerTool({
234350
+ name: "fn_task_browse_gitlab_project_issues",
234351
+ label: "fn: Browse GitLab Project Issues",
234352
+ description: "List GitLab project issues from the configured GitLab instance.",
234353
+ promptSnippet: "Browse GitLab project issues",
234354
+ parameters: Type9.Object({ project: Type9.String({ description: "GitLab project path or numeric ID" }), limit: Type9.Optional(Type9.Number({ minimum: 1, maximum: 100 })), labels: Type9.Optional(Type9.Array(Type9.String())) }),
234355
+ async execute(_id, params, _signal, _onUpdate, ctx) {
234356
+ const { client } = await createGitLabClient(ctx);
234357
+ const issues = await client.listProjectIssues(params.project, { limit: params.limit, labels: params.labels });
234358
+ return { content: [{ type: "text", text: issues.map((issue2) => `#${issue2.iid}: ${issue2.title}
234359
+ ${issue2.webUrl}`).join("\n") || `No GitLab project issues found in ${params.project}.` }], details: { count: issues.length, issues } };
234360
+ }
234361
+ });
234362
+ pi2.registerTool({
234363
+ name: "fn_task_import_gitlab_project_issues",
234364
+ label: "fn: Import GitLab Project Issues",
234365
+ description: "Import GitLab project issues as Fusion tasks using configured GitLab HTTP API auth.",
234366
+ promptSnippet: "Import GitLab project issues",
234367
+ parameters: Type9.Object({ project: Type9.String({ description: "GitLab project path or numeric ID" }), limit: Type9.Optional(Type9.Number({ minimum: 1, maximum: 100 })), labels: Type9.Optional(Type9.Array(Type9.String())) }),
234368
+ async execute(_id, params, _signal, _onUpdate, ctx) {
234369
+ const { client } = await createGitLabClient(ctx);
234370
+ const issues = await client.listProjectIssues(params.project, { limit: params.limit, labels: params.labels });
234371
+ const createdTasks = await importGitLabItems(ctx, "project_issue", params.project, issues);
234372
+ return { content: [{ type: "text", text: `Imported ${createdTasks.length} GitLab project issue tasks.` }], details: { createdTasks } };
234373
+ }
234374
+ });
234375
+ pi2.registerTool({
234376
+ name: "fn_task_browse_gitlab_group_issues",
234377
+ label: "fn: Browse GitLab Group Issues",
234378
+ description: "List GitLab group issues while preserving each issue's originating project identity.",
234379
+ promptSnippet: "Browse GitLab group issues",
234380
+ parameters: Type9.Object({ group: Type9.String({ description: "GitLab group path or numeric ID" }), limit: Type9.Optional(Type9.Number({ minimum: 1, maximum: 100 })), labels: Type9.Optional(Type9.Array(Type9.String())) }),
234381
+ async execute(_id, params, _signal, _onUpdate, ctx) {
234382
+ const { client } = await createGitLabClient(ctx);
234383
+ const issues = await client.listGroupIssues(params.group, { limit: params.limit, labels: params.labels });
234384
+ return { content: [{ type: "text", text: issues.map((issue2) => `#${issue2.iid}: ${issue2.projectPath ?? issue2.projectId} \u2014 ${issue2.title}
234385
+ ${issue2.webUrl}`).join("\n") || `No GitLab group issues found in ${params.group}.` }], details: { count: issues.length, issues } };
234386
+ }
234387
+ });
234388
+ pi2.registerTool({
234389
+ name: "fn_task_import_gitlab_group_issues",
234390
+ label: "fn: Import GitLab Group Issues",
234391
+ description: "Import GitLab group issues as Fusion tasks using each issue's originating project identity.",
234392
+ promptSnippet: "Import GitLab group issues",
234393
+ parameters: Type9.Object({ group: Type9.String({ description: "GitLab group path or numeric ID" }), limit: Type9.Optional(Type9.Number({ minimum: 1, maximum: 100 })), labels: Type9.Optional(Type9.Array(Type9.String())) }),
234394
+ async execute(_id, params, _signal, _onUpdate, ctx) {
234395
+ const { client } = await createGitLabClient(ctx);
234396
+ const issues = await client.listGroupIssues(params.group, { limit: params.limit, labels: params.labels });
234397
+ const createdTasks = await importGitLabItems(ctx, "group_issue", params.group, issues);
234398
+ return { content: [{ type: "text", text: `Imported ${createdTasks.length} GitLab group issue tasks.` }], details: { createdTasks } };
234399
+ }
234400
+ });
234401
+ pi2.registerTool({
234402
+ name: "fn_task_browse_gitlab_merge_requests",
234403
+ label: "fn: Browse GitLab Merge Requests",
234404
+ description: "List GitLab project merge requests from the configured GitLab instance.",
234405
+ promptSnippet: "Browse GitLab merge requests",
234406
+ parameters: Type9.Object({ project: Type9.String({ description: "GitLab project path or numeric ID" }), limit: Type9.Optional(Type9.Number({ minimum: 1, maximum: 100 })), labels: Type9.Optional(Type9.Array(Type9.String())) }),
234407
+ async execute(_id, params, _signal, _onUpdate, ctx) {
234408
+ const { client } = await createGitLabClient(ctx);
234409
+ const mergeRequests = await client.listMergeRequests(params.project, { limit: params.limit, labels: params.labels });
234410
+ return { content: [{ type: "text", text: mergeRequests.map((mr2) => `!${mr2.iid}: ${mr2.title}
234411
+ ${mr2.webUrl}`).join("\n") || `No GitLab merge requests found in ${params.project}.` }], details: { count: mergeRequests.length, mergeRequests } };
234412
+ }
234413
+ });
234414
+ pi2.registerTool({
234415
+ name: "fn_task_import_gitlab_merge_requests",
234416
+ label: "fn: Import GitLab Merge Requests",
234417
+ description: "Import GitLab project merge requests as Fusion review tasks using configured GitLab HTTP API auth.",
234418
+ promptSnippet: "Import GitLab merge requests",
234419
+ parameters: Type9.Object({ project: Type9.String({ description: "GitLab project path or numeric ID" }), limit: Type9.Optional(Type9.Number({ minimum: 1, maximum: 100 })), labels: Type9.Optional(Type9.Array(Type9.String())) }),
234420
+ async execute(_id, params, _signal, _onUpdate, ctx) {
234421
+ const { client } = await createGitLabClient(ctx);
234422
+ const mergeRequests = await client.listMergeRequests(params.project, { limit: params.limit, labels: params.labels });
234423
+ const createdTasks = await importGitLabItems(ctx, "merge_request", params.project, mergeRequests);
234424
+ return { content: [{ type: "text", text: `Imported ${createdTasks.length} GitLab merge request tasks.` }], details: { createdTasks } };
234425
+ }
234426
+ });
232840
234427
  pi2.registerTool({
232841
234428
  name: "fn_task_plan",
232842
234429
  label: "fn: Plan Task",
@@ -234777,6 +236364,214 @@ Status: ${updated.status}`
234777
236364
  };
234778
236365
  }
234779
236366
  });
236367
+ pi2.registerTool({
236368
+ name: "fn_agent_update",
236369
+ label: "fn: Update Agent",
236370
+ description: "Update editable configuration for an existing non-ephemeral agent. Agent callers can only update direct or indirect reports inside their management subtree; user/operator calls are privileged.",
236371
+ promptSnippet: "Update an existing Fusion agent without deleting and recreating it",
236372
+ promptGuidelines: [
236373
+ "Use to update editable agent configuration such as role, instructions, manager, and heartbeat settings",
236374
+ "Agent callers can only target direct or indirect reports, never themselves, peers, ancestors, or unrelated agents",
236375
+ 'Use reportsTo as an agent ID/name for a new manager; privileged user/operator calls may pass reportsTo: "" to clear the manager'
236376
+ ],
236377
+ parameters: Type9.Object({
236378
+ agent_id: Type9.String({ description: "Target agent ID or name to update" }),
236379
+ name: Type9.Optional(Type9.String({ description: "New display name" })),
236380
+ role: Type9.Optional(Type9.Union([
236381
+ Type9.Literal("triage"),
236382
+ Type9.Literal("executor"),
236383
+ Type9.Literal("reviewer"),
236384
+ Type9.Literal("merger"),
236385
+ Type9.Literal("engineer"),
236386
+ Type9.Literal("custom")
236387
+ ], { description: "Agent role/capability" })),
236388
+ title: Type9.Optional(Type9.String({ description: "Optional title shown for the agent" })),
236389
+ icon: Type9.Optional(Type9.String({ description: "Optional compact icon/emoji" })),
236390
+ soul: Type9.Optional(Type9.String({ description: "Agent personality/identity text", maxLength: 1e4 })),
236391
+ instructions_text: Type9.Optional(Type9.String({ description: "Inline custom instructions", maxLength: 5e4 })),
236392
+ instructions_path: Type9.Optional(Type9.String({ description: "Path to instructions markdown", maxLength: 500 })),
236393
+ heartbeat_procedure_path: Type9.Optional(Type9.String({ description: "Path to heartbeat procedure markdown", maxLength: 500 })),
236394
+ reportsTo: Type9.Optional(Type9.String({ description: "Manager agent ID/name. Pass empty string to clear for privileged user/operator calls." })),
236395
+ heartbeat_interval_ms: Type9.Optional(Type9.Number({ minimum: 1e3, description: "Heartbeat polling interval in ms" })),
236396
+ heartbeat_timeout_ms: Type9.Optional(Type9.Number({ minimum: 5e3, description: "Heartbeat timeout in ms" })),
236397
+ max_concurrent_runs: Type9.Optional(Type9.Number({ minimum: 1, description: "Max concurrent heartbeat runs" })),
236398
+ message_response_mode: Type9.Optional(Type9.Union([
236399
+ Type9.Literal("immediate"),
236400
+ Type9.Literal("on-heartbeat")
236401
+ ], { description: "How agent responds to messages" }))
236402
+ }),
236403
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
236404
+ const { AgentStore: AgentStore2 } = await Promise.resolve().then(() => (init_src(), src_exports));
236405
+ const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
236406
+ await agentStore.init();
236407
+ const updateParamKeys = [
236408
+ "name",
236409
+ "role",
236410
+ "title",
236411
+ "icon",
236412
+ "soul",
236413
+ "instructions_text",
236414
+ "instructions_path",
236415
+ "heartbeat_procedure_path",
236416
+ "reportsTo",
236417
+ "heartbeat_interval_ms",
236418
+ "heartbeat_timeout_ms",
236419
+ "max_concurrent_runs",
236420
+ "message_response_mode"
236421
+ ];
236422
+ const providedKeys = updateParamKeys.filter((key) => params[key] !== void 0);
236423
+ const invalid = (field, message, extra = {}) => ({
236424
+ content: [{ type: "text", text: `ERROR: ${message}` }],
236425
+ isError: true,
236426
+ details: { outcome: "invalid", field, error: message, ...extra }
236427
+ });
236428
+ const denied = (message, extra = {}) => ({
236429
+ content: [{ type: "text", text: `ERROR: ${message}` }],
236430
+ isError: true,
236431
+ details: { outcome: "denied", error: message, ...extra }
236432
+ });
236433
+ if (providedKeys.length === 0) {
236434
+ return invalid("fields", "Provide at least one field to update");
236435
+ }
236436
+ if (params.soul !== void 0 && params.soul.length > 1e4) {
236437
+ return invalid("soul", "soul exceeds 10000 character limit");
236438
+ }
236439
+ if (params.instructions_text !== void 0 && params.instructions_text.length > 5e4) {
236440
+ return invalid("instructions_text", "instructions_text exceeds 50000 character limit");
236441
+ }
236442
+ if (params.instructions_path !== void 0 && params.instructions_path.length > 500) {
236443
+ return invalid("instructions_path", "instructions_path exceeds 500 character limit");
236444
+ }
236445
+ if (params.heartbeat_procedure_path !== void 0 && params.heartbeat_procedure_path.length > 500) {
236446
+ return invalid("heartbeat_procedure_path", "heartbeat_procedure_path exceeds 500 character limit");
236447
+ }
236448
+ if (params.heartbeat_interval_ms !== void 0 && params.heartbeat_interval_ms < 1e3) {
236449
+ return invalid("heartbeat_interval_ms", "heartbeat_interval_ms must be at least 1000");
236450
+ }
236451
+ if (params.heartbeat_timeout_ms !== void 0 && params.heartbeat_timeout_ms < 5e3) {
236452
+ return invalid("heartbeat_timeout_ms", "heartbeat_timeout_ms must be at least 5000");
236453
+ }
236454
+ if (params.max_concurrent_runs !== void 0 && params.max_concurrent_runs < 1) {
236455
+ return invalid("max_concurrent_runs", "max_concurrent_runs must be at least 1");
236456
+ }
236457
+ const target = await agentStore.getAgent(params.agent_id) ?? await agentStore.resolveAgent(params.agent_id);
236458
+ if (!target) {
236459
+ return {
236460
+ content: [{ type: "text", text: `Agent '${params.agent_id}' not found` }],
236461
+ isError: true,
236462
+ details: { outcome: "not_found", error: "Agent not found", agentId: params.agent_id }
236463
+ };
236464
+ }
236465
+ if (isEphemeralAgent(target)) {
236466
+ return invalid("agent_id", `Cannot update ephemeral/runtime agent ${target.id}`, { agentId: target.id });
236467
+ }
236468
+ const fnCtx = ctx;
236469
+ const callerAgentId = fnCtx.agentId;
236470
+ const targetChain = await agentStore.getChainOfCommand(target.id);
236471
+ if (callerAgentId) {
236472
+ if (callerAgentId === target.id) {
236473
+ return denied("You can only update your own direct or indirect reports, not yourself.", {
236474
+ agentId: target.id,
236475
+ callerAgentId,
236476
+ rule: "direct-or-indirect-reports-only"
236477
+ });
236478
+ }
236479
+ const callerIndex = targetChain.findIndex((agent) => agent.id === callerAgentId);
236480
+ if (callerIndex < 1) {
236481
+ return denied("You can only update your own direct or indirect reports.", {
236482
+ agentId: target.id,
236483
+ callerAgentId,
236484
+ rule: "direct-or-indirect-reports-only"
236485
+ });
236486
+ }
236487
+ }
236488
+ let resolvedReportsTo;
236489
+ let managerForCycleCheck;
236490
+ if (params.reportsTo !== void 0) {
236491
+ if (params.reportsTo === "") {
236492
+ if (callerAgentId) {
236493
+ return denied("Only privileged user/operator calls can clear an agent's manager.", {
236494
+ agentId: target.id,
236495
+ callerAgentId,
236496
+ rule: "privileged-clear-manager-only"
236497
+ });
236498
+ }
236499
+ resolvedReportsTo = void 0;
236500
+ } else {
236501
+ const manager = await agentStore.resolveAgent(params.reportsTo);
236502
+ if (!manager) {
236503
+ return invalid("reportsTo", `Manager '${params.reportsTo}' not found`, { agentId: target.id });
236504
+ }
236505
+ if (manager.id === target.id) {
236506
+ return invalid("reportsTo", "An agent cannot report to itself", { agentId: target.id });
236507
+ }
236508
+ const managerChain = await agentStore.getChainOfCommand(manager.id);
236509
+ if (managerChain.some((agent) => agent.id === target.id)) {
236510
+ return invalid("reportsTo", "reportsTo would create a management cycle", {
236511
+ agentId: target.id,
236512
+ managerId: manager.id
236513
+ });
236514
+ }
236515
+ if (callerAgentId) {
236516
+ const managerCallerIndex = managerChain.findIndex((agent) => agent.id === callerAgentId);
236517
+ if (manager.id !== callerAgentId && managerCallerIndex < 1) {
236518
+ return denied("You can only reparent reports to yourself or another agent in your management subtree.", {
236519
+ agentId: target.id,
236520
+ callerAgentId,
236521
+ managerId: manager.id,
236522
+ rule: "reparent-within-subtree-only"
236523
+ });
236524
+ }
236525
+ }
236526
+ resolvedReportsTo = manager.id;
236527
+ managerForCycleCheck = manager.id;
236528
+ }
236529
+ }
236530
+ const hasRuntimeConfigUpdates = [
236531
+ params.heartbeat_interval_ms,
236532
+ params.heartbeat_timeout_ms,
236533
+ params.max_concurrent_runs,
236534
+ params.message_response_mode
236535
+ ].some((value) => value !== void 0);
236536
+ const updateInput = {};
236537
+ const updatedFields = [];
236538
+ const setField = (field, value) => {
236539
+ updateInput[field] = value;
236540
+ updatedFields.push(String(field));
236541
+ };
236542
+ if (params.name !== void 0) setField("name", params.name);
236543
+ if (params.role !== void 0) setField("role", params.role);
236544
+ if (params.title !== void 0) setField("title", params.title);
236545
+ if (params.icon !== void 0) setField("icon", params.icon);
236546
+ if (params.soul !== void 0) setField("soul", params.soul);
236547
+ if (params.instructions_text !== void 0) setField("instructionsText", params.instructions_text);
236548
+ if (params.instructions_path !== void 0) setField("instructionsPath", params.instructions_path);
236549
+ if (params.heartbeat_procedure_path !== void 0) setField("heartbeatProcedurePath", params.heartbeat_procedure_path);
236550
+ if (params.reportsTo !== void 0) {
236551
+ setField("reportsTo", resolvedReportsTo);
236552
+ }
236553
+ if (hasRuntimeConfigUpdates) {
236554
+ setField("runtimeConfig", {
236555
+ ...target.runtimeConfig ?? {},
236556
+ ...params.heartbeat_interval_ms !== void 0 ? { heartbeatIntervalMs: params.heartbeat_interval_ms } : {},
236557
+ ...params.heartbeat_timeout_ms !== void 0 ? { heartbeatTimeoutMs: params.heartbeat_timeout_ms } : {},
236558
+ ...params.max_concurrent_runs !== void 0 ? { maxConcurrentRuns: params.max_concurrent_runs } : {},
236559
+ ...params.message_response_mode !== void 0 ? { messageResponseMode: params.message_response_mode } : {}
236560
+ });
236561
+ }
236562
+ if (managerForCycleCheck && managerForCycleCheck === target.id) {
236563
+ return invalid("reportsTo", "An agent cannot report to itself", { agentId: target.id });
236564
+ }
236565
+ const updated = await agentStore.updateAgent(target.id, updateInput);
236566
+ return {
236567
+ content: [{
236568
+ type: "text",
236569
+ text: `Updated ${updated.name} (${updated.id}): ${updatedFields.join(", ")}`
236570
+ }],
236571
+ details: { outcome: "updated", agentId: updated.id, updatedFields, agent: updated }
236572
+ };
236573
+ }
236574
+ });
234780
236575
  pi2.registerTool({
234781
236576
  name: "fn_agent_set_instructions",
234782
236577
  label: "fn: Set Agent Instructions",