@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/extension.js CHANGED
@@ -982,6 +982,8 @@ var init_execution_and_ui = __esm({
982
982
  "midnight",
983
983
  // 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.
984
984
  "velvet",
985
+ // 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.
986
+ "iceberg",
985
987
  // 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.
986
988
  "factory-dark",
987
989
  // 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.
@@ -1460,6 +1462,13 @@ function getTaskHardMergeBlocker(task, options = {}) {
1460
1462
  requiredPreMergeStepIds: options.requiredPreMergeStepIds
1461
1463
  });
1462
1464
  }
1465
+ function getMergeConfirmedFinalizationBlocker(task, options = {}) {
1466
+ const hasDurableMergeRecord = task.mergeDetails?.mergeConfirmed === true && typeof task.mergeDetails.commitSha === "string" && task.mergeDetails.commitSha.length > 0;
1467
+ return getTaskHardMergeBlocker(hasDurableMergeRecord ? { ...task, steps: [] } : task, options);
1468
+ }
1469
+ function getUnfinishedStepTitles(task) {
1470
+ return (task.steps ?? []).filter((step) => NON_TERMINAL_STEP_STATUSES.has(step.status)).map((step, index2) => step.name?.trim() || `step ${index2 + 1}`);
1471
+ }
1463
1472
  function isTaskReadyForMerge(task, options = {}) {
1464
1473
  return getTaskMergeBlocker(task, options) === void 0;
1465
1474
  }
@@ -2446,12 +2455,16 @@ var init_settings_schema = __esm({
2446
2455
  executorEscalationProvider: void 0,
2447
2456
  executorEscalationModelId: void 0,
2448
2457
  executorEscalationNodeId: void 0,
2449
- reviewConvergenceEscalationEnabled: true,
2450
- reviewConvergenceEscalationProvider: void 0,
2451
- reviewConvergenceEscalationModelId: void 0,
2452
- reviewArbitrationEnabled: true,
2453
- reviewArbitrationProvider: void 0,
2454
- reviewArbitrationModelId: void 0,
2458
+ /*
2459
+ FNXC:ReviewConvergence 2026-08-23-21:05:
2460
+ FN-149's six review-convergence/arbitration keys are declared in BUILTIN_WORKFLOW_SETTINGS, which
2461
+ the U4 hard-move made the SINGLE source of truth for a moved key's default. Re-declaring them here
2462
+ put them back in the legacy object the hard-move emptied, so an undeclared key on a custom workflow
2463
+ could resolve to this literal instead of the declaration default — the exact drift
2464
+ `workflow-settings-fallback-alignment` guards. The declaration defaults are identical (both
2465
+ `*Enabled` default true, the provider/model lanes carry no default), and the reader uses
2466
+ `!== false`, so removing them here preserves behaviour and restores the invariant.
2467
+ */
2455
2468
  /**
2456
2469
  * FNXC:Merge 2026-06-26-00:00:
2457
2470
  * 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.
@@ -10612,6 +10625,35 @@ var init_workflow_ir_types = __esm({
10612
10625
  }
10613
10626
  });
10614
10627
 
10628
+ // ../core/src/workflows/workflow-merge-region.ts
10629
+ function isMergeRegionNode(node) {
10630
+ return MERGE_REGION_ENTRY_NODE_KINDS.has(node.kind) || node.config?.seam === "merge";
10631
+ }
10632
+ function classifyWorkflowNodeMergeRegion(ir2, nodeId) {
10633
+ if (ir2.version !== "v2" || !Array.isArray(ir2.nodes)) return "unknown";
10634
+ const node = ir2.nodes.find((candidate) => candidate.id === nodeId);
10635
+ if (!node) return "unknown";
10636
+ return isMergeRegionNode(node) ? "merge-region" : "outside-merge-region";
10637
+ }
10638
+ var MERGE_REGION_ENTRY_NODE_KINDS;
10639
+ var init_workflow_merge_region = __esm({
10640
+ "../core/src/workflows/workflow-merge-region.ts"() {
10641
+ "use strict";
10642
+ MERGE_REGION_ENTRY_NODE_KINDS = /* @__PURE__ */ new Set([
10643
+ "merge-gate",
10644
+ "merge-attempt",
10645
+ "manual-merge-hold",
10646
+ "retry-backoff",
10647
+ "recovery-router",
10648
+ "branch-group-member-integration",
10649
+ "branch-group-promotion",
10650
+ "pr-create",
10651
+ "pr-respond",
10652
+ "pr-merge"
10653
+ ]);
10654
+ }
10655
+ });
10656
+
10615
10657
  // ../core/src/workflows/workflow-extension-types.ts
10616
10658
  function workflowExtensionRegistryId(pluginId, extensionId) {
10617
10659
  return `plugin:${pluginId}:${extensionId}`;
@@ -12351,6 +12393,7 @@ var init_workflow_ir = __esm({
12351
12393
  "../core/src/workflows/workflow-ir.ts"() {
12352
12394
  "use strict";
12353
12395
  init_workflow_ir_types();
12396
+ init_workflow_merge_region();
12354
12397
  init_workflow_extension_registry();
12355
12398
  init_types();
12356
12399
  init_trait_registry();
@@ -12431,18 +12474,7 @@ var init_workflow_ir = __esm({
12431
12474
  "done",
12432
12475
  "archived"
12433
12476
  ];
12434
- INTERPRETER_ENTRY_NODE_KINDS = /* @__PURE__ */ new Set([
12435
- "merge-gate",
12436
- "merge-attempt",
12437
- "manual-merge-hold",
12438
- "retry-backoff",
12439
- "recovery-router",
12440
- "branch-group-member-integration",
12441
- "branch-group-promotion",
12442
- "pr-create",
12443
- "pr-respond",
12444
- "pr-merge"
12445
- ]);
12477
+ INTERPRETER_ENTRY_NODE_KINDS = MERGE_REGION_ENTRY_NODE_KINDS;
12446
12478
  MERGE_CLASS_NODE_KINDS = /* @__PURE__ */ new Set([
12447
12479
  "merge-gate",
12448
12480
  "merge-attempt",
@@ -13848,6 +13880,13 @@ Lint, tests, and typecheck are also hard quality gates:
13848
13880
  - Keep fixing failures caused by your change until lint, impacted tests, build, and typecheck pass.
13849
13881
  - If the repository exposes a typecheck command, run it and fix failures caused by your change.
13850
13882
  - 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.
13883
+
13884
+ **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:
13885
+ - 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.
13886
+ - 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.
13887
+ - 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.
13888
+ - Added a public store/service method? Update the inventory/drift guards that require every public surface to be classified.
13889
+ 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.
13851
13890
  - 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.
13852
13891
  - Do not repeatedly rerun a broad failing or hanging workspace command without a new hypothesis and a narrower confirming command.
13853
13892
 
@@ -14120,6 +14159,7 @@ files with assertions that run via a test runner. Typechecks and builds are NOT
14120
14159
  tests. Manual verification is NOT a test.
14121
14160
 
14122
14161
  - Each implementation step should include writing tests for the code being changed
14162
+ - **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.
14123
14163
  - 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.
14124
14164
  - 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.
14125
14165
  - 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)
@@ -24444,7 +24484,16 @@ var init_project = __esm({
24444
24484
  "chat_rooms",
24445
24485
  "chat_room_members",
24446
24486
  "chat_room_messages",
24447
- "chat_token_usage"
24487
+ "chat_token_usage",
24488
+ /*
24489
+ FNXC:WorkspaceCoordination 2026-08-23-20:05:
24490
+ FN-9059 (migration 0060) added these two tables to the schema without registering them here, so
24491
+ every registry consumer under-counted them: the PostgreSQL test harness never TRUNCATEd them
24492
+ between tests (a held `repo:<repo>` lease leaked from one case into the next and made the next
24493
+ land report the previous test's successor as the busy holder), and health compaction skipped them.
24494
+ */
24495
+ "workspace_coordination_leases",
24496
+ "workspace_land_intents"
24448
24497
  ];
24449
24498
  }
24450
24499
  });
@@ -28574,10 +28623,6 @@ function resolveRequiredPreMergeStepIds(ir2, enabledWorkflowSteps) {
28574
28623
  resolveWorkflowOptionalSteps(ir2).filter((step) => step.phase === "pre-merge").filter((step) => isWorkflowOptionalGroupEnabled(enabledWorkflowSteps, step.templateId, step.defaultOn)).map((step) => step.templateId)
28575
28624
  );
28576
28625
  }
28577
- function findUnrunRequiredPreMergeStepIds(ir2, task) {
28578
- const results = task.workflowStepResults ?? [];
28579
- return [...resolveRequiredPreMergeStepIds(ir2, task.enabledWorkflowSteps)].filter((workflowStepId) => !results.some((result2) => result2.workflowStepId === workflowStepId));
28580
- }
28581
28626
  var init_required_pre_merge_steps = __esm({
28582
28627
  "../core/src/merge/required-pre-merge-steps.ts"() {
28583
28628
  "use strict";
@@ -28585,6 +28630,42 @@ var init_required_pre_merge_steps = __esm({
28585
28630
  }
28586
28631
  });
28587
28632
 
28633
+ // ../core/src/merge/merge-sweep-admission.ts
28634
+ function classifyMergeSweepAdmission(input) {
28635
+ if (input.hasLiveSession) return { admit: false, reason: "live-session" };
28636
+ if (input.mergeConfirmed) return { admit: true, reason: "merge-confirmed-finalization" };
28637
+ if (!input.gatesSatisfied) return { admit: false, reason: "gates-unsatisfied" };
28638
+ if (!input.continuationsReadable) return { admit: false, reason: "continuations-unreadable" };
28639
+ if (input.irTrust === "unresolved-selection") {
28640
+ return quiescentStallDecision(input, "unresolved-workflow-ir");
28641
+ }
28642
+ if (input.continuationPositions.length > 0) {
28643
+ if (input.continuationPositions.includes("merge-region")) {
28644
+ return { admit: true, reason: "at-merge-region-node" };
28645
+ }
28646
+ if (input.continuationPositions.includes("unknown")) {
28647
+ return { admit: true, reason: "drifted-continuation-node" };
28648
+ }
28649
+ return { admit: false, reason: "not-at-merge-region-node" };
28650
+ }
28651
+ if (input.interruptedMergeAttempt) {
28652
+ return { admit: true, reason: "interrupted-merge-attempt" };
28653
+ }
28654
+ return quiescentStallDecision(input, "quiescent-stall-recovery");
28655
+ }
28656
+ function quiescentStallDecision(input, reason) {
28657
+ const floorMs = input.quiescenceFloorMs ?? DEFAULT_MERGE_SWEEP_QUIESCENCE_MS;
28658
+ if (!(input.quiescentMs >= floorMs)) return { admit: false, reason: "too-recent" };
28659
+ return { admit: true, reason };
28660
+ }
28661
+ var DEFAULT_MERGE_SWEEP_QUIESCENCE_MS;
28662
+ var init_merge_sweep_admission = __esm({
28663
+ "../core/src/merge/merge-sweep-admission.ts"() {
28664
+ "use strict";
28665
+ DEFAULT_MERGE_SWEEP_QUIESCENCE_MS = 2 * 6e4;
28666
+ }
28667
+ });
28668
+
28588
28669
  // ../core/src/config/moved-settings.ts
28589
28670
  function isMovedSettingsKey(key) {
28590
28671
  return MOVED_SETTINGS_KEY_SET.has(key);
@@ -71595,6 +71676,21 @@ async function listWorkflowWorkItemsForTaskImpl(store, taskId, opts = {}) {
71595
71676
  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;
71596
71677
  return rows.map((row) => store.rowToWorkflowWorkItem(row));
71597
71678
  }
71679
+ async function listWorkflowWorkItemsForTasksImpl(store, taskIds, opts = {}) {
71680
+ const byTask = /* @__PURE__ */ new Map();
71681
+ for (const taskId of taskIds) byTask.set(taskId, []);
71682
+ if (taskIds.length === 0) return byTask;
71683
+ const layer = store.asyncLayer;
71684
+ const scope = projectScopeFor(project_exports.workflowWorkItems.projectId, layer.projectId);
71685
+ const conditions = [scope, inArray(project_exports.workflowWorkItems.taskId, [...taskIds])];
71686
+ if (opts.kinds?.length) conditions.push(inArray(project_exports.workflowWorkItems.kind, opts.kinds));
71687
+ const rows = await layer.db.select().from(project_exports.workflowWorkItems).where(and(...conditions));
71688
+ for (const row of rows) {
71689
+ const item = store.rowToWorkflowWorkItem(row);
71690
+ byTask.get(item.taskId)?.push(item);
71691
+ }
71692
+ return byTask;
71693
+ }
71598
71694
  async function listDueWorkflowWorkItemsImpl(store, filter = {}) {
71599
71695
  const layer = store.asyncLayer;
71600
71696
  return listDueWorkflowWorkItems(layer.db, filter);
@@ -90913,6 +91009,17 @@ async function getMergeRequestRecordAsyncImpl(store, taskId) {
90913
91009
  const row = rows[0];
90914
91010
  return row ? store.rowToMergeRequestRecord(row) : null;
90915
91011
  }
91012
+ async function getMergeRequestRecordsAsyncImpl(store, taskIds) {
91013
+ const byTask = /* @__PURE__ */ new Map();
91014
+ if (taskIds.length === 0) return byTask;
91015
+ const layer = store.asyncLayer;
91016
+ const rows = await layer.db.select().from(project_exports.mergeRequests).where(inArray(project_exports.mergeRequests.taskId, [...taskIds]));
91017
+ for (const row of rows) {
91018
+ const record2 = store.rowToMergeRequestRecord(row);
91019
+ byTask.set(record2.taskId, record2);
91020
+ }
91021
+ return byTask;
91022
+ }
90916
91023
  function getWorkflowWorkItemByIdentityImpl(store, runId, taskId, nodeId, kind) {
90917
91024
  const row = store.db.prepare("SELECT * FROM workflow_work_items WHERE runId = ? AND taskId = ? AND nodeId = ? AND kind = ?").get(runId, taskId, nodeId, kind);
90918
91025
  return row ? store.rowToWorkflowWorkItem(row) : null;
@@ -106872,6 +106979,10 @@ var init_store = __esm({
106872
106979
  async getMergeRequestRecordAsync(taskId) {
106873
106980
  return getMergeRequestRecordAsyncImpl(this, taskId);
106874
106981
  }
106982
+ /** Batched `getMergeRequestRecordAsync` — one query for many tasks (FNXC:MergeAuthority 2026-08-23-20:05). */
106983
+ async getMergeRequestRecordsAsync(taskIds) {
106984
+ return getMergeRequestRecordsAsyncImpl(this, taskIds);
106985
+ }
106875
106986
  async projectMergeRequestToWorkflowWorkItem(taskId, opts = {}) {
106876
106987
  return projectMergeRequestToWorkflowWorkItemImpl(this, taskId, opts);
106877
106988
  }
@@ -106914,6 +107025,10 @@ var init_store = __esm({
106914
107025
  async listWorkflowWorkItemsForTask(taskId, opts = {}) {
106915
107026
  return listWorkflowWorkItemsForTaskImpl(this, taskId, opts);
106916
107027
  }
107028
+ /** Batched `listWorkflowWorkItemsForTask` — one query for many tasks (FNXC:MergeAuthority 2026-08-23-20:05). */
107029
+ async listWorkflowWorkItemsForTasks(taskIds, opts = {}) {
107030
+ return listWorkflowWorkItemsForTasksImpl(this, taskIds, opts);
107031
+ }
106917
107032
  /**
106918
107033
  * FNXC:RuntimeWorkflowAsync 2026-06-24-17:12:
106919
107034
  */
@@ -136626,6 +136741,7 @@ __export(src_exports, {
136626
136741
  DEFAULT_MAX_WORKTREES: () => DEFAULT_MAX_WORKTREES,
136627
136742
  DEFAULT_MEMORY_BACKEND: () => DEFAULT_MEMORY_BACKEND,
136628
136743
  DEFAULT_MEMORY_DREAMS_SCHEDULE: () => DEFAULT_MEMORY_DREAMS_SCHEDULE,
136744
+ DEFAULT_MERGE_SWEEP_QUIESCENCE_MS: () => DEFAULT_MERGE_SWEEP_QUIESCENCE_MS,
136629
136745
  DEFAULT_MIN_INTERVAL_MS: () => DEFAULT_MIN_INTERVAL_MS,
136630
136746
  DEFAULT_MISSION_ASSERTION_TYPE: () => DEFAULT_MISSION_ASSERTION_TYPE,
136631
136747
  DEFAULT_MOBILE_NAV_PRIMARY_ITEMS: () => DEFAULT_MOBILE_NAV_PRIMARY_ITEMS,
@@ -136792,6 +136908,7 @@ __export(src_exports, {
136792
136908
  MERGE_AUDIT_AUTO_RECOVERY_MODES: () => MERGE_AUDIT_AUTO_RECOVERY_MODES,
136793
136909
  MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT: () => MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT,
136794
136910
  MERGE_CONFIRMED_TRANSIENT_STATUSES: () => MERGE_CONFIRMED_TRANSIENT_STATUSES,
136911
+ MERGE_REGION_ENTRY_NODE_KINDS: () => MERGE_REGION_ENTRY_NODE_KINDS,
136795
136912
  MESH_INTENT_STATES: () => MESH_INTENT_STATES,
136796
136913
  MESH_RECONCILIATION_OUTCOMES: () => MESH_RECONCILIATION_OUTCOMES,
136797
136914
  MESH_WRITE_CLASSES: () => MESH_WRITE_CLASSES,
@@ -137160,6 +137277,7 @@ __export(src_exports, {
137160
137277
  classifyFileScopeFromPrompt: () => classifyFileScopeFromPrompt,
137161
137278
  classifyGhError: () => classifyGhError,
137162
137279
  classifyInsightRunError: () => classifyInsightRunError,
137280
+ classifyMergeSweepAdmission: () => classifyMergeSweepAdmission,
137163
137281
  classifyPlannerActionSideEffect: () => classifyPlannerActionSideEffect,
137164
137282
  classifyProviderError: () => classifyProviderError,
137165
137283
  classifyReadStaleness: () => classifyReadStaleness,
@@ -137170,6 +137288,7 @@ __export(src_exports, {
137170
137288
  classifyTaskCommitAssociationConfidence: () => classifyTaskCommitAssociationConfidence,
137171
137289
  classifyTerminalFailureAutoRecovery: () => classifyTerminalFailureAutoRecovery,
137172
137290
  classifyWorkflowAgentNode: () => classifyWorkflowAgentNode,
137291
+ classifyWorkflowNodeMergeRegion: () => classifyWorkflowNodeMergeRegion,
137173
137292
  cleanupOldAiSessions: () => cleanupOldAiSessions,
137174
137293
  cleanupStaleAiSessions: () => cleanupStaleAiSessions,
137175
137294
  clearActiveEmbeddedRuntimeUrl: () => clearActiveEmbeddedRuntimeUrl,
@@ -137369,7 +137488,6 @@ __export(src_exports, {
137369
137488
  findOrphanedSettingValues: () => findOrphanedSettingValues,
137370
137489
  findPendingPreMergeStep: () => findPendingPreMergeStep,
137371
137490
  findSameAgentDuplicates: () => findSameAgentDuplicates,
137372
- findUnrunRequiredPreMergeStepIds: () => findUnrunRequiredPreMergeStepIds,
137373
137491
  findVitestProcessIds: () => findVitestProcessIds,
137374
137492
  findWorkflowColumn: () => findWorkflowColumn,
137375
137493
  findWorkflowEventShapeViolations: () => findWorkflowEventShapeViolations,
@@ -137441,6 +137559,7 @@ __export(src_exports, {
137441
137559
  getMcpDiscoverySources: () => getMcpDiscoverySources,
137442
137560
  getMemoryBackend: () => getMemoryBackend,
137443
137561
  getMemoryBackendCapabilities: () => getMemoryBackendCapabilities,
137562
+ getMergeConfirmedFinalizationBlocker: () => getMergeConfirmedFinalizationBlocker,
137444
137563
  getOpenIncidentByGroupingKeyAsync: () => getOpenIncidentByGroupingKeyAsync,
137445
137564
  getOverriddenKeys: () => getOverriddenKeys,
137446
137565
  getPiExtensionDiscoveryDirs: () => getPiExtensionDiscoveryDirs,
@@ -137479,6 +137598,7 @@ __export(src_exports, {
137479
137598
  getTotalAgentActiveMs: () => getTotalAgentActiveMs,
137480
137599
  getTrait: () => getTrait,
137481
137600
  getTraitRegistry: () => getTraitRegistry,
137601
+ getUnfinishedStepTitles: () => getUnfinishedStepTitles,
137482
137602
  getValidTransitions: () => getValidTransitions,
137483
137603
  getWorkflowEventBus: () => getWorkflowEventBus,
137484
137604
  getWorkflowExtensionRegistry: () => getWorkflowExtensionRegistry,
@@ -137562,6 +137682,7 @@ __export(src_exports, {
137562
137682
  isManualTrigger: () => isManualTrigger,
137563
137683
  isMcpSecretRef: () => isMcpSecretRef,
137564
137684
  isMechanicalRoutableContext: () => isMechanicalRoutableContext,
137685
+ isMergeRegionNode: () => isMergeRegionNode,
137565
137686
  isMergeRequestContractShadowEnabled: () => isMergeRequestContractShadowEnabled,
137566
137687
  isMeshIntentState: () => isMeshIntentState,
137567
137688
  isMeshWriteClass: () => isMeshWriteClass,
@@ -138265,6 +138386,8 @@ var init_src2 = __esm({
138265
138386
  init_workflow_merge_proof();
138266
138387
  init_workflow_optional_steps();
138267
138388
  init_required_pre_merge_steps();
138389
+ init_merge_sweep_admission();
138390
+ init_workflow_merge_region();
138268
138391
  init_workflow_prompt_overrides();
138269
138392
  init_builtin_stepwise_coding_workflow_ir();
138270
138393
  init_builtin_stepwise_final_review_coding_workflow_ir();
@@ -147613,39 +147736,42 @@ var init_provider_registration = __esm({
147613
147736
  });
147614
147737
 
147615
147738
  // ../engine/src/util/emit-bounded-run-audit.ts
147616
- async function emitBoundedRunAudit2(host, event, options = {}) {
147739
+ function emitBoundedRunAudit2(host, event, options = {}) {
147740
+ return emitBoundedRunAuditWithOutcome2(host, event, options).then(() => void 0);
147741
+ }
147742
+ async function emitBoundedRunAuditWithOutcome2(host, event, options = {}) {
147617
147743
  let defaultLog2;
147618
147744
  try {
147619
147745
  defaultLog2 = runAuditLog;
147620
147746
  } catch {
147621
147747
  defaultLog2 = void 0;
147622
147748
  }
147623
- const log42 = options.log ?? defaultLog2 ?? { warn: (message) => console.warn(message) };
147749
+ const log42 = options.log ?? defaultLog2 ?? { warn: () => void 0 };
147624
147750
  const sink = host?.recordRunAuditEvent;
147625
- if (typeof sink !== "function") return;
147751
+ if (typeof sink !== "function") return { outcome: "absent" };
147626
147752
  let sinkPromise;
147627
147753
  try {
147628
147754
  sinkPromise = Promise.resolve(sink.call(host, event));
147629
- } catch {
147755
+ } catch (error3) {
147630
147756
  log42.warn(`[run-audit] failed to record ${event.mutationType}`);
147631
- return;
147757
+ return { outcome: "failed", error: error3 };
147632
147758
  }
147633
147759
  void sinkPromise.catch(() => void 0);
147634
- await new Promise((resolve64) => {
147760
+ return await new Promise((resolve64) => {
147635
147761
  const timer2 = setTimeout(() => {
147636
147762
  log42.warn(`[run-audit] timed out recording ${event.mutationType}`);
147637
- resolve64();
147763
+ resolve64({ outcome: "timed-out" });
147638
147764
  }, options.timeoutMs ?? RUN_AUDIT_EMIT_TIMEOUT_MS);
147639
147765
  timer2.unref?.();
147640
147766
  void sinkPromise.then(
147641
147767
  () => {
147642
147768
  clearTimeout(timer2);
147643
- resolve64();
147769
+ resolve64({ outcome: "recorded" });
147644
147770
  },
147645
- () => {
147771
+ (error3) => {
147646
147772
  clearTimeout(timer2);
147647
147773
  log42.warn(`[run-audit] failed to record ${event.mutationType}`);
147648
- resolve64();
147774
+ resolve64({ outcome: "failed", error: error3 });
147649
147775
  }
147650
147776
  );
147651
147777
  });
@@ -180792,6 +180918,7 @@ function createRunAuditor(store, context2) {
180792
180918
  },
180793
180919
  database: async () => {
180794
180920
  },
180921
+ databaseWithOutcome: async () => ({ outcome: "absent" }),
180795
180922
  filesystem: async () => {
180796
180923
  },
180797
180924
  sandbox: async () => {
@@ -180805,6 +180932,7 @@ function createRunAuditor(store, context2) {
180805
180932
  },
180806
180933
  database: async () => {
180807
180934
  },
180935
+ databaseWithOutcome: async () => ({ outcome: "absent" }),
180808
180936
  filesystem: async () => {
180809
180937
  },
180810
180938
  sandbox: async () => {
@@ -180847,6 +180975,28 @@ function createRunAuditor(store, context2) {
180847
180975
  };
180848
180976
  await emitBoundedRunAudit2(store, eventInput);
180849
180977
  },
180978
+ /*
180979
+ FNXC:RunAudit 2026-08-23-18:30:
180980
+ Identical write to `database`, returning whether the row landed for the emitters that gate their
180981
+ own state on a proven write (see the interface note). Still bounded and non-throwing.
180982
+ */
180983
+ databaseWithOutcome: async (input) => {
180984
+ const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context2.taskId;
180985
+ return await emitBoundedRunAuditWithOutcome2(store, {
180986
+ taskId: inferredTaskId,
180987
+ agentId: context2.agentId,
180988
+ runId: context2.runId,
180989
+ domain: "database",
180990
+ mutationType: input.type,
180991
+ target: input.target,
180992
+ metadata: {
180993
+ phase: context2.phase,
180994
+ ...context2.source ? { source: context2.source } : {},
180995
+ ...context2.taskLineageId ? { taskLineageId: context2.taskLineageId } : {},
180996
+ ...input.metadata
180997
+ }
180998
+ });
180999
+ },
180850
181000
  filesystem: async (input) => {
180851
181001
  const eventInput = {
180852
181002
  taskId: context2.taskId,
@@ -182782,10 +182932,13 @@ var init_scheduler = __esm({
182782
182932
  * Validate that a task's filesystem state is intact.
182783
182933
  * Checks that the task directory exists and PROMPT.md is present and non-empty.
182784
182934
  *
182785
- * @param id - The task ID to validate
182935
+ * @param task - The task (id + title) to validate
182786
182936
  * @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
182787
182937
  */
182788
- async validateTaskFilesystem(id) {
182938
+ async validateTaskFilesystem(task) {
182939
+ const id = task.id;
182940
+ const titleRedirect = nonExecutableDuplicateRedirectReason(null, task.title);
182941
+ if (titleRedirect) return { valid: false, reason: titleRedirect };
182789
182942
  if (typeof this.store.getTasksDir !== "function") {
182790
182943
  return { valid: true };
182791
182944
  }
@@ -182802,7 +182955,7 @@ var init_scheduler = __esm({
182802
182955
  if (!content || content.trim().length === 0) {
182803
182956
  return { valid: false, reason: "missing or empty PROMPT.md" };
182804
182957
  }
182805
- const duplicateOnly = nonExecutableDuplicateRedirectReason(content);
182958
+ const duplicateOnly = nonExecutableDuplicateRedirectReason(content, task.title);
182806
182959
  if (duplicateOnly) {
182807
182960
  return { valid: false, reason: duplicateOnly };
182808
182961
  }
@@ -183480,7 +183633,7 @@ var init_scheduler = __esm({
183480
183633
  );
183481
183634
  }
183482
183635
  }
183483
- const validation = await this.validateTaskFilesystem(task.id);
183636
+ const validation = await this.validateTaskFilesystem(task);
183484
183637
  if (!validation.valid) {
183485
183638
  schedulerLog.warn(`Task ${task.id} filesystem validation failed: ${validation.reason}`);
183486
183639
  const replanColumn = await moveTaskToReplanColumn(this.store, task);
@@ -192514,6 +192667,20 @@ var init_step_session_executor = __esm({
192514
192667
  * Safe to call multiple times (idempotent). Call this in a `finally` block
192515
192668
  * after `executeAll()`.
192516
192669
  */
192670
+ /**
192671
+ * FNXC:StepParallelWorktrees 2026-08-23-21:40:
192672
+ * `parallelBranches` holds only branches THIS executor created in `createStepWorktree`
192673
+ * (`fusion/step-<idx>-<worktree-name>`), so their provenance is known here rather than inferred.
192674
+ * FN-9161's `isFusionDeletableBranch` answers by NAME and reads anything that is not
192675
+ * `fusion/<task-id>...` as operator-supplied, so applying it here silently skipped every
192676
+ * step branch and leaked one ref per parallel step. Protect what an operator can actually own —
192677
+ * the task's working branch and its explicit override — and dispose of the rest.
192678
+ */
192679
+ isDisposableStepBranch(branchName) {
192680
+ const task = this.options.taskDetail;
192681
+ const operatorOwned = [task.branch, task.branchContext?.branchOverride?.branch].filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.trim());
192682
+ return !operatorOwned.includes(branchName.trim());
192683
+ }
192517
192684
  async cleanup() {
192518
192685
  if (this.activeSessions.size > 0) {
192519
192686
  await this.terminateAllSessions();
@@ -192540,7 +192707,7 @@ var init_step_session_executor = __esm({
192540
192707
  }
192541
192708
  for (const [stepIdx, branchName] of this.parallelBranches) {
192542
192709
  try {
192543
- if (!isFusionDeletableBranch(this.options.taskDetail, branchName)) continue;
192710
+ if (!this.isDisposableStepBranch(branchName)) continue;
192544
192711
  await execAsync18(`git branch -D "${branchName}"`, {
192545
192712
  cwd: this.options.rootDir
192546
192713
  });
@@ -193095,7 +193262,7 @@ Follow instructions precisely and avoid unrelated changes.`,
193095
193262
  });
193096
193263
  }
193097
193264
  const branch = this.parallelBranches.get(stepIdx);
193098
- if (branch && isFusionDeletableBranch(this.options.taskDetail, branch)) {
193265
+ if (branch && this.isDisposableStepBranch(branch)) {
193099
193266
  await execAsync18(`git branch -D "${branch}"`, {
193100
193267
  cwd: this.options.rootDir
193101
193268
  });
@@ -200410,7 +200577,7 @@ async function finalizeProvenAutoMergeTask({
200410
200577
  });
200411
200578
  return { outcome: "blocked", task: latest, previousColumn: latest.column, reason };
200412
200579
  }
200413
- const hardBlocker = getTaskHardMergeBlocker({
200580
+ const hardBlocker = getMergeConfirmedFinalizationBlocker({
200414
200581
  ...latest,
200415
200582
  /*
200416
200583
  FNXC:WorkflowMerge 2026-06-29-09:15:
@@ -200425,6 +200592,14 @@ async function finalizeProvenAutoMergeTask({
200425
200592
  status: clearMergeConfirmedTransientStatus(latest.status),
200426
200593
  error: void 0
200427
200594
  });
200595
+ const unfinishedSteps = getUnfinishedStepTitles(latest);
200596
+ if (unfinishedSteps.length > 0) {
200597
+ await store.logEntry(
200598
+ taskId,
200599
+ `Finalizing proven merge with ${unfinishedSteps.length} unfinished step(s) \u2014 the branch already landed, so these did not run: ${unfinishedSteps.slice(0, 8).join("; ")}`,
200600
+ "MergeConfirmedFinalizeUnfinishedSteps"
200601
+ ).catch(() => void 0);
200602
+ }
200428
200603
  if (hardBlocker) {
200429
200604
  fence?.assertOwned("finalization");
200430
200605
  await store.updateTask(taskId, {
@@ -220453,15 +220628,21 @@ var init_self_healing = __esm({
220453
220628
  const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonical);
220454
220629
  if (!isNearDuplicateCanonicalInactive(canonical ?? void 0, canonicalFlags)) continue;
220455
220630
  const promptPath = join76(this.options.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
220456
- if (existsSync61(promptPath)) {
220631
+ const written = existsSync61(promptPath) ? readFileSync23(promptPath, "utf-8") : "";
220632
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
220633
+ const canonicalMarkerId = canonicalId.toUpperCase();
220634
+ if (duplicateResolution.conflict || duplicateResolution.marker && duplicateResolution.marker.canonicalId !== canonicalMarkerId) {
220635
+ continue;
220636
+ }
220637
+ if (duplicateResolution.source === "prompt") {
220457
220638
  try {
220458
- const written = readFileSync23(promptPath, "utf-8");
220459
- if (parseExplicitDuplicateMarker(written)) {
220460
- rmSync7(promptPath, { force: true });
220461
- }
220639
+ rmSync7(promptPath, { force: true });
220462
220640
  } catch {
220463
220641
  }
220464
220642
  }
220643
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === canonicalMarkerId) {
220644
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${canonicalMarkerId}` });
220645
+ }
220465
220646
  await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalId));
220466
220647
  if (typeof this.store.logEntry === "function") {
220467
220648
  await Promise.resolve(this.store.logEntry(
@@ -223847,7 +224028,7 @@ var init_self_healing = __esm({
223847
224028
  mergeTargetBranch: mergeTarget.branch,
223848
224029
  mergeTargetSource: mergeTarget.source
223849
224030
  };
223850
- const hardBlocker = getTaskHardMergeBlocker({
224031
+ const hardBlocker = getMergeConfirmedFinalizationBlocker({
223851
224032
  ...task,
223852
224033
  steps: task.steps ?? [],
223853
224034
  workflowStepResults: task.workflowStepResults
@@ -224032,7 +224213,7 @@ var init_self_healing = __esm({
224032
224213
  mergeTargetBranch: mergeTarget.branch,
224033
224214
  mergeTargetSource: mergeTarget.source
224034
224215
  };
224035
- const hardBlocker = getTaskHardMergeBlocker({
224216
+ const hardBlocker = getMergeConfirmedFinalizationBlocker({
224036
224217
  ...task,
224037
224218
  steps: task.steps ?? [],
224038
224219
  workflowStepResults: task.workflowStepResults
@@ -226727,14 +226908,12 @@ var init_self_healing = __esm({
226727
226908
  for (const task of candidates) {
226728
226909
  try {
226729
226910
  const promptPath = join76(this.options.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
226730
- if (!existsSync61(promptPath)) {
226731
- continue;
226732
- }
226733
- const written = readFileSync23(promptPath, "utf-8");
226734
- const marker = parseExplicitDuplicateMarker(written);
226735
- if (!marker) {
226911
+ const written = existsSync61(promptPath) ? readFileSync23(promptPath, "utf-8") : "";
226912
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
226913
+ if (!duplicateResolution.marker || duplicateResolution.conflict) {
226736
226914
  continue;
226737
226915
  }
226916
+ const marker = duplicateResolution.marker;
226738
226917
  if (processedMarkers >= 50) {
226739
226918
  break;
226740
226919
  }
@@ -226747,7 +226926,10 @@ var init_self_healing = __esm({
226747
226926
  const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonicalTask);
226748
226927
  if (!canonicalTask || isNearDuplicateCanonicalInactive(canonicalTask, canonicalFlags)) {
226749
226928
  if (canClearInactiveMarker) {
226750
- rmSync7(promptPath, { force: true });
226929
+ if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
226930
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
226931
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
226932
+ }
226751
226933
  const priorClearCount = typeof task.sourceMetadata?.duplicateMarkerClearCount === "number" ? task.sourceMetadata.duplicateMarkerClearCount : 0;
226752
226934
  const patch = buildMarkerClearedReplanTaskPatch(marker.canonicalId, priorClearCount);
226753
226935
  await this.store.updateTask(task.id, patch);
@@ -226766,7 +226948,10 @@ var init_self_healing = __esm({
226766
226948
  const resolution = settings.triageDuplicateResolution ?? "prompt";
226767
226949
  if (resolution === "prompt" && isTriageDuplicateKeepAcknowledged(task.sourceMetadata, canonicalTask.id)) {
226768
226950
  if (canClearInactiveMarker) {
226769
- rmSync7(promptPath, { force: true });
226951
+ if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
226952
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
226953
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
226954
+ }
226770
226955
  const priorKeepClears = typeof task.sourceMetadata?.duplicateMarkerClearCount === "number" ? task.sourceMetadata.duplicateMarkerClearCount : 0;
226771
226956
  const keepExhausted = priorKeepClears >= 1;
226772
226957
  await this.store.updateTask(
@@ -226791,7 +226976,10 @@ var init_self_healing = __esm({
226791
226976
  await flagTriageDuplicate(this.store, task.id, canonicalTask.id);
226792
226977
  await this.store.updateTask(task.id, { paused: true, pausedReason: "duplicate-decision-required", status: null });
226793
226978
  } else {
226794
- rmSync7(promptPath, { force: true });
226979
+ if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
226980
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
226981
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
226982
+ }
226795
226983
  await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalTask.id));
226796
226984
  if (typeof this.store.logEntry === "function") {
226797
226985
  await Promise.resolve(this.store.logEntry(
@@ -228828,7 +229016,7 @@ var init_triage = __esm({
228828
229016
  planLog.warn(`${task.id} planning recovery skipped \u2014 PROMPT.md failed deterministic validation (${deterministicSpecFailure})`);
228829
229017
  return false;
228830
229018
  }
228831
- const isExplicitDuplicateRedirect = Boolean(parseExplicitDuplicateMarker(written));
229019
+ const isExplicitDuplicateRedirect = Boolean(resolveExplicitDuplicateMarker(written, task.title).marker);
228832
229020
  const workflow = await resolveWorkflowIrForTask(this.store, task.id).catch(() => void 0);
228833
229021
  const requiresPromptImplementationSteps = workflow?.nodes.some(
228834
229022
  (node) => node.kind === "parse-steps" && (node.config?.artifact === void 0 || node.config.artifact === "PROMPT.md") && node.config?.parser === "step-headings" && node.config?.requireStepsUnlessNoCommits === true
@@ -229250,7 +229438,7 @@ var init_triage = __esm({
229250
229438
  phase: "triage",
229251
229439
  source: "triage"
229252
229440
  });
229253
- void throttleAuditor.database({
229441
+ void (throttleAuditor.databaseWithOutcome ?? throttleAuditor.database)({
229254
229442
  type: "task:plan-admission-throttled",
229255
229443
  target: eligibleIds[0] ?? this.rootDir,
229256
229444
  metadata: {
@@ -229263,7 +229451,9 @@ var init_triage = __esm({
229263
229451
  processingCount: this.processing.size,
229264
229452
  processingTaskIds: processingIds
229265
229453
  }
229266
- }).then(() => {
229454
+ }).then((result2) => {
229455
+ const outcome = result2?.outcome;
229456
+ if (outcome !== "recorded" && outcome !== "absent") return;
229267
229457
  this.lastPlanThrottleSignature = throttleSignature;
229268
229458
  void Promise.all(eligibleIds.map((taskId) => this.store.logEntry(taskId, capacityReason))).catch((logErr) => {
229269
229459
  planLog.warn(`Failed to write planning capacity reason: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
@@ -229407,6 +229597,44 @@ var init_triage = __esm({
229407
229597
  return false;
229408
229598
  }
229409
229599
  }
229600
+ /**
229601
+ * Resolves an exact prompt/title redirect before this task claims any planning capacity.
229602
+ *
229603
+ * FNXC:DuplicateIntake 2026-08-09-01:31:
229604
+ * FN-8840 requires title redirects to take the same duplicate-decision route as prompt
229605
+ * redirects before `specifyTask()` can start a planner session. Reading the prompt here is
229606
+ * required only to detect a conflicting exact marker; a missing or unreadable prompt leaves
229607
+ * a title-only redirect actionable and never lets it consume an implementation session.
229608
+ *
229609
+ * FNXC:DuplicateIntake 2026-08-23-18:30:
229610
+ * This whole title-aware path (plus the `source: "prompt" | "title"` plumbing in
229611
+ * `clearExplicitDuplicateMarker` and the conflict guard in `finalizeApprovedTaskBody`) was
229612
+ * removed by accident in 1cf86baa1c ("refactor: package code organization wave 18"), which
229613
+ * reverted FN-8840 in triage.ts only — every other FN-8840 surface (dashboard task routes,
229614
+ * `handle-graph-failure.ts`, core `resolveExplicitDuplicateMarker`) kept it. A title-only
229615
+ * `DUPLICATE: <id>` card therefore consumed a full planner session again instead of taking the
229616
+ * duplicate-decision route. Restored here; do not "simplify" back to prompt-only parsing.
229617
+ */
229618
+ async finalizeExplicitDuplicateBeforePlanning(task) {
229619
+ try {
229620
+ const liveTask = await this.store.getTask(task.id).catch(() => null);
229621
+ if (!liveTask || liveTask.paused === true || liveTask.userPaused === true) return false;
229622
+ const promptPath = join77(this.rootDir, ".fusion", "tasks", liveTask.id, "PROMPT.md");
229623
+ const written = await readFile45(promptPath, "utf-8").catch(() => "");
229624
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, liveTask.title);
229625
+ if (!duplicateResolution.marker && !duplicateResolution.conflict) return false;
229626
+ const settings = await mergeEffectiveSettings(this.store, liveTask, await this.store.getSettings());
229627
+ if (duplicateResolution.conflict) {
229628
+ await this.finalizeApprovedTask(liveTask, written, settings);
229629
+ return true;
229630
+ }
229631
+ return await this.tryFinalizeExplicitDuplicateMarker(liveTask, written, settings);
229632
+ } catch (error3) {
229633
+ const message = error3 instanceof Error ? error3.message : String(error3);
229634
+ planLog.warn(`${task.id}: pre-planning duplicate resolution failed open: ${message}`);
229635
+ return false;
229636
+ }
229637
+ }
229410
229638
  async specifyTask(task) {
229411
229639
  if (this.resetFence.isResetHoldActive(task.id)) {
229412
229640
  if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
@@ -229419,6 +229647,11 @@ var init_triage = __esm({
229419
229647
  this.coordinatorAdmittedTaskIds.delete(task.id);
229420
229648
  return;
229421
229649
  }
229650
+ if (await this.finalizeExplicitDuplicateBeforePlanning(task)) {
229651
+ if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
229652
+ this.coordinatorAdmittedTaskIds.delete(task.id);
229653
+ return;
229654
+ }
229422
229655
  if (await this.flagImportNearDuplicateBeforePlanning(task)) {
229423
229656
  if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
229424
229657
  return;
@@ -229952,12 +230185,28 @@ This is a multi-repository workspace task. Your final PROMPT.md MUST include a n
229952
230185
  content,
229953
230186
  false
229954
230187
  ),
230188
+ /*
230189
+ FNXC:PlanArtifactPersistence 2026-08-23-19:24:
230190
+ The project-database mirror is BEST-EFFORT — the comment above says so and validation below
230191
+ owns the verdict. FN-151 (c1818ea819) routed it through the reset-fenced writer, which does
230192
+ not swallow, so an infrastructure failure inside the lifecycle lock (e.g. a
230193
+ PlanningLifecycleLockTransportError) aborted a planning attempt whose authoritative PROMPT.md
230194
+ had already been written, and pre-empted both the unchanged-PROMPT verdict and FN-8911's
230195
+ marker-consuming path. Degrade the mirror the way `mirrorPlanToProjectDb` always did; the
230196
+ authoritative `writeAuthoritativePrompt` half deliberately still throws, and only a
230197
+ lifecycle-lock TRANSPORT rejection is degraded here — every other mirror failure keeps
230198
+ aborting the attempt exactly as before.
230199
+ */
229955
230200
  mirrorAuthoritativePlan: async (content) => await this.persistResetFencedPlanningArtifact(
229956
230201
  task,
229957
230202
  planningGeneration,
229958
230203
  content,
229959
230204
  true
229960
- )
230205
+ ).catch((error3) => {
230206
+ if (!isPlanningLifecycleLockTransportError(error3)) throw error3;
230207
+ planLog.warn(`${task.id}: lifecycle-lock transport failure while mirroring the authoritative plan into the project database: ${error3.message}`);
230208
+ return false;
230209
+ })
229961
230210
  });
229962
230211
  if (planPersistence.outcome === "recovery-fenced") return;
229963
230212
  if (planPersistence.outcome === "recovered") {
@@ -230621,11 +230870,11 @@ ${lines.join("\n")}` }],
230621
230870
  }
230622
230871
  async tryFinalizeExplicitDuplicateMarker(task, written, settings, options = {}, report2 = { outcome: "parked" }) {
230623
230872
  try {
230624
- const explicitDuplicateMarker = parseExplicitDuplicateMarker(written);
230625
- if (!explicitDuplicateMarker) {
230873
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
230874
+ if (!duplicateResolution.marker || duplicateResolution.conflict) {
230626
230875
  return false;
230627
230876
  }
230628
- const canonicalId = explicitDuplicateMarker.canonicalId;
230877
+ const canonicalId = duplicateResolution.marker.canonicalId;
230629
230878
  const canonicalTask = await this.store.getTask(canonicalId);
230630
230879
  if (canonicalTask?.id.toLowerCase() === task.id.toLowerCase()) {
230631
230880
  return false;
@@ -230740,7 +230989,12 @@ ${lines.join("\n")}` }],
230740
230989
  */
230741
230990
  async clearDuplicateMarkerForReplan(task, canonicalId, feedback, options) {
230742
230991
  if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
230743
- await rm11(join77(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
230992
+ if (options?.source !== "title") {
230993
+ await rm11(join77(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
230994
+ }
230995
+ if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === canonicalId) {
230996
+ await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${canonicalId}` });
230997
+ }
230744
230998
  })) return false;
230745
230999
  const priorClearCount = options?.priorClearCount ?? 0;
230746
231000
  if (options?.exhausted) {
@@ -230770,8 +231024,14 @@ ${lines.join("\n")}` }],
230770
231024
  }
230771
231025
  async finalizeApprovedTaskBody(task, writtenInput, settings, options = {}, report2 = { outcome: "parked" }) {
230772
231026
  let written = writtenInput;
230773
- if (await this.recoverMissingPromptBeforeRelease(task)) return;
230774
- const explicitDuplicateMarker = parseExplicitDuplicateMarker(written);
231027
+ const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
231028
+ if (duplicateResolution.conflict) {
231029
+ await this.updatePlanningStateIfStillCurrent(task, { status: "needs-replan", error: null });
231030
+ 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.");
231031
+ return;
231032
+ }
231033
+ if (!duplicateResolution.marker && await this.recoverMissingPromptBeforeRelease(task)) return;
231034
+ const explicitDuplicateMarker = duplicateResolution.marker;
230775
231035
  if (explicitDuplicateMarker) {
230776
231036
  await this.updatePlanningStateIfStillCurrent(task, (live) => {
230777
231037
  const customFields = { ...live.customFields ?? {} };
@@ -230779,6 +231039,7 @@ ${lines.join("\n")}` }],
230779
231039
  return { customFields };
230780
231040
  });
230781
231041
  const canonicalId = explicitDuplicateMarker.canonicalId;
231042
+ const duplicateSource = duplicateResolution.source ?? "prompt";
230782
231043
  const canonicalTask = await this.store.getTask(canonicalId).catch(() => null);
230783
231044
  const canClearInactiveMarker = task.userPaused !== true && (task.paused !== true || task.pausedReason === "duplicate-decision-required") && (task.pausedReason == null || task.pausedReason === "duplicate-decision-required");
230784
231045
  const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonicalTask);
@@ -230790,7 +231051,7 @@ ${lines.join("\n")}` }],
230790
231051
  task,
230791
231052
  canonicalId,
230792
231053
  buildInactiveDuplicateClearFeedback(canonicalId),
230793
- { exhausted: false, priorClearCount }
231054
+ { exhausted: false, priorClearCount, source: duplicateSource }
230794
231055
  );
230795
231056
  }
230796
231057
  return;
@@ -230803,7 +231064,7 @@ ${lines.join("\n")}` }],
230803
231064
  task,
230804
231065
  canonicalId,
230805
231066
  buildKeepDuplicateClearFeedback(canonicalId),
230806
- { exhausted: priorClearCount >= 1, priorClearCount }
231067
+ { exhausted: priorClearCount >= 1, priorClearCount, source: duplicateSource }
230807
231068
  );
230808
231069
  }
230809
231070
  return;
@@ -230841,7 +231102,8 @@ ${lines.join("\n")}` }],
230841
231102
  await this.clearDuplicateMarkerForReplan(
230842
231103
  task,
230843
231104
  canonicalId,
230844
- buildKeepDuplicateClearFeedback(canonicalId)
231105
+ buildKeepDuplicateClearFeedback(canonicalId),
231106
+ { source: duplicateSource }
230845
231107
  );
230846
231108
  return;
230847
231109
  }
@@ -242477,6 +242739,7 @@ function buildActionGateContext(deps, taskId, agent, projectDefaultPolicy) {
242477
242739
  executorLog.warn(`${taskId}: failed to suspend in-flight session while awaiting approval: ${error3 instanceof Error ? error3.message : String(error3)}`);
242478
242740
  });
242479
242741
  }
242742
+ void emitApprovalMail({ messageStore: deps.messageStore ?? void 0, approvalRequestId, toolName: decision.toolName, taskId, agentId: actorId, agentName: actorName });
242480
242743
  if (agent && deps.agentStore) {
242481
242744
  await deps.agentStore.updateAgentState(agent.id, "paused");
242482
242745
  await deps.agentStore.updateAgent(agent.id, { pauseReason: "awaiting-approval" });
@@ -242495,6 +242758,7 @@ var init_build_action_gate_context = __esm({
242495
242758
  "../engine/src/executor/build-action-gate-context.ts"() {
242496
242759
  "use strict";
242497
242760
  init_src2();
242761
+ init_approval_mail();
242498
242762
  init_workflow_agent_router();
242499
242763
  init_logger3();
242500
242764
  }
@@ -242554,6 +242818,7 @@ function buildPermanentAgentGatingContext(deps, taskId, agent, projectDefaultPol
242554
242818
  deps.approvalSuspended.delete(taskId);
242555
242819
  throw error3;
242556
242820
  }
242821
+ void emitApprovalMail({ messageStore: deps.messageStore ?? void 0, approvalRequestId, toolName, taskId, agentId: actorId, agentName: actorName });
242557
242822
  }
242558
242823
  };
242559
242824
  }
@@ -242561,6 +242826,7 @@ var init_build_permanent_agent_gating_context = __esm({
242561
242826
  "../engine/src/executor/build-permanent-agent-gating-context.ts"() {
242562
242827
  "use strict";
242563
242828
  init_src2();
242829
+ init_approval_mail();
242564
242830
  init_permanent_agent_gating();
242565
242831
  }
242566
242832
  });
@@ -257874,6 +258140,7 @@ function buildBuildActionGateContextDeps(host) {
257874
258140
  get approvalRequestStore() {
257875
258141
  return host.approvalRequestStore;
257876
258142
  },
258143
+ messageStore: host.options.messageStore,
257877
258144
  activeWorkflowAuthorities: host.activeWorkflowAuthorities,
257878
258145
  activeWorkflowGraphAbortControllers: host.activeWorkflowGraphAbortControllers
257879
258146
  };
@@ -258030,7 +258297,8 @@ function buildBuildPermanentAgentGatingContextDeps(host) {
258030
258297
  approvalSuspended: host.approvalSuspended,
258031
258298
  get approvalRequestStore() {
258032
258299
  return host.approvalRequestStore;
258033
- }
258300
+ },
258301
+ messageStore: host.options.messageStore
258034
258302
  };
258035
258303
  }
258036
258304
  function buildPersistTokenUsageDeps(host) {
@@ -264810,6 +265078,10 @@ async function enforceAiMergeSquashGates(params) {
264810
265078
  if (!(error3 instanceof FileScopeViolationError)) throw error3;
264811
265079
  await execFileAsync26("git", ["reset", "--hard", params.tipSha], { cwd: params.mergeRoot });
264812
265080
  await execFileAsync26("git", ["clean", "-fd"], { cwd: params.mergeRoot });
265081
+ try {
265082
+ await params.store.updateTask(params.taskId, { aiMergeReviewReconciliation: null });
265083
+ } catch {
265084
+ }
264813
265085
  throw error3;
264814
265086
  }
264815
265087
  }
@@ -265683,11 +265955,12 @@ async function cleanupAiMergeWorktree(input) {
265683
265955
  }
265684
265956
  const removalTargets = canonicalRoot === mergeRoot ? [mergeRoot] : [canonicalRoot, mergeRoot];
265685
265957
  const cleanupMetadata = { taskId, mergeRoot: canonicalRoot, requestedMergeRoot: mergeRoot };
265686
- let alreadyAbsent = false;
265958
+ let alreadyAbsent = removalTargets.every((target) => !existsSync83(target));
265687
265959
  if (worktreeAdded) {
265688
265960
  try {
265689
265961
  await gitRunner(["worktree", "remove", "--force", canonicalRoot], projectRootDir);
265690
- await audit.git({ type: "merge:ai-worktree-cleanup", target: canonicalRoot, metadata: { ...cleanupMetadata, phase: "git-remove", success: true } });
265962
+ if (alreadyAbsent) await log42(`AI merge cleanup: worktree ${canonicalRoot} was already absent before git removal; treating cleanup as idempotent`);
265963
+ await audit.git({ type: "merge:ai-worktree-cleanup", target: canonicalRoot, metadata: { ...cleanupMetadata, phase: "git-remove", success: true, ...alreadyAbsent ? { alreadyAbsent: true, idempotent: true } : {} } });
265691
265964
  } catch (err) {
265692
265965
  const error3 = describeCleanupError(err);
265693
265966
  const code = getErrorStringProperty(err, "code");
@@ -267227,7 +267500,7 @@ async function landWorkspaceTask(store, task, workspaceRootDir, options = {}, de
267227
267500
  const requiresRepositoryReviewEvidence = approvedReviewEvidence !== void 0 || (mergeBoundaryTask.enabledWorkflowSteps ?? []).some((step) => /review/i.test(step));
267228
267501
  const approvalMissingRepositories = Object.entries(mergeBoundaryFingerprints).filter(([repoRel]) => requiresRepositoryReviewEvidence && !approvedReviewEvidence?.[repoRel]).map(([repoRel]) => repoRel).sort();
267229
267502
  const contentChangedRepositories = Object.entries(mergeBoundaryFingerprints).filter(([repoRel, fingerprint]) => requiresRepositoryReviewEvidence && Boolean(approvedReviewEvidence?.[repoRel]) && approvedReviewEvidence?.[repoRel]?.fingerprint !== fingerprint).map(([repoRel]) => repoRel).sort();
267230
- const changedFiles = normalizedMergeBoundaryFiles.filter((file) => !persistedReviewFiles.includes(file)).sort();
267503
+ const changedFiles = requiresRepositoryReviewEvidence ? normalizedMergeBoundaryFiles.filter((file) => !persistedReviewFiles.includes(file)).sort() : [];
267231
267504
  if (approvalMissingRepositories.length > 0) {
267232
267505
  throw new WorkspaceReviewRequiredError(taskId, {
267233
267506
  kind: "approval-missing",
@@ -284228,6 +284501,9 @@ function emitMergeRequestShadowDequeueParityAudit(store, legacyTaskId, shadowTas
284228
284501
  metadata: { legacyTaskId, shadowTaskId, agree }
284229
284502
  }, { log: runtimeLog });
284230
284503
  }
284504
+ function isActiveMergeRequestState(state) {
284505
+ return state === "running" || state === "retrying";
284506
+ }
284231
284507
  function getPrMergeRetryNotBefore(task) {
284232
284508
  const retries = task.mergeRetries ?? 0;
284233
284509
  const updatedAt = task.updatedAt ? Date.parse(task.updatedAt) : NaN;
@@ -284309,6 +284585,7 @@ var init_project_engine = __esm({
284309
284585
  init_overseer_advisor_service();
284310
284586
  init_overseer_advise_tool();
284311
284587
  init_agent_session_helpers();
284588
+ init_active_session_registry();
284312
284589
  init_pr_reconcile();
284313
284590
  init_pr_comment_handler();
284314
284591
  init_notifier();
@@ -284509,6 +284786,12 @@ var init_project_engine = __esm({
284509
284786
  };
284510
284787
  // ── Auto-merge state ──
284511
284788
  mergeQueue = [];
284789
+ /* FNXC:MergeAuthority 2026-08-23-18:05: last logged sweep-hold reason per task, so a held card is
284790
+ reported once per reason change instead of every 15s poll. In-memory by design — it is a log
284791
+ de-duplicator, not state anything reads back. Pruned each sweep to the current candidate set
284792
+ (review finding #11), so a card that leaves review by ANY route drops out; admission and
284793
+ soft-delete also clear their entry directly. */
284794
+ mergeSweepHoldReasons = /* @__PURE__ */ new Map();
284512
284795
  mergeActive = /* @__PURE__ */ new Set();
284513
284796
  /** Capacity-deferred ids stay out of the runnable queue until their retry timer fires. */
284514
284797
  capacityDeferredMergeTaskIds = /* @__PURE__ */ new Set();
@@ -286690,25 +286973,174 @@ ${detail}`);
286690
286973
  );
286691
286974
  });
286692
286975
  const allowFlags = await Promise.all(candidates.map((t) => this.allowInReviewMergeProcessing(t, settings, this.runtime.getTaskStore())));
286693
- const unrunGateFlags = await Promise.all(
286694
- candidates.map(async (t) => {
286695
- try {
286696
- const ir2 = await resolveWorkflowIrForTask(this.runtime.getTaskStore(), t.id, reviewLaneIrCache);
286697
- if (!ir2) return false;
286698
- return findUnrunRequiredPreMergeStepIds(ir2, t).length > 0;
286699
- } catch {
286976
+ const sweepBatch = await this.loadMergeSweepBatch(candidates);
286977
+ const admissions = await Promise.all(
286978
+ candidates.map((t) => this.classifyMergeSweepCandidate(t, reviewLaneIrCache, sweepBatch))
286979
+ );
286980
+ const candidateIds = new Set(candidates.map((t) => t.id));
286981
+ for (const heldTaskId of [...this.mergeSweepHoldReasons.keys()]) {
286982
+ if (!candidateIds.has(heldTaskId)) this.mergeSweepHoldReasons.delete(heldTaskId);
286983
+ }
286984
+ const eligible = sortTasksByPriorityThenAgeAndId(
286985
+ candidates.filter((t, i) => {
286986
+ if (!allowFlags[i]) return false;
286987
+ const admission = admissions[i];
286988
+ if (!admission.admit) {
286989
+ const previous = this.mergeSweepHoldReasons.get(t.id);
286990
+ if (previous !== admission.reason) {
286991
+ this.mergeSweepHoldReasons.set(t.id, admission.reason);
286992
+ runtimeLog.log(`Auto-merge sweep holding ${t.id}: ${admission.reason} (graph owns the merge decision)`);
286993
+ }
286700
286994
  return false;
286701
286995
  }
286996
+ this.mergeSweepHoldReasons.delete(t.id);
286997
+ return true;
286702
286998
  })
286703
286999
  );
286704
- const eligible = sortTasksByPriorityThenAgeAndId(
286705
- candidates.filter((_2, i) => allowFlags[i] && !unrunGateFlags[i])
286706
- );
286707
287000
  for (const t of eligible) {
286708
287001
  this.internalEnqueueMerge(t.id);
286709
287002
  }
286710
287003
  return eligible.length;
286711
287004
  }
287005
+ /*
287006
+ FNXC:MergeAuthority 2026-08-23-18:05 (FN-9191 + FN-9193 wedges):
287007
+ Gather the sweep-admission inputs for one candidate. Probes are best-effort, but they do NOT all
287008
+ degrade in the same direction — the direction is chosen per probe by what a wrong answer costs:
287009
+
287010
+ - liveness, continuations: a throw must not wedge the whole lane, and the classifier's other
287011
+ fences still hold, so they degrade toward "not live" / "unreadable" (the latter refuses
287012
+ initiation on its own — see `continuationsReadable`).
287013
+ - `gatesSatisfied`: the ONE deliberate fail-CLOSED probe (review finding #8). If we cannot prove
287014
+ the pre-merge gates are satisfied we must not start a merge; withholding costs a delay, while
287015
+ admitting costs an unreviewed branch on the target. The real door re-checks authoritatively.
287016
+
287017
+ MULTI-REPO / WORKSPACE (and shared-branch groups) resolve through the SAME rules, deliberately:
287018
+ - `branch-group-member-integration` and `branch-group-promotion` are merge-region kinds, so a
287019
+ shared-branch member parked at either is admitted like any other merge-region card.
287020
+ - A workspace sub-repo land registers `workspace-repo-land` / `workspace-repo-acquire` paths in
287021
+ `activeSessionRegistry`, so a task mid-land reads as live and is held — this is what stops a
287022
+ second dispatch racing a partial land.
287023
+ - A workspace partial land re-enqueues through `internalEnqueueMerge` directly (the error path
287024
+ below), which never consults this gate; the merging status it leaves also satisfies
287025
+ `interruptedMergeAttempt` so a restarted engine re-drives the same land.
287026
+ - `isMergePending` covers the cross-node case: another engine holding a durable merge-dispatch
287027
+ lease reads as live here, so two nodes on one central DB cannot both dispatch the same land.
287028
+ NOTE it answers `true` from its OWN catch on an unreadable lease probe (see its FNXC note), so
287029
+ a persistently broken lease read holds every card in this lane rather than racing another
287030
+ node. That is the intended conservative direction; it is not a silent no-op.
287031
+
287032
+ `batch` carries the sweep-wide batched reads (review finding #10) so this runs at O(1) queries per
287033
+ poll instead of O(cards). It is optional: single-card callers (the column-entry handoff, the
287034
+ unpause re-enqueue, the pre-dispatch re-check) pass nothing and fall back to per-task reads.
287035
+ */
287036
+ async classifyMergeSweepCandidate(task, irCache, batch, opts = {}) {
287037
+ const store = this.runtime.getTaskStore();
287038
+ let irTrust = "unresolved-selection";
287039
+ let ir2 = null;
287040
+ try {
287041
+ const resolved = await resolveWorkflowIrForTaskWithProvenance(store, task.id, irCache);
287042
+ ir2 = resolved.ir;
287043
+ irTrust = resolved.source === "selection" ? "cards-own" : resolved.selectionAbsent === true ? "effective-default" : "unresolved-selection";
287044
+ } catch {
287045
+ ir2 = null;
287046
+ irTrust = "unresolved-selection";
287047
+ }
287048
+ let hasLiveSession = false;
287049
+ try {
287050
+ hasLiveSession = executingTaskLock.has(task.id) || activeSessionRegistry.pathsForTask(task.id).length > 0 || !opts.ignoreOwnMergePipeline && (this.activeMergeTaskId === task.id || await this.isMergePending(task.id));
287051
+ } catch {
287052
+ hasLiveSession = false;
287053
+ }
287054
+ let continuationPositions = [];
287055
+ let continuationsReadable = true;
287056
+ try {
287057
+ const readContinuations = store.listWorkflowWorkItemsForTask;
287058
+ const items = batch?.continuations ? batch.continuations.get(task.id) ?? [] : typeof readContinuations === "function" ? await readContinuations.call(store, task.id, { kinds: ["task"] }) : [];
287059
+ continuationPositions = items.filter((item) => ACTIVE_WORKFLOW_WORK_ITEM_STATES.includes(item.state)).map((item) => ir2 ? classifyWorkflowNodeMergeRegion(ir2, item.nodeId) : "unknown");
287060
+ } catch {
287061
+ continuationsReadable = false;
287062
+ continuationPositions = [];
287063
+ }
287064
+ if (batch && !batch.continuationsReadable) continuationsReadable = false;
287065
+ let mergeRequestActive = false;
287066
+ if (!isActiveMergeStatus(task.status)) {
287067
+ try {
287068
+ mergeRequestActive = batch?.mergeRequests ? isActiveMergeRequestState(batch.mergeRequests.get(task.id)?.state) : isActiveMergeRequestState((await store.getMergeRequestRecordAsync?.(task.id))?.state);
287069
+ } catch {
287070
+ mergeRequestActive = false;
287071
+ }
287072
+ }
287073
+ let gatesSatisfied = true;
287074
+ try {
287075
+ const reviewColumns = /* @__PURE__ */ new Set([task.column]);
287076
+ gatesSatisfied = !getTaskMergeBlocker({ ...task, steps: task.steps ?? [] }, {
287077
+ reviewColumns,
287078
+ requiredPreMergeStepIds: ir2 ? resolveRequiredPreMergeStepIds(ir2, task.enabledWorkflowSteps) : void 0
287079
+ });
287080
+ } catch {
287081
+ gatesSatisfied = false;
287082
+ }
287083
+ const updatedAtMs = Date.parse(task.updatedAt ?? "");
287084
+ const quiescentMs = Number.isFinite(updatedAtMs) ? Math.max(0, Date.now() - updatedAtMs) : Number.POSITIVE_INFINITY;
287085
+ return classifyMergeSweepAdmission({
287086
+ irTrust,
287087
+ continuationPositions,
287088
+ continuationsReadable,
287089
+ mergeConfirmed: task.mergeDetails?.mergeConfirmed === true,
287090
+ hasLiveSession,
287091
+ interruptedMergeAttempt: isActiveMergeStatus(task.status) || mergeRequestActive,
287092
+ quiescentMs,
287093
+ gatesSatisfied
287094
+ });
287095
+ }
287096
+ /*
287097
+ FNXC:MergeAuthority 2026-08-23-20:05 (review finding #10):
287098
+ One batched read per sweep instead of one per card. The sweep asks the same two questions of every
287099
+ review-lane card on every 15s poll; per-task reads made that 2N queries. A failed batch read is
287100
+ reported as unreadable rather than as empty, so it refuses initiation exactly like the per-task
287101
+ failure path does.
287102
+ */
287103
+ /*
287104
+ FNXC:MergeAuthority 2026-08-23-20:05 (review finding #2):
287105
+ Position-only dispatch guard. Answers ONE question — "do this card's active continuations still
287106
+ place it inside its workflow's merge region?" — and fails OPEN on every uncertainty (unreadable
287107
+ continuations, an unresolvable workflow, no continuation at all), because the merge door behind it
287108
+ is the authority on everything else. A card with NO active continuation is authorized here by
287109
+ design: that is the shape of the interrupted-merge and quiescent-stall recoveries.
287110
+ */
287111
+ async isDispatchStillGraphAuthorized(task) {
287112
+ const store = this.runtime.getTaskStore();
287113
+ try {
287114
+ const readContinuations = store.listWorkflowWorkItemsForTask;
287115
+ if (typeof readContinuations !== "function") return true;
287116
+ const items = await readContinuations.call(this.runtime.getTaskStore(), task.id, { kinds: ["task"] });
287117
+ const active2 = items.filter((item) => ACTIVE_WORKFLOW_WORK_ITEM_STATES.includes(item.state));
287118
+ if (active2.length === 0) return true;
287119
+ const resolved = await resolveWorkflowIrForTaskWithProvenance(this.runtime.getTaskStore(), task.id);
287120
+ if (resolved.source !== "selection" && resolved.selectionAbsent !== true) return true;
287121
+ return active2.some((item) => classifyWorkflowNodeMergeRegion(resolved.ir, item.nodeId) !== "outside-merge-region");
287122
+ } catch {
287123
+ return true;
287124
+ }
287125
+ }
287126
+ async loadMergeSweepBatch(candidates) {
287127
+ const store = this.runtime.getTaskStore();
287128
+ const ids = candidates.map((t) => t.id);
287129
+ const batch = { continuationsReadable: true };
287130
+ if (ids.length === 0) return batch;
287131
+ try {
287132
+ batch.continuations = typeof store.listWorkflowWorkItemsForTasks === "function" ? await store.listWorkflowWorkItemsForTasks.call(this.runtime.getTaskStore(), ids, { kinds: ["task"] }) : void 0;
287133
+ } catch {
287134
+ batch.continuations = void 0;
287135
+ batch.continuationsReadable = false;
287136
+ }
287137
+ try {
287138
+ batch.mergeRequests = typeof store.getMergeRequestRecordsAsync === "function" ? await store.getMergeRequestRecordsAsync.call(this.runtime.getTaskStore(), ids) : void 0;
287139
+ } catch {
287140
+ batch.mergeRequests = void 0;
287141
+ }
287142
+ return batch;
287143
+ }
286712
287144
  reconcileStaleMergeActive() {
286713
287145
  let cleared = 0;
286714
287146
  for (const taskId of [...this.mergeActive]) {
@@ -287027,6 +287459,11 @@ ${detail}`);
287027
287459
  }
287028
287460
  continue;
287029
287461
  }
287462
+ if (!task.mergeDetails?.mergeConfirmed && !await this.isDispatchStillGraphAuthorized(task)) {
287463
+ runtimeLog.log(`Auto-merge dispatch skipped for ${taskId}: graph moved the card out of its merge region while it was queued`);
287464
+ this.clearMergeActive(taskId);
287465
+ continue;
287466
+ }
287030
287467
  if (task.mergeDetails?.mergeConfirmed) {
287031
287468
  const branchGroupForFastPathCandidate = isSharedBranchGroupMemberIntegration(task) ? store.getBranchGroup?.(task.branchContext?.groupId) : null;
287032
287469
  const fastPathDefaultBranch = await resolveIntegrationBranch(this.config.workingDirectory, settings2);
@@ -287146,7 +287583,7 @@ ${detail}`);
287146
287583
  continue;
287147
287584
  }
287148
287585
  }
287149
- const blockerReason = getTaskHardMergeBlocker({
287586
+ const blockerReason = getMergeConfirmedFinalizationBlocker({
287150
287587
  ...task,
287151
287588
  /*
287152
287589
  FNXC:WorkflowResolvedColumns 2026-07-30-18:05 (this parked ALREADY-MERGED work as failed):
@@ -287160,7 +287597,13 @@ ${detail}`);
287160
287597
  and for the reason recorded there: `"in-review"` is the review-eligible SENTINEL for this
287161
287598
  helper, not a lifecycle column, so a merge-confirmed card evaluates the same blocker set
287162
287599
  on a custom workflow as on the builtin one. The column identity of an already-landed card
287163
- is not what this check is for — paused / error / incomplete steps still apply.
287600
+ is not what this check is for.
287601
+
287602
+ FNXC:MergeConfirmedFinalization 2026-08-23-21:40 (FN-9193): incomplete steps NO LONGER
287603
+ apply here. The fast path above already proved this merge landed, and holding a landed
287604
+ card out of `done` for unfinished steps is what left FN-9193 permanently unfinalizable —
287605
+ a restart then replanned fresh pending steps and re-created the very block it was meant
287606
+ to clear. Paused/error/pre-merge-step blockers still apply.
287164
287607
  */
287165
287608
  column: REVIEW_ELIGIBLE_SENTINEL_COLUMN,
287166
287609
  // Merge-confirmed tasks have already landed. Treat stale merge
@@ -287170,6 +287613,14 @@ ${detail}`);
287170
287613
  status: clearMergeConfirmedTransientStatus(task.status),
287171
287614
  error: void 0
287172
287615
  });
287616
+ const unfinishedFastPathSteps = getUnfinishedStepTitles(task);
287617
+ if (unfinishedFastPathSteps.length > 0) {
287618
+ await store.logEntry(
287619
+ taskId,
287620
+ `Finalizing proven merge with ${unfinishedFastPathSteps.length} unfinished step(s) \u2014 the branch already landed, so these did not run: ${unfinishedFastPathSteps.slice(0, 8).join("; ")}`,
287621
+ "MergeConfirmedFinalizeUnfinishedSteps"
287622
+ ).catch(() => void 0);
287623
+ }
287173
287624
  if (blockerReason) {
287174
287625
  await store.updateTask(taskId, {
287175
287626
  status: "failed",
@@ -288321,6 +288772,11 @@ ${detail}`);
288321
288772
  runtimeLog.warn(`Auto-merge handoff (${task.id}): clearing stale mergeActive before enqueue`);
288322
288773
  this.clearMergeActive(task.id);
288323
288774
  }
288775
+ const handoffAdmission = await this.classifyMergeSweepCandidate(latestTask, /* @__PURE__ */ new Map(), void 0, { ignoreOwnMergePipeline: true });
288776
+ if (!handoffAdmission.admit) {
288777
+ runtimeLog.log(`Auto-merge handoff (${task.id}) skipped: ${handoffAdmission.reason} (graph owns the merge decision)`);
288778
+ return;
288779
+ }
288324
288780
  this.internalEnqueueMerge(task.id);
288325
288781
  } catch (err) {
288326
288782
  runtimeLog.warn(
@@ -288408,6 +288864,11 @@ ${detail}`);
288408
288864
  if (this.options.getTaskMergeBlocker?.(task)) {
288409
288865
  return;
288410
288866
  }
288867
+ const unpauseAdmission = await this.classifyMergeSweepCandidate(task, /* @__PURE__ */ new Map(), void 0, { ignoreOwnMergePipeline: true });
288868
+ if (!unpauseAdmission.admit) {
288869
+ runtimeLog.log(`In-review unpause: ${task.id} not re-enqueued: ${unpauseAdmission.reason} (graph owns the merge decision)`);
288870
+ return;
288871
+ }
288411
288872
  runtimeLog.log(`Unpaused in-review task re-enqueued for auto-merge: ${task.id}`);
288412
288873
  this.internalEnqueueMerge(task.id);
288413
288874
  } catch (err) {
@@ -288418,6 +288879,7 @@ ${detail}`);
288418
288879
  };
288419
288880
  this.taskDeletedHandler = (task) => {
288420
288881
  this.pausedReviewTaskIds.delete(task.id);
288882
+ this.mergeSweepHoldReasons.delete(task.id);
288421
288883
  const queueLengthBefore = this.mergeQueue.length;
288422
288884
  this.mergeQueue = this.mergeQueue.filter((queuedTaskId) => queuedTaskId !== task.id);
288423
288885
  const removedFromQueue = this.mergeQueue.length !== queueLengthBefore;