@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.
- package/dist/bin.js +548 -86
- package/dist/child-process-worker.js +546 -86
- package/dist/client/.vite/manifest.json +249 -249
- package/dist/client/assets/{AgentDetailView-C1eh_vXv.js → AgentDetailView-DsMRc-aH.js} +1 -1
- package/dist/client/assets/{AgentPermissionPolicyEditor-CeKsO1LR.js → AgentPermissionPolicyEditor-BXJ52YpO.js} +1 -1
- package/dist/client/assets/{AgentsView-BNe2ABOc.js → AgentsView-DO-25nlB.js} +3 -3
- package/dist/client/assets/{CommandCenter-BuREM-G1.js → CommandCenter-BMXMg_I2.js} +1 -1
- package/dist/client/assets/{DevServerView-D0i1b-QO.js → DevServerView-DAeL5TYz.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-VOx91hQt.js → DirectoryPicker-Cw-XRjAa.js} +1 -1
- package/dist/client/assets/{DocumentsView-CO5W_WVb.js → DocumentsView-CusClN9u.js} +1 -1
- package/dist/client/assets/{EvalsView-BqSINSrZ.js → EvalsView-gNdeUDww.js} +1 -1
- package/dist/client/assets/{ExperimentalAgentOnboardingModal-DEcFgONi.js → ExperimentalAgentOnboardingModal-Bmzbqzhs.js} +1 -1
- package/dist/client/assets/{GoalsView-DZ57wXmJ.js → GoalsView-J8JMsoLn.js} +1 -1
- package/dist/client/assets/{InsightsView-DSWfXG7Q.js → InsightsView-BVbv56K0.js} +1 -1
- package/dist/client/assets/{MemoryView-BEX10uhl.js → MemoryView-DNvJFoDu.js} +1 -1
- package/dist/client/assets/{PiExtensionsManager-DcAmXXoR.js → PiExtensionsManager-BPMexS34.js} +1 -1
- package/dist/client/assets/{PluginManager-BU0omQUv.js → PluginManager-DGEv-ptu.js} +1 -1
- package/dist/client/assets/{PullRequestView-B-JL_8Kk.js → PullRequestView-CF7fx-Uo.js} +1 -1
- package/dist/client/assets/ReportModal-B8S5BFgn.js +21 -0
- package/dist/client/assets/ReportModal-Dx3f2aj7.css +1 -0
- package/dist/client/assets/{ResearchView-CnwTXfQO.js → ResearchView-CBeXsevy.js} +1 -1
- package/dist/client/assets/{SecretsView-BDvzsLFz.js → SecretsView-Dmi9wZkv.js} +1 -1
- package/dist/client/assets/{SettingsModal-801eFQ9u.js → SettingsModal-D2kXP7Um.js} +3 -3
- package/dist/client/assets/{SettingsModal-BZHeQ3ED.js → SettingsModal-DSW3xnGk.js} +1 -1
- package/dist/client/assets/{SettingsTextareaRow-B0rTuelr.js → SettingsTextareaRow-CLab4Vcd.js} +1 -1
- package/dist/client/assets/{SetupWizardModal-CoExPwIn.js → SetupWizardModal-Ct6juX9x.js} +2 -2
- package/dist/client/assets/{SkillsView-WVP25LPj.js → SkillsView-Dn0bwT6i.js} +1 -1
- package/dist/client/assets/{WorkflowNodeEditor-pCMyKcnS.js → WorkflowNodeEditor-cvReMlG-.js} +1 -1
- package/dist/client/assets/{agent-import-generation-M_hKtjgN.js → agent-import-generation-C8_ewVzC.js} +1 -1
- package/dist/client/assets/{agentsOrgChartLayout-DjvIMhFD.js → agentsOrgChartLayout-p1i6XROQ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-CkzEm8i9.js → architectureDiagram-3BPJPVTR-CzCk1QdH.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Dc-dUMjY.js → blockDiagram-GPEHLZMM-74Rj-5ov.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-DnYjsrR0.js → c4Diagram-AAUBKEIU-B570qYH-.js} +1 -1
- package/dist/client/assets/channel-DWAK5y-J.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-0E1GEFEr.js → chunk-2J33WTMH-DejJkxt6.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-Dj5oZd51.js → chunk-4BX2VUAB-DlgxkBuE.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-DfNT0s8E.js → chunk-55IACEB6-XCfE8PwC.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-BnFjhiIQ.js → chunk-727SXJPM-D-nDw0Bi.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DR84wSqR.js → chunk-AQP2D5EJ-C4KySao0.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-IjByNOrR.js → chunk-FMBD7UC4-CXKdLygA.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-By0q_jZm.js → chunk-ND2GUHAM-bNYWWGVJ.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-BELcl3N5.js → chunk-QZHKN3VN-cfBQJ-MX.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-WTkXRkfm.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-WTkXRkfm.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-D4cCw9-_.js → cose-bilkent-S5V4N54A-CiZrg3mQ.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-JJp5dVeY.js → dagre-BM42HDAG-Bckph2U4.js} +1 -1
- package/dist/client/assets/{dashboard-view-Bl9CPhxE.js → dashboard-view-CN_fkDM1.js} +1 -1
- package/dist/client/assets/{dashboard-view-C5Nt8cvY.js → dashboard-view-CVYHQZwx.js} +1 -1
- package/dist/client/assets/{dashboard-view-6zPJm9zC.js → dashboard-view-DDRKwfFs.js} +1 -1
- package/dist/client/assets/{dashboard-view-MAC_LHDX.js → dashboard-view-IuPjDVGa.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-D4L7j_t_.js → diagram-2AECGRRQ-Bnaurex7.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-VAGFaXqG.js → diagram-5GNKFQAL-BKz1omVN.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-hqXSggfW.js → diagram-KO2AKTUF-C51lEZwo.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-B3eAIuy8.js → diagram-LMA3HP47-BYm5gql7.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BGufp0RK.js → diagram-OG6HWLK6-B9CFA4ha.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DplXdx8w.js → erDiagram-TEJ5UH35-BLyCSssl.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-C1Ms2bko.js → flowDiagram-I6XJVG4X-yDVmpkro.js} +1 -1
- package/dist/client/assets/{folder-open-nMraSDXu.js → folder-open-Cf2NA9nV.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-DVgxMtXb.js → ganttDiagram-6RSMTGT7-mC9YPFcx.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DH2_n0J8.js → gitGraphDiagram-PVQCEYII-DS0bEccv.js} +1 -1
- package/dist/client/assets/index-BjM7LxzY.js +2805 -0
- package/dist/client/assets/index-DcKIzuMk.css +1 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-DmZdWVcU.js → infoDiagram-5YYISTIA-CuuNZITP.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DsEIHSXr.js → ishikawaDiagram-YF4QCWOH-MBSaC94m.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CsfIWmu1.js → journeyDiagram-JHISSGLW-DkaAow9E.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-CQKqeZvc.js → kanban-definition-UN3LZRKU-DSf5OLtq.js} +1 -1
- package/dist/client/assets/{mermaid.core-DkC_dr4-.js → mermaid.core-HmTn21MF.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-RnmE-NUe.js → mindmap-definition-RKZ34NQL-sv0Dl8aI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DPu6FPc4.js → pieDiagram-4H26LBE5-BDK2IQNq.js} +1 -1
- package/dist/client/assets/{puzzle-DwhHOf32.js → puzzle-D00mP67o.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-CYbypO2p.js → quadrantDiagram-W4KKPZXB-CiWAhwf1.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DFk1vK_N.js → requirementDiagram-4Y6WPE33-DwC99D35.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CkUY1Zzu.js → sankeyDiagram-5OEKKPKP-CQn8z721.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CFivaLOO.js → sequenceDiagram-3UESZ5HK-B0CT8-vw.js} +1 -1
- package/dist/client/assets/{standing-instructions-template-BbqKGAC1.js → standing-instructions-template-BIBO_Prr.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-BPeD5PwG.js → stateDiagram-AJRCARHV-BYAB_dFF.js} +1 -1
- package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-DduiCPMb.js → stateDiagram-v2-BHNVJYJU-Cd19uJBk.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-CGUNNVro.js → timeline-definition-PNZ67QCA-DQLh8XkY.js} +1 -1
- package/dist/client/assets/{upload-BS96H1o7.js → upload-D4pkNOz2.js} +1 -1
- package/dist/client/assets/{users--irrWoyU.js → users-COgviS_S.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CybTaJJH.js → vennDiagram-CIIHVFJN-BmwG1Wud.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-DSxaSCGi.js → wardley-L42UT6IY-BiO7jkmj.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-YFb0AxNt.js → wardleyDiagram-YWT4CUSO-Dq0IPg5H.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-Cv6zlMJC.js → xychartDiagram-2RQKCTM6-BlMOlSBc.js} +1 -1
- package/dist/client/index.html +13 -6
- package/dist/client/theme-data.css +85 -0
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/extension.js +548 -86
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/plugin-sdk/index.d.ts +5 -1
- package/dist/plugin-sdk/index.js +43 -19
- package/dist/plugins/fusion-plugin-acp-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-claude-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-claude-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-cli-printing-press/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
- package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
- package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
- package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-grok-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-grok-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-linear-import/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-linear-import/package.json +1 -1
- package/dist/plugins/fusion-plugin-omp-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-omp-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-quality/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-quality/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-reports/package.json +1 -1
- package/dist/plugins/fusion-plugin-roadmap/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
- package/dist/plugins/fusion-plugin-todos/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-todos/package.json +1 -1
- package/dist/plugins/fusion-plugin-whatsapp-chat/bundled.js +43 -19
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
- package/package.json +1 -1
- package/dist/client/assets/ReportModal-DxggF_RH.js +0 -21
- package/dist/client/assets/ReportModal-lzN_xrnM.css +0 -1
- package/dist/client/assets/channel-CxQ40s_U.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C2ARdiRL.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C2ARdiRL.js +0 -1
- package/dist/client/assets/index-BJc-A7eB.css +0 -1
- 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
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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 =
|
|
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
|
-
|
|
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: (
|
|
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
|
|
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(
|
|
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
|
|
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 (!
|
|
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 &&
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
227600
|
-
|
|
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(
|
|
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
|
|
231494
|
-
if (!
|
|
231742
|
+
const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
|
|
231743
|
+
if (!duplicateResolution.marker || duplicateResolution.conflict) {
|
|
231495
231744
|
return false;
|
|
231496
231745
|
}
|
|
231497
|
-
const 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
|
-
|
|
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
|
-
|
|
231643
|
-
|
|
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 =
|
|
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
|
|
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
|
|
287563
|
-
|
|
287564
|
-
|
|
287565
|
-
|
|
287566
|
-
|
|
287567
|
-
|
|
287568
|
-
|
|
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 =
|
|
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
|
|
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;
|