@runfusion/fusion 0.77.0-beta.7 → 0.77.0-beta.8

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 +548 -86
  2. package/dist/child-process-worker.js +546 -86
  3. package/dist/client/.vite/manifest.json +249 -249
  4. package/dist/client/assets/{AgentDetailView-C1eh_vXv.js → AgentDetailView-DsMRc-aH.js} +1 -1
  5. package/dist/client/assets/{AgentPermissionPolicyEditor-CeKsO1LR.js → AgentPermissionPolicyEditor-BXJ52YpO.js} +1 -1
  6. package/dist/client/assets/{AgentsView-BNe2ABOc.js → AgentsView-DO-25nlB.js} +3 -3
  7. package/dist/client/assets/{CommandCenter-BuREM-G1.js → CommandCenter-BMXMg_I2.js} +1 -1
  8. package/dist/client/assets/{DevServerView-D0i1b-QO.js → DevServerView-DAeL5TYz.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-VOx91hQt.js → DirectoryPicker-Cw-XRjAa.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-CO5W_WVb.js → DocumentsView-CusClN9u.js} +1 -1
  11. package/dist/client/assets/{EvalsView-BqSINSrZ.js → EvalsView-gNdeUDww.js} +1 -1
  12. package/dist/client/assets/{ExperimentalAgentOnboardingModal-DEcFgONi.js → ExperimentalAgentOnboardingModal-Bmzbqzhs.js} +1 -1
  13. package/dist/client/assets/{GoalsView-DZ57wXmJ.js → GoalsView-J8JMsoLn.js} +1 -1
  14. package/dist/client/assets/{InsightsView-DSWfXG7Q.js → InsightsView-BVbv56K0.js} +1 -1
  15. package/dist/client/assets/{MemoryView-BEX10uhl.js → MemoryView-DNvJFoDu.js} +1 -1
  16. package/dist/client/assets/{PiExtensionsManager-DcAmXXoR.js → PiExtensionsManager-BPMexS34.js} +1 -1
  17. package/dist/client/assets/{PluginManager-BU0omQUv.js → PluginManager-DGEv-ptu.js} +1 -1
  18. package/dist/client/assets/{PullRequestView-B-JL_8Kk.js → PullRequestView-CF7fx-Uo.js} +1 -1
  19. package/dist/client/assets/ReportModal-B8S5BFgn.js +21 -0
  20. package/dist/client/assets/ReportModal-Dx3f2aj7.css +1 -0
  21. package/dist/client/assets/{ResearchView-CnwTXfQO.js → ResearchView-CBeXsevy.js} +1 -1
  22. package/dist/client/assets/{SecretsView-BDvzsLFz.js → SecretsView-Dmi9wZkv.js} +1 -1
  23. package/dist/client/assets/{SettingsModal-801eFQ9u.js → SettingsModal-D2kXP7Um.js} +3 -3
  24. package/dist/client/assets/{SettingsModal-BZHeQ3ED.js → SettingsModal-DSW3xnGk.js} +1 -1
  25. package/dist/client/assets/{SettingsTextareaRow-B0rTuelr.js → SettingsTextareaRow-CLab4Vcd.js} +1 -1
  26. package/dist/client/assets/{SetupWizardModal-CoExPwIn.js → SetupWizardModal-Ct6juX9x.js} +2 -2
  27. package/dist/client/assets/{SkillsView-WVP25LPj.js → SkillsView-Dn0bwT6i.js} +1 -1
  28. package/dist/client/assets/{WorkflowNodeEditor-pCMyKcnS.js → WorkflowNodeEditor-cvReMlG-.js} +1 -1
  29. package/dist/client/assets/{agent-import-generation-M_hKtjgN.js → agent-import-generation-C8_ewVzC.js} +1 -1
  30. package/dist/client/assets/{agentsOrgChartLayout-DjvIMhFD.js → agentsOrgChartLayout-p1i6XROQ.js} +1 -1
  31. package/dist/client/assets/{architectureDiagram-3BPJPVTR-CkzEm8i9.js → architectureDiagram-3BPJPVTR-CzCk1QdH.js} +1 -1
  32. package/dist/client/assets/{blockDiagram-GPEHLZMM-Dc-dUMjY.js → blockDiagram-GPEHLZMM-74Rj-5ov.js} +1 -1
  33. package/dist/client/assets/{c4Diagram-AAUBKEIU-DnYjsrR0.js → c4Diagram-AAUBKEIU-B570qYH-.js} +1 -1
  34. package/dist/client/assets/channel-DWAK5y-J.js +1 -0
  35. package/dist/client/assets/{chunk-2J33WTMH-0E1GEFEr.js → chunk-2J33WTMH-DejJkxt6.js} +1 -1
  36. package/dist/client/assets/{chunk-4BX2VUAB-Dj5oZd51.js → chunk-4BX2VUAB-DlgxkBuE.js} +1 -1
  37. package/dist/client/assets/{chunk-55IACEB6-DfNT0s8E.js → chunk-55IACEB6-XCfE8PwC.js} +1 -1
  38. package/dist/client/assets/{chunk-727SXJPM-BnFjhiIQ.js → chunk-727SXJPM-D-nDw0Bi.js} +1 -1
  39. package/dist/client/assets/{chunk-AQP2D5EJ-DR84wSqR.js → chunk-AQP2D5EJ-C4KySao0.js} +1 -1
  40. package/dist/client/assets/{chunk-FMBD7UC4-IjByNOrR.js → chunk-FMBD7UC4-CXKdLygA.js} +1 -1
  41. package/dist/client/assets/{chunk-ND2GUHAM-By0q_jZm.js → chunk-ND2GUHAM-bNYWWGVJ.js} +1 -1
  42. package/dist/client/assets/{chunk-QZHKN3VN-BELcl3N5.js → chunk-QZHKN3VN-cfBQJ-MX.js} +1 -1
  43. package/dist/client/assets/classDiagram-4FO5ZUOK-WTkXRkfm.js +1 -0
  44. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-WTkXRkfm.js +1 -0
  45. package/dist/client/assets/{cose-bilkent-S5V4N54A-D4cCw9-_.js → cose-bilkent-S5V4N54A-CiZrg3mQ.js} +1 -1
  46. package/dist/client/assets/{dagre-BM42HDAG-JJp5dVeY.js → dagre-BM42HDAG-Bckph2U4.js} +1 -1
  47. package/dist/client/assets/{dashboard-view-Bl9CPhxE.js → dashboard-view-CN_fkDM1.js} +1 -1
  48. package/dist/client/assets/{dashboard-view-C5Nt8cvY.js → dashboard-view-CVYHQZwx.js} +1 -1
  49. package/dist/client/assets/{dashboard-view-6zPJm9zC.js → dashboard-view-DDRKwfFs.js} +1 -1
  50. package/dist/client/assets/{dashboard-view-MAC_LHDX.js → dashboard-view-IuPjDVGa.js} +1 -1
  51. package/dist/client/assets/{diagram-2AECGRRQ-D4L7j_t_.js → diagram-2AECGRRQ-Bnaurex7.js} +1 -1
  52. package/dist/client/assets/{diagram-5GNKFQAL-VAGFaXqG.js → diagram-5GNKFQAL-BKz1omVN.js} +1 -1
  53. package/dist/client/assets/{diagram-KO2AKTUF-hqXSggfW.js → diagram-KO2AKTUF-C51lEZwo.js} +1 -1
  54. package/dist/client/assets/{diagram-LMA3HP47-B3eAIuy8.js → diagram-LMA3HP47-BYm5gql7.js} +1 -1
  55. package/dist/client/assets/{diagram-OG6HWLK6-BGufp0RK.js → diagram-OG6HWLK6-B9CFA4ha.js} +1 -1
  56. package/dist/client/assets/{erDiagram-TEJ5UH35-DplXdx8w.js → erDiagram-TEJ5UH35-BLyCSssl.js} +1 -1
  57. package/dist/client/assets/{flowDiagram-I6XJVG4X-C1Ms2bko.js → flowDiagram-I6XJVG4X-yDVmpkro.js} +1 -1
  58. package/dist/client/assets/{folder-open-nMraSDXu.js → folder-open-Cf2NA9nV.js} +1 -1
  59. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DVgxMtXb.js → ganttDiagram-6RSMTGT7-mC9YPFcx.js} +1 -1
  60. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DH2_n0J8.js → gitGraphDiagram-PVQCEYII-DS0bEccv.js} +1 -1
  61. package/dist/client/assets/index-BjM7LxzY.js +2805 -0
  62. package/dist/client/assets/index-DcKIzuMk.css +1 -0
  63. package/dist/client/assets/{infoDiagram-5YYISTIA-DmZdWVcU.js → infoDiagram-5YYISTIA-CuuNZITP.js} +1 -1
  64. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DsEIHSXr.js → ishikawaDiagram-YF4QCWOH-MBSaC94m.js} +1 -1
  65. package/dist/client/assets/{journeyDiagram-JHISSGLW-CsfIWmu1.js → journeyDiagram-JHISSGLW-DkaAow9E.js} +1 -1
  66. package/dist/client/assets/{kanban-definition-UN3LZRKU-CQKqeZvc.js → kanban-definition-UN3LZRKU-DSf5OLtq.js} +1 -1
  67. package/dist/client/assets/{mermaid.core-DkC_dr4-.js → mermaid.core-HmTn21MF.js} +4 -4
  68. package/dist/client/assets/{mindmap-definition-RKZ34NQL-RnmE-NUe.js → mindmap-definition-RKZ34NQL-sv0Dl8aI.js} +1 -1
  69. package/dist/client/assets/{pieDiagram-4H26LBE5-DPu6FPc4.js → pieDiagram-4H26LBE5-BDK2IQNq.js} +1 -1
  70. package/dist/client/assets/{puzzle-DwhHOf32.js → puzzle-D00mP67o.js} +1 -1
  71. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-CYbypO2p.js → quadrantDiagram-W4KKPZXB-CiWAhwf1.js} +1 -1
  72. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DFk1vK_N.js → requirementDiagram-4Y6WPE33-DwC99D35.js} +1 -1
  73. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CkUY1Zzu.js → sankeyDiagram-5OEKKPKP-CQn8z721.js} +1 -1
  74. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CFivaLOO.js → sequenceDiagram-3UESZ5HK-B0CT8-vw.js} +1 -1
  75. package/dist/client/assets/{standing-instructions-template-BbqKGAC1.js → standing-instructions-template-BIBO_Prr.js} +1 -1
  76. package/dist/client/assets/{stateDiagram-AJRCARHV-BPeD5PwG.js → stateDiagram-AJRCARHV-BYAB_dFF.js} +1 -1
  77. package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-DduiCPMb.js → stateDiagram-v2-BHNVJYJU-Cd19uJBk.js} +1 -1
  78. package/dist/client/assets/{timeline-definition-PNZ67QCA-CGUNNVro.js → timeline-definition-PNZ67QCA-DQLh8XkY.js} +1 -1
  79. package/dist/client/assets/{upload-BS96H1o7.js → upload-D4pkNOz2.js} +1 -1
  80. package/dist/client/assets/{users--irrWoyU.js → users-COgviS_S.js} +1 -1
  81. package/dist/client/assets/{vennDiagram-CIIHVFJN-CybTaJJH.js → vennDiagram-CIIHVFJN-BmwG1Wud.js} +1 -1
  82. package/dist/client/assets/{wardley-L42UT6IY-DSxaSCGi.js → wardley-L42UT6IY-BiO7jkmj.js} +1 -1
  83. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-YFb0AxNt.js → wardleyDiagram-YWT4CUSO-Dq0IPg5H.js} +1 -1
  84. package/dist/client/assets/{xychartDiagram-2RQKCTM6-Cv6zlMJC.js → xychartDiagram-2RQKCTM6-BlMOlSBc.js} +1 -1
  85. package/dist/client/index.html +13 -6
  86. package/dist/client/theme-data.css +85 -0
  87. package/dist/client/version.json +1 -1
  88. package/dist/droid-cli/package.json +1 -1
  89. package/dist/extension.js +548 -86
  90. package/dist/pi-claude-cli/package.json +1 -1
  91. package/dist/plugin-sdk/index.d.ts +5 -1
  92. package/dist/plugin-sdk/index.js +43 -19
  93. package/dist/plugins/fusion-plugin-acp-runtime/bundled.js +43 -19
  94. package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
  95. package/dist/plugins/fusion-plugin-claude-runtime/bundled.js +43 -19
  96. package/dist/plugins/fusion-plugin-claude-runtime/package.json +1 -1
  97. package/dist/plugins/fusion-plugin-cli-printing-press/bundled.js +43 -19
  98. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  99. package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +43 -19
  100. package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
  101. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +43 -19
  102. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  103. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +43 -19
  104. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  105. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +43 -19
  106. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  107. package/dist/plugins/fusion-plugin-grok-runtime/bundled.js +43 -19
  108. package/dist/plugins/fusion-plugin-grok-runtime/package.json +1 -1
  109. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +43 -19
  110. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  111. package/dist/plugins/fusion-plugin-linear-import/bundled.js +43 -19
  112. package/dist/plugins/fusion-plugin-linear-import/package.json +1 -1
  113. package/dist/plugins/fusion-plugin-omp-runtime/bundled.js +43 -19
  114. package/dist/plugins/fusion-plugin-omp-runtime/package.json +1 -1
  115. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +43 -19
  116. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  117. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +43 -19
  118. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  119. package/dist/plugins/fusion-plugin-quality/bundled.js +43 -19
  120. package/dist/plugins/fusion-plugin-quality/package.json +1 -1
  121. package/dist/plugins/fusion-plugin-reports/bundled.js +43 -19
  122. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  123. package/dist/plugins/fusion-plugin-roadmap/bundled.js +43 -19
  124. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  125. package/dist/plugins/fusion-plugin-todos/bundled.js +43 -19
  126. package/dist/plugins/fusion-plugin-todos/package.json +1 -1
  127. package/dist/plugins/fusion-plugin-whatsapp-chat/bundled.js +43 -19
  128. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  129. package/package.json +1 -1
  130. package/dist/client/assets/ReportModal-DxggF_RH.js +0 -21
  131. package/dist/client/assets/ReportModal-lzN_xrnM.css +0 -1
  132. package/dist/client/assets/channel-CxQ40s_U.js +0 -1
  133. package/dist/client/assets/classDiagram-4FO5ZUOK-C2ARdiRL.js +0 -1
  134. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C2ARdiRL.js +0 -1
  135. package/dist/client/assets/index-BJc-A7eB.css +0 -1
  136. package/dist/client/assets/index-CBFPgFTp.js +0 -2787
package/dist/bin.js CHANGED
@@ -1099,6 +1099,8 @@ var init_execution_and_ui = __esm({
1099
1099
  "midnight",
1100
1100
  // FNXC:DashboardTheming 2026-08-18-05:45: Velvet is persisted immediately after Midnight; keep this ID and order synchronized across core, selector metadata, and both first-paint validators so saved selections survive pre-hydration validation.
1101
1101
  "velvet",
1102
+ // FNXC:DashboardTheming 2026-08-23-01:51: Iceberg is persisted immediately after Velvet; keep this ID and order synchronized across core, selector metadata, both first-paint validators, token blocks, and swatch CSS so saved selections survive pre-hydration validation.
1103
+ "iceberg",
1102
1104
  // FNXC:DashboardTheming 2026-07-31-20:39: Factory Dark must stay order-synchronized across core, selector metadata, and both first-paint validators so saved preferences survive pre-hydration validation.
1103
1105
  "factory-dark",
1104
1106
  // FNXC:DashboardTheming 2026-07-31-23:51: Factory Light must stay order-synchronized across core, selector metadata, and both first-paint validators so saved preferences survive pre-hydration validation.
@@ -1577,6 +1579,13 @@ function getTaskHardMergeBlocker(task, options = {}) {
1577
1579
  requiredPreMergeStepIds: options.requiredPreMergeStepIds
1578
1580
  });
1579
1581
  }
1582
+ function getMergeConfirmedFinalizationBlocker(task, options = {}) {
1583
+ const hasDurableMergeRecord = task.mergeDetails?.mergeConfirmed === true && typeof task.mergeDetails.commitSha === "string" && task.mergeDetails.commitSha.length > 0;
1584
+ return getTaskHardMergeBlocker(hasDurableMergeRecord ? { ...task, steps: [] } : task, options);
1585
+ }
1586
+ function getUnfinishedStepTitles(task) {
1587
+ return (task.steps ?? []).filter((step) => NON_TERMINAL_STEP_STATUSES.has(step.status)).map((step, index3) => step.name?.trim() || `step ${index3 + 1}`);
1588
+ }
1580
1589
  function isTaskReadyForMerge(task, options = {}) {
1581
1590
  return getTaskMergeBlocker(task, options) === void 0;
1582
1591
  }
@@ -2563,12 +2572,16 @@ var init_settings_schema = __esm({
2563
2572
  executorEscalationProvider: void 0,
2564
2573
  executorEscalationModelId: void 0,
2565
2574
  executorEscalationNodeId: void 0,
2566
- reviewConvergenceEscalationEnabled: true,
2567
- reviewConvergenceEscalationProvider: void 0,
2568
- reviewConvergenceEscalationModelId: void 0,
2569
- reviewArbitrationEnabled: true,
2570
- reviewArbitrationProvider: void 0,
2571
- reviewArbitrationModelId: void 0,
2575
+ /*
2576
+ FNXC:ReviewConvergence 2026-08-23-21:05:
2577
+ FN-149's six review-convergence/arbitration keys are declared in BUILTIN_WORKFLOW_SETTINGS, which
2578
+ the U4 hard-move made the SINGLE source of truth for a moved key's default. Re-declaring them here
2579
+ put them back in the legacy object the hard-move emptied, so an undeclared key on a custom workflow
2580
+ could resolve to this literal instead of the declaration default — the exact drift
2581
+ `workflow-settings-fallback-alignment` guards. The declaration defaults are identical (both
2582
+ `*Enabled` default true, the provider/model lanes carry no default), and the reader uses
2583
+ `!== false`, so removing them here preserves behaviour and restores the invariant.
2584
+ */
2572
2585
  /**
2573
2586
  * FNXC:Merge 2026-06-26-00:00:
2574
2587
  * New and unconfigured projects default AI merge to sync a dirty checked-out integration branch, restoring the legacy stash → fast-forward → restore landing behavior. Explicit persisted merger.allowDirtyLocalCheckoutSync values still win, and no existing-project migration stamps this default into storage.
@@ -10729,6 +10742,35 @@ var init_workflow_ir_types = __esm({
10729
10742
  }
10730
10743
  });
10731
10744
 
10745
+ // ../core/src/workflows/workflow-merge-region.ts
10746
+ function isMergeRegionNode(node) {
10747
+ return MERGE_REGION_ENTRY_NODE_KINDS.has(node.kind) || node.config?.seam === "merge";
10748
+ }
10749
+ function classifyWorkflowNodeMergeRegion(ir2, nodeId) {
10750
+ if (ir2.version !== "v2" || !Array.isArray(ir2.nodes)) return "unknown";
10751
+ const node = ir2.nodes.find((candidate) => candidate.id === nodeId);
10752
+ if (!node) return "unknown";
10753
+ return isMergeRegionNode(node) ? "merge-region" : "outside-merge-region";
10754
+ }
10755
+ var MERGE_REGION_ENTRY_NODE_KINDS;
10756
+ var init_workflow_merge_region = __esm({
10757
+ "../core/src/workflows/workflow-merge-region.ts"() {
10758
+ "use strict";
10759
+ MERGE_REGION_ENTRY_NODE_KINDS = /* @__PURE__ */ new Set([
10760
+ "merge-gate",
10761
+ "merge-attempt",
10762
+ "manual-merge-hold",
10763
+ "retry-backoff",
10764
+ "recovery-router",
10765
+ "branch-group-member-integration",
10766
+ "branch-group-promotion",
10767
+ "pr-create",
10768
+ "pr-respond",
10769
+ "pr-merge"
10770
+ ]);
10771
+ }
10772
+ });
10773
+
10732
10774
  // ../core/src/workflows/workflow-extension-types.ts
10733
10775
  function workflowExtensionRegistryId(pluginId, extensionId) {
10734
10776
  return `plugin:${pluginId}:${extensionId}`;
@@ -12468,6 +12510,7 @@ var init_workflow_ir = __esm({
12468
12510
  "../core/src/workflows/workflow-ir.ts"() {
12469
12511
  "use strict";
12470
12512
  init_workflow_ir_types();
12513
+ init_workflow_merge_region();
12471
12514
  init_workflow_extension_registry();
12472
12515
  init_types();
12473
12516
  init_trait_registry();
@@ -12548,18 +12591,7 @@ var init_workflow_ir = __esm({
12548
12591
  "done",
12549
12592
  "archived"
12550
12593
  ];
12551
- INTERPRETER_ENTRY_NODE_KINDS = /* @__PURE__ */ new Set([
12552
- "merge-gate",
12553
- "merge-attempt",
12554
- "manual-merge-hold",
12555
- "retry-backoff",
12556
- "recovery-router",
12557
- "branch-group-member-integration",
12558
- "branch-group-promotion",
12559
- "pr-create",
12560
- "pr-respond",
12561
- "pr-merge"
12562
- ]);
12594
+ INTERPRETER_ENTRY_NODE_KINDS = MERGE_REGION_ENTRY_NODE_KINDS;
12563
12595
  MERGE_CLASS_NODE_KINDS = /* @__PURE__ */ new Set([
12564
12596
  "merge-gate",
12565
12597
  "merge-attempt",
@@ -13965,6 +13997,13 @@ Lint, tests, and typecheck are also hard quality gates:
13965
13997
  - Keep fixing failures caused by your change until lint, impacted tests, build, and typecheck pass.
13966
13998
  - If the repository exposes a typecheck command, run it and fix failures caused by your change.
13967
13999
  - When tests fail, classify whether the failure is caused by your change, a pre-existing defect, an unrelated flaky test, or an outdated test expectation.
14000
+
14001
+ **A behavior change owns every test that asserts the old behavior.** Targeted verification runs the tests for files you TOUCHED; the tests encoding the behavior you just changed usually live in files you did NOT touch, so a green targeted run is not evidence that none exist. Before finishing, search for them:
14002
+ - Added a guard, validation, or required field? Grep for the FIXTURES it will now refuse \u2014 they are outside your File Scope \u2014 and fix each to state its intent explicitly.
14003
+ - Removed a feature? Delete its tests. A test asserting a deliberately removed contract guards nothing, and making it pass later means re-adding removed behavior.
14004
+ - Changed an order, default, constant, or prompt string? Grep for the literal; topology lists, prompt-content assertions, and size budgets live far from the code they describe.
14005
+ - Added a public store/service method? Update the inventory/drift guards that require every public surface to be classified.
14006
+ Fix at the SHARED FIXTURE FACTORY, not per test \u2014 one helper usually explains dozens of failures. Never make a stale test pass by weakening it: update the fixture, record the new truth, or delete the test naming the change that removed its subject.
13968
14007
  - If broad workspace verification fails on unrelated or pre-existing failures after impacted checks pass, do NOT expand this task by fixing unrelated areas. Log the evidence, quarantine flakes per project policy, or create/link a follow-up task.
13969
14008
  - Do not repeatedly rerun a broad failing or hanging workspace command without a new hypothesis and a narrower confirming command.
13970
14009
 
@@ -14237,6 +14276,7 @@ files with assertions that run via a test runner. Typechecks and builds are NOT
14237
14276
  tests. Manual verification is NOT a test.
14238
14277
 
14239
14278
  - Each implementation step should include writing tests for the code being changed
14279
+ - **If the task CHANGES, GATES, or REMOVES existing behavior, include a step to find and update the tests that assert the OLD behavior.** Name the search in that step: the fixtures a new guard will now refuse, the tests of a removed feature, or the literal to grep for a changed order/default/constant/prompt string. Those tests live OUTSIDE the File Scope, so targeted verification will not surface them and the executor will not find them by accident. Measured 2026-08-24: five such changes left ~135 stale failing tests behind, every one having passed its own targeted verification; one shared fixture line accounted for 37 of them.
14240
14280
  - For bug fixes and UI-affordance add/remove tasks, the spec MUST include a \`## Surface Enumeration\` section. The workflow Plan Review gate validates this before execution when plan review is enabled; missing coverage is a blocking REVISE.
14241
14281
  - For bug fixes and UI-affordance add/remove tasks, populate \`## Surface Enumeration\` with this checklist from \`docs/testing.md\`: providers/bridges/execution paths; desktop + mobile breakpoints/platforms; empty/undefined/duplicate/populated data states; shared hooks/components/modules/helpers; every component that renders the affordance; leftover shells after removal.
14242
14282
  - For bug fixes and UI-affordance add/remove tasks, regression tests must assert the invariant across all known surfaces \u2014 enumerate every provider/bridge, desktop + mobile breakpoints, empty/undefined/populated data states, and for UI-affordance changes every component rendering the affordance plus leftover shells after removal \u2014 not just the reported repro (see FN-5787/FN-5789/FN-5803, FN-5751, and FN-6115/FN-6118/FN-6123)
@@ -24561,7 +24601,16 @@ var init_project = __esm({
24561
24601
  "chat_rooms",
24562
24602
  "chat_room_members",
24563
24603
  "chat_room_messages",
24564
- "chat_token_usage"
24604
+ "chat_token_usage",
24605
+ /*
24606
+ FNXC:WorkspaceCoordination 2026-08-23-20:05:
24607
+ FN-9059 (migration 0060) added these two tables to the schema without registering them here, so
24608
+ every registry consumer under-counted them: the PostgreSQL test harness never TRUNCATEd them
24609
+ between tests (a held `repo:<repo>` lease leaked from one case into the next and made the next
24610
+ land report the previous test's successor as the busy holder), and health compaction skipped them.
24611
+ */
24612
+ "workspace_coordination_leases",
24613
+ "workspace_land_intents"
24565
24614
  ];
24566
24615
  }
24567
24616
  });
@@ -28691,10 +28740,6 @@ function resolveRequiredPreMergeStepIds(ir2, enabledWorkflowSteps) {
28691
28740
  resolveWorkflowOptionalSteps(ir2).filter((step) => step.phase === "pre-merge").filter((step) => isWorkflowOptionalGroupEnabled(enabledWorkflowSteps, step.templateId, step.defaultOn)).map((step) => step.templateId)
28692
28741
  );
28693
28742
  }
28694
- function findUnrunRequiredPreMergeStepIds(ir2, task) {
28695
- const results = task.workflowStepResults ?? [];
28696
- return [...resolveRequiredPreMergeStepIds(ir2, task.enabledWorkflowSteps)].filter((workflowStepId) => !results.some((result2) => result2.workflowStepId === workflowStepId));
28697
- }
28698
28743
  var init_required_pre_merge_steps = __esm({
28699
28744
  "../core/src/merge/required-pre-merge-steps.ts"() {
28700
28745
  "use strict";
@@ -28702,6 +28747,42 @@ var init_required_pre_merge_steps = __esm({
28702
28747
  }
28703
28748
  });
28704
28749
 
28750
+ // ../core/src/merge/merge-sweep-admission.ts
28751
+ function classifyMergeSweepAdmission(input) {
28752
+ if (input.hasLiveSession) return { admit: false, reason: "live-session" };
28753
+ if (input.mergeConfirmed) return { admit: true, reason: "merge-confirmed-finalization" };
28754
+ if (!input.gatesSatisfied) return { admit: false, reason: "gates-unsatisfied" };
28755
+ if (!input.continuationsReadable) return { admit: false, reason: "continuations-unreadable" };
28756
+ if (input.irTrust === "unresolved-selection") {
28757
+ return quiescentStallDecision(input, "unresolved-workflow-ir");
28758
+ }
28759
+ if (input.continuationPositions.length > 0) {
28760
+ if (input.continuationPositions.includes("merge-region")) {
28761
+ return { admit: true, reason: "at-merge-region-node" };
28762
+ }
28763
+ if (input.continuationPositions.includes("unknown")) {
28764
+ return { admit: true, reason: "drifted-continuation-node" };
28765
+ }
28766
+ return { admit: false, reason: "not-at-merge-region-node" };
28767
+ }
28768
+ if (input.interruptedMergeAttempt) {
28769
+ return { admit: true, reason: "interrupted-merge-attempt" };
28770
+ }
28771
+ return quiescentStallDecision(input, "quiescent-stall-recovery");
28772
+ }
28773
+ function quiescentStallDecision(input, reason) {
28774
+ const floorMs = input.quiescenceFloorMs ?? DEFAULT_MERGE_SWEEP_QUIESCENCE_MS;
28775
+ if (!(input.quiescentMs >= floorMs)) return { admit: false, reason: "too-recent" };
28776
+ return { admit: true, reason };
28777
+ }
28778
+ var DEFAULT_MERGE_SWEEP_QUIESCENCE_MS;
28779
+ var init_merge_sweep_admission = __esm({
28780
+ "../core/src/merge/merge-sweep-admission.ts"() {
28781
+ "use strict";
28782
+ DEFAULT_MERGE_SWEEP_QUIESCENCE_MS = 2 * 6e4;
28783
+ }
28784
+ });
28785
+
28705
28786
  // ../core/src/config/moved-settings.ts
28706
28787
  function isMovedSettingsKey(key) {
28707
28788
  return MOVED_SETTINGS_KEY_SET.has(key);
@@ -71712,6 +71793,21 @@ async function listWorkflowWorkItemsForTaskImpl(store, taskId, opts = {}) {
71712
71793
  const rows = opts.kinds?.length ? await layer.db.select().from(project_exports.workflowWorkItems).where(and(scope, eq(project_exports.workflowWorkItems.taskId, taskId), inArray(project_exports.workflowWorkItems.kind, opts.kinds))) : await q2;
71713
71794
  return rows.map((row) => store.rowToWorkflowWorkItem(row));
71714
71795
  }
71796
+ async function listWorkflowWorkItemsForTasksImpl(store, taskIds, opts = {}) {
71797
+ const byTask = /* @__PURE__ */ new Map();
71798
+ for (const taskId of taskIds) byTask.set(taskId, []);
71799
+ if (taskIds.length === 0) return byTask;
71800
+ const layer = store.asyncLayer;
71801
+ const scope = projectScopeFor(project_exports.workflowWorkItems.projectId, layer.projectId);
71802
+ const conditions = [scope, inArray(project_exports.workflowWorkItems.taskId, [...taskIds])];
71803
+ if (opts.kinds?.length) conditions.push(inArray(project_exports.workflowWorkItems.kind, opts.kinds));
71804
+ const rows = await layer.db.select().from(project_exports.workflowWorkItems).where(and(...conditions));
71805
+ for (const row of rows) {
71806
+ const item = store.rowToWorkflowWorkItem(row);
71807
+ byTask.get(item.taskId)?.push(item);
71808
+ }
71809
+ return byTask;
71810
+ }
71715
71811
  async function listDueWorkflowWorkItemsImpl(store, filter2 = {}) {
71716
71812
  const layer = store.asyncLayer;
71717
71813
  return listDueWorkflowWorkItems(layer.db, filter2);
@@ -91030,6 +91126,17 @@ async function getMergeRequestRecordAsyncImpl(store, taskId) {
91030
91126
  const row = rows[0];
91031
91127
  return row ? store.rowToMergeRequestRecord(row) : null;
91032
91128
  }
91129
+ async function getMergeRequestRecordsAsyncImpl(store, taskIds) {
91130
+ const byTask = /* @__PURE__ */ new Map();
91131
+ if (taskIds.length === 0) return byTask;
91132
+ const layer = store.asyncLayer;
91133
+ const rows = await layer.db.select().from(project_exports.mergeRequests).where(inArray(project_exports.mergeRequests.taskId, [...taskIds]));
91134
+ for (const row of rows) {
91135
+ const record2 = store.rowToMergeRequestRecord(row);
91136
+ byTask.set(record2.taskId, record2);
91137
+ }
91138
+ return byTask;
91139
+ }
91033
91140
  function getWorkflowWorkItemByIdentityImpl(store, runId, taskId, nodeId, kind) {
91034
91141
  const row = store.db.prepare("SELECT * FROM workflow_work_items WHERE runId = ? AND taskId = ? AND nodeId = ? AND kind = ?").get(runId, taskId, nodeId, kind);
91035
91142
  return row ? store.rowToWorkflowWorkItem(row) : null;
@@ -106989,6 +107096,10 @@ var init_store = __esm({
106989
107096
  async getMergeRequestRecordAsync(taskId) {
106990
107097
  return getMergeRequestRecordAsyncImpl(this, taskId);
106991
107098
  }
107099
+ /** Batched `getMergeRequestRecordAsync` — one query for many tasks (FNXC:MergeAuthority 2026-08-23-20:05). */
107100
+ async getMergeRequestRecordsAsync(taskIds) {
107101
+ return getMergeRequestRecordsAsyncImpl(this, taskIds);
107102
+ }
106992
107103
  async projectMergeRequestToWorkflowWorkItem(taskId, opts = {}) {
106993
107104
  return projectMergeRequestToWorkflowWorkItemImpl(this, taskId, opts);
106994
107105
  }
@@ -107031,6 +107142,10 @@ var init_store = __esm({
107031
107142
  async listWorkflowWorkItemsForTask(taskId, opts = {}) {
107032
107143
  return listWorkflowWorkItemsForTaskImpl(this, taskId, opts);
107033
107144
  }
107145
+ /** Batched `listWorkflowWorkItemsForTask` — one query for many tasks (FNXC:MergeAuthority 2026-08-23-20:05). */
107146
+ async listWorkflowWorkItemsForTasks(taskIds, opts = {}) {
107147
+ return listWorkflowWorkItemsForTasksImpl(this, taskIds, opts);
107148
+ }
107034
107149
  /**
107035
107150
  * FNXC:RuntimeWorkflowAsync 2026-06-24-17:12:
107036
107151
  */
@@ -136743,6 +136858,7 @@ __export(src_exports, {
136743
136858
  DEFAULT_MAX_WORKTREES: () => DEFAULT_MAX_WORKTREES,
136744
136859
  DEFAULT_MEMORY_BACKEND: () => DEFAULT_MEMORY_BACKEND,
136745
136860
  DEFAULT_MEMORY_DREAMS_SCHEDULE: () => DEFAULT_MEMORY_DREAMS_SCHEDULE,
136861
+ DEFAULT_MERGE_SWEEP_QUIESCENCE_MS: () => DEFAULT_MERGE_SWEEP_QUIESCENCE_MS,
136746
136862
  DEFAULT_MIN_INTERVAL_MS: () => DEFAULT_MIN_INTERVAL_MS,
136747
136863
  DEFAULT_MISSION_ASSERTION_TYPE: () => DEFAULT_MISSION_ASSERTION_TYPE,
136748
136864
  DEFAULT_MOBILE_NAV_PRIMARY_ITEMS: () => DEFAULT_MOBILE_NAV_PRIMARY_ITEMS,
@@ -136909,6 +137025,7 @@ __export(src_exports, {
136909
137025
  MERGE_AUDIT_AUTO_RECOVERY_MODES: () => MERGE_AUDIT_AUTO_RECOVERY_MODES,
136910
137026
  MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT: () => MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT,
136911
137027
  MERGE_CONFIRMED_TRANSIENT_STATUSES: () => MERGE_CONFIRMED_TRANSIENT_STATUSES,
137028
+ MERGE_REGION_ENTRY_NODE_KINDS: () => MERGE_REGION_ENTRY_NODE_KINDS,
136912
137029
  MESH_INTENT_STATES: () => MESH_INTENT_STATES,
136913
137030
  MESH_RECONCILIATION_OUTCOMES: () => MESH_RECONCILIATION_OUTCOMES,
136914
137031
  MESH_WRITE_CLASSES: () => MESH_WRITE_CLASSES,
@@ -137277,6 +137394,7 @@ __export(src_exports, {
137277
137394
  classifyFileScopeFromPrompt: () => classifyFileScopeFromPrompt,
137278
137395
  classifyGhError: () => classifyGhError,
137279
137396
  classifyInsightRunError: () => classifyInsightRunError,
137397
+ classifyMergeSweepAdmission: () => classifyMergeSweepAdmission,
137280
137398
  classifyPlannerActionSideEffect: () => classifyPlannerActionSideEffect,
137281
137399
  classifyProviderError: () => classifyProviderError,
137282
137400
  classifyReadStaleness: () => classifyReadStaleness,
@@ -137287,6 +137405,7 @@ __export(src_exports, {
137287
137405
  classifyTaskCommitAssociationConfidence: () => classifyTaskCommitAssociationConfidence,
137288
137406
  classifyTerminalFailureAutoRecovery: () => classifyTerminalFailureAutoRecovery,
137289
137407
  classifyWorkflowAgentNode: () => classifyWorkflowAgentNode,
137408
+ classifyWorkflowNodeMergeRegion: () => classifyWorkflowNodeMergeRegion,
137290
137409
  cleanupOldAiSessions: () => cleanupOldAiSessions,
137291
137410
  cleanupStaleAiSessions: () => cleanupStaleAiSessions,
137292
137411
  clearActiveEmbeddedRuntimeUrl: () => clearActiveEmbeddedRuntimeUrl,
@@ -137486,7 +137605,6 @@ __export(src_exports, {
137486
137605
  findOrphanedSettingValues: () => findOrphanedSettingValues,
137487
137606
  findPendingPreMergeStep: () => findPendingPreMergeStep,
137488
137607
  findSameAgentDuplicates: () => findSameAgentDuplicates,
137489
- findUnrunRequiredPreMergeStepIds: () => findUnrunRequiredPreMergeStepIds,
137490
137608
  findVitestProcessIds: () => findVitestProcessIds,
137491
137609
  findWorkflowColumn: () => findWorkflowColumn,
137492
137610
  findWorkflowEventShapeViolations: () => findWorkflowEventShapeViolations,
@@ -137558,6 +137676,7 @@ __export(src_exports, {
137558
137676
  getMcpDiscoverySources: () => getMcpDiscoverySources,
137559
137677
  getMemoryBackend: () => getMemoryBackend,
137560
137678
  getMemoryBackendCapabilities: () => getMemoryBackendCapabilities,
137679
+ getMergeConfirmedFinalizationBlocker: () => getMergeConfirmedFinalizationBlocker,
137561
137680
  getOpenIncidentByGroupingKeyAsync: () => getOpenIncidentByGroupingKeyAsync,
137562
137681
  getOverriddenKeys: () => getOverriddenKeys,
137563
137682
  getPiExtensionDiscoveryDirs: () => getPiExtensionDiscoveryDirs,
@@ -137596,6 +137715,7 @@ __export(src_exports, {
137596
137715
  getTotalAgentActiveMs: () => getTotalAgentActiveMs,
137597
137716
  getTrait: () => getTrait,
137598
137717
  getTraitRegistry: () => getTraitRegistry,
137718
+ getUnfinishedStepTitles: () => getUnfinishedStepTitles,
137599
137719
  getValidTransitions: () => getValidTransitions,
137600
137720
  getWorkflowEventBus: () => getWorkflowEventBus,
137601
137721
  getWorkflowExtensionRegistry: () => getWorkflowExtensionRegistry,
@@ -137679,6 +137799,7 @@ __export(src_exports, {
137679
137799
  isManualTrigger: () => isManualTrigger,
137680
137800
  isMcpSecretRef: () => isMcpSecretRef,
137681
137801
  isMechanicalRoutableContext: () => isMechanicalRoutableContext,
137802
+ isMergeRegionNode: () => isMergeRegionNode,
137682
137803
  isMergeRequestContractShadowEnabled: () => isMergeRequestContractShadowEnabled,
137683
137804
  isMeshIntentState: () => isMeshIntentState,
137684
137805
  isMeshWriteClass: () => isMeshWriteClass,
@@ -138382,6 +138503,8 @@ var init_src2 = __esm({
138382
138503
  init_workflow_merge_proof();
138383
138504
  init_workflow_optional_steps();
138384
138505
  init_required_pre_merge_steps();
138506
+ init_merge_sweep_admission();
138507
+ init_workflow_merge_region();
138385
138508
  init_workflow_prompt_overrides();
138386
138509
  init_builtin_stepwise_coding_workflow_ir();
138387
138510
  init_builtin_stepwise_final_review_coding_workflow_ir();
@@ -148482,39 +148605,42 @@ var init_provider_registration = __esm({
148482
148605
  });
148483
148606
 
148484
148607
  // ../engine/src/util/emit-bounded-run-audit.ts
148485
- async function emitBoundedRunAudit2(host, event, options = {}) {
148608
+ function emitBoundedRunAudit2(host, event, options = {}) {
148609
+ return emitBoundedRunAuditWithOutcome2(host, event, options).then(() => void 0);
148610
+ }
148611
+ async function emitBoundedRunAuditWithOutcome2(host, event, options = {}) {
148486
148612
  let defaultLog2;
148487
148613
  try {
148488
148614
  defaultLog2 = runAuditLog;
148489
148615
  } catch {
148490
148616
  defaultLog2 = void 0;
148491
148617
  }
148492
- const log44 = options.log ?? defaultLog2 ?? { warn: (message) => console.warn(message) };
148618
+ const log44 = options.log ?? defaultLog2 ?? { warn: () => void 0 };
148493
148619
  const sink2 = host?.recordRunAuditEvent;
148494
- if (typeof sink2 !== "function") return;
148620
+ if (typeof sink2 !== "function") return { outcome: "absent" };
148495
148621
  let sinkPromise;
148496
148622
  try {
148497
148623
  sinkPromise = Promise.resolve(sink2.call(host, event));
148498
- } catch {
148624
+ } catch (error3) {
148499
148625
  log44.warn(`[run-audit] failed to record ${event.mutationType}`);
148500
- return;
148626
+ return { outcome: "failed", error: error3 };
148501
148627
  }
148502
148628
  void sinkPromise.catch(() => void 0);
148503
- await new Promise((resolve115) => {
148629
+ return await new Promise((resolve115) => {
148504
148630
  const timer2 = setTimeout(() => {
148505
148631
  log44.warn(`[run-audit] timed out recording ${event.mutationType}`);
148506
- resolve115();
148632
+ resolve115({ outcome: "timed-out" });
148507
148633
  }, options.timeoutMs ?? RUN_AUDIT_EMIT_TIMEOUT_MS);
148508
148634
  timer2.unref?.();
148509
148635
  void sinkPromise.then(
148510
148636
  () => {
148511
148637
  clearTimeout(timer2);
148512
- resolve115();
148638
+ resolve115({ outcome: "recorded" });
148513
148639
  },
148514
- () => {
148640
+ (error3) => {
148515
148641
  clearTimeout(timer2);
148516
148642
  log44.warn(`[run-audit] failed to record ${event.mutationType}`);
148517
- resolve115();
148643
+ resolve115({ outcome: "failed", error: error3 });
148518
148644
  }
148519
148645
  );
148520
148646
  });
@@ -181661,6 +181787,7 @@ function createRunAuditor(store, context2) {
181661
181787
  },
181662
181788
  database: async () => {
181663
181789
  },
181790
+ databaseWithOutcome: async () => ({ outcome: "absent" }),
181664
181791
  filesystem: async () => {
181665
181792
  },
181666
181793
  sandbox: async () => {
@@ -181674,6 +181801,7 @@ function createRunAuditor(store, context2) {
181674
181801
  },
181675
181802
  database: async () => {
181676
181803
  },
181804
+ databaseWithOutcome: async () => ({ outcome: "absent" }),
181677
181805
  filesystem: async () => {
181678
181806
  },
181679
181807
  sandbox: async () => {
@@ -181716,6 +181844,28 @@ function createRunAuditor(store, context2) {
181716
181844
  };
181717
181845
  await emitBoundedRunAudit2(store, eventInput);
181718
181846
  },
181847
+ /*
181848
+ FNXC:RunAudit 2026-08-23-18:30:
181849
+ Identical write to `database`, returning whether the row landed for the emitters that gate their
181850
+ own state on a proven write (see the interface note). Still bounded and non-throwing.
181851
+ */
181852
+ databaseWithOutcome: async (input) => {
181853
+ const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context2.taskId;
181854
+ return await emitBoundedRunAuditWithOutcome2(store, {
181855
+ taskId: inferredTaskId,
181856
+ agentId: context2.agentId,
181857
+ runId: context2.runId,
181858
+ domain: "database",
181859
+ mutationType: input.type,
181860
+ target: input.target,
181861
+ metadata: {
181862
+ phase: context2.phase,
181863
+ ...context2.source ? { source: context2.source } : {},
181864
+ ...context2.taskLineageId ? { taskLineageId: context2.taskLineageId } : {},
181865
+ ...input.metadata
181866
+ }
181867
+ });
181868
+ },
181719
181869
  filesystem: async (input) => {
181720
181870
  const eventInput = {
181721
181871
  taskId: context2.taskId,
@@ -183651,10 +183801,13 @@ var init_scheduler = __esm({
183651
183801
  * Validate that a task's filesystem state is intact.
183652
183802
  * Checks that the task directory exists and PROMPT.md is present and non-empty.
183653
183803
  *
183654
- * @param id - The task ID to validate
183804
+ * @param task - The task (id + title) to validate
183655
183805
  * @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
183656
183806
  */
183657
- async validateTaskFilesystem(id) {
183807
+ async validateTaskFilesystem(task) {
183808
+ const id = task.id;
183809
+ const titleRedirect = nonExecutableDuplicateRedirectReason(null, task.title);
183810
+ if (titleRedirect) return { valid: false, reason: titleRedirect };
183658
183811
  if (typeof this.store.getTasksDir !== "function") {
183659
183812
  return { valid: true };
183660
183813
  }
@@ -183671,7 +183824,7 @@ var init_scheduler = __esm({
183671
183824
  if (!content || content.trim().length === 0) {
183672
183825
  return { valid: false, reason: "missing or empty PROMPT.md" };
183673
183826
  }
183674
- const duplicateOnly = nonExecutableDuplicateRedirectReason(content);
183827
+ const duplicateOnly = nonExecutableDuplicateRedirectReason(content, task.title);
183675
183828
  if (duplicateOnly) {
183676
183829
  return { valid: false, reason: duplicateOnly };
183677
183830
  }
@@ -184349,7 +184502,7 @@ var init_scheduler = __esm({
184349
184502
  );
184350
184503
  }
184351
184504
  }
184352
- const validation = await this.validateTaskFilesystem(task.id);
184505
+ const validation = await this.validateTaskFilesystem(task);
184353
184506
  if (!validation.valid) {
184354
184507
  schedulerLog.warn(`Task ${task.id} filesystem validation failed: ${validation.reason}`);
184355
184508
  const replanColumn = await moveTaskToReplanColumn(this.store, task);
@@ -193383,6 +193536,20 @@ var init_step_session_executor = __esm({
193383
193536
  * Safe to call multiple times (idempotent). Call this in a `finally` block
193384
193537
  * after `executeAll()`.
193385
193538
  */
193539
+ /**
193540
+ * FNXC:StepParallelWorktrees 2026-08-23-21:40:
193541
+ * `parallelBranches` holds only branches THIS executor created in `createStepWorktree`
193542
+ * (`fusion/step-<idx>-<worktree-name>`), so their provenance is known here rather than inferred.
193543
+ * FN-9161's `isFusionDeletableBranch` answers by NAME and reads anything that is not
193544
+ * `fusion/<task-id>...` as operator-supplied, so applying it here silently skipped every
193545
+ * step branch and leaked one ref per parallel step. Protect what an operator can actually own —
193546
+ * the task's working branch and its explicit override — and dispose of the rest.
193547
+ */
193548
+ isDisposableStepBranch(branchName) {
193549
+ const task = this.options.taskDetail;
193550
+ const operatorOwned = [task.branch, task.branchContext?.branchOverride?.branch].filter((value2) => typeof value2 === "string" && value2.trim().length > 0).map((value2) => value2.trim());
193551
+ return !operatorOwned.includes(branchName.trim());
193552
+ }
193386
193553
  async cleanup() {
193387
193554
  if (this.activeSessions.size > 0) {
193388
193555
  await this.terminateAllSessions();
@@ -193409,7 +193576,7 @@ var init_step_session_executor = __esm({
193409
193576
  }
193410
193577
  for (const [stepIdx, branchName] of this.parallelBranches) {
193411
193578
  try {
193412
- if (!isFusionDeletableBranch(this.options.taskDetail, branchName)) continue;
193579
+ if (!this.isDisposableStepBranch(branchName)) continue;
193413
193580
  await execAsync18(`git branch -D "${branchName}"`, {
193414
193581
  cwd: this.options.rootDir
193415
193582
  });
@@ -193964,7 +194131,7 @@ Follow instructions precisely and avoid unrelated changes.`,
193964
194131
  });
193965
194132
  }
193966
194133
  const branch = this.parallelBranches.get(stepIdx);
193967
- if (branch && isFusionDeletableBranch(this.options.taskDetail, branch)) {
194134
+ if (branch && this.isDisposableStepBranch(branch)) {
193968
194135
  await execAsync18(`git branch -D "${branch}"`, {
193969
194136
  cwd: this.options.rootDir
193970
194137
  });
@@ -201279,7 +201446,7 @@ async function finalizeProvenAutoMergeTask({
201279
201446
  });
201280
201447
  return { outcome: "blocked", task: latest, previousColumn: latest.column, reason };
201281
201448
  }
201282
- const hardBlocker = getTaskHardMergeBlocker({
201449
+ const hardBlocker = getMergeConfirmedFinalizationBlocker({
201283
201450
  ...latest,
201284
201451
  /*
201285
201452
  FNXC:WorkflowMerge 2026-06-29-09:15:
@@ -201294,6 +201461,14 @@ async function finalizeProvenAutoMergeTask({
201294
201461
  status: clearMergeConfirmedTransientStatus(latest.status),
201295
201462
  error: void 0
201296
201463
  });
201464
+ const unfinishedSteps = getUnfinishedStepTitles(latest);
201465
+ if (unfinishedSteps.length > 0) {
201466
+ await store.logEntry(
201467
+ taskId,
201468
+ `Finalizing proven merge with ${unfinishedSteps.length} unfinished step(s) \u2014 the branch already landed, so these did not run: ${unfinishedSteps.slice(0, 8).join("; ")}`,
201469
+ "MergeConfirmedFinalizeUnfinishedSteps"
201470
+ ).catch(() => void 0);
201471
+ }
201297
201472
  if (hardBlocker) {
201298
201473
  fence?.assertOwned("finalization");
201299
201474
  await store.updateTask(taskId, {
@@ -221322,15 +221497,21 @@ var init_self_healing = __esm({
221322
221497
  const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonical);
221323
221498
  if (!isNearDuplicateCanonicalInactive(canonical ?? void 0, canonicalFlags)) continue;
221324
221499
  const promptPath = join76(this.options.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
221325
- if (existsSync62(promptPath)) {
221500
+ const written = existsSync62(promptPath) ? readFileSync24(promptPath, "utf-8") : "";
221501
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
221502
+ const canonicalMarkerId = canonicalId.toUpperCase();
221503
+ if (duplicateResolution.conflict || duplicateResolution.marker && duplicateResolution.marker.canonicalId !== canonicalMarkerId) {
221504
+ continue;
221505
+ }
221506
+ if (duplicateResolution.source === "prompt") {
221326
221507
  try {
221327
- const written = readFileSync24(promptPath, "utf-8");
221328
- if (parseExplicitDuplicateMarker(written)) {
221329
- rmSync7(promptPath, { force: true });
221330
- }
221508
+ rmSync7(promptPath, { force: true });
221331
221509
  } catch {
221332
221510
  }
221333
221511
  }
221512
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === canonicalMarkerId) {
221513
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${canonicalMarkerId}` });
221514
+ }
221334
221515
  await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalId));
221335
221516
  if (typeof this.store.logEntry === "function") {
221336
221517
  await Promise.resolve(this.store.logEntry(
@@ -224716,7 +224897,7 @@ var init_self_healing = __esm({
224716
224897
  mergeTargetBranch: mergeTarget.branch,
224717
224898
  mergeTargetSource: mergeTarget.source
224718
224899
  };
224719
- const hardBlocker = getTaskHardMergeBlocker({
224900
+ const hardBlocker = getMergeConfirmedFinalizationBlocker({
224720
224901
  ...task,
224721
224902
  steps: task.steps ?? [],
224722
224903
  workflowStepResults: task.workflowStepResults
@@ -224901,7 +225082,7 @@ var init_self_healing = __esm({
224901
225082
  mergeTargetBranch: mergeTarget.branch,
224902
225083
  mergeTargetSource: mergeTarget.source
224903
225084
  };
224904
- const hardBlocker = getTaskHardMergeBlocker({
225085
+ const hardBlocker = getMergeConfirmedFinalizationBlocker({
224905
225086
  ...task,
224906
225087
  steps: task.steps ?? [],
224907
225088
  workflowStepResults: task.workflowStepResults
@@ -227596,14 +227777,12 @@ var init_self_healing = __esm({
227596
227777
  for (const task of candidates) {
227597
227778
  try {
227598
227779
  const promptPath = join76(this.options.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
227599
- if (!existsSync62(promptPath)) {
227600
- continue;
227601
- }
227602
- const written = readFileSync24(promptPath, "utf-8");
227603
- const marker = parseExplicitDuplicateMarker(written);
227604
- if (!marker) {
227780
+ const written = existsSync62(promptPath) ? readFileSync24(promptPath, "utf-8") : "";
227781
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
227782
+ if (!duplicateResolution.marker || duplicateResolution.conflict) {
227605
227783
  continue;
227606
227784
  }
227785
+ const marker = duplicateResolution.marker;
227607
227786
  if (processedMarkers >= 50) {
227608
227787
  break;
227609
227788
  }
@@ -227616,7 +227795,10 @@ var init_self_healing = __esm({
227616
227795
  const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonicalTask);
227617
227796
  if (!canonicalTask || isNearDuplicateCanonicalInactive(canonicalTask, canonicalFlags)) {
227618
227797
  if (canClearInactiveMarker) {
227619
- rmSync7(promptPath, { force: true });
227798
+ if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
227799
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
227800
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
227801
+ }
227620
227802
  const priorClearCount = typeof task.sourceMetadata?.duplicateMarkerClearCount === "number" ? task.sourceMetadata.duplicateMarkerClearCount : 0;
227621
227803
  const patch = buildMarkerClearedReplanTaskPatch(marker.canonicalId, priorClearCount);
227622
227804
  await this.store.updateTask(task.id, patch);
@@ -227635,7 +227817,10 @@ var init_self_healing = __esm({
227635
227817
  const resolution = settings.triageDuplicateResolution ?? "prompt";
227636
227818
  if (resolution === "prompt" && isTriageDuplicateKeepAcknowledged(task.sourceMetadata, canonicalTask.id)) {
227637
227819
  if (canClearInactiveMarker) {
227638
- rmSync7(promptPath, { force: true });
227820
+ if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
227821
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
227822
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
227823
+ }
227639
227824
  const priorKeepClears = typeof task.sourceMetadata?.duplicateMarkerClearCount === "number" ? task.sourceMetadata.duplicateMarkerClearCount : 0;
227640
227825
  const keepExhausted = priorKeepClears >= 1;
227641
227826
  await this.store.updateTask(
@@ -227660,7 +227845,10 @@ var init_self_healing = __esm({
227660
227845
  await flagTriageDuplicate(this.store, task.id, canonicalTask.id);
227661
227846
  await this.store.updateTask(task.id, { paused: true, pausedReason: "duplicate-decision-required", status: null });
227662
227847
  } else {
227663
- rmSync7(promptPath, { force: true });
227848
+ if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
227849
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
227850
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
227851
+ }
227664
227852
  await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalTask.id));
227665
227853
  if (typeof this.store.logEntry === "function") {
227666
227854
  await Promise.resolve(this.store.logEntry(
@@ -229697,7 +229885,7 @@ var init_triage = __esm({
229697
229885
  planLog.warn(`${task.id} planning recovery skipped \u2014 PROMPT.md failed deterministic validation (${deterministicSpecFailure})`);
229698
229886
  return false;
229699
229887
  }
229700
- const isExplicitDuplicateRedirect = Boolean(parseExplicitDuplicateMarker(written));
229888
+ const isExplicitDuplicateRedirect = Boolean(resolveExplicitDuplicateMarker(written, task.title).marker);
229701
229889
  const workflow = await resolveWorkflowIrForTask(this.store, task.id).catch(() => void 0);
229702
229890
  const requiresPromptImplementationSteps = workflow?.nodes.some(
229703
229891
  (node) => node.kind === "parse-steps" && (node.config?.artifact === void 0 || node.config.artifact === "PROMPT.md") && node.config?.parser === "step-headings" && node.config?.requireStepsUnlessNoCommits === true
@@ -230119,7 +230307,7 @@ var init_triage = __esm({
230119
230307
  phase: "triage",
230120
230308
  source: "triage"
230121
230309
  });
230122
- void throttleAuditor.database({
230310
+ void (throttleAuditor.databaseWithOutcome ?? throttleAuditor.database)({
230123
230311
  type: "task:plan-admission-throttled",
230124
230312
  target: eligibleIds[0] ?? this.rootDir,
230125
230313
  metadata: {
@@ -230132,7 +230320,9 @@ var init_triage = __esm({
230132
230320
  processingCount: this.processing.size,
230133
230321
  processingTaskIds: processingIds
230134
230322
  }
230135
- }).then(() => {
230323
+ }).then((result2) => {
230324
+ const outcome = result2?.outcome;
230325
+ if (outcome !== "recorded" && outcome !== "absent") return;
230136
230326
  this.lastPlanThrottleSignature = throttleSignature;
230137
230327
  void Promise.all(eligibleIds.map((taskId) => this.store.logEntry(taskId, capacityReason))).catch((logErr) => {
230138
230328
  planLog.warn(`Failed to write planning capacity reason: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
@@ -230276,6 +230466,44 @@ var init_triage = __esm({
230276
230466
  return false;
230277
230467
  }
230278
230468
  }
230469
+ /**
230470
+ * Resolves an exact prompt/title redirect before this task claims any planning capacity.
230471
+ *
230472
+ * FNXC:DuplicateIntake 2026-08-09-01:31:
230473
+ * FN-8840 requires title redirects to take the same duplicate-decision route as prompt
230474
+ * redirects before `specifyTask()` can start a planner session. Reading the prompt here is
230475
+ * required only to detect a conflicting exact marker; a missing or unreadable prompt leaves
230476
+ * a title-only redirect actionable and never lets it consume an implementation session.
230477
+ *
230478
+ * FNXC:DuplicateIntake 2026-08-23-18:30:
230479
+ * This whole title-aware path (plus the `source: "prompt" | "title"` plumbing in
230480
+ * `clearExplicitDuplicateMarker` and the conflict guard in `finalizeApprovedTaskBody`) was
230481
+ * removed by accident in 1cf86baa1c ("refactor: package code organization wave 18"), which
230482
+ * reverted FN-8840 in triage.ts only — every other FN-8840 surface (dashboard task routes,
230483
+ * `handle-graph-failure.ts`, core `resolveExplicitDuplicateMarker`) kept it. A title-only
230484
+ * `DUPLICATE: <id>` card therefore consumed a full planner session again instead of taking the
230485
+ * duplicate-decision route. Restored here; do not "simplify" back to prompt-only parsing.
230486
+ */
230487
+ async finalizeExplicitDuplicateBeforePlanning(task) {
230488
+ try {
230489
+ const liveTask = await this.store.getTask(task.id).catch(() => null);
230490
+ if (!liveTask || liveTask.paused === true || liveTask.userPaused === true) return false;
230491
+ const promptPath = join77(this.rootDir, ".fusion", "tasks", liveTask.id, "PROMPT.md");
230492
+ const written = await readFile45(promptPath, "utf-8").catch(() => "");
230493
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, liveTask.title);
230494
+ if (!duplicateResolution.marker && !duplicateResolution.conflict) return false;
230495
+ const settings = await mergeEffectiveSettings(this.store, liveTask, await this.store.getSettings());
230496
+ if (duplicateResolution.conflict) {
230497
+ await this.finalizeApprovedTask(liveTask, written, settings);
230498
+ return true;
230499
+ }
230500
+ return await this.tryFinalizeExplicitDuplicateMarker(liveTask, written, settings);
230501
+ } catch (error3) {
230502
+ const message = error3 instanceof Error ? error3.message : String(error3);
230503
+ planLog.warn(`${task.id}: pre-planning duplicate resolution failed open: ${message}`);
230504
+ return false;
230505
+ }
230506
+ }
230279
230507
  async specifyTask(task) {
230280
230508
  if (this.resetFence.isResetHoldActive(task.id)) {
230281
230509
  if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
@@ -230288,6 +230516,11 @@ var init_triage = __esm({
230288
230516
  this.coordinatorAdmittedTaskIds.delete(task.id);
230289
230517
  return;
230290
230518
  }
230519
+ if (await this.finalizeExplicitDuplicateBeforePlanning(task)) {
230520
+ if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
230521
+ this.coordinatorAdmittedTaskIds.delete(task.id);
230522
+ return;
230523
+ }
230291
230524
  if (await this.flagImportNearDuplicateBeforePlanning(task)) {
230292
230525
  if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
230293
230526
  return;
@@ -230821,12 +231054,28 @@ This is a multi-repository workspace task. Your final PROMPT.md MUST include a n
230821
231054
  content,
230822
231055
  false
230823
231056
  ),
231057
+ /*
231058
+ FNXC:PlanArtifactPersistence 2026-08-23-19:24:
231059
+ The project-database mirror is BEST-EFFORT — the comment above says so and validation below
231060
+ owns the verdict. FN-151 (c1818ea819) routed it through the reset-fenced writer, which does
231061
+ not swallow, so an infrastructure failure inside the lifecycle lock (e.g. a
231062
+ PlanningLifecycleLockTransportError) aborted a planning attempt whose authoritative PROMPT.md
231063
+ had already been written, and pre-empted both the unchanged-PROMPT verdict and FN-8911's
231064
+ marker-consuming path. Degrade the mirror the way `mirrorPlanToProjectDb` always did; the
231065
+ authoritative `writeAuthoritativePrompt` half deliberately still throws, and only a
231066
+ lifecycle-lock TRANSPORT rejection is degraded here — every other mirror failure keeps
231067
+ aborting the attempt exactly as before.
231068
+ */
230824
231069
  mirrorAuthoritativePlan: async (content) => await this.persistResetFencedPlanningArtifact(
230825
231070
  task,
230826
231071
  planningGeneration,
230827
231072
  content,
230828
231073
  true
230829
- )
231074
+ ).catch((error3) => {
231075
+ if (!isPlanningLifecycleLockTransportError(error3)) throw error3;
231076
+ planLog.warn(`${task.id}: lifecycle-lock transport failure while mirroring the authoritative plan into the project database: ${error3.message}`);
231077
+ return false;
231078
+ })
230830
231079
  });
230831
231080
  if (planPersistence.outcome === "recovery-fenced") return;
230832
231081
  if (planPersistence.outcome === "recovered") {
@@ -231490,11 +231739,11 @@ ${lines.join("\n")}` }],
231490
231739
  }
231491
231740
  async tryFinalizeExplicitDuplicateMarker(task, written, settings, options = {}, report2 = { outcome: "parked" }) {
231492
231741
  try {
231493
- const explicitDuplicateMarker = parseExplicitDuplicateMarker(written);
231494
- if (!explicitDuplicateMarker) {
231742
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
231743
+ if (!duplicateResolution.marker || duplicateResolution.conflict) {
231495
231744
  return false;
231496
231745
  }
231497
- const canonicalId = explicitDuplicateMarker.canonicalId;
231746
+ const canonicalId = duplicateResolution.marker.canonicalId;
231498
231747
  const canonicalTask = await this.store.getTask(canonicalId);
231499
231748
  if (canonicalTask?.id.toLowerCase() === task.id.toLowerCase()) {
231500
231749
  return false;
@@ -231609,7 +231858,12 @@ ${lines.join("\n")}` }],
231609
231858
  */
231610
231859
  async clearDuplicateMarkerForReplan(task, canonicalId, feedback, options) {
231611
231860
  if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
231612
- await rm11(join77(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
231861
+ if (options?.source !== "title") {
231862
+ await rm11(join77(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
231863
+ }
231864
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === canonicalId) {
231865
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${canonicalId}` });
231866
+ }
231613
231867
  })) return false;
231614
231868
  const priorClearCount = options?.priorClearCount ?? 0;
231615
231869
  if (options?.exhausted) {
@@ -231639,8 +231893,14 @@ ${lines.join("\n")}` }],
231639
231893
  }
231640
231894
  async finalizeApprovedTaskBody(task, writtenInput, settings, options = {}, report2 = { outcome: "parked" }) {
231641
231895
  let written = writtenInput;
231642
- if (await this.recoverMissingPromptBeforeRelease(task)) return;
231643
- const explicitDuplicateMarker = parseExplicitDuplicateMarker(written);
231896
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
231897
+ if (duplicateResolution.conflict) {
231898
+ await this.updatePlanningStateIfStillCurrent(task, { status: "needs-replan", error: null });
231899
+ await this.store.logEntry(task.id, "Duplicate redirect sources conflict", "PROMPT.md and task title name different canonical tasks; correct one exact redirect before planning.");
231900
+ return;
231901
+ }
231902
+ if (!duplicateResolution.marker && await this.recoverMissingPromptBeforeRelease(task)) return;
231903
+ const explicitDuplicateMarker = duplicateResolution.marker;
231644
231904
  if (explicitDuplicateMarker) {
231645
231905
  await this.updatePlanningStateIfStillCurrent(task, (live) => {
231646
231906
  const customFields = { ...live.customFields ?? {} };
@@ -231648,6 +231908,7 @@ ${lines.join("\n")}` }],
231648
231908
  return { customFields };
231649
231909
  });
231650
231910
  const canonicalId = explicitDuplicateMarker.canonicalId;
231911
+ const duplicateSource = duplicateResolution.source ?? "prompt";
231651
231912
  const canonicalTask = await this.store.getTask(canonicalId).catch(() => null);
231652
231913
  const canClearInactiveMarker = task.userPaused !== true && (task.paused !== true || task.pausedReason === "duplicate-decision-required") && (task.pausedReason == null || task.pausedReason === "duplicate-decision-required");
231653
231914
  const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonicalTask);
@@ -231659,7 +231920,7 @@ ${lines.join("\n")}` }],
231659
231920
  task,
231660
231921
  canonicalId,
231661
231922
  buildInactiveDuplicateClearFeedback(canonicalId),
231662
- { exhausted: false, priorClearCount }
231923
+ { exhausted: false, priorClearCount, source: duplicateSource }
231663
231924
  );
231664
231925
  }
231665
231926
  return;
@@ -231672,7 +231933,7 @@ ${lines.join("\n")}` }],
231672
231933
  task,
231673
231934
  canonicalId,
231674
231935
  buildKeepDuplicateClearFeedback(canonicalId),
231675
- { exhausted: priorClearCount >= 1, priorClearCount }
231936
+ { exhausted: priorClearCount >= 1, priorClearCount, source: duplicateSource }
231676
231937
  );
231677
231938
  }
231678
231939
  return;
@@ -231710,7 +231971,8 @@ ${lines.join("\n")}` }],
231710
231971
  await this.clearDuplicateMarkerForReplan(
231711
231972
  task,
231712
231973
  canonicalId,
231713
- buildKeepDuplicateClearFeedback(canonicalId)
231974
+ buildKeepDuplicateClearFeedback(canonicalId),
231975
+ { source: duplicateSource }
231714
231976
  );
231715
231977
  return;
231716
231978
  }
@@ -243346,6 +243608,7 @@ function buildActionGateContext(deps, taskId, agent, projectDefaultPolicy) {
243346
243608
  executorLog.warn(`${taskId}: failed to suspend in-flight session while awaiting approval: ${error3 instanceof Error ? error3.message : String(error3)}`);
243347
243609
  });
243348
243610
  }
243611
+ void emitApprovalMail({ messageStore: deps.messageStore ?? void 0, approvalRequestId, toolName: decision.toolName, taskId, agentId: actorId, agentName: actorName });
243349
243612
  if (agent && deps.agentStore) {
243350
243613
  await deps.agentStore.updateAgentState(agent.id, "paused");
243351
243614
  await deps.agentStore.updateAgent(agent.id, { pauseReason: "awaiting-approval" });
@@ -243364,6 +243627,7 @@ var init_build_action_gate_context = __esm({
243364
243627
  "../engine/src/executor/build-action-gate-context.ts"() {
243365
243628
  "use strict";
243366
243629
  init_src2();
243630
+ init_approval_mail();
243367
243631
  init_workflow_agent_router();
243368
243632
  init_logger3();
243369
243633
  }
@@ -243423,6 +243687,7 @@ function buildPermanentAgentGatingContext(deps, taskId, agent, projectDefaultPol
243423
243687
  deps.approvalSuspended.delete(taskId);
243424
243688
  throw error3;
243425
243689
  }
243690
+ void emitApprovalMail({ messageStore: deps.messageStore ?? void 0, approvalRequestId, toolName, taskId, agentId: actorId, agentName: actorName });
243426
243691
  }
243427
243692
  };
243428
243693
  }
@@ -243430,6 +243695,7 @@ var init_build_permanent_agent_gating_context = __esm({
243430
243695
  "../engine/src/executor/build-permanent-agent-gating-context.ts"() {
243431
243696
  "use strict";
243432
243697
  init_src2();
243698
+ init_approval_mail();
243433
243699
  init_permanent_agent_gating();
243434
243700
  }
243435
243701
  });
@@ -258743,6 +259009,7 @@ function buildBuildActionGateContextDeps(host) {
258743
259009
  get approvalRequestStore() {
258744
259010
  return host.approvalRequestStore;
258745
259011
  },
259012
+ messageStore: host.options.messageStore,
258746
259013
  activeWorkflowAuthorities: host.activeWorkflowAuthorities,
258747
259014
  activeWorkflowGraphAbortControllers: host.activeWorkflowGraphAbortControllers
258748
259015
  };
@@ -258899,7 +259166,8 @@ function buildBuildPermanentAgentGatingContextDeps(host) {
258899
259166
  approvalSuspended: host.approvalSuspended,
258900
259167
  get approvalRequestStore() {
258901
259168
  return host.approvalRequestStore;
258902
- }
259169
+ },
259170
+ messageStore: host.options.messageStore
258903
259171
  };
258904
259172
  }
258905
259173
  function buildPersistTokenUsageDeps(host) {
@@ -265679,6 +265947,10 @@ async function enforceAiMergeSquashGates(params) {
265679
265947
  if (!(error3 instanceof FileScopeViolationError)) throw error3;
265680
265948
  await execFileAsync26("git", ["reset", "--hard", params.tipSha], { cwd: params.mergeRoot });
265681
265949
  await execFileAsync26("git", ["clean", "-fd"], { cwd: params.mergeRoot });
265950
+ try {
265951
+ await params.store.updateTask(params.taskId, { aiMergeReviewReconciliation: null });
265952
+ } catch {
265953
+ }
265682
265954
  throw error3;
265683
265955
  }
265684
265956
  }
@@ -266552,11 +266824,12 @@ async function cleanupAiMergeWorktree(input) {
266552
266824
  }
266553
266825
  const removalTargets = canonicalRoot === mergeRoot ? [mergeRoot] : [canonicalRoot, mergeRoot];
266554
266826
  const cleanupMetadata = { taskId, mergeRoot: canonicalRoot, requestedMergeRoot: mergeRoot };
266555
- let alreadyAbsent = false;
266827
+ let alreadyAbsent = removalTargets.every((target) => !existsSync84(target));
266556
266828
  if (worktreeAdded) {
266557
266829
  try {
266558
266830
  await gitRunner(["worktree", "remove", "--force", canonicalRoot], projectRootDir);
266559
- await audit.git({ type: "merge:ai-worktree-cleanup", target: canonicalRoot, metadata: { ...cleanupMetadata, phase: "git-remove", success: true } });
266831
+ if (alreadyAbsent) await log44(`AI merge cleanup: worktree ${canonicalRoot} was already absent before git removal; treating cleanup as idempotent`);
266832
+ await audit.git({ type: "merge:ai-worktree-cleanup", target: canonicalRoot, metadata: { ...cleanupMetadata, phase: "git-remove", success: true, ...alreadyAbsent ? { alreadyAbsent: true, idempotent: true } : {} } });
266560
266833
  } catch (err) {
266561
266834
  const error3 = describeCleanupError(err);
266562
266835
  const code = getErrorStringProperty(err, "code");
@@ -268096,7 +268369,7 @@ async function landWorkspaceTask(store, task, workspaceRootDir, options = {}, de
268096
268369
  const requiresRepositoryReviewEvidence = approvedReviewEvidence !== void 0 || (mergeBoundaryTask.enabledWorkflowSteps ?? []).some((step) => /review/i.test(step));
268097
268370
  const approvalMissingRepositories = Object.entries(mergeBoundaryFingerprints).filter(([repoRel]) => requiresRepositoryReviewEvidence && !approvedReviewEvidence?.[repoRel]).map(([repoRel]) => repoRel).sort();
268098
268371
  const contentChangedRepositories = Object.entries(mergeBoundaryFingerprints).filter(([repoRel, fingerprint]) => requiresRepositoryReviewEvidence && Boolean(approvedReviewEvidence?.[repoRel]) && approvedReviewEvidence?.[repoRel]?.fingerprint !== fingerprint).map(([repoRel]) => repoRel).sort();
268099
- const changedFiles = normalizedMergeBoundaryFiles.filter((file) => !persistedReviewFiles.includes(file)).sort();
268372
+ const changedFiles = requiresRepositoryReviewEvidence ? normalizedMergeBoundaryFiles.filter((file) => !persistedReviewFiles.includes(file)).sort() : [];
268100
268373
  if (approvalMissingRepositories.length > 0) {
268101
268374
  throw new WorkspaceReviewRequiredError(taskId, {
268102
268375
  kind: "approval-missing",
@@ -285097,6 +285370,9 @@ function emitMergeRequestShadowDequeueParityAudit(store, legacyTaskId, shadowTas
285097
285370
  metadata: { legacyTaskId, shadowTaskId, agree }
285098
285371
  }, { log: runtimeLog });
285099
285372
  }
285373
+ function isActiveMergeRequestState(state) {
285374
+ return state === "running" || state === "retrying";
285375
+ }
285100
285376
  function getPrMergeRetryNotBefore(task) {
285101
285377
  const retries = task.mergeRetries ?? 0;
285102
285378
  const updatedAt = task.updatedAt ? Date.parse(task.updatedAt) : NaN;
@@ -285178,6 +285454,7 @@ var init_project_engine = __esm({
285178
285454
  init_overseer_advisor_service();
285179
285455
  init_overseer_advise_tool();
285180
285456
  init_agent_session_helpers();
285457
+ init_active_session_registry();
285181
285458
  init_pr_reconcile();
285182
285459
  init_pr_comment_handler();
285183
285460
  init_notifier();
@@ -285378,6 +285655,12 @@ var init_project_engine = __esm({
285378
285655
  };
285379
285656
  // ── Auto-merge state ──
285380
285657
  mergeQueue = [];
285658
+ /* FNXC:MergeAuthority 2026-08-23-18:05: last logged sweep-hold reason per task, so a held card is
285659
+ reported once per reason change instead of every 15s poll. In-memory by design — it is a log
285660
+ de-duplicator, not state anything reads back. Pruned each sweep to the current candidate set
285661
+ (review finding #11), so a card that leaves review by ANY route drops out; admission and
285662
+ soft-delete also clear their entry directly. */
285663
+ mergeSweepHoldReasons = /* @__PURE__ */ new Map();
285381
285664
  mergeActive = /* @__PURE__ */ new Set();
285382
285665
  /** Capacity-deferred ids stay out of the runnable queue until their retry timer fires. */
285383
285666
  capacityDeferredMergeTaskIds = /* @__PURE__ */ new Set();
@@ -287559,25 +287842,174 @@ ${detail}`);
287559
287842
  );
287560
287843
  });
287561
287844
  const allowFlags = await Promise.all(candidates.map((t) => this.allowInReviewMergeProcessing(t, settings, this.runtime.getTaskStore())));
287562
- const unrunGateFlags = await Promise.all(
287563
- candidates.map(async (t) => {
287564
- try {
287565
- const ir2 = await resolveWorkflowIrForTask(this.runtime.getTaskStore(), t.id, reviewLaneIrCache);
287566
- if (!ir2) return false;
287567
- return findUnrunRequiredPreMergeStepIds(ir2, t).length > 0;
287568
- } catch {
287845
+ const sweepBatch = await this.loadMergeSweepBatch(candidates);
287846
+ const admissions = await Promise.all(
287847
+ candidates.map((t) => this.classifyMergeSweepCandidate(t, reviewLaneIrCache, sweepBatch))
287848
+ );
287849
+ const candidateIds = new Set(candidates.map((t) => t.id));
287850
+ for (const heldTaskId of [...this.mergeSweepHoldReasons.keys()]) {
287851
+ if (!candidateIds.has(heldTaskId)) this.mergeSweepHoldReasons.delete(heldTaskId);
287852
+ }
287853
+ const eligible = sortTasksByPriorityThenAgeAndId(
287854
+ candidates.filter((t, i) => {
287855
+ if (!allowFlags[i]) return false;
287856
+ const admission = admissions[i];
287857
+ if (!admission.admit) {
287858
+ const previous = this.mergeSweepHoldReasons.get(t.id);
287859
+ if (previous !== admission.reason) {
287860
+ this.mergeSweepHoldReasons.set(t.id, admission.reason);
287861
+ runtimeLog.log(`Auto-merge sweep holding ${t.id}: ${admission.reason} (graph owns the merge decision)`);
287862
+ }
287569
287863
  return false;
287570
287864
  }
287865
+ this.mergeSweepHoldReasons.delete(t.id);
287866
+ return true;
287571
287867
  })
287572
287868
  );
287573
- const eligible = sortTasksByPriorityThenAgeAndId(
287574
- candidates.filter((_3, i) => allowFlags[i] && !unrunGateFlags[i])
287575
- );
287576
287869
  for (const t of eligible) {
287577
287870
  this.internalEnqueueMerge(t.id);
287578
287871
  }
287579
287872
  return eligible.length;
287580
287873
  }
287874
+ /*
287875
+ FNXC:MergeAuthority 2026-08-23-18:05 (FN-9191 + FN-9193 wedges):
287876
+ Gather the sweep-admission inputs for one candidate. Probes are best-effort, but they do NOT all
287877
+ degrade in the same direction — the direction is chosen per probe by what a wrong answer costs:
287878
+
287879
+ - liveness, continuations: a throw must not wedge the whole lane, and the classifier's other
287880
+ fences still hold, so they degrade toward "not live" / "unreadable" (the latter refuses
287881
+ initiation on its own — see `continuationsReadable`).
287882
+ - `gatesSatisfied`: the ONE deliberate fail-CLOSED probe (review finding #8). If we cannot prove
287883
+ the pre-merge gates are satisfied we must not start a merge; withholding costs a delay, while
287884
+ admitting costs an unreviewed branch on the target. The real door re-checks authoritatively.
287885
+
287886
+ MULTI-REPO / WORKSPACE (and shared-branch groups) resolve through the SAME rules, deliberately:
287887
+ - `branch-group-member-integration` and `branch-group-promotion` are merge-region kinds, so a
287888
+ shared-branch member parked at either is admitted like any other merge-region card.
287889
+ - A workspace sub-repo land registers `workspace-repo-land` / `workspace-repo-acquire` paths in
287890
+ `activeSessionRegistry`, so a task mid-land reads as live and is held — this is what stops a
287891
+ second dispatch racing a partial land.
287892
+ - A workspace partial land re-enqueues through `internalEnqueueMerge` directly (the error path
287893
+ below), which never consults this gate; the merging status it leaves also satisfies
287894
+ `interruptedMergeAttempt` so a restarted engine re-drives the same land.
287895
+ - `isMergePending` covers the cross-node case: another engine holding a durable merge-dispatch
287896
+ lease reads as live here, so two nodes on one central DB cannot both dispatch the same land.
287897
+ NOTE it answers `true` from its OWN catch on an unreadable lease probe (see its FNXC note), so
287898
+ a persistently broken lease read holds every card in this lane rather than racing another
287899
+ node. That is the intended conservative direction; it is not a silent no-op.
287900
+
287901
+ `batch` carries the sweep-wide batched reads (review finding #10) so this runs at O(1) queries per
287902
+ poll instead of O(cards). It is optional: single-card callers (the column-entry handoff, the
287903
+ unpause re-enqueue, the pre-dispatch re-check) pass nothing and fall back to per-task reads.
287904
+ */
287905
+ async classifyMergeSweepCandidate(task, irCache, batch, opts = {}) {
287906
+ const store = this.runtime.getTaskStore();
287907
+ let irTrust = "unresolved-selection";
287908
+ let ir2 = null;
287909
+ try {
287910
+ const resolved = await resolveWorkflowIrForTaskWithProvenance(store, task.id, irCache);
287911
+ ir2 = resolved.ir;
287912
+ irTrust = resolved.source === "selection" ? "cards-own" : resolved.selectionAbsent === true ? "effective-default" : "unresolved-selection";
287913
+ } catch {
287914
+ ir2 = null;
287915
+ irTrust = "unresolved-selection";
287916
+ }
287917
+ let hasLiveSession = false;
287918
+ try {
287919
+ hasLiveSession = executingTaskLock.has(task.id) || activeSessionRegistry.pathsForTask(task.id).length > 0 || !opts.ignoreOwnMergePipeline && (this.activeMergeTaskId === task.id || await this.isMergePending(task.id));
287920
+ } catch {
287921
+ hasLiveSession = false;
287922
+ }
287923
+ let continuationPositions = [];
287924
+ let continuationsReadable = true;
287925
+ try {
287926
+ const readContinuations = store.listWorkflowWorkItemsForTask;
287927
+ const items = batch?.continuations ? batch.continuations.get(task.id) ?? [] : typeof readContinuations === "function" ? await readContinuations.call(store, task.id, { kinds: ["task"] }) : [];
287928
+ continuationPositions = items.filter((item) => ACTIVE_WORKFLOW_WORK_ITEM_STATES.includes(item.state)).map((item) => ir2 ? classifyWorkflowNodeMergeRegion(ir2, item.nodeId) : "unknown");
287929
+ } catch {
287930
+ continuationsReadable = false;
287931
+ continuationPositions = [];
287932
+ }
287933
+ if (batch && !batch.continuationsReadable) continuationsReadable = false;
287934
+ let mergeRequestActive = false;
287935
+ if (!isActiveMergeStatus(task.status)) {
287936
+ try {
287937
+ mergeRequestActive = batch?.mergeRequests ? isActiveMergeRequestState(batch.mergeRequests.get(task.id)?.state) : isActiveMergeRequestState((await store.getMergeRequestRecordAsync?.(task.id))?.state);
287938
+ } catch {
287939
+ mergeRequestActive = false;
287940
+ }
287941
+ }
287942
+ let gatesSatisfied = true;
287943
+ try {
287944
+ const reviewColumns = /* @__PURE__ */ new Set([task.column]);
287945
+ gatesSatisfied = !getTaskMergeBlocker({ ...task, steps: task.steps ?? [] }, {
287946
+ reviewColumns,
287947
+ requiredPreMergeStepIds: ir2 ? resolveRequiredPreMergeStepIds(ir2, task.enabledWorkflowSteps) : void 0
287948
+ });
287949
+ } catch {
287950
+ gatesSatisfied = false;
287951
+ }
287952
+ const updatedAtMs = Date.parse(task.updatedAt ?? "");
287953
+ const quiescentMs = Number.isFinite(updatedAtMs) ? Math.max(0, Date.now() - updatedAtMs) : Number.POSITIVE_INFINITY;
287954
+ return classifyMergeSweepAdmission({
287955
+ irTrust,
287956
+ continuationPositions,
287957
+ continuationsReadable,
287958
+ mergeConfirmed: task.mergeDetails?.mergeConfirmed === true,
287959
+ hasLiveSession,
287960
+ interruptedMergeAttempt: isActiveMergeStatus(task.status) || mergeRequestActive,
287961
+ quiescentMs,
287962
+ gatesSatisfied
287963
+ });
287964
+ }
287965
+ /*
287966
+ FNXC:MergeAuthority 2026-08-23-20:05 (review finding #10):
287967
+ One batched read per sweep instead of one per card. The sweep asks the same two questions of every
287968
+ review-lane card on every 15s poll; per-task reads made that 2N queries. A failed batch read is
287969
+ reported as unreadable rather than as empty, so it refuses initiation exactly like the per-task
287970
+ failure path does.
287971
+ */
287972
+ /*
287973
+ FNXC:MergeAuthority 2026-08-23-20:05 (review finding #2):
287974
+ Position-only dispatch guard. Answers ONE question — "do this card's active continuations still
287975
+ place it inside its workflow's merge region?" — and fails OPEN on every uncertainty (unreadable
287976
+ continuations, an unresolvable workflow, no continuation at all), because the merge door behind it
287977
+ is the authority on everything else. A card with NO active continuation is authorized here by
287978
+ design: that is the shape of the interrupted-merge and quiescent-stall recoveries.
287979
+ */
287980
+ async isDispatchStillGraphAuthorized(task) {
287981
+ const store = this.runtime.getTaskStore();
287982
+ try {
287983
+ const readContinuations = store.listWorkflowWorkItemsForTask;
287984
+ if (typeof readContinuations !== "function") return true;
287985
+ const items = await readContinuations.call(this.runtime.getTaskStore(), task.id, { kinds: ["task"] });
287986
+ const active2 = items.filter((item) => ACTIVE_WORKFLOW_WORK_ITEM_STATES.includes(item.state));
287987
+ if (active2.length === 0) return true;
287988
+ const resolved = await resolveWorkflowIrForTaskWithProvenance(this.runtime.getTaskStore(), task.id);
287989
+ if (resolved.source !== "selection" && resolved.selectionAbsent !== true) return true;
287990
+ return active2.some((item) => classifyWorkflowNodeMergeRegion(resolved.ir, item.nodeId) !== "outside-merge-region");
287991
+ } catch {
287992
+ return true;
287993
+ }
287994
+ }
287995
+ async loadMergeSweepBatch(candidates) {
287996
+ const store = this.runtime.getTaskStore();
287997
+ const ids = candidates.map((t) => t.id);
287998
+ const batch = { continuationsReadable: true };
287999
+ if (ids.length === 0) return batch;
288000
+ try {
288001
+ batch.continuations = typeof store.listWorkflowWorkItemsForTasks === "function" ? await store.listWorkflowWorkItemsForTasks.call(this.runtime.getTaskStore(), ids, { kinds: ["task"] }) : void 0;
288002
+ } catch {
288003
+ batch.continuations = void 0;
288004
+ batch.continuationsReadable = false;
288005
+ }
288006
+ try {
288007
+ batch.mergeRequests = typeof store.getMergeRequestRecordsAsync === "function" ? await store.getMergeRequestRecordsAsync.call(this.runtime.getTaskStore(), ids) : void 0;
288008
+ } catch {
288009
+ batch.mergeRequests = void 0;
288010
+ }
288011
+ return batch;
288012
+ }
287581
288013
  reconcileStaleMergeActive() {
287582
288014
  let cleared = 0;
287583
288015
  for (const taskId of [...this.mergeActive]) {
@@ -287896,6 +288328,11 @@ ${detail}`);
287896
288328
  }
287897
288329
  continue;
287898
288330
  }
288331
+ if (!task.mergeDetails?.mergeConfirmed && !await this.isDispatchStillGraphAuthorized(task)) {
288332
+ runtimeLog.log(`Auto-merge dispatch skipped for ${taskId}: graph moved the card out of its merge region while it was queued`);
288333
+ this.clearMergeActive(taskId);
288334
+ continue;
288335
+ }
287899
288336
  if (task.mergeDetails?.mergeConfirmed) {
287900
288337
  const branchGroupForFastPathCandidate = isSharedBranchGroupMemberIntegration(task) ? store.getBranchGroup?.(task.branchContext?.groupId) : null;
287901
288338
  const fastPathDefaultBranch = await resolveIntegrationBranch(this.config.workingDirectory, settings2);
@@ -288015,7 +288452,7 @@ ${detail}`);
288015
288452
  continue;
288016
288453
  }
288017
288454
  }
288018
- const blockerReason = getTaskHardMergeBlocker({
288455
+ const blockerReason = getMergeConfirmedFinalizationBlocker({
288019
288456
  ...task,
288020
288457
  /*
288021
288458
  FNXC:WorkflowResolvedColumns 2026-07-30-18:05 (this parked ALREADY-MERGED work as failed):
@@ -288029,7 +288466,13 @@ ${detail}`);
288029
288466
  and for the reason recorded there: `"in-review"` is the review-eligible SENTINEL for this
288030
288467
  helper, not a lifecycle column, so a merge-confirmed card evaluates the same blocker set
288031
288468
  on a custom workflow as on the builtin one. The column identity of an already-landed card
288032
- is not what this check is for — paused / error / incomplete steps still apply.
288469
+ is not what this check is for.
288470
+
288471
+ FNXC:MergeConfirmedFinalization 2026-08-23-21:40 (FN-9193): incomplete steps NO LONGER
288472
+ apply here. The fast path above already proved this merge landed, and holding a landed
288473
+ card out of `done` for unfinished steps is what left FN-9193 permanently unfinalizable —
288474
+ a restart then replanned fresh pending steps and re-created the very block it was meant
288475
+ to clear. Paused/error/pre-merge-step blockers still apply.
288033
288476
  */
288034
288477
  column: REVIEW_ELIGIBLE_SENTINEL_COLUMN,
288035
288478
  // Merge-confirmed tasks have already landed. Treat stale merge
@@ -288039,6 +288482,14 @@ ${detail}`);
288039
288482
  status: clearMergeConfirmedTransientStatus(task.status),
288040
288483
  error: void 0
288041
288484
  });
288485
+ const unfinishedFastPathSteps = getUnfinishedStepTitles(task);
288486
+ if (unfinishedFastPathSteps.length > 0) {
288487
+ await store.logEntry(
288488
+ taskId,
288489
+ `Finalizing proven merge with ${unfinishedFastPathSteps.length} unfinished step(s) \u2014 the branch already landed, so these did not run: ${unfinishedFastPathSteps.slice(0, 8).join("; ")}`,
288490
+ "MergeConfirmedFinalizeUnfinishedSteps"
288491
+ ).catch(() => void 0);
288492
+ }
288042
288493
  if (blockerReason) {
288043
288494
  await store.updateTask(taskId, {
288044
288495
  status: "failed",
@@ -289190,6 +289641,11 @@ ${detail}`);
289190
289641
  runtimeLog.warn(`Auto-merge handoff (${task.id}): clearing stale mergeActive before enqueue`);
289191
289642
  this.clearMergeActive(task.id);
289192
289643
  }
289644
+ const handoffAdmission = await this.classifyMergeSweepCandidate(latestTask, /* @__PURE__ */ new Map(), void 0, { ignoreOwnMergePipeline: true });
289645
+ if (!handoffAdmission.admit) {
289646
+ runtimeLog.log(`Auto-merge handoff (${task.id}) skipped: ${handoffAdmission.reason} (graph owns the merge decision)`);
289647
+ return;
289648
+ }
289193
289649
  this.internalEnqueueMerge(task.id);
289194
289650
  } catch (err) {
289195
289651
  runtimeLog.warn(
@@ -289277,6 +289733,11 @@ ${detail}`);
289277
289733
  if (this.options.getTaskMergeBlocker?.(task)) {
289278
289734
  return;
289279
289735
  }
289736
+ const unpauseAdmission = await this.classifyMergeSweepCandidate(task, /* @__PURE__ */ new Map(), void 0, { ignoreOwnMergePipeline: true });
289737
+ if (!unpauseAdmission.admit) {
289738
+ runtimeLog.log(`In-review unpause: ${task.id} not re-enqueued: ${unpauseAdmission.reason} (graph owns the merge decision)`);
289739
+ return;
289740
+ }
289280
289741
  runtimeLog.log(`Unpaused in-review task re-enqueued for auto-merge: ${task.id}`);
289281
289742
  this.internalEnqueueMerge(task.id);
289282
289743
  } catch (err) {
@@ -289287,6 +289748,7 @@ ${detail}`);
289287
289748
  };
289288
289749
  this.taskDeletedHandler = (task) => {
289289
289750
  this.pausedReviewTaskIds.delete(task.id);
289751
+ this.mergeSweepHoldReasons.delete(task.id);
289290
289752
  const queueLengthBefore = this.mergeQueue.length;
289291
289753
  this.mergeQueue = this.mergeQueue.filter((queuedTaskId) => queuedTaskId !== task.id);
289292
289754
  const removedFromQueue = this.mergeQueue.length !== queueLengthBefore;