@runfusion/fusion 0.78.0-beta.5 → 0.78.0-beta.6

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 (155) hide show
  1. package/dist/bin.js +3574 -1910
  2. package/dist/child-process-worker.js +1440 -628
  3. package/dist/client/.vite/manifest.json +257 -257
  4. package/dist/client/assets/{AgentDetailView-CdoJDm2q.js → AgentDetailView-F_LCM2Fh.js} +1 -1
  5. package/dist/client/assets/{AgentPermissionPolicyEditor-vlsZ36BL.js → AgentPermissionPolicyEditor-Da7PHXMp.js} +1 -1
  6. package/dist/client/assets/{AgentsView-9EwgX1OK.js → AgentsView-nIFjsi9d.js} +3 -3
  7. package/dist/client/assets/{CommandCenter--BVLmh3G.js → CommandCenter-BDtrZ6JK.js} +1 -1
  8. package/dist/client/assets/{DevServerView-d1CVa_YB.js → DevServerView-CXE7Swoe.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-_uNfzkVu.js → DirectoryPicker-CYI--LmS.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-SHFZKBkK.js → DocumentsView-BUwtzdpV.js} +1 -1
  11. package/dist/client/assets/{EvalsView-CQMcojzl.js → EvalsView-0qtYh3sd.js} +1 -1
  12. package/dist/client/assets/{ExperimentalAgentOnboardingModal-BhwVl29h.js → ExperimentalAgentOnboardingModal-D3YNBI3S.js} +1 -1
  13. package/dist/client/assets/{GoalsView-Ju8PyHt8.js → GoalsView-C8M2Ufj6.js} +1 -1
  14. package/dist/client/assets/{InsightsView-BHoI4E2e.js → InsightsView-ByhApqco.js} +1 -1
  15. package/dist/client/assets/{MemoryView-Co-efjDQ.js → MemoryView-BG3vxXrX.js} +1 -1
  16. package/dist/client/assets/{PatchnodeView-CBJpVpFG.js → PatchnodeView-D4gg6xMq.js} +1 -1
  17. package/dist/client/assets/{PiExtensionsManager-DDk1a_Qy.js → PiExtensionsManager-CaadPnc-.js} +1 -1
  18. package/dist/client/assets/{PluginManager-DtGflOpe.js → PluginManager-BC9_Ajdd.js} +1 -1
  19. package/dist/client/assets/{PullRequestView-DwHock6V.js → PullRequestView-BfouYDOl.js} +1 -1
  20. package/dist/client/assets/{ReportModal-CJaxJ6ua.js → ReportModal-BDsen5Oh.js} +1 -1
  21. package/dist/client/assets/{ResearchView-ZD-2JxsH.js → ResearchView-vmH_K4of.js} +1 -1
  22. package/dist/client/assets/{SecretsView-Cz99j53t.js → SecretsView-BinJNcAF.js} +1 -1
  23. package/dist/client/assets/{SettingsModal-dmitxbA_.js → SettingsModal-Bffo4m2D.js} +4 -4
  24. package/dist/client/assets/{SettingsModal-BfiZ2-GY.js → SettingsModal-DdnLlKn8.js} +1 -1
  25. package/dist/client/assets/{SettingsTextareaRow-XyVN0ZEP.js → SettingsTextareaRow-CV1xXRhq.js} +1 -1
  26. package/dist/client/assets/{SetupWizardModal-mVUAy77v.js → SetupWizardModal-B2O0HOZR.js} +2 -2
  27. package/dist/client/assets/{SkillsView-DYL1ZeGi.js → SkillsView-CPE3Jkt7.js} +1 -1
  28. package/dist/client/assets/{WorkflowNodeEditor-E9sIzyMz.js → WorkflowNodeEditor-CTVfvX3A.js} +1 -1
  29. package/dist/client/assets/{agent-import-generation-DwUlR4qa.js → agent-import-generation-Bu2uCPd0.js} +1 -1
  30. package/dist/client/assets/{agentsOrgChartLayout-B5F8JGin.js → agentsOrgChartLayout-BGOBewpH.js} +1 -1
  31. package/dist/client/assets/app-AzX-7oMC.js +11 -0
  32. package/dist/client/assets/app-B5vKhbbZ.js +11 -0
  33. package/dist/client/assets/app-BZHD4Tdt.js +5 -0
  34. package/dist/client/assets/app-CWzmFjgT.js +11 -0
  35. package/dist/client/assets/app-C_yEgSaj.js +11 -0
  36. package/dist/client/assets/app-Crvy006P.js +11 -0
  37. package/dist/client/assets/{app-Dsv4VHMo.js → app-UI8thBW4.js} +1 -1
  38. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BFirFbC8.js → architectureDiagram-3BPJPVTR-BDzTDKvR.js} +1 -1
  39. package/dist/client/assets/{blockDiagram-GPEHLZMM-CK-1Yktz.js → blockDiagram-GPEHLZMM-BbnxXgkY.js} +1 -1
  40. package/dist/client/assets/{c4Diagram-AAUBKEIU-C7uG_4cR.js → c4Diagram-AAUBKEIU-B_upVmlM.js} +1 -1
  41. package/dist/client/assets/channel-2BQ2i0n6.js +1 -0
  42. package/dist/client/assets/{chunk-2J33WTMH-BhPS_gnq.js → chunk-2J33WTMH-oH3mFmAA.js} +1 -1
  43. package/dist/client/assets/{chunk-4BX2VUAB-N_uAN5vN.js → chunk-4BX2VUAB-vezh3jfu.js} +1 -1
  44. package/dist/client/assets/{chunk-55IACEB6-CNYK0ezP.js → chunk-55IACEB6-BmGhY313.js} +1 -1
  45. package/dist/client/assets/{chunk-727SXJPM-C9EkXgMB.js → chunk-727SXJPM-ChDetrFZ.js} +1 -1
  46. package/dist/client/assets/{chunk-AQP2D5EJ-CYcOZI0r.js → chunk-AQP2D5EJ-BY329LM6.js} +1 -1
  47. package/dist/client/assets/{chunk-FMBD7UC4-D-oVdpAz.js → chunk-FMBD7UC4-zuiiPcMa.js} +1 -1
  48. package/dist/client/assets/{chunk-ND2GUHAM-79zqWauw.js → chunk-ND2GUHAM-CqF_-gMe.js} +1 -1
  49. package/dist/client/assets/{chunk-QZHKN3VN-T2WIzSUZ.js → chunk-QZHKN3VN-DtETgl8P.js} +1 -1
  50. package/dist/client/assets/classDiagram-4FO5ZUOK-CmoQsyhJ.js +1 -0
  51. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CmoQsyhJ.js +1 -0
  52. package/dist/client/assets/{cose-bilkent-S5V4N54A-DYcEyEdj.js → cose-bilkent-S5V4N54A-CB7l3IMd.js} +1 -1
  53. package/dist/client/assets/{dagre-BM42HDAG-CG6eBqe1.js → dagre-BM42HDAG-B-yrcude.js} +1 -1
  54. package/dist/client/assets/{dashboard-view-Cthbyplu.js → dashboard-view-BHUOC-dp.js} +1 -1
  55. package/dist/client/assets/{dashboard-view-CpfQSqFD.js → dashboard-view-C2e-lWkw.js} +1 -1
  56. package/dist/client/assets/{dashboard-view-C3jacppL.js → dashboard-view-DAkh7C8T.js} +1 -1
  57. package/dist/client/assets/{dashboard-view-BV97_KM2.js → dashboard-view-lR50LvCR.js} +1 -1
  58. package/dist/client/assets/{diagram-2AECGRRQ-CA-oSX6v.js → diagram-2AECGRRQ-cV9rcK7D.js} +1 -1
  59. package/dist/client/assets/{diagram-5GNKFQAL-7zlBlYUe.js → diagram-5GNKFQAL-COswZy2u.js} +1 -1
  60. package/dist/client/assets/{diagram-KO2AKTUF-kFIrOk6O.js → diagram-KO2AKTUF-CRYtHA-0.js} +1 -1
  61. package/dist/client/assets/{diagram-LMA3HP47-10NYAStZ.js → diagram-LMA3HP47-BSc2gwi-.js} +1 -1
  62. package/dist/client/assets/{diagram-OG6HWLK6-YUx0Utvh.js → diagram-OG6HWLK6-CkTvh_OG.js} +1 -1
  63. package/dist/client/assets/{erDiagram-TEJ5UH35-B9jMu7Zv.js → erDiagram-TEJ5UH35-IZxjOfAZ.js} +1 -1
  64. package/dist/client/assets/{flowDiagram-I6XJVG4X-C540twrl.js → flowDiagram-I6XJVG4X-DP1Krzf1.js} +1 -1
  65. package/dist/client/assets/{folder-open-Da5UOz-O.js → folder-open-3vwTBlrn.js} +1 -1
  66. package/dist/client/assets/{ganttDiagram-6RSMTGT7-BRTG1iq_.js → ganttDiagram-6RSMTGT7-DTrMtkeI.js} +1 -1
  67. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DGzLquoM.js → gitGraphDiagram-PVQCEYII-CZzumF1O.js} +1 -1
  68. package/dist/client/assets/index-BG6hrVhL.css +1 -0
  69. package/dist/client/assets/{index-B8Ei6RK_.js → index-jWzr0HgU.js} +343 -332
  70. package/dist/client/assets/{infoDiagram-5YYISTIA-DxDL2NPL.js → infoDiagram-5YYISTIA-DPToV-eq.js} +1 -1
  71. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CugUocbL.js → ishikawaDiagram-YF4QCWOH-V9akKrsQ.js} +1 -1
  72. package/dist/client/assets/{journeyDiagram-JHISSGLW-4J1DbTfn.js → journeyDiagram-JHISSGLW-CMZmXLJq.js} +1 -1
  73. package/dist/client/assets/{kanban-definition-UN3LZRKU-3wWdkkqE.js → kanban-definition-UN3LZRKU-DT2oKh0N.js} +1 -1
  74. package/dist/client/assets/{mermaid.core-CdJo4vtY.js → mermaid.core-C9xSBL9K.js} +4 -4
  75. package/dist/client/assets/{mindmap-definition-RKZ34NQL-3ggmNH1R.js → mindmap-definition-RKZ34NQL-BKyCGR7J.js} +1 -1
  76. package/dist/client/assets/{pieDiagram-4H26LBE5-Br6qnYUo.js → pieDiagram-4H26LBE5-BcyGtqQ4.js} +1 -1
  77. package/dist/client/assets/{puzzle-COtNelRH.js → puzzle-acZM2rkn.js} +1 -1
  78. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-q7eumSaG.js → quadrantDiagram-W4KKPZXB-C3FhvUs0.js} +1 -1
  79. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CFQyk2vj.js → requirementDiagram-4Y6WPE33-gcTHwql9.js} +1 -1
  80. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BPRBSEnx.js → sankeyDiagram-5OEKKPKP-CvYBofJY.js} +1 -1
  81. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-C3oaAZUP.js → sequenceDiagram-3UESZ5HK-D9v9NJKo.js} +1 -1
  82. package/dist/client/assets/{standing-instructions-template-_wMqEkKb.js → standing-instructions-template-B_qbkt6Q.js} +1 -1
  83. package/dist/client/assets/{stateDiagram-AJRCARHV-Gf3SuYgE.js → stateDiagram-AJRCARHV-Dq3-ZEwg.js} +1 -1
  84. package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-Cuc4jpXw.js → stateDiagram-v2-BHNVJYJU-Dt4J-On6.js} +1 -1
  85. package/dist/client/assets/{timeline-definition-PNZ67QCA-D5sPA5Fn.js → timeline-definition-PNZ67QCA-BRP1zY5x.js} +1 -1
  86. package/dist/client/assets/{upload--LqDmM0k.js → upload-Bx4Vz2lr.js} +1 -1
  87. package/dist/client/assets/{users-C-TzkWjB.js → users-CCfOZK-x.js} +1 -1
  88. package/dist/client/assets/{vennDiagram-CIIHVFJN-BzsN4FbO.js → vennDiagram-CIIHVFJN-BqtShvFk.js} +1 -1
  89. package/dist/client/assets/{wardley-L42UT6IY-BJIIgfK4.js → wardley-L42UT6IY-D04-M_2p.js} +1 -1
  90. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DFwxRQ3d.js → wardleyDiagram-YWT4CUSO-BmkvFHY9.js} +1 -1
  91. package/dist/client/assets/{xychartDiagram-2RQKCTM6-Djbd80bq.js → xychartDiagram-2RQKCTM6-C1N68DRK.js} +1 -1
  92. package/dist/client/index.html +2 -2
  93. package/dist/client/version.json +1 -1
  94. package/dist/droid-cli/index.ts +19 -6
  95. package/dist/droid-cli/package.json +1 -1
  96. package/dist/droid-cli/src/__tests__/index.test.ts +10 -11
  97. package/dist/extension.js +2504 -1047
  98. package/dist/pi-claude-cli/index.ts +25 -18
  99. package/dist/pi-claude-cli/package.json +5 -5
  100. package/dist/pi-claude-cli/src/__tests__/acp-dispatch.test.ts +17 -0
  101. package/dist/pi-claude-cli/src/event-bridge.ts +2 -1
  102. package/dist/plugin-sdk/index.d.ts +23 -1
  103. package/dist/plugin-sdk/index.js +17 -3
  104. package/dist/plugins/fusion-plugin-acp-runtime/bundled.js +25 -11
  105. package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
  106. package/dist/plugins/fusion-plugin-antigravity-runtime/bundled.js +20674 -0
  107. package/dist/plugins/fusion-plugin-antigravity-runtime/manifest.json +6 -0
  108. package/dist/plugins/fusion-plugin-antigravity-runtime/mcp-schema-server.cjs +155 -0
  109. package/dist/plugins/fusion-plugin-antigravity-runtime/package.json +11 -0
  110. package/dist/plugins/fusion-plugin-claude-runtime/bundled.js +25 -11
  111. package/dist/plugins/fusion-plugin-claude-runtime/package.json +1 -1
  112. package/dist/plugins/fusion-plugin-cli-printing-press/bundled.js +17 -3
  113. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  114. package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +17 -3
  115. package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
  116. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +17 -3
  117. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  118. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +17 -3
  119. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  120. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +44 -12
  121. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  122. package/dist/plugins/fusion-plugin-grok-runtime/bundled.js +25 -11
  123. package/dist/plugins/fusion-plugin-grok-runtime/package.json +1 -1
  124. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +17 -3
  125. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  126. package/dist/plugins/fusion-plugin-linear-import/bundled.js +17 -3
  127. package/dist/plugins/fusion-plugin-linear-import/package.json +1 -1
  128. package/dist/plugins/fusion-plugin-omp-runtime/bundled.js +25 -11
  129. package/dist/plugins/fusion-plugin-omp-runtime/package.json +1 -1
  130. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +17 -3
  131. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  132. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +17 -3
  133. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  134. package/dist/plugins/fusion-plugin-quality/bundled.js +17 -3
  135. package/dist/plugins/fusion-plugin-quality/package.json +1 -1
  136. package/dist/plugins/fusion-plugin-reports/bundled.js +17 -3
  137. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  138. package/dist/plugins/fusion-plugin-roadmap/bundled.js +17 -3
  139. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  140. package/dist/plugins/fusion-plugin-todos/bundled.js +17 -3
  141. package/dist/plugins/fusion-plugin-todos/package.json +1 -1
  142. package/dist/plugins/fusion-plugin-whatsapp-chat/bundled.js +17 -3
  143. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  144. package/package.json +3 -3
  145. package/skill/fusion/references/engine-tools.md +1 -1
  146. package/dist/client/assets/app-BHtkLolA.js +0 -9
  147. package/dist/client/assets/app-BNkvP9Zj.js +0 -9
  148. package/dist/client/assets/app-BaaOD_8W.js +0 -9
  149. package/dist/client/assets/app-CTDJyVYb.js +0 -9
  150. package/dist/client/assets/app-CYBEnq88.js +0 -9
  151. package/dist/client/assets/app-D_qEfmZH.js +0 -3
  152. package/dist/client/assets/channel-BcOi2QPy.js +0 -1
  153. package/dist/client/assets/classDiagram-4FO5ZUOK-Do_hlHfK.js +0 -1
  154. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Do_hlHfK.js +0 -1
  155. package/dist/client/assets/index-D-BMoPtn.css +0 -1
@@ -1308,7 +1308,43 @@ var init_documents_artifacts = __esm({
1308
1308
  }
1309
1309
  });
1310
1310
 
1311
+ // ../core/src/secrets/redact-secrets.ts
1312
+ function redactSecrets(text2) {
1313
+ return text2.replace(/(authorization\s*[:=]\s*)(bearer\s+)?[^\s,;"']+/gi, "$1$2[REDACTED]").replace(/\b(bearer)\s+[A-Za-z0-9._\-+/=]+/gi, "$1 [REDACTED]").replace(
1314
+ /\b((?:api[_-]?key|key|token|secret|password|passwd|pwd|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*)("?)[^\s,;"']+\2/gi,
1315
+ "$1$2[REDACTED]$2"
1316
+ ).replace(/\b(sk-|ghp_|gho_|github_pat_|xox[abpr]-|AKIA)[A-Za-z0-9_-]{8,}/g, "[REDACTED]").replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[REDACTED]").replace(/\b[0-9a-fA-F]{32,}\b/g, "[REDACTED]");
1317
+ }
1318
+ var init_redact_secrets = __esm({
1319
+ "../core/src/secrets/redact-secrets.ts"() {
1320
+ "use strict";
1321
+ }
1322
+ });
1323
+
1311
1324
  // ../core/src/tasks/task-external-block.ts
1325
+ function fallbackReport(block) {
1326
+ const condition = `${block.origin} limitation (${block.code || "UNCLASSIFIED"})`;
1327
+ return {
1328
+ verifiedCondition: `Fusion verified an external ${condition}.`,
1329
+ stopReason: "The task is safely paused to preserve its current execution state.",
1330
+ unimplementedWork: "Remaining implementation work was not performed.",
1331
+ unblockCondition: `Resolve the ${condition} and retry the task.`
1332
+ };
1333
+ }
1334
+ function sanitizeReportField(value, fallback) {
1335
+ if (typeof value !== "string") return fallback;
1336
+ const sanitized = redactSecrets(value.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim()).slice(0, REPORT_FIELD_MAX_LENGTH).trim();
1337
+ return sanitized.length > 0 && !UNSAFE_REPORT_CONTENT.test(sanitized) ? sanitized : fallback;
1338
+ }
1339
+ function buildTaskExternalBlockReport(block, report2) {
1340
+ const fallback = fallbackReport(block);
1341
+ return {
1342
+ verifiedCondition: sanitizeReportField(report2?.verifiedCondition, fallback.verifiedCondition),
1343
+ stopReason: sanitizeReportField(report2?.stopReason, fallback.stopReason),
1344
+ unimplementedWork: sanitizeReportField(report2?.unimplementedWork, fallback.unimplementedWork),
1345
+ unblockCondition: sanitizeReportField(report2?.unblockCondition, fallback.unblockCondition)
1346
+ };
1347
+ }
1312
1348
  function isTaskExternallyBlocked(task) {
1313
1349
  return task.status === EXTERNAL_BLOCK_STATUS && task.externalBlock !== void 0;
1314
1350
  }
@@ -1337,12 +1373,15 @@ function buildTaskExternalBlockClearPatch() {
1337
1373
  externalBlock: null
1338
1374
  };
1339
1375
  }
1340
- var EXTERNAL_BLOCK_STATUS, EXTERNAL_BLOCK_PAUSE_REASON;
1376
+ var EXTERNAL_BLOCK_STATUS, EXTERNAL_BLOCK_PAUSE_REASON, REPORT_FIELD_MAX_LENGTH, UNSAFE_REPORT_CONTENT;
1341
1377
  var init_task_external_block = __esm({
1342
1378
  "../core/src/tasks/task-external-block.ts"() {
1343
1379
  "use strict";
1380
+ init_redact_secrets();
1344
1381
  EXTERNAL_BLOCK_STATUS = "blocked";
1345
1382
  EXTERNAL_BLOCK_PAUSE_REASON = "external-block";
1383
+ REPORT_FIELD_MAX_LENGTH = 320;
1384
+ UNSAFE_REPORT_CONTENT = /\b(?:system prompt|tool (?:output|log)|stack trace|\bat \S+ \([^)]*:\d+:\d+\))/i;
1346
1385
  }
1347
1386
  });
1348
1387
 
@@ -1570,7 +1609,7 @@ var init_builtin_plan_review_group = __esm({
1570
1609
  5. **Risk callouts** \u2014 migrations, data-loss paths, external integrations, secrets, or plugin/runtime dependencies that need explicit handling.
1571
1610
 
1572
1611
  ## Environment feasibility
1573
- When the supplied \`## Environment Capabilities\` block lists a runtime as unavailable, REVISE only if a blocking acceptance/completion criterion requires that runtime and the plan provides no runnable substitute. APPROVE when the plan specifies a runnable substitute or records the ideal check under an explicitly non-blocking \`## Environment Constraints\` section (use APPROVE_WITH_NOTES only for a genuine P2 note). Never REVISE merely because a desirable check is unavailable when the plan already marks it non-blocking.
1612
+ When the supplied \`## Environment Capabilities\` block confirms a runtime unavailable without an applicable confirmed wrapper, REVISE only if a blocking acceptance/completion criterion requires it and the plan provides no runnable substitute. A documented or configured command using a confirmed wrapper remains binding even when its inner executable is absent from the host PATH. APPROVE a correctly scoped non-blocking \`## Environment Constraints\` substitute (use APPROVE_WITH_NOTES only for a genuine P2 note); unlisted commands are UNKNOWN, never absent.
1574
1613
 
1575
1614
  ${PLAN_REVIEW_COMPLETENESS_POLICY}
1576
1615
 
@@ -3004,6 +3043,9 @@ var init_settings_schema = __esm({
3004
3043
  Grok CLI binary overrides are global operator settings because executable locations are machine-local. Blank/undefined preserves PATH auto-detection through grok.
3005
3044
  */
3006
3045
  grokCliBinaryPath: void 0,
3046
+ // FNXC:AntigravityProvider 2026-09-20-18:32: Default disabled and preserve PATH auto-detection until an operator enables a verified agy binary.
3047
+ useAntigravityCli: void 0,
3048
+ antigravityCliBinaryPath: void 0,
3007
3049
  /*
3008
3050
  FNXC:OmpAcp 2026-07-13-22:50:
3009
3051
  Oh My Pi (omp) CLI enable + binary override are global operator settings (machine-local), mirroring Grok/Cursor.
@@ -6190,19 +6232,6 @@ var init_task_priority = __esm({
6190
6232
  }
6191
6233
  });
6192
6234
 
6193
- // ../core/src/secrets/redact-secrets.ts
6194
- function redactSecrets(text2) {
6195
- return text2.replace(/(authorization\s*[:=]\s*)(bearer\s+)?[^\s,;"']+/gi, "$1$2[REDACTED]").replace(/\b(bearer)\s+[A-Za-z0-9._\-+/=]+/gi, "$1 [REDACTED]").replace(
6196
- /\b((?:api[_-]?key|key|token|secret|password|passwd|pwd|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*)("?)[^\s,;"']+\2/gi,
6197
- "$1$2[REDACTED]$2"
6198
- ).replace(/\b(sk-|ghp_|gho_|github_pat_|xox[abpr]-|AKIA)[A-Za-z0-9_-]{8,}/g, "[REDACTED]").replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[REDACTED]").replace(/\b[0-9a-fA-F]{32,}\b/g, "[REDACTED]");
6199
- }
6200
- var init_redact_secrets = __esm({
6201
- "../core/src/secrets/redact-secrets.ts"() {
6202
- "use strict";
6203
- }
6204
- });
6205
-
6206
6235
  // ../core/src/missions/mission-blockers.ts
6207
6236
  function normalizeMissionBlockerReason(raw) {
6208
6237
  if (raw && KNOWN_REASONS.has(raw)) return { reason: raw };
@@ -8382,7 +8411,7 @@ var init_sql = __esm({
8382
8411
  return new SQL([new StringChunk(str)]);
8383
8412
  }
8384
8413
  sql2.raw = raw;
8385
- function join103(chunks, separator) {
8414
+ function join104(chunks, separator) {
8386
8415
  const result = [];
8387
8416
  for (const [i, chunk] of chunks.entries()) {
8388
8417
  if (i > 0 && separator !== void 0) {
@@ -8392,7 +8421,7 @@ var init_sql = __esm({
8392
8421
  }
8393
8422
  return new SQL(result);
8394
8423
  }
8395
- sql2.join = join103;
8424
+ sql2.join = join104;
8396
8425
  function identifier(value) {
8397
8426
  return new Name(value);
8398
8427
  }
@@ -12178,26 +12207,26 @@ function validateParallelism(nodes2, outgoing, nodesById) {
12178
12207
  }
12179
12208
  const joinsReached = /* @__PURE__ */ new Set();
12180
12209
  for (const edge of branchEdges) {
12181
- const join104 = walkBranchToJoin(edge.to, split.id, outgoing, nodesById);
12182
- if (!join104) {
12210
+ const join105 = walkBranchToJoin(edge.to, split.id, outgoing, nodesById);
12211
+ if (!join105) {
12183
12212
  throw new WorkflowIrError(`split '${split.id}' has a branch with no reachable matching join`);
12184
12213
  }
12185
- joinsReached.add(join104);
12214
+ joinsReached.add(join105);
12186
12215
  }
12187
12216
  if (joinsReached.size !== 1) {
12188
12217
  throw new WorkflowIrError(`split '${split.id}' branches converge on more than one join`);
12189
12218
  }
12190
12219
  const joinId = [...joinsReached][0];
12191
- const join103 = nodesById.get(joinId);
12192
- const mode = join103.config?.mode;
12220
+ const join104 = nodesById.get(joinId);
12221
+ const mode = join104.config?.mode;
12193
12222
  if (mode && typeof mode === "object" && "quorum" in mode) {
12194
12223
  const n = mode.quorum;
12195
12224
  if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
12196
- throw new WorkflowIrError(`join '${join103.id}' quorum must be a positive integer`);
12225
+ throw new WorkflowIrError(`join '${join104.id}' quorum must be a positive integer`);
12197
12226
  }
12198
12227
  if (n > branchEdges.length) {
12199
12228
  throw new WorkflowIrError(
12200
- `join '${join103.id}' quorum(${n}) exceeds the split's ${branchEdges.length} branches`
12229
+ `join '${join104.id}' quorum(${n}) exceeds the split's ${branchEdges.length} branches`
12201
12230
  );
12202
12231
  }
12203
12232
  }
@@ -14748,7 +14777,7 @@ The quality of your spec directly determines execution quality, review churn, an
14748
14777
  - Access to the project's files so you can understand context
14749
14778
 
14750
14779
  ## Environment feasibility
14751
- When an \`## Environment Capabilities\` section is supplied, no acceptance criterion, completion criterion, or required verification command may depend on a runtime listed unavailable. Specify a runnable substitute instead and record the ideal-but-impossible check under an \`## Environment Constraints\` heading marked explicitly non-blocking. Never state that a plan is blocked because a runtime is missing.
14780
+ When an \`## Environment Capabilities\` section is supplied, require a runnable substitute only for a runtime confirmed unavailable without an applicable confirmed wrapper. A documented or configured command using a confirmed wrapper remains binding even when its inner executable is absent from the host PATH. Commands not listed are UNKNOWN, never absent; never state that a plan is blocked because a runtime is missing.
14752
14781
 
14753
14782
  ## What you produce
14754
14783
  Write a complete PROMPT.md specification to the given path using the write tool.
@@ -15958,6 +15987,9 @@ var init_builtin_code_review_group = __esm({
15958
15987
  CODE_REVIEW_DESCRIPTION = "Diff-review the task's changes for correctness bugs, regressions, and intent mismatches that tests miss";
15959
15988
  CODE_REVIEW_PROMPT = `You are a senior code reviewer. Review the task's diff for the correctness value automated tests do NOT catch.
15960
15989
 
15990
+ ## Review stage
15991
+ This is a pre-merge review. Evidence that can exist only after landing (for example a push-to-main CI run) belongs to an enabled, blocking post-merge verification gate. Verify that the workflow preserves that requirement before allowing merge; if the gate or its evidence contract is missing, request that concrete configuration fix. Do not request the future run itself as a pre-merge implementation fix, or claim its evidence already exists. Pre-merge correctness and verification requirements still apply.
15992
+
15961
15993
  ## Step 1: Read the change
15962
15994
  1. Read the full diff against the base branch: \`git diff <base>...HEAD\` (or \`git diff <base>\`). Determine the base from the task context / merge target.
15963
15995
  2. Read the changed files in full where the diff is non-trivial, so you see the surrounding code paths the change touches \u2014 not just the hunks.
@@ -16043,9 +16075,17 @@ var init_builtin_post_merge_group = __esm({
16043
16075
  2. Check the final merged diff and task summary for obvious mismatches, missing verification evidence, or integration-only regressions.
16044
16076
  3. If configured test/build commands are available in the task context, inspect their latest result or explain why no post-merge command was applicable.
16045
16077
 
16078
+ ## Required post-landing Full Suite evidence
16079
+ This enabled gate requires post-landing Full Suite evidence. Do NOT approve until its delivery record names all of the following:
16080
+ 1. The landed SHA and the first Full Suite push-to-main run at or after that SHA, including the run ID and run SHA.
16081
+ 2. A conclusion for every Test shard: 1/4, 2/4, 3/4, and 4/4.
16082
+ 3. All four timing artifacts: test-timings-shard-1, test-timings-shard-2, test-timings-shard-3, and test-timings-shard-4.
16083
+
16084
+ Pre-landing, unrelated-main, or partial evidence does not satisfy this contract. If the required run or any required evidence is unavailable, return REVISE and state that final completion remains blocked pending the post-landing evidence. Record verified evidence in the task delivery record before approving.
16085
+
16046
16086
  ## Output Requirements
16047
16087
  - APPROVE: post-merge verification is acceptable.
16048
- - APPROVE_WITH_NOTES: completion may proceed with non-blocking notes.
16088
+ - APPROVE_WITH_NOTES: completion may proceed with non-blocking notes only when every post-landing evidence item above is recorded.
16049
16089
  - REVISE: completion should be blocked; include the concrete post-merge issue and the needed follow-up.
16050
16090
  - \`notes\` MUST contain one to three non-empty sentences naming what was checked and why the verdict was reached. An empty \`notes\` string is a protocol violation.
16051
16091
  - Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
@@ -20333,7 +20373,7 @@ var init_select2 = __esm({
20333
20373
  const baseTableName = this.tableName;
20334
20374
  const tableName = getTableLikeName(table);
20335
20375
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
20336
- if (typeof tableName === "string" && this.config.joins?.some((join103) => join103.alias === tableName)) {
20376
+ if (typeof tableName === "string" && this.config.joins?.some((join104) => join104.alias === tableName)) {
20337
20377
  throw new Error(`Alias "${tableName}" is already used in this query`);
20338
20378
  }
20339
20379
  if (!this.isPartialSelect) {
@@ -21868,7 +21908,7 @@ var init_update = __esm({
21868
21908
  createJoin(joinType) {
21869
21909
  return (table, on) => {
21870
21910
  const tableName = getTableLikeName(table);
21871
- if (typeof tableName === "string" && this.config.joins.some((join103) => join103.alias === tableName)) {
21911
+ if (typeof tableName === "string" && this.config.joins.some((join104) => join104.alias === tableName)) {
21872
21912
  throw new Error(`Alias "${tableName}" is already used in this query`);
21873
21913
  }
21874
21914
  if (typeof on === "function") {
@@ -21964,10 +22004,10 @@ var init_update = __esm({
21964
22004
  const fromFields = this.getTableLikeFields(this.config.from);
21965
22005
  fields[tableName] = fromFields;
21966
22006
  }
21967
- for (const join103 of this.config.joins) {
21968
- const tableName2 = getTableLikeName(join103.table);
21969
- if (typeof tableName2 === "string" && !is(join103.table, SQL)) {
21970
- const fromFields = this.getTableLikeFields(join103.table);
22007
+ for (const join104 of this.config.joins) {
22008
+ const tableName2 = getTableLikeName(join104.table);
22009
+ if (typeof tableName2 === "string" && !is(join104.table, SQL)) {
22010
+ const fromFields = this.getTableLikeFields(join104.table);
21971
22011
  fields[tableName2] = fromFields;
21972
22012
  }
21973
22013
  }
@@ -26280,8 +26320,10 @@ var init_schema2 = __esm({
26280
26320
  import { randomUUID as randomUUID2 } from "node:crypto";
26281
26321
  function createAsyncDataLayer(connections, options) {
26282
26322
  const db = connections.runtime;
26323
+ const healthDb = connections.health;
26283
26324
  return {
26284
26325
  db,
26326
+ healthDb,
26285
26327
  projectId: options?.projectId,
26286
26328
  backend: connections.backend,
26287
26329
  async transaction(fn, options2) {
@@ -30427,13 +30469,13 @@ async function detectWorkspaceRepos(dir, runner = runGitCommand, timeout = DEFAU
30427
30469
  return [];
30428
30470
  }
30429
30471
  const { stat: stat14 } = await import("node:fs/promises");
30430
- const { join: join103 } = await import("node:path");
30472
+ const { join: join104 } = await import("node:path");
30431
30473
  const found = [];
30432
30474
  for (const entry of entries) {
30433
30475
  if (EXCLUDED_WORKSPACE_ENTRIES.has(entry)) continue;
30434
- const childDir = join103(dir, entry);
30476
+ const childDir = join104(dir, entry);
30435
30477
  try {
30436
- const s3 = await stat14(join103(childDir, ".git"));
30478
+ const s3 = await stat14(join104(childDir, ".git"));
30437
30479
  if (!s3.isDirectory() && !s3.isFile()) continue;
30438
30480
  } catch {
30439
30481
  continue;
@@ -30446,9 +30488,9 @@ async function detectWorkspaceRepos(dir, runner = runGitCommand, timeout = DEFAU
30446
30488
  }
30447
30489
  async function readConfiguredIntegrationBranch(projectPath) {
30448
30490
  try {
30449
- const { readFile: readFile54 } = await import("node:fs/promises");
30450
- const { join: join103 } = await import("node:path");
30451
- const raw = await readFile54(join103(projectPath, ".fusion", "config.json"), "utf-8");
30491
+ const { readFile: readFile55 } = await import("node:fs/promises");
30492
+ const { join: join104 } = await import("node:path");
30493
+ const raw = await readFile55(join104(projectPath, ".fusion", "config.json"), "utf-8");
30452
30494
  const config3 = JSON.parse(raw);
30453
30495
  for (const value of [config3.settings?.integrationBranch, config3.settings?.baseBranch]) {
30454
30496
  if (typeof value !== "string") continue;
@@ -30461,9 +30503,9 @@ async function readConfiguredIntegrationBranch(projectPath) {
30461
30503
  }
30462
30504
  async function isWorkspaceModeExplicitlyDisabled(projectPath) {
30463
30505
  try {
30464
- const { readFile: readFile54 } = await import("node:fs/promises");
30465
- const { join: join103 } = await import("node:path");
30466
- const raw = await readFile54(join103(projectPath, ".fusion", "config.json"), "utf-8");
30506
+ const { readFile: readFile55 } = await import("node:fs/promises");
30507
+ const { join: join104 } = await import("node:path");
30508
+ const raw = await readFile55(join104(projectPath, ".fusion", "config.json"), "utf-8");
30467
30509
  const config3 = JSON.parse(raw);
30468
30510
  return config3.settings?.workspaceMode === false;
30469
30511
  } catch {
@@ -30471,12 +30513,12 @@ async function isWorkspaceModeExplicitlyDisabled(projectPath) {
30471
30513
  }
30472
30514
  }
30473
30515
  async function setWorkspaceModeInConfig(projectPath, value) {
30474
- const { readFile: readFile54, writeFile: writeFile31, mkdir: mkdir32 } = await import("node:fs/promises");
30475
- const { join: join103 } = await import("node:path");
30476
- const configPath = join103(projectPath, ".fusion", "config.json");
30516
+ const { readFile: readFile55, writeFile: writeFile31, mkdir: mkdir32 } = await import("node:fs/promises");
30517
+ const { join: join104 } = await import("node:path");
30518
+ const configPath = join104(projectPath, ".fusion", "config.json");
30477
30519
  let config3 = {};
30478
30520
  try {
30479
- config3 = JSON.parse(await readFile54(configPath, "utf-8"));
30521
+ config3 = JSON.parse(await readFile55(configPath, "utf-8"));
30480
30522
  } catch (err) {
30481
30523
  if (err?.code !== "ENOENT") throw err;
30482
30524
  }
@@ -30485,7 +30527,7 @@ async function setWorkspaceModeInConfig(projectPath, value) {
30485
30527
  }
30486
30528
  const settings = config3.settings;
30487
30529
  settings.workspaceMode = value;
30488
- await mkdir32(join103(projectPath, ".fusion"), { recursive: true });
30530
+ await mkdir32(join104(projectPath, ".fusion"), { recursive: true });
30489
30531
  await writeFile31(configPath, JSON.stringify(config3, null, 2), "utf-8");
30490
30532
  }
30491
30533
  function isInRootRelativePath(entry, pathMod) {
@@ -30498,12 +30540,12 @@ function isInRootRelativePath(entry, pathMod) {
30498
30540
  return true;
30499
30541
  }
30500
30542
  async function loadWorkspaceConfig(rootDir) {
30501
- const { readFile: readFile54 } = await import("node:fs/promises");
30543
+ const { readFile: readFile55 } = await import("node:fs/promises");
30502
30544
  const pathMod = await import("node:path");
30503
- const { join: join103 } = pathMod;
30504
- const configPath = join103(rootDir, ".fusion", WORKSPACE_CONFIG_FILENAME);
30545
+ const { join: join104 } = pathMod;
30546
+ const configPath = join104(rootDir, ".fusion", WORKSPACE_CONFIG_FILENAME);
30505
30547
  try {
30506
- const raw = await readFile54(configPath, "utf-8");
30548
+ const raw = await readFile55(configPath, "utf-8");
30507
30549
  const parsed = JSON.parse(raw);
30508
30550
  if (parsed !== null && typeof parsed === "object" && "repos" in parsed && Array.isArray(parsed.repos) && parsed.repos.every((r) => typeof r === "string")) {
30509
30551
  const rawRepos = parsed.repos;
@@ -30554,20 +30596,20 @@ async function addWorkspaceRepo(rootDir, repoRelPath, options = {}) {
30554
30596
  }
30555
30597
  async function saveWorkspaceConfig(rootDir, config3) {
30556
30598
  const { mkdir: mkdir32, writeFile: writeFile31 } = await import("node:fs/promises");
30557
- const { join: join103 } = await import("node:path");
30558
- const fusionDir = join103(rootDir, ".fusion");
30599
+ const { join: join104 } = await import("node:path");
30600
+ const fusionDir = join104(rootDir, ".fusion");
30559
30601
  await mkdir32(fusionDir, { recursive: true });
30560
30602
  await writeFile31(
30561
- join103(fusionDir, WORKSPACE_CONFIG_FILENAME),
30603
+ join104(fusionDir, WORKSPACE_CONFIG_FILENAME),
30562
30604
  JSON.stringify(config3, null, 2),
30563
30605
  "utf-8"
30564
30606
  );
30565
30607
  }
30566
30608
  async function removeWorkspaceConfig(rootDir) {
30567
30609
  const { unlink: unlink13 } = await import("node:fs/promises");
30568
- const { join: join103 } = await import("node:path");
30610
+ const { join: join104 } = await import("node:path");
30569
30611
  try {
30570
- await unlink13(join103(rootDir, ".fusion", WORKSPACE_CONFIG_FILENAME));
30612
+ await unlink13(join104(rootDir, ".fusion", WORKSPACE_CONFIG_FILENAME));
30571
30613
  return true;
30572
30614
  } catch (error2) {
30573
30615
  if (error2?.code === "ENOENT") return false;
@@ -33642,6 +33684,9 @@ async function replaceActiveTaskWorkflowContinuation(layer, input) {
33642
33684
  eq(project_exports.workflowWorkItems.kind, "task"),
33643
33685
  inArray(project_exports.workflowWorkItems.state, ACTIVE_TASK_CONTINUATION_STATES)
33644
33686
  ));
33687
+ if (input.onlyIfNoActiveTaskContinuation && activeRows.length > 0) {
33688
+ throw new ActiveTaskContinuationError(input.taskId);
33689
+ }
33645
33690
  for (const row of activeRows) {
33646
33691
  if (row.runId === input.runId && row.nodeId === input.nodeId && row.kind === input.kind) continue;
33647
33692
  await transitionWorkflowWorkItem(layer, row.id, "succeeded", { leaseOwner: null, leaseExpiresAt: null, lastError: null }, tx);
@@ -33667,6 +33712,24 @@ async function replaceActiveTaskWorkflowContinuation(layer, input) {
33667
33712
  }
33668
33713
  async function seedWorkspaceCodeReviewContinuationIfIdle(layer, input) {
33669
33714
  return layer.transactionImmediate(async (tx) => withTaskWorkflowSerialization(tx, layer.projectId, input.taskId, async () => {
33715
+ if (input.expectedWorkflowSelection !== void 0) {
33716
+ const rows = await tx.select({
33717
+ workflowId: project_exports.taskWorkflowSelection.workflowId,
33718
+ stepIds: project_exports.taskWorkflowSelection.stepIds
33719
+ }).from(project_exports.taskWorkflowSelection).where(and(
33720
+ projectScopeFor(project_exports.taskWorkflowSelection.projectId, layer.projectId),
33721
+ eq(project_exports.taskWorkflowSelection.taskId, input.taskId)
33722
+ )).limit(1);
33723
+ const row = rows[0];
33724
+ const current = row ? {
33725
+ workflowId: row.workflowId,
33726
+ stepIds: Array.isArray(row.stepIds) ? row.stepIds.filter((id) => typeof id === "string") : []
33727
+ } : null;
33728
+ const expected = input.expectedWorkflowSelection;
33729
+ if (current?.workflowId !== expected?.workflowId || current?.stepIds.length !== expected?.stepIds.length || current?.stepIds.some((id, index2) => id !== expected?.stepIds[index2])) {
33730
+ return { seeded: false, reason: "workflow-selection-changed" };
33731
+ }
33732
+ }
33670
33733
  const active2 = await tx.select({ id: project_exports.workflowWorkItems.id }).from(project_exports.workflowWorkItems).where(and(
33671
33734
  projectScopeFor(project_exports.workflowWorkItems.projectId, layer.projectId),
33672
33735
  eq(project_exports.workflowWorkItems.taskId, input.taskId),
@@ -33825,7 +33888,7 @@ async function getCompletionHandoffMarker(db, taskId) {
33825
33888
  async function clearCompletionHandoffMarker(db, taskId) {
33826
33889
  await db.delete(project_exports.completionHandoffMarkers).where(eq(project_exports.completionHandoffMarkers.taskId, taskId));
33827
33890
  }
33828
- var TERMINAL_WORKFLOW_WORK_ITEM_STATES, ACTIVE_TASK_CONTINUATION_STATES;
33891
+ var TERMINAL_WORKFLOW_WORK_ITEM_STATES, ACTIVE_TASK_CONTINUATION_STATES, ActiveTaskContinuationError;
33829
33892
  var init_async_workflow_workitems = __esm({
33830
33893
  "../core/src/task-store/async/async-workflow-workitems.ts"() {
33831
33894
  "use strict";
@@ -33840,6 +33903,12 @@ var init_async_workflow_workitems = __esm({
33840
33903
  "cancelled"
33841
33904
  ]);
33842
33905
  ACTIVE_TASK_CONTINUATION_STATES = [...ACTIVE_WORKFLOW_WORK_ITEM_STATES];
33906
+ ActiveTaskContinuationError = class extends Error {
33907
+ constructor(taskId) {
33908
+ super(`active task continuation exists for ${taskId}`);
33909
+ this.name = "ActiveTaskContinuationError";
33910
+ }
33911
+ };
33843
33912
  }
33844
33913
  });
33845
33914
 
@@ -44684,10 +44753,10 @@ async function createConnectionSet(env = process.env, options = {}) {
44684
44753
  "External backend resolved but runtimeUrl is null. This is an internal error."
44685
44754
  );
44686
44755
  }
44687
- return createConnectionSetFromUrl(backend, options);
44756
+ return createConnectionSetFromUrl(backend, { ...options, env });
44688
44757
  }
44689
44758
  async function createConnectionSetFromUrl(backend, options = {}) {
44690
- const poolMax = options.poolMax ?? DEFAULT_POOL_MAX;
44759
+ const poolMax = resolvePoolMax(options);
44691
44760
  const connectTimeout = options.connectTimeoutSeconds ?? DEFAULT_CONNECT_TIMEOUT_SECONDS;
44692
44761
  const idleTimeout = options.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS;
44693
44762
  const onWarning = options.onWarning ?? ((msg) => log3.warn(msg));
@@ -44722,6 +44791,16 @@ async function createConnectionSetFromUrl(backend, options = {}) {
44722
44791
  }
44723
44792
  });
44724
44793
  const runtimeDb = drizzle(runtimeSql);
44794
+ const healthSql = src_default(runtimeUrl, {
44795
+ max: 1,
44796
+ connect_timeout: connectTimeout,
44797
+ idle_timeout: idleTimeout,
44798
+ prepare: runtimePrepare,
44799
+ connection: runtimeConnectionParameters,
44800
+ onnotice: () => {
44801
+ }
44802
+ });
44803
+ const healthDb = drizzle(healthSql);
44725
44804
  const migrationUrl = backend.migrationUrl ?? runtimeUrl;
44726
44805
  const migrationSql = src_default(migrationUrl, {
44727
44806
  max: 1,
@@ -44733,23 +44812,40 @@ async function createConnectionSetFromUrl(backend, options = {}) {
44733
44812
  }
44734
44813
  });
44735
44814
  const migrationDb = drizzle(migrationSql);
44815
+ let closePromise;
44736
44816
  const connections = {
44737
44817
  runtime: runtimeDb,
44738
44818
  migration: migrationDb,
44819
+ health: healthDb,
44739
44820
  backend,
44740
- async close() {
44741
- const closePromises = [
44742
- migrationSql.end({ timeout: 5 }),
44743
- runtimeSql.end({ timeout: 5 })
44744
- ];
44745
- await Promise.allSettled(closePromises);
44821
+ close() {
44822
+ closePromise ??= (async () => {
44823
+ await Promise.allSettled([
44824
+ migrationSql.end({ timeout: 5 }),
44825
+ healthSql.end({ timeout: 5 }),
44826
+ runtimeSql.end({ timeout: 5 })
44827
+ ]);
44828
+ })();
44829
+ return closePromise;
44746
44830
  },
44747
44831
  async ping() {
44748
- await runtimeSql`SELECT 1`;
44832
+ await healthSql`SELECT 1`;
44749
44833
  }
44750
44834
  };
44751
44835
  return connections;
44752
44836
  }
44837
+ function resolvePoolMax(options = {}) {
44838
+ if (options.poolMax !== void 0) return options.poolMax;
44839
+ const raw = options.env?.[FUSION_PG_POOL_MAX_ENV] ?? process.env[FUSION_PG_POOL_MAX_ENV];
44840
+ if (raw === void 0) return DEFAULT_POOL_MAX;
44841
+ const trimmed = raw.trim();
44842
+ const value = Number(trimmed);
44843
+ if (trimmed !== "" && Number.isInteger(value) && value >= MIN_POOL_MAX && value <= MAX_POOL_MAX) return value;
44844
+ (options.onWarning ?? ((msg) => log3.warn(msg)))(
44845
+ `${FUSION_PG_POOL_MAX_ENV} must be an integer from ${MIN_POOL_MAX} through ${MAX_POOL_MAX}; using default ${DEFAULT_POOL_MAX}`
44846
+ );
44847
+ return DEFAULT_POOL_MAX;
44848
+ }
44753
44849
  async function verifyConnection(url2, timeoutSeconds = DEFAULT_CONNECT_TIMEOUT_SECONDS) {
44754
44850
  const sql2 = src_default(url2, {
44755
44851
  max: 1,
@@ -44768,7 +44864,7 @@ async function verifyConnection(url2, timeoutSeconds = DEFAULT_CONNECT_TIMEOUT_S
44768
44864
  });
44769
44865
  }
44770
44866
  }
44771
- var log3, DEFAULT_POOL_MAX, DEFAULT_CONNECT_TIMEOUT_SECONDS, DEFAULT_IDLE_TIMEOUT_SECONDS, DatabaseConnectionError;
44867
+ var log3, DEFAULT_POOL_MAX, FUSION_PG_POOL_MAX_ENV, MIN_POOL_MAX, MAX_POOL_MAX, DEFAULT_CONNECT_TIMEOUT_SECONDS, DEFAULT_IDLE_TIMEOUT_SECONDS, DatabaseConnectionError;
44772
44868
  var init_connection2 = __esm({
44773
44869
  "../core/src/postgres/connection.ts"() {
44774
44870
  "use strict";
@@ -44779,6 +44875,9 @@ var init_connection2 = __esm({
44779
44875
  init_credential_redact();
44780
44876
  log3 = createLogger2("postgres-connection");
44781
44877
  DEFAULT_POOL_MAX = 3;
44878
+ FUSION_PG_POOL_MAX_ENV = "FUSION_PG_POOL_MAX";
44879
+ MIN_POOL_MAX = 1;
44880
+ MAX_POOL_MAX = 500;
44782
44881
  DEFAULT_CONNECT_TIMEOUT_SECONDS = 10;
44783
44882
  DEFAULT_IDLE_TIMEOUT_SECONDS = 20;
44784
44883
  DatabaseConnectionError = class extends Error {
@@ -46000,7 +46099,7 @@ async function checkPostgresHealth(layer) {
46000
46099
  return errors;
46001
46100
  }
46002
46101
  try {
46003
- const db = layer.db;
46102
+ const db = layer.healthDb;
46004
46103
  const rows = await db.execute(
46005
46104
  sql.raw(`
46006
46105
  SELECT datallowconn, now() - pg_postmaster_start_time() AS uptime
@@ -46254,15 +46353,17 @@ function buildReport(checkedAt, anomalies) {
46254
46353
  anomalies
46255
46354
  };
46256
46355
  }
46257
- async function detectTaskIdIntegrityAnomaliesAsync(db) {
46356
+ async function detectTaskIdIntegrityAnomaliesAsync(db, options) {
46258
46357
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
46358
+ const projectId2 = options?.projectId?.trim() || void 0;
46359
+ const projectFilter = projectId2 ? sql` WHERE project_id = ${projectId2}` : sql``;
46259
46360
  try {
46260
46361
  const anomalies = [];
46261
46362
  const activeRows = await db.execute(
46262
- sql.raw(`SELECT id FROM ${PROJECT_SCHEMA}.tasks`)
46363
+ sql`SELECT id FROM ${sql.raw(`${PROJECT_SCHEMA}.tasks`)}${projectFilter}`
46263
46364
  );
46264
46365
  const archivedRows = await db.execute(
46265
- sql.raw(`SELECT id FROM ${PROJECT_SCHEMA}.archived_tasks`)
46366
+ sql`SELECT id FROM ${sql.raw(`${PROJECT_SCHEMA}.archived_tasks`)}${projectFilter}`
46266
46367
  );
46267
46368
  const activeIds = activeRows.map((r) => String(r.id ?? ""));
46268
46369
  const archivedIds = archivedRows.map((r) => String(r.id ?? ""));
@@ -46313,7 +46414,7 @@ async function detectTaskIdIntegrityAnomaliesAsync(db) {
46313
46414
  }
46314
46415
  }
46315
46416
  const stateRows = await db.execute(
46316
- sql.raw(`SELECT prefix, next_sequence FROM ${PROJECT_SCHEMA}.distributed_task_id_state`)
46417
+ sql`SELECT prefix, next_sequence FROM ${sql.raw(`${PROJECT_SCHEMA}.distributed_task_id_state`)}${projectFilter}`
46317
46418
  );
46318
46419
  for (const stateRow of stateRows) {
46319
46420
  const prefix = String(stateRow.prefix).trim().toUpperCase();
@@ -46685,6 +46786,11 @@ async function migrateSqliteToPostgresOnSession(migrationDb, sources, options) {
46685
46786
  emitMigrationProgress(options, { phase: "copy-started", tableCount });
46686
46787
  let tableIndex = 0;
46687
46788
  for (const { source, plan } of plannedSources) {
46789
+ const sourceHasPreexistingTargetAhead = await hasPreexistingTargetAhead(
46790
+ migrationDb,
46791
+ source,
46792
+ plan
46793
+ );
46688
46794
  for (const tablePlan of plan) {
46689
46795
  tableIndex += 1;
46690
46796
  const progressBase = {
@@ -46699,6 +46805,7 @@ async function migrateSqliteToPostgresOnSession(migrationDb, sources, options) {
46699
46805
  source,
46700
46806
  tablePlan,
46701
46807
  dryRun,
46808
+ sourceHasPreexistingTargetAhead,
46702
46809
  {
46703
46810
  onCopyProgress: (processedRows, sourceRows) => emitMigrationProgress(options, {
46704
46811
  phase: "table-progress",
@@ -46834,6 +46941,7 @@ async function buildMigrationPlan(db, source, projectId2) {
46834
46941
  pgTable: table,
46835
46942
  columns: [],
46836
46943
  unmappedSourceColumns: [],
46944
+ ignoredRetiredSourceColumns: [],
46837
46945
  allowedSkipReason: "redirected to central plugin registry and project state"
46838
46946
  });
46839
46947
  continue;
@@ -46850,12 +46958,19 @@ async function buildMigrationPlan(db, source, projectId2) {
46850
46958
  columns: [],
46851
46959
  partitionProjectId: projectId2,
46852
46960
  unmappedSourceColumns: [],
46961
+ ignoredRetiredSourceColumns: [],
46853
46962
  legacyPreservation: { sourceSchemaSql: readSqliteTableSchema(sqlite, table) }
46854
46963
  });
46855
46964
  continue;
46856
46965
  }
46857
46966
  const pgTable2 = toSnakeCase2(table);
46858
- const { columns: resolvedColumns, targetColumnNames, unmappedSourceColumns } = resolveColumnMapping(
46967
+ const {
46968
+ columns: resolvedColumns,
46969
+ targetColumnNames,
46970
+ unmappedSourceColumns,
46971
+ ignoredRetiredSourceColumns
46972
+ } = resolveColumnMapping(
46973
+ source.pgSchema,
46859
46974
  pgTable2,
46860
46975
  table,
46861
46976
  sqlite,
@@ -46871,7 +46986,8 @@ async function buildMigrationPlan(db, source, projectId2) {
46871
46986
  columns: cols,
46872
46987
  partitionProjectId,
46873
46988
  unmappedSourceColumns,
46874
- allowedSkipReason: disposableSqliteTableReason(ftsVirtualTables, table)
46989
+ ignoredRetiredSourceColumns,
46990
+ allowedSkipReason: retiredSqliteTableSkipReason(source.pgSchema, table) ?? disposableSqliteTableReason(ftsVirtualTables, table)
46875
46991
  });
46876
46992
  continue;
46877
46993
  }
@@ -46881,7 +46997,8 @@ async function buildMigrationPlan(db, source, projectId2) {
46881
46997
  pgTable: pgTable2,
46882
46998
  columns: cols,
46883
46999
  partitionProjectId,
46884
- unmappedSourceColumns
47000
+ unmappedSourceColumns,
47001
+ ignoredRetiredSourceColumns
46885
47002
  });
46886
47003
  }
46887
47004
  return plans;
@@ -46889,6 +47006,79 @@ async function buildMigrationPlan(db, source, projectId2) {
46889
47006
  sqlite.close();
46890
47007
  }
46891
47008
  }
47009
+ async function hasPreexistingTargetAhead(db, source, plans) {
47010
+ const sqlite = openSqlite(source.sqlitePath);
47011
+ try {
47012
+ for (const plan of plans) {
47013
+ if (plan.columns.length === 0 || plan.unmappedSourceColumns.length > 0) continue;
47014
+ const row = sqlite.prepare(`SELECT COUNT(*) AS n FROM ${quoteIdent(plan.table)}`).get();
47015
+ const sourceRows = Number(row.n);
47016
+ if (sourceRows === 0) continue;
47017
+ const targetRows = await countTargetRows(db, plan.pgSchema, plan.pgTable, plan.partitionProjectId);
47018
+ if (targetRows > sourceRows) return true;
47019
+ }
47020
+ return false;
47021
+ } finally {
47022
+ sqlite.close();
47023
+ }
47024
+ }
47025
+ function staleDuplicateSkipReason(targetRows) {
47026
+ return `stale SQLite duplicate; PostgreSQL already has ${targetRows} scoped rows`;
47027
+ }
47028
+ async function sourceRowsArePrimaryKeyConflictCovered(db, sqlite, plan, insertableCols) {
47029
+ const primaryKeyRows = await db.execute(sql`
47030
+ SELECT key_columns.column_name
47031
+ FROM information_schema.table_constraints AS constraints
47032
+ JOIN information_schema.key_column_usage AS key_columns
47033
+ ON key_columns.constraint_catalog = constraints.constraint_catalog
47034
+ AND key_columns.constraint_schema = constraints.constraint_schema
47035
+ AND key_columns.constraint_name = constraints.constraint_name
47036
+ WHERE constraints.constraint_type = 'PRIMARY KEY'
47037
+ AND constraints.table_schema = ${plan.pgSchema}
47038
+ AND constraints.table_name = ${plan.pgTable}
47039
+ ORDER BY key_columns.ordinal_position
47040
+ `);
47041
+ if (primaryKeyRows.length === 0) return false;
47042
+ const columnByPgName = new Map(insertableCols.map((column) => [column.pgName, column]));
47043
+ const primaryKeyColumns = primaryKeyRows.map(({ column_name }) => {
47044
+ if (column_name === "project_id" && plan.partitionProjectId !== void 0) {
47045
+ return { pgName: column_name, valueFor: () => plan.partitionProjectId };
47046
+ }
47047
+ const column = columnByPgName.get(column_name);
47048
+ if (!column) return void 0;
47049
+ return {
47050
+ pgName: column_name,
47051
+ valueFor: (row) => convertValue(
47052
+ row[column.sqliteName],
47053
+ column.type,
47054
+ column.nullJsonbFallback,
47055
+ column.preserveEmptyJsonbString
47056
+ )
47057
+ };
47058
+ });
47059
+ if (primaryKeyColumns.some((column) => column === void 0)) return false;
47060
+ const selectableCols = [...new Set(
47061
+ primaryKeyColumns.flatMap((column) => {
47062
+ if (!column || column.pgName === "project_id" && plan.partitionProjectId !== void 0) return [];
47063
+ return [quoteIdent(columnByPgName.get(column.pgName).sqliteName)];
47064
+ })
47065
+ )].join(", ") || "1";
47066
+ const rows = sqlite.prepare(`SELECT ${selectableCols} FROM ${quoteIdent(plan.table)}`).all();
47067
+ for (const row of rows) {
47068
+ const predicates = primaryKeyColumns.map((column) => {
47069
+ const resolved = column;
47070
+ return sql`${sql.raw(quoteIdent(resolved.pgName))} IS NOT DISTINCT FROM ${resolved.valueFor(row)}`;
47071
+ });
47072
+ const existing = await db.execute(sql`
47073
+ SELECT 1
47074
+ FROM ${sql.raw(quoteIdent(plan.pgSchema))}.${sql.raw(quoteIdent(plan.pgTable))}
47075
+ WHERE ${sql.join(predicates, sql` AND `)}
47076
+ LIMIT 1
47077
+ `);
47078
+ if (existing.length === 0) return false;
47079
+ }
47080
+ return true;
47081
+ }
46892
47082
  function readSqliteTableSchema(db, table) {
46893
47083
  const row = db.prepare(
46894
47084
  `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`
@@ -47052,15 +47242,21 @@ async function loadTargetColumnMetadata(db, pgSchema2) {
47052
47242
  }
47053
47243
  return byTable;
47054
47244
  }
47055
- function resolveColumnMapping(pgTable2, table, sqlite, targetColumnsByTable) {
47245
+ function resolveColumnMapping(sourceSchema, pgTable2, table, sqlite, targetColumnsByTable) {
47056
47246
  const pgCols = targetColumnsByTable.get(pgTable2) ?? [];
47057
47247
  if (pgCols.length === 0) {
47058
- return { columns: [], targetColumnNames: /* @__PURE__ */ new Set(), unmappedSourceColumns: [] };
47248
+ return {
47249
+ columns: [],
47250
+ targetColumnNames: /* @__PURE__ */ new Set(),
47251
+ unmappedSourceColumns: [],
47252
+ ignoredRetiredSourceColumns: []
47253
+ };
47059
47254
  }
47060
47255
  const pgByName = new Map(pgCols.map((c) => [c.column_name, c]));
47061
47256
  const sqliteCols = sqlite.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all();
47062
47257
  const mapping = [];
47063
47258
  const unmappedSourceColumns = [];
47259
+ const ignoredRetiredSourceColumns = [];
47064
47260
  for (const sc of sqliteCols) {
47065
47261
  let pgName = toSnakeCase2(sc.name);
47066
47262
  if (pgName === "project_id" && pgByName.has("owner_project_id")) {
@@ -47068,7 +47264,11 @@ function resolveColumnMapping(pgTable2, table, sqlite, targetColumnsByTable) {
47068
47264
  }
47069
47265
  const pgCol = pgByName.get(pgName);
47070
47266
  if (!pgCol) {
47071
- unmappedSourceColumns.push(sc.name);
47267
+ if (retiredSqliteColumnReason(sourceSchema, table, sc.name)) {
47268
+ ignoredRetiredSourceColumns.push(sc.name);
47269
+ } else {
47270
+ unmappedSourceColumns.push(sc.name);
47271
+ }
47072
47272
  continue;
47073
47273
  }
47074
47274
  const type = classifyColumnType(pgCol);
@@ -47096,9 +47296,20 @@ function resolveColumnMapping(pgTable2, table, sqlite, targetColumnsByTable) {
47096
47296
  return {
47097
47297
  columns: mapping,
47098
47298
  targetColumnNames: new Set(pgByName.keys()),
47099
- unmappedSourceColumns
47299
+ unmappedSourceColumns,
47300
+ ignoredRetiredSourceColumns
47100
47301
  };
47101
47302
  }
47303
+ function retiredSqliteColumnReason(schema, table, column) {
47304
+ return schema === PROJECT_SCHEMA && table === "tasks" && column === "breakIntoSubtasks" ? "retired task-splitting field" : void 0;
47305
+ }
47306
+ function retiredSqliteTableSkipReason(schema, table) {
47307
+ return schema === CENTRAL_SCHEMA && table === "globalConcurrency" ? "retired global concurrency table; PostgreSQL target was removed" : void 0;
47308
+ }
47309
+ function retiredColumnSkipReason(plan) {
47310
+ if (plan.ignoredRetiredSourceColumns.length === 0) return void 0;
47311
+ return `ignored retired SQLite column(s): ${plan.ignoredRetiredSourceColumns.join(", ")}`;
47312
+ }
47102
47313
  function classifyColumnType(pgCol) {
47103
47314
  if (Number(pgCol.is_generated) === 1) {
47104
47315
  return "generated";
@@ -47278,7 +47489,7 @@ async function migrateLegacyPreservationTable(db, source, plan, dryRun, progress
47278
47489
  sqlite.close();
47279
47490
  }
47280
47491
  }
47281
- async function migrateTable(db, source, plan, dryRun, progress = {}) {
47492
+ async function migrateTable(db, source, plan, dryRun, sourceHasPreexistingTargetAhead, progress = {}) {
47282
47493
  if (plan.legacyPreservation) {
47283
47494
  return migrateLegacyPreservationTable(db, source, plan, dryRun, progress);
47284
47495
  }
@@ -47342,15 +47553,17 @@ async function migrateTable(db, source, plan, dryRun, progress = {}) {
47342
47553
  const countRow = sqlite.prepare(`SELECT COUNT(*) AS n FROM ${quoteIdent(plan.table)}`).get();
47343
47554
  sourceRows = Number(countRow.n);
47344
47555
  if (dryRun || sourceRows === 0) {
47556
+ const targetRows2 = await countTargetRows(db, plan.pgSchema, plan.pgTable, plan.partitionProjectId);
47557
+ const staleDuplicate2 = dryRun && sourceRows > 0 && sourceHasPreexistingTargetAhead && targetRows2 >= sourceRows && await sourceRowsArePrimaryKeyConflictCovered(db, sqlite, plan, insertableCols);
47345
47558
  return {
47346
47559
  schema: plan.pgSchema,
47347
47560
  table: plan.pgTable,
47348
47561
  sourceRows,
47349
47562
  insertedRows: 0,
47350
- targetRows: dryRun ? 0 : await countTargetRows(db, plan.pgSchema, plan.pgTable, plan.partitionProjectId),
47351
- verified: dryRun ? false : true,
47352
- skipped: dryRun ? true : false,
47353
- skipReason: dryRun ? "dry-run" : "no source rows"
47563
+ targetRows: targetRows2,
47564
+ verified: staleDuplicate2 ? true : !dryRun,
47565
+ skipped: staleDuplicate2 || dryRun,
47566
+ skipReason: staleDuplicate2 ? staleDuplicateSkipReason(targetRows2) : retiredColumnSkipReason(plan) ?? (dryRun ? "dry-run" : "no source rows")
47354
47567
  };
47355
47568
  }
47356
47569
  const stmt = sqlite.prepare(`SELECT ${selectableCols} FROM ${quoteIdent(plan.table)}`);
@@ -47420,7 +47633,13 @@ async function migrateTable(db, source, plan, dryRun, progress = {}) {
47420
47633
  `Row-count mismatch for ${plan.pgSchema}.${plan.pgTable}: source=${sourceRows}, target=${targetRows}`
47421
47634
  );
47422
47635
  }
47423
- const verified = rowCountOk && contentOk;
47636
+ const staleDuplicate = insertedRows === 0 && sourceHasPreexistingTargetAhead && targetRows >= sourceRows;
47637
+ if (staleDuplicate) {
47638
+ log4.log(
47639
+ `Skipping stale SQLite duplicate for ${plan.pgSchema}.${plan.pgTable}: all ${sourceRows} source rows conflicted while PostgreSQL already has ${targetRows} scoped rows.`
47640
+ );
47641
+ }
47642
+ const verified = staleDuplicate || rowCountOk && contentOk;
47424
47643
  return {
47425
47644
  schema: plan.pgSchema,
47426
47645
  table: plan.pgTable,
@@ -47428,7 +47647,8 @@ async function migrateTable(db, source, plan, dryRun, progress = {}) {
47428
47647
  insertedRows,
47429
47648
  targetRows,
47430
47649
  verified,
47431
- skipped: false
47650
+ skipped: staleDuplicate,
47651
+ skipReason: staleDuplicate ? staleDuplicateSkipReason(targetRows) : retiredColumnSkipReason(plan)
47432
47652
  };
47433
47653
  } finally {
47434
47654
  sqlite.close();
@@ -47813,11 +48033,11 @@ async function rekeyFallbackProjectPartition(db, fallbackProjectId, registeredPr
47813
48033
  const parentKey = `${fk.parentSchema}.${fk.parentTable}`;
47814
48034
  if (!targetKeys.has(parentKey)) continue;
47815
48035
  const childTarget = targetKeys.has(`${fk.childSchema}.${fk.childTable}`);
47816
- const join103 = fk.childColumns.map((column, index2) => `child.${quoteIdentifier(column)} IS NOT DISTINCT FROM parent.${quoteIdentifier(fk.parentColumns[index2])}`).join(" AND ");
48036
+ const join104 = fk.childColumns.map((column, index2) => `child.${quoteIdentifier(column)} IS NOT DISTINCT FROM parent.${quoteIdentifier(fk.parentColumns[index2])}`).join(" AND ");
47817
48037
  const registeredChildScope = childTarget ? ` AND child.${quoteIdentifier("project_id")} = ${quoteLiteral(registeredProjectId)}` : "";
47818
48038
  const unreplaced = await tx.execute(sql.raw(`SELECT EXISTS (
47819
48039
  SELECT 1 FROM ${relationName({ schema: fk.childSchema, table: fk.childTable })} child
47820
- JOIN ${relationName({ schema: fk.parentSchema, table: fk.parentTable })} parent ON ${join103}
48040
+ JOIN ${relationName({ schema: fk.parentSchema, table: fk.parentTable })} parent ON ${join104}
47821
48041
  JOIN fusion_rekey_conflict_candidates parent_candidate
47822
48042
  ON parent_candidate.schema_name = ${quoteLiteral(fk.parentSchema)}
47823
48043
  AND parent_candidate.table_name = ${quoteLiteral(fk.parentTable)}
@@ -58813,8 +59033,8 @@ var require_CronFileParser = __commonJS({
58813
59033
  * @throws If file cannot be read
58814
59034
  */
58815
59035
  static async parseFile(filePath) {
58816
- const { readFile: readFile54 } = await Promise.resolve().then(() => __importStar(__require("fs/promises")));
58817
- const data = await readFile54(filePath, "utf8");
59036
+ const { readFile: readFile55 } = await Promise.resolve().then(() => __importStar(__require("fs/promises")));
59037
+ const data = await readFile55(filePath, "utf8");
58818
59038
  return _CronFileParser.#parseContent(data);
58819
59039
  }
58820
59040
  /**
@@ -62244,6 +62464,7 @@ async function bootSchemaBackendOnce(options, bypassProjectIsolation = false) {
62244
62464
  bypassProjectIsolation
62245
62465
  }) : await createConnectionSetFromUrl(resolvedBackend, {
62246
62466
  poolMax: options.poolMax,
62467
+ env,
62247
62468
  bypassProjectIsolation
62248
62469
  });
62249
62470
  await applySchemaBaseline(connections.migration);
@@ -62598,6 +62819,7 @@ async function createTaskStoreForBackend(options) {
62598
62819
  await connections.close();
62599
62820
  connections = await createConnectionSetFromUrl(resolvedBackend, {
62600
62821
  poolMax: options.poolMax,
62822
+ env,
62601
62823
  projectId: resolvedProjectId,
62602
62824
  useRuntimeRole: runtimeRoleRows[0]?.usable === true
62603
62825
  });
@@ -62646,6 +62868,7 @@ async function createTaskStoreForBackend(options) {
62646
62868
  bypassProjectIsolation: true
62647
62869
  }) : await createConnectionSetFromUrl(resolvedBackend, {
62648
62870
  poolMax: 1,
62871
+ env,
62649
62872
  bypassProjectIsolation: true
62650
62873
  });
62651
62874
  try {
@@ -71734,6 +71957,8 @@ function archiveEntryToTask(entry, slim = false) {
71734
71957
  attachments: slim ? void 0 : entry.attachments,
71735
71958
  comments: entry.comments,
71736
71959
  review: slim ? void 0 : entry.review,
71960
+ // FNXC:ArchivedRecommendations 2026-09-20-17:23: archive reads retain actionable terminal recommendation state.
71961
+ recommendations: entry.recommendations,
71737
71962
  log: slim ? [] : entry.log ?? [],
71738
71963
  timedExecutionMs: slim ? computeTimedExecutionMs(entry.log) : void 0,
71739
71964
  createdAt: entry.createdAt,
@@ -95698,9 +95923,9 @@ async function deleteArchivedTaskEntry(db, id, projectId2) {
95698
95923
  async function archiveParentTaskWithLineageGate(layer, taskId, entry, options = {}) {
95699
95924
  const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
95700
95925
  return layer.transactionImmediate(async (tx) => {
95926
+ await acquireTaskAdvisoryXactLock(tx, layer.projectId, taskId);
95927
+ const live = await readTaskRowInTransaction(tx, taskId, { includeDeleted: true }, layer.projectId);
95701
95928
  if (options.livenessWipLanes) {
95702
- await acquireTaskAdvisoryXactLock(tx, layer.projectId, taskId);
95703
- const live = await readTaskRowInTransaction(tx, taskId, void 0, layer.projectId);
95704
95929
  const verdict = decideArchiveLiveness({ column: String(live?.column ?? ""), status: live?.status, wipLanes: options.livenessWipLanes });
95705
95930
  if (verdict.live) return { archived: false, liveVerdict: verdict };
95706
95931
  }
@@ -95712,7 +95937,8 @@ async function archiveParentTaskWithLineageGate(layer, taskId, entry, options =
95712
95937
  if (options.removeLineageReferences && options.revalidateAgainst !== void 0) assertLineageCandidatesUnchanged(liveChildIds, options.revalidateAgainst);
95713
95938
  const lineageOutcome = options.removeLineageReferences ? await removeLineageReferences(tx, taskId, options.revalidateAgainst ?? liveChildIds, now, layer.projectId, options.promptByChildId, options.evidenceTargetVersionForTest) : { clearedChildIds: [], evidenceVersionByChild: /* @__PURE__ */ new Map(), evidenceUnavailableChildIds: [], evidenceInsertAttempts: 0 };
95714
95939
  await options.beforeArchive?.(tx);
95715
- await upsertArchivedTaskEntry(tx, entry, layer.projectId);
95940
+ const lockedEntry = Array.isArray(live?.recommendations) ? { ...entry, recommendations: live.recommendations } : entry;
95941
+ await upsertArchivedTaskEntry(tx, lockedEntry, layer.projectId);
95716
95942
  await softDeleteTaskRowInTransaction(tx, taskId, now, false, layer.projectId);
95717
95943
  return options.revalidateAgainst === void 0 ? { archived: true } : { archived: true, lineageOutcome };
95718
95944
  });
@@ -99376,10 +99602,11 @@ async function linkTaskRecommendationImpl(store, id, recommendationId, createdTa
99376
99602
  await acquireTaskAdvisoryXactLock(tx, layer.projectId, id);
99377
99603
  const row = await readTaskRowInTransaction(tx, id, { includeDeleted: true }, layer.projectId);
99378
99604
  if (!row) throw new TaskNotFoundError(id);
99379
- if (row.deletedAt) throw new TaskDeletedError(id, row.deletedAt);
99380
- const current = store.rowToTask(store.pgRowToTaskRow(row));
99381
- if (completeColumns && !completeColumns.has(current.column)) {
99382
- throw new Error("Recommendations are available only on completed tasks");
99605
+ const archivedEntry = row.deletedAt ? await findArchivedTaskEntry(tx, id, layer.projectId) : void 0;
99606
+ if (row.deletedAt && !archivedEntry) throw new TaskDeletedError(id, row.deletedAt);
99607
+ const current = archivedEntry ? store.archiveEntryToTask(archivedEntry, false) : store.rowToTask(store.pgRowToTaskRow(row));
99608
+ if (!archivedEntry && completeColumns && !completeColumns.has(current.column)) {
99609
+ throw new Error("Recommendations are available only on completed or archived tasks");
99383
99610
  }
99384
99611
  const index2 = current.recommendations?.findIndex((item) => item.id === recommendationId) ?? -1;
99385
99612
  if (index2 < 0) throw new Error("Recommendation no longer exists");
@@ -99387,26 +99614,39 @@ async function linkTaskRecommendationImpl(store, id, recommendationId, createdTa
99387
99614
  if (recommendation.createdTaskId && recommendation.createdTaskId !== createdTaskId) {
99388
99615
  throw new Error("Recommendation is already linked to another task");
99389
99616
  }
99390
- if (recommendation.createdTaskId === createdTaskId) return current;
99617
+ if (recommendation.createdTaskId === createdTaskId) return { task: current, archived: Boolean(archivedEntry) };
99391
99618
  const recommendations = current.recommendations.map(
99392
99619
  (item) => item.id === recommendationId ? { ...item, createdTaskId } : item
99393
99620
  );
99394
99621
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
99622
+ if (archivedEntry) {
99623
+ const updatedEntry = { ...archivedEntry, recommendations, updatedAt };
99624
+ await upsertArchivedTaskEntry(tx, updatedEntry, layer.projectId);
99625
+ await tx.update(project_exports.tasks).set({ recommendations, updatedAt }).where(and(
99626
+ eq(project_exports.tasks.id, id),
99627
+ taskProjectScope(layer),
99628
+ isNotNull(project_exports.tasks.deletedAt)
99629
+ ));
99630
+ return { task: store.archiveEntryToTask(updatedEntry, false), archived: true };
99631
+ }
99395
99632
  const [updatedRow] = await tx.update(project_exports.tasks).set({ recommendations, updatedAt }).where(and(
99396
99633
  eq(project_exports.tasks.id, id),
99397
99634
  taskProjectScope(layer),
99635
+ isNull(project_exports.tasks.deletedAt),
99398
99636
  ...completeColumns ? [inArray(project_exports.tasks.column, [...completeColumns])] : []
99399
99637
  )).returning();
99400
99638
  if (!updatedRow) {
99401
- if (completeColumns) throw new Error("Recommendations are available only on completed tasks");
99639
+ if (completeColumns) throw new Error("Recommendations are available only on completed or archived tasks");
99402
99640
  throw new TaskNotFoundError(id);
99403
99641
  }
99404
- return store.rowToTask(store.pgRowToTaskRow(updatedRow));
99642
+ return { task: store.rowToTask(store.pgRowToTaskRow(updatedRow)), archived: false };
99405
99643
  });
99406
- await store.writeTaskJsonFile(store.taskDir(id), updated);
99407
- if (store.isWatching) store.taskCache.set(id, { ...updated });
99408
- store.emitTaskLifecycleEventSafely("task:updated", [updated]);
99409
- return updated;
99644
+ if (!updated.archived) {
99645
+ await store.writeTaskJsonFile(store.taskDir(id), updated.task);
99646
+ if (store.isWatching) store.taskCache.set(id, { ...updated.task });
99647
+ }
99648
+ store.emitTaskLifecycleEventSafely("task:updated", [updated.task]);
99649
+ return updated.task;
99410
99650
  });
99411
99651
  }
99412
99652
  async function normalizeWorkspaceTaskWorktreeMetadataImpl(store, id) {
@@ -102980,6 +103220,12 @@ async function taskToArchiveEntryImpl(store, task, archivedAt) {
102980
103220
  comments: task.comments,
102981
103221
  review: task.review,
102982
103222
  reviewState: task.reviewState,
103223
+ /*
103224
+ FNXC:ArchivedRecommendations 2026-09-20-17:23:
103225
+ The cold archive is the authoritative terminal snapshot. Preserve recommendation records,
103226
+ including createdTaskId, so archival cannot erase an accepted follow-up link.
103227
+ */
103228
+ recommendations: task.recommendations,
102983
103229
  prompt,
102984
103230
  ...agentLogFields,
102985
103231
  log: [{ timestamp: archivedAt, action: "Task archived" }],
@@ -108747,27 +108993,36 @@ async function listTaskRecommendationsImpl(store, options) {
108747
108993
  const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.min(200, Math.max(1, Math.trunc(rawLimit))) : 50;
108748
108994
  const offset = typeof rawOffset === "number" && Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
108749
108995
  const columns = [...completeColumns];
108750
- if (columns.length === 0) return { items: [], rowOffset: offset, rowLimit: limit, returnedRowCount: 0, totalRowCount: 0, hasMore: false };
108751
- const filter = and(
108996
+ const liveFilter = columns.length === 0 ? void 0 : and(
108752
108997
  taskProjectScope(layer),
108753
108998
  isNull(project_exports.tasks.deletedAt),
108754
108999
  inArray(project_exports.tasks.column, columns),
108755
109000
  isNotNull(project_exports.tasks.recommendations),
108756
109001
  sql`jsonb_array_length(${project_exports.tasks.recommendations}) > 0`
108757
109002
  );
108758
- const [countRows, rows] = await Promise.all([
108759
- layer.db.select({ count: sql`count(*)` }).from(project_exports.tasks).where(filter),
108760
- layer.db.select({ id: project_exports.tasks.id, title: project_exports.tasks.title, column: project_exports.tasks.column, updatedAt: project_exports.tasks.updatedAt, recommendations: project_exports.tasks.recommendations }).from(project_exports.tasks).where(filter).orderBy(desc(project_exports.tasks.updatedAt), desc(project_exports.tasks.id)).limit(limit).offset(offset)
109003
+ const [liveRows, archivedEntries] = await Promise.all([
109004
+ liveFilter ? layer.db.select({ id: project_exports.tasks.id, title: project_exports.tasks.title, column: project_exports.tasks.column, updatedAt: project_exports.tasks.updatedAt, recommendations: project_exports.tasks.recommendations }).from(project_exports.tasks).where(liveFilter).orderBy(desc(project_exports.tasks.updatedAt), desc(project_exports.tasks.id)) : Promise.resolve([]),
109005
+ listArchivedTasks(layer.db, layer.projectId)
108761
109006
  ]);
109007
+ const rows = [
109008
+ ...liveRows.map((row) => ({
109009
+ id: row.id,
109010
+ title: row.title ?? void 0,
109011
+ column: row.column,
109012
+ updatedAt: row.updatedAt,
109013
+ recommendations: Array.isArray(row.recommendations) ? row.recommendations : []
109014
+ })),
109015
+ ...archivedEntries.filter((entry) => Array.isArray(entry.recommendations) && entry.recommendations.length > 0).map((entry) => ({ id: entry.id, title: entry.title, column: "archived", updatedAt: entry.updatedAt, recommendations: entry.recommendations }))
109016
+ ].sort((a, b3) => b3.updatedAt.localeCompare(a.updatedAt) || b3.id.localeCompare(a.id));
109017
+ const page = rows.slice(offset, offset + limit);
108762
109018
  const items = [];
108763
- for (const row of rows) {
108764
- if (!Array.isArray(row.recommendations) || row.recommendations.length === 0) continue;
109019
+ for (const row of page) {
108765
109020
  for (const recommendation of row.recommendations) {
108766
- items.push({ taskId: row.id, taskTitle: row.title ?? void 0, taskColumn: row.column, updatedAt: row.updatedAt, recommendation });
109021
+ items.push({ taskId: row.id, taskTitle: row.title, taskColumn: row.column, updatedAt: row.updatedAt, recommendation });
108767
109022
  }
108768
109023
  }
108769
- const totalRowCount = Number(countRows[0]?.count ?? 0);
108770
- const returnedRowCount = rows.length;
109024
+ const totalRowCount = rows.length;
109025
+ const returnedRowCount = page.length;
108771
109026
  return { items, rowOffset: offset, rowLimit: limit, returnedRowCount, totalRowCount, hasMore: offset + returnedRowCount < totalRowCount };
108772
109027
  }
108773
109028
  var LEGACY_REVIEW_LANES5, LEGACY_ARCHIVE_LANES2;
@@ -120354,6 +120609,7 @@ var init_bundled_plugin_install = __esm({
120354
120609
  "fusion-plugin-openclaw-runtime",
120355
120610
  "fusion-plugin-paperclip-runtime",
120356
120611
  "fusion-plugin-cursor-runtime",
120612
+ "fusion-plugin-antigravity-runtime",
120357
120613
  "fusion-plugin-grok-runtime",
120358
120614
  "fusion-plugin-claude-runtime",
120359
120615
  // FNXC:OmpAcp 2026-07-11-23:35: Oh My Pi ACP runtime available as a staged/bundled install target.
@@ -143053,6 +143309,7 @@ __export(src_exports, {
143053
143309
  buildTaskDeleteNoticeIdempotencyKey: () => buildTaskDeleteNoticeIdempotencyKey,
143054
143310
  buildTaskExternalBlockClearPatch: () => buildTaskExternalBlockClearPatch,
143055
143311
  buildTaskExternalBlockPatch: () => buildTaskExternalBlockPatch,
143312
+ buildTaskExternalBlockReport: () => buildTaskExternalBlockReport,
143056
143313
  buildTaskLineageTrailer: () => buildTaskLineageTrailer,
143057
143314
  buildTaskLogReadOnlyMessage: () => buildTaskLogReadOnlyMessage,
143058
143315
  buildTaskNotFoundMessage: () => buildTaskNotFoundMessage,
@@ -144595,8 +144852,8 @@ var init_src2 = __esm({
144595
144852
  });
144596
144853
 
144597
144854
  // ../engine/src/execution/lifecycle-move.ts
144598
- async function moveTaskToContainedBackwardTarget(store, taskId, reason, options, liveColumn) {
144599
- const column = liveColumn ?? (await store.getTask(taskId)).column;
144855
+ async function moveTaskToContainedBackwardTarget(store, taskId, reason, options, _liveColumn) {
144856
+ const column = (await store.getTask(taskId)).column;
144600
144857
  const revisionReasons = /* @__PURE__ */ new Set([
144601
144858
  "plan-review-revise-replan",
144602
144859
  "code-review-revise-remediation",
@@ -154573,8 +154830,8 @@ var require_graceful_fs = __commonJS({
154573
154830
  fs11.createReadStream = createReadStream;
154574
154831
  fs11.createWriteStream = createWriteStream;
154575
154832
  var fs$readFile = fs11.readFile;
154576
- fs11.readFile = readFile54;
154577
- function readFile54(path6, options, cb) {
154833
+ fs11.readFile = readFile55;
154834
+ function readFile55(path6, options, cb) {
154578
154835
  if (typeof options === "function")
154579
154836
  cb = options, options = null;
154580
154837
  return go$readFile(path6, options, cb);
@@ -155736,6 +155993,27 @@ function createFusionCredentialStore(authStorage, resolvedCredentialInstance) {
155736
155993
  const normalizedProviderId = providerId === ANTHROPIC_PROVIDER_ID2 ? ANTHROPIC_PROVIDER_ID2 : providerId;
155737
155994
  return resolvedCredentialInstance?.providerId === normalizedProviderId ? resolvedCredentialInstance : void 0;
155738
155995
  };
155996
+ const readCredential = (providerId) => {
155997
+ const scopedRef = scopedRefFor(providerId);
155998
+ return scopedRef ? authStorage.getInstance(scopedRef) : authStorage.get(providerId);
155999
+ };
156000
+ const modifyCredential = async (providerId, fn) => {
156001
+ const scopedRef = scopedRefFor(providerId);
156002
+ if (!scopedRef) {
156003
+ return authStorage.modify(
156004
+ providerId,
156005
+ async (current) => fn(current)
156006
+ );
156007
+ }
156008
+ const next = await fn(authStorage.getInstance(scopedRef));
156009
+ if (next) await authStorage.setInstance(scopedRef, next);
156010
+ return next;
156011
+ };
156012
+ const deleteCredential = async (providerId) => {
156013
+ const scopedRef = scopedRefFor(providerId);
156014
+ if (scopedRef) await authStorage.removeInstance(scopedRef);
156015
+ else await authStorage.remove(providerId);
156016
+ };
155739
156017
  return {
155740
156018
  /*
155741
156019
  FNXC:ProviderAuth 2026-07-17-06:30:
@@ -155747,23 +156025,36 @@ function createFusionCredentialStore(authStorage, resolvedCredentialInstance) {
155747
156025
  const token = await authStorage.getApiKey(ANTHROPIC_PROVIDER_ID2, scopedRef);
155748
156026
  return token ? { type: "api_key", key: token } : void 0;
155749
156027
  }
155750
- return scopedRef ? authStorage.getInstance(scopedRef) : authStorage.get(providerId);
156028
+ if (providerId === META_PROVIDER_ID) {
156029
+ const rawCredential = readCredential(META_PROVIDER_ID);
156030
+ if (rawCredential?.type === "api_key") return rawCredential;
156031
+ const subscriptionCredential = readCredential(META_SUBSCRIPTION_PROVIDER_ID);
156032
+ return subscriptionCredential?.type === "oauth" ? subscriptionCredential : rawCredential;
156033
+ }
156034
+ return readCredential(providerId);
155751
156035
  },
155752
156036
  list: async () => authStorage.list().flatMap((providerId) => {
155753
156037
  const credential = scopedRefFor(providerId) ? authStorage.getInstance(scopedRefFor(providerId)) : authStorage.get(providerId);
155754
156038
  return credential?.type === "api_key" || credential?.type === "oauth" ? [{ providerId, type: credential.type }] : [];
155755
156039
  }),
155756
156040
  modify: async (providerId, fn) => {
155757
- const scopedRef = scopedRefFor(providerId);
155758
- if (!scopedRef) return authStorage.modify(providerId, async (current) => fn(current));
155759
- const next = await fn(authStorage.getInstance(scopedRef));
155760
- if (next) await authStorage.setInstance(scopedRef, next);
155761
- return next;
156041
+ if (providerId === META_PROVIDER_ID) {
156042
+ const rawCredential = readCredential(META_PROVIDER_ID);
156043
+ if (rawCredential?.type !== "api_key" && readCredential(META_SUBSCRIPTION_PROVIDER_ID)?.type === "oauth") {
156044
+ return modifyCredential(META_SUBSCRIPTION_PROVIDER_ID, fn);
156045
+ }
156046
+ }
156047
+ return modifyCredential(providerId, fn);
155762
156048
  },
155763
156049
  delete: async (providerId) => {
155764
- const scopedRef = scopedRefFor(providerId);
155765
- if (scopedRef) await authStorage.removeInstance(scopedRef);
155766
- else await authStorage.remove(providerId);
156050
+ if (providerId === META_PROVIDER_ID) {
156051
+ const rawCredential = readCredential(META_PROVIDER_ID);
156052
+ if (rawCredential?.type !== "api_key" && readCredential(META_SUBSCRIPTION_PROVIDER_ID)?.type === "oauth") {
156053
+ await deleteCredential(META_SUBSCRIPTION_PROVIDER_ID);
156054
+ return;
156055
+ }
156056
+ }
156057
+ await deleteCredential(providerId);
155767
156058
  }
155768
156059
  };
155769
156060
  }
@@ -156369,7 +156660,7 @@ function createFusionAuthStorage() {
156369
156660
  }
156370
156661
  });
156371
156662
  }
156372
- var import_proper_lockfile, AUTH_LOCK_OPTIONS, authWriteQueues, FusionFileAuthStorage, CredentialInstanceResolutionError, OAUTH_REFRESH_BUFFER_MS, ANTHROPIC_PROVIDER_ID2, OAUTH_REFRESH_FAILURE_COOLDOWN_MS;
156663
+ var import_proper_lockfile, AUTH_LOCK_OPTIONS, authWriteQueues, FusionFileAuthStorage, CredentialInstanceResolutionError, OAUTH_REFRESH_BUFFER_MS, ANTHROPIC_PROVIDER_ID2, META_PROVIDER_ID, META_SUBSCRIPTION_PROVIDER_ID, OAUTH_REFRESH_FAILURE_COOLDOWN_MS;
156373
156664
  var init_auth_storage = __esm({
156374
156665
  "../engine/src/auth/auth-storage.ts"() {
156375
156666
  "use strict";
@@ -156670,8 +156961,9 @@ var init_auth_storage = __esm({
156670
156961
  return { result: existing, changed: false };
156671
156962
  });
156672
156963
  }
156964
+ // FNXC:ProviderAuth 2026-09-20-16:20: Pi 0.86.1 registers Meta's Muse OAuth login; preserve the raw runtime id for ModelRuntime.login.
156673
156965
  getOAuthProviders() {
156674
- return [{ id: "anthropic", name: "Anthropic" }, { id: "openai-codex", name: "OpenAI Codex" }, { id: "github-copilot", name: "GitHub Copilot" }];
156966
+ return [{ id: "anthropic", name: "Anthropic" }, { id: "openai-codex", name: "OpenAI Codex" }, { id: "github-copilot", name: "GitHub Copilot" }, { id: "meta", name: "Meta (Muse)" }];
156675
156967
  }
156676
156968
  setModelRuntime(modelRuntime) {
156677
156969
  this.modelRuntime = modelRuntime;
@@ -156713,6 +157005,8 @@ var init_auth_storage = __esm({
156713
157005
  };
156714
157006
  OAUTH_REFRESH_BUFFER_MS = 5 * 6e4;
156715
157007
  ANTHROPIC_PROVIDER_ID2 = "anthropic";
157008
+ META_PROVIDER_ID = "meta";
157009
+ META_SUBSCRIPTION_PROVIDER_ID = "meta-subscription";
156716
157010
  OAUTH_REFRESH_FAILURE_COOLDOWN_MS = 3e4;
156717
157011
  }
156718
157012
  });
@@ -157540,7 +157834,7 @@ function derivePreviousText(partial2, contentIndex, delta, kind) {
157540
157834
  const content = partial2?.content;
157541
157835
  const block = Array.isArray(content) && Number.isInteger(contentIndex) && contentIndex >= 0 ? content[contentIndex] : void 0;
157542
157836
  const accumulated = getContentText(block, kind);
157543
- return accumulated && delta && accumulated.endsWith(delta) ? accumulated.slice(0, Math.max(0, accumulated.length - delta.length)) : accumulated;
157837
+ return accumulated && delta && accumulated.endsWith(delta) ? accumulated.slice(0, Math.max(0, accumulated.length - delta.length)) : "";
157544
157838
  }
157545
157839
  function identityFor(partial2, contentIndex) {
157546
157840
  const objectPartial = partial2 !== null && typeof partial2 === "object" ? partial2 : void 0;
@@ -157591,17 +157885,10 @@ function indexOf(value) {
157591
157885
  if (value === void 0) return 0;
157592
157886
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
157593
157887
  }
157594
- function blockText(partial2, index2, kind) {
157595
- const content = record(partial2)?.content;
157596
- if (!Array.isArray(content) || index2 === void 0) return "";
157597
- const block = record(content[index2]);
157598
- if (block?.type !== kind) return "";
157599
- const value = block[kind === "text" ? "text" : "thinking"];
157600
- return typeof value === "string" ? value : "";
157601
- }
157602
157888
  function createAssistantStreamCapture(sinks) {
157603
157889
  const normalizer = createStreamingDeltaNormalizer();
157604
157890
  const emitted = { text: /* @__PURE__ */ new Map(), thinking: /* @__PURE__ */ new Map() };
157891
+ const seen = { text: /* @__PURE__ */ new Map(), thinking: /* @__PURE__ */ new Map() };
157605
157892
  let lastPartial;
157606
157893
  let lastTextPartial;
157607
157894
  let lastTextIndex;
@@ -157609,6 +157896,8 @@ function createAssistantStreamCapture(sinks) {
157609
157896
  const reset2 = () => {
157610
157897
  emitted.text.clear();
157611
157898
  emitted.thinking.clear();
157899
+ seen.text.clear();
157900
+ seen.thinking.clear();
157612
157901
  normalizer.noteBoundary("text");
157613
157902
  normalizer.noteBoundary("thinking");
157614
157903
  lastPartial = void 0;
@@ -157664,12 +157953,17 @@ function createAssistantStreamCapture(sinks) {
157664
157953
  if (!kind) return;
157665
157954
  if (type === `${kind}_delta`) {
157666
157955
  if (typeof update.delta !== "string" || index2 === void 0) return;
157667
- const delta = normalizer.normalize(partial2, index2, update.delta, kind);
157668
- emit(kind, delta, partial2, index2);
157669
- const full = blockText(partial2, index2, kind);
157670
- emitted[kind].set(index2, full ? full.length : (emitted[kind].get(index2) ?? 0) + update.delta.length);
157956
+ const seenBefore = seen[kind].get(index2) ?? 0;
157957
+ const seenAfter = seenBefore + update.delta.length;
157958
+ const emittedBefore = emitted[kind].get(index2) ?? 0;
157959
+ const sourceSegment = update.delta.slice(Math.max(0, emittedBefore - seenBefore));
157960
+ if (sourceSegment) {
157961
+ const delta = normalizer.normalize(partial2, index2, sourceSegment, kind);
157962
+ emit(kind, delta, partial2, index2);
157963
+ }
157964
+ seen[kind].set(index2, seenAfter);
157965
+ emitted[kind].set(index2, Math.max(emittedBefore, seenAfter));
157671
157966
  } else if (type === `${kind}_start`) {
157672
- flush(kind, blockText(partial2, index2, kind), partial2, index2);
157673
157967
  } else if (type === `${kind}_end`) {
157674
157968
  flush(kind, typeof update.content === "string" ? update.content : "", partial2, index2);
157675
157969
  }
@@ -158218,6 +158512,9 @@ function buildMissingClaudeRuntimeError() {
158218
158512
  function buildMissingCursorRuntimeError() {
158219
158513
  return unavailable("Cursor CLI", "Install and enable the Cursor runtime plugin (fusion-plugin-cursor-runtime), install `cursor-agent`, and authenticate with `cursor-agent login`.");
158220
158514
  }
158515
+ function buildMissingAntigravityRuntimeError() {
158516
+ return unavailable("Google Antigravity CLI", "Install and enable the Antigravity runtime plugin (fusion-plugin-antigravity-runtime), install `agy`, and complete its vendor login flow.");
158517
+ }
158221
158518
  function getCliProviderRouting(providerId) {
158222
158519
  return CLI_PROVIDER_ROUTING_CENSUS.find((entry) => entry.providerId === providerId);
158223
158520
  }
@@ -158302,7 +158599,9 @@ var init_cli_provider_routing = __esm({
158302
158599
  { providerId: "grok-cli", classification: "runtime-routed", runtimeId: "grok", autoDerive: "fail-fast", guardNotApplicable: "pinned-pi-fallback", onExplicitHint: "defer-to-resolve-runtime", fallbackPolicy: "defer-to-runtime", missingRuntimeError: buildMissingGrokRuntimeError, rationale: "Visible-key, fallback-only, and explicit-hint paths intentionally preserve the shipped direct xAI/pi fallback." },
158303
158600
  { providerId: "hermes", classification: "runtime-routed", runtimeId: "hermes", autoDerive: "fail-fast", guardNotApplicable: "pinned-pi-fallback", onExplicitHint: "assert-available", fallbackPolicy: "drop-with-warning", missingRuntimeError: buildMissingHermesRuntimeError, rationale: "Fallback-only Hermes cannot be resolved by a healthy primary pi runtime." },
158304
158601
  { providerId: "claude-cli", classification: "runtime-routed", runtimeId: "claude", autoDerive: "fail-fast", guardNotApplicable: "pinned-pi-fallback", onExplicitHint: "assert-available", fallbackPolicy: "drop-with-warning", missingRuntimeError: buildMissingClaudeRuntimeError, rationale: "Fallback-only Claude CLI cannot be resolved by a healthy primary pi runtime." },
158305
- { providerId: "cursor-cli", classification: "runtime-routed", runtimeId: "cursor", autoDerive: "fail-fast", guardNotApplicable: "pinned-pi-fallback", onExplicitHint: "assert-available", fallbackPolicy: "defer-cross-runtime", missingRuntimeError: buildMissingCursorRuntimeError, rationale: "Cursor fallback is withheld from a healthy foreign runtime and armed for a single prompt-time cross-runtime swap." }
158602
+ { providerId: "cursor-cli", classification: "runtime-routed", runtimeId: "cursor", autoDerive: "fail-fast", guardNotApplicable: "pinned-pi-fallback", onExplicitHint: "assert-available", fallbackPolicy: "defer-cross-runtime", missingRuntimeError: buildMissingCursorRuntimeError, rationale: "Cursor fallback is withheld from a healthy foreign runtime and armed for a single prompt-time cross-runtime swap." },
158603
+ // FNXC:AntigravityProvider 2026-09-20-18:32: Agy has no proven safe cross-runtime context transfer, so fallback-only selection must retain the healthy primary runtime.
158604
+ { providerId: "antigravity-cli", classification: "runtime-routed", runtimeId: "antigravity", autoDerive: "fail-fast", guardNotApplicable: "pinned-pi-fallback", onExplicitHint: "assert-available", fallbackPolicy: "drop-with-warning", missingRuntimeError: buildMissingAntigravityRuntimeError, rationale: "Non-ACP agy fallback is dropped rather than silently replacing the active primary." }
158306
158605
  ];
158307
158606
  }
158308
158607
  });
@@ -187682,8 +187981,8 @@ async function runWorkflowMergeAttemptNode(deps, ctx, task) {
187682
187981
  };
187683
187982
  }
187684
187983
  function classifyMergePrimitiveResult(data, value, primitiveOutcome) {
187685
- if (value === MERGE_BOUNDARY_UNPROVEN_VALUE) {
187686
- return { outcome: "failure", value: MERGE_BOUNDARY_UNPROVEN_VALUE };
187984
+ if (value === MERGE_BOUNDARY_RECOVERY_VALUE || value === MERGE_BOUNDARY_UNPROVEN_VALUE) {
187985
+ return { outcome: "failure", value };
187687
187986
  }
187688
187987
  if (data?.status === "merged") {
187689
187988
  return { outcome: "success", value: data.noOp ? "already-landed" : "merged" };
@@ -187727,11 +188026,12 @@ function classifyMergeFailure(reason) {
187727
188026
  }
187728
188027
  return { outcome: "failure", value: "merge-failed" };
187729
188028
  }
187730
- var MERGE_BOUNDARY_UNPROVEN_VALUE, PRESERVED_MERGE_FAILURE_REASONS;
188029
+ var MERGE_BOUNDARY_UNPROVEN_VALUE, MERGE_BOUNDARY_RECOVERY_VALUE, PRESERVED_MERGE_FAILURE_REASONS;
187731
188030
  var init_workflow_merge_nodes = __esm({
187732
188031
  "../engine/src/workflows/workflow-merge-nodes.ts"() {
187733
188032
  "use strict";
187734
188033
  MERGE_BOUNDARY_UNPROVEN_VALUE = "merge-boundary-unproven";
188034
+ MERGE_BOUNDARY_RECOVERY_VALUE = "merge-boundary-evidence-recovery";
187735
188035
  PRESERVED_MERGE_FAILURE_REASONS = /* @__PURE__ */ new Set(["implementation-incomplete", "merge-unavailable", "workspace-review-required"]);
187736
188036
  }
187737
188037
  });
@@ -194881,9 +195181,9 @@ async function isRememberedWorkspaceWorktreeLive(repoRootDir, worktreePath) {
194881
195181
  async function acquireWorkspaceRepoWorktree(opts) {
194882
195182
  const { repoRelPath, workspaceRootDir, task, store, settings, logger: logger3, secretsStore, audit, runContext, runConfiguredCommand: runConfiguredCommand2, ensureDependencyReadiness, taskEnv, validateTaskBeforeCreate } = opts;
194883
195183
  const registry3 = opts.registry ?? activeSessionRegistry;
194884
- const { join: join103 } = await import("node:path");
195184
+ const { join: join104 } = await import("node:path");
194885
195185
  assertInRootRepoRelPath(repoRelPath);
194886
- const repoAbsPath = join103(workspaceRootDir, repoRelPath);
195186
+ const repoAbsPath = join104(workspaceRootDir, repoRelPath);
194887
195187
  let durableAcquireLease;
194888
195188
  let acquireLeaseLost = false;
194889
195189
  let acquireLeaseRenewalInFlight;
@@ -196589,6 +196889,15 @@ function shouldWithholdWedgeAlertForAutoRecovery(task, options) {
196589
196889
  }
196590
196890
  function describeTaskWedge(task) {
196591
196891
  if (isTaskProgressing(task)) return null;
196892
+ if (task.status === "blocked" && task.paused === true && task.pausedReason === "external-block" && task.userPaused !== true && task.autoMerge !== false && task.externalBlock) {
196893
+ const report2 = buildTaskExternalBlockReport(task.externalBlock, task.externalBlock.report);
196894
+ return {
196895
+ reasonKey: `external-block:${task.externalBlock.origin}:${task.externalBlock.code}`,
196896
+ reason: report2.stopReason,
196897
+ action: report2.unblockCondition,
196898
+ externalBlockReport: report2
196899
+ };
196900
+ }
196592
196901
  const error2 = task.error ?? "";
196593
196902
  const hasPauseProof = task.paused === true || task.status === "paused";
196594
196903
  if (hasPauseProof && task.pausedReason === "completed-blocked") {
@@ -196713,7 +197022,56 @@ var init_task_wedge_notification = __esm({
196713
197022
  }
196714
197023
  });
196715
197024
 
197025
+ // ../engine/src/notification/mailbox-delivery.ts
197026
+ async function deliverMailboxMessageOnce(store, input, idempotencyKey, timeoutMs = MAILBOX_DELIVERY_TIMEOUT_MS) {
197027
+ if (!store?.sendMessageOnce) return "unavailable";
197028
+ let timer2;
197029
+ try {
197030
+ const delivery = Promise.resolve().then(() => store.sendMessageOnce(input, idempotencyKey));
197031
+ void delivery.catch(() => void 0);
197032
+ const timedOut = new Promise((resolve54) => {
197033
+ timer2 = setTimeout(() => resolve54("timeout"), Math.max(1, timeoutMs));
197034
+ timer2.unref?.();
197035
+ });
197036
+ const outcome = await Promise.race([delivery.then(() => "delivered", () => "unavailable"), timedOut]);
197037
+ return outcome === "delivered" ? "delivered" : "unavailable";
197038
+ } catch {
197039
+ return "unavailable";
197040
+ } finally {
197041
+ if (timer2) clearTimeout(timer2);
197042
+ }
197043
+ }
197044
+ var MAILBOX_DELIVERY_TIMEOUT_MS;
197045
+ var init_mailbox_delivery = __esm({
197046
+ "../engine/src/notification/mailbox-delivery.ts"() {
197047
+ "use strict";
197048
+ MAILBOX_DELIVERY_TIMEOUT_MS = 5e3;
197049
+ }
197050
+ });
197051
+
196716
197052
  // ../engine/src/notification/notification-service.ts
197053
+ function formatWedgeMailboxContent(task, descriptor, link) {
197054
+ if (descriptor.externalBlockReport) {
197055
+ const report2 = descriptor.externalBlockReport;
197056
+ return [
197057
+ `**${formatTaskIdentifier(task)} is blocked and needs operator action**`,
197058
+ "",
197059
+ `**Verified:** ${report2.verifiedCondition}`,
197060
+ `**Why work stopped:** ${report2.stopReason}`,
197061
+ `**Not implemented:** ${report2.unimplementedWork}`,
197062
+ `**Unblock:** ${report2.unblockCondition}`,
197063
+ ...link ? ["", `[Open ${task.id}](${link})`] : []
197064
+ ].join("\n");
197065
+ }
197066
+ return [
197067
+ `**${formatTaskIdentifier(task)} needs operator action**`,
197068
+ "",
197069
+ descriptor.reason,
197070
+ ...descriptor.gate ? [`Gate: \`${descriptor.gate}\`.`] : [],
197071
+ `Recommended action: ${descriptor.action}`,
197072
+ ...link ? ["", `[Open ${task.id}](${link})`] : []
197073
+ ].join("\n");
197074
+ }
196717
197075
  var NotificationService;
196718
197076
  var init_notification_service = __esm({
196719
197077
  "../engine/src/notification/notification-service.ts"() {
@@ -196724,6 +197082,7 @@ var init_notification_service = __esm({
196724
197082
  init_ntfy_provider();
196725
197083
  init_webhook_provider();
196726
197084
  init_task_wedge_notification();
197085
+ init_mailbox_delivery();
196727
197086
  NotificationService = class {
196728
197087
  constructor(store, options = {}) {
196729
197088
  this.store = store;
@@ -197171,14 +197530,7 @@ var init_notification_service = __esm({
197171
197530
  if (!episode) return "suppressed";
197172
197531
  }
197173
197532
  const link = buildNtfyClickUrl({ dashboardHost: this.dashboardHost, projectId: this.options.projectId, taskId: task.id });
197174
- const content = [
197175
- `**${formatTaskIdentifier(task)} needs operator action**`,
197176
- "",
197177
- descriptor.reason,
197178
- ...descriptor.gate ? [`Gate: \`${descriptor.gate}\`.`] : [],
197179
- `Recommended action: ${descriptor.action}`,
197180
- ...link ? ["", `[Open ${task.id}](${link})`] : []
197181
- ].join("\n");
197533
+ const content = formatWedgeMailboxContent(task, descriptor, link);
197182
197534
  const payload = {
197183
197535
  taskId: task.id,
197184
197536
  taskTitle: task.title,
@@ -197188,19 +197540,15 @@ var init_notification_service = __esm({
197188
197540
  metadata: { wedgeReason: descriptor.reasonKey, reason: descriptor.reason, action: descriptor.action, ...descriptor.gate ? { gate: descriptor.gate } : {}, notificationDedupeKey: `task-wedge:${episode}` }
197189
197541
  };
197190
197542
  void this.dispatch("task-wedged", payload);
197191
- try {
197192
- await this.options.messageStore?.sendMessageOnce?.({
197193
- fromId: "system",
197194
- fromType: "system",
197195
- toId: DASHBOARD_USER_ID,
197196
- toType: "user",
197197
- type: "system",
197198
- content,
197199
- metadata: { taskId: task.id, kind: "task-wedge", wedgeReason: descriptor.reasonKey, ...descriptor.gate ? { gate: descriptor.gate } : {} }
197200
- }, `task-wedge:${episode}`);
197201
- } catch (error2) {
197202
- schedulerLog.log(`[notify] ${task.id} wedge mailbox message failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
197203
- }
197543
+ await deliverMailboxMessageOnce(this.options.messageStore, {
197544
+ fromId: "system",
197545
+ fromType: "system",
197546
+ toId: DASHBOARD_USER_ID,
197547
+ toType: "user",
197548
+ type: "system",
197549
+ content,
197550
+ metadata: { taskId: task.id, kind: "task-wedge", wedgeReason: descriptor.reasonKey, ...descriptor.gate ? { gate: descriptor.gate } : {} }
197551
+ }, `task-wedge:${episode}`);
197204
197552
  if (isAutoRecoveryEscalationDispatch && classification.action === "notify") {
197205
197553
  try {
197206
197554
  await this.store.markTerminalFailureAutoRecoveryEscalationDelivered?.(task.id, {
@@ -197338,14 +197686,8 @@ var init_notification_service = __esm({
197338
197686
  }
197339
197687
  const payload = this.createTaskPayload(task, "task-wedged", { wedgeReason: descriptor.reasonKey, reason: descriptor.reason, action: descriptor.action, ...descriptor.gate ? { gate: descriptor.gate } : {}, notificationDedupeKey: `task-wedge:${claim.episodeId}` });
197340
197688
  void this.dispatch("task-wedged", payload);
197341
- const content = `**${formatTaskIdentifier(task)} needs operator action**
197342
-
197343
- ${descriptor.reason}
197344
- Recommended action: ${descriptor.action}`;
197345
- try {
197346
- await this.options.messageStore?.sendMessageOnce?.({ fromId: "system", fromType: "system", toId: DASHBOARD_USER_ID, toType: "user", type: "system", content, metadata: { taskId, kind: "task-wedge", wedgeReason: descriptor.reasonKey } }, `task-wedge:${claim.episodeId}`);
197347
- } catch {
197348
- }
197689
+ const content = formatWedgeMailboxContent(task, descriptor, buildNtfyClickUrl({ dashboardHost: this.dashboardHost, projectId: this.options.projectId, taskId }));
197690
+ await deliverMailboxMessageOnce(this.options.messageStore, { fromId: "system", fromType: "system", toId: DASHBOARD_USER_ID, toType: "user", type: "system", content, metadata: { taskId, kind: "task-wedge", wedgeReason: descriptor.reasonKey } }, `task-wedge:${claim.episodeId}`);
197349
197691
  return { outcome: "delivered", reasonKey: descriptor.reasonKey };
197350
197692
  } catch (error2) {
197351
197693
  schedulerLog.debug(`[notify] ${taskId} pending wedge completion failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
@@ -211450,7 +211792,7 @@ var agent_reflection_exports = {};
211450
211792
  __export(agent_reflection_exports, {
211451
211793
  AgentReflectionService: () => AgentReflectionService
211452
211794
  });
211453
- import { readFile as readFile53 } from "node:fs/promises";
211795
+ import { readFile as readFile54 } from "node:fs/promises";
211454
211796
  import { isAbsolute as isAbsolute26, resolve as resolve52 } from "node:path";
211455
211797
  var reflectionLog, REFLECTION_SYSTEM_PROMPT, DEFAULT_OUTCOME_LIMIT, AgentReflectionService;
211456
211798
  var init_agent_reflection = __esm({
@@ -212038,7 +212380,7 @@ Rules:
212038
212380
  if (agent.instructionsPath?.trim()) {
212039
212381
  const resolvedPath = isAbsolute26(agent.instructionsPath) ? agent.instructionsPath : resolve52(this.rootDir, agent.instructionsPath);
212040
212382
  try {
212041
- const content = await readFile53(resolvedPath, "utf-8");
212383
+ const content = await readFile54(resolvedPath, "utf-8");
212042
212384
  if (content.trim()) {
212043
212385
  pieces.push(content.trim());
212044
212386
  }
@@ -213025,16 +213367,16 @@ async function persistBranchState(persistence, state) {
213025
213367
  );
213026
213368
  }
213027
213369
  }
213028
- function resolveJoinConfig(join103) {
213029
- const rawMode = join103.config?.mode;
213370
+ function resolveJoinConfig(join104) {
213371
+ const rawMode = join104.config?.mode;
213030
213372
  let mode = "all";
213031
213373
  if (rawMode === "all" || rawMode === "any") mode = rawMode;
213032
213374
  else if (rawMode && typeof rawMode === "object" && "quorum" in rawMode) {
213033
213375
  const n = rawMode.quorum;
213034
213376
  if (typeof n === "number" && Number.isInteger(n) && n > 0) mode = { quorum: n };
213035
- else throw new WorkflowIrError(`join '${join103.id}' quorum must be a positive integer`);
213377
+ else throw new WorkflowIrError(`join '${join104.id}' quorum must be a positive integer`);
213036
213378
  }
213037
- const rawFail = join103.config?.onBranchFailure;
213379
+ const rawFail = join104.config?.onBranchFailure;
213038
213380
  const onBranchFailure = rawFail === "collect" ? "collect" : "fail-fast";
213039
213381
  return { mode, onBranchFailure };
213040
213382
  }
@@ -213050,9 +213392,9 @@ async function runSplitJoin(split, env) {
213050
213392
  if (branchEdges.length === 0) {
213051
213393
  throw new WorkflowIrError(`split '${split.id}' has no traversable branches`);
213052
213394
  }
213053
- const join103 = findMatchingJoin(branchEdges[0].to, env);
213054
- if (!join103) throw new WorkflowIrError(`split '${split.id}' has no reachable matching join`);
213055
- const joinConfig = resolveJoinConfig(env.nodeMap.get(join103));
213395
+ const join104 = findMatchingJoin(branchEdges[0].to, env);
213396
+ if (!join104) throw new WorkflowIrError(`split '${split.id}' has no reachable matching join`);
213397
+ const joinConfig = resolveJoinConfig(env.nodeMap.get(join104));
213056
213398
  const controller = new AbortController();
213057
213399
  const branchCount = branchEdges.length;
213058
213400
  const required2 = requiredCompletions(joinConfig.mode, branchCount);
@@ -213088,7 +213430,7 @@ async function runSplitJoin(split, env) {
213088
213430
  };
213089
213431
  const branchPromises = branchEdges.map((edge) => {
213090
213432
  const branchId = edge.to;
213091
- return walkBranch(branchId, join103, env, controller.signal, visitedNodeIds).then((result) => {
213433
+ return walkBranch(branchId, join104, env, controller.signal, visitedNodeIds).then((result) => {
213092
213434
  branchOutcomes.push({ branchId, outcome: result.outcome, nodeId: result.lastNodeId });
213093
213435
  if (result.outcome === "success") succeeded += 1;
213094
213436
  else failed += 1;
@@ -213106,7 +213448,7 @@ async function runSplitJoin(split, env) {
213106
213448
  });
213107
213449
  const outcome = await joinReached;
213108
213450
  await Promise.allSettled(branchPromises);
213109
- return { joinNodeId: join103, outcome, branchOutcomes, visitedNodeIds };
213451
+ return { joinNodeId: join104, outcome, branchOutcomes, visitedNodeIds };
213110
213452
  }
213111
213453
  async function walkBranch(startNodeId, joinId, env, signal, visitedNodeIds) {
213112
213454
  let currentId = startNodeId;
@@ -222074,8 +222416,11 @@ async function rerouteSingularStaleContentToReview(store, task, options) {
222074
222416
 
222075
222417
  // ../engine/src/merge/pre-merge-gate-reseed.ts
222076
222418
  init_src2();
222419
+ function isRecoverableUnrunGatePark(task) {
222420
+ return task.status === "failed" && !task.userPaused && !task.deletedAt && task.autoMerge !== false && (!task.paused || task.pausedReason === IN_REVIEW_STALL_DEADLOCK_PAUSE_REASON) && typeof task.error === "string" && (task.error.endsWith(PRE_MERGE_STEPS_NOT_RUN_BLOCKER) || !task.workflowIrPin && task.error.startsWith("Workflow drift park:") && task.error.includes("Stale IR pin cleared"));
222421
+ }
222077
222422
  async function rerouteUnrunPreMergeGateToReview(store, task, options) {
222078
- const { mergeContent, requiredPreMergeStepIds } = options;
222423
+ const { mergeContent, requiredPreMergeStepIds, expectedWorkflowSelection } = options;
222079
222424
  if (mergeContent.kind !== "singular" || task.workspaceWorktrees !== void 0) return { rerouted: false, reason: "not-singular" };
222080
222425
  if (task.paused || task.userPaused || task.deletedAt || task.autoMerge === false) return { rerouted: false, reason: "operator-held" };
222081
222426
  if (requiredPreMergeStepIds.size === 0) return { rerouted: false, reason: "no-unrun-gate" };
@@ -222095,9 +222440,16 @@ async function rerouteUnrunPreMergeGateToReview(store, task, options) {
222095
222440
  continuationSequence: items.length,
222096
222441
  sourceColumn: task.column,
222097
222442
  targetColumn: node.column ?? task.column,
222098
- irHash: computeWorkflowIrPin(ir2, node.id).irHash
222443
+ irHash: computeWorkflowIrPin(ir2, node.id).irHash,
222444
+ expectedWorkflowSelection
222099
222445
  });
222100
- return result.seeded ? { rerouted: true, reason: "seeded", nodeId: node.id, workflowStepId: node.id } : { rerouted: false, reason: "active-continuation", nodeId: node.id, workflowStepId: node.id };
222446
+ if (result.seeded) return { rerouted: true, reason: "seeded", nodeId: node.id, workflowStepId: node.id };
222447
+ return {
222448
+ rerouted: false,
222449
+ reason: result.reason === "workflow-selection-changed" ? "workflow-selection-changed" : "active-continuation",
222450
+ nodeId: node.id,
222451
+ workflowStepId: node.id
222452
+ };
222101
222453
  }
222102
222454
 
222103
222455
  // ../engine/src/healing/auto-recovery.ts
@@ -222571,6 +222923,469 @@ async function terminalizeEmptyReviewContent(deps, taskId, fence) {
222571
222923
  return true;
222572
222924
  }
222573
222925
 
222926
+ // ../engine/src/executor/evaluate-workflow-merge-boundary.ts
222927
+ init_src2();
222928
+ async function evaluateWorkflowMergeBoundary(deps, task, runId) {
222929
+ const relevant = (task.workflowStepResults ?? []).filter(
222930
+ (result) => result.source === "node" && (result.phase ?? "pre-merge") === "pre-merge"
222931
+ );
222932
+ const hasRelevantNodeResult = relevant.length > 0;
222933
+ const nonTerminalResult = relevant.find((result) => result.status !== "passed" && result.status !== "skipped");
222934
+ const allResultsTerminal = nonTerminalResult === void 0;
222935
+ let ir2;
222936
+ try {
222937
+ ir2 = await resolveWorkflowIrForTask(deps.store, task.id);
222938
+ } catch {
222939
+ }
222940
+ if (!ir2) return { resolved: false, hasRelevantNodeResult, allResultsTerminal, coverageComplete: true, hasLiveStepImplementationProof: false, hasForeachStepExecute: false, missingInstanceIds: [], nonTerminalResult, complete: false };
222941
+ let persistedInstances = [];
222942
+ try {
222943
+ persistedInstances = await deps.loadMergeBoundaryInstances(task.id, runId);
222944
+ } catch {
222945
+ }
222946
+ const coverage = evaluateForeachMergeProof({ ir: ir2, steps: task.steps, workflowStepResults: task.workflowStepResults, persistedInstances });
222947
+ const coverageComplete = coverage.missingInstanceIds.length === 0;
222948
+ const hasLiveStepImplementationProof = coverage.expectedInstanceIds.length > 0 && coverage.expectedInstanceIds.every((id) => coverage.liveStepSatisfiedInstanceIds.includes(id));
222949
+ const complete = allResultsTerminal && coverageComplete && (hasRelevantNodeResult || hasLiveStepImplementationProof);
222950
+ return { resolved: true, hasRelevantNodeResult, allResultsTerminal, coverageComplete, hasLiveStepImplementationProof, hasForeachStepExecute: coverage.hasForeachStepExecute, missingInstanceIds: coverage.missingInstanceIds, nonTerminalResult, complete };
222951
+ }
222952
+ async function getWorkflowMergeImplementationProofFailure(deps, task) {
222953
+ const taint = evaluateSkipBypassTaint(task);
222954
+ if (taint.blocked) return "implementation did not run: steps were skipped after a bulk-step-completion refusal without an accepted fn_task_done";
222955
+ if (task.noCommitsExpected === true) return void 0;
222956
+ let ir2;
222957
+ try {
222958
+ ir2 = await resolveWorkflowIrForTask(deps.store, task.id);
222959
+ } catch {
222960
+ ir2 = void 0;
222961
+ }
222962
+ if (!ir2) return void 0;
222963
+ const usesParsedSteps = ir2.nodes.some((node) => node.kind === "parse-steps");
222964
+ const usesExecuteSeam = ir2.nodes.some((node) => node.kind === "prompt" && node.config?.seam === "execute");
222965
+ if (!usesParsedSteps && !usesExecuteSeam) return void 0;
222966
+ const steps = Array.isArray(task.steps) ? task.steps : [];
222967
+ const hasTerminalParsedSteps = steps.length > 0 && steps.every((step) => step.status === "done" || step.status === "skipped");
222968
+ const hasModifiedFiles = (task.modifiedFiles?.length ?? 0) > 0;
222969
+ const proof = await deps.evaluateWorkflowMergeBoundary(task);
222970
+ const hasGraphNativeImplementationProof = proof.hasRelevantNodeResult && proof.allResultsTerminal && proof.coverageComplete;
222971
+ if (usesParsedSteps) {
222972
+ if (hasTerminalParsedSteps || hasGraphNativeImplementationProof) return void 0;
222973
+ return proof.hasForeachStepExecute && !proof.coverageComplete ? `implementation did not run: foreach step instances are incomplete (missing ${proof.missingInstanceIds.join(", ")})` : "implementation did not run: parsed coding steps are missing or incomplete";
222974
+ }
222975
+ if (usesExecuteSeam) return hasTerminalParsedSteps || hasModifiedFiles || hasGraphNativeImplementationProof ? void 0 : "implementation did not run: execute seam has no completion proof";
222976
+ return void 0;
222977
+ }
222978
+
222979
+ // ../engine/src/executor/workflow-merge-boundary-helpers.ts
222980
+ init_src2();
222981
+ async function resolveMergeBoundaryColumn(deps, taskId, nodeId) {
222982
+ try {
222983
+ const ir2 = await resolveWorkflowIrForTask(deps.store, taskId);
222984
+ const named = ir2.nodes.find((n) => n.id === nodeId);
222985
+ if (named && MERGE_REGION_KINDS.has(named.kind) && named.column) return named.column;
222986
+ const mergeNode = ir2.nodes.find((n) => MERGE_REGION_KINDS.has(n.kind) && n.column);
222987
+ if (mergeNode?.column) return mergeNode.column;
222988
+ return "in-review";
222989
+ } catch {
222990
+ return "in-review";
222991
+ }
222992
+ }
222993
+ async function loadMergeBoundaryInstances(deps, taskId, runId) {
222994
+ if (!runId) return [];
222995
+ const store = deps.store;
222996
+ try {
222997
+ return await store.loadWorkflowRunStepInstancesAsync?.(taskId, runId) ?? store.loadWorkflowRunStepInstances?.(taskId, runId) ?? [];
222998
+ } catch {
222999
+ return [];
223000
+ }
223001
+ }
223002
+ function shouldCompleteChecklistAtWorkflowMerge(task, proof) {
223003
+ if (!Array.isArray(task.steps) || task.steps.length === 0) return false;
223004
+ if (task.steps.every((step) => step.status === "done" || step.status === "skipped")) return false;
223005
+ if (proof) return proof.complete;
223006
+ const graphNodeResults = (task.workflowStepResults ?? []).filter((result) => result.source === "node" && (result.phase ?? "pre-merge") === "pre-merge");
223007
+ return graphNodeResults.length > 0 && graphNodeResults.every((result) => result.status === "passed" || result.status === "skipped");
223008
+ }
223009
+
223010
+ // ../engine/src/executor/route-graph-failure-to-execution-resume.ts
223011
+ init_src2();
223012
+
223013
+ // ../engine/src/execution-block-classifier.ts
223014
+ init_usage_limit_detector();
223015
+ init_transient_error_detector();
223016
+ var HOST_EXTERNAL_CODES = ["ENOSPC", "EDQUOT", "EROFS", "ENOMEM", "EMFILE", "ENFILE"];
223017
+ var NETWORK_EXTERNAL_CODES = ["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ENOTFOUND"];
223018
+ function classifyExternalObstacle(message) {
223019
+ const normalized3 = message.trim();
223020
+ if (!normalized3) return void 0;
223021
+ if (/Forbidden lifecycle path|rejected by the task lifecycle projection|TransitionRejectionError/i.test(normalized3)) {
223022
+ return void 0;
223023
+ }
223024
+ for (const code of HOST_EXTERNAL_CODES) {
223025
+ if (new RegExp(`\\b${code}\\b`, "i").test(normalized3)) return { origin: "host-environment", code };
223026
+ }
223027
+ for (const code of NETWORK_EXTERNAL_CODES) {
223028
+ if (new RegExp(`\\b${code}\\b`, "i").test(normalized3)) return { origin: "network", code };
223029
+ }
223030
+ if (/socket hang up/i.test(normalized3)) return { origin: "network", code: "SOCKET_HANG_UP" };
223031
+ if (isTransientAuthCredentialError(normalized3)) return void 0;
223032
+ if (isUsageLimitError(normalized3)) return { origin: "model-provider", code: "USAGE_LIMIT" };
223033
+ if (isOperatorActionableAgentError(normalized3)) return { origin: "credentials", code: "CREDENTIALS" };
223034
+ return void 0;
223035
+ }
223036
+ function detectRepairableObstacleHint(message) {
223037
+ const normalized3 = message.trim();
223038
+ if (!normalized3) return void 0;
223039
+ const missingSignal = /\b(?:command not found|not installed|is not recognized as an internal or external command)\b|\bneither\b.{0,80}\binstalled\b/i;
223040
+ const missingFileSignal = /(?:\b(?:spawn|exec(?:ute)?|binary|executable|interpreter|command)\b.{0,80}\b(?:ENOENT|no such file or directory)\b|\bENOENT\b.{0,80}\b(?:binary|executable|interpreter|command)\b)/i;
223041
+ if (!missingSignal.test(normalized3) && !missingFileSignal.test(normalized3)) return void 0;
223042
+ return /\b(?:service|daemon)\b/i.test(normalized3) ? "missing-service" : "missing-tooling";
223043
+ }
223044
+ var TASK_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/i;
223045
+ var LEGACY_PR_REF_RE = /^(?:pr[:#-]|#|PR-)(\d+)$/i;
223046
+ function partitionBlockedByRefs(blockedBy) {
223047
+ const taskIds = [];
223048
+ for (const raw of blockedBy) {
223049
+ const id = raw.trim();
223050
+ if (!id) continue;
223051
+ if (LEGACY_PR_REF_RE.test(id)) continue;
223052
+ if (TASK_ID_RE.test(id)) {
223053
+ const m2 = id.toUpperCase().match(/^([A-Z][A-Z0-9]*)-(\d+)$/);
223054
+ taskIds.push(m2 ? `${m2[1]}-${m2[2]}` : id.toUpperCase());
223055
+ }
223056
+ }
223057
+ return { taskIds: [...new Set(taskIds)] };
223058
+ }
223059
+ function classifyBlockedExit(_reason, blockedBy = []) {
223060
+ const { taskIds } = partitionBlockedByRefs(blockedBy);
223061
+ if (taskIds.length > 0) {
223062
+ return {
223063
+ class: "external",
223064
+ thrashSignature: `tasks:${taskIds.slice().sort().join(",")}`
223065
+ };
223066
+ }
223067
+ return {
223068
+ class: "plan-defect",
223069
+ thrashSignature: "plan-defect"
223070
+ };
223071
+ }
223072
+ var BLOCKED_THRASH_LIMIT = 3;
223073
+ var BLOCKED_THRASH_WINDOW_MS = 60 * 60 * 1e3;
223074
+ function countBlockedThrashHits(log42, signature, nowMs = Date.now(), windowMs = BLOCKED_THRASH_WINDOW_MS) {
223075
+ if (!log42?.length) return 0;
223076
+ if (signature === "plan-defect") return 0;
223077
+ const cutoff = nowMs - windowMs;
223078
+ let count2 = 0;
223079
+ for (const entry of log42) {
223080
+ const ts4 = entry.timestamp ? Date.parse(entry.timestamp) : Number.NaN;
223081
+ if (Number.isFinite(ts4) && ts4 < cutoff) continue;
223082
+ const action = entry.action ?? "";
223083
+ if (!action.startsWith("BLOCKED:") && !action.includes("DUPLICATE_REPLAN_EXHAUSTED") && !action.includes("BLOCKED_THRASH")) {
223084
+ if (!action.includes("parked failed (honest blocked exit") && !action.includes("durable external block")) {
223085
+ continue;
223086
+ }
223087
+ }
223088
+ for (const part of signature.split("|")) {
223089
+ if (part.startsWith("tasks:") && part.slice(6).split(",").some((id) => id && action.includes(id))) {
223090
+ count2 += 1;
223091
+ break;
223092
+ }
223093
+ }
223094
+ }
223095
+ return count2;
223096
+ }
223097
+ function isDurableBlockedTask(task) {
223098
+ const meta2 = task.sourceMetadata;
223099
+ if (!meta2 || typeof meta2 !== "object") return false;
223100
+ if (meta2.blockedClass !== "external") return false;
223101
+ return task.status === "failed" || task.status === "needs-replan";
223102
+ }
223103
+ function buildExternalBlockMetadataPatch(classification, thrashCount) {
223104
+ return {
223105
+ blockedClass: classification.class,
223106
+ blockedThrashSignature: classification.thrashSignature,
223107
+ blockedThrashCount: thrashCount,
223108
+ externalBlockers: []
223109
+ };
223110
+ }
223111
+
223112
+ // ../engine/src/executor/route-graph-failure-to-execution-resume.ts
223113
+ init_logger();
223114
+ init_lifecycle_columns();
223115
+
223116
+ // ../engine/src/executor/workflow-step-satisfaction.ts
223117
+ function hasNonTerminalWorkflowSteps(task) {
223118
+ return task.steps.length > 0 && task.steps.some((step) => step.status !== "done" && step.status !== "skipped");
223119
+ }
223120
+ function workflowStepResultPassed(task, workflowStepId) {
223121
+ const results = task?.workflowStepResults ?? [];
223122
+ return results.some(
223123
+ (result) => result.workflowStepId === workflowStepId && result.phase === "pre-merge" && result.status === "passed"
223124
+ );
223125
+ }
223126
+ function areEnabledPreMergeWorkflowStepsSatisfied(task) {
223127
+ const preMergeGateIds = /* @__PURE__ */ new Set(["plan-review", "browser-verification", "code-review"]);
223128
+ const enabled = task?.enabledWorkflowSteps;
223129
+ const enabledPreMerge = Array.isArray(enabled) && enabled.length > 0 ? enabled.filter((id) => preMergeGateIds.has(id)) : ["plan-review", "code-review"];
223130
+ if (enabledPreMerge.length === 0) return false;
223131
+ if (Array.isArray(enabled) && enabledPreMerge.length !== enabled.length) return false;
223132
+ return enabledPreMerge.every((id) => workflowStepResultPassed(task, id));
223133
+ }
223134
+ function preservePreExecutionWorkflowStepResults(task) {
223135
+ const preserved = (task.workflowStepResults ?? []).filter((result) => result.workflowStepId === "plan-review");
223136
+ if (preserved.length > 0) return preserved;
223137
+ let latest;
223138
+ for (const entry of task.log ?? []) {
223139
+ if (entry.action === "[pre-merge] Workflow step completed: Plan Review") {
223140
+ latest = { timestamp: entry.timestamp, outcome: entry.outcome, status: "passed" };
223141
+ } else if (entry.action === "[pre-merge] Workflow step failed: Plan Review") {
223142
+ latest = { timestamp: entry.timestamp, outcome: entry.outcome, status: "failed" };
223143
+ }
223144
+ }
223145
+ if (latest?.status !== "passed") return [];
223146
+ return [
223147
+ {
223148
+ workflowStepId: "plan-review",
223149
+ workflowStepName: "Plan Review",
223150
+ phase: "pre-merge",
223151
+ status: "passed",
223152
+ verdict: "APPROVE",
223153
+ ...latest.outcome ? { output: latest.outcome, notes: latest.outcome } : {},
223154
+ ...latest.timestamp ? { startedAt: latest.timestamp, completedAt: latest.timestamp } : {}
223155
+ }
223156
+ ];
223157
+ }
223158
+
223159
+ // ../engine/src/executor/route-graph-failure-to-execution-resume.ts
223160
+ init_workflow_merge_nodes();
223161
+ function isExecutableNode(node) {
223162
+ const seam = node.config?.seam;
223163
+ return node.kind === "foreach" || seam === "execute" || seam === "step-execute";
223164
+ }
223165
+ function isTerminalWorkItem(item) {
223166
+ return item.state === "succeeded" || item.state === "failed" || item.state === "cancelled" || item.state === "exhausted" || item.state === "manual-required";
223167
+ }
223168
+ function findBoundaryRecoveryOwners(task, nodes2, evidence, workItems) {
223169
+ if (!evidence || !["no-node-result", "non-terminal-node-result", "missing-foreach-instances"].includes(evidence.code)) return void 0;
223170
+ if (evidence.code === "non-terminal-node-result") {
223171
+ const id = evidence.nonTerminalNodeId;
223172
+ const result = task.workflowStepResults?.find((entry) => entry.workflowStepId === id && entry.source === "node");
223173
+ return id && result && result.status !== "passed" && result.status !== "failed" && result.status !== "skipped" && nodes2.some((node) => node.id === id && isExecutableNode(node)) ? [id] : void 0;
223174
+ }
223175
+ if (evidence.code === "missing-foreach-instances") {
223176
+ const ids = evidence.missingInstanceIds;
223177
+ if (ids.length === 0) return void 0;
223178
+ const foreachOwners = nodes2.filter((node) => node.kind === "foreach");
223179
+ const owners2 = ids.map(
223180
+ (id) => (
223181
+ // Prefer the longest prefix so nested/custom node names cannot misclassify an instance.
223182
+ foreachOwners.filter((node) => id.startsWith(`${node.id}#`)).sort((left, right) => right.id.length - left.id.length)[0]?.id
223183
+ )
223184
+ );
223185
+ if (owners2.some((owner) => owner === void 0)) return void 0;
223186
+ return foreachOwners.filter((node) => owners2.includes(node.id)).map((node) => node.id);
223187
+ }
223188
+ if (!hasNonTerminalWorkflowSteps(task)) return void 0;
223189
+ const terminalNodes = new Set(workItems.filter(isTerminalWorkItem).map((item) => item.nodeId));
223190
+ const candidates = nodes2.filter((node) => isExecutableNode(node) && !terminalNodes.has(node.id));
223191
+ return candidates.length === 1 && candidates[0] ? [candidates[0].id] : void 0;
223192
+ }
223193
+ var BOUNDARY_RECOVERY_CLAIM_STATUS = "merge-boundary-evidence-recovery";
223194
+ var BOUNDARY_RECOVERY_HELD_STATUS = "merge-boundary-evidence-recovery-held";
223195
+ var BOUNDARY_RECOVERY_CONCURRENT = /* @__PURE__ */ Symbol("merge-boundary-recovery-concurrent");
223196
+ var MAX_BOUNDARY_RECOVERY_ATTEMPTS = 4;
223197
+ var BOUNDARY_RECOVERY_BACKOFF_MS = [3e4, 6e4, 12e4, 3e5];
223198
+ function boundaryRecoverySignature(task, failedNode, evidence) {
223199
+ return JSON.stringify({
223200
+ failedNode,
223201
+ evidence,
223202
+ steps: task.steps.map((step, index2) => [index2, step.name, step.status]),
223203
+ nodeResults: (task.workflowStepResults ?? []).filter((result) => result.source === "node").map((result) => [result.workflowStepId, result.status]).sort(([left], [right]) => String(left).localeCompare(String(right)))
223204
+ });
223205
+ }
223206
+ async function recoverMergeBoundaryEvidenceGap(deps, live, failedNode, evidence, wipColumn, autoMergeSettings, liveSharedBranchMember = false) {
223207
+ let claimed = false;
223208
+ let deferred = false;
223209
+ let held = false;
223210
+ const now = Date.now();
223211
+ const signature = boundaryRecoverySignature(live, failedNode, evidence);
223212
+ await deps.store.updateTaskAtomic(live.id, (current) => {
223213
+ if (!current || current.deletedAt || current.paused || current.userPaused || autoMergeSettings && !liveSharedBranchMember && (current.autoMerge === false || !allowsAutoMergeProcessing(current, autoMergeSettings)) || current.column !== live.column || current.status !== live.status || current.error !== live.error || live.columnMovedAt !== void 0 && current.columnMovedAt !== live.columnMovedAt) return null;
223214
+ const prior = current.mergeDetails?.mergeBoundaryRecovery;
223215
+ if (prior?.signature === signature) {
223216
+ const nextCheckAt2 = Date.parse(prior.nextCheckAt ?? "");
223217
+ if (Number.isFinite(nextCheckAt2) && nextCheckAt2 > now) {
223218
+ deferred = true;
223219
+ return null;
223220
+ }
223221
+ if (prior.attempt >= MAX_BOUNDARY_RECOVERY_ATTEMPTS) {
223222
+ held = true;
223223
+ return {
223224
+ status: BOUNDARY_RECOVERY_HELD_STATUS,
223225
+ error: "Workflow merge evidence recovery is held after unchanged proof gaps; waiting for durable implementation progress",
223226
+ mergeDetails: {
223227
+ ...current.mergeDetails,
223228
+ mergeBoundaryRecovery: { ...prior, nextCheckAt: null, heldAt: new Date(now).toISOString() }
223229
+ }
223230
+ };
223231
+ }
223232
+ }
223233
+ const attempt = prior?.signature === signature ? prior.attempt + 1 : 1;
223234
+ const nextCheckAt = new Date(now + BOUNDARY_RECOVERY_BACKOFF_MS[attempt - 1]).toISOString();
223235
+ claimed = true;
223236
+ return {
223237
+ status: BOUNDARY_RECOVERY_CLAIM_STATUS,
223238
+ mergeDetails: {
223239
+ ...current.mergeDetails,
223240
+ mergeBoundaryRecovery: { signature, attempt, nextCheckAt }
223241
+ }
223242
+ };
223243
+ }, deps.getRunContextFor(live.id));
223244
+ if (deferred) {
223245
+ await deps.store.logEntry(live.id, "Workflow merge evidence recovery is waiting for its bounded recheck window before retrying unchanged proof", void 0, deps.getRunContextFor(live.id));
223246
+ return "deferred";
223247
+ }
223248
+ if (held) {
223249
+ await deps.store.logEntry(live.id, "Workflow merge evidence recovery is held because the same proof gap exhausted its bounded retry budget", void 0, deps.getRunContextFor(live.id));
223250
+ return "held";
223251
+ }
223252
+ if (!claimed) return "declined";
223253
+ const claimedTask = typeof deps.store.getTask === "function" ? await deps.store.getTask(live.id) : live;
223254
+ if (!claimedTask || claimedTask.column !== live.column || claimedTask !== live && claimedTask.status !== BOUNDARY_RECOVERY_CLAIM_STATUS || autoMergeSettings && !liveSharedBranchMember && (claimedTask.autoMerge === false || !allowsAutoMergeProcessing(claimedTask, autoMergeSettings))) {
223255
+ await releaseBoundaryRecoveryClaim(deps.store, live, deps.getRunContextFor(live.id));
223256
+ return "declined";
223257
+ }
223258
+ let owner;
223259
+ try {
223260
+ owner = await seedBoundaryRecoveryContinuation(deps.store, live, failedNode, evidence);
223261
+ } catch (error2) {
223262
+ await releaseBoundaryRecoveryClaim(deps.store, live, deps.getRunContextFor(live.id));
223263
+ throw error2;
223264
+ }
223265
+ if (!owner) {
223266
+ await releaseBoundaryRecoveryClaim(deps.store, live, deps.getRunContextFor(live.id));
223267
+ return "declined";
223268
+ }
223269
+ if (owner === BOUNDARY_RECOVERY_CONCURRENT) {
223270
+ await releaseBoundaryRecoveryClaim(deps.store, live, deps.getRunContextFor(live.id));
223271
+ return "concurrent";
223272
+ }
223273
+ const remediation = `Workflow merge evidence recovery: '${owner}' is being resumed after missing proof at '${failedNode}'; returning to '${wipColumn}' before merge proof is rechecked`;
223274
+ executorLog.warn(`${live.id}: ${remediation}`);
223275
+ await deps.store.logEntry(live.id, remediation, void 0, deps.getRunContextFor(live.id));
223276
+ const moveOptions = {
223277
+ moveSource: "engine",
223278
+ preserveProgress: true,
223279
+ preserveWorktree: true,
223280
+ workflowMoveSource: "workflow-graph",
223281
+ workflowMoveMetadata: { reason: "merge-boundary-evidence-recovery", nodeId: failedNode }
223282
+ };
223283
+ const conditionalMoveStore = deps.store;
223284
+ const moved = typeof conditionalMoveStore.moveTaskIf === "function" ? (await conditionalMoveStore.moveTaskIf(
223285
+ live.id,
223286
+ wipColumn,
223287
+ (current) => !current.deletedAt && !current.paused && !current.userPaused && (!autoMergeSettings || liveSharedBranchMember || current.autoMerge !== false && allowsAutoMergeProcessing(current, autoMergeSettings)) && current.column === live.column && current.status === BOUNDARY_RECOVERY_CLAIM_STATUS,
223288
+ moveOptions
223289
+ )).moved : (await deps.store.moveTask(live.id, wipColumn, moveOptions), true);
223290
+ if (!moved) {
223291
+ await releaseBoundaryRecoveryClaim(deps.store, live, deps.getRunContextFor(live.id));
223292
+ return "raced";
223293
+ }
223294
+ let completed = false;
223295
+ await deps.store.updateTaskAtomic(live.id, (current) => {
223296
+ if (!current || current.deletedAt || current.paused || current.userPaused || current.column !== wipColumn || current.status !== BOUNDARY_RECOVERY_CLAIM_STATUS) return null;
223297
+ completed = true;
223298
+ return { status: null, error: null };
223299
+ }, deps.getRunContextFor(live.id));
223300
+ return completed ? "recovered" : "declined";
223301
+ }
223302
+ async function releaseBoundaryRecoveryClaim(store, live, runContext) {
223303
+ await store.updateTaskAtomic(live.id, (current) => {
223304
+ if (!current || current.deletedAt || current.paused || current.userPaused || current.column !== live.column || current.status !== BOUNDARY_RECOVERY_CLAIM_STATUS) return null;
223305
+ return { status: live.status };
223306
+ }, runContext);
223307
+ }
223308
+ async function seedBoundaryRecoveryContinuation(store, task, failedNode, evidence) {
223309
+ const ir2 = await resolveWorkflowIrForTask(store, task.id).catch(() => void 0);
223310
+ const workItems = typeof store.listWorkflowWorkItemsForTask === "function" ? await store.listWorkflowWorkItemsForTask(task.id, { kinds: ["task"] }).catch(() => []) : [];
223311
+ const owners2 = ir2 ? findBoundaryRecoveryOwners(task, ir2.nodes, evidence, workItems) : void 0;
223312
+ const owner = owners2?.[0];
223313
+ if (!owner || typeof store.replaceActiveTaskWorkflowContinuation !== "function") return void 0;
223314
+ const seeded = await Promise.resolve(store.replaceActiveTaskWorkflowContinuation({
223315
+ taskId: task.id,
223316
+ runId: `${task.id}:merge-boundary-recovery:${owner}`,
223317
+ nodeId: owner,
223318
+ kind: "task",
223319
+ state: "runnable",
223320
+ blockedReason: `merge-boundary-evidence-recovery:${failedNode}`,
223321
+ lastError: null,
223322
+ leaseOwner: null,
223323
+ leaseExpiresAt: null,
223324
+ onlyIfNoActiveTaskContinuation: true
223325
+ })).catch((error2) => {
223326
+ if (error2 instanceof Error && error2.name === "ActiveTaskContinuationError") return null;
223327
+ throw error2;
223328
+ });
223329
+ return seeded === null ? BOUNDARY_RECOVERY_CONCURRENT : owner;
223330
+ }
223331
+ async function routeGraphFailureToExecutionResume(deps, live, failedNode, failureValue, resumeLanesMemo, boundaryEvidence) {
223332
+ if (live.deletedAt) return false;
223333
+ if (isTaskExternallyBlocked(live)) {
223334
+ executorLog.log(`${live.id}: graph failure resume skipped \u2014 external Blocked freeze honored`);
223335
+ return false;
223336
+ }
223337
+ if (live.paused || live.userPaused === true) return false;
223338
+ if ((await resolveTerminalColumnsFor(deps.store, live.id)).includes(live.column)) return false;
223339
+ if (isDurableBlockedTask(live)) {
223340
+ executorLog.log(
223341
+ `${live.id}: graph failure resume skipped \u2014 durable BLOCKED park honored (task-dependency block)`
223342
+ );
223343
+ return false;
223344
+ }
223345
+ if (failedNode === COMPLETION_SUMMARY_NODE_ID) return false;
223346
+ const incompleteSteps = hasNonTerminalWorkflowSteps(live);
223347
+ if (!incompleteSteps && (failureValue === "remediation-not-scheduled" || failureValue === "missing-remediation-context") && await deps.isRemediationGraphNode(live.id, failedNode)) return false;
223348
+ const implementationIncompleteMergeFailure = isMergeGraphFailure(failedNode) && failureValue === "implementation-incomplete";
223349
+ const boundaryEvidenceRecovery = isMergeGraphFailure(failedNode) && failureValue === MERGE_BOUNDARY_RECOVERY_VALUE;
223350
+ if (implementationIncompleteMergeFailure && !incompleteSteps) return false;
223351
+ const resumeRouterLanes = await deps.resolveResumeLanes(live.id, resumeLanesMemo);
223352
+ if (!resumeRouterLanes.wipDeclared) return false;
223353
+ const mayResumeInPlace = live.column === resumeRouterLanes.wip && implementationIncompleteMergeFailure;
223354
+ const mayRepairBoundary = boundaryEvidenceRecovery && live.column === resumeRouterLanes.review;
223355
+ if (!mayResumeInPlace && !mayRepairBoundary) {
223356
+ const message2 = `Workflow graph failed at node '${failedNode}'${failureValue ? ` (${failureValue})` : ""} \u2014 automatic recovery cannot move '${live.column}' backward; card remains in place`;
223357
+ executorLog.warn(`${live.id}: ${message2}`);
223358
+ await deps.store.logEntry(live.id, message2, void 0, deps.getRunContextFor(live.id));
223359
+ return false;
223360
+ }
223361
+ if (mayRepairBoundary) {
223362
+ const settings = await deps.store.getSettings?.().catch(() => void 0) ?? { autoMerge: true };
223363
+ if (settings.globalPause === true || settings.enginePaused === true) return false;
223364
+ const liveSharedBranchMember = await deps.isLiveSharedBranchGroupMember?.(live) ?? false;
223365
+ if ((live.autoMerge === false || !allowsAutoMergeProcessing(live, settings)) && !liveSharedBranchMember) return false;
223366
+ const outcome = await recoverMergeBoundaryEvidenceGap(
223367
+ deps,
223368
+ live,
223369
+ failedNode,
223370
+ boundaryEvidence,
223371
+ resumeRouterLanes.wip,
223372
+ settings,
223373
+ liveSharedBranchMember
223374
+ );
223375
+ if (outcome === "declined") return false;
223376
+ if (outcome === "concurrent" || outcome === "raced" || outcome === "deferred" || outcome === "held") return true;
223377
+ }
223378
+ const message = mayRepairBoundary ? `Workflow merge evidence recovery scheduled implementation resume in '${resumeRouterLanes.wip}'` : `Workflow graph failed at node '${failedNode}'${failureValue ? ` (${failureValue})` : ""} with incomplete work \u2014 resuming in place in '${live.column}'`;
223379
+ executorLog.warn(`${live.id}: ${message}`);
223380
+ await deps.store.logEntry(live.id, message, void 0, deps.getRunContextFor(live.id));
223381
+ if (!mayRepairBoundary) {
223382
+ await deps.store.updateTask(live.id, { status: null, error: null }, deps.getRunContextFor(live.id));
223383
+ }
223384
+ await deps.clearTerminalStepFailuresForRetry(live.id, "archive");
223385
+ await deps.persistTokenUsage(live.id);
223386
+ return true;
223387
+ }
223388
+
222574
223389
  // ../engine/src/agent-browser-lifecycle.ts
222575
223390
  import { execFile as execFile19 } from "node:child_process";
222576
223391
  import { lstatSync as lstatSync3, mkdirSync as mkdirSync9, readdirSync as readdirSync10, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync5, statSync as statSync13, writeFileSync as writeFileSync9 } from "node:fs";
@@ -224754,6 +225569,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
224754
225569
  // stall-deadlock ride this sweep exists to prevent.
224755
225570
  { name: "reconcile-orphaned-pending-step-results", fn: () => this.reconcileOrphanedPendingStepResults().then(() => void 0) },
224756
225571
  { name: "reconcile-unproven-review-approvals", fn: () => this.reconcileUnprovenReviewApprovals().then(() => void 0) },
225572
+ { name: "reconcile-merge-boundary-evidence-gaps", fn: () => this.reconcileMergeBoundaryEvidenceGaps().then(() => void 0) },
224757
225573
  /*
224758
225574
  FNXC:WorkspaceContention 2026-08-23-08:00:
224759
225575
  `contention-hold` is owned by an in-memory retry timer. After a process restart that
@@ -224776,6 +225592,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
224776
225592
  { name: "completed-tasks", fn: () => this.recoverCompletedTasks().then(() => void 0) },
224777
225593
  { name: "recover-stranded-completed-todo", fn: () => this.recoverStrandedCompletedTodoTasks().then(() => void 0) },
224778
225594
  { name: "recover-advanced-triage", fn: () => this.recoverAdvancedTriageTasks().then(() => void 0) },
225595
+ { name: "recover-mergeable-review", fn: () => this.recoverMergeableReviewTasks().then(() => void 0) },
224779
225596
  { name: "failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps().then(() => void 0) },
224780
225597
  { name: "missing-worktree-review-failures", fn: () => this.recoverMissingWorktreeReviewFailures().then(() => void 0) },
224781
225598
  /*
@@ -225649,6 +226466,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
225649
226466
  */
225650
226467
  { name: "reconcile-orphaned-pending-step-results", fn: () => this.reconcileOrphanedPendingStepResults() },
225651
226468
  { name: "reconcile-unproven-review-approvals", fn: () => this.reconcileUnprovenReviewApprovals() },
226469
+ { name: "reconcile-merge-boundary-evidence-gaps", fn: () => this.reconcileMergeBoundaryEvidenceGaps() },
225652
226470
  { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
225653
226471
  { name: "recover-missing-worktree-review-failures", fn: () => this.recoverMissingWorktreeReviewFailures() },
225654
226472
  { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
@@ -230160,6 +230978,63 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
230160
230978
  would discard that valid result. Re-check lane, user pause, workspace ownership, and session liveness
230161
230979
  in the callback, then log and audit only a repair whose atomic mutation actually applied.
230162
230980
  */
230981
+ /*
230982
+ FNXC:WorkflowMergeRecovery 2026-09-20-19:38:
230983
+ A historic merge-boundary-unproven park can outlive the executor process that
230984
+ created it. Recover only durable failed evidence with unfinished checklist
230985
+ work, then use the ordinary named merge-fix remediation transition; no sweep
230986
+ may invent node proof, clear an external blocker, or disturb a live session.
230987
+ */
230988
+ async reconcileMergeBoundaryEvidenceGaps() {
230989
+ const settings = await this.store.getSettings().catch(() => void 0);
230990
+ if (!settings || settings.globalPause || settings.enginePaused) return 0;
230991
+ const reviewColumns = await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES);
230992
+ const candidates = /* @__PURE__ */ new Map();
230993
+ for (const column of reviewColumns) {
230994
+ for (const task of await this.store.listTasks({ column, slim: true })) candidates.set(task.id, task);
230995
+ }
230996
+ const isSessionLive = (taskId) => activeSessionRegistry.pathsForTask(taskId).some((path6) => activeSessionRegistry.isPathActive(path6)) || executingTaskLock.has(taskId) || this.options.isTaskActive?.(taskId) === true;
230997
+ let recovered = 0;
230998
+ for (const candidate of candidates.values()) {
230999
+ if (!candidate.error?.includes("merge-boundary-unproven")) continue;
231000
+ const fresh = await this.store.getTask(candidate.id);
231001
+ if (!fresh || fresh.deletedAt || fresh.userPaused || fresh.paused || fresh.workspaceWorktrees !== void 0 || isTaskExternallyBlocked(fresh) || isSessionLive(fresh.id) || !allowsAutoMergeProcessing(fresh, settings)) continue;
231002
+ const ir2 = await resolveWorkflowIrForTask(this.store, fresh.id).catch(() => void 0);
231003
+ const column = ir2?.columns.find((entry) => entry.id === fresh.column);
231004
+ const wipColumn = ir2?.columns.find((entry) => resolveColumnFlags(entry).countsTowardWip === true)?.id;
231005
+ if (!column || !wipColumn || !isReviewColumnRole(resolveColumnFlags(column), fresh.column)) continue;
231006
+ const proof = await evaluateWorkflowMergeBoundary(
231007
+ { store: this.store, loadMergeBoundaryInstances: (taskId, runId) => loadMergeBoundaryInstances({ store: this.store }, taskId, runId) },
231008
+ fresh
231009
+ ).catch(() => void 0);
231010
+ if (!proof?.hasForeachStepExecute || proof.complete || fresh.noCommitsExpected === true && !hasNonTerminalSteps(fresh)) continue;
231011
+ const evidence = !proof.hasRelevantNodeResult ? { code: "no-node-result", missingInstanceIds: [] } : !proof.allResultsTerminal ? { code: "non-terminal-node-result", nonTerminalNodeId: proof.nonTerminalResult?.workflowStepId, missingInstanceIds: [] } : { code: "missing-foreach-instances", missingInstanceIds: proof.missingInstanceIds };
231012
+ const outcome = await recoverMergeBoundaryEvidenceGap(
231013
+ { store: this.store, getRunContextFor: () => void 0 },
231014
+ fresh,
231015
+ "merge-boundary",
231016
+ evidence,
231017
+ wipColumn,
231018
+ settings
231019
+ );
231020
+ if (outcome !== "recovered") continue;
231021
+ recovered += 1;
231022
+ await this.store.logEntry(
231023
+ fresh.id,
231024
+ "Workflow merge evidence recovery resumed the proven implementation owner; merge proof will be checked after durable progress"
231025
+ ).catch(() => void 0);
231026
+ await emitBoundedRunAudit2(this.store, {
231027
+ taskId: fresh.id,
231028
+ agentId: "self-healing",
231029
+ runId: generateSyntheticRunId("reconcile-merge-boundary-evidence-gap", fresh.id),
231030
+ domain: "database",
231031
+ mutationType: "task:merge-boundary-evidence-recovered",
231032
+ target: fresh.id,
231033
+ metadata: { taskId: fresh.id, outcome: "resumed" }
231034
+ }, { log: log34 });
231035
+ }
231036
+ return recovered;
231037
+ }
230163
231038
  async reconcileUnprovenReviewApprovals() {
230164
231039
  try {
230165
231040
  const reviewColumns = await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES);
@@ -230662,9 +231537,25 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
230662
231537
  mergeRetriesReset
230663
231538
  };
230664
231539
  }
230665
- async routeUnrunPreMergeGateBackToReview(task, reviewColumns, mergeGate, mergeContent) {
231540
+ async routeUnrunPreMergeGateBackToReview(task, reviewColumns, mergeGate, mergeContent, expectedWorkflowSelection, failedPark) {
230666
231541
  if (getTaskMergeBlocker(task, { reviewColumns, requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds, mergeContent }) !== "task has enabled pre-merge workflow steps that never ran") return false;
230667
- const reroute = await rerouteUnrunPreMergeGateToReview(this.store, task, { requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds, mergeContent }).catch(() => ({ rerouted: false, reason: "no-unrun-gate", nodeId: void 0, workflowStepId: void 0 }));
231542
+ const reroute = await rerouteUnrunPreMergeGateToReview(this.store, task, {
231543
+ requiredPreMergeStepIds: mergeGate.requiredPreMergeStepIds,
231544
+ mergeContent,
231545
+ expectedWorkflowSelection
231546
+ }).catch(() => ({ rerouted: false, reason: "no-unrun-gate", nodeId: void 0, workflowStepId: void 0 }));
231547
+ if (reroute.reason === "workflow-selection-changed" && failedPark) {
231548
+ await this.store.updateTaskAtomic(task.id, (live) => live.status === null && live.error === null ? {
231549
+ status: failedPark.status,
231550
+ error: failedPark.error,
231551
+ paused: failedPark.paused,
231552
+ pausedReason: failedPark.pausedReason,
231553
+ mergeRetries: failedPark.mergeRetries,
231554
+ workflowIrPin: failedPark.workflowIrPin,
231555
+ workflowIrPinNodeId: failedPark.workflowIrPinNodeId,
231556
+ workflowIrPinColumnId: failedPark.workflowIrPinColumnId
231557
+ } : null);
231558
+ }
230668
231559
  if (reroute.rerouted) {
230669
231560
  await this.store.logEntry(task.id, "[pre-merge] Self-healing re-seeded the workflow graph at an enabled pre-merge gate that never ran.");
230670
231561
  log34.warn(`Unrun pre-merge gate for ${task.id} re-seeded at ${reroute.nodeId ?? "unknown"}`);
@@ -230718,8 +231609,68 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
230718
231609
  await this.canRecoverMergeRequest(task, settings)
230719
231610
  ]));
230720
231611
  const mergeAdmissionByTaskId = new Map(mergeAdmission);
231612
+ const recoveredUnrunGateIds = /* @__PURE__ */ new Set();
231613
+ for (const task of tasks2) {
231614
+ if (!isRecoverableUnrunGatePark(task) || !mergeAdmissionByTaskId.get(task.id) || executingIds.has(task.id)) continue;
231615
+ try {
231616
+ const reviewColumns = await ownReviewLanesFor(task);
231617
+ if (!reviewColumns.has(task.column)) continue;
231618
+ const gate = await resolvePreMergeGateForTask(this.store, task.id, task.enabledWorkflowSteps, task);
231619
+ if (gate.provenance === "default" && !gate.selectionAbsent) continue;
231620
+ const content = await captureMergeContentDescriptor(task, { workspaceRootDir: this.options.rootDir, settings });
231621
+ if (content.kind !== "singular" || task.workspaceWorktrees !== void 0) continue;
231622
+ const recoverable = { ...task, status: void 0, paused: false };
231623
+ if (getTaskMergeBlocker(recoverable, { reviewColumns, requiredPreMergeStepIds: gate.requiredPreMergeStepIds, mergeContent: content }) !== PRE_MERGE_STEPS_NOT_RUN_BLOCKER) continue;
231624
+ const items = await this.store.listWorkflowWorkItemsForTask(task.id);
231625
+ if (items.some((item) => ACTIVE_WORKFLOW_WORK_ITEM_STATES.includes(item.state))) continue;
231626
+ const snapshot = { updatedAt: task.updatedAt, error: task.error, column: task.column };
231627
+ const failedPark = { ...task };
231628
+ let released = false;
231629
+ await this.store.updateTaskAtomic(task.id, (live2) => {
231630
+ if (!isRecoverableUnrunGatePark(live2) || live2.updatedAt !== snapshot.updatedAt || live2.error !== snapshot.error || live2.column !== snapshot.column || !allowsAutoMergeProcessing(live2, settings)) return null;
231631
+ released = true;
231632
+ return {
231633
+ status: null,
231634
+ error: null,
231635
+ paused: false,
231636
+ pausedReason: null,
231637
+ mergeRetries: 0,
231638
+ workflowIrPin: null,
231639
+ workflowIrPinNodeId: null,
231640
+ workflowIrPinColumnId: null
231641
+ };
231642
+ });
231643
+ if (!released) continue;
231644
+ const live = await this.store.getTask(task.id);
231645
+ if (!live) continue;
231646
+ const selection = this.store.getTaskWorkflowSelectionAsync ? await this.store.getTaskWorkflowSelectionAsync(live.id) : this.store.getTaskWorkflowSelection(live.id);
231647
+ const expectedWorkflowSelection = selection ? { workflowId: selection.workflowId, stepIds: [...selection.stepIds] } : null;
231648
+ const currentGate = await resolvePreMergeGateForTask(this.store, live.id, live.enabledWorkflowSteps, live);
231649
+ if (currentGate.provenance === "default" && !currentGate.selectionAbsent) continue;
231650
+ const currentContent = await captureMergeContentDescriptor(live, { workspaceRootDir: this.options.rootDir, settings });
231651
+ if (currentContent.kind !== "singular" || live.workspaceWorktrees !== void 0) continue;
231652
+ const currentRecoverable = { ...live, status: void 0, paused: false };
231653
+ if (getTaskMergeBlocker(currentRecoverable, {
231654
+ reviewColumns: currentGate.reviewColumns,
231655
+ requiredPreMergeStepIds: currentGate.requiredPreMergeStepIds,
231656
+ mergeContent: currentContent
231657
+ }) !== PRE_MERGE_STEPS_NOT_RUN_BLOCKER) continue;
231658
+ if (await this.routeUnrunPreMergeGateBackToReview(
231659
+ live,
231660
+ currentGate.reviewColumns,
231661
+ currentGate,
231662
+ currentContent,
231663
+ expectedWorkflowSelection,
231664
+ failedPark
231665
+ )) {
231666
+ recoveredUnrunGateIds.add(task.id);
231667
+ }
231668
+ } catch (error2) {
231669
+ log34.warn(`Unrun pre-merge gate recovery deferred for ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`);
231670
+ }
231671
+ }
230721
231672
  const mergeable = tasks2.filter(
230722
- (t) => mergeAdmissionByTaskId.get(t.id) === true && !t.paused && !executingIds.has(t.id) && t.status !== "failed" && // Exclude transient merge statuses. Active merges should be left alone;
231673
+ (t) => !recoveredUnrunGateIds.has(t.id) && mergeAdmissionByTaskId.get(t.id) === true && !t.paused && !executingIds.has(t.id) && t.status !== "failed" && // Exclude transient merge statuses. Active merges should be left alone;
230723
231674
  // stale ones are handled by recoverStaleMergingStatus().
230724
231675
  t.status !== "merging" && t.status !== "merging-pr" && // FNXC:Workspace 2026-06-22-09:30 (Phase D U1, KTD1 — admit workspace tasks):
230725
231676
  // A workspace task has task.worktree===null (its worktrees live per-repo in
@@ -236596,49 +237547,6 @@ function isGenericAbortProvenance(provenance) {
236596
237547
  return provenance === "hard-cancel" || provenance === "engine-abort";
236597
237548
  }
236598
237549
 
236599
- // ../engine/src/executor/workflow-step-satisfaction.ts
236600
- function hasNonTerminalWorkflowSteps(task) {
236601
- return task.steps.length > 0 && task.steps.some((step) => step.status !== "done" && step.status !== "skipped");
236602
- }
236603
- function workflowStepResultPassed(task, workflowStepId) {
236604
- const results = task?.workflowStepResults ?? [];
236605
- return results.some(
236606
- (result) => result.workflowStepId === workflowStepId && result.phase === "pre-merge" && result.status === "passed"
236607
- );
236608
- }
236609
- function areEnabledPreMergeWorkflowStepsSatisfied(task) {
236610
- const preMergeGateIds = /* @__PURE__ */ new Set(["plan-review", "browser-verification", "code-review"]);
236611
- const enabled = task?.enabledWorkflowSteps;
236612
- const enabledPreMerge = Array.isArray(enabled) && enabled.length > 0 ? enabled.filter((id) => preMergeGateIds.has(id)) : ["plan-review", "code-review"];
236613
- if (enabledPreMerge.length === 0) return false;
236614
- if (Array.isArray(enabled) && enabledPreMerge.length !== enabled.length) return false;
236615
- return enabledPreMerge.every((id) => workflowStepResultPassed(task, id));
236616
- }
236617
- function preservePreExecutionWorkflowStepResults(task) {
236618
- const preserved = (task.workflowStepResults ?? []).filter((result) => result.workflowStepId === "plan-review");
236619
- if (preserved.length > 0) return preserved;
236620
- let latest;
236621
- for (const entry of task.log ?? []) {
236622
- if (entry.action === "[pre-merge] Workflow step completed: Plan Review") {
236623
- latest = { timestamp: entry.timestamp, outcome: entry.outcome, status: "passed" };
236624
- } else if (entry.action === "[pre-merge] Workflow step failed: Plan Review") {
236625
- latest = { timestamp: entry.timestamp, outcome: entry.outcome, status: "failed" };
236626
- }
236627
- }
236628
- if (latest?.status !== "passed") return [];
236629
- return [
236630
- {
236631
- workflowStepId: "plan-review",
236632
- workflowStepName: "Plan Review",
236633
- phase: "pre-merge",
236634
- status: "passed",
236635
- verdict: "APPROVE",
236636
- ...latest.outcome ? { output: latest.outcome, notes: latest.outcome } : {},
236637
- ...latest.timestamp ? { startedAt: latest.timestamp, completedAt: latest.timestamp } : {}
236638
- }
236639
- ];
236640
- }
236641
-
236642
237550
  // ../engine/src/executor/graph-resume-predicates.ts
236643
237551
  function isTransientResumeAfterRestartGraphFailure(live, result) {
236644
237552
  if ((result.reason ?? "").trim().length > 0) return false;
@@ -238742,6 +239650,8 @@ init_session_skill_context();
238742
239650
 
238743
239651
  // ../engine/src/environment/environment-capabilities.ts
238744
239652
  import { exec as exec39 } from "node:child_process";
239653
+ import { readFile as readFile47 } from "node:fs/promises";
239654
+ import { join as join86 } from "node:path";
238745
239655
  import { promisify as promisify57 } from "node:util";
238746
239656
  var execAsync36 = promisify57(exec39);
238747
239657
  var BASELINE_ENVIRONMENT_CAPABILITY_COMMANDS = Object.freeze([
@@ -238764,17 +239674,23 @@ var BASELINE_ENVIRONMENT_CAPABILITY_COMMANDS = Object.freeze([
238764
239674
  "dotnet",
238765
239675
  "make",
238766
239676
  "docker",
238767
- "git"
239677
+ "git",
239678
+ "nix",
239679
+ "direnv",
239680
+ "devbox",
239681
+ "nvm",
239682
+ "fnm",
239683
+ "asdf",
239684
+ "mise"
238768
239685
  ]);
238769
239686
  var DEFAULT_PROBE_TIMEOUT_MS = 5e3;
239687
+ var PROBE_CACHE_TTL_MS = 6e4;
238770
239688
  var PROBE_MAX_BUFFER_BYTES = 64 * 1024;
238771
239689
  var SAFE_COMMAND_NAME = /^[A-Za-z0-9._+/-]+$/;
238772
239690
  var ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
238773
239691
  var probeCache = /* @__PURE__ */ new Map();
238774
239692
  function unquoteToken(token) {
238775
- if (token.length >= 2 && (token.startsWith('"') && token.endsWith('"') || token.startsWith("'") && token.endsWith("'"))) {
238776
- return token.slice(1, -1);
238777
- }
239693
+ if (token.length >= 2 && (token.startsWith('"') && token.endsWith('"') || token.startsWith("'") && token.endsWith("'"))) return token.slice(1, -1);
238778
239694
  return token;
238779
239695
  }
238780
239696
  function extractCommandBinaries(command) {
@@ -238790,70 +239706,126 @@ function extractCommandBinaries(command) {
238790
239706
  }
238791
239707
  return [...new Set(binaries)];
238792
239708
  }
238793
- function collectCandidates(extraCommands) {
239709
+ function commandTokens(command) {
239710
+ if (/[;&|`$<>()[\]{}\\]/.test(command)) return null;
239711
+ const tokens2 = command.trim().split(/\s+/).map(unquoteToken).filter(Boolean);
239712
+ let index2 = tokens2[0] === "env" ? 1 : 0;
239713
+ while (index2 < tokens2.length && ENV_ASSIGNMENT.test(tokens2[index2])) index2 += 1;
239714
+ const result = tokens2.slice(index2);
239715
+ return result.length > 0 && result.every((token) => SAFE_COMMAND_NAME.test(token)) ? result : null;
239716
+ }
239717
+ function parseRecognizedWrapper(command) {
239718
+ if (!command) return null;
239719
+ const tokens2 = commandTokens(command);
239720
+ if (!tokens2) return null;
239721
+ const wrapper = tokens2[0];
239722
+ let prefix = null;
239723
+ let inner;
239724
+ if (wrapper === "nix" && tokens2[1] === "develop" && tokens2[2] === "--command") {
239725
+ prefix = ["nix", "develop", "--command"];
239726
+ inner = tokens2[3];
239727
+ } else if (wrapper === "devbox" && tokens2[1] === "run") {
239728
+ prefix = ["devbox", "run"];
239729
+ inner = tokens2[2];
239730
+ } else if ((wrapper === "nvm" || wrapper === "fnm") && tokens2[1] === "exec") {
239731
+ prefix = tokens2.slice(0, 3);
239732
+ inner = tokens2[3];
239733
+ } else if ((wrapper === "asdf" || wrapper === "mise") && tokens2[1] === "exec") {
239734
+ prefix = tokens2.slice(0, 2);
239735
+ inner = tokens2[2];
239736
+ } else if (wrapper === "direnv" && tokens2[1] === "exec" && tokens2[2]) {
239737
+ prefix = tokens2.slice(0, 3);
239738
+ inner = tokens2[3];
239739
+ }
239740
+ if (!prefix || !inner || !SAFE_COMMAND_NAME.test(inner)) return null;
239741
+ return { prefix, display: prefix.join(" "), innerCandidates: [inner] };
239742
+ }
239743
+ function collectCandidates(extraCommands, wrappers) {
238794
239744
  return [.../* @__PURE__ */ new Set([
238795
239745
  ...BASELINE_ENVIRONMENT_CAPABILITY_COMMANDS,
238796
- ...extraCommands.map((name2) => name2.trim()).filter((name2) => SAFE_COMMAND_NAME.test(name2))
239746
+ ...extraCommands.map((name2) => name2.trim()).filter((name2) => SAFE_COMMAND_NAME.test(name2)),
239747
+ ...wrappers.flatMap((wrapper) => wrapper.innerCandidates)
238797
239748
  ])].sort((a, b3) => a.localeCompare(b3));
238798
239749
  }
238799
239750
  function quoteShellToken(token) {
238800
239751
  return `'${token.replaceAll("'", "'\\''")}'`;
238801
239752
  }
238802
239753
  function buildProbeCommand(candidates) {
238803
- const args = candidates.map(quoteShellToken).join(" ");
238804
- return `for name in ${args}; do if command -v "$name" >/dev/null 2>&1; then printf '%s=1\\n' "$name"; else printf '%s=0\\n' "$name"; fi; done`;
239754
+ return `for name in ${candidates.map(quoteShellToken).join(" ")}; do if command -v "$name" >/dev/null 2>&1; then printf '%s=1\\n' "$name"; else printf '%s=0\\n' "$name"; fi; done`;
239755
+ }
239756
+ function buildWrapperProbeCommand(wrapper, candidates) {
239757
+ return [...wrapper.prefix.map(quoteShellToken), quoteShellToken("/bin/bash"), quoteShellToken("-c"), quoteShellToken(buildProbeCommand(candidates))].join(" ");
238805
239758
  }
238806
239759
  function parseProbeOutput(stdout, candidates) {
238807
239760
  const parsed = /* @__PURE__ */ new Map();
238808
239761
  for (const line2 of stdout.split("\n").map((value) => value.trim()).filter(Boolean)) {
238809
239762
  const match = /^(.+)=(0|1)$/.exec(line2);
238810
- if (!match || !candidates.includes(match[1]) || parsed.has(match[1])) {
238811
- return { capabilities: [], degraded: true };
238812
- }
239763
+ if (!match || !candidates.includes(match[1]) || parsed.has(match[1])) return { capabilities: [], degraded: true };
238813
239764
  parsed.set(match[1], match[2] === "1");
238814
239765
  }
238815
239766
  if (parsed.size !== candidates.length) return { capabilities: [], degraded: true };
238816
- return {
238817
- capabilities: candidates.map((name2) => ({ name: name2, available: parsed.get(name2) })),
238818
- degraded: false
238819
- };
239767
+ return { capabilities: candidates.map((name2) => ({ name: name2, available: parsed.get(name2) })), degraded: false };
238820
239768
  }
238821
- async function probeUncached(candidates, timeoutMs) {
239769
+ async function runProbe2(command, candidates, timeoutMs) {
238822
239770
  try {
238823
- const { stdout } = await execAsync36(buildProbeCommand(candidates), {
238824
- timeout: timeoutMs,
238825
- maxBuffer: PROBE_MAX_BUFFER_BYTES,
238826
- encoding: "utf-8",
238827
- shell: "/bin/bash"
238828
- });
239771
+ const { stdout } = await execAsync36(command, { timeout: timeoutMs, maxBuffer: PROBE_MAX_BUFFER_BYTES, encoding: "utf-8", shell: "/bin/bash" });
238829
239772
  return parseProbeOutput(stdout ?? "", candidates);
238830
239773
  } catch {
238831
239774
  return { capabilities: [], degraded: true };
238832
239775
  }
238833
239776
  }
238834
- function probeEnvironmentCapabilities(options = {}) {
238835
- const candidates = collectCandidates(options.extraCommands ?? []);
238836
- const cacheKey = candidates.join("\0");
238837
- let promise = probeCache.get(cacheKey);
238838
- if (!promise) {
238839
- promise = probeUncached(candidates, options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
238840
- probeCache.set(cacheKey, promise);
238841
- }
239777
+ async function rootNixWrapper(rootDir) {
239778
+ if (!rootDir) return { wrapper: null, fingerprint: "no-root" };
239779
+ try {
239780
+ const content = await readFile47(join86(rootDir, "flake.nix"), "utf8");
239781
+ return { wrapper: { prefix: ["nix", "develop", "--command"], display: "nix develop --command", innerCandidates: [] }, fingerprint: `flake:${content.length}:${content.slice(0, 256)}` };
239782
+ } catch {
239783
+ return { wrapper: null, fingerprint: "no-flake" };
239784
+ }
239785
+ }
239786
+ async function probeUncached(candidates, wrappers, timeoutMs) {
239787
+ const host = await runProbe2(buildProbeCommand(candidates), candidates, timeoutMs);
239788
+ if (host.degraded) return host;
239789
+ const merged = new Map(host.capabilities.map((capability) => [capability.name, capability]));
239790
+ for (const wrapper of wrappers) {
239791
+ if (!merged.get(wrapper.prefix[0])?.available) continue;
239792
+ const wrapped = await runProbe2(buildWrapperProbeCommand(wrapper, candidates), candidates, timeoutMs);
239793
+ if (wrapped.degraded) continue;
239794
+ for (const capability of wrapped.capabilities) {
239795
+ if (capability.available && !merged.get(capability.name)?.available) merged.set(capability.name, { ...capability, via: wrapper.display });
239796
+ }
239797
+ }
239798
+ return { capabilities: candidates.map((candidate) => merged.get(candidate)), degraded: false };
239799
+ }
239800
+ async function probeEnvironmentCapabilities(options = {}) {
239801
+ const marker = await rootNixWrapper(options.rootDir);
239802
+ const configured = [parseRecognizedWrapper(options.testCommand), parseRecognizedWrapper(options.buildCommand)].filter((value) => value !== null);
239803
+ const wrappers = [...new Map([...configured, ...marker.wrapper ? [marker.wrapper] : []].map((wrapper) => [wrapper.display, wrapper])).values()];
239804
+ const candidates = collectCandidates(options.extraCommands ?? [], wrappers);
239805
+ const normalizedCommands = [options.testCommand?.trim() ?? "", options.buildCommand?.trim() ?? ""].join("\0");
239806
+ const cacheKey = [options.projectId ?? options.rootDir ?? "host", normalizedCommands, marker.fingerprint, candidates.join("\0")].join("");
239807
+ const now = options.now?.() ?? Date.now();
239808
+ const cached2 = probeCache.get(cacheKey);
239809
+ if (cached2 && now - cached2.createdAt < PROBE_CACHE_TTL_MS) return cached2.promise;
239810
+ const promise = probeUncached(candidates, wrappers, options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
239811
+ probeCache.set(cacheKey, { createdAt: now, promise });
238842
239812
  return promise;
238843
239813
  }
238844
239814
  function formatEnvironmentCapabilitiesSection(probe) {
238845
239815
  if (probe.degraded || probe.capabilities.length === 0) return "";
238846
- const available2 = probe.capabilities.filter((capability) => capability.available).map(({ name: name2 }) => name2);
239816
+ const available2 = probe.capabilities.filter((capability) => capability.available && !capability.via).map(({ name: name2 }) => name2);
239817
+ const wrapperAvailable = probe.capabilities.filter((capability) => capability.available && capability.via).map(({ name: name2, via }) => `${name2} via ${via}`);
238847
239818
  const unavailable2 = probe.capabilities.filter((capability) => !capability.available).map(({ name: name2 }) => name2);
238848
239819
  return [
238849
239820
  "## Environment Capabilities",
238850
239821
  "",
238851
- `Available commands: ${available2.length > 0 ? available2.join(", ") : "none"}`,
238852
- `Unavailable commands: ${unavailable2.length > 0 ? unavailable2.join(", ") : "none"}`,
239822
+ `Host-available commands: ${available2.length > 0 ? available2.join(", ") : "none"}`,
239823
+ `Available via confirmed project wrapper: ${wrapperAvailable.length > 0 ? wrapperAvailable.join(", ") : "none"}`,
239824
+ `Confirmed-unavailable commands: ${unavailable2.length > 0 ? unavailable2.join(", ") : "none"}`,
238853
239825
  "",
238854
- "- Do not make an acceptance criterion or required verification depend on an unavailable runtime.",
238855
- "- When the ideal check needs an absent runtime, keep the objective, specify a runnable substitute, and record the ideal check under an `## Environment Constraints` section of PROMPT.md as explicitly non-blocking.",
238856
- "- This inventory describes the execution host; a repository script whose interpreter is unavailable cannot run here."
239826
+ "- Only the commands listed here were probed. Any unlisted command is UNKNOWN, not absent.",
239827
+ "- A documented or configured command using a confirmed wrapper remains runnable and binding even if its inner executable is absent from the host PATH.",
239828
+ "- Only a confirmed-unavailable runtime without an applicable confirmed wrapper needs a runnable substitute; record an ideal impossible check under `## Environment Constraints` as explicitly non-blocking."
238857
239829
  ].join("\n");
238858
239830
  }
238859
239831
 
@@ -240454,16 +241426,16 @@ init_effective_settings();
240454
241426
  // ../engine/src/executor/workflow-step-failure-injection.ts
240455
241427
  init_src2();
240456
241428
  init_logger();
240457
- import { join as join86 } from "node:path";
240458
- import { readFile as readFile47, writeFile as writeFile27 } from "node:fs/promises";
241429
+ import { join as join87 } from "node:path";
241430
+ import { readFile as readFile48, writeFile as writeFile27 } from "node:fs/promises";
240459
241431
  var ADVISORY_SECTION_HEADER = "## Review Advisory Notes";
240460
241432
  async function injectReviewAdvisoryNotes(store, task, stepName, findings) {
240461
241433
  findings = findings.filter(isOpenWorkflowReviewFinding);
240462
241434
  if (findings.length === 0) return;
240463
- const promptPath = join86(store.getFusionDir(), "tasks", task.id, "PROMPT.md");
241435
+ const promptPath = join87(store.getFusionDir(), "tasks", task.id, "PROMPT.md");
240464
241436
  let content;
240465
241437
  try {
240466
- content = await readFile47(promptPath, "utf-8");
241438
+ content = await readFile48(promptPath, "utf-8");
240467
241439
  } catch {
240468
241440
  executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping review advisory injection`);
240469
241441
  return;
@@ -240487,10 +241459,10 @@ ${section}`;
240487
241459
  }
240488
241460
  }
240489
241461
  async function injectWorkflowStepFailureInstructions(store, task, failureFeedback, stepName, retry, findings) {
240490
- const promptPath = join86(store.getFusionDir(), "tasks", task.id, "PROMPT.md");
241462
+ const promptPath = join87(store.getFusionDir(), "tasks", task.id, "PROMPT.md");
240491
241463
  let content;
240492
241464
  try {
240493
- content = await readFile47(promptPath, "utf-8");
241465
+ content = await readFile48(promptPath, "utf-8");
240494
241466
  } catch {
240495
241467
  executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping workflow failure injection`);
240496
241468
  return;
@@ -240568,7 +241540,7 @@ ${scopeGuard}
240568
241540
  }
240569
241541
 
240570
241542
  // ../engine/src/executor/skill-path-helpers.ts
240571
- import { basename as basename23, join as join87 } from "node:path";
241543
+ import { basename as basename23, join as join88 } from "node:path";
240572
241544
  import { existsSync as existsSync68 } from "node:fs";
240573
241545
  function mergeAdditionalSkillPaths2(...pathGroups) {
240574
241546
  const merged = Array.from(new Set(pathGroups.flatMap((paths2) => paths2 ?? [])));
@@ -240581,7 +241553,7 @@ function isWorkflowStepSkillDiscoverable(skillName, additionalSkillPaths, ceSkil
240581
241553
  return false;
240582
241554
  }
240583
241555
  return (additionalSkillPaths ?? []).some(
240584
- (skillPath) => basename23(skillPath) === bareSkillName && existsSync68(join87(skillPath, "SKILL.md")) || existsSync68(join87(skillPath, bareSkillName, "SKILL.md"))
241556
+ (skillPath) => basename23(skillPath) === bareSkillName && existsSync68(join88(skillPath, "SKILL.md")) || existsSync68(join88(skillPath, bareSkillName, "SKILL.md"))
240585
241557
  );
240586
241558
  }
240587
241559
 
@@ -240860,7 +241832,11 @@ async function executeWorkflowStep(deps, task, workflowStep, worktreePath, setti
240860
241832
  extraCommands: [
240861
241833
  ...extractCommandBinaries(settings.testCommand),
240862
241834
  ...extractCommandBinaries(settings.buildCommand)
240863
- ]
241835
+ ],
241836
+ testCommand: settings.testCommand,
241837
+ buildCommand: settings.buildCommand,
241838
+ rootDir: deps.rootDir,
241839
+ projectId: deps.store.getProjectId?.() ?? deps.rootDir
240864
241840
  }).catch(() => ({ capabilities: [], degraded: true })) : void 0;
240865
241841
  const planReviewEnvironmentCapabilitiesBlock = planReviewEnvironmentCapabilities ? formatEnvironmentCapabilitiesSection(planReviewEnvironmentCapabilities) : "";
240866
241842
  if (isReviewTypeWorkflowStep && !workflowReviewSpecText.trim()) {
@@ -241717,7 +242693,7 @@ If you have just finished a step's work, immediately call \`fn_task_update\` to
241717
242693
 
241718
242694
  The user is not watching this conversation in real-time. They will read the final result. Asking permission wastes a full retry cycle and may orphan committed work.
241719
242695
 
241720
- **Cannot proceed \u2014 the honest blocked exit.** If the work genuinely cannot be finished because of a host-resource, network, model-provider, or credential failure, or because a prerequisite Fusion task is incomplete, call \`fn_task_done(outcome="blocked", reason="<concrete blocker + what would unblock it>", blockedBy=["FN-XXXX"])\`. Missing tooling, optional services, and unrunnable commands are not blocked exits: resolve them, substitute a runnable check, or complete achievable work and record the deferred verification. A classified external block freezes in place; task dependencies park durable failed WITHOUT auto-replan and requeue when those tasks complete. Blockers must be Fusion board tasks \u2014 do NOT treat open GitHub PRs touching the same files as blockers; other PRs are not claims on your file scope. Do NOT skip remaining steps to fake completion.
242696
+ **Cannot proceed \u2014 the honest blocked exit.** If the work genuinely cannot be finished because of a host-resource, network, model-provider, or credential failure, or because a prerequisite Fusion task is incomplete, call \`fn_task_done(outcome="blocked", reason="<concrete blocker + what would unblock it>", blockedBy=["FN-XXXX"])\`. Missing tooling, optional services, and unrunnable commands are not blocked exits: resolve them, substitute a runnable check, or complete achievable work and record the deferred verification. A classified external block freezes in place; task dependencies park durable failed WITHOUT auto-replan and requeue when those tasks complete. For an accepted external block, include \`blockedReport\` with concise verified condition, stop reason, unimplemented work, and precise unblock condition; never include prompts, tool logs, stack traces, or credentials. Blockers must be Fusion board tasks \u2014 do NOT treat open GitHub PRs touching the same files as blockers; other PRs are not claims on your file scope. Do NOT skip remaining steps to fake completion.
241721
242697
  This is THE correct action when you are stuck \u2014 do NOT instead mark the remaining steps \`skipped\` and call \`fn_task_done\` to make the task look finished. Skipping steps to escape a blocker launders a failure into \`done\` and is never the right move. (\`skipped\` remains valid only for the stale-premise path below, when the requested work is already present on HEAD.) Never write the blocker as plain prose.
241722
242698
 
241723
242699
  ## How to work
@@ -242147,7 +243123,7 @@ Use \`fn_task_create\` for truly separate follow-up work, including unrelated/pr
242147
243123
  Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N \u2014 <short summary>"${sourceIssueRef ? ` -m "Ref: ${sourceIssueRef}"` : ""}${authorArg}\`
242148
243124
  The \`<short summary>\` is required \u2014 replace it with a concrete 5\u201310 word description of what the step changed.
242149
243125
  When all steps are complete: call \`fn_task_done()\`
242150
- For \`fn_task_done(outcome="blocked")\`, reserve \`obstacle:"outside-worktree"\` for host-resource, network, model-provider, and credential failures. An unavailable command, interpreter, optional service, or unrunnable test is never a blocked exit: resolve it with project tooling or an available equivalent, substitute a runnable automated check, or\u2014when it does not prevent the task objective\u2014complete the achievable work and record the deferred verification. If PROMPT.md has an \`## Environment Constraints\` section, its substitute is the sanctioned verification path and must not be re-litigated as a blocker. Set \`obstacle:"inside-worktree"\` for code, tests, lint, types, plan, merge, or review failures so AI self-repair remains active.
243126
+ For \`fn_task_done(outcome="blocked")\`, reserve \`obstacle:"outside-worktree"\` for host-resource, network, model-provider, and credential failures. An unavailable command, interpreter, optional service, or unrunnable test is never a blocked exit: resolve it with project tooling or an available equivalent, substitute a runnable automated check, or\u2014when it does not prevent the task objective\u2014complete the achievable work and record the deferred verification. Treat an \`## Environment Constraints\` substitute as sanctioned only when the accompanying capability block confirms its absence premise. Re-evaluate a stale host-only downgrade when the documented command uses a detected or configured confirmed wrapper; that wrapper command remains binding. Set \`obstacle:"inside-worktree"\` for code, tests, lint, types, plan, merge, or review failures so AI self-repair remains active.
242151
243127
 
242152
243128
  If a build command is configured, run that exact command in this worktree before calling \`fn_task_done()\`.
242153
243129
  Treat a non-zero exit code as a blocking failure. Do not claim success without a real passing run.
@@ -243112,7 +244088,7 @@ import { existsSync as existsSync74 } from "node:fs";
243112
244088
 
243113
244089
  // ../engine/src/executor/session-worktree-paths.ts
243114
244090
  init_worktree_paths();
243115
- import { readFile as readFile48 } from "node:fs/promises";
244091
+ import { readFile as readFile49 } from "node:fs/promises";
243116
244092
  import { realpathSync as realpathSync12 } from "node:fs";
243117
244093
  import { resolve as resolvePath6 } from "node:path";
243118
244094
  function canonicalizePath2(path6) {
@@ -243137,7 +244113,7 @@ function normalizeWorktreePath(pathValue) {
243137
244113
  }
243138
244114
  async function extractPersistedSessionWorktreePath(sessionFile, rootDir, settings) {
243139
244115
  try {
243140
- const content = await readFile48(sessionFile, "utf-8");
244116
+ const content = await readFile49(sessionFile, "utf-8");
243141
244117
  const matches = content.match(buildSessionWorktreePathRegex(rootDir, settings)) ?? [];
243142
244118
  if (matches.length === 0) return null;
243143
244119
  const normalizedCounts = /* @__PURE__ */ new Map();
@@ -244644,9 +245620,6 @@ async function handleImplicitTaskDoneRefusal(deps, task, refusal) {
244644
245620
  ...taintUpdate,
244645
245621
  paused: false,
244646
245622
  pausedByAgentId: null,
244647
- worktree: null,
244648
- branch: null,
244649
- branchWriteOrigin: "engine",
244650
245623
  sessionFile: null
244651
245624
  });
244652
245625
  await deps.store.logEntry(
@@ -244664,9 +245637,6 @@ async function handleImplicitTaskDoneRefusal(deps, task, refusal) {
244664
245637
  ...taintUpdate,
244665
245638
  paused: false,
244666
245639
  pausedByAgentId: null,
244667
- worktree: null,
244668
- branch: null,
244669
- branchWriteOrigin: "engine",
244670
245640
  sessionFile: null
244671
245641
  });
244672
245642
  await deps.store.logEntry(task.id, `${refusal.message} \u2014 execution failed because implicit fn_task_done was refused`, void 0, deps.getRunContextFor(task.id));
@@ -245622,7 +246592,15 @@ async function ensureWorkflowMergeBoundaryTask(deps, task, metadata) {
245622
246592
  await deps.store.logEntry(live.id, `Workflow merge boundary blocked: ${blocked.reason}`, void 0, deps.getRunContextFor(live.id));
245623
246593
  return {
245624
246594
  task: live,
245625
- blocked: { ...blocked, missingInstanceCount: mergeProof.missingInstanceIds.length }
246595
+ blocked: {
246596
+ ...blocked,
246597
+ missingInstanceCount: mergeProof.missingInstanceIds.length,
246598
+ evidence: {
246599
+ code: blocked.code,
246600
+ nonTerminalNodeId: mergeProof.nonTerminalResult?.workflowStepId,
246601
+ missingInstanceIds: mergeProof.missingInstanceIds
246602
+ }
246603
+ }
245626
246604
  };
245627
246605
  }
245628
246606
  if (deps.shouldCompleteChecklistAtWorkflowMerge(live, mergeProof)) {
@@ -245944,6 +246922,7 @@ async function performWorkflowRerunBounce(deps, taskId, worktreePath, preserveRe
245944
246922
 
245945
246923
  // ../engine/src/executor/unpause-resume.ts
245946
246924
  init_logger();
246925
+ init_scheduler();
245947
246926
  async function dispatchUnpauseResume(deps, task) {
245948
246927
  if (task.status === "failed") {
245949
246928
  return false;
@@ -245959,6 +246938,13 @@ async function dispatchUnpauseResume(deps, task) {
245959
246938
  executorLog.debug(`Skipping unpause resume for ${task.id} \u2014 ${pauseLabel} active`);
245960
246939
  return false;
245961
246940
  }
246941
+ if (task.dependencies?.length) {
246942
+ const tasks2 = await deps.store.listTasks({ includeArchived: false, slim: true });
246943
+ const liveTask = tasks2.find((candidate) => candidate.id === task.id) ?? task;
246944
+ if (getUnmetSchedulingDependencies(liveTask, tasks2).length > 0) {
246945
+ return false;
246946
+ }
246947
+ }
245962
246948
  if (deps.executing.has(task.id) || deps.recoveringCompleted.has(task.id) || deps.activeSessions.has(task.id) || deps.activeStepExecutors.has(task.id) || deps.activeWorkflowStepSessions.has(task.id) || deps.graphRouting.has(task.id)) {
245963
246949
  return false;
245964
246950
  }
@@ -248338,8 +249324,8 @@ async function resolveSeamColumnAgent(deps, task, detail) {
248338
249324
  init_src2();
248339
249325
  init_logger();
248340
249326
  import { setImmediate as setImmediateCb3 } from "node:timers";
248341
- import { readFile as readFile49, rm as rm13 } from "node:fs/promises";
248342
- import { join as join88 } from "node:path";
249327
+ import { readFile as readFile50, rm as rm13 } from "node:fs/promises";
249328
+ import { join as join89 } from "node:path";
248343
249329
 
248344
249330
  // ../engine/src/executor/resume-orphan-delay.ts
248345
249331
  function getResumeOrphanDelayMs() {
@@ -248385,12 +249371,12 @@ async function resumeOrphaned(deps) {
248385
249371
  for (const task of inProgress) {
248386
249372
  if (yieldNext) await yieldEventLoop3();
248387
249373
  yieldNext = true;
248388
- const tasksDir = typeof deps.store.getTasksDir === "function" ? deps.store.getTasksDir() : deps.rootDir ? join88(deps.rootDir, ".fusion", "tasks") : void 0;
248389
- const intentPath = tasksDir ? join88(tasksDir, task.id, "deferred-terminal-park.json") : void 0;
249374
+ const tasksDir = typeof deps.store.getTasksDir === "function" ? deps.store.getTasksDir() : deps.rootDir ? join89(deps.rootDir, ".fusion", "tasks") : void 0;
249375
+ const intentPath = tasksDir ? join89(tasksDir, task.id, "deferred-terminal-park.json") : void 0;
248390
249376
  if (intentPath) {
248391
249377
  let raw;
248392
249378
  try {
248393
- raw = await readFile49(intentPath, "utf-8");
249379
+ raw = await readFile50(intentPath, "utf-8");
248394
249380
  } catch (error2) {
248395
249381
  if (error2?.code !== "ENOENT") {
248396
249382
  executorLog.warn(`${task.id}: deferred terminal-park intent could not be read (${error2 instanceof Error ? error2.message : String(error2)}) \u2014 retaining intent and skipping orphan recovery`);
@@ -250398,11 +251384,11 @@ init_logger();
250398
251384
  // ../engine/src/cli-agent/task-session.ts
250399
251385
  import { mkdtemp as mkdtemp3, rm as rm15 } from "node:fs/promises";
250400
251386
  import { tmpdir as tmpdir10 } from "node:os";
250401
- import { join as join90 } from "node:path";
251387
+ import { join as join91 } from "node:path";
250402
251388
 
250403
251389
  // ../engine/src/cli-agent/hook-scripts.ts
250404
251390
  import { mkdir as mkdir31, rm as rm14, writeFile as writeFile28 } from "node:fs/promises";
250405
- import { join as join89 } from "node:path";
251391
+ import { join as join90 } from "node:path";
250406
251392
  var HOOK_SCRIPT_NAMES = {
250407
251393
  /** Main hook script: POSTs stdin payload (with the hook event name) to the route. */
250408
251394
  hook: "fusion-hook.sh",
@@ -250484,8 +251470,8 @@ exit 0
250484
251470
  async function writeSessionHookScripts(opts) {
250485
251471
  const { sessionId, token, endpointUrl, dir } = opts;
250486
251472
  await mkdir31(dir, { recursive: true, mode: 448 });
250487
- const hookScriptPath = join89(dir, HOOK_SCRIPT_NAMES.hook);
250488
- const notifyScriptPath = join89(dir, HOOK_SCRIPT_NAMES.notify);
251473
+ const hookScriptPath = join90(dir, HOOK_SCRIPT_NAMES.hook);
251474
+ const notifyScriptPath = join90(dir, HOOK_SCRIPT_NAMES.notify);
250489
251475
  await writeFile28(hookScriptPath, buildHookScriptContent({ sessionId, token, endpointUrl }), {
250490
251476
  mode: 448
250491
251477
  });
@@ -250661,9 +251647,9 @@ var CliTaskSession = class _CliTaskSession {
250661
251647
  isApproved: opts.isAutonomyApproved ?? (() => false)
250662
251648
  });
250663
251649
  const root = opts.hookDirRoot ?? tmpdir10();
250664
- const hookDir = await mkdtemp3(join90(root, "fusion-cli-hooks-"));
250665
- const hookScriptPath = join90(hookDir, HOOK_SCRIPT_NAMES.hook);
250666
- const settingsPath = join90(hookDir, "settings.json");
251650
+ const hookDir = await mkdtemp3(join91(root, "fusion-cli-hooks-"));
251651
+ const hookScriptPath = join91(hookDir, HOOK_SCRIPT_NAMES.hook);
251652
+ const settingsPath = join91(hookDir, "settings.json");
250667
251653
  const cliAgentSettings = opts.config.cliAgentSettings ?? null;
250668
251654
  const operatorEnvAdditions = (cliAgentSettings?.envAdditions ?? []).filter(
250669
251655
  (k2) => !/^FUSION_/i.test(k2)
@@ -250917,7 +251903,7 @@ function killLiveTaskSessions(taskId, manager, store) {
250917
251903
  init_logger();
250918
251904
  import * as fs8 from "node:fs";
250919
251905
  import { createRequire as createRequire7 } from "node:module";
250920
- import { join as join91, dirname as dirname43 } from "node:path";
251906
+ import { join as join92, dirname as dirname43 } from "node:path";
250921
251907
  import { pathToFileURL as pathToFileURL2 } from "node:url";
250922
251908
  var log35 = createLogger("terminal");
250923
251909
  var ptyModule = null;
@@ -250949,16 +251935,16 @@ function findInstalledNodePtyNativeDir() {
250949
251935
  try {
250950
251936
  const umbrellaEntry = require4.resolve("node-pty");
250951
251937
  const umbrellaRoot = dirname43(umbrellaEntry);
250952
- const releaseDir = join91(umbrellaRoot, "build", "Release");
250953
- if (fs8.existsSync(join91(releaseDir, "pty.node"))) return releaseDir;
251938
+ const releaseDir = join92(umbrellaRoot, "build", "Release");
251939
+ if (fs8.existsSync(join92(releaseDir, "pty.node"))) return releaseDir;
250954
251940
  const packageName = nodePtyPlatformPackageName();
250955
251941
  const nativeEntry = nodePtyRequiredNativeEntry();
250956
251942
  if (!packageName || !nativeEntry) return null;
250957
251943
  const platformRequire = createRequire7(umbrellaEntry);
250958
251944
  const platformEntry = platformRequire.resolve(packageName);
250959
251945
  const platformRoot = dirname43(dirname43(platformEntry));
250960
- const prebuildDir = join91(platformRoot, "prebuilds", getNativePrebuildName());
250961
- return fs8.existsSync(join91(prebuildDir, nativeEntry)) ? prebuildDir : null;
251946
+ const prebuildDir = join92(platformRoot, "prebuilds", getNativePrebuildName());
251947
+ return fs8.existsSync(join92(prebuildDir, nativeEntry)) ? prebuildDir : null;
250962
251948
  } catch {
250963
251949
  return null;
250964
251950
  }
@@ -250968,14 +251954,14 @@ function findStagedNativeDir(platform3 = process.platform, arch = process.arch)
250968
251954
  const nativeEntry = nodePtyRequiredNativeEntry(platform3);
250969
251955
  if (!nativeEntry) return null;
250970
251956
  if (process.env.FUSION_RUNTIME_DIR) {
250971
- const envPath = join91(process.env.FUSION_RUNTIME_DIR, prebuildName);
250972
- if (fs8.existsSync(join91(envPath, nativeEntry))) {
251957
+ const envPath = join92(process.env.FUSION_RUNTIME_DIR, prebuildName);
251958
+ if (fs8.existsSync(join92(envPath, nativeEntry))) {
250973
251959
  return envPath;
250974
251960
  }
250975
251961
  }
250976
251962
  const execDir = dirname43(process.execPath);
250977
- const nextToBinary = join91(execDir, "runtime", prebuildName);
250978
- if (fs8.existsSync(join91(nextToBinary, nativeEntry))) {
251963
+ const nextToBinary = join92(execDir, "runtime", prebuildName);
251964
+ if (fs8.existsSync(join92(nextToBinary, nativeEntry))) {
250979
251965
  return nextToBinary;
250980
251966
  }
250981
251967
  return null;
@@ -250998,8 +251984,8 @@ function ensureNodePtyNativePermissions() {
250998
251984
  candidateDirs.add(installedNativeDir);
250999
251985
  }
251000
251986
  for (const nativeDir of candidateDirs) {
251001
- const helperPath = join91(nativeDir, "spawn-helper");
251002
- const nativeModulePath = join91(nativeDir, "pty.node");
251987
+ const helperPath = join92(nativeDir, "spawn-helper");
251988
+ const nativeModulePath = join92(nativeDir, "pty.node");
251003
251989
  try {
251004
251990
  fs8.chmodSync(helperPath, 493);
251005
251991
  } catch {
@@ -251028,7 +252014,7 @@ async function loadPtyModule() {
251028
252014
  process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
251029
252015
  }
251030
252016
  process.env.FUSION_NATIVE_ASSETS_PATH = nativeDir;
251031
- const stagedUmbrellaEntry = join91(nativeDir, "node-pty-umbrella", "index.js");
252017
+ const stagedUmbrellaEntry = join92(nativeDir, "node-pty-umbrella", "index.js");
251032
252018
  if (fs8.existsSync(stagedUmbrellaEntry)) {
251033
252019
  try {
251034
252020
  const stagedImport = await import(pathToFileURL2(stagedUmbrellaEntry).href);
@@ -252557,7 +253543,7 @@ async function runGraphTaskStep(deps, task, stepIndex, instanceId, governingNode
252557
253543
  // ../engine/src/executor/get-authoritative-assigned-agent.ts
252558
253544
  init_src2();
252559
253545
  init_logger();
252560
- import { join as join92 } from "node:path";
253546
+ import { join as join93 } from "node:path";
252561
253547
  async function getAuthoritativeAssignedAgent(deps, assignedAgentId) {
252562
253548
  const normalizedId = assignedAgentId?.trim();
252563
253549
  if (!normalizedId) return null;
@@ -252568,7 +253554,7 @@ async function getAuthoritativeAssignedAgent(deps, assignedAgentId) {
252568
253554
  let authoritativeStore = deps.getAuthoritativeAssignedAgentStore();
252569
253555
  if (!authoritativeStore || authoritativeAgentLayer && !authoritativeStore.backendMode) {
252570
253556
  authoritativeStore = new AgentStore({
252571
- rootDir: join92(deps.rootDir, ".fusion"),
253557
+ rootDir: join93(deps.rootDir, ".fusion"),
252572
253558
  taskStore: deps.store,
252573
253559
  ...authoritativeAgentLayer ? { asyncLayer: authoritativeAgentLayer } : {}
252574
253560
  });
@@ -254057,107 +255043,6 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
254057
255043
  // ../engine/src/executor/create-task-done-tool.ts
254058
255044
  init_src2();
254059
255045
  import { Type as Type8 } from "@earendil-works/pi-ai";
254060
-
254061
- // ../engine/src/execution-block-classifier.ts
254062
- init_usage_limit_detector();
254063
- init_transient_error_detector();
254064
- var HOST_EXTERNAL_CODES = ["ENOSPC", "EDQUOT", "EROFS", "ENOMEM", "EMFILE", "ENFILE"];
254065
- var NETWORK_EXTERNAL_CODES = ["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ENOTFOUND"];
254066
- function classifyExternalObstacle(message) {
254067
- const normalized3 = message.trim();
254068
- if (!normalized3) return void 0;
254069
- if (/Forbidden lifecycle path|rejected by the task lifecycle projection|TransitionRejectionError/i.test(normalized3)) {
254070
- return void 0;
254071
- }
254072
- for (const code of HOST_EXTERNAL_CODES) {
254073
- if (new RegExp(`\\b${code}\\b`, "i").test(normalized3)) return { origin: "host-environment", code };
254074
- }
254075
- for (const code of NETWORK_EXTERNAL_CODES) {
254076
- if (new RegExp(`\\b${code}\\b`, "i").test(normalized3)) return { origin: "network", code };
254077
- }
254078
- if (/socket hang up/i.test(normalized3)) return { origin: "network", code: "SOCKET_HANG_UP" };
254079
- if (isTransientAuthCredentialError(normalized3)) return void 0;
254080
- if (isUsageLimitError(normalized3)) return { origin: "model-provider", code: "USAGE_LIMIT" };
254081
- if (isOperatorActionableAgentError(normalized3)) return { origin: "credentials", code: "CREDENTIALS" };
254082
- return void 0;
254083
- }
254084
- function detectRepairableObstacleHint(message) {
254085
- const normalized3 = message.trim();
254086
- if (!normalized3) return void 0;
254087
- const missingSignal = /\b(?:command not found|not installed|is not recognized as an internal or external command)\b|\bneither\b.{0,80}\binstalled\b/i;
254088
- const missingFileSignal = /(?:\b(?:spawn|exec(?:ute)?|binary|executable|interpreter|command)\b.{0,80}\b(?:ENOENT|no such file or directory)\b|\bENOENT\b.{0,80}\b(?:binary|executable|interpreter|command)\b)/i;
254089
- if (!missingSignal.test(normalized3) && !missingFileSignal.test(normalized3)) return void 0;
254090
- return /\b(?:service|daemon)\b/i.test(normalized3) ? "missing-service" : "missing-tooling";
254091
- }
254092
- var TASK_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/i;
254093
- var LEGACY_PR_REF_RE = /^(?:pr[:#-]|#|PR-)(\d+)$/i;
254094
- function partitionBlockedByRefs(blockedBy) {
254095
- const taskIds = [];
254096
- for (const raw of blockedBy) {
254097
- const id = raw.trim();
254098
- if (!id) continue;
254099
- if (LEGACY_PR_REF_RE.test(id)) continue;
254100
- if (TASK_ID_RE.test(id)) {
254101
- const m2 = id.toUpperCase().match(/^([A-Z][A-Z0-9]*)-(\d+)$/);
254102
- taskIds.push(m2 ? `${m2[1]}-${m2[2]}` : id.toUpperCase());
254103
- }
254104
- }
254105
- return { taskIds: [...new Set(taskIds)] };
254106
- }
254107
- function classifyBlockedExit(_reason, blockedBy = []) {
254108
- const { taskIds } = partitionBlockedByRefs(blockedBy);
254109
- if (taskIds.length > 0) {
254110
- return {
254111
- class: "external",
254112
- thrashSignature: `tasks:${taskIds.slice().sort().join(",")}`
254113
- };
254114
- }
254115
- return {
254116
- class: "plan-defect",
254117
- thrashSignature: "plan-defect"
254118
- };
254119
- }
254120
- var BLOCKED_THRASH_LIMIT = 3;
254121
- var BLOCKED_THRASH_WINDOW_MS = 60 * 60 * 1e3;
254122
- function countBlockedThrashHits(log42, signature, nowMs = Date.now(), windowMs = BLOCKED_THRASH_WINDOW_MS) {
254123
- if (!log42?.length) return 0;
254124
- if (signature === "plan-defect") return 0;
254125
- const cutoff = nowMs - windowMs;
254126
- let count2 = 0;
254127
- for (const entry of log42) {
254128
- const ts4 = entry.timestamp ? Date.parse(entry.timestamp) : Number.NaN;
254129
- if (Number.isFinite(ts4) && ts4 < cutoff) continue;
254130
- const action = entry.action ?? "";
254131
- if (!action.startsWith("BLOCKED:") && !action.includes("DUPLICATE_REPLAN_EXHAUSTED") && !action.includes("BLOCKED_THRASH")) {
254132
- if (!action.includes("parked failed (honest blocked exit") && !action.includes("durable external block")) {
254133
- continue;
254134
- }
254135
- }
254136
- for (const part of signature.split("|")) {
254137
- if (part.startsWith("tasks:") && part.slice(6).split(",").some((id) => id && action.includes(id))) {
254138
- count2 += 1;
254139
- break;
254140
- }
254141
- }
254142
- }
254143
- return count2;
254144
- }
254145
- function isDurableBlockedTask(task) {
254146
- const meta2 = task.sourceMetadata;
254147
- if (!meta2 || typeof meta2 !== "object") return false;
254148
- if (meta2.blockedClass !== "external") return false;
254149
- return task.status === "failed" || task.status === "needs-replan";
254150
- }
254151
- function buildExternalBlockMetadataPatch(classification, thrashCount) {
254152
- return {
254153
- blockedClass: classification.class,
254154
- blockedThrashSignature: classification.thrashSignature,
254155
- blockedThrashCount: thrashCount,
254156
- externalBlockers: []
254157
- };
254158
- }
254159
-
254160
- // ../engine/src/executor/create-task-done-tool.ts
254161
255046
  init_effective_settings();
254162
255047
  init_run_audit();
254163
255048
  init_logger();
@@ -254231,7 +255116,13 @@ function createTaskDoneTool(deps, taskId, worktreePath, promptContent, codeRevie
254231
255116
  )),
254232
255117
  reason: Type8.Optional(Type8.String({
254233
255118
  description: 'Required when outcome="blocked": concrete explanation of what is blocking the work and what is needed to unblock it.'
254234
- }))
255119
+ })),
255120
+ blockedReport: Type8.Optional(Type8.Object({
255121
+ verifiedCondition: Type8.Optional(Type8.String()),
255122
+ stopReason: Type8.Optional(Type8.String()),
255123
+ unimplementedWork: Type8.Optional(Type8.String()),
255124
+ unblockCondition: Type8.Optional(Type8.String())
255125
+ }, { description: "Optional safe operator report for an accepted outside-worktree block. State what was verified, why work stopped, what remains undone, and the precise unblock condition. Never include prompts, logs, stack traces, or credentials." }))
254235
255126
  }),
254236
255127
  execute: async (_id, params) => {
254237
255128
  if (params.outcome === "blocked") {
@@ -254282,6 +255173,7 @@ function createTaskDoneTool(deps, taskId, worktreePath, promptContent, codeRevie
254282
255173
  const externalBlock = {
254283
255174
  ...classifiedObstacle,
254284
255175
  message: reason,
255176
+ report: buildTaskExternalBlockReport(classifiedObstacle, params.blockedReport),
254285
255177
  source: "agent-declaration",
254286
255178
  blockedAt: (/* @__PURE__ */ new Date()).toISOString(),
254287
255179
  resume: {
@@ -254484,9 +255376,6 @@ function createTaskDoneTool(deps, taskId, worktreePath, promptContent, codeRevie
254484
255376
  ...taintUpdate,
254485
255377
  paused: false,
254486
255378
  pausedByAgentId: null,
254487
- worktree: null,
254488
- branch: null,
254489
- branchWriteOrigin: "engine",
254490
255379
  sessionFile: null
254491
255380
  });
254492
255381
  await store.logEntry(
@@ -254504,9 +255393,6 @@ function createTaskDoneTool(deps, taskId, worktreePath, promptContent, codeRevie
254504
255393
  ...taintUpdate,
254505
255394
  paused: false,
254506
255395
  pausedByAgentId: null,
254507
- worktree: null,
254508
- branch: null,
254509
- branchWriteOrigin: "engine",
254510
255396
  sessionFile: null
254511
255397
  });
254512
255398
  await store.logEntry(taskId, `${refusalMessage} \u2014 fn_task_done refusal retry budget exhausted`, void 0, deps.getRunContextFor(task.id));
@@ -254839,46 +255725,6 @@ async function isReentrantPausedAbortedInFlightNode(deps, live, result, abortPro
254839
255725
  return live.column === resumeLanes.hold || live.column === resumeLanes.review || live.column === resumeLanes.wip;
254840
255726
  }
254841
255727
 
254842
- // ../engine/src/executor/route-graph-failure-to-execution-resume.ts
254843
- init_src2();
254844
- init_logger();
254845
- init_lifecycle_columns();
254846
- async function routeGraphFailureToExecutionResume(deps, live, failedNode, failureValue, resumeLanesMemo) {
254847
- if (live.deletedAt) return false;
254848
- if (isTaskExternallyBlocked(live)) {
254849
- executorLog.log(`${live.id}: graph failure resume skipped \u2014 external Blocked freeze honored`);
254850
- return false;
254851
- }
254852
- if (live.paused || live.userPaused === true) return false;
254853
- if ((await resolveTerminalColumnsFor(deps.store, live.id)).includes(live.column)) return false;
254854
- if (isDurableBlockedTask(live)) {
254855
- executorLog.log(
254856
- `${live.id}: graph failure resume skipped \u2014 durable BLOCKED park honored (task-dependency block)`
254857
- );
254858
- return false;
254859
- }
254860
- if (failedNode === COMPLETION_SUMMARY_NODE_ID) return false;
254861
- const incompleteSteps = hasNonTerminalWorkflowSteps(live);
254862
- if (!incompleteSteps && (failureValue === "remediation-not-scheduled" || failureValue === "missing-remediation-context") && await deps.isRemediationGraphNode(live.id, failedNode)) return false;
254863
- const implementationIncompleteMergeFailure = isMergeGraphFailure(failedNode) && failureValue === "implementation-incomplete";
254864
- if (implementationIncompleteMergeFailure && !incompleteSteps) return false;
254865
- const resumeRouterLanes = await deps.resolveResumeLanes(live.id, resumeLanesMemo);
254866
- if (!resumeRouterLanes.wipDeclared) return false;
254867
- if (live.column !== resumeRouterLanes.wip || !implementationIncompleteMergeFailure) {
254868
- const message2 = `Workflow graph failed at node '${failedNode}'${failureValue ? ` (${failureValue})` : ""} \u2014 automatic recovery cannot move '${live.column}' backward; card remains in place`;
254869
- executorLog.warn(`${live.id}: ${message2}`);
254870
- await deps.store.logEntry(live.id, message2, void 0, deps.getRunContextFor(live.id));
254871
- return false;
254872
- }
254873
- const message = `Workflow graph failed at node '${failedNode}'${failureValue ? ` (${failureValue})` : ""} with incomplete work \u2014 resuming in place in '${live.column}'`;
254874
- executorLog.warn(`${live.id}: ${message}`);
254875
- await deps.store.logEntry(live.id, message, void 0, deps.getRunContextFor(live.id));
254876
- await deps.store.updateTask(live.id, { status: null, error: null }, deps.getRunContextFor(live.id));
254877
- await deps.clearTerminalStepFailuresForRetry(live.id, "archive");
254878
- await deps.persistTokenUsage(live.id);
254879
- return true;
254880
- }
254881
-
254882
255728
  // ../engine/src/executor/reenter-paused-aborted-workflow-node.ts
254883
255729
  init_run_audit();
254884
255730
  init_logger();
@@ -255141,6 +255987,7 @@ async function handleStaleInReviewParsePauseAbortReplay(deps, live, result, abor
255141
255987
  }
255142
255988
 
255143
255989
  // ../engine/src/executor/route-graph-merge-failure-to-retry.ts
255990
+ init_src2();
255144
255991
  init_logger();
255145
255992
  init_workflow_merge_nodes();
255146
255993
 
@@ -255233,11 +256080,23 @@ async function routeGraphMergeFailureToRetry(deps, live, result, abortProvenance
255233
256080
  throw firstBoundaryError;
255234
256081
  }
255235
256082
  if (mergeBoundary.blocked) {
255236
- const { reason: reason2, code, missingInstanceCount } = mergeBoundary.blocked;
256083
+ const { reason: reason2, code, missingInstanceCount, evidence } = mergeBoundary.blocked;
256084
+ if (await deps.routeGraphFailureToExecutionResume?.(live, failedNode, MERGE_BOUNDARY_RECOVERY_VALUE, void 0, evidence)) {
256085
+ await deps.store.logEntry(live.id, `Workflow merge boundary retry scheduled evidence recovery: ${reason2}`, void 0, deps.getRunContextFor(live.id));
256086
+ await persistTokenUsageBestEffort(deps.persistTokenUsage, live.id);
256087
+ return true;
256088
+ }
256089
+ const settings = typeof deps.store.getSettings === "function" ? await deps.store.getSettings().catch(() => void 0) : void 0;
256090
+ const current = typeof deps.store.getTask === "function" ? await deps.store.getTask(live.id).catch(() => void 0) : live;
256091
+ if (!current || current.deletedAt || current.paused || current.userPaused || current.autoMerge === false || settings && !allowsAutoMergeProcessing(current, settings)) {
256092
+ await deps.store.logEntry(live.id, `Workflow merge boundary retry is held for human control: ${reason2}`, void 0, deps.getRunContextFor(live.id));
256093
+ await persistTokenUsageBestEffort(deps.persistTokenUsage, live.id);
256094
+ return true;
256095
+ }
255237
256096
  await deps.store.logEntry(live.id, `Workflow merge boundary retry parked task: ${reason2}`, void 0, deps.getRunContextFor(live.id));
255238
256097
  let parked2 = false;
255239
- await deps.store.updateTaskAtomic(live.id, (current) => {
255240
- if (!current || current.deletedAt || current.status != null || current.paused || current.userPaused || typeof live.columnMovedAt === "string" && typeof current.columnMovedAt === "string" && current.columnMovedAt !== live.columnMovedAt) return null;
256098
+ await deps.store.updateTaskAtomic(live.id, (current2) => {
256099
+ if (!current2 || current2.deletedAt || current2.status != null || current2.paused || current2.userPaused || current2.autoMerge === false || settings && !allowsAutoMergeProcessing(current2, settings) || typeof live.columnMovedAt === "string" && typeof current2.columnMovedAt === "string" && current2.columnMovedAt !== live.columnMovedAt) return null;
255241
256100
  parked2 = true;
255242
256101
  return {
255243
256102
  status: "failed",
@@ -255298,6 +256157,10 @@ async function routeGraphMergeFailureToRetry(deps, live, result, abortProvenance
255298
256157
  return true;
255299
256158
  }
255300
256159
  const reason = mergeRequestRejection instanceof Error ? mergeRequestRejection.message : String(mergeRequestRejection);
256160
+ if (reason.endsWith(PRE_MERGE_STEPS_NOT_RUN_BLOCKER)) {
256161
+ await persistTokenUsageBestEffort(deps.persistTokenUsage, live.id);
256162
+ return true;
256163
+ }
255301
256164
  try {
255302
256165
  await deps.store.logEntry(
255303
256166
  live.id,
@@ -255321,7 +256184,8 @@ async function routeGraphMergeFailureToRetry(deps, live, result, abortProvenance
255321
256184
  // ../engine/src/executor/route-implementation-incomplete-merge-graph-failure.ts
255322
256185
  init_logger();
255323
256186
  init_lifecycle_columns();
255324
- async function routeImplementationIncompleteMergeGraphFailure(deps, live, failedNode) {
256187
+ init_workflow_merge_nodes();
256188
+ async function routeImplementationIncompleteMergeGraphFailure(deps, live, failedNode, failureValue = "implementation-incomplete", boundaryEvidence) {
255325
256189
  deps.clearPausedAborted(live.id);
255326
256190
  let resumeLive = live;
255327
256191
  if (live.paused === true && live.userPaused !== true) {
@@ -255331,7 +256195,7 @@ async function routeImplementationIncompleteMergeGraphFailure(deps, live, failed
255331
256195
  }, deps.getRunContextFor(live.id));
255332
256196
  resumeLive = { ...live, paused: false, pausedReason: void 0 };
255333
256197
  }
255334
- if (hasNonTerminalWorkflowSteps(resumeLive) && await deps.routeGraphFailureToExecutionResume(resumeLive, failedNode, "implementation-incomplete")) {
256198
+ if ((hasNonTerminalWorkflowSteps(resumeLive) || failureValue === MERGE_BOUNDARY_RECOVERY_VALUE) && await deps.routeGraphFailureToExecutionResume(resumeLive, failedNode, failureValue, void 0, boundaryEvidence)) {
255335
256199
  return true;
255336
256200
  }
255337
256201
  deps.activeWorktrees.delete(live.id);
@@ -255839,7 +256703,15 @@ function createAuthoritativeWorkflowPrimitivesFromExecutor(deps, settings, outpu
255839
256703
  runId: ctx.run.runId
255840
256704
  });
255841
256705
  if (mergeBoundary.blocked) {
255842
- return { outcome: "failure", value: MERGE_BOUNDARY_UNPROVEN_VALUE };
256706
+ return {
256707
+ outcome: "failure",
256708
+ value: MERGE_BOUNDARY_RECOVERY_VALUE,
256709
+ contextPatch: {
256710
+ "workflow:merge-boundary-recovery-code": mergeBoundary.blocked.evidence.code,
256711
+ "workflow:merge-boundary-non-terminal-node-id": mergeBoundary.blocked.evidence.nonTerminalNodeId,
256712
+ "workflow:merge-boundary-missing-instance-ids": mergeBoundary.blocked.evidence.missingInstanceIds
256713
+ }
256714
+ };
255843
256715
  }
255844
256716
  const mergeTask = mergeBoundary.task;
255845
256717
  const missingImplementationProof = await deps.getWorkflowMergeImplementationProofFailure(mergeTask);
@@ -256397,7 +257269,7 @@ Repository scope (task-level; review this plan once): ${detail.repositoryScope?.
256397
257269
  init_src2();
256398
257270
  init_logger();
256399
257271
  import { existsSync as existsSync81 } from "node:fs";
256400
- import { join as join93 } from "node:path";
257272
+ import { join as join94 } from "node:path";
256401
257273
 
256402
257274
  // ../engine/src/executor/workflow-step-recommendations.ts
256403
257275
  init_src2();
@@ -256485,7 +257357,7 @@ function resolveGraphNodeSessionBoundary(input) {
256485
257357
  if (!input.isWorkspace || input.legacyWorkspaceLayout) return void 0;
256486
257358
  const repoRoots = (input.confirmedRepositories ?? []).map((repoRelPath) => ({
256487
257359
  repoRelPath,
256488
- repoRootDir: join93(input.rootDir, repoRelPath)
257360
+ repoRootDir: join94(input.rootDir, repoRelPath)
256489
257361
  }));
256490
257362
  if (repoRoots.length === 0) return void 0;
256491
257363
  return {
@@ -256928,7 +257800,7 @@ ${prompt}`;
256928
257800
  const legacyWorkspaceLayout = isLegacyWorkspaceWorktreeLayout(workspaceReviewTarget, workspaceTaskDir2);
256929
257801
  const scopedRepoRoots = workspaceReviewTarget.repositoryScope.repositories.map((repoRelPath) => ({
256930
257802
  repoRelPath,
256931
- repoRootDir: join93(deps.rootDir, repoRelPath)
257803
+ repoRootDir: join94(deps.rootDir, repoRelPath)
256932
257804
  }));
256933
257805
  let aggregate = await reviewWorkspacePerRepo(workspaceReviewTarget, async (repoWorktreePath) => {
256934
257806
  const repoRelPath = workspaceConfig.repos.find(
@@ -256937,7 +257809,7 @@ ${prompt}`;
256937
257809
  const reviewBoundary = legacyWorkspaceLayout ? {
256938
257810
  kind: "task-worktree",
256939
257811
  writableRoot: repoWorktreePath,
256940
- projectRoot: repoRelPath ? join93(deps.rootDir, repoRelPath) : deps.rootDir
257812
+ projectRoot: repoRelPath ? join94(deps.rootDir, repoRelPath) : deps.rootDir
256941
257813
  } : {
256942
257814
  kind: "workspace-task-dir",
256943
257815
  writableRoot: workspaceTaskDir2,
@@ -257117,8 +257989,8 @@ ${prompt}`;
257117
257989
 
257118
257990
  // ../engine/src/executor/handle-graph-failure.ts
257119
257991
  init_src2();
257120
- import { join as join94 } from "node:path";
257121
- import { readFile as readFile50, rm as rm16, writeFile as writeFile29 } from "node:fs/promises";
257992
+ import { join as join95 } from "node:path";
257993
+ import { readFile as readFile51, rm as rm16, writeFile as writeFile29 } from "node:fs/promises";
257122
257994
  init_spec_staleness();
257123
257995
  init_logger();
257124
257996
  init_run_audit();
@@ -257492,10 +258364,18 @@ async function handleGraphFailure(deps, task, result) {
257492
258364
  const failedNode = result.visitedNodeIds[result.visitedNodeIds.length - 1];
257493
258365
  const mergeGraphFailure = isMergeGraphFailure(failedNode);
257494
258366
  const failureValue = graphFailureValue(result);
258367
+ const recoveryCode = result.context?.["workflow:merge-boundary-recovery-code"];
258368
+ const recoveryMissingIds = result.context?.["workflow:merge-boundary-missing-instance-ids"];
258369
+ const recoveryNonTerminalNodeId = result.context?.["workflow:merge-boundary-non-terminal-node-id"];
258370
+ const boundaryEvidence = failureValue === MERGE_BOUNDARY_RECOVERY_VALUE && (recoveryCode === "no-node-result" || recoveryCode === "non-terminal-node-result" || recoveryCode === "missing-foreach-instances") && Array.isArray(recoveryMissingIds) && recoveryMissingIds.every((id) => typeof id === "string") ? {
258371
+ code: recoveryCode,
258372
+ missingInstanceIds: recoveryMissingIds,
258373
+ nonTerminalNodeId: typeof recoveryNonTerminalNodeId === "string" ? recoveryNonTerminalNodeId : void 0
258374
+ } : void 0;
257495
258375
  if (!live.paused && !live.userPaused && !live.deletedAt && typeof failedNode === "string" && (failedNode === "parse" || failedNode.endsWith(":parse") || failedNode.includes("parse-steps") || failureValue === "parse-error" || failureValue === "missing-implementation-steps")) {
257496
258376
  try {
257497
- const tasksDir = typeof deps.store.getTasksDir === "function" ? deps.store.getTasksDir() : join94(deps.rootDir, ".fusion", "tasks");
257498
- const promptContent = await readFile50(getPromptPath(tasksDir, live.id), "utf-8").catch(() => "");
258377
+ const tasksDir = typeof deps.store.getTasksDir === "function" ? deps.store.getTasksDir() : join95(deps.rootDir, ".fusion", "tasks");
258378
+ const promptContent = await readFile51(getPromptPath(tasksDir, live.id), "utf-8").catch(() => "");
257499
258379
  const redirectReason = nonExecutableDuplicateRedirectReason(promptContent, live.title);
257500
258380
  if (redirectReason) {
257501
258381
  const duplicateResolution = resolveExplicitDuplicateMarker(promptContent, live.title);
@@ -257599,8 +258479,8 @@ async function handleGraphFailure(deps, task, result) {
257599
258479
  await deps.persistTokenUsage(task.id);
257600
258480
  return;
257601
258481
  }
257602
- if (mergeGraphFailure && failureValue === "implementation-incomplete") {
257603
- if (await deps.routeImplementationIncompleteMergeGraphFailure(live, failedNode ?? "unknown")) {
258482
+ if (mergeGraphFailure && (failureValue === "implementation-incomplete" || failureValue === MERGE_BOUNDARY_RECOVERY_VALUE)) {
258483
+ if (await deps.routeImplementationIncompleteMergeGraphFailure(live, failedNode ?? "unknown", failureValue, boundaryEvidence)) {
257604
258484
  return;
257605
258485
  }
257606
258486
  }
@@ -257862,8 +258742,8 @@ async function handleGraphFailure(deps, task, result) {
257862
258742
  if (!parked) {
257863
258743
  const capturedRunId = deps.getRunContextFor(task.id)?.runId;
257864
258744
  const capturedColumnMovedAt = live.columnMovedAt;
257865
- const tasksDir = typeof deps.store.getTasksDir === "function" ? deps.store.getTasksDir() : join94(deps.rootDir, ".fusion", "tasks");
257866
- const deferredParkIntentPath = join94(tasksDir, task.id, "deferred-terminal-park.json");
258745
+ const tasksDir = typeof deps.store.getTasksDir === "function" ? deps.store.getTasksDir() : join95(deps.rootDir, ".fusion", "tasks");
258746
+ const deferredParkIntentPath = join95(tasksDir, task.id, "deferred-terminal-park.json");
257867
258747
  const intentWrite = writeFile29(deferredParkIntentPath, JSON.stringify({ message, writtenAt: (/* @__PURE__ */ new Date()).toISOString(), columnMovedAt: capturedColumnMovedAt }), "utf-8").catch((intentError) => {
257868
258748
  executorLog.warn(`${task.id}: failed to persist deferred terminal-park intent: ${intentError instanceof Error ? intentError.message : String(intentError)}`);
257869
258749
  });
@@ -258129,7 +259009,7 @@ init_src2();
258129
259009
  import { exec as exec56 } from "node:child_process";
258130
259010
  import { promisify as promisify78 } from "node:util";
258131
259011
  import { existsSync as existsSync83 } from "node:fs";
258132
- import { join as join96 } from "node:path";
259012
+ import { join as join97 } from "node:path";
258133
259013
  import { SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
258134
259014
  init_agent_tools();
258135
259015
  init_agent_instructions();
@@ -258175,7 +259055,7 @@ init_src2();
258175
259055
  init_logger();
258176
259056
  init_verification_concurrency();
258177
259057
  import { existsSync as existsSync82, readdirSync as readdirSync13, readFileSync as readFileSync28 } from "node:fs";
258178
- import { isAbsolute as isAbsolute24, join as join95, relative as relative22 } from "node:path";
259058
+ import { isAbsolute as isAbsolute24, join as join96, relative as relative22 } from "node:path";
258179
259059
  import { Type as Type10 } from "@earendil-works/pi-ai";
258180
259060
  var MAX_OUTPUT_BYTES = 200 * 1024;
258181
259061
  var VERIFICATION_FAILURE_SUMMARY_MAX_CHARS = 8e3;
@@ -258303,7 +259183,7 @@ function findWorkspacePackageDir(rootDir, packageName) {
258303
259183
  for (const candidate of candidates) {
258304
259184
  if (!candidate) continue;
258305
259185
  try {
258306
- const pkg = JSON.parse(readFileSync28(join95(rootDir, candidate, "package.json"), "utf8"));
259186
+ const pkg = JSON.parse(readFileSync28(join96(rootDir, candidate, "package.json"), "utf8"));
258307
259187
  if (pkg.name === packageName) {
258308
259188
  packageDirCache.set(cacheKey, candidate);
258309
259189
  return candidate;
@@ -258318,7 +259198,7 @@ function findWorkspacePackageDir(rootDir, packageName) {
258318
259198
  while (queue.length > 0) {
258319
259199
  const current = queue.shift();
258320
259200
  if (!current) break;
258321
- const abs = join95(rootDir, current.dir);
259201
+ const abs = join96(rootDir, current.dir);
258322
259202
  let entries;
258323
259203
  try {
258324
259204
  entries = readdirSync13(abs, { withFileTypes: true });
@@ -258328,9 +259208,9 @@ function findWorkspacePackageDir(rootDir, packageName) {
258328
259208
  for (const entry of entries) {
258329
259209
  if (!entry.isDirectory()) continue;
258330
259210
  if (entry.name === "node_modules" || entry.name === "dist") continue;
258331
- const child = join95(current.dir, entry.name);
259211
+ const child = join96(current.dir, entry.name);
258332
259212
  try {
258333
- const pkg = JSON.parse(readFileSync28(join95(rootDir, child, "package.json"), "utf8"));
259213
+ const pkg = JSON.parse(readFileSync28(join96(rootDir, child, "package.json"), "utf8"));
258334
259214
  if (pkg.name === packageName) {
258335
259215
  packageDirCache.set(cacheKey, child);
258336
259216
  return child;
@@ -258350,7 +259230,7 @@ function toPackageRelativeFilter(token, rootDir, packageDir) {
258350
259230
  return normalized3.slice(normalizedPackageDir.length + 1);
258351
259231
  }
258352
259232
  if (isAbsolute24(token)) {
258353
- const rel = relative22(join95(rootDir, packageDir), token).replace(/\\/g, "/");
259233
+ const rel = relative22(join96(rootDir, packageDir), token).replace(/\\/g, "/");
258354
259234
  if (!rel.startsWith("../") && rel !== "..") return rel;
258355
259235
  }
258356
259236
  return token;
@@ -258804,7 +259684,7 @@ function createRunVerificationTool(opts) {
258804
259684
  if (params.cwd && isAbsolute24(params.cwd)) {
258805
259685
  resolvedCwd = params.cwd;
258806
259686
  } else if (params.cwd) {
258807
- resolvedCwd = join95(selectedRepo?.worktreePath ?? worktreePath, params.cwd);
259687
+ resolvedCwd = join96(selectedRepo?.worktreePath ?? worktreePath, params.cwd);
258808
259688
  } else {
258809
259689
  resolvedCwd = selectedRepo?.worktreePath ?? worktreePath;
258810
259690
  }
@@ -258838,7 +259718,7 @@ function createRunVerificationTool(opts) {
258838
259718
  warnings.push(...normalized3.warnings);
258839
259719
  }
258840
259720
  if (effectiveCommand.trimStart().startsWith("pnpm --filter")) {
258841
- const modulesYaml = join95(verificationRootDir, "node_modules", ".modules.yaml");
259721
+ const modulesYaml = join96(verificationRootDir, "node_modules", ".modules.yaml");
258842
259722
  if (!existsSync82(modulesYaml)) {
258843
259723
  const installCmd = "pnpm install --prefer-offline";
258844
259724
  const msg = `node_modules/.modules.yaml not found in workspace root \u2014 auto-prepending \`${installCmd}\` before running the command.`;
@@ -259160,6 +260040,7 @@ async function parkExternalSessionObstacle(deps, taskId, errorMessage3) {
259160
260040
  const externalBlock = {
259161
260041
  ...obstacle,
259162
260042
  message: errorMessage3,
260043
+ report: buildTaskExternalBlockReport(obstacle),
259163
260044
  source: "session-failure",
259164
260045
  blockedAt: (/* @__PURE__ */ new Date()).toISOString(),
259165
260046
  resume: {
@@ -259287,7 +260168,7 @@ async function runImplementation(deps, task, graphCompletion, reportImplementati
259287
260168
  const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
259288
260169
  const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
259289
260170
  if (!isActiveTask) {
259290
- const tasksDir = join96(deps.store.getFusionDir(), "tasks");
260171
+ const tasksDir = join97(deps.store.getFusionDir(), "tasks");
259291
260172
  const promptPath = getPromptPath(tasksDir, task.id);
259292
260173
  const staleness = await evaluateSpecStaleness({
259293
260174
  settings,
@@ -259479,14 +260360,14 @@ async function runImplementation(deps, task, graphCompletion, reportImplementati
259479
260360
  return {
259480
260361
  kind: "task-worktree",
259481
260362
  writableRoot: worktreePath,
259482
- projectRoot: repoRelPath ? join96(deps.rootDir, repoRelPath) : deps.rootDir
260363
+ projectRoot: repoRelPath ? join97(deps.rootDir, repoRelPath) : deps.rootDir
259483
260364
  };
259484
260365
  }
259485
260366
  return {
259486
260367
  kind: "workspace-task-dir",
259487
260368
  writableRoot: worktreePath,
259488
260369
  projectRoot: deps.rootDir,
259489
- repoRoots: task.repositoryScope?.state === "confirmed" ? task.repositoryScope.repositories.map((repoRelPath) => ({ repoRelPath, repoRootDir: join96(deps.rootDir, repoRelPath) })) : []
260370
+ repoRoots: task.repositoryScope?.state === "confirmed" ? task.repositoryScope.repositories.map((repoRelPath) => ({ repoRelPath, repoRootDir: join97(deps.rootDir, repoRelPath) })) : []
259490
260371
  };
259491
260372
  })() : {
259492
260373
  kind: "task-worktree",
@@ -262133,59 +263014,6 @@ async function terminateChildAgent(deps, childId) {
262133
263014
  deps.setTotalSpawnedCount(Math.max(0, deps.totalSpawnedCount - 1));
262134
263015
  }
262135
263016
 
262136
- // ../engine/src/executor/evaluate-workflow-merge-boundary.ts
262137
- init_src2();
262138
- async function evaluateWorkflowMergeBoundary(deps, task, runId) {
262139
- const relevant = (task.workflowStepResults ?? []).filter(
262140
- (result) => result.source === "node" && (result.phase ?? "pre-merge") === "pre-merge"
262141
- );
262142
- const hasRelevantNodeResult = relevant.length > 0;
262143
- const nonTerminalResult = relevant.find((result) => result.status !== "passed" && result.status !== "skipped");
262144
- const allResultsTerminal = nonTerminalResult === void 0;
262145
- let ir2;
262146
- try {
262147
- ir2 = await resolveWorkflowIrForTask(deps.store, task.id);
262148
- } catch {
262149
- }
262150
- if (!ir2) return { resolved: false, hasRelevantNodeResult, allResultsTerminal, coverageComplete: true, hasLiveStepImplementationProof: false, hasForeachStepExecute: false, missingInstanceIds: [], nonTerminalResult, complete: false };
262151
- let persistedInstances = [];
262152
- try {
262153
- persistedInstances = await deps.loadMergeBoundaryInstances(task.id, runId);
262154
- } catch {
262155
- }
262156
- const coverage = evaluateForeachMergeProof({ ir: ir2, steps: task.steps, workflowStepResults: task.workflowStepResults, persistedInstances });
262157
- const coverageComplete = coverage.missingInstanceIds.length === 0;
262158
- const hasLiveStepImplementationProof = coverage.expectedInstanceIds.length > 0 && coverage.expectedInstanceIds.every((id) => coverage.liveStepSatisfiedInstanceIds.includes(id));
262159
- const complete = allResultsTerminal && coverageComplete && (hasRelevantNodeResult || hasLiveStepImplementationProof);
262160
- return { resolved: true, hasRelevantNodeResult, allResultsTerminal, coverageComplete, hasLiveStepImplementationProof, hasForeachStepExecute: coverage.hasForeachStepExecute, missingInstanceIds: coverage.missingInstanceIds, nonTerminalResult, complete };
262161
- }
262162
- async function getWorkflowMergeImplementationProofFailure(deps, task) {
262163
- const taint = evaluateSkipBypassTaint(task);
262164
- if (taint.blocked) return "implementation did not run: steps were skipped after a bulk-step-completion refusal without an accepted fn_task_done";
262165
- if (task.noCommitsExpected === true) return void 0;
262166
- let ir2;
262167
- try {
262168
- ir2 = await resolveWorkflowIrForTask(deps.store, task.id);
262169
- } catch {
262170
- ir2 = void 0;
262171
- }
262172
- if (!ir2) return void 0;
262173
- const usesParsedSteps = ir2.nodes.some((node) => node.kind === "parse-steps");
262174
- const usesExecuteSeam = ir2.nodes.some((node) => node.kind === "prompt" && node.config?.seam === "execute");
262175
- if (!usesParsedSteps && !usesExecuteSeam) return void 0;
262176
- const steps = Array.isArray(task.steps) ? task.steps : [];
262177
- const hasTerminalParsedSteps = steps.length > 0 && steps.every((step) => step.status === "done" || step.status === "skipped");
262178
- const hasModifiedFiles = (task.modifiedFiles?.length ?? 0) > 0;
262179
- const proof = await deps.evaluateWorkflowMergeBoundary(task);
262180
- const hasGraphNativeImplementationProof = proof.hasRelevantNodeResult && proof.allResultsTerminal && proof.coverageComplete;
262181
- if (usesParsedSteps) {
262182
- if (hasTerminalParsedSteps || hasGraphNativeImplementationProof) return void 0;
262183
- return proof.hasForeachStepExecute && !proof.coverageComplete ? `implementation did not run: foreach step instances are incomplete (missing ${proof.missingInstanceIds.join(", ")})` : "implementation did not run: parsed coding steps are missing or incomplete";
262184
- }
262185
- if (usesExecuteSeam) return hasTerminalParsedSteps || hasModifiedFiles || hasGraphNativeImplementationProof ? void 0 : "implementation did not run: execute seam has no completion proof";
262186
- return void 0;
262187
- }
262188
-
262189
263017
  // ../engine/src/executor/renew-task-lease.ts
262190
263018
  async function renewTaskLease(deps, taskId, agentId, leaseEpoch, nodeId, runId) {
262191
263019
  const renewedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -262242,37 +263070,6 @@ async function getExecutionPauseLabel(deps) {
262242
263070
  return null;
262243
263071
  }
262244
263072
 
262245
- // ../engine/src/executor/workflow-merge-boundary-helpers.ts
262246
- init_src2();
262247
- async function resolveMergeBoundaryColumn(deps, taskId, nodeId) {
262248
- try {
262249
- const ir2 = await resolveWorkflowIrForTask(deps.store, taskId);
262250
- const named = ir2.nodes.find((n) => n.id === nodeId);
262251
- if (named && MERGE_REGION_KINDS.has(named.kind) && named.column) return named.column;
262252
- const mergeNode = ir2.nodes.find((n) => MERGE_REGION_KINDS.has(n.kind) && n.column);
262253
- if (mergeNode?.column) return mergeNode.column;
262254
- return "in-review";
262255
- } catch {
262256
- return "in-review";
262257
- }
262258
- }
262259
- async function loadMergeBoundaryInstances(deps, taskId, runId) {
262260
- if (!runId) return [];
262261
- const store = deps.store;
262262
- try {
262263
- return await store.loadWorkflowRunStepInstancesAsync?.(taskId, runId) ?? store.loadWorkflowRunStepInstances?.(taskId, runId) ?? [];
262264
- } catch {
262265
- return [];
262266
- }
262267
- }
262268
- function shouldCompleteChecklistAtWorkflowMerge(task, proof) {
262269
- if (!Array.isArray(task.steps) || task.steps.length === 0) return false;
262270
- if (task.steps.every((step) => step.status === "done" || step.status === "skipped")) return false;
262271
- if (proof) return proof.complete;
262272
- const graphNodeResults = (task.workflowStepResults ?? []).filter((result) => result.source === "node" && (result.phase ?? "pre-merge") === "pre-merge");
262273
- return graphNodeResults.length > 0 && graphNodeResults.every((result) => result.status === "passed" || result.status === "skipped");
262274
- }
262275
-
262276
263073
  // ../engine/src/executor/mark-paused-aborted.ts
262277
263074
  function markPausedAborted(deps, taskId, provenance = "hard-cancel", source = "unspecified", options = {}) {
262278
263075
  const previousProvenance = deps.pausedAbortProvenance.get(taskId);
@@ -263397,6 +264194,7 @@ function buildRouteGraphFailureToExecutionResumeDeps(host) {
263397
264194
  ...facadeMethods(host, [
263398
264195
  "getRunContextFor",
263399
264196
  "resolveResumeLanes",
264197
+ "isLiveSharedBranchGroupMember",
263400
264198
  "clearTerminalStepFailuresForRetry",
263401
264199
  "persistTokenUsage",
263402
264200
  // FNXC:WorkflowRemediation 2026-08-09-21:41: FN-8910 completed-review park for refused remediation.
@@ -263533,7 +264331,7 @@ function buildRouteGraphMergeFailureToRetryDeps(host) {
263533
264331
  return {
263534
264332
  ...buildStoreRunContextDeps(host),
263535
264333
  mergeRequester: host.mergeRequester,
263536
- ...facadeMethods(host, ["ensureWorkflowMergeBoundaryTask", "persistTokenUsage"])
264334
+ ...facadeMethods(host, ["ensureWorkflowMergeBoundaryTask", "persistTokenUsage", "routeGraphFailureToExecutionResume"])
263537
264335
  };
263538
264336
  }
263539
264337
  function buildRouteImplementationIncompleteMergeGraphFailureDeps(host) {
@@ -264251,10 +265049,10 @@ init_logger();
264251
265049
 
264252
265050
  // ../engine/src/review-artifacts/feature-video.ts
264253
265051
  init_src2();
264254
- import { access as access10, mkdtemp as mkdtemp4, readFile as readFile51, rm as rm17 } from "node:fs/promises";
265052
+ import { access as access10, mkdtemp as mkdtemp4, readFile as readFile52, rm as rm17 } from "node:fs/promises";
264255
265053
  import { constants as constants2 } from "node:fs";
264256
265054
  import { tmpdir as tmpdir11 } from "node:os";
264257
- import { join as join97 } from "node:path";
265055
+ import { join as join98 } from "node:path";
264258
265056
  var SCENARIO_DOCUMENT_KEY = "review-artifact-scenario";
264259
265057
  var MAX_DURATION_MS = 15e3;
264260
265058
  var DEFAULT_DURATION_MS = 3e3;
@@ -264285,7 +265083,7 @@ async function generateFeatureVideo(options) {
264285
265083
  let browser;
264286
265084
  let context;
264287
265085
  try {
264288
- recordingDir = await mkdtemp4(join97(tmpdir11(), "fusion-feature-video-"));
265086
+ recordingDir = await mkdtemp4(join98(tmpdir11(), "fusion-feature-video-"));
264289
265087
  try {
264290
265088
  browser = await client.launch({ executablePath, headless: true });
264291
265089
  } catch {
@@ -264303,7 +265101,7 @@ async function generateFeatureVideo(options) {
264303
265101
  await (options.sleep ?? defaultSleep)(Math.min(Math.max(options.durationMs ?? DEFAULT_DURATION_MS, 0), MAX_DURATION_MS));
264304
265102
  await context.close();
264305
265103
  context = void 0;
264306
- const data = await readFile51(await video.path());
265104
+ const data = await readFile52(await video.path());
264307
265105
  if (data.byteLength > (options.maxBytes ?? DEFAULT_MAX_BYTES2)) {
264308
265106
  return { status: "failed", reason: "size-cap-exceeded" };
264309
265107
  }
@@ -274535,7 +275333,7 @@ init_effective_settings();
274535
275333
  // ../engine/src/spec-validation/task-document-references.ts
274536
275334
  init_src2();
274537
275335
  import { access as access11 } from "node:fs/promises";
274538
- import { join as join98 } from "node:path";
275336
+ import { join as join99 } from "node:path";
274539
275337
  var TASK_PATH_REGEX = /\.fusion\/tasks\/[A-Z0-9]+(?:-[A-Z0-9]+)*\/[^\s`'"\]]+/g;
274540
275338
  function normalizePathToken(token) {
274541
275339
  return token.replace(/^[`([{"']+/, "").replace(/[`),.;:!?\]}'"]+$/, "");
@@ -274623,7 +275421,7 @@ async function detectDanglingTaskDocReferences(promptContent, opts) {
274623
275421
  }
274624
275422
  const dangling = [];
274625
275423
  for (const [path6, sections2] of references.entries()) {
274626
- const absPath = join98(opts.rootDir, path6);
275424
+ const absPath = join99(opts.rootDir, path6);
274627
275425
  if (await existsImpl(absPath)) continue;
274628
275426
  dangling.push({ path: path6, sections: Array.from(sections2).sort() });
274629
275427
  }
@@ -274721,8 +275519,8 @@ init_recovery_policy();
274721
275519
  init_agent_tools();
274722
275520
  import { execFile as execFile36 } from "node:child_process";
274723
275521
  import { existsSync as existsSync87 } from "node:fs";
274724
- import { readFile as readFile52, writeFile as writeFile30, rm as rm18 } from "node:fs/promises";
274725
- import { join as join99 } from "node:path";
275522
+ import { readFile as readFile53, writeFile as writeFile30, rm as rm18 } from "node:fs/promises";
275523
+ import { join as join100 } from "node:path";
274726
275524
  import { promisify as promisify83 } from "node:util";
274727
275525
 
274728
275526
  // ../engine/src/triage-domain/triage-preflight.ts
@@ -275734,8 +276532,8 @@ var TriageProcessor = class _TriageProcessor {
275734
276532
  }
275735
276533
  const settings = await mergeEffectiveSettings(this.store, task, await this.store.getSettings());
275736
276534
  const approvalRequired = resolvePlanApprovalRequired(settings);
275737
- const promptPath = join99(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
275738
- const written = await readFile52(promptPath, "utf-8").catch((err) => {
276535
+ const promptPath = join100(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
276536
+ const written = await readFile53(promptPath, "utf-8").catch((err) => {
275739
276537
  const msg = err instanceof Error ? err.message : String(err);
275740
276538
  planLog.warn(`${task.id}: failed to read PROMPT.md during planning recovery (${promptPath}): ${msg}`);
275741
276539
  return "";
@@ -275773,8 +276571,8 @@ var TriageProcessor = class _TriageProcessor {
275773
276571
  return report2.outcome !== "withheld";
275774
276572
  }
275775
276573
  async readNonEmptyPromptDraft(taskId, context) {
275776
- const promptPath = join99(this.rootDir, ".fusion", "tasks", taskId, "PROMPT.md");
275777
- const written = await readFile52(promptPath, "utf-8").catch((err) => {
276574
+ const promptPath = join100(this.rootDir, ".fusion", "tasks", taskId, "PROMPT.md");
276575
+ const written = await readFile53(promptPath, "utf-8").catch((err) => {
275778
276576
  const msg = err instanceof Error ? err.message : String(err);
275779
276577
  const code = err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
275780
276578
  if (code === "ENOENT") {
@@ -276012,8 +276810,8 @@ var TriageProcessor = class _TriageProcessor {
276012
276810
  continue;
276013
276811
  }
276014
276812
  try {
276015
- const promptPath = join99(this.rootDir, ".fusion", "tasks", todoTask.id, "PROMPT.md");
276016
- const content = await readFile52(promptPath, "utf-8");
276813
+ const promptPath = join100(this.rootDir, ".fusion", "tasks", todoTask.id, "PROMPT.md");
276814
+ const content = await readFile53(promptPath, "utf-8");
276017
276815
  if (isTaskAwaitingPlanning(todoTask, content)) {
276018
276816
  eligibleTodoTasks.push(todoTask);
276019
276817
  }
@@ -276396,8 +277194,8 @@ var TriageProcessor = class _TriageProcessor {
276396
277194
  try {
276397
277195
  const liveTask = await this.store.getTask(task.id).catch(() => null);
276398
277196
  if (!liveTask || liveTask.paused === true || liveTask.userPaused === true) return false;
276399
- const promptPath = join99(this.rootDir, ".fusion", "tasks", liveTask.id, "PROMPT.md");
276400
- const written = await readFile52(promptPath, "utf-8").catch(() => "");
277197
+ const promptPath = join100(this.rootDir, ".fusion", "tasks", liveTask.id, "PROMPT.md");
277198
+ const written = await readFile53(promptPath, "utf-8").catch(() => "");
276401
277199
  const duplicateResolution = resolveExplicitDuplicateMarker(written, liveTask.title);
276402
277200
  if (!duplicateResolution.marker && !duplicateResolution.conflict) return false;
276403
277201
  const settings = await mergeEffectiveSettings(this.store, liveTask, await this.store.getSettings());
@@ -276799,8 +277597,8 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
276799
277597
  pluginContributions: triagePluginContributions
276800
277598
  });
276801
277599
  const triageSystemPromptFinal = collapsePromptLayers(triageLayers);
276802
- const authoritativePromptPath = join99(this.rootDir, promptPath);
276803
- const planningAttemptBaseline = existsSync87(authoritativePromptPath) ? await readFile52(authoritativePromptPath, "utf-8").catch(() => void 0) : void 0;
277600
+ const authoritativePromptPath = join100(this.rootDir, promptPath);
277601
+ const planningAttemptBaseline = existsSync87(authoritativePromptPath) ? await readFile53(authoritativePromptPath, "utf-8").catch(() => void 0) : void 0;
276804
277602
  const planningAttempt = {
276805
277603
  id: `${task.id}:${++this.planningAttemptSequence}`,
276806
277604
  baseline: planningAttemptBaseline,
@@ -276929,7 +277727,11 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
276929
277727
  extraCommands: [
276930
277728
  ...extractCommandBinaries(settings?.testCommand),
276931
277729
  ...extractCommandBinaries(settings?.buildCommand)
276932
- ]
277730
+ ],
277731
+ testCommand: settings?.testCommand,
277732
+ buildCommand: settings?.buildCommand,
277733
+ rootDir: this.rootDir,
277734
+ projectId: this.store.getProjectId?.() ?? this.rootDir
276933
277735
  }).catch(() => ({ capabilities: [], degraded: true }));
276934
277736
  const agentPrompt = buildSpecificationPrompt(
276935
277737
  detail,
@@ -277040,8 +277842,8 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
277040
277842
  "Recovered the plan written inside the task worktree into the project .fusion folder"
277041
277843
  ).catch(() => void 0);
277042
277844
  }
277043
- let written = await readFile52(
277044
- join99(this.rootDir, promptPath),
277845
+ let written = await readFile53(
277846
+ join100(this.rootDir, promptPath),
277045
277847
  "utf-8"
277046
277848
  ).catch((err) => {
277047
277849
  const msg = err instanceof Error ? err.message : String(err);
@@ -277921,7 +278723,7 @@ ${lines.join("\n")}` }],
277921
278723
  async clearDuplicateMarkerForReplan(task, canonicalId, feedback, options) {
277922
278724
  if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
277923
278725
  if (options?.source !== "title") {
277924
- await rm18(join99(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
278726
+ await rm18(join100(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
277925
278727
  }
277926
278728
  if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === canonicalId) {
277927
278729
  await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${canonicalId}` });
@@ -278089,7 +278891,7 @@ ${lines.join("\n")}` }],
278089
278891
  let nextPrompt = applyOriginalDescription(written, originalDescription);
278090
278892
  nextPrompt = applyFrontendUxCriteria(nextPrompt, parsedFileScope);
278091
278893
  if (nextPrompt !== written) {
278092
- const promptPath = join99(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
278894
+ const promptPath = join100(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
278093
278895
  try {
278094
278896
  if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
278095
278897
  await writeFile30(promptPath, nextPrompt, "utf-8");
@@ -278424,10 +279226,10 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
278424
279226
  if (!attachments || attachments.length === 0) {
278425
279227
  return { attachmentContents, imageContents };
278426
279228
  }
278427
- const { readFile: readFile54 } = await import("node:fs/promises");
278428
- const { join: join103 } = await import("node:path");
279229
+ const { readFile: readFile55 } = await import("node:fs/promises");
279230
+ const { join: join104 } = await import("node:path");
278429
279231
  for (const att of attachments) {
278430
- const filePath = join103(
279232
+ const filePath = join104(
278431
279233
  rootDir,
278432
279234
  ".fusion",
278433
279235
  "tasks",
@@ -278437,7 +279239,7 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
278437
279239
  );
278438
279240
  try {
278439
279241
  if (IMAGE_MIME_TYPES.has(att.mimeType)) {
278440
- const data = await readFile54(filePath);
279242
+ const data = await readFile55(filePath);
278441
279243
  const detectedMimeType = detectImageMimeFromBytes(data);
278442
279244
  const imageMimeType = detectedMimeType ?? att.mimeType;
278443
279245
  if (detectedMimeType && detectedMimeType !== att.mimeType) {
@@ -278454,7 +279256,7 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
278454
279256
  text: null
278455
279257
  });
278456
279258
  } else {
278457
- const data = await readFile54(filePath, "utf-8");
279259
+ const data = await readFile55(filePath, "utf-8");
278458
279260
  const text2 = data.length > TEXT_INLINE_LIMIT ? data.slice(0, TEXT_INLINE_LIMIT) + "\n... (truncated at 50KB)" : data;
278459
279261
  attachmentContents.push({
278460
279262
  originalName: att.originalName,
@@ -282249,7 +283051,7 @@ function extractAdvisorAssistantText(session) {
282249
283051
  // ../engine/src/overseer/overseer-watchdog.ts
282250
283052
  init_logger();
282251
283053
  import { existsSync as existsSync88, readFileSync as readFileSync29 } from "node:fs";
282252
- import { dirname as dirname44, join as join100, relative as relative23, resolve as resolve53, sep as sep15 } from "node:path";
283054
+ import { dirname as dirname44, join as join101, relative as relative23, resolve as resolve53, sep as sep15 } from "node:path";
282253
283055
  import { homedir as homedir13 } from "node:os";
282254
283056
  var log39 = createLogger("overseer-watchdog");
282255
283057
  var WATCHDOG_FILENAMES = ["OVERSEER.md", "WATCHDOG.md"];
@@ -282280,7 +283082,7 @@ function discoverOverseerWatchdogFiles(options) {
282280
283082
  };
282281
283083
  if (agentDir) {
282282
283084
  for (const name2 of WATCHDOG_FILENAMES) {
282283
- tryAdd(join100(agentDir, name2), "user", 999);
283085
+ tryAdd(join101(agentDir, name2), "user", 999);
282284
283086
  }
282285
283087
  }
282286
283088
  let current = cwd;
@@ -282288,9 +283090,9 @@ function discoverOverseerWatchdogFiles(options) {
282288
283090
  for (let hop = 0; hop < 64; hop++) {
282289
283091
  const depth = relative23(cwd, current) === "" ? 0 : relative23(cwd, current).split(sep15).filter(Boolean).length;
282290
283092
  for (const name2 of WATCHDOG_FILENAMES) {
282291
- tryAdd(join100(current, name2), "project", depth);
282292
- tryAdd(join100(current, ".fusion", name2), "project", depth);
282293
- tryAdd(join100(current, ".omp", name2), "project", depth);
283093
+ tryAdd(join101(current, name2), "project", depth);
283094
+ tryAdd(join101(current, ".fusion", name2), "project", depth);
283095
+ tryAdd(join101(current, ".omp", name2), "project", depth);
282294
283096
  }
282295
283097
  if (current === stopAt) break;
282296
283098
  const parent = dirname44(current);
@@ -283728,7 +284530,7 @@ import { promisify as promisify88 } from "node:util";
283728
284530
  import { realpathSync as realpathSync15, readdirSync as readdirSync15 } from "node:fs";
283729
284531
  import { mkdtemp as mkdtemp5 } from "node:fs/promises";
283730
284532
  import { tmpdir as tmpdir13 } from "node:os";
283731
- import { join as join102 } from "node:path";
284533
+ import { join as join103 } from "node:path";
283732
284534
  init_workspace_base_branch();
283733
284535
  init_merger_ref_update_advance();
283734
284536
 
@@ -284199,7 +285001,7 @@ import { execFile as execFile40 } from "node:child_process";
284199
285001
  import { appendFileSync as appendFileSync2, existsSync as existsSync90, mkdirSync as mkdirSync13, readdirSync as readdirSync14, readFileSync as readFileSync31, realpathSync as realpathSync14, rmSync as rmSync9, statSync as statSync18 } from "node:fs";
284200
285002
  import { rm as rm19 } from "node:fs/promises";
284201
285003
  import { tmpdir as tmpdir12 } from "node:os";
284202
- import { isAbsolute as isAbsolute27, join as join101, relative as relative24 } from "node:path";
285004
+ import { isAbsolute as isAbsolute27, join as join102, relative as relative24 } from "node:path";
284203
285005
  import { promisify as promisify87 } from "node:util";
284204
285006
  init_worktree_paths();
284205
285007
  init_worktree_removal_retry();
@@ -284230,7 +285032,7 @@ function isBenignAbsentWorktreeError(err) {
284230
285032
  return isBenignAbsentRemovalError(err);
284231
285033
  }
284232
285034
  function ensureAiMergeRootIgnored(projectRootDir, settings) {
284233
- const excludePath = join101(projectRootDir, ".git", "info", "exclude");
285035
+ const excludePath = join102(projectRootDir, ".git", "info", "exclude");
284234
285036
  if (!existsSync90(excludePath)) return;
284235
285037
  try {
284236
285038
  const current = readFileSync31(excludePath, "utf-8");
@@ -284255,7 +285057,7 @@ function getAiMergeTempSearchRoots(projectRootDir, settings) {
284255
285057
  if (testWorkerRoot) {
284256
285058
  try {
284257
285059
  for (const entry of readdirSync14(testWorkerRoot)) {
284258
- if (entry.startsWith("redir-")) roots.push(join101(testWorkerRoot, entry));
285060
+ if (entry.startsWith("redir-")) roots.push(join102(testWorkerRoot, entry));
284259
285061
  }
284260
285062
  } catch {
284261
285063
  }
@@ -284278,7 +285080,7 @@ async function pruneExistingAiMergeWorktrees(taskId, projectRootDir, audit, log4
284278
285080
  continue;
284279
285081
  }
284280
285082
  for (const entry of entries) {
284281
- const candidatePath = join101(tempRoot, entry);
285083
+ const candidatePath = join102(tempRoot, entry);
284282
285084
  let canonicalPath = candidatePath;
284283
285085
  try {
284284
285086
  canonicalPath = realpathSync14(candidatePath);
@@ -284815,7 +285617,7 @@ function listAiMergeWorktreeCandidates(taskId, projectRootDir, settings) {
284815
285617
  if (testWorkerRoot) {
284816
285618
  try {
284817
285619
  for (const entry of readdirSync15(testWorkerRoot)) {
284818
- if (entry.startsWith("redir-")) roots.push(join102(testWorkerRoot, entry));
285620
+ if (entry.startsWith("redir-")) roots.push(join103(testWorkerRoot, entry));
284819
285621
  }
284820
285622
  } catch {
284821
285623
  }
@@ -284828,7 +285630,7 @@ function listAiMergeWorktreeCandidates(taskId, projectRootDir, settings) {
284828
285630
  } catch {
284829
285631
  continue;
284830
285632
  }
284831
- for (const entry of entries) candidates.push(join102(root, entry));
285633
+ for (const entry of entries) candidates.push(join103(root, entry));
284832
285634
  }
284833
285635
  return candidates;
284834
285636
  }
@@ -285265,7 +286067,7 @@ async function landOneRepo(repoRootDir, branch, integrationBranch, ctx) {
285265
286067
  registeredMergePaths.add(pathToRegister);
285266
286068
  };
285267
286069
  try {
285268
- mergeRoot = await mkdtemp5(join102(resolveAiMergeRoot(repoRootDir, settings), `fusion-ai-merge-${taskId.toLowerCase()}-`));
286070
+ mergeRoot = await mkdtemp5(join103(resolveAiMergeRoot(repoRootDir, settings), `fusion-ai-merge-${taskId.toLowerCase()}-`));
285269
286071
  registerMergeRoot(mergeRoot);
285270
286072
  await git9(["worktree", "add", "--detach", mergeRoot, tipSha], repoRootDir);
285271
286073
  worktreeAdded = true;
@@ -286227,7 +287029,7 @@ async function landWorkspaceTask(store, task, workspaceRootDir, options = {}, de
286227
287029
  for (const repoRel of repoKeys) {
286228
287030
  const entry = workspaceWorktrees[repoRel];
286229
287031
  if (!entry) throw new WorkspaceMergeTechnicalError("workspace-entry", `Workspace repository entry is missing for ${repoRel}`);
286230
- const repoRootDir = join102(workspaceRootDir, repoRel);
287032
+ const repoRootDir = join103(workspaceRootDir, repoRel);
286231
287033
  try {
286232
287034
  const baseResolution = await resolveWorkspaceRepoBaseBranch({
286233
287035
  mode: "recorded",
@@ -286285,7 +287087,7 @@ async function landWorkspaceTask(store, task, workspaceRootDir, options = {}, de
286285
287087
  handle: options.workspaceDispatchFence,
286286
287088
  claimOutcome: "reentrant",
286287
287089
  remote: target.remote,
286288
- cwd: join102(workspaceRootDir, repoRel),
287090
+ cwd: join103(workspaceRootDir, repoRel),
286289
287091
  fenceRefName: mergeDispatchFenceRef(taskId)
286290
287092
  });
286291
287093
  options.workspaceDispatchFence = ensuredDispatchFence;
@@ -286316,7 +287118,7 @@ async function landWorkspaceTask(store, task, workspaceRootDir, options = {}, de
286316
287118
  for (const repoRel of repoKeys) {
286317
287119
  throwIfAborted3(options.signal, taskId);
286318
287120
  const entry = workspaceWorktrees[repoRel];
286319
- const repoRootDir = join102(workspaceRootDir, repoRel);
287121
+ const repoRootDir = join103(workspaceRootDir, repoRel);
286320
287122
  const workspaceTarget = workspaceTargets.get(repoRel);
286321
287123
  if (!workspaceTarget) throw new WorkspaceMergeTechnicalError("integration-target", `Workspace integration plan is missing for ${repoRel}`);
286322
287124
  const integrationBranch = workspaceTarget.integrationBranch;
@@ -286976,7 +287778,7 @@ async function pushAfterMergeToRemote(input) {
286976
287778
  let worktreeAdded = false;
286977
287779
  const registeredPaths = /* @__PURE__ */ new Set();
286978
287780
  try {
286979
- pushRoot = await mkdtemp5(join102(resolveAiMergeRoot(projectRootDir, settings), `fusion-ai-merge-push-${taskId.toLowerCase()}-`));
287781
+ pushRoot = await mkdtemp5(join103(resolveAiMergeRoot(projectRootDir, settings), `fusion-ai-merge-push-${taskId.toLowerCase()}-`));
286980
287782
  for (const p2 of [pushRoot]) {
286981
287783
  activeSessionRegistry.registerPath(p2, { taskId, kind: "ai-merge", ownerKey: `ai-merge-push:${taskId}` });
286982
287784
  registeredPaths.add(p2);
@@ -290338,10 +291140,20 @@ ${detail}`);
290338
291140
  return false;
290339
291141
  }
290340
291142
  this.plannerLiveRetrySkipLogDedup.delete(`${task.id}::${decision.watchedStage ?? "executor"}`);
290341
- await moveTaskToContainedBackwardTarget(store, task.id, "self-healing-stranded-recovery", {
291143
+ const recovery = await moveTaskToContainedBackwardTarget(store, task.id, "self-healing-stranded-recovery", {
290342
291144
  preserveProgress: true,
290343
291145
  moveSource: "engine"
290344
291146
  }, task.column);
291147
+ if (!recovery.moved) {
291148
+ if (!("reason" in recovery) || recovery.reason !== "in-place-recovery" || task.status !== "failed" || (await this.resolveTaskColumnFlags(store, task, /* @__PURE__ */ new Map()))?.countsTowardWip !== true) return false;
291149
+ let resumed = false;
291150
+ await store.updateTaskAtomic(task.id, (current) => {
291151
+ if (current.column !== task.column || current.status !== "failed" || current.error !== task.error || current.updatedAt !== task.updatedAt || current.paused || current.userPaused || current.deletedAt || executor?.isTaskLiveForOverseerRetry?.(task.id) === true) return null;
291152
+ resumed = true;
291153
+ return { status: "queued", error: null, sessionFile: null };
291154
+ });
291155
+ if (!resumed) return false;
291156
+ }
290345
291157
  await this.emitOverseerInterventionSafe(
290346
291158
  () => emitOverseerRetry({
290347
291159
  store,