@runfusion/fusion 0.75.1-beta.0 → 0.75.1-beta.2

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 (136) hide show
  1. package/dist/bin.js +523 -212
  2. package/dist/child-process-worker.js +323 -99
  3. package/dist/client/.vite/manifest.json +249 -249
  4. package/dist/client/assets/{AgentDetailView-swTdiyeE.js → AgentDetailView-ltOG8f0-.js} +1 -1
  5. package/dist/client/assets/{AgentPermissionPolicyEditor-Dq-KuDaA.js → AgentPermissionPolicyEditor-CC_rrbbG.js} +1 -1
  6. package/dist/client/assets/{AgentsView-D1GEHyH0.js → AgentsView-DWUdi27N.js} +3 -3
  7. package/dist/client/assets/{ChatView-D1czGvvT.js → ChatView-mFKfoegS.js} +1 -1
  8. package/dist/client/assets/{CommandCenter-DWZ7OTOB.js → CommandCenter-C2wvDq_N.js} +1 -1
  9. package/dist/client/assets/{DevServerView-CAcUKOXd.js → DevServerView-0MWEJM7o.js} +1 -1
  10. package/dist/client/assets/{DirectoryPicker-DVUl5jOk.js → DirectoryPicker-BjMPhCdh.js} +1 -1
  11. package/dist/client/assets/{DocumentsView-CnexOxtr.js → DocumentsView-ckH3rjmd.js} +1 -1
  12. package/dist/client/assets/{EvalsView-rTupzcDf.js → EvalsView-NyA644VF.js} +1 -1
  13. package/dist/client/assets/{ExperimentalAgentOnboardingModal-CXE6Bj3V.js → ExperimentalAgentOnboardingModal-D0ZfbBRN.js} +1 -1
  14. package/dist/client/assets/{GoalsView-CjHwnnIi.js → GoalsView-Ctt5P_yo.js} +1 -1
  15. package/dist/client/assets/{InsightsView-C3XocYP-.js → InsightsView-NDp0bdNB.js} +1 -1
  16. package/dist/client/assets/{MemoryView-CsNHcx_d.js → MemoryView-BKbA4Bm4.js} +1 -1
  17. package/dist/client/assets/{PiExtensionsManager-BPTPoZBI.js → PiExtensionsManager-V3AFWMHm.js} +1 -1
  18. package/dist/client/assets/{PluginManager-CXadn8JE.js → PluginManager-D88G_TZh.js} +1 -1
  19. package/dist/client/assets/{PullRequestView-CC03CtZJ.js → PullRequestView-DQQmFFYC.js} +1 -1
  20. package/dist/client/assets/{ReportModal-Ci0na1eI.js → ReportModal-Clh1S8RF.js} +1 -1
  21. package/dist/client/assets/{ResearchView-DGWyAo8p.js → ResearchView-kxvq4zp5.js} +1 -1
  22. package/dist/client/assets/SecretsView-Dm0Vk8SA.js +1 -0
  23. package/dist/client/assets/{SessionTerminal-B22RlQeo.js → SessionTerminal-DJO4JTEX.js} +1 -1
  24. package/dist/client/assets/{SettingsModal-BkgBQZ19.js → SettingsModal-BN5mL_ym.js} +1 -1
  25. package/dist/client/assets/SettingsModal-CTf5voEW.js +21 -0
  26. package/dist/client/assets/{SettingsTextareaRow-0LXwMUQR.js → SettingsTextareaRow-CvKszqO-.js} +1 -1
  27. package/dist/client/assets/{SetupWizardModal-CyW3AlRO.js → SetupWizardModal-D5hVZh1G.js} +2 -2
  28. package/dist/client/assets/{SkillsView-3oqB1ubq.js → SkillsView-DQVz21w2.js} +1 -1
  29. package/dist/client/assets/{WorkflowNodeEditor-CfUK_xs1.js → WorkflowNodeEditor-CpKIi6Tb.js} +3 -3
  30. package/dist/client/assets/{agent-import-generation-C7-Cmo3M.js → agent-import-generation-DGUNF3nd.js} +1 -1
  31. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BQ7ZN1r_.js → architectureDiagram-3BPJPVTR-CV83wubP.js} +1 -1
  32. package/dist/client/assets/{blockDiagram-GPEHLZMM-Debw1Xka.js → blockDiagram-GPEHLZMM-CNQsA3dD.js} +1 -1
  33. package/dist/client/assets/{c4Diagram-AAUBKEIU-CiU212s3.js → c4Diagram-AAUBKEIU-CXe7ii5H.js} +1 -1
  34. package/dist/client/assets/channel-DoX862zl.js +1 -0
  35. package/dist/client/assets/{chunk-2J33WTMH-BUJQ86zb.js → chunk-2J33WTMH-BS6W9bWv.js} +1 -1
  36. package/dist/client/assets/{chunk-4BX2VUAB-C6GdEm1z.js → chunk-4BX2VUAB-DbehRhdW.js} +1 -1
  37. package/dist/client/assets/{chunk-55IACEB6-CImvrxQL.js → chunk-55IACEB6-DMI5SR42.js} +1 -1
  38. package/dist/client/assets/{chunk-727SXJPM-Dt-A3pkM.js → chunk-727SXJPM-D1d-GBVj.js} +1 -1
  39. package/dist/client/assets/{chunk-AQP2D5EJ-DDj4oXTx.js → chunk-AQP2D5EJ-BzO6K3Uf.js} +1 -1
  40. package/dist/client/assets/{chunk-FMBD7UC4-BD4r1ukC.js → chunk-FMBD7UC4-BVRLV3lK.js} +1 -1
  41. package/dist/client/assets/{chunk-ND2GUHAM-DFSteefi.js → chunk-ND2GUHAM-DdWByQfn.js} +1 -1
  42. package/dist/client/assets/{chunk-QZHKN3VN-t2zF8tjB.js → chunk-QZHKN3VN-1ia12cRV.js} +1 -1
  43. package/dist/client/assets/classDiagram-4FO5ZUOK-mb2IeAGN.js +1 -0
  44. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-mb2IeAGN.js +1 -0
  45. package/dist/client/assets/{cose-bilkent-S5V4N54A-tkvDkXQA.js → cose-bilkent-S5V4N54A-BPDT41HR.js} +1 -1
  46. package/dist/client/assets/{dagre-BM42HDAG-DD2rW7wo.js → dagre-BM42HDAG-Cktn26EH.js} +1 -1
  47. package/dist/client/assets/{dashboard-view-B1vduc8x.js → dashboard-view-8o0EUcRX.js} +1 -1
  48. package/dist/client/assets/{dashboard-view-CciuDS5A.js → dashboard-view-CkCerlVJ.js} +1 -1
  49. package/dist/client/assets/{dashboard-view-DruCruAc.js → dashboard-view-D_HC4lnr.js} +1 -1
  50. package/dist/client/assets/{dashboard-view-C7jJ1lA4.js → dashboard-view-zd8GxC49.js} +1 -1
  51. package/dist/client/assets/{diagram-2AECGRRQ-DASuxGNc.js → diagram-2AECGRRQ-But4Lvf-.js} +1 -1
  52. package/dist/client/assets/{diagram-5GNKFQAL-f9WtiIUk.js → diagram-5GNKFQAL-2bpQAGFc.js} +1 -1
  53. package/dist/client/assets/{diagram-KO2AKTUF-QivICYCd.js → diagram-KO2AKTUF-Ckn9Qd1u.js} +1 -1
  54. package/dist/client/assets/{diagram-LMA3HP47-CMwcDFHR.js → diagram-LMA3HP47-Bjn8h84N.js} +1 -1
  55. package/dist/client/assets/{diagram-OG6HWLK6-C-vPMeHZ.js → diagram-OG6HWLK6-BI6541hx.js} +1 -1
  56. package/dist/client/assets/{erDiagram-TEJ5UH35-CxGtk9vj.js → erDiagram-TEJ5UH35-BqMiwHiM.js} +1 -1
  57. package/dist/client/assets/{flowDiagram-I6XJVG4X-qBZRZoV1.js → flowDiagram-I6XJVG4X-Q_SAB10M.js} +1 -1
  58. package/dist/client/assets/{folder-open-Dysi9Zzg.js → folder-open-CHiAJbTN.js} +1 -1
  59. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DAaXroiu.js → ganttDiagram-6RSMTGT7-CqWFSfPF.js} +1 -1
  60. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DNUNQot_.js → gitGraphDiagram-PVQCEYII-B1o4uEjO.js} +1 -1
  61. package/dist/client/assets/index-Bdw5eNKV.css +1 -0
  62. package/dist/client/assets/index-kwRywd9M.js +2767 -0
  63. package/dist/client/assets/{infoDiagram-5YYISTIA-BSJmWLwh.js → infoDiagram-5YYISTIA-BzaMa0c2.js} +1 -1
  64. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-AyVKvoDq.js → ishikawaDiagram-YF4QCWOH-I38yge00.js} +1 -1
  65. package/dist/client/assets/{journeyDiagram-JHISSGLW-972txzzd.js → journeyDiagram-JHISSGLW-WVQSgx4P.js} +1 -1
  66. package/dist/client/assets/{kanban-definition-UN3LZRKU-1xiNvIqK.js → kanban-definition-UN3LZRKU-BWtPQ3AH.js} +1 -1
  67. package/dist/client/assets/{mermaid.core-oeDA8jOm.js → mermaid.core-D1hnBtbi.js} +4 -4
  68. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DcktmI8s.js → mindmap-definition-RKZ34NQL-DqiSLkW7.js} +1 -1
  69. package/dist/client/assets/{pieDiagram-4H26LBE5-ekBwLt-O.js → pieDiagram-4H26LBE5-CcgwbdIN.js} +1 -1
  70. package/dist/client/assets/{puzzle-I6pr6ZrW.js → puzzle-BhXUaTQl.js} +1 -1
  71. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D_wjTsvn.js → quadrantDiagram-W4KKPZXB-MsqQQfDN.js} +1 -1
  72. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CKGHyiJ1.js → requirementDiagram-4Y6WPE33-CILGjMMA.js} +1 -1
  73. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CtiZFNk5.js → sankeyDiagram-5OEKKPKP-c14fWbtR.js} +1 -1
  74. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B6OMrfGk.js → sequenceDiagram-3UESZ5HK-BD5bxMom.js} +1 -1
  75. package/dist/client/assets/{shield-alert-CVMmviMG.js → shield-alert-C9W2WywS.js} +1 -1
  76. package/dist/client/assets/{standing-instructions-template-Cg1a3tjW.js → standing-instructions-template-CJdbDSyg.js} +1 -1
  77. package/dist/client/assets/{stateDiagram-AJRCARHV-CyTY2M_7.js → stateDiagram-AJRCARHV-CZ5YqeOn.js} +1 -1
  78. package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-NWPCCiqC.js → stateDiagram-v2-BHNVJYJU-mdMN-PNX.js} +1 -1
  79. package/dist/client/assets/{timeline-definition-PNZ67QCA-DU4TVYpw.js → timeline-definition-PNZ67QCA-DHKay9uj.js} +1 -1
  80. package/dist/client/assets/{upload-VCzeCiFH.js → upload-DxEwowRU.js} +1 -1
  81. package/dist/client/assets/{users-DB3knZtm.js → users-mNFgW_Zd.js} +1 -1
  82. package/dist/client/assets/{vennDiagram-CIIHVFJN-DX0Ppll6.js → vennDiagram-CIIHVFJN-X29K9hWM.js} +1 -1
  83. package/dist/client/assets/{wardley-L42UT6IY-CBoWxpDv.js → wardley-L42UT6IY-yEoTrfmN.js} +1 -1
  84. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-B_eVqdhC.js → wardleyDiagram-YWT4CUSO-ny4k3kG0.js} +1 -1
  85. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DeZzEX0f.js → xychartDiagram-2RQKCTM6-DC8ODtRJ.js} +1 -1
  86. package/dist/client/index.html +2 -2
  87. package/dist/client/version.json +1 -1
  88. package/dist/droid-cli/package.json +1 -1
  89. package/dist/extension.js +411 -155
  90. package/dist/pi-claude-cli/package.json +1 -1
  91. package/dist/plugin-sdk/index.d.ts +159 -141
  92. package/dist/plugin-sdk/index.js +19 -0
  93. package/dist/plugins/fusion-plugin-acp-runtime/bundled.js +19 -0
  94. package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
  95. package/dist/plugins/fusion-plugin-claude-runtime/bundled.js +19 -0
  96. package/dist/plugins/fusion-plugin-claude-runtime/package.json +1 -1
  97. package/dist/plugins/fusion-plugin-cli-printing-press/bundled.js +19 -0
  98. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  99. package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +19 -0
  100. package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
  101. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +19 -0
  102. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  103. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +19 -0
  104. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  105. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +19 -0
  106. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  107. package/dist/plugins/fusion-plugin-grok-runtime/bundled.js +19 -0
  108. package/dist/plugins/fusion-plugin-grok-runtime/package.json +1 -1
  109. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +19 -0
  110. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  111. package/dist/plugins/fusion-plugin-linear-import/bundled.js +19 -0
  112. package/dist/plugins/fusion-plugin-linear-import/package.json +1 -1
  113. package/dist/plugins/fusion-plugin-omp-runtime/bundled.js +19 -0
  114. package/dist/plugins/fusion-plugin-omp-runtime/package.json +1 -1
  115. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +19 -0
  116. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  117. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +19 -0
  118. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  119. package/dist/plugins/fusion-plugin-quality/bundled.js +19 -0
  120. package/dist/plugins/fusion-plugin-quality/package.json +1 -1
  121. package/dist/plugins/fusion-plugin-reports/bundled.js +19 -0
  122. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  123. package/dist/plugins/fusion-plugin-roadmap/bundled.js +19 -0
  124. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  125. package/dist/plugins/fusion-plugin-todos/bundled.js +19 -0
  126. package/dist/plugins/fusion-plugin-todos/package.json +1 -1
  127. package/dist/plugins/fusion-plugin-whatsapp-chat/bundled.js +19 -0
  128. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  129. package/package.json +1 -1
  130. package/dist/client/assets/SecretsView-Cv2IuqFO.js +0 -1
  131. package/dist/client/assets/SettingsModal-CPRpRK71.js +0 -21
  132. package/dist/client/assets/channel-D-O4mRXx.js +0 -1
  133. package/dist/client/assets/classDiagram-4FO5ZUOK-BhHdSetA.js +0 -1
  134. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BhHdSetA.js +0 -1
  135. package/dist/client/assets/index-D0QgE5F5.js +0 -2767
  136. package/dist/client/assets/index-DHk_s6rA.css +0 -1
@@ -1181,6 +1181,9 @@ function isFusionSiblingBranch(branch) {
1181
1181
  function resolveEffectiveAutoMerge(task, settings) {
1182
1182
  return task.autoMerge ?? settings.autoMerge;
1183
1183
  }
1184
+ function hasUserAutoMergeHold(task) {
1185
+ return task.autoMerge === false && task.autoMergeProvenance === "user";
1186
+ }
1184
1187
  function allowsAutoMergeProcessing(task, settings) {
1185
1188
  return (settings.autoMerge !== false || task.autoMerge === true) && !taskHasManualOpenPullRequest(task);
1186
1189
  }
@@ -10046,6 +10049,7 @@ function validateForeach(node, topLevelNodeIds, columnIds) {
10046
10049
  );
10047
10050
  }
10048
10051
  for (const inner of templateNodes) {
10052
+ validateReviewKind(inner, "nested");
10049
10053
  if (inner.kind === "foreach" || inner.kind === "loop") {
10050
10054
  throw new WorkflowIrError(
10051
10055
  `foreach node '${node.id}' template may not contain nested loop/foreach ('${inner.id}')`
@@ -10164,6 +10168,7 @@ function validateLoop(node, topLevelNodeIds, columnIds) {
10164
10168
  );
10165
10169
  }
10166
10170
  for (const inner of templateNodes) {
10171
+ validateReviewKind(inner, "nested");
10167
10172
  if (inner.kind === "loop" || inner.kind === "foreach") {
10168
10173
  throw new WorkflowIrError(
10169
10174
  `loop node '${node.id}' template may not contain nested loop/foreach ('${inner.id}')`
@@ -10257,6 +10262,7 @@ function validateOptionalGroup(node, topLevelNodeIds, columnIds) {
10257
10262
  throw new WorkflowIrError(`optional-group node '${node.id}' template has duplicate node ids`);
10258
10263
  }
10259
10264
  for (const inner of templateNodes) {
10265
+ validateReviewKind(inner, "nested");
10260
10266
  if (inner.kind === "loop" || inner.kind === "foreach" || inner.kind === "optional-group") {
10261
10267
  throw new WorkflowIrError(
10262
10268
  `optional-group node '${node.id}' template may not contain nested loop/foreach/optional-group ('${inner.id}')`
@@ -10928,6 +10934,19 @@ function validateColumnRecovery(column) {
10928
10934
  );
10929
10935
  }
10930
10936
  }
10937
+ function validateReviewKind(node, placement) {
10938
+ const value = node.config?.reviewKind;
10939
+ if (value === void 0) return;
10940
+ if (value !== "plan" && value !== "code") {
10941
+ throw new WorkflowIrError(`Workflow node '${node.id}' has invalid reviewKind '${String(value)}'; expected 'plan' or 'code'`);
10942
+ }
10943
+ if (placement === "nested") {
10944
+ throw new WorkflowIrError(`Workflow node '${node.id}' has reviewKind in an unsupported nested template placement`);
10945
+ }
10946
+ if (node.kind !== "prompt" && node.kind !== "gate" && node.kind !== "script" && node.kind !== "optional-group") {
10947
+ throw new WorkflowIrError(`Workflow node '${node.id}' has reviewKind on unsupported node kind '${node.kind}'`);
10948
+ }
10949
+ }
10931
10950
  function validateV2(ir2) {
10932
10951
  validateColumns(ir2);
10933
10952
  for (const [index2, column] of ir2.columns.entries()) {
@@ -10950,6 +10969,7 @@ function validateV2(ir2) {
10950
10969
  }
10951
10970
  const nodesById = new Map(ir2.nodes.map((n) => [n.id, n]));
10952
10971
  for (const node of ir2.nodes) {
10972
+ validateReviewKind(node, "top-level");
10953
10973
  validateExtensionMetadata(`Workflow node '${node.id}'`, node.extensions);
10954
10974
  if (node.column !== void 0 && !columnIds.has(node.column)) {
10955
10975
  throw new WorkflowIrError(
@@ -13413,6 +13433,7 @@ function codeReviewOptionalGroupNode(column, options = {}) {
13413
13433
  column,
13414
13434
  config: {
13415
13435
  name: CODE_REVIEW_NAME,
13436
+ reviewKind: "code",
13416
13437
  // Default-ON: runs for every coding task by default, but operators can toggle it
13417
13438
  // off per task (remove `code-review` from enabledWorkflowSteps).
13418
13439
  defaultOn: options.defaultOn ?? true,
@@ -13558,6 +13579,7 @@ function planReviewOptionalGroupNode(column, options = {}) {
13558
13579
  ...column ? { column } : {},
13559
13580
  config: {
13560
13581
  name: PLAN_REVIEW_NAME,
13582
+ reviewKind: "plan",
13561
13583
  defaultOn: options.defaultOn ?? true,
13562
13584
  /*
13563
13585
  * FNXC:WorkflowRemediation 2026-06-29-12:14:
@@ -63402,7 +63424,7 @@ function rowToTask(row) {
63402
63424
  executionStartBranch: row.executionStartBranch || void 0,
63403
63425
  branch: row.branch || void 0,
63404
63426
  autoMerge: row.autoMerge === null ? void 0 : row.autoMerge === 1,
63405
- autoMergeProvenance: row.autoMergeProvenance === "user" || row.autoMergeProvenance === "legacy-stamp" ? row.autoMergeProvenance : void 0,
63427
+ autoMergeProvenance: row.autoMergeProvenance === "user" || row.autoMergeProvenance === "mission" || row.autoMergeProvenance === "legacy-stamp" ? row.autoMergeProvenance : void 0,
63406
63428
  baseCommitSha: row.baseCommitSha || void 0,
63407
63429
  scopeOverride: row.scopeOverride ? true : void 0,
63408
63430
  scopeOverrideReason: row.scopeOverrideReason || void 0,
@@ -72918,10 +72940,10 @@ ${feature.acceptanceCriteria}`);
72918
72940
  Apply the selected Missions header workflow atomically during TaskStore.createTask so newly triaged features land in the intended workflow lane. Duplicate-guard reuses skip this create path, preserving existing duplicate tasks without workflow mutation.
72919
72941
  */
72920
72942
  /*
72921
- FNXC:MissionAutoMerge 2026-07-18-12:00:
72922
- An autoMerge:false mission stamps each newly triaged task so its shared branch produces one PR instead of per-task auto-merges. Duplicate reuse intentionally bypasses this create-only override.
72943
+ FNXC:MissionAutoMerge 2026-08-05-22:50:
72944
+ An autoMerge:false mission stamps each newly triaged task as mission policy so its shared branch produces one PR instead of per-task auto-merges. Duplicate reuse intentionally bypasses this create-only override; policy must not impersonate an operator manual-hold choice.
72923
72945
  */
72924
- ...mission?.autoMerge === false ? { autoMerge: false } : {},
72946
+ ...mission?.autoMerge === false ? { autoMerge: false, autoMergeProvenance: "mission" } : {},
72925
72947
  // FNXC:MissionTaskPrefix 2026-07-26-12:00: thread the mission's optional taskPrefix into TaskCreateInput for distributed id minting.
72926
72948
  ...mission?.taskPrefix ? { taskPrefix: mission.taskPrefix } : {},
72927
72949
  ...branchOptions?.workflowId !== void 0 ? { workflowId: branchOptions.workflowId } : {}
@@ -76099,10 +76121,10 @@ ${feature.acceptanceCriteria}`);
76099
76121
  }
76100
76122
  } : {},
76101
76123
  /*
76102
- FNXC:MissionAutoMerge 2026-07-18-12:00:
76103
- An autoMerge:false mission stamps each newly triaged task so its shared branch produces one PR instead of per-task auto-merges. Duplicate reuse intentionally bypasses this create-only override.
76124
+ FNXC:MissionAutoMerge 2026-08-05-22:50:
76125
+ An autoMerge:false mission stamps each newly triaged task as mission policy so its shared branch produces one PR instead of per-task auto-merges. Duplicate reuse intentionally bypasses this create-only override; policy must not impersonate an operator manual-hold choice.
76104
76126
  */
76105
- ...mission?.autoMerge === false ? { autoMerge: false } : {},
76127
+ ...mission?.autoMerge === false ? { autoMerge: false, autoMergeProvenance: "mission" } : {},
76106
76128
  // FNXC:MissionTaskPrefix 2026-07-26-12:00: thread the mission's optional taskPrefix into TaskCreateInput so the distributed allocator mints ERR-N (etc.) instead of the project prefix.
76107
76129
  ...mission?.taskPrefix ? { taskPrefix: mission.taskPrefix } : {},
76108
76130
  ...branchOptions?.workflowId !== void 0 ? { workflowId: branchOptions.workflowId } : {}
@@ -86453,7 +86475,9 @@ async function _createTaskInternalBackendImpl(store, input, title, resolvedWorkf
86453
86475
  sourceMetadata: withTaskBranchContextInSourceMetadata(input.source?.sourceMetadata, input.branchContext),
86454
86476
  branchContext: input.branchContext,
86455
86477
  autoMerge: input.autoMerge,
86456
- autoMergeProvenance: input.autoMerge === void 0 ? void 0 : "user",
86478
+ // FNXC:SharedBranchMemberHold 2026-08-05-22:50: trusted mission creation
86479
+ // preserves policy provenance; operator/API create requests retain user provenance.
86480
+ autoMergeProvenance: input.autoMerge === void 0 ? void 0 : input.autoMergeProvenance ?? "user",
86457
86481
  // FNXC:CodingIdeasWorkflow 2026-07-05-19:45: land the task in its
86458
86482
  // workflow's manual intake column (e.g. Coding (Ideas) → "ideas") when
86459
86483
  // no explicit column is given (main FN-7591 parity).
@@ -127158,6 +127182,36 @@ var init_async_knowledge = __esm({
127158
127182
  });
127159
127183
 
127160
127184
  // ../core/src/workflows/workflow-step-results.ts
127185
+ function normalizeWorkflowReviewFindings(raw) {
127186
+ if (!Array.isArray(raw)) return void 0;
127187
+ const normalized = [];
127188
+ const usedIds = /* @__PURE__ */ new Set();
127189
+ for (const candidate of raw) {
127190
+ if (normalized.length >= MAX_WORKFLOW_REVIEW_FINDINGS || !candidate || typeof candidate !== "object") continue;
127191
+ const value = candidate;
127192
+ const title = boundedTrimmedString(value.title, MAX_FINDING_TITLE_LENGTH);
127193
+ const body = boundedTrimmedString(value.body, MAX_FINDING_BODY_LENGTH);
127194
+ if (!title || !body) continue;
127195
+ const baseId = boundedTrimmedString(value.id, MAX_FINDING_ID_LENGTH) || `finding-${normalized.length + 1}`;
127196
+ let id = baseId;
127197
+ let suffix = 2;
127198
+ while (usedIds.has(id)) id = `${baseId.slice(0, Math.max(1, MAX_FINDING_ID_LENGTH - String(suffix).length - 1))}-${suffix++}`;
127199
+ usedIds.add(id);
127200
+ const filePath = boundedTrimmedString(value.filePath, MAX_FINDING_PATH_LENGTH);
127201
+ const line2 = typeof value.line === "number" && Number.isFinite(value.line) && value.line > 0 ? Math.floor(value.line) : void 0;
127202
+ const severity = isWorkflowReviewFindingSeverity(value.severity) ? value.severity : void 0;
127203
+ normalized.push({ id, title, body, ...filePath ? { filePath } : {}, ...line2 ? { line: line2 } : {}, ...severity ? { severity } : {} });
127204
+ }
127205
+ return normalized.length > 0 ? normalized : void 0;
127206
+ }
127207
+ function boundedTrimmedString(value, maxLength) {
127208
+ if (typeof value !== "string") return void 0;
127209
+ const trimmed = value.trim();
127210
+ return trimmed && trimmed.length <= maxLength ? trimmed : void 0;
127211
+ }
127212
+ function isWorkflowReviewFindingSeverity(value) {
127213
+ return typeof value === "string" && WORKFLOW_REVIEW_FINDING_SEVERITIES.includes(value);
127214
+ }
127161
127215
  function isTerminalFailure(result) {
127162
127216
  return TERMINAL_FAILURE_STATUSES.has(result.status);
127163
127217
  }
@@ -127223,10 +127277,16 @@ function makeReviewLeaseRecord(args) {
127223
127277
  leaseOwner: args.owner
127224
127278
  };
127225
127279
  }
127226
- var MAX_WORKFLOW_STEP_PRIOR_ATTEMPTS, TERMINAL_FAILURE_STATUSES, PLAN_REVIEW_LEASE_STALENESS_MS, TERMINAL_STEP_STATUSES;
127280
+ var WORKFLOW_REVIEW_FINDING_SEVERITIES, MAX_WORKFLOW_REVIEW_FINDINGS, MAX_FINDING_ID_LENGTH, MAX_FINDING_TITLE_LENGTH, MAX_FINDING_BODY_LENGTH, MAX_FINDING_PATH_LENGTH, MAX_WORKFLOW_STEP_PRIOR_ATTEMPTS, TERMINAL_FAILURE_STATUSES, PLAN_REVIEW_LEASE_STALENESS_MS, TERMINAL_STEP_STATUSES;
127227
127281
  var init_workflow_step_results = __esm({
127228
127282
  "../core/src/workflows/workflow-step-results.ts"() {
127229
127283
  "use strict";
127284
+ WORKFLOW_REVIEW_FINDING_SEVERITIES = ["low", "medium", "high", "critical"];
127285
+ MAX_WORKFLOW_REVIEW_FINDINGS = 20;
127286
+ MAX_FINDING_ID_LENGTH = 128;
127287
+ MAX_FINDING_TITLE_LENGTH = 240;
127288
+ MAX_FINDING_BODY_LENGTH = 4e3;
127289
+ MAX_FINDING_PATH_LENGTH = 1e3;
127230
127290
  MAX_WORKFLOW_STEP_PRIOR_ATTEMPTS = 5;
127231
127291
  TERMINAL_FAILURE_STATUSES = /* @__PURE__ */ new Set([
127232
127292
  "failed",
@@ -127295,6 +127355,7 @@ var init_task_timing = __esm({
127295
127355
  });
127296
127356
 
127297
127357
  // ../core/src/session-identity-registry.ts
127358
+ import { AsyncLocalStorage } from "node:async_hooks";
127298
127359
  import { resolve as resolve21 } from "node:path";
127299
127360
  import { realpathSync as realpathSync5 } from "node:fs";
127300
127361
  function getRegistry() {
@@ -127306,6 +127367,16 @@ function getRegistry() {
127306
127367
  }
127307
127368
  return registry3;
127308
127369
  }
127370
+ function getActiveInvocationStorage() {
127371
+ const holder = globalThis;
127372
+ const existing = holder[ACTIVE_INVOCATION_KEY];
127373
+ if (existing instanceof AsyncLocalStorage) {
127374
+ return existing;
127375
+ }
127376
+ const storage = new AsyncLocalStorage();
127377
+ holder[ACTIVE_INVOCATION_KEY] = storage;
127378
+ return storage;
127379
+ }
127309
127380
  function canonicalizeCwd(cwd) {
127310
127381
  const resolved = resolve21(cwd);
127311
127382
  try {
@@ -127333,8 +127404,13 @@ function registerFusionSessionIdentity(cwd, identity) {
127333
127404
  };
127334
127405
  }
127335
127406
  function resolveFusionSessionPrincipal(cwd) {
127407
+ const canonicalCwd = canonicalizeCwd(cwd);
127408
+ const activeInvocation = getActiveInvocationStorage().getStore();
127409
+ if (activeInvocation?.cwdKeys.has(canonicalCwd)) {
127410
+ return { kind: "agent", identity: activeInvocation.identity };
127411
+ }
127336
127412
  const registry3 = getRegistry();
127337
- const list = registry3.get(canonicalizeCwd(cwd));
127413
+ const list = registry3.get(canonicalCwd);
127338
127414
  if (!list || list.length === 0) {
127339
127415
  return { kind: "operator" };
127340
127416
  }
@@ -127343,14 +127419,24 @@ function resolveFusionSessionPrincipal(cwd) {
127343
127419
  }
127344
127420
  return { kind: "ambiguous", identities: [...list] };
127345
127421
  }
127422
+ function runWithFusionSessionIdentity(cwdKeys, identity, callback) {
127423
+ const invocation = {
127424
+ identity: { ...identity, registeredAt: Date.now() },
127425
+ cwdKeys: new Set(cwdKeys.map(canonicalizeCwd))
127426
+ };
127427
+ return getActiveInvocationStorage().run(invocation, callback);
127428
+ }
127346
127429
  function __clearFusionSessionIdentityRegistryForTests() {
127347
- globalThis[REGISTRY_KEY] = /* @__PURE__ */ new Map();
127430
+ const holder = globalThis;
127431
+ holder[REGISTRY_KEY] = /* @__PURE__ */ new Map();
127432
+ holder[ACTIVE_INVOCATION_KEY] = new AsyncLocalStorage();
127348
127433
  }
127349
- var REGISTRY_KEY;
127434
+ var REGISTRY_KEY, ACTIVE_INVOCATION_KEY;
127350
127435
  var init_session_identity_registry = __esm({
127351
127436
  "../core/src/session-identity-registry.ts"() {
127352
127437
  "use strict";
127353
127438
  REGISTRY_KEY = "__FUSION_SESSION_IDENTITY_REGISTRY_V1__";
127439
+ ACTIVE_INVOCATION_KEY = "__FUSION_SESSION_IDENTITY_ACTIVE_INVOCATION_V1__";
127354
127440
  }
127355
127441
  });
127356
127442
 
@@ -127713,6 +127799,7 @@ __export(src_exports, {
127713
127799
  MAX_TITLE_LENGTH: () => MAX_TITLE_LENGTH,
127714
127800
  MAX_TITLE_SUMMARIZE_INPUT_LENGTH: () => MAX_TITLE_SUMMARIZE_INPUT_LENGTH,
127715
127801
  MAX_WORKFLOW_ICON_LENGTH: () => MAX_WORKFLOW_ICON_LENGTH,
127802
+ MAX_WORKFLOW_REVIEW_FINDINGS: () => MAX_WORKFLOW_REVIEW_FINDINGS,
127716
127803
  MAX_WORKFLOW_STEP_PRIOR_ATTEMPTS: () => MAX_WORKFLOW_STEP_PRIOR_ATTEMPTS,
127717
127804
  MEMORY_AUDIT_PATH: () => MEMORY_AUDIT_PATH,
127718
127805
  MEMORY_BACKEND_SETTINGS_KEYS: () => MEMORY_BACKEND_SETTINGS_KEYS,
@@ -127914,6 +128001,7 @@ __export(src_exports, {
127914
128001
  WAKE_DELTA_ASSIGNED_TASKS_CAP: () => WAKE_DELTA_ASSIGNED_TASKS_CAP,
127915
128002
  WEDGE_RENOTIFY_COOLDOWN_MS: () => WEDGE_RENOTIFY_COOLDOWN_MS,
127916
128003
  WORKFLOW_EXTENSION_SCHEMA_VERSION: () => WORKFLOW_EXTENSION_SCHEMA_VERSION,
128004
+ WORKFLOW_REVIEW_FINDING_SEVERITIES: () => WORKFLOW_REVIEW_FINDING_SEVERITIES,
127917
128005
  WORKFLOW_SETTING_REJECTION_CODES: () => WORKFLOW_SETTING_REJECTION_CODES,
127918
128006
  WORKFLOW_SETTING_TYPES: () => WORKFLOW_SETTING_TYPES,
127919
128007
  WORKFLOW_WORK_ITEM_KINDS: () => WORKFLOW_WORK_ITEM_KINDS,
@@ -128319,6 +128407,7 @@ __export(src_exports, {
128319
128407
  hasRoleOverrides: () => hasRoleOverrides,
128320
128408
  hasSyncPassphraseConfigured: () => hasSyncPassphraseConfigured,
128321
128409
  hasTitleIdDrift: () => hasTitleIdDrift,
128410
+ hasUserAutoMergeHold: () => hasUserAutoMergeHold,
128322
128411
  hashWorkflowIr: () => hashWorkflowIr,
128323
128412
  healSchemaDrift: () => healSchemaDrift,
128324
128413
  importMcpServersJson: () => importMcpServersJson,
@@ -128441,6 +128530,7 @@ __export(src_exports, {
128441
128530
  isWipColumn: () => isWipColumn,
128442
128531
  isWipColumnRole: () => isWipColumnRole,
128443
128532
  isWorkflowOptionalGroupEnabled: () => isWorkflowOptionalGroupEnabled,
128533
+ isWorkflowReviewFindingSeverity: () => isWorkflowReviewFindingSeverity,
128444
128534
  isWorkspaceTask: () => isWorkspaceTask,
128445
128535
  layoutForIr: () => layoutForIr,
128446
128536
  listActiveAiSessions: () => listActiveAiSessions,
@@ -128514,6 +128604,7 @@ __export(src_exports, {
128514
128604
  normalizeValidationDiagnostics: () => normalizeValidationDiagnostics,
128515
128605
  normalizeWorkflowIcon: () => normalizeWorkflowIcon,
128516
128606
  normalizeWorkflowPromptOverrides: () => normalizeWorkflowPromptOverrides,
128607
+ normalizeWorkflowReviewFindings: () => normalizeWorkflowReviewFindings,
128517
128608
  notifyOperatorOfNonOperatorDelete: () => notifyOperatorOfNonOperatorDelete,
128518
128609
  overseerAdviceSeverityRank: () => overseerAdviceSeverityRank,
128519
128610
  parseAgentManifest: () => parseAgentManifest,
@@ -128777,6 +128868,7 @@ __export(src_exports, {
128777
128868
  runMemoryBackupCommand: () => runMemoryBackupCommand,
128778
128869
  runReconciliationAbort: () => runReconciliationAbort,
128779
128870
  runScheduledEvalBatch: () => runScheduledEvalBatch,
128871
+ runWithFusionSessionIdentity: () => runWithFusionSessionIdentity,
128780
128872
  sanitizeBranchSegment: () => sanitizeBranchSegment,
128781
128873
  sanitizeCliAgentSettings: () => sanitizeCliAgentSettings,
128782
128874
  sanitizeCliAgentsSettings: () => sanitizeCliAgentsSettings,
@@ -158175,6 +158267,7 @@ import { existsSync as existsSync49, readFileSync as readFileSync16, realpathSyn
158175
158267
  import { exec as exec11, execFile as execFile9 } from "node:child_process";
158176
158268
  import { promisify as promisify15 } from "node:util";
158177
158269
  import { createRequire as createRequire5 } from "node:module";
158270
+ import { randomUUID as randomUUID42 } from "node:crypto";
158178
158271
  import { basename as basename16, dirname as dirname24, join as join59, relative as relative8, isAbsolute as isAbsolute10, resolve as resolve30 } from "node:path";
158179
158272
  import {
158180
158273
  createAgentSession,
@@ -159879,7 +159972,8 @@ async function createFnAgent2(options) {
159879
159972
  });
159880
159973
  };
159881
159974
  const sessionIdentity = (() => {
159882
- const principalAgentId = options.actionGateContext?.agentId ?? options.permanentAgentGating?.requester?.actorId ?? "engine-session";
159975
+ const namedAgentId = options.actionGateContext?.agentId ?? options.permanentAgentGating?.requester?.actorId;
159976
+ const principalAgentId = namedAgentId ?? `engine-session-${randomUUID42()}`;
159883
159977
  const principalAgentName = options.actionGateContext?.agentName ?? options.permanentAgentGating?.requester?.actorName;
159884
159978
  return {
159885
159979
  agentId: principalAgentId,
@@ -159891,6 +159985,14 @@ async function createFnAgent2(options) {
159891
159985
  const sessionIdentityKeys = [...new Set([options.cwd, resolvedProjectRoot].filter((key) => Boolean(key)))];
159892
159986
  const attachSessionIdentity = (session) => {
159893
159987
  const identityDisposers = sessionIdentityKeys.map((key) => registerFusionSessionIdentity(key, sessionIdentity));
159988
+ const sessionInvocations = session;
159989
+ const wrapInvocation = (methodName) => {
159990
+ const original = sessionInvocations[methodName];
159991
+ if (typeof original !== "function") return;
159992
+ sessionInvocations[methodName] = (...args) => runWithFusionSessionIdentity(sessionIdentityKeys, sessionIdentity, () => original.apply(session, args));
159993
+ };
159994
+ wrapInvocation("prompt");
159995
+ wrapInvocation("promptWithFallback");
159894
159996
  const disposeBeforeIdentity = typeof session.dispose === "function" ? session.dispose.bind(session) : () => void 0;
159895
159997
  session.dispose = async () => {
159896
159998
  for (const disposeIdentity of identityDisposers) {
@@ -162686,10 +162788,11 @@ function createMergeAttemptHandler(deps) {
162686
162788
  );
162687
162789
  };
162688
162790
  }
162689
- function createMergeGateHandler() {
162791
+ function createMergeGateHandler(deps = {}) {
162690
162792
  return async (_node, ctx) => {
162691
162793
  const settingsAutoMerge = ctx.settings?.autoMerge;
162692
- const autoMerge = ctx.task.autoMerge !== false && settingsAutoMerge !== false;
162794
+ const settings = { autoMerge: settingsAutoMerge ?? true };
162795
+ const autoMerge = !hasUserAutoMergeHold(ctx.task) && (await deps.isLiveSharedBranchMember?.(ctx.task, settings) === true || ctx.task.autoMerge !== false && settings.autoMerge !== false);
162693
162796
  return {
162694
162797
  outcome: "success",
162695
162798
  value: autoMerge ? "auto-on" : "auto-off"
@@ -162699,6 +162802,7 @@ function createMergeGateHandler() {
162699
162802
  var init_merge_runner = __esm({
162700
162803
  "../engine/src/workflow-node-runners/merge-runner.ts"() {
162701
162804
  "use strict";
162805
+ init_src2();
162702
162806
  init_workflow_merge_nodes();
162703
162807
  }
162704
162808
  });
@@ -163042,7 +163146,7 @@ function createDefaultNodeHandlers(seams, runCustomNode, deps) {
163042
163146
  "parse-steps": parseSteps,
163043
163147
  code: createCodeNodeHandler(deps?.runCode),
163044
163148
  notify: createNotifyHandler(deps?.notifyDispatch),
163045
- "merge-gate": createMergeGateHandler(),
163149
+ "merge-gate": createMergeGateHandler({ isLiveSharedBranchMember: deps?.isLiveSharedBranchMember }),
163046
163150
  "merge-attempt": createMergeAttemptHandler({
163047
163151
  primitives: deps?.primitives,
163048
163152
  seams,
@@ -164660,37 +164764,6 @@ var init_code_node_runner = __esm({
164660
164764
  }
164661
164765
  });
164662
164766
 
164663
- // ../engine/src/errors/transient-merge-error-classifier.ts
164664
- function classifyTransientMergeError(error2) {
164665
- if (!error2) return null;
164666
- if (/lease-handoff-failed[^a-z]+target-not-queued/i.test(error2)) {
164667
- return "lease-handoff-target-not-queued";
164668
- }
164669
- if (/\bACP turn failed\b/i.test(error2) || /\bacp rpc code -32(?:603|00[0-3])\b/i.test(error2)) {
164670
- return "ai-provider-turn-failure";
164671
- }
164672
- if (/\bspawn(?:\s+\S+)?\s+ENO(?:TDIR|ENT)\b/i.test(error2)) {
164673
- return "process-spawn-failure";
164674
- }
164675
- if (/\bis not a working tree\b/i.test(error2)) {
164676
- return "process-spawn-failure";
164677
- }
164678
- const sameSha = error2.match(/advanced concurrently \(expected ([0-9a-f]{7,40}),\s+observed ([0-9a-f]{7,40})\)/i);
164679
- if (sameSha && sameSha[1].toLowerCase() === sameSha[2].toLowerCase()) {
164680
- return "spurious-concurrent-advance-same-sha";
164681
- }
164682
- if (isTransientError(error2)) {
164683
- return "network-transport-failure";
164684
- }
164685
- return null;
164686
- }
164687
- var init_transient_merge_error_classifier = __esm({
164688
- "../engine/src/errors/transient-merge-error-classifier.ts"() {
164689
- "use strict";
164690
- init_transient_error_patterns();
164691
- }
164692
- });
164693
-
164694
164767
  // ../engine/src/notification/ntfy-provider.ts
164695
164768
  function resolveParticipantLabel(metadata, kind) {
164696
164769
  const nameKey = kind === "from" ? "fromName" : "toName";
@@ -165164,7 +165237,50 @@ var init_webhook_provider = __esm({
165164
165237
  }
165165
165238
  });
165166
165239
 
165240
+ // ../engine/src/errors/transient-merge-error-classifier.ts
165241
+ function classifyTransientMergeError(error2) {
165242
+ if (!error2) return null;
165243
+ if (/lease-handoff-failed[^a-z]+target-not-queued/i.test(error2)) {
165244
+ return "lease-handoff-target-not-queued";
165245
+ }
165246
+ if (/\bACP turn failed\b/i.test(error2) || /\bacp rpc code -32(?:603|00[0-3])\b/i.test(error2)) {
165247
+ return "ai-provider-turn-failure";
165248
+ }
165249
+ if (/\bspawn(?:\s+\S+)?\s+ENO(?:TDIR|ENT)\b/i.test(error2)) {
165250
+ return "process-spawn-failure";
165251
+ }
165252
+ if (/\bis not a working tree\b/i.test(error2)) {
165253
+ return "process-spawn-failure";
165254
+ }
165255
+ const sameSha = error2.match(/advanced concurrently \(expected ([0-9a-f]{7,40}),\s+observed ([0-9a-f]{7,40})\)/i);
165256
+ if (sameSha && sameSha[1].toLowerCase() === sameSha[2].toLowerCase()) {
165257
+ return "spurious-concurrent-advance-same-sha";
165258
+ }
165259
+ if (isTransientError(error2)) {
165260
+ return "network-transport-failure";
165261
+ }
165262
+ return null;
165263
+ }
165264
+ function hasTransientMergeRecoveryOwner(task) {
165265
+ return classifyTransientMergeError(task.error) !== null && typeof task.mergeTransientRetryCount === "number" && Number.isInteger(task.mergeTransientRetryCount) && task.mergeTransientRetryCount >= 0 && task.mergeTransientRetryCount < MAX_AUTO_MERGE_TRANSIENT_RETRIES;
165266
+ }
165267
+ var MAX_AUTO_MERGE_TRANSIENT_RETRIES;
165268
+ var init_transient_merge_error_classifier = __esm({
165269
+ "../engine/src/errors/transient-merge-error-classifier.ts"() {
165270
+ "use strict";
165271
+ init_transient_error_patterns();
165272
+ MAX_AUTO_MERGE_TRANSIENT_RETRIES = 5;
165273
+ }
165274
+ });
165275
+
165167
165276
  // ../engine/src/notification/task-wedge-notification.ts
165277
+ function describeTaskRecoveryOwner(task) {
165278
+ if (hasTransientMergeRecoveryOwner(task)) return { kind: "transient-merge-retry" };
165279
+ if (typeof task.recoveryRetryCount === "number" && Number.isInteger(task.recoveryRetryCount) && task.recoveryRetryCount >= 0 && typeof task.nextRecoveryAt === "string" && Number.isFinite(Date.parse(task.nextRecoveryAt))) {
165280
+ return { kind: "scheduled-recovery" };
165281
+ }
165282
+ return null;
165283
+ }
165168
165284
  function describeSelfHealingNoActionWedge(task, stage, metadata) {
165169
165285
  const description = SELF_HEALING_NO_ACTIONS[stage];
165170
165286
  if (!description || task.userPaused || task.paused || task.autoMerge === false) return null;
@@ -165222,6 +165338,7 @@ function describeTaskWedge(task) {
165222
165338
  if (/merge.*(?:blocked|verification|gate)|(?:verification|gate).*(?:failed|exhaust)/i.test(error2)) {
165223
165339
  return { reasonKey: "merge-blocked", reason: "Merge verification cannot progress without operator action.", action: "Fix the failing verification, then retry the task." };
165224
165340
  }
165341
+ if (describeTaskRecoveryOwner(task)) return null;
165225
165342
  if (!error2 && (task.mergeRetries ?? 0) < 3) return null;
165226
165343
  return {
165227
165344
  reasonKey: "terminal-failed",
@@ -165233,6 +165350,7 @@ var SELF_HEALING_NO_ACTIONS;
165233
165350
  var init_task_wedge_notification = __esm({
165234
165351
  "../engine/src/notification/task-wedge-notification.ts"() {
165235
165352
  "use strict";
165353
+ init_transient_merge_error_classifier();
165236
165354
  SELF_HEALING_NO_ACTIONS = {
165237
165355
  "reclaim-pr-conflict": {
165238
165356
  reason: "Self-healing could not reclaim a stalled pull-request conflict.",
@@ -165298,7 +165416,6 @@ var init_notification_service = __esm({
165298
165416
  init_src2();
165299
165417
  init_notifier();
165300
165418
  init_logger();
165301
- init_transient_merge_error_classifier();
165302
165419
  init_ntfy_provider();
165303
165420
  init_webhook_provider();
165304
165421
  init_task_wedge_notification();
@@ -165524,10 +165641,10 @@ var init_notification_service = __esm({
165524
165641
  }
165525
165642
  }
165526
165643
  handleTaskUpdated = (task, meta2) => {
165527
- const transientFailure = task.status === "failed" ? classifyTransientMergeError(task.error) : null;
165528
- const wedge = transientFailure ? null : describeTaskWedge(task);
165644
+ const recoveryOwner = task.status === "failed" ? describeTaskRecoveryOwner(task) : null;
165645
+ const wedge = describeTaskWedge(task);
165529
165646
  if (wedge) this.cancelPendingFailureNotification(task.id, "classified-terminal-wedge");
165530
- if (!transientFailure) void this.enqueueWedgeHandling(task.id, () => this.maybeNotifyTaskWedge(task, wedge));
165647
+ void this.enqueueWedgeHandling(task.id, () => this.maybeNotifyTaskWedge(task, wedge));
165531
165648
  void this.maybeSuppressTransientFailedNotification(task, `status=${task.status ?? "undefined"}`);
165532
165649
  if (task.status === "awaiting-approval") {
165533
165650
  void this.writeAwaitingApprovalMailboxMessage(task);
@@ -165538,17 +165655,12 @@ var init_notification_service = __esm({
165538
165655
  if (!this.notificationsEnabled) {
165539
165656
  return;
165540
165657
  }
165541
- if (task.status === "failed" && !wedge) {
165542
- const transientClass = classifyTransientMergeError(task.error);
165543
- if (transientClass) {
165544
- this.failureNotificationSuppressedCount += 1;
165545
- schedulerLog.debug(
165546
- `[notify] ${task.id} transient merge failure (${transientClass}) \u2014 suppressed notification (self-heal in flight)`
165547
- );
165548
- return;
165549
- }
165658
+ if (task.status === "failed" && recoveryOwner) {
165659
+ this.failureNotificationSuppressedCount += 1;
165660
+ schedulerLog.debug(`[notify] ${task.id} recovery-owned failure \u2014 suppressed notification`);
165661
+ } else if (task.status === "failed" && !wedge) {
165550
165662
  if (this.failureNotificationMode === "all") {
165551
- this.maybeNotify(task.id, "failed", this.createTaskPayload(task, "failed"));
165663
+ void this.maybeNotifyImmediateFailure(task);
165552
165664
  } else {
165553
165665
  this.scheduleFailureNotification(task);
165554
165666
  }
@@ -165635,7 +165747,17 @@ var init_notification_service = __esm({
165635
165747
  await this.enqueueWedgeHandling(task.id, () => this.maybeNotifyTaskWedge(task, descriptor));
165636
165748
  }
165637
165749
  async maybeNotifyTaskWedge(task, suppliedDescriptor) {
165638
- const descriptor = suppliedDescriptor ?? describeTaskWedge(task);
165750
+ const liveTask = this.store.getTask ? await this.store.getTask(task.id) ?? task : task;
165751
+ const recoveryOwner = describeTaskRecoveryOwner(liveTask);
165752
+ if (recoveryOwner) {
165753
+ if (this.activeWedgeReasons.has(task.id) || liveTask.wedgeNotification?.status === "active") {
165754
+ this.activeWedgeReasons.delete(task.id);
165755
+ await this.store.claimTaskWedgeNotificationEpisode?.(task.id, null);
165756
+ }
165757
+ return;
165758
+ }
165759
+ const descriptor = suppliedDescriptor ?? describeTaskWedge(liveTask);
165760
+ task = liveTask;
165639
165761
  let episode;
165640
165762
  if (!descriptor) {
165641
165763
  const isActiveSelfHealingNoAction = task.wedgeNotification?.status === "active" && task.wedgeNotification.reasonKey.startsWith("self-healing-no-action:");
@@ -166002,6 +166124,14 @@ var init_notification_service = __esm({
166002
166124
  this.failureNotificationDelayMs = typeof settings.failureNotificationDelayMs === "number" && settings.failureNotificationDelayMs >= 0 ? settings.failureNotificationDelayMs : this.failedNotificationGraceMs;
166003
166125
  this.failureNotificationMode = settings.failureNotificationMode ?? "sticky-only";
166004
166126
  }
166127
+ async maybeNotifyImmediateFailure(task) {
166128
+ const liveTask = this.store.getTask ? await this.store.getTask(task.id) ?? task : task;
166129
+ if (liveTask.status !== "failed" || describeTaskRecoveryOwner(liveTask) || describeTaskWedge(liveTask)) {
166130
+ this.failureNotificationSuppressedCount += 1;
166131
+ return;
166132
+ }
166133
+ this.maybeNotify(liveTask.id, "failed", this.createTaskPayload(liveTask, "failed"));
166134
+ }
166005
166135
  scheduleFailureNotification(task) {
166006
166136
  if (this.pendingFailureNotifications.has(task.id)) {
166007
166137
  return;
@@ -166058,12 +166188,9 @@ var init_notification_service = __esm({
166058
166188
  schedulerLog.debug(`[notify] ${taskId} no longer failed at dispatch time \u2014 suppressed notification`);
166059
166189
  return;
166060
166190
  }
166061
- const transientClassAtDispatch = classifyTransientMergeError(task.error);
166062
- if (transientClassAtDispatch) {
166191
+ if (describeTaskRecoveryOwner(task)) {
166063
166192
  this.failureNotificationSuppressedCount += 1;
166064
- schedulerLog.debug(
166065
- `[notify] ${taskId} transient merge failure (${transientClassAtDispatch}) at dispatch time \u2014 suppressed notification (self-heal in flight)`
166066
- );
166193
+ schedulerLog.debug(`[notify] ${taskId} recovery-owned failure at dispatch time \u2014 suppressed notification`);
166067
166194
  return;
166068
166195
  }
166069
166196
  if (describeTaskWedge(task)) {
@@ -169630,7 +169757,7 @@ var init_goal_anchoring_audit = __esm({
169630
169757
  // ../engine/src/agent-tools.ts
169631
169758
  import { appendFile as appendFile3, mkdir as mkdir26, readFile as readFile34, readdir as readdir9, realpath, stat as stat9, writeFile as writeFile24 } from "node:fs/promises";
169632
169759
  import { existsSync as existsSync52 } from "node:fs";
169633
- import { createHash as createHash19, randomUUID as randomUUID42 } from "node:crypto";
169760
+ import { createHash as createHash19, randomUUID as randomUUID43 } from "node:crypto";
169634
169761
  import { tmpdir as tmpdir6 } from "node:os";
169635
169762
  import { extname as extname3, isAbsolute as isAbsolute13, join as join64, relative as relative11, resolve as resolve33, sep as sep9 } from "node:path";
169636
169763
  import { Type as Type2 } from "@earendil-works/pi-ai";
@@ -170276,7 +170403,7 @@ function createTaskCreateTool(store, provenance, options) {
170276
170403
  toType: "user",
170277
170404
  type: "agent-to-user",
170278
170405
  content: `Task proposal awaiting validation: ${title}`,
170279
- metadata: { kind: "task-proposal", proposalStatus: "pending", proposalIdempotencyKey: randomUUID42(), taskId: options.sourceTaskId, proposedTask: { title, description: params.description, priority: params.priority, workflowId: params.workflow_id, dependencies: params.dependencies } }
170406
+ metadata: { kind: "task-proposal", proposalStatus: "pending", proposalIdempotencyKey: randomUUID43(), taskId: options.sourceTaskId, proposedTask: { title, description: params.description, priority: params.priority, workflowId: params.workflow_id, dependencies: params.dependencies } }
170280
170407
  });
170281
170408
  return { content: [{ type: "text", text: "Task proposal submitted to the operator for validation; no task was created." }], details: { proposed: true } };
170282
170409
  }
@@ -192043,10 +192170,21 @@ var Scheduler = class {
192043
192170
  const unmetDeps = getUnmetSchedulingDependencies(task, tasks2, schedulingDependencyOptions);
192044
192171
  if (unmetDeps.length > 0) {
192045
192172
  const normalizedUnmetDeps = [...new Set(unmetDeps)].sort();
192173
+ let activeOverlapBlockedBy = null;
192174
+ if (activeScopes.size > 0) {
192175
+ try {
192176
+ const taskScope = await getFilteredFileScope(task.id);
192177
+ if (taskScope.length > 0 && !isCoordinationOnlyTask(task, taskScope)) {
192178
+ activeOverlapBlockedBy = Array.from(activeScopes.entries()).sort(([aId], [bId]) => aId.localeCompare(bId)).find(([, activeScope]) => this.pathsOverlap(taskScope, activeScope))?.[0] ?? null;
192179
+ }
192180
+ } catch (error2) {
192181
+ schedulerLog.warn(`Failed to refresh file-scope overlap blocker for dependency-blocked task ${task.id}`, error2);
192182
+ }
192183
+ }
192046
192184
  await this.transitionQueuedEpisode(task, {
192047
192185
  signature: `dependency:${normalizedUnmetDeps.join(",")}`,
192048
192186
  blockedBy: unmetDeps[0] ?? null,
192049
- overlapBlockedBy: task.overlapBlockedBy ?? null,
192187
+ overlapBlockedBy: activeOverlapBlockedBy,
192050
192188
  action: `queued \u2014 unmet dependencies: ${unmetDeps.join(", ")}`
192051
192189
  });
192052
192190
  this.options.onBlocked?.(task, unmetDeps);
@@ -194569,6 +194707,7 @@ var SESSION_CONTENTION_HOLD_VALUE = "session-contention-hold";
194569
194707
  var WORKFLOW_INTERRUPTED_NODE_ID_CONTEXT_KEY = "workflow:interruptedNodeId";
194570
194708
  var WORKFLOW_INTERRUPTED_NODE_ABORT_KIND_CONTEXT_KEY = "workflow:interruptedNodeAbortKind";
194571
194709
  var WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY = "workflow:optionalGroupActive";
194710
+ var WORKFLOW_REVIEW_KIND_CONTEXT_KEY = "workflow:reviewKind";
194572
194711
  var WORKFLOW_NODE_ENGINE_PAUSE_ABORT_KIND = "engine-pause";
194573
194712
  var WORKFLOW_DRIFT_PARK_CONTEXT_KEY = "workflow:driftPark";
194574
194713
  var WorkflowGraphSuspended = class extends Error {
@@ -194672,7 +194811,8 @@ var WorkflowGraphExecutor = class {
194672
194811
  parseSteps: deps.parseStepsDeps,
194673
194812
  runCode: deps.runCode,
194674
194813
  notifyDispatch: deps.notifyDispatch,
194675
- prNodes: deps.prNodes
194814
+ prNodes: deps.prNodes,
194815
+ isLiveSharedBranchMember: deps.isLiveSharedBranchMember
194676
194816
  }),
194677
194817
  ...deps.runnerRegistry?.toHandlers() ?? {},
194678
194818
  ...deps.handlers ?? {}
@@ -194893,6 +195033,7 @@ var WorkflowGraphExecutor = class {
194893
195033
  phase: stepPhase,
194894
195034
  status: "pending",
194895
195035
  source: "optional-group",
195036
+ ...this.workflowReviewKind(node) ? { reviewKind: this.workflowReviewKind(node) } : {},
194896
195037
  startedAt: stepStartedAt,
194897
195038
  // U3/KTD-4: stamp the lease owner so a concurrent/crashed re-entry
194898
195039
  // adopts this pending gate instead of dispatching a second reviewer.
@@ -194912,7 +195053,8 @@ var WorkflowGraphExecutor = class {
194912
195053
  runTemplateNode: (tNode, sig, contextOverride) => {
194913
195054
  const optionalGroupContext = {
194914
195055
  ...contextOverride ?? context,
194915
- [WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY]: node.id
195056
+ [WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY]: node.id,
195057
+ ...this.workflowReviewKind(node) ? { [WORKFLOW_REVIEW_KIND_CONTEXT_KEY]: this.workflowReviewKind(node) } : {}
194916
195058
  };
194917
195059
  return this.executeNodeWithRetries(tNode, task, settings, optionalGroupContext, ir2, sig, false);
194918
195060
  },
@@ -194930,6 +195072,7 @@ var WorkflowGraphExecutor = class {
194930
195072
  const exitContextPatch = exitResult?.contextPatch;
194931
195073
  let stepOutput = typeof exitContextPatch?.output === "string" ? exitContextPatch.output : void 0;
194932
195074
  const stepNotes = typeof exitContextPatch?.notes === "string" ? exitContextPatch.notes : void 0;
195075
+ const stepFindings = this.workflowReviewKind(node) && Array.isArray(exitContextPatch?.findings) ? exitContextPatch.findings : void 0;
194933
195076
  if (stepStatus === "failed" && !verdict && stepOutput === void 0 && stepNotes === void 0) {
194934
195077
  const lastVisited = groupResult.visitedNodeIds[groupResult.visitedNodeIds.length - 1];
194935
195078
  const templateNodeId = lastVisited?.includes("::") ? lastVisited.slice(lastVisited.indexOf("::") + 2) : void 0;
@@ -194947,9 +195090,11 @@ var WorkflowGraphExecutor = class {
194947
195090
  phase: stepPhase,
194948
195091
  source: "optional-group",
194949
195092
  status: stepStatus,
195093
+ ...this.workflowReviewKind(node) ? { reviewKind: this.workflowReviewKind(node) } : {},
194950
195094
  ...verdict ? { verdict } : {},
194951
195095
  ...stepOutput !== void 0 ? { output: stepOutput } : {},
194952
195096
  ...stepNotes !== void 0 ? { notes: stepNotes } : {},
195097
+ ...stepFindings?.length ? { findings: stepFindings } : {},
194953
195098
  startedAt: stepStartedAt,
194954
195099
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
194955
195100
  });
@@ -195409,9 +195554,15 @@ var WorkflowGraphExecutor = class {
195409
195554
  }
195410
195555
  return failureResult;
195411
195556
  }
195557
+ /** FNXC:WorkflowReviewKind 2026-08-05-02:31: Persist only the declared closed
195558
+ * marker; execution never derives review semantics from a label, verdict, or gate mode. */
195559
+ workflowReviewKind(node) {
195560
+ return node.config?.reviewKind === "plan" || node.config?.reviewKind === "code" ? node.config.reviewKind : void 0;
195561
+ }
195412
195562
  shouldRecordNodeProgress(node) {
195413
195563
  const skillName = typeof node.config?.skillName === "string" ? node.config.skillName.trim() : "";
195414
- return skillName.length > 0 && (node.kind === "prompt" || node.kind === "gate");
195564
+ const markedReview = this.workflowReviewKind(node) !== void 0;
195565
+ return markedReview && (node.kind === "prompt" || node.kind === "gate" || node.kind === "script") || skillName.length > 0 && (node.kind === "prompt" || node.kind === "gate");
195415
195566
  }
195416
195567
  workflowNodeProgressName(node) {
195417
195568
  const configuredName = typeof node.config?.name === "string" ? node.config.name.trim() : "";
@@ -195425,6 +195576,7 @@ var WorkflowGraphExecutor = class {
195425
195576
  phase: node.config?.phase === "post-merge" ? "post-merge" : "pre-merge",
195426
195577
  source: "node",
195427
195578
  status: "pending",
195579
+ ...this.workflowReviewKind(node) ? { reviewKind: this.workflowReviewKind(node) } : {},
195428
195580
  startedAt
195429
195581
  };
195430
195582
  await this.recordOptionalGroupStepResult(taskId, result);
@@ -195435,6 +195587,7 @@ var WorkflowGraphExecutor = class {
195435
195587
  const contextPatch = nodeResult.contextPatch ?? {};
195436
195588
  let output = typeof contextPatch.output === "string" ? contextPatch.output : void 0;
195437
195589
  const notes = typeof contextPatch.notes === "string" ? contextPatch.notes : void 0;
195590
+ const findings = this.workflowReviewKind(node) && Array.isArray(contextPatch.findings) ? contextPatch.findings : void 0;
195438
195591
  if (status === "failed" && output === void 0 && notes === void 0) {
195439
195592
  output = this.synthesizeNonVerdictFailureOutput({
195440
195593
  stepLabel: this.workflowNodeProgressName(node),
@@ -195449,8 +195602,10 @@ var WorkflowGraphExecutor = class {
195449
195602
  phase: started?.phase ?? (node.config?.phase === "post-merge" ? "post-merge" : "pre-merge"),
195450
195603
  source: "node",
195451
195604
  status,
195605
+ ...this.workflowReviewKind(node) ? { reviewKind: this.workflowReviewKind(node) } : {},
195452
195606
  ...output !== void 0 ? { output } : {},
195453
195607
  ...notes !== void 0 ? { notes } : {},
195608
+ ...findings?.length ? { findings } : {},
195454
195609
  startedAt: started?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
195455
195610
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
195456
195611
  });
@@ -195900,6 +196055,10 @@ var WorkflowGraphTaskRunner = class {
195900
196055
  runCode: this.deps.runCode,
195901
196056
  notifyDispatch: this.deps.notifyDispatch,
195902
196057
  prNodes: this.deps.prNodes,
196058
+ // FNXC:SharedBranchMemberHold 2026-08-06-00:24: carry the executor's
196059
+ // live group resolver into the graph merge gate; omitting it turns
196060
+ // mission-policy members into manual holds before integration.
196061
+ isLiveSharedBranchMember: this.deps.isLiveSharedBranchMember,
195903
196062
  /*
195904
196063
  FNXC:WorkflowResume 2026-06-29-08:49:
195905
196064
  Production graph runs must fetch live task steps during foreach replay. The runner is the workflow boundary that has store access, so it supplies the fresh projection seam instead of making executor self-healing guess after a stale step node fails.
@@ -207306,16 +207465,29 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
207306
207465
  activeWork: await this.resolveActiveWorkColumnsFor(taskId, cache)
207307
207466
  };
207308
207467
  }
207309
- classifyPausedAbortWorkflowRecovery(task, settings, isExecuting, columns) {
207468
+ /*
207469
+ FNXC:SharedBranchMemberHold 2026-08-05-23:35:
207470
+ Pause-abort recovery must resolve the same live intermediate-group predicate as
207471
+ merge admission. Shared metadata alone is stale after group closure or a
207472
+ default-branch collision, so it cannot suppress that task's standalone hold.
207473
+ */
207474
+ async isLiveSharedMemberIntegration(task, settings) {
207475
+ const groupId = task.branchContext?.groupId?.trim();
207476
+ const group = groupId ? await this.store.getBranchGroup?.(groupId) : null;
207477
+ const defaultBranch = await resolveIntegrationBranch(this.options.rootDir, settings);
207478
+ return isLiveSharedBranchGroupMemberIntegration(task, group, defaultBranch);
207479
+ }
207480
+ async classifyPausedAbortWorkflowRecovery(task, settings, isExecuting, columns) {
207310
207481
  const isPausedAbortPark = task.status === "failed" && typeof task.error === "string" && task.error.includes(PAUSE_ABORT_PARK_OPERATOR_MARKER) && task.error.includes(PAUSE_ABORT_PARK_ERROR_MARKER);
207311
207482
  if (!isPausedAbortPark) return { kind: "no-action", reason: "not-pause-abort" };
207312
207483
  if (task.paused || task.userPaused || isExecuting) return { kind: "no-action", reason: "unsafe-or-not-routable" };
207313
207484
  const errorText = typeof task.error === "string" ? task.error.toLowerCase() : "";
207314
207485
  const isTerminalMergePark = errorText.includes("conflict") || errorText.includes("contamination") || errorText.includes("foreign") || errorText.includes("retry-exhausted") || errorText.includes("retries exhausted") || errorText.includes("max retries");
207315
207486
  const completedSteps = task.steps.length > 0 && task.steps.every((step) => step.status === "done" || step.status === "skipped");
207316
- const sharedBranchMember = isSharedBranchGroupMemberIntegration(task);
207317
- const hasReviewProgress = columns.review.has(task.column) && allowsAutoMergeProcessing(task, settings) && task.mergeDetails?.mergeConfirmed !== true && !isTerminalMergePark && completedSteps;
207318
- const hasManualMergeHoldProgress = columns.review.has(task.column) && (!allowsAutoMergeProcessing(task, settings) || resolveEffectiveAutoMerge(task, settings) === false) && !sharedBranchMember && task.mergeDetails?.mergeConfirmed !== true && !isTerminalMergePark && completedSteps;
207487
+ const liveSharedBranchMember = await this.isLiveSharedMemberIntegration(task, settings);
207488
+ const autoMergeProcessing = allowsAutoMergeProcessing(task, settings) || liveSharedBranchMember;
207489
+ const hasReviewProgress = columns.review.has(task.column) && autoMergeProcessing && task.mergeDetails?.mergeConfirmed !== true && !isTerminalMergePark && completedSteps;
207490
+ const hasManualMergeHoldProgress = columns.review.has(task.column) && (hasUserAutoMergeHold(task) || !allowsAutoMergeProcessing(task, settings) || resolveEffectiveAutoMerge(task, settings) === false) && (!liveSharedBranchMember || hasUserAutoMergeHold(task)) && task.mergeDetails?.mergeConfirmed !== true && !isTerminalMergePark && completedSteps;
207319
207491
  if (hasManualMergeHoldProgress) {
207320
207492
  return { kind: "work-item-resume", reason: "pause-abort-manual-merge-hold" };
207321
207493
  }
@@ -212983,8 +213155,23 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
212983
213155
  return own.length > 0 ? new Set(own) : /* @__PURE__ */ new Set(["in-review"]);
212984
213156
  };
212985
213157
  const executingIds = this.options.getExecutingTaskIds?.() ?? /* @__PURE__ */ new Set();
213158
+ const canRecoverMergeableReviewTask = async (task) => {
213159
+ if (hasUserAutoMergeHold(task)) return false;
213160
+ const groupId = task.branchContext?.groupId?.trim();
213161
+ const branchGroup = groupId ? await this.store.getBranchGroup(groupId) : null;
213162
+ const projectDefaultBranch = await resolveIntegrationBranch(this.options.rootDir, settings);
213163
+ if (isLiveSharedBranchGroupMemberIntegration(task, branchGroup, projectDefaultBranch)) {
213164
+ return true;
213165
+ }
213166
+ return allowsAutoMergeProcessing(task, settings) && resolveEffectiveAutoMerge(task, settings) !== false;
213167
+ };
213168
+ const mergeAdmission = await Promise.all(tasks2.map(async (task) => [
213169
+ task.id,
213170
+ await canRecoverMergeableReviewTask(task)
213171
+ ]));
213172
+ const mergeAdmissionByTaskId = new Map(mergeAdmission);
212986
213173
  const mergeable = tasks2.filter(
212987
- (t) => allowsAutoMergeProcessing(t, settings) && !t.paused && !executingIds.has(t.id) && t.status !== "failed" && // Exclude transient merge statuses. Active merges should be left alone;
213174
+ (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;
212988
213175
  // stale ones are handled by recoverStaleMergingStatus().
212989
213176
  t.status !== "merging" && t.status !== "merging-pr" && // FNXC:Workspace 2026-06-22-09:30 (Phase D U1, KTD1 — admit workspace tasks):
212990
213177
  // A workspace task has task.worktree===null (its worktrees live per-repo in
@@ -216031,7 +216218,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
216031
216218
  for (const t of tasks2) {
216032
216219
  if (!this.isPauseAbortParkCandidate(t)) continue;
216033
216220
  const columns = await this.resolvePauseAbortColumnsFor(t.id, columnCache);
216034
- if (this.classifyPausedAbortWorkflowRecovery(t, settings, executingIds.has(t.id), columns).kind !== "no-action") {
216221
+ if ((await this.classifyPausedAbortWorkflowRecovery(t, settings, executingIds.has(t.id), columns)).kind !== "no-action") {
216035
216222
  parked.push(t);
216036
216223
  }
216037
216224
  }
@@ -216048,7 +216235,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
216048
216235
  continue;
216049
216236
  }
216050
216237
  const freshColumns = await this.resolvePauseAbortColumnsFor(fresh.id, columnCache);
216051
- const route = this.classifyPausedAbortWorkflowRecovery(fresh, settings, latestExecutingIds.has(fresh.id), freshColumns);
216238
+ const route = await this.classifyPausedAbortWorkflowRecovery(fresh, settings, latestExecutingIds.has(fresh.id), freshColumns);
216052
216239
  if (route.kind === "no-action") {
216053
216240
  continue;
216054
216241
  }
@@ -219935,9 +220122,11 @@ function parseWorkflowStepVerdict(rawOutput) {
219935
220122
  verdict = "REVISE";
219936
220123
  }
219937
220124
  if (!verdict) continue;
220125
+ const findings = normalizeWorkflowReviewFindings(parsed.findings);
219938
220126
  return {
219939
220127
  verdict,
219940
- notes: typeof parsed.notes === "string" ? parsed.notes : ""
220128
+ notes: typeof parsed.notes === "string" ? parsed.notes : "",
220129
+ ...findings ? { findings } : {}
219941
220130
  };
219942
220131
  } catch {
219943
220132
  }
@@ -219969,7 +220158,8 @@ function parseWorkflowStepOutput(rawOutput, options = {}) {
219969
220158
  return {
219970
220159
  output: parsed.notes || "",
219971
220160
  verdict: parsed.verdict,
219972
- notes: parsed.notes
220161
+ notes: parsed.notes,
220162
+ ...parsed.findings ? { findings: parsed.findings } : {}
219973
220163
  };
219974
220164
  }
219975
220165
  const inferred = inferWorkflowStepVerdictFromProse(trimmed);
@@ -222770,6 +222960,7 @@ ${feedback}`,
222770
222960
  if (info.phase !== "pre-merge") return false;
222771
222961
  if (info.status !== "advisory_failure" && info.status !== "failed") return false;
222772
222962
  const liveTask = await this.store.getTask(taskId).catch(() => fallbackTask);
222963
+ if (hasUserAutoMergeHold(liveTask)) return false;
222773
222964
  const missingArtifactKeys = parseRequiredArtifactMissingValue(info.failureValue);
222774
222965
  if (missingArtifactKeys) {
222775
222966
  await this.recoverMissingRequiredArtifacts(liveTask, missingArtifactKeys, {
@@ -223026,6 +223217,7 @@ ${info.feedback}`, revisionKey),
223026
223217
  */
223027
223218
  async recoverFailedPreMergeWorkflowStep(task) {
223028
223219
  try {
223220
+ if (hasUserAutoMergeHold(task)) return false;
223029
223221
  const failed = (task.workflowStepResults ?? []).filter((r) => (r.phase || "pre-merge") === "pre-merge" && r.status === "failed").sort((a, b3) => {
223030
223222
  const aTs = Date.parse(a.completedAt || a.startedAt || "");
223031
223223
  const bTs = Date.parse(b3.completedAt || b3.startedAt || "");
@@ -223552,6 +223744,7 @@ ${info.feedback}`, revisionKey),
223552
223744
  getTask: (taskId) => this.store.getTask(taskId)
223553
223745
  },
223554
223746
  runId: resolvedRunId,
223747
+ isLiveSharedBranchMember: (nodeTask) => this.isLiveSharedBranchGroupMember(nodeTask),
223555
223748
  primitives: this.createAuthoritativeWorkflowPrimitives(settings),
223556
223749
  seams: this.createAuthoritativeWorkflowSeams(settings),
223557
223750
  prepareNodeExecution: (node, nodeTask, requirement) => this.prepareGraphNodeExecution(node, nodeTask, settings, requirement),
@@ -225794,6 +225987,7 @@ ${info.feedback}`, revisionKey),
225794
225987
  return this.runCliAgentNode(node, await this.store.getTask(live.id), cfg);
225795
225988
  }
225796
225989
  const optionalGroupId = typeof graphContext?.[WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY] === "string" ? graphContext[WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY] : void 0;
225990
+ const declaredReviewKind = cfg.reviewKind === "plan" || cfg.reviewKind === "code" ? cfg.reviewKind : graphContext?.[WORKFLOW_REVIEW_KIND_CONTEXT_KEY] === "plan" || graphContext?.[WORKFLOW_REVIEW_KIND_CONTEXT_KEY] === "code" ? graphContext[WORKFLOW_REVIEW_KIND_CONTEXT_KEY] : void 0;
225797
225991
  const isCompletionSummaryNode2 = cfg.summaryTarget === "task" || node.id === "completion-summary";
225798
225992
  if (live.executionMode === "fast" && !isCompletionSummaryNode2 && !optionalGroupId && !cfg.seam && (node.kind === "prompt" || node.kind === "script" || node.kind === "gate")) {
225799
225993
  executorLog.debug(`${live.id}: fast mode \u2014 skipping custom graph node '${node.id}'`);
@@ -225942,6 +226136,9 @@ ${prompt}`;
225942
226136
  if (optionalGroupId) {
225943
226137
  step.optionalGroupId = optionalGroupId;
225944
226138
  }
226139
+ if (declaredReviewKind) {
226140
+ step.reviewKind = declaredReviewKind;
226141
+ }
225945
226142
  if (cfg.reviewCanFixInline === true) {
225946
226143
  step.reviewCanFixInline = true;
225947
226144
  }
@@ -225954,7 +226151,11 @@ ${prompt}`;
225954
226151
  executorLog.debug(`${live.id}: graph node '${node.id}' runtime env injected (${injected.pathEntryCount} PATH entries, ${injected.injectedKeyCount} env keys)`);
225955
226152
  }
225956
226153
  const unattended = this.graphUnattendedRuns.has(live.id);
225957
- const outcome = mode === "script" ? await this.executeScriptWorkflowStep(live, step, worktreePath, settings, nodeEnv) : await this.executeWorkflowStep(live, step, worktreePath, settings, nodeEnv, { unattended });
226154
+ let outcome = mode === "script" ? await this.executeScriptWorkflowStep(live, step, worktreePath, settings, nodeEnv) : await this.executeWorkflowStep(live, step, worktreePath, settings, nodeEnv, { unattended });
226155
+ if (declaredReviewKind && typeof outcome.output === "string") {
226156
+ const parsedReviewOutput = parseWorkflowStepOutput(outcome.output, { requireVerdict: false });
226157
+ if (parsedReviewOutput.findings?.length) outcome = { ...outcome, findings: parsedReviewOutput.findings };
226158
+ }
225958
226159
  const awaitQuestion = parseAwaitInputSentinel(outcome.output);
225959
226160
  if (awaitQuestion) {
225960
226161
  await this.store.logEntry(
@@ -225977,6 +226178,8 @@ ${prompt}`;
225977
226178
  const contextPatch = {};
225978
226179
  if (typeof stepOutput === "string") contextPatch.output = stepOutput;
225979
226180
  if (typeof stepNotes === "string" && stepNotes) contextPatch.notes = stepNotes;
226181
+ const stepFindings = outcome.findings;
226182
+ if (stepFindings?.length) contextPatch.findings = stepFindings;
225980
226183
  if (cfg.summaryTarget === "task" && typeof stepOutput === "string" && stepOutput.trim()) {
225981
226184
  contextPatch.summary = stepOutput.trim();
225982
226185
  }
@@ -226483,6 +226686,7 @@ Graph value: ${failureValue}` : ""}`, budget.key),
226483
226686
  } catch {
226484
226687
  return false;
226485
226688
  }
226689
+ if (hasUserAutoMergeHold(live)) return false;
226486
226690
  const sharedBranchMember = await this.isLiveSharedBranchGroupMember(live);
226487
226691
  if (!sharedBranchMember && !allowsAutoMergeProcessing(live, settings)) return false;
226488
226692
  if (!sharedBranchMember && resolveEffectiveAutoMerge(live, settings) === false) return false;
@@ -226544,8 +226748,8 @@ Graph value: ${failureValue}` : ""}`, budget.key),
226544
226748
  } catch {
226545
226749
  return false;
226546
226750
  }
226547
- if (await this.isLiveSharedBranchGroupMember(live)) return false;
226548
- return !allowsAutoMergeProcessing(live, settings) || resolveEffectiveAutoMerge(live, settings) === false;
226751
+ if (await this.isLiveSharedBranchGroupMember(live) && !hasUserAutoMergeHold(live)) return false;
226752
+ return hasUserAutoMergeHold(live) || !allowsAutoMergeProcessing(live, settings) || resolveEffectiveAutoMerge(live, settings) === false;
226549
226753
  }
226550
226754
  async handleStaleInReviewPlanPauseAbortReplay(live, result, abortProvenance, pausedAborted, userCanceled, resumeLanesMemo) {
226551
226755
  if (!pausedAborted) return false;
@@ -226709,9 +226913,9 @@ Graph value: ${failureValue}` : ""}`, budget.key),
226709
226913
  if (settings.globalPause === true) return false;
226710
226914
  }
226711
226915
  if (live.column === resumeLanes.review) {
226712
- if (live.autoMerge === false) return false;
226713
226916
  if (!settings) return false;
226714
226917
  const sharedBranchMember = await this.isLiveSharedBranchGroupMember(live);
226918
+ if (hasUserAutoMergeHold(live) || live.autoMerge === false && !sharedBranchMember) return false;
226715
226919
  if (!sharedBranchMember && !allowsAutoMergeProcessing(live, settings)) return false;
226716
226920
  if (live.mergeDetails?.mergeConfirmed === true) return false;
226717
226921
  }
@@ -232479,13 +232683,14 @@ CRITICAL SCOPING RULES \u2014 read before doing anything else:
232479
232683
  const isSkillStep = typeof workflowStep.skillName === "string" && workflowStep.skillName.trim().length > 0;
232480
232684
  const isSummaryProjectionStep = workflowStep.summaryTarget === "task";
232481
232685
  const requireVerdict = !isSummaryProjectionStep && (workflowStep.gateMode === "gate" || !isSkillStep);
232686
+ const reviewFindingsContract = workflowStepMetadata.reviewKind === "plan" || workflowStepMetadata.reviewKind === "code";
232482
232687
  const verdictBlock = requireVerdict ? `
232483
232688
 
232484
232689
  ## Feedback Format
232485
232690
 
232486
232691
  When your review is complete, your final line MUST be a single JSON object (no markdown fences):
232487
232692
 
232488
- {"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}
232693
+ ${reviewFindingsContract ? '{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"...","findings":[{"id":"stable-id","title":"concise issue","body":"actionable detail","filePath":"optional/path","line":1,"severity":"low|medium|high|critical"}]}' : '{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}'}
232489
232694
 
232490
232695
  Rules:
232491
232696
  - Output exactly one trailing JSON object and stop.
@@ -232795,7 +233000,8 @@ ${detectedQuestion}
232795
233000
  revisionRequested,
232796
233001
  output: parsed.output,
232797
233002
  verdict: parsed.verdict,
232798
- notes: parsed.notes
233003
+ notes: parsed.notes,
233004
+ ...parsed.findings ? { findings: parsed.findings } : {}
232799
233005
  };
232800
233006
  }
232801
233007
  if (parsed.malformed) {
@@ -247766,6 +247972,7 @@ var InProcessRuntime = class extends EventEmitter27 {
247766
247972
  }
247767
247973
  }
247768
247974
  const prNodeGithubOps = this.config.prNodeGithubOps;
247975
+ const secretsStore = await this.taskStore.getSecretsStore();
247769
247976
  const executorOptions = {
247770
247977
  /*
247771
247978
  FNXC:PlanReviewLease 2026-07-26-21:12:
@@ -247781,6 +247988,7 @@ var InProcessRuntime = class extends EventEmitter27 {
247781
247988
  cliAgentRuntime: this.cliAgentRuntime?.bundle,
247782
247989
  pluginRunner: this.pluginRunner,
247783
247990
  messageStore: this.messageStore,
247991
+ secretsStore,
247784
247992
  missionStore,
247785
247993
  reflectionService,
247786
247994
  // PR-entity nodes (U3): assemble the handler deps from the CLI-injected
@@ -247882,6 +248090,7 @@ var InProcessRuntime = class extends EventEmitter27 {
247882
248090
  reflectionService,
247883
248091
  selfImproveService,
247884
248092
  credentialRotator: this.credentialRotator,
248093
+ secretsStore,
247885
248094
  snapshotManager: autoClaimSnapshotManager,
247886
248095
  onMissed: (agentId, reason) => {
247887
248096
  runtimeLog.warn(`Agent ${agentId} missed heartbeat: ${reason}`);
@@ -254921,7 +255130,21 @@ var ProjectEngine = class _ProjectEngine {
254921
255130
  Cleared on the first non-busy outcome (success path resets it).
254922
255131
  */
254923
255132
  workspaceBusyReenqueues = /* @__PURE__ */ new Map();
255133
+ workspaceBusyReenqueueTimers = /* @__PURE__ */ new Set();
254924
255134
  static WORKSPACE_BUSY_MAX_REENQUEUES = 10;
255135
+ /*
255136
+ FNXC:WorkspaceMergeDispatch 2026-08-05-23:56:
255137
+ Workspace lease-contention retries are engine-owned lifecycle work, not detached callbacks.
255138
+ Track only these busy re-enqueue timers so stop() can cancel them and tests can measure the
255139
+ capped workspace ladder without confusing it with merge-body or maintenance timers.
255140
+ */
255141
+ scheduleWorkspaceBusyReenqueue(taskId, delayMs) {
255142
+ const timer2 = setTimeout(() => {
255143
+ this.workspaceBusyReenqueueTimers.delete(timer2);
255144
+ if (!this.shuttingDown) this.internalEnqueueMerge(taskId);
255145
+ }, delayMs);
255146
+ this.workspaceBusyReenqueueTimers.add(timer2);
255147
+ }
254925
255148
  /**
254926
255149
  * Pending manual merge resolvers — keyed by taskId.
254927
255150
  * When `onMerge` is called, the task is enqueued like auto-merge but a
@@ -254982,7 +255205,7 @@ var ProjectEngine = class _ProjectEngine {
254982
255205
  *
254983
255206
  * Readable (not private) so tests derive the cap from this single source of truth rather
254984
255207
  * than hardcoding it — the FN-8004 bump broke two suites that had baked in the old `3`. */
254985
- static MAX_AUTO_MERGE_TRANSIENT_RETRIES = 5;
255208
+ static MAX_AUTO_MERGE_TRANSIENT_RETRIES = MAX_AUTO_MERGE_TRANSIENT_RETRIES;
254986
255209
  static MERGE_REQUEST_RETRY_EXHAUSTED_AGE_MS = 30 * 60 * 1e3;
254987
255210
  /** Cap on outer in-review→in-progress bounces caused by deterministic
254988
255211
  * verification failures during auto-merge. After this many failed merges
@@ -255456,6 +255679,10 @@ ${detail}`);
255456
255679
  clearInterval(this.mergeActiveReconcileTimer);
255457
255680
  this.mergeActiveReconcileTimer = null;
255458
255681
  }
255682
+ for (const timer2 of this.workspaceBusyReenqueueTimers) {
255683
+ clearTimeout(timer2);
255684
+ }
255685
+ this.workspaceBusyReenqueueTimers.clear();
255459
255686
  for (const [taskId, deferred] of this.capacityDeferredMerges) {
255460
255687
  clearTimeout(deferred.timer);
255461
255688
  for (const resolver of deferred.resolvers) {
@@ -256755,16 +256982,15 @@ ${detail}`);
256755
256982
  * older low-priority task would start before a later urgent one.
256756
256983
  */
256757
256984
  async allowInReviewMergeProcessing(task, settings, store = this.runtime.getTaskStore()) {
256758
- if (allowsAutoMergeProcessing(task, settings)) {
256759
- return true;
256760
- }
256985
+ if (hasUserAutoMergeHold(task)) return false;
256761
256986
  const groupId = task.branchContext?.groupId?.trim();
256762
256987
  const branchGroup = groupId ? await store.getBranchGroup?.(groupId) : null;
256763
- if (!branchGroup || branchGroup.status !== "open" || !branchGroup.branchName.trim()) {
256764
- return false;
256765
- }
256766
256988
  const projectDefaultBranch = await resolveIntegrationBranch(this.config.workingDirectory, settings);
256767
- return isLiveSharedBranchGroupMemberIntegration(task, branchGroup, projectDefaultBranch);
256989
+ if (isLiveSharedBranchGroupMemberIntegration(task, branchGroup, projectDefaultBranch)) {
256990
+ return true;
256991
+ }
256992
+ if (task.autoMerge === false && groupId) return false;
256993
+ return allowsAutoMergeProcessing(task, settings);
256768
256994
  }
256769
256995
  async emitLegacyAutoMergeStampAdvisory(store) {
256770
256996
  if (this.legacyAutoMergeStampAdvisoryEmitted) {
@@ -257717,9 +257943,7 @@ ${detail}`);
257717
257943
  runtimeLog.log(
257718
257944
  `Workspace land busy re-enqueue ${busyCount + 1}/${_ProjectEngine.WORKSPACE_BUSY_MAX_REENQUEUES} for ${taskId} in ${delayMs / 1e3}s (no mergeRetry consumed \u2014 pure lease contention)`
257719
257945
  );
257720
- setTimeout(() => {
257721
- if (!this.shuttingDown) this.internalEnqueueMerge(taskId);
257722
- }, delayMs);
257946
+ this.scheduleWorkspaceBusyReenqueue(taskId, delayMs);
257723
257947
  } else {
257724
257948
  this.workspaceBusyReenqueues.delete(taskId);
257725
257949
  await store.updateTask(taskId, { status: "failed", error: errorMsg }).catch(() => void 0);