@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
|
@@ -1061,6 +1061,8 @@ var init_execution_and_ui = __esm({
|
|
|
1061
1061
|
"midnight",
|
|
1062
1062
|
// 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.
|
|
1063
1063
|
"velvet",
|
|
1064
|
+
// 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.
|
|
1065
|
+
"iceberg",
|
|
1064
1066
|
// 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.
|
|
1065
1067
|
"factory-dark",
|
|
1066
1068
|
// 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.
|
|
@@ -1539,6 +1541,13 @@ function getTaskHardMergeBlocker(task, options = {}) {
|
|
|
1539
1541
|
requiredPreMergeStepIds: options.requiredPreMergeStepIds
|
|
1540
1542
|
});
|
|
1541
1543
|
}
|
|
1544
|
+
function getMergeConfirmedFinalizationBlocker(task, options = {}) {
|
|
1545
|
+
const hasDurableMergeRecord = task.mergeDetails?.mergeConfirmed === true && typeof task.mergeDetails.commitSha === "string" && task.mergeDetails.commitSha.length > 0;
|
|
1546
|
+
return getTaskHardMergeBlocker(hasDurableMergeRecord ? { ...task, steps: [] } : task, options);
|
|
1547
|
+
}
|
|
1548
|
+
function getUnfinishedStepTitles(task) {
|
|
1549
|
+
return (task.steps ?? []).filter((step) => NON_TERMINAL_STEP_STATUSES.has(step.status)).map((step, index2) => step.name?.trim() || `step ${index2 + 1}`);
|
|
1550
|
+
}
|
|
1542
1551
|
function isTaskReadyForMerge(task, options = {}) {
|
|
1543
1552
|
return getTaskMergeBlocker(task, options) === void 0;
|
|
1544
1553
|
}
|
|
@@ -2525,12 +2534,16 @@ var init_settings_schema = __esm({
|
|
|
2525
2534
|
executorEscalationProvider: void 0,
|
|
2526
2535
|
executorEscalationModelId: void 0,
|
|
2527
2536
|
executorEscalationNodeId: void 0,
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2537
|
+
/*
|
|
2538
|
+
FNXC:ReviewConvergence 2026-08-23-21:05:
|
|
2539
|
+
FN-149's six review-convergence/arbitration keys are declared in BUILTIN_WORKFLOW_SETTINGS, which
|
|
2540
|
+
the U4 hard-move made the SINGLE source of truth for a moved key's default. Re-declaring them here
|
|
2541
|
+
put them back in the legacy object the hard-move emptied, so an undeclared key on a custom workflow
|
|
2542
|
+
could resolve to this literal instead of the declaration default — the exact drift
|
|
2543
|
+
`workflow-settings-fallback-alignment` guards. The declaration defaults are identical (both
|
|
2544
|
+
`*Enabled` default true, the provider/model lanes carry no default), and the reader uses
|
|
2545
|
+
`!== false`, so removing them here preserves behaviour and restores the invariant.
|
|
2546
|
+
*/
|
|
2534
2547
|
/**
|
|
2535
2548
|
* FNXC:Merge 2026-06-26-00:00:
|
|
2536
2549
|
* 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.
|
|
@@ -10691,6 +10704,35 @@ var init_workflow_ir_types = __esm({
|
|
|
10691
10704
|
}
|
|
10692
10705
|
});
|
|
10693
10706
|
|
|
10707
|
+
// ../core/src/workflows/workflow-merge-region.ts
|
|
10708
|
+
function isMergeRegionNode(node) {
|
|
10709
|
+
return MERGE_REGION_ENTRY_NODE_KINDS.has(node.kind) || node.config?.seam === "merge";
|
|
10710
|
+
}
|
|
10711
|
+
function classifyWorkflowNodeMergeRegion(ir2, nodeId) {
|
|
10712
|
+
if (ir2.version !== "v2" || !Array.isArray(ir2.nodes)) return "unknown";
|
|
10713
|
+
const node = ir2.nodes.find((candidate) => candidate.id === nodeId);
|
|
10714
|
+
if (!node) return "unknown";
|
|
10715
|
+
return isMergeRegionNode(node) ? "merge-region" : "outside-merge-region";
|
|
10716
|
+
}
|
|
10717
|
+
var MERGE_REGION_ENTRY_NODE_KINDS;
|
|
10718
|
+
var init_workflow_merge_region = __esm({
|
|
10719
|
+
"../core/src/workflows/workflow-merge-region.ts"() {
|
|
10720
|
+
"use strict";
|
|
10721
|
+
MERGE_REGION_ENTRY_NODE_KINDS = /* @__PURE__ */ new Set([
|
|
10722
|
+
"merge-gate",
|
|
10723
|
+
"merge-attempt",
|
|
10724
|
+
"manual-merge-hold",
|
|
10725
|
+
"retry-backoff",
|
|
10726
|
+
"recovery-router",
|
|
10727
|
+
"branch-group-member-integration",
|
|
10728
|
+
"branch-group-promotion",
|
|
10729
|
+
"pr-create",
|
|
10730
|
+
"pr-respond",
|
|
10731
|
+
"pr-merge"
|
|
10732
|
+
]);
|
|
10733
|
+
}
|
|
10734
|
+
});
|
|
10735
|
+
|
|
10694
10736
|
// ../core/src/workflows/workflow-extension-types.ts
|
|
10695
10737
|
function workflowExtensionRegistryId(pluginId, extensionId) {
|
|
10696
10738
|
return `plugin:${pluginId}:${extensionId}`;
|
|
@@ -12430,6 +12472,7 @@ var init_workflow_ir = __esm({
|
|
|
12430
12472
|
"../core/src/workflows/workflow-ir.ts"() {
|
|
12431
12473
|
"use strict";
|
|
12432
12474
|
init_workflow_ir_types();
|
|
12475
|
+
init_workflow_merge_region();
|
|
12433
12476
|
init_workflow_extension_registry();
|
|
12434
12477
|
init_types();
|
|
12435
12478
|
init_trait_registry();
|
|
@@ -12510,18 +12553,7 @@ var init_workflow_ir = __esm({
|
|
|
12510
12553
|
"done",
|
|
12511
12554
|
"archived"
|
|
12512
12555
|
];
|
|
12513
|
-
INTERPRETER_ENTRY_NODE_KINDS =
|
|
12514
|
-
"merge-gate",
|
|
12515
|
-
"merge-attempt",
|
|
12516
|
-
"manual-merge-hold",
|
|
12517
|
-
"retry-backoff",
|
|
12518
|
-
"recovery-router",
|
|
12519
|
-
"branch-group-member-integration",
|
|
12520
|
-
"branch-group-promotion",
|
|
12521
|
-
"pr-create",
|
|
12522
|
-
"pr-respond",
|
|
12523
|
-
"pr-merge"
|
|
12524
|
-
]);
|
|
12556
|
+
INTERPRETER_ENTRY_NODE_KINDS = MERGE_REGION_ENTRY_NODE_KINDS;
|
|
12525
12557
|
MERGE_CLASS_NODE_KINDS = /* @__PURE__ */ new Set([
|
|
12526
12558
|
"merge-gate",
|
|
12527
12559
|
"merge-attempt",
|
|
@@ -13927,6 +13959,13 @@ Lint, tests, and typecheck are also hard quality gates:
|
|
|
13927
13959
|
- Keep fixing failures caused by your change until lint, impacted tests, build, and typecheck pass.
|
|
13928
13960
|
- If the repository exposes a typecheck command, run it and fix failures caused by your change.
|
|
13929
13961
|
- 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.
|
|
13962
|
+
|
|
13963
|
+
**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:
|
|
13964
|
+
- 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.
|
|
13965
|
+
- 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.
|
|
13966
|
+
- 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.
|
|
13967
|
+
- Added a public store/service method? Update the inventory/drift guards that require every public surface to be classified.
|
|
13968
|
+
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.
|
|
13930
13969
|
- 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.
|
|
13931
13970
|
- Do not repeatedly rerun a broad failing or hanging workspace command without a new hypothesis and a narrower confirming command.
|
|
13932
13971
|
|
|
@@ -14199,6 +14238,7 @@ files with assertions that run via a test runner. Typechecks and builds are NOT
|
|
|
14199
14238
|
tests. Manual verification is NOT a test.
|
|
14200
14239
|
|
|
14201
14240
|
- Each implementation step should include writing tests for the code being changed
|
|
14241
|
+
- **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.
|
|
14202
14242
|
- 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.
|
|
14203
14243
|
- 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.
|
|
14204
14244
|
- 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)
|
|
@@ -24523,7 +24563,16 @@ var init_project = __esm({
|
|
|
24523
24563
|
"chat_rooms",
|
|
24524
24564
|
"chat_room_members",
|
|
24525
24565
|
"chat_room_messages",
|
|
24526
|
-
"chat_token_usage"
|
|
24566
|
+
"chat_token_usage",
|
|
24567
|
+
/*
|
|
24568
|
+
FNXC:WorkspaceCoordination 2026-08-23-20:05:
|
|
24569
|
+
FN-9059 (migration 0060) added these two tables to the schema without registering them here, so
|
|
24570
|
+
every registry consumer under-counted them: the PostgreSQL test harness never TRUNCATEd them
|
|
24571
|
+
between tests (a held `repo:<repo>` lease leaked from one case into the next and made the next
|
|
24572
|
+
land report the previous test's successor as the busy holder), and health compaction skipped them.
|
|
24573
|
+
*/
|
|
24574
|
+
"workspace_coordination_leases",
|
|
24575
|
+
"workspace_land_intents"
|
|
24527
24576
|
];
|
|
24528
24577
|
}
|
|
24529
24578
|
});
|
|
@@ -28653,10 +28702,6 @@ function resolveRequiredPreMergeStepIds(ir2, enabledWorkflowSteps) {
|
|
|
28653
28702
|
resolveWorkflowOptionalSteps(ir2).filter((step) => step.phase === "pre-merge").filter((step) => isWorkflowOptionalGroupEnabled(enabledWorkflowSteps, step.templateId, step.defaultOn)).map((step) => step.templateId)
|
|
28654
28703
|
);
|
|
28655
28704
|
}
|
|
28656
|
-
function findUnrunRequiredPreMergeStepIds(ir2, task) {
|
|
28657
|
-
const results = task.workflowStepResults ?? [];
|
|
28658
|
-
return [...resolveRequiredPreMergeStepIds(ir2, task.enabledWorkflowSteps)].filter((workflowStepId) => !results.some((result) => result.workflowStepId === workflowStepId));
|
|
28659
|
-
}
|
|
28660
28705
|
var init_required_pre_merge_steps = __esm({
|
|
28661
28706
|
"../core/src/merge/required-pre-merge-steps.ts"() {
|
|
28662
28707
|
"use strict";
|
|
@@ -28664,6 +28709,42 @@ var init_required_pre_merge_steps = __esm({
|
|
|
28664
28709
|
}
|
|
28665
28710
|
});
|
|
28666
28711
|
|
|
28712
|
+
// ../core/src/merge/merge-sweep-admission.ts
|
|
28713
|
+
function classifyMergeSweepAdmission(input) {
|
|
28714
|
+
if (input.hasLiveSession) return { admit: false, reason: "live-session" };
|
|
28715
|
+
if (input.mergeConfirmed) return { admit: true, reason: "merge-confirmed-finalization" };
|
|
28716
|
+
if (!input.gatesSatisfied) return { admit: false, reason: "gates-unsatisfied" };
|
|
28717
|
+
if (!input.continuationsReadable) return { admit: false, reason: "continuations-unreadable" };
|
|
28718
|
+
if (input.irTrust === "unresolved-selection") {
|
|
28719
|
+
return quiescentStallDecision(input, "unresolved-workflow-ir");
|
|
28720
|
+
}
|
|
28721
|
+
if (input.continuationPositions.length > 0) {
|
|
28722
|
+
if (input.continuationPositions.includes("merge-region")) {
|
|
28723
|
+
return { admit: true, reason: "at-merge-region-node" };
|
|
28724
|
+
}
|
|
28725
|
+
if (input.continuationPositions.includes("unknown")) {
|
|
28726
|
+
return { admit: true, reason: "drifted-continuation-node" };
|
|
28727
|
+
}
|
|
28728
|
+
return { admit: false, reason: "not-at-merge-region-node" };
|
|
28729
|
+
}
|
|
28730
|
+
if (input.interruptedMergeAttempt) {
|
|
28731
|
+
return { admit: true, reason: "interrupted-merge-attempt" };
|
|
28732
|
+
}
|
|
28733
|
+
return quiescentStallDecision(input, "quiescent-stall-recovery");
|
|
28734
|
+
}
|
|
28735
|
+
function quiescentStallDecision(input, reason) {
|
|
28736
|
+
const floorMs = input.quiescenceFloorMs ?? DEFAULT_MERGE_SWEEP_QUIESCENCE_MS;
|
|
28737
|
+
if (!(input.quiescentMs >= floorMs)) return { admit: false, reason: "too-recent" };
|
|
28738
|
+
return { admit: true, reason };
|
|
28739
|
+
}
|
|
28740
|
+
var DEFAULT_MERGE_SWEEP_QUIESCENCE_MS;
|
|
28741
|
+
var init_merge_sweep_admission = __esm({
|
|
28742
|
+
"../core/src/merge/merge-sweep-admission.ts"() {
|
|
28743
|
+
"use strict";
|
|
28744
|
+
DEFAULT_MERGE_SWEEP_QUIESCENCE_MS = 2 * 6e4;
|
|
28745
|
+
}
|
|
28746
|
+
});
|
|
28747
|
+
|
|
28667
28748
|
// ../core/src/config/moved-settings.ts
|
|
28668
28749
|
function isMovedSettingsKey(key) {
|
|
28669
28750
|
return MOVED_SETTINGS_KEY_SET.has(key);
|
|
@@ -71674,6 +71755,21 @@ async function listWorkflowWorkItemsForTaskImpl(store, taskId, opts = {}) {
|
|
|
71674
71755
|
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;
|
|
71675
71756
|
return rows.map((row) => store.rowToWorkflowWorkItem(row));
|
|
71676
71757
|
}
|
|
71758
|
+
async function listWorkflowWorkItemsForTasksImpl(store, taskIds, opts = {}) {
|
|
71759
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
71760
|
+
for (const taskId of taskIds) byTask.set(taskId, []);
|
|
71761
|
+
if (taskIds.length === 0) return byTask;
|
|
71762
|
+
const layer = store.asyncLayer;
|
|
71763
|
+
const scope = projectScopeFor(project_exports.workflowWorkItems.projectId, layer.projectId);
|
|
71764
|
+
const conditions = [scope, inArray(project_exports.workflowWorkItems.taskId, [...taskIds])];
|
|
71765
|
+
if (opts.kinds?.length) conditions.push(inArray(project_exports.workflowWorkItems.kind, opts.kinds));
|
|
71766
|
+
const rows = await layer.db.select().from(project_exports.workflowWorkItems).where(and(...conditions));
|
|
71767
|
+
for (const row of rows) {
|
|
71768
|
+
const item = store.rowToWorkflowWorkItem(row);
|
|
71769
|
+
byTask.get(item.taskId)?.push(item);
|
|
71770
|
+
}
|
|
71771
|
+
return byTask;
|
|
71772
|
+
}
|
|
71677
71773
|
async function listDueWorkflowWorkItemsImpl(store, filter = {}) {
|
|
71678
71774
|
const layer = store.asyncLayer;
|
|
71679
71775
|
return listDueWorkflowWorkItems(layer.db, filter);
|
|
@@ -90992,6 +91088,17 @@ async function getMergeRequestRecordAsyncImpl(store, taskId) {
|
|
|
90992
91088
|
const row = rows[0];
|
|
90993
91089
|
return row ? store.rowToMergeRequestRecord(row) : null;
|
|
90994
91090
|
}
|
|
91091
|
+
async function getMergeRequestRecordsAsyncImpl(store, taskIds) {
|
|
91092
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
91093
|
+
if (taskIds.length === 0) return byTask;
|
|
91094
|
+
const layer = store.asyncLayer;
|
|
91095
|
+
const rows = await layer.db.select().from(project_exports.mergeRequests).where(inArray(project_exports.mergeRequests.taskId, [...taskIds]));
|
|
91096
|
+
for (const row of rows) {
|
|
91097
|
+
const record2 = store.rowToMergeRequestRecord(row);
|
|
91098
|
+
byTask.set(record2.taskId, record2);
|
|
91099
|
+
}
|
|
91100
|
+
return byTask;
|
|
91101
|
+
}
|
|
90995
91102
|
function getWorkflowWorkItemByIdentityImpl(store, runId, taskId, nodeId, kind) {
|
|
90996
91103
|
const row = store.db.prepare("SELECT * FROM workflow_work_items WHERE runId = ? AND taskId = ? AND nodeId = ? AND kind = ?").get(runId, taskId, nodeId, kind);
|
|
90997
91104
|
return row ? store.rowToWorkflowWorkItem(row) : null;
|
|
@@ -106951,6 +107058,10 @@ var init_store = __esm({
|
|
|
106951
107058
|
async getMergeRequestRecordAsync(taskId) {
|
|
106952
107059
|
return getMergeRequestRecordAsyncImpl(this, taskId);
|
|
106953
107060
|
}
|
|
107061
|
+
/** Batched `getMergeRequestRecordAsync` — one query for many tasks (FNXC:MergeAuthority 2026-08-23-20:05). */
|
|
107062
|
+
async getMergeRequestRecordsAsync(taskIds) {
|
|
107063
|
+
return getMergeRequestRecordsAsyncImpl(this, taskIds);
|
|
107064
|
+
}
|
|
106954
107065
|
async projectMergeRequestToWorkflowWorkItem(taskId, opts = {}) {
|
|
106955
107066
|
return projectMergeRequestToWorkflowWorkItemImpl(this, taskId, opts);
|
|
106956
107067
|
}
|
|
@@ -106993,6 +107104,10 @@ var init_store = __esm({
|
|
|
106993
107104
|
async listWorkflowWorkItemsForTask(taskId, opts = {}) {
|
|
106994
107105
|
return listWorkflowWorkItemsForTaskImpl(this, taskId, opts);
|
|
106995
107106
|
}
|
|
107107
|
+
/** Batched `listWorkflowWorkItemsForTask` — one query for many tasks (FNXC:MergeAuthority 2026-08-23-20:05). */
|
|
107108
|
+
async listWorkflowWorkItemsForTasks(taskIds, opts = {}) {
|
|
107109
|
+
return listWorkflowWorkItemsForTasksImpl(this, taskIds, opts);
|
|
107110
|
+
}
|
|
106996
107111
|
/**
|
|
106997
107112
|
* FNXC:RuntimeWorkflowAsync 2026-06-24-17:12:
|
|
106998
107113
|
*/
|
|
@@ -136705,6 +136820,7 @@ __export(src_exports, {
|
|
|
136705
136820
|
DEFAULT_MAX_WORKTREES: () => DEFAULT_MAX_WORKTREES,
|
|
136706
136821
|
DEFAULT_MEMORY_BACKEND: () => DEFAULT_MEMORY_BACKEND,
|
|
136707
136822
|
DEFAULT_MEMORY_DREAMS_SCHEDULE: () => DEFAULT_MEMORY_DREAMS_SCHEDULE,
|
|
136823
|
+
DEFAULT_MERGE_SWEEP_QUIESCENCE_MS: () => DEFAULT_MERGE_SWEEP_QUIESCENCE_MS,
|
|
136708
136824
|
DEFAULT_MIN_INTERVAL_MS: () => DEFAULT_MIN_INTERVAL_MS,
|
|
136709
136825
|
DEFAULT_MISSION_ASSERTION_TYPE: () => DEFAULT_MISSION_ASSERTION_TYPE,
|
|
136710
136826
|
DEFAULT_MOBILE_NAV_PRIMARY_ITEMS: () => DEFAULT_MOBILE_NAV_PRIMARY_ITEMS,
|
|
@@ -136871,6 +136987,7 @@ __export(src_exports, {
|
|
|
136871
136987
|
MERGE_AUDIT_AUTO_RECOVERY_MODES: () => MERGE_AUDIT_AUTO_RECOVERY_MODES,
|
|
136872
136988
|
MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT: () => MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT,
|
|
136873
136989
|
MERGE_CONFIRMED_TRANSIENT_STATUSES: () => MERGE_CONFIRMED_TRANSIENT_STATUSES,
|
|
136990
|
+
MERGE_REGION_ENTRY_NODE_KINDS: () => MERGE_REGION_ENTRY_NODE_KINDS,
|
|
136874
136991
|
MESH_INTENT_STATES: () => MESH_INTENT_STATES,
|
|
136875
136992
|
MESH_RECONCILIATION_OUTCOMES: () => MESH_RECONCILIATION_OUTCOMES,
|
|
136876
136993
|
MESH_WRITE_CLASSES: () => MESH_WRITE_CLASSES,
|
|
@@ -137239,6 +137356,7 @@ __export(src_exports, {
|
|
|
137239
137356
|
classifyFileScopeFromPrompt: () => classifyFileScopeFromPrompt,
|
|
137240
137357
|
classifyGhError: () => classifyGhError,
|
|
137241
137358
|
classifyInsightRunError: () => classifyInsightRunError,
|
|
137359
|
+
classifyMergeSweepAdmission: () => classifyMergeSweepAdmission,
|
|
137242
137360
|
classifyPlannerActionSideEffect: () => classifyPlannerActionSideEffect,
|
|
137243
137361
|
classifyProviderError: () => classifyProviderError,
|
|
137244
137362
|
classifyReadStaleness: () => classifyReadStaleness,
|
|
@@ -137249,6 +137367,7 @@ __export(src_exports, {
|
|
|
137249
137367
|
classifyTaskCommitAssociationConfidence: () => classifyTaskCommitAssociationConfidence,
|
|
137250
137368
|
classifyTerminalFailureAutoRecovery: () => classifyTerminalFailureAutoRecovery,
|
|
137251
137369
|
classifyWorkflowAgentNode: () => classifyWorkflowAgentNode,
|
|
137370
|
+
classifyWorkflowNodeMergeRegion: () => classifyWorkflowNodeMergeRegion,
|
|
137252
137371
|
cleanupOldAiSessions: () => cleanupOldAiSessions,
|
|
137253
137372
|
cleanupStaleAiSessions: () => cleanupStaleAiSessions,
|
|
137254
137373
|
clearActiveEmbeddedRuntimeUrl: () => clearActiveEmbeddedRuntimeUrl,
|
|
@@ -137448,7 +137567,6 @@ __export(src_exports, {
|
|
|
137448
137567
|
findOrphanedSettingValues: () => findOrphanedSettingValues,
|
|
137449
137568
|
findPendingPreMergeStep: () => findPendingPreMergeStep,
|
|
137450
137569
|
findSameAgentDuplicates: () => findSameAgentDuplicates,
|
|
137451
|
-
findUnrunRequiredPreMergeStepIds: () => findUnrunRequiredPreMergeStepIds,
|
|
137452
137570
|
findVitestProcessIds: () => findVitestProcessIds,
|
|
137453
137571
|
findWorkflowColumn: () => findWorkflowColumn,
|
|
137454
137572
|
findWorkflowEventShapeViolations: () => findWorkflowEventShapeViolations,
|
|
@@ -137520,6 +137638,7 @@ __export(src_exports, {
|
|
|
137520
137638
|
getMcpDiscoverySources: () => getMcpDiscoverySources,
|
|
137521
137639
|
getMemoryBackend: () => getMemoryBackend,
|
|
137522
137640
|
getMemoryBackendCapabilities: () => getMemoryBackendCapabilities,
|
|
137641
|
+
getMergeConfirmedFinalizationBlocker: () => getMergeConfirmedFinalizationBlocker,
|
|
137523
137642
|
getOpenIncidentByGroupingKeyAsync: () => getOpenIncidentByGroupingKeyAsync,
|
|
137524
137643
|
getOverriddenKeys: () => getOverriddenKeys,
|
|
137525
137644
|
getPiExtensionDiscoveryDirs: () => getPiExtensionDiscoveryDirs,
|
|
@@ -137558,6 +137677,7 @@ __export(src_exports, {
|
|
|
137558
137677
|
getTotalAgentActiveMs: () => getTotalAgentActiveMs,
|
|
137559
137678
|
getTrait: () => getTrait,
|
|
137560
137679
|
getTraitRegistry: () => getTraitRegistry,
|
|
137680
|
+
getUnfinishedStepTitles: () => getUnfinishedStepTitles,
|
|
137561
137681
|
getValidTransitions: () => getValidTransitions,
|
|
137562
137682
|
getWorkflowEventBus: () => getWorkflowEventBus,
|
|
137563
137683
|
getWorkflowExtensionRegistry: () => getWorkflowExtensionRegistry,
|
|
@@ -137641,6 +137761,7 @@ __export(src_exports, {
|
|
|
137641
137761
|
isManualTrigger: () => isManualTrigger,
|
|
137642
137762
|
isMcpSecretRef: () => isMcpSecretRef,
|
|
137643
137763
|
isMechanicalRoutableContext: () => isMechanicalRoutableContext,
|
|
137764
|
+
isMergeRegionNode: () => isMergeRegionNode,
|
|
137644
137765
|
isMergeRequestContractShadowEnabled: () => isMergeRequestContractShadowEnabled,
|
|
137645
137766
|
isMeshIntentState: () => isMeshIntentState,
|
|
137646
137767
|
isMeshWriteClass: () => isMeshWriteClass,
|
|
@@ -138344,6 +138465,8 @@ var init_src2 = __esm({
|
|
|
138344
138465
|
init_workflow_merge_proof();
|
|
138345
138466
|
init_workflow_optional_steps();
|
|
138346
138467
|
init_required_pre_merge_steps();
|
|
138468
|
+
init_merge_sweep_admission();
|
|
138469
|
+
init_workflow_merge_region();
|
|
138347
138470
|
init_workflow_prompt_overrides();
|
|
138348
138471
|
init_builtin_stepwise_coding_workflow_ir();
|
|
138349
138472
|
init_builtin_stepwise_final_review_coding_workflow_ir();
|
|
@@ -143609,39 +143732,42 @@ var init_worktree_names = __esm({
|
|
|
143609
143732
|
});
|
|
143610
143733
|
|
|
143611
143734
|
// ../engine/src/util/emit-bounded-run-audit.ts
|
|
143612
|
-
|
|
143735
|
+
function emitBoundedRunAudit2(host, event, options = {}) {
|
|
143736
|
+
return emitBoundedRunAuditWithOutcome2(host, event, options).then(() => void 0);
|
|
143737
|
+
}
|
|
143738
|
+
async function emitBoundedRunAuditWithOutcome2(host, event, options = {}) {
|
|
143613
143739
|
let defaultLog2;
|
|
143614
143740
|
try {
|
|
143615
143741
|
defaultLog2 = runAuditLog;
|
|
143616
143742
|
} catch {
|
|
143617
143743
|
defaultLog2 = void 0;
|
|
143618
143744
|
}
|
|
143619
|
-
const log42 = options.log ?? defaultLog2 ?? { warn: (
|
|
143745
|
+
const log42 = options.log ?? defaultLog2 ?? { warn: () => void 0 };
|
|
143620
143746
|
const sink = host?.recordRunAuditEvent;
|
|
143621
|
-
if (typeof sink !== "function") return;
|
|
143747
|
+
if (typeof sink !== "function") return { outcome: "absent" };
|
|
143622
143748
|
let sinkPromise;
|
|
143623
143749
|
try {
|
|
143624
143750
|
sinkPromise = Promise.resolve(sink.call(host, event));
|
|
143625
|
-
} catch {
|
|
143751
|
+
} catch (error2) {
|
|
143626
143752
|
log42.warn(`[run-audit] failed to record ${event.mutationType}`);
|
|
143627
|
-
return;
|
|
143753
|
+
return { outcome: "failed", error: error2 };
|
|
143628
143754
|
}
|
|
143629
143755
|
void sinkPromise.catch(() => void 0);
|
|
143630
|
-
await new Promise((resolve51) => {
|
|
143756
|
+
return await new Promise((resolve51) => {
|
|
143631
143757
|
const timer2 = setTimeout(() => {
|
|
143632
143758
|
log42.warn(`[run-audit] timed out recording ${event.mutationType}`);
|
|
143633
|
-
resolve51();
|
|
143759
|
+
resolve51({ outcome: "timed-out" });
|
|
143634
143760
|
}, options.timeoutMs ?? RUN_AUDIT_EMIT_TIMEOUT_MS);
|
|
143635
143761
|
timer2.unref?.();
|
|
143636
143762
|
void sinkPromise.then(
|
|
143637
143763
|
() => {
|
|
143638
143764
|
clearTimeout(timer2);
|
|
143639
|
-
resolve51();
|
|
143765
|
+
resolve51({ outcome: "recorded" });
|
|
143640
143766
|
},
|
|
143641
|
-
() => {
|
|
143767
|
+
(error2) => {
|
|
143642
143768
|
clearTimeout(timer2);
|
|
143643
143769
|
log42.warn(`[run-audit] failed to record ${event.mutationType}`);
|
|
143644
|
-
resolve51();
|
|
143770
|
+
resolve51({ outcome: "failed", error: error2 });
|
|
143645
143771
|
}
|
|
143646
143772
|
);
|
|
143647
143773
|
});
|
|
@@ -143690,6 +143816,7 @@ function createRunAuditor(store, context) {
|
|
|
143690
143816
|
},
|
|
143691
143817
|
database: async () => {
|
|
143692
143818
|
},
|
|
143819
|
+
databaseWithOutcome: async () => ({ outcome: "absent" }),
|
|
143693
143820
|
filesystem: async () => {
|
|
143694
143821
|
},
|
|
143695
143822
|
sandbox: async () => {
|
|
@@ -143703,6 +143830,7 @@ function createRunAuditor(store, context) {
|
|
|
143703
143830
|
},
|
|
143704
143831
|
database: async () => {
|
|
143705
143832
|
},
|
|
143833
|
+
databaseWithOutcome: async () => ({ outcome: "absent" }),
|
|
143706
143834
|
filesystem: async () => {
|
|
143707
143835
|
},
|
|
143708
143836
|
sandbox: async () => {
|
|
@@ -143745,6 +143873,28 @@ function createRunAuditor(store, context) {
|
|
|
143745
143873
|
};
|
|
143746
143874
|
await emitBoundedRunAudit2(store, eventInput);
|
|
143747
143875
|
},
|
|
143876
|
+
/*
|
|
143877
|
+
FNXC:RunAudit 2026-08-23-18:30:
|
|
143878
|
+
Identical write to `database`, returning whether the row landed for the emitters that gate their
|
|
143879
|
+
own state on a proven write (see the interface note). Still bounded and non-throwing.
|
|
143880
|
+
*/
|
|
143881
|
+
databaseWithOutcome: async (input) => {
|
|
143882
|
+
const inferredTaskId = input.target.startsWith("FN-") || input.target.startsWith("KB-") ? input.target : context.taskId;
|
|
143883
|
+
return await emitBoundedRunAuditWithOutcome2(store, {
|
|
143884
|
+
taskId: inferredTaskId,
|
|
143885
|
+
agentId: context.agentId,
|
|
143886
|
+
runId: context.runId,
|
|
143887
|
+
domain: "database",
|
|
143888
|
+
mutationType: input.type,
|
|
143889
|
+
target: input.target,
|
|
143890
|
+
metadata: {
|
|
143891
|
+
phase: context.phase,
|
|
143892
|
+
...context.source ? { source: context.source } : {},
|
|
143893
|
+
...context.taskLineageId ? { taskLineageId: context.taskLineageId } : {},
|
|
143894
|
+
...input.metadata
|
|
143895
|
+
}
|
|
143896
|
+
});
|
|
143897
|
+
},
|
|
143748
143898
|
filesystem: async (input) => {
|
|
143749
143899
|
const eventInput = {
|
|
143750
143900
|
taskId: context.taskId,
|
|
@@ -146642,10 +146792,13 @@ var init_scheduler = __esm({
|
|
|
146642
146792
|
* Validate that a task's filesystem state is intact.
|
|
146643
146793
|
* Checks that the task directory exists and PROMPT.md is present and non-empty.
|
|
146644
146794
|
*
|
|
146645
|
-
* @param
|
|
146795
|
+
* @param task - The task (id + title) to validate
|
|
146646
146796
|
* @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
|
|
146647
146797
|
*/
|
|
146648
|
-
async validateTaskFilesystem(
|
|
146798
|
+
async validateTaskFilesystem(task) {
|
|
146799
|
+
const id = task.id;
|
|
146800
|
+
const titleRedirect = nonExecutableDuplicateRedirectReason(null, task.title);
|
|
146801
|
+
if (titleRedirect) return { valid: false, reason: titleRedirect };
|
|
146649
146802
|
if (typeof this.store.getTasksDir !== "function") {
|
|
146650
146803
|
return { valid: true };
|
|
146651
146804
|
}
|
|
@@ -146662,7 +146815,7 @@ var init_scheduler = __esm({
|
|
|
146662
146815
|
if (!content || content.trim().length === 0) {
|
|
146663
146816
|
return { valid: false, reason: "missing or empty PROMPT.md" };
|
|
146664
146817
|
}
|
|
146665
|
-
const duplicateOnly = nonExecutableDuplicateRedirectReason(content);
|
|
146818
|
+
const duplicateOnly = nonExecutableDuplicateRedirectReason(content, task.title);
|
|
146666
146819
|
if (duplicateOnly) {
|
|
146667
146820
|
return { valid: false, reason: duplicateOnly };
|
|
146668
146821
|
}
|
|
@@ -147340,7 +147493,7 @@ var init_scheduler = __esm({
|
|
|
147340
147493
|
);
|
|
147341
147494
|
}
|
|
147342
147495
|
}
|
|
147343
|
-
const validation = await this.validateTaskFilesystem(task
|
|
147496
|
+
const validation = await this.validateTaskFilesystem(task);
|
|
147344
147497
|
if (!validation.valid) {
|
|
147345
147498
|
schedulerLog.warn(`Task ${task.id} filesystem validation failed: ${validation.reason}`);
|
|
147346
147499
|
const replanColumn = await moveTaskToReplanColumn(this.store, task);
|
|
@@ -213747,7 +213900,7 @@ async function finalizeProvenAutoMergeTask({
|
|
|
213747
213900
|
});
|
|
213748
213901
|
return { outcome: "blocked", task: latest, previousColumn: latest.column, reason };
|
|
213749
213902
|
}
|
|
213750
|
-
const hardBlocker =
|
|
213903
|
+
const hardBlocker = getMergeConfirmedFinalizationBlocker({
|
|
213751
213904
|
...latest,
|
|
213752
213905
|
/*
|
|
213753
213906
|
FNXC:WorkflowMerge 2026-06-29-09:15:
|
|
@@ -213762,6 +213915,14 @@ async function finalizeProvenAutoMergeTask({
|
|
|
213762
213915
|
status: clearMergeConfirmedTransientStatus(latest.status),
|
|
213763
213916
|
error: void 0
|
|
213764
213917
|
});
|
|
213918
|
+
const unfinishedSteps = getUnfinishedStepTitles(latest);
|
|
213919
|
+
if (unfinishedSteps.length > 0) {
|
|
213920
|
+
await store.logEntry(
|
|
213921
|
+
taskId,
|
|
213922
|
+
`Finalizing proven merge with ${unfinishedSteps.length} unfinished step(s) \u2014 the branch already landed, so these did not run: ${unfinishedSteps.slice(0, 8).join("; ")}`,
|
|
213923
|
+
"MergeConfirmedFinalizeUnfinishedSteps"
|
|
213924
|
+
).catch(() => void 0);
|
|
213925
|
+
}
|
|
213765
213926
|
if (hardBlocker) {
|
|
213766
213927
|
fence?.assertOwned("finalization");
|
|
213767
213928
|
await store.updateTask(taskId, {
|
|
@@ -220594,15 +220755,21 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
|
|
|
220594
220755
|
const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonical);
|
|
220595
220756
|
if (!isNearDuplicateCanonicalInactive(canonical ?? void 0, canonicalFlags)) continue;
|
|
220596
220757
|
const promptPath = join75(this.options.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
|
220597
|
-
|
|
220758
|
+
const written = existsSync60(promptPath) ? readFileSync23(promptPath, "utf-8") : "";
|
|
220759
|
+
const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
|
|
220760
|
+
const canonicalMarkerId = canonicalId.toUpperCase();
|
|
220761
|
+
if (duplicateResolution.conflict || duplicateResolution.marker && duplicateResolution.marker.canonicalId !== canonicalMarkerId) {
|
|
220762
|
+
continue;
|
|
220763
|
+
}
|
|
220764
|
+
if (duplicateResolution.source === "prompt") {
|
|
220598
220765
|
try {
|
|
220599
|
-
|
|
220600
|
-
if (parseExplicitDuplicateMarker(written)) {
|
|
220601
|
-
rmSync7(promptPath, { force: true });
|
|
220602
|
-
}
|
|
220766
|
+
rmSync7(promptPath, { force: true });
|
|
220603
220767
|
} catch {
|
|
220604
220768
|
}
|
|
220605
220769
|
}
|
|
220770
|
+
if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === canonicalMarkerId) {
|
|
220771
|
+
await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${canonicalMarkerId}` });
|
|
220772
|
+
}
|
|
220606
220773
|
await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalId));
|
|
220607
220774
|
if (typeof this.store.logEntry === "function") {
|
|
220608
220775
|
await Promise.resolve(this.store.logEntry(
|
|
@@ -223988,7 +224155,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
|
|
|
223988
224155
|
mergeTargetBranch: mergeTarget.branch,
|
|
223989
224156
|
mergeTargetSource: mergeTarget.source
|
|
223990
224157
|
};
|
|
223991
|
-
const hardBlocker =
|
|
224158
|
+
const hardBlocker = getMergeConfirmedFinalizationBlocker({
|
|
223992
224159
|
...task,
|
|
223993
224160
|
steps: task.steps ?? [],
|
|
223994
224161
|
workflowStepResults: task.workflowStepResults
|
|
@@ -224173,7 +224340,7 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
|
|
|
224173
224340
|
mergeTargetBranch: mergeTarget.branch,
|
|
224174
224341
|
mergeTargetSource: mergeTarget.source
|
|
224175
224342
|
};
|
|
224176
|
-
const hardBlocker =
|
|
224343
|
+
const hardBlocker = getMergeConfirmedFinalizationBlocker({
|
|
224177
224344
|
...task,
|
|
224178
224345
|
steps: task.steps ?? [],
|
|
224179
224346
|
workflowStepResults: task.workflowStepResults
|
|
@@ -226868,14 +227035,12 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
|
|
|
226868
227035
|
for (const task of candidates) {
|
|
226869
227036
|
try {
|
|
226870
227037
|
const promptPath = join75(this.options.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
|
226871
|
-
|
|
226872
|
-
|
|
226873
|
-
|
|
226874
|
-
const written = readFileSync23(promptPath, "utf-8");
|
|
226875
|
-
const marker = parseExplicitDuplicateMarker(written);
|
|
226876
|
-
if (!marker) {
|
|
227038
|
+
const written = existsSync60(promptPath) ? readFileSync23(promptPath, "utf-8") : "";
|
|
227039
|
+
const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
|
|
227040
|
+
if (!duplicateResolution.marker || duplicateResolution.conflict) {
|
|
226877
227041
|
continue;
|
|
226878
227042
|
}
|
|
227043
|
+
const marker = duplicateResolution.marker;
|
|
226879
227044
|
if (processedMarkers >= 50) {
|
|
226880
227045
|
break;
|
|
226881
227046
|
}
|
|
@@ -226888,7 +227053,10 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
|
|
|
226888
227053
|
const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonicalTask);
|
|
226889
227054
|
if (!canonicalTask || isNearDuplicateCanonicalInactive(canonicalTask, canonicalFlags)) {
|
|
226890
227055
|
if (canClearInactiveMarker) {
|
|
226891
|
-
rmSync7(promptPath, { force: true });
|
|
227056
|
+
if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
|
|
227057
|
+
if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
|
|
227058
|
+
await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
|
|
227059
|
+
}
|
|
226892
227060
|
const priorClearCount = typeof task.sourceMetadata?.duplicateMarkerClearCount === "number" ? task.sourceMetadata.duplicateMarkerClearCount : 0;
|
|
226893
227061
|
const patch = buildMarkerClearedReplanTaskPatch(marker.canonicalId, priorClearCount);
|
|
226894
227062
|
await this.store.updateTask(task.id, patch);
|
|
@@ -226907,7 +227075,10 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
|
|
|
226907
227075
|
const resolution = settings.triageDuplicateResolution ?? "prompt";
|
|
226908
227076
|
if (resolution === "prompt" && isTriageDuplicateKeepAcknowledged(task.sourceMetadata, canonicalTask.id)) {
|
|
226909
227077
|
if (canClearInactiveMarker) {
|
|
226910
|
-
rmSync7(promptPath, { force: true });
|
|
227078
|
+
if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
|
|
227079
|
+
if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
|
|
227080
|
+
await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
|
|
227081
|
+
}
|
|
226911
227082
|
const priorKeepClears = typeof task.sourceMetadata?.duplicateMarkerClearCount === "number" ? task.sourceMetadata.duplicateMarkerClearCount : 0;
|
|
226912
227083
|
const keepExhausted = priorKeepClears >= 1;
|
|
226913
227084
|
await this.store.updateTask(
|
|
@@ -226932,7 +227103,10 @@ var SelfHealingManager = class _SelfHealingManager extends SelfHealingGitEvidenc
|
|
|
226932
227103
|
await flagTriageDuplicate(this.store, task.id, canonicalTask.id);
|
|
226933
227104
|
await this.store.updateTask(task.id, { paused: true, pausedReason: "duplicate-decision-required", status: null });
|
|
226934
227105
|
} else {
|
|
226935
|
-
rmSync7(promptPath, { force: true });
|
|
227106
|
+
if (duplicateResolution.source !== "title") rmSync7(promptPath, { force: true });
|
|
227107
|
+
if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === marker.canonicalId) {
|
|
227108
|
+
await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${marker.canonicalId}` });
|
|
227109
|
+
}
|
|
226936
227110
|
await this.store.updateTask(task.id, buildMarkerClearedReplanTaskPatch(canonicalTask.id));
|
|
226937
227111
|
if (typeof this.store.logEntry === "function") {
|
|
226938
227112
|
await Promise.resolve(this.store.logEntry(
|
|
@@ -235940,6 +236114,7 @@ function buildActionGateContext(deps, taskId, agent, projectDefaultPolicy) {
|
|
|
235940
236114
|
executorLog.warn(`${taskId}: failed to suspend in-flight session while awaiting approval: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
235941
236115
|
});
|
|
235942
236116
|
}
|
|
236117
|
+
void emitApprovalMail({ messageStore: deps.messageStore ?? void 0, approvalRequestId, toolName: decision.toolName, taskId, agentId: actorId, agentName: actorName });
|
|
235943
236118
|
if (agent && deps.agentStore) {
|
|
235944
236119
|
await deps.agentStore.updateAgentState(agent.id, "paused");
|
|
235945
236120
|
await deps.agentStore.updateAgent(agent.id, { pauseReason: "awaiting-approval" });
|
|
@@ -236011,6 +236186,7 @@ function buildPermanentAgentGatingContext(deps, taskId, agent, projectDefaultPol
|
|
|
236011
236186
|
deps.approvalSuspended.delete(taskId);
|
|
236012
236187
|
throw error2;
|
|
236013
236188
|
}
|
|
236189
|
+
void emitApprovalMail({ messageStore: deps.messageStore ?? void 0, approvalRequestId, toolName, taskId, agentId: actorId, agentName: actorName });
|
|
236014
236190
|
}
|
|
236015
236191
|
};
|
|
236016
236192
|
}
|
|
@@ -245105,6 +245281,20 @@ var StepSessionExecutor = class {
|
|
|
245105
245281
|
* Safe to call multiple times (idempotent). Call this in a `finally` block
|
|
245106
245282
|
* after `executeAll()`.
|
|
245107
245283
|
*/
|
|
245284
|
+
/**
|
|
245285
|
+
* FNXC:StepParallelWorktrees 2026-08-23-21:40:
|
|
245286
|
+
* `parallelBranches` holds only branches THIS executor created in `createStepWorktree`
|
|
245287
|
+
* (`fusion/step-<idx>-<worktree-name>`), so their provenance is known here rather than inferred.
|
|
245288
|
+
* FN-9161's `isFusionDeletableBranch` answers by NAME and reads anything that is not
|
|
245289
|
+
* `fusion/<task-id>...` as operator-supplied, so applying it here silently skipped every
|
|
245290
|
+
* step branch and leaked one ref per parallel step. Protect what an operator can actually own —
|
|
245291
|
+
* the task's working branch and its explicit override — and dispose of the rest.
|
|
245292
|
+
*/
|
|
245293
|
+
isDisposableStepBranch(branchName) {
|
|
245294
|
+
const task = this.options.taskDetail;
|
|
245295
|
+
const operatorOwned = [task.branch, task.branchContext?.branchOverride?.branch].filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.trim());
|
|
245296
|
+
return !operatorOwned.includes(branchName.trim());
|
|
245297
|
+
}
|
|
245108
245298
|
async cleanup() {
|
|
245109
245299
|
if (this.activeSessions.size > 0) {
|
|
245110
245300
|
await this.terminateAllSessions();
|
|
@@ -245131,7 +245321,7 @@ var StepSessionExecutor = class {
|
|
|
245131
245321
|
}
|
|
245132
245322
|
for (const [stepIdx, branchName] of this.parallelBranches) {
|
|
245133
245323
|
try {
|
|
245134
|
-
if (!
|
|
245324
|
+
if (!this.isDisposableStepBranch(branchName)) continue;
|
|
245135
245325
|
await execAsync50(`git branch -D "${branchName}"`, {
|
|
245136
245326
|
cwd: this.options.rootDir
|
|
245137
245327
|
});
|
|
@@ -245686,7 +245876,7 @@ Follow instructions precisely and avoid unrelated changes.`,
|
|
|
245686
245876
|
});
|
|
245687
245877
|
}
|
|
245688
245878
|
const branch = this.parallelBranches.get(stepIdx);
|
|
245689
|
-
if (branch &&
|
|
245879
|
+
if (branch && this.isDisposableStepBranch(branch)) {
|
|
245690
245880
|
await execAsync50(`git branch -D "${branch}"`, {
|
|
245691
245881
|
cwd: this.options.rootDir
|
|
245692
245882
|
});
|
|
@@ -251894,6 +252084,7 @@ function buildBuildActionGateContextDeps(host) {
|
|
|
251894
252084
|
get approvalRequestStore() {
|
|
251895
252085
|
return host.approvalRequestStore;
|
|
251896
252086
|
},
|
|
252087
|
+
messageStore: host.options.messageStore,
|
|
251897
252088
|
activeWorkflowAuthorities: host.activeWorkflowAuthorities,
|
|
251898
252089
|
activeWorkflowGraphAbortControllers: host.activeWorkflowGraphAbortControllers
|
|
251899
252090
|
};
|
|
@@ -252050,7 +252241,8 @@ function buildBuildPermanentAgentGatingContextDeps(host) {
|
|
|
252050
252241
|
approvalSuspended: host.approvalSuspended,
|
|
252051
252242
|
get approvalRequestStore() {
|
|
252052
252243
|
return host.approvalRequestStore;
|
|
252053
|
-
}
|
|
252244
|
+
},
|
|
252245
|
+
messageStore: host.options.messageStore
|
|
252054
252246
|
};
|
|
252055
252247
|
}
|
|
252056
252248
|
function buildPersistTokenUsageDeps(host) {
|
|
@@ -263921,7 +264113,7 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
263921
264113
|
planLog.warn(`${task.id} planning recovery skipped \u2014 PROMPT.md failed deterministic validation (${deterministicSpecFailure})`);
|
|
263922
264114
|
return false;
|
|
263923
264115
|
}
|
|
263924
|
-
const isExplicitDuplicateRedirect = Boolean(
|
|
264116
|
+
const isExplicitDuplicateRedirect = Boolean(resolveExplicitDuplicateMarker(written, task.title).marker);
|
|
263925
264117
|
const workflow = await resolveWorkflowIrForTask(this.store, task.id).catch(() => void 0);
|
|
263926
264118
|
const requiresPromptImplementationSteps = workflow?.nodes.some(
|
|
263927
264119
|
(node) => node.kind === "parse-steps" && (node.config?.artifact === void 0 || node.config.artifact === "PROMPT.md") && node.config?.parser === "step-headings" && node.config?.requireStepsUnlessNoCommits === true
|
|
@@ -264343,7 +264535,7 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
264343
264535
|
phase: "triage",
|
|
264344
264536
|
source: "triage"
|
|
264345
264537
|
});
|
|
264346
|
-
void throttleAuditor.database({
|
|
264538
|
+
void (throttleAuditor.databaseWithOutcome ?? throttleAuditor.database)({
|
|
264347
264539
|
type: "task:plan-admission-throttled",
|
|
264348
264540
|
target: eligibleIds[0] ?? this.rootDir,
|
|
264349
264541
|
metadata: {
|
|
@@ -264356,7 +264548,9 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
264356
264548
|
processingCount: this.processing.size,
|
|
264357
264549
|
processingTaskIds: processingIds
|
|
264358
264550
|
}
|
|
264359
|
-
}).then(() => {
|
|
264551
|
+
}).then((result) => {
|
|
264552
|
+
const outcome = result?.outcome;
|
|
264553
|
+
if (outcome !== "recorded" && outcome !== "absent") return;
|
|
264360
264554
|
this.lastPlanThrottleSignature = throttleSignature;
|
|
264361
264555
|
void Promise.all(eligibleIds.map((taskId) => this.store.logEntry(taskId, capacityReason))).catch((logErr) => {
|
|
264362
264556
|
planLog.warn(`Failed to write planning capacity reason: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
|
|
@@ -264500,6 +264694,44 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
264500
264694
|
return false;
|
|
264501
264695
|
}
|
|
264502
264696
|
}
|
|
264697
|
+
/**
|
|
264698
|
+
* Resolves an exact prompt/title redirect before this task claims any planning capacity.
|
|
264699
|
+
*
|
|
264700
|
+
* FNXC:DuplicateIntake 2026-08-09-01:31:
|
|
264701
|
+
* FN-8840 requires title redirects to take the same duplicate-decision route as prompt
|
|
264702
|
+
* redirects before `specifyTask()` can start a planner session. Reading the prompt here is
|
|
264703
|
+
* required only to detect a conflicting exact marker; a missing or unreadable prompt leaves
|
|
264704
|
+
* a title-only redirect actionable and never lets it consume an implementation session.
|
|
264705
|
+
*
|
|
264706
|
+
* FNXC:DuplicateIntake 2026-08-23-18:30:
|
|
264707
|
+
* This whole title-aware path (plus the `source: "prompt" | "title"` plumbing in
|
|
264708
|
+
* `clearExplicitDuplicateMarker` and the conflict guard in `finalizeApprovedTaskBody`) was
|
|
264709
|
+
* removed by accident in 1cf86baa1c ("refactor: package code organization wave 18"), which
|
|
264710
|
+
* reverted FN-8840 in triage.ts only — every other FN-8840 surface (dashboard task routes,
|
|
264711
|
+
* `handle-graph-failure.ts`, core `resolveExplicitDuplicateMarker`) kept it. A title-only
|
|
264712
|
+
* `DUPLICATE: <id>` card therefore consumed a full planner session again instead of taking the
|
|
264713
|
+
* duplicate-decision route. Restored here; do not "simplify" back to prompt-only parsing.
|
|
264714
|
+
*/
|
|
264715
|
+
async finalizeExplicitDuplicateBeforePlanning(task) {
|
|
264716
|
+
try {
|
|
264717
|
+
const liveTask = await this.store.getTask(task.id).catch(() => null);
|
|
264718
|
+
if (!liveTask || liveTask.paused === true || liveTask.userPaused === true) return false;
|
|
264719
|
+
const promptPath = join89(this.rootDir, ".fusion", "tasks", liveTask.id, "PROMPT.md");
|
|
264720
|
+
const written = await readFile49(promptPath, "utf-8").catch(() => "");
|
|
264721
|
+
const duplicateResolution = resolveExplicitDuplicateMarker(written, liveTask.title);
|
|
264722
|
+
if (!duplicateResolution.marker && !duplicateResolution.conflict) return false;
|
|
264723
|
+
const settings = await mergeEffectiveSettings(this.store, liveTask, await this.store.getSettings());
|
|
264724
|
+
if (duplicateResolution.conflict) {
|
|
264725
|
+
await this.finalizeApprovedTask(liveTask, written, settings);
|
|
264726
|
+
return true;
|
|
264727
|
+
}
|
|
264728
|
+
return await this.tryFinalizeExplicitDuplicateMarker(liveTask, written, settings);
|
|
264729
|
+
} catch (error2) {
|
|
264730
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
264731
|
+
planLog.warn(`${task.id}: pre-planning duplicate resolution failed open: ${message}`);
|
|
264732
|
+
return false;
|
|
264733
|
+
}
|
|
264734
|
+
}
|
|
264503
264735
|
async specifyTask(task) {
|
|
264504
264736
|
if (this.resetFence.isResetHoldActive(task.id)) {
|
|
264505
264737
|
if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
|
|
@@ -264512,6 +264744,11 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
264512
264744
|
this.coordinatorAdmittedTaskIds.delete(task.id);
|
|
264513
264745
|
return;
|
|
264514
264746
|
}
|
|
264747
|
+
if (await this.finalizeExplicitDuplicateBeforePlanning(task)) {
|
|
264748
|
+
if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
|
|
264749
|
+
this.coordinatorAdmittedTaskIds.delete(task.id);
|
|
264750
|
+
return;
|
|
264751
|
+
}
|
|
264515
264752
|
if (await this.flagImportNearDuplicateBeforePlanning(task)) {
|
|
264516
264753
|
if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
|
|
264517
264754
|
return;
|
|
@@ -265045,12 +265282,28 @@ This is a multi-repository workspace task. Your final PROMPT.md MUST include a n
|
|
|
265045
265282
|
content,
|
|
265046
265283
|
false
|
|
265047
265284
|
),
|
|
265285
|
+
/*
|
|
265286
|
+
FNXC:PlanArtifactPersistence 2026-08-23-19:24:
|
|
265287
|
+
The project-database mirror is BEST-EFFORT — the comment above says so and validation below
|
|
265288
|
+
owns the verdict. FN-151 (c1818ea819) routed it through the reset-fenced writer, which does
|
|
265289
|
+
not swallow, so an infrastructure failure inside the lifecycle lock (e.g. a
|
|
265290
|
+
PlanningLifecycleLockTransportError) aborted a planning attempt whose authoritative PROMPT.md
|
|
265291
|
+
had already been written, and pre-empted both the unchanged-PROMPT verdict and FN-8911's
|
|
265292
|
+
marker-consuming path. Degrade the mirror the way `mirrorPlanToProjectDb` always did; the
|
|
265293
|
+
authoritative `writeAuthoritativePrompt` half deliberately still throws, and only a
|
|
265294
|
+
lifecycle-lock TRANSPORT rejection is degraded here — every other mirror failure keeps
|
|
265295
|
+
aborting the attempt exactly as before.
|
|
265296
|
+
*/
|
|
265048
265297
|
mirrorAuthoritativePlan: async (content) => await this.persistResetFencedPlanningArtifact(
|
|
265049
265298
|
task,
|
|
265050
265299
|
planningGeneration,
|
|
265051
265300
|
content,
|
|
265052
265301
|
true
|
|
265053
|
-
)
|
|
265302
|
+
).catch((error2) => {
|
|
265303
|
+
if (!isPlanningLifecycleLockTransportError(error2)) throw error2;
|
|
265304
|
+
planLog.warn(`${task.id}: lifecycle-lock transport failure while mirroring the authoritative plan into the project database: ${error2.message}`);
|
|
265305
|
+
return false;
|
|
265306
|
+
})
|
|
265054
265307
|
});
|
|
265055
265308
|
if (planPersistence.outcome === "recovery-fenced") return;
|
|
265056
265309
|
if (planPersistence.outcome === "recovered") {
|
|
@@ -265714,11 +265967,11 @@ ${lines.join("\n")}` }],
|
|
|
265714
265967
|
}
|
|
265715
265968
|
async tryFinalizeExplicitDuplicateMarker(task, written, settings, options = {}, report2 = { outcome: "parked" }) {
|
|
265716
265969
|
try {
|
|
265717
|
-
const
|
|
265718
|
-
if (!
|
|
265970
|
+
const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
|
|
265971
|
+
if (!duplicateResolution.marker || duplicateResolution.conflict) {
|
|
265719
265972
|
return false;
|
|
265720
265973
|
}
|
|
265721
|
-
const canonicalId =
|
|
265974
|
+
const canonicalId = duplicateResolution.marker.canonicalId;
|
|
265722
265975
|
const canonicalTask = await this.store.getTask(canonicalId);
|
|
265723
265976
|
if (canonicalTask?.id.toLowerCase() === task.id.toLowerCase()) {
|
|
265724
265977
|
return false;
|
|
@@ -265833,7 +266086,12 @@ ${lines.join("\n")}` }],
|
|
|
265833
266086
|
*/
|
|
265834
266087
|
async clearDuplicateMarkerForReplan(task, canonicalId, feedback, options) {
|
|
265835
266088
|
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
|
265836
|
-
|
|
266089
|
+
if (options?.source !== "title") {
|
|
266090
|
+
await rm16(join89(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
|
|
266091
|
+
}
|
|
266092
|
+
if (resolveExplicitDuplicateMarker(null, task.title).marker?.canonicalId === canonicalId) {
|
|
266093
|
+
await this.store.updateTask(task.id, { title: `Duplicate redirect cleared: ${canonicalId}` });
|
|
266094
|
+
}
|
|
265837
266095
|
})) return false;
|
|
265838
266096
|
const priorClearCount = options?.priorClearCount ?? 0;
|
|
265839
266097
|
if (options?.exhausted) {
|
|
@@ -265863,8 +266121,14 @@ ${lines.join("\n")}` }],
|
|
|
265863
266121
|
}
|
|
265864
266122
|
async finalizeApprovedTaskBody(task, writtenInput, settings, options = {}, report2 = { outcome: "parked" }) {
|
|
265865
266123
|
let written = writtenInput;
|
|
265866
|
-
|
|
265867
|
-
|
|
266124
|
+
const duplicateResolution = resolveExplicitDuplicateMarker(written, task.title);
|
|
266125
|
+
if (duplicateResolution.conflict) {
|
|
266126
|
+
await this.updatePlanningStateIfStillCurrent(task, { status: "needs-replan", error: null });
|
|
266127
|
+
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.");
|
|
266128
|
+
return;
|
|
266129
|
+
}
|
|
266130
|
+
if (!duplicateResolution.marker && await this.recoverMissingPromptBeforeRelease(task)) return;
|
|
266131
|
+
const explicitDuplicateMarker = duplicateResolution.marker;
|
|
265868
266132
|
if (explicitDuplicateMarker) {
|
|
265869
266133
|
await this.updatePlanningStateIfStillCurrent(task, (live) => {
|
|
265870
266134
|
const customFields = { ...live.customFields ?? {} };
|
|
@@ -265872,6 +266136,7 @@ ${lines.join("\n")}` }],
|
|
|
265872
266136
|
return { customFields };
|
|
265873
266137
|
});
|
|
265874
266138
|
const canonicalId = explicitDuplicateMarker.canonicalId;
|
|
266139
|
+
const duplicateSource = duplicateResolution.source ?? "prompt";
|
|
265875
266140
|
const canonicalTask = await this.store.getTask(canonicalId).catch(() => null);
|
|
265876
266141
|
const canClearInactiveMarker = task.userPaused !== true && (task.paused !== true || task.pausedReason === "duplicate-decision-required") && (task.pausedReason == null || task.pausedReason === "duplicate-decision-required");
|
|
265877
266142
|
const canonicalFlags = await resolveNearDuplicateCanonicalFlags(this.store, canonicalTask);
|
|
@@ -265883,7 +266148,7 @@ ${lines.join("\n")}` }],
|
|
|
265883
266148
|
task,
|
|
265884
266149
|
canonicalId,
|
|
265885
266150
|
buildInactiveDuplicateClearFeedback(canonicalId),
|
|
265886
|
-
{ exhausted: false, priorClearCount }
|
|
266151
|
+
{ exhausted: false, priorClearCount, source: duplicateSource }
|
|
265887
266152
|
);
|
|
265888
266153
|
}
|
|
265889
266154
|
return;
|
|
@@ -265896,7 +266161,7 @@ ${lines.join("\n")}` }],
|
|
|
265896
266161
|
task,
|
|
265897
266162
|
canonicalId,
|
|
265898
266163
|
buildKeepDuplicateClearFeedback(canonicalId),
|
|
265899
|
-
{ exhausted: priorClearCount >= 1, priorClearCount }
|
|
266164
|
+
{ exhausted: priorClearCount >= 1, priorClearCount, source: duplicateSource }
|
|
265900
266165
|
);
|
|
265901
266166
|
}
|
|
265902
266167
|
return;
|
|
@@ -265934,7 +266199,8 @@ ${lines.join("\n")}` }],
|
|
|
265934
266199
|
await this.clearDuplicateMarkerForReplan(
|
|
265935
266200
|
task,
|
|
265936
266201
|
canonicalId,
|
|
265937
|
-
buildKeepDuplicateClearFeedback(canonicalId)
|
|
266202
|
+
buildKeepDuplicateClearFeedback(canonicalId),
|
|
266203
|
+
{ source: duplicateSource }
|
|
265938
266204
|
);
|
|
265939
266205
|
return;
|
|
265940
266206
|
}
|
|
@@ -270270,6 +270536,7 @@ var OverseerAdvisorService = class {
|
|
|
270270
270536
|
|
|
270271
270537
|
// ../engine/src/project-engine.ts
|
|
270272
270538
|
init_agent_session_helpers();
|
|
270539
|
+
init_active_session_registry();
|
|
270273
270540
|
|
|
270274
270541
|
// ../engine/src/merge/pr-reconcile.ts
|
|
270275
270542
|
init_src2();
|
|
@@ -271451,6 +271718,10 @@ async function enforceAiMergeSquashGates(params) {
|
|
|
271451
271718
|
if (!(error2 instanceof FileScopeViolationError)) throw error2;
|
|
271452
271719
|
await execFileAsync27("git", ["reset", "--hard", params.tipSha], { cwd: params.mergeRoot });
|
|
271453
271720
|
await execFileAsync27("git", ["clean", "-fd"], { cwd: params.mergeRoot });
|
|
271721
|
+
try {
|
|
271722
|
+
await params.store.updateTask(params.taskId, { aiMergeReviewReconciliation: null });
|
|
271723
|
+
} catch {
|
|
271724
|
+
}
|
|
271454
271725
|
throw error2;
|
|
271455
271726
|
}
|
|
271456
271727
|
}
|
|
@@ -271993,11 +272264,12 @@ async function cleanupAiMergeWorktree(input) {
|
|
|
271993
272264
|
}
|
|
271994
272265
|
const removalTargets = canonicalRoot === mergeRoot ? [mergeRoot] : [canonicalRoot, mergeRoot];
|
|
271995
272266
|
const cleanupMetadata = { taskId, mergeRoot: canonicalRoot, requestedMergeRoot: mergeRoot };
|
|
271996
|
-
let alreadyAbsent =
|
|
272267
|
+
let alreadyAbsent = removalTargets.every((target) => !existsSync86(target));
|
|
271997
272268
|
if (worktreeAdded) {
|
|
271998
272269
|
try {
|
|
271999
272270
|
await gitRunner(["worktree", "remove", "--force", canonicalRoot], projectRootDir);
|
|
272000
|
-
await
|
|
272271
|
+
if (alreadyAbsent) await log42(`AI merge cleanup: worktree ${canonicalRoot} was already absent before git removal; treating cleanup as idempotent`);
|
|
272272
|
+
await audit.git({ type: "merge:ai-worktree-cleanup", target: canonicalRoot, metadata: { ...cleanupMetadata, phase: "git-remove", success: true, ...alreadyAbsent ? { alreadyAbsent: true, idempotent: true } : {} } });
|
|
272001
272273
|
} catch (err) {
|
|
272002
272274
|
const error2 = describeCleanupError(err);
|
|
272003
272275
|
const code = getErrorStringProperty(err, "code");
|
|
@@ -273593,7 +273865,7 @@ async function landWorkspaceTask(store, task, workspaceRootDir, options = {}, de
|
|
|
273593
273865
|
const requiresRepositoryReviewEvidence = approvedReviewEvidence !== void 0 || (mergeBoundaryTask.enabledWorkflowSteps ?? []).some((step) => /review/i.test(step));
|
|
273594
273866
|
const approvalMissingRepositories = Object.entries(mergeBoundaryFingerprints).filter(([repoRel]) => requiresRepositoryReviewEvidence && !approvedReviewEvidence?.[repoRel]).map(([repoRel]) => repoRel).sort();
|
|
273595
273867
|
const contentChangedRepositories = Object.entries(mergeBoundaryFingerprints).filter(([repoRel, fingerprint]) => requiresRepositoryReviewEvidence && Boolean(approvedReviewEvidence?.[repoRel]) && approvedReviewEvidence?.[repoRel]?.fingerprint !== fingerprint).map(([repoRel]) => repoRel).sort();
|
|
273596
|
-
const changedFiles = normalizedMergeBoundaryFiles.filter((file) => !persistedReviewFiles.includes(file)).sort();
|
|
273868
|
+
const changedFiles = requiresRepositoryReviewEvidence ? normalizedMergeBoundaryFiles.filter((file) => !persistedReviewFiles.includes(file)).sort() : [];
|
|
273597
273869
|
if (approvalMissingRepositories.length > 0) {
|
|
273598
273870
|
throw new WorkspaceReviewRequiredError(taskId, {
|
|
273599
273871
|
kind: "approval-missing",
|
|
@@ -275528,6 +275800,9 @@ function emitMergeRequestShadowDequeueParityAudit(store, legacyTaskId, shadowTas
|
|
|
275528
275800
|
}
|
|
275529
275801
|
var execFileAsync33 = promisify87(execFile39);
|
|
275530
275802
|
var MERGE_HANDOFF_GRACE_MS = 300;
|
|
275803
|
+
function isActiveMergeRequestState(state) {
|
|
275804
|
+
return state === "running" || state === "retrying";
|
|
275805
|
+
}
|
|
275531
275806
|
var PR_MERGE_RETRY_BACKOFF_BASE_MS = 5e3;
|
|
275532
275807
|
function getPrMergeRetryNotBefore(task) {
|
|
275533
275808
|
const retries = task.mergeRetries ?? 0;
|
|
@@ -275752,6 +276027,12 @@ var ProjectEngine = class _ProjectEngine {
|
|
|
275752
276027
|
};
|
|
275753
276028
|
// ── Auto-merge state ──
|
|
275754
276029
|
mergeQueue = [];
|
|
276030
|
+
/* FNXC:MergeAuthority 2026-08-23-18:05: last logged sweep-hold reason per task, so a held card is
|
|
276031
|
+
reported once per reason change instead of every 15s poll. In-memory by design — it is a log
|
|
276032
|
+
de-duplicator, not state anything reads back. Pruned each sweep to the current candidate set
|
|
276033
|
+
(review finding #11), so a card that leaves review by ANY route drops out; admission and
|
|
276034
|
+
soft-delete also clear their entry directly. */
|
|
276035
|
+
mergeSweepHoldReasons = /* @__PURE__ */ new Map();
|
|
275755
276036
|
mergeActive = /* @__PURE__ */ new Set();
|
|
275756
276037
|
/** Capacity-deferred ids stay out of the runnable queue until their retry timer fires. */
|
|
275757
276038
|
capacityDeferredMergeTaskIds = /* @__PURE__ */ new Set();
|
|
@@ -277933,25 +278214,174 @@ ${detail}`);
|
|
|
277933
278214
|
);
|
|
277934
278215
|
});
|
|
277935
278216
|
const allowFlags = await Promise.all(candidates.map((t) => this.allowInReviewMergeProcessing(t, settings, this.runtime.getTaskStore())));
|
|
277936
|
-
const
|
|
277937
|
-
|
|
277938
|
-
|
|
277939
|
-
|
|
277940
|
-
|
|
277941
|
-
|
|
277942
|
-
|
|
278217
|
+
const sweepBatch = await this.loadMergeSweepBatch(candidates);
|
|
278218
|
+
const admissions = await Promise.all(
|
|
278219
|
+
candidates.map((t) => this.classifyMergeSweepCandidate(t, reviewLaneIrCache, sweepBatch))
|
|
278220
|
+
);
|
|
278221
|
+
const candidateIds = new Set(candidates.map((t) => t.id));
|
|
278222
|
+
for (const heldTaskId of [...this.mergeSweepHoldReasons.keys()]) {
|
|
278223
|
+
if (!candidateIds.has(heldTaskId)) this.mergeSweepHoldReasons.delete(heldTaskId);
|
|
278224
|
+
}
|
|
278225
|
+
const eligible = sortTasksByPriorityThenAgeAndId(
|
|
278226
|
+
candidates.filter((t, i) => {
|
|
278227
|
+
if (!allowFlags[i]) return false;
|
|
278228
|
+
const admission = admissions[i];
|
|
278229
|
+
if (!admission.admit) {
|
|
278230
|
+
const previous = this.mergeSweepHoldReasons.get(t.id);
|
|
278231
|
+
if (previous !== admission.reason) {
|
|
278232
|
+
this.mergeSweepHoldReasons.set(t.id, admission.reason);
|
|
278233
|
+
runtimeLog.log(`Auto-merge sweep holding ${t.id}: ${admission.reason} (graph owns the merge decision)`);
|
|
278234
|
+
}
|
|
277943
278235
|
return false;
|
|
277944
278236
|
}
|
|
278237
|
+
this.mergeSweepHoldReasons.delete(t.id);
|
|
278238
|
+
return true;
|
|
277945
278239
|
})
|
|
277946
278240
|
);
|
|
277947
|
-
const eligible = sortTasksByPriorityThenAgeAndId(
|
|
277948
|
-
candidates.filter((_2, i) => allowFlags[i] && !unrunGateFlags[i])
|
|
277949
|
-
);
|
|
277950
278241
|
for (const t of eligible) {
|
|
277951
278242
|
this.internalEnqueueMerge(t.id);
|
|
277952
278243
|
}
|
|
277953
278244
|
return eligible.length;
|
|
277954
278245
|
}
|
|
278246
|
+
/*
|
|
278247
|
+
FNXC:MergeAuthority 2026-08-23-18:05 (FN-9191 + FN-9193 wedges):
|
|
278248
|
+
Gather the sweep-admission inputs for one candidate. Probes are best-effort, but they do NOT all
|
|
278249
|
+
degrade in the same direction — the direction is chosen per probe by what a wrong answer costs:
|
|
278250
|
+
|
|
278251
|
+
- liveness, continuations: a throw must not wedge the whole lane, and the classifier's other
|
|
278252
|
+
fences still hold, so they degrade toward "not live" / "unreadable" (the latter refuses
|
|
278253
|
+
initiation on its own — see `continuationsReadable`).
|
|
278254
|
+
- `gatesSatisfied`: the ONE deliberate fail-CLOSED probe (review finding #8). If we cannot prove
|
|
278255
|
+
the pre-merge gates are satisfied we must not start a merge; withholding costs a delay, while
|
|
278256
|
+
admitting costs an unreviewed branch on the target. The real door re-checks authoritatively.
|
|
278257
|
+
|
|
278258
|
+
MULTI-REPO / WORKSPACE (and shared-branch groups) resolve through the SAME rules, deliberately:
|
|
278259
|
+
- `branch-group-member-integration` and `branch-group-promotion` are merge-region kinds, so a
|
|
278260
|
+
shared-branch member parked at either is admitted like any other merge-region card.
|
|
278261
|
+
- A workspace sub-repo land registers `workspace-repo-land` / `workspace-repo-acquire` paths in
|
|
278262
|
+
`activeSessionRegistry`, so a task mid-land reads as live and is held — this is what stops a
|
|
278263
|
+
second dispatch racing a partial land.
|
|
278264
|
+
- A workspace partial land re-enqueues through `internalEnqueueMerge` directly (the error path
|
|
278265
|
+
below), which never consults this gate; the merging status it leaves also satisfies
|
|
278266
|
+
`interruptedMergeAttempt` so a restarted engine re-drives the same land.
|
|
278267
|
+
- `isMergePending` covers the cross-node case: another engine holding a durable merge-dispatch
|
|
278268
|
+
lease reads as live here, so two nodes on one central DB cannot both dispatch the same land.
|
|
278269
|
+
NOTE it answers `true` from its OWN catch on an unreadable lease probe (see its FNXC note), so
|
|
278270
|
+
a persistently broken lease read holds every card in this lane rather than racing another
|
|
278271
|
+
node. That is the intended conservative direction; it is not a silent no-op.
|
|
278272
|
+
|
|
278273
|
+
`batch` carries the sweep-wide batched reads (review finding #10) so this runs at O(1) queries per
|
|
278274
|
+
poll instead of O(cards). It is optional: single-card callers (the column-entry handoff, the
|
|
278275
|
+
unpause re-enqueue, the pre-dispatch re-check) pass nothing and fall back to per-task reads.
|
|
278276
|
+
*/
|
|
278277
|
+
async classifyMergeSweepCandidate(task, irCache, batch, opts = {}) {
|
|
278278
|
+
const store = this.runtime.getTaskStore();
|
|
278279
|
+
let irTrust = "unresolved-selection";
|
|
278280
|
+
let ir2 = null;
|
|
278281
|
+
try {
|
|
278282
|
+
const resolved = await resolveWorkflowIrForTaskWithProvenance(store, task.id, irCache);
|
|
278283
|
+
ir2 = resolved.ir;
|
|
278284
|
+
irTrust = resolved.source === "selection" ? "cards-own" : resolved.selectionAbsent === true ? "effective-default" : "unresolved-selection";
|
|
278285
|
+
} catch {
|
|
278286
|
+
ir2 = null;
|
|
278287
|
+
irTrust = "unresolved-selection";
|
|
278288
|
+
}
|
|
278289
|
+
let hasLiveSession = false;
|
|
278290
|
+
try {
|
|
278291
|
+
hasLiveSession = executingTaskLock.has(task.id) || activeSessionRegistry.pathsForTask(task.id).length > 0 || !opts.ignoreOwnMergePipeline && (this.activeMergeTaskId === task.id || await this.isMergePending(task.id));
|
|
278292
|
+
} catch {
|
|
278293
|
+
hasLiveSession = false;
|
|
278294
|
+
}
|
|
278295
|
+
let continuationPositions = [];
|
|
278296
|
+
let continuationsReadable = true;
|
|
278297
|
+
try {
|
|
278298
|
+
const readContinuations = store.listWorkflowWorkItemsForTask;
|
|
278299
|
+
const items = batch?.continuations ? batch.continuations.get(task.id) ?? [] : typeof readContinuations === "function" ? await readContinuations.call(store, task.id, { kinds: ["task"] }) : [];
|
|
278300
|
+
continuationPositions = items.filter((item) => ACTIVE_WORKFLOW_WORK_ITEM_STATES.includes(item.state)).map((item) => ir2 ? classifyWorkflowNodeMergeRegion(ir2, item.nodeId) : "unknown");
|
|
278301
|
+
} catch {
|
|
278302
|
+
continuationsReadable = false;
|
|
278303
|
+
continuationPositions = [];
|
|
278304
|
+
}
|
|
278305
|
+
if (batch && !batch.continuationsReadable) continuationsReadable = false;
|
|
278306
|
+
let mergeRequestActive = false;
|
|
278307
|
+
if (!isActiveMergeStatus(task.status)) {
|
|
278308
|
+
try {
|
|
278309
|
+
mergeRequestActive = batch?.mergeRequests ? isActiveMergeRequestState(batch.mergeRequests.get(task.id)?.state) : isActiveMergeRequestState((await store.getMergeRequestRecordAsync?.(task.id))?.state);
|
|
278310
|
+
} catch {
|
|
278311
|
+
mergeRequestActive = false;
|
|
278312
|
+
}
|
|
278313
|
+
}
|
|
278314
|
+
let gatesSatisfied = true;
|
|
278315
|
+
try {
|
|
278316
|
+
const reviewColumns = /* @__PURE__ */ new Set([task.column]);
|
|
278317
|
+
gatesSatisfied = !getTaskMergeBlocker({ ...task, steps: task.steps ?? [] }, {
|
|
278318
|
+
reviewColumns,
|
|
278319
|
+
requiredPreMergeStepIds: ir2 ? resolveRequiredPreMergeStepIds(ir2, task.enabledWorkflowSteps) : void 0
|
|
278320
|
+
});
|
|
278321
|
+
} catch {
|
|
278322
|
+
gatesSatisfied = false;
|
|
278323
|
+
}
|
|
278324
|
+
const updatedAtMs = Date.parse(task.updatedAt ?? "");
|
|
278325
|
+
const quiescentMs = Number.isFinite(updatedAtMs) ? Math.max(0, Date.now() - updatedAtMs) : Number.POSITIVE_INFINITY;
|
|
278326
|
+
return classifyMergeSweepAdmission({
|
|
278327
|
+
irTrust,
|
|
278328
|
+
continuationPositions,
|
|
278329
|
+
continuationsReadable,
|
|
278330
|
+
mergeConfirmed: task.mergeDetails?.mergeConfirmed === true,
|
|
278331
|
+
hasLiveSession,
|
|
278332
|
+
interruptedMergeAttempt: isActiveMergeStatus(task.status) || mergeRequestActive,
|
|
278333
|
+
quiescentMs,
|
|
278334
|
+
gatesSatisfied
|
|
278335
|
+
});
|
|
278336
|
+
}
|
|
278337
|
+
/*
|
|
278338
|
+
FNXC:MergeAuthority 2026-08-23-20:05 (review finding #10):
|
|
278339
|
+
One batched read per sweep instead of one per card. The sweep asks the same two questions of every
|
|
278340
|
+
review-lane card on every 15s poll; per-task reads made that 2N queries. A failed batch read is
|
|
278341
|
+
reported as unreadable rather than as empty, so it refuses initiation exactly like the per-task
|
|
278342
|
+
failure path does.
|
|
278343
|
+
*/
|
|
278344
|
+
/*
|
|
278345
|
+
FNXC:MergeAuthority 2026-08-23-20:05 (review finding #2):
|
|
278346
|
+
Position-only dispatch guard. Answers ONE question — "do this card's active continuations still
|
|
278347
|
+
place it inside its workflow's merge region?" — and fails OPEN on every uncertainty (unreadable
|
|
278348
|
+
continuations, an unresolvable workflow, no continuation at all), because the merge door behind it
|
|
278349
|
+
is the authority on everything else. A card with NO active continuation is authorized here by
|
|
278350
|
+
design: that is the shape of the interrupted-merge and quiescent-stall recoveries.
|
|
278351
|
+
*/
|
|
278352
|
+
async isDispatchStillGraphAuthorized(task) {
|
|
278353
|
+
const store = this.runtime.getTaskStore();
|
|
278354
|
+
try {
|
|
278355
|
+
const readContinuations = store.listWorkflowWorkItemsForTask;
|
|
278356
|
+
if (typeof readContinuations !== "function") return true;
|
|
278357
|
+
const items = await readContinuations.call(this.runtime.getTaskStore(), task.id, { kinds: ["task"] });
|
|
278358
|
+
const active2 = items.filter((item) => ACTIVE_WORKFLOW_WORK_ITEM_STATES.includes(item.state));
|
|
278359
|
+
if (active2.length === 0) return true;
|
|
278360
|
+
const resolved = await resolveWorkflowIrForTaskWithProvenance(this.runtime.getTaskStore(), task.id);
|
|
278361
|
+
if (resolved.source !== "selection" && resolved.selectionAbsent !== true) return true;
|
|
278362
|
+
return active2.some((item) => classifyWorkflowNodeMergeRegion(resolved.ir, item.nodeId) !== "outside-merge-region");
|
|
278363
|
+
} catch {
|
|
278364
|
+
return true;
|
|
278365
|
+
}
|
|
278366
|
+
}
|
|
278367
|
+
async loadMergeSweepBatch(candidates) {
|
|
278368
|
+
const store = this.runtime.getTaskStore();
|
|
278369
|
+
const ids = candidates.map((t) => t.id);
|
|
278370
|
+
const batch = { continuationsReadable: true };
|
|
278371
|
+
if (ids.length === 0) return batch;
|
|
278372
|
+
try {
|
|
278373
|
+
batch.continuations = typeof store.listWorkflowWorkItemsForTasks === "function" ? await store.listWorkflowWorkItemsForTasks.call(this.runtime.getTaskStore(), ids, { kinds: ["task"] }) : void 0;
|
|
278374
|
+
} catch {
|
|
278375
|
+
batch.continuations = void 0;
|
|
278376
|
+
batch.continuationsReadable = false;
|
|
278377
|
+
}
|
|
278378
|
+
try {
|
|
278379
|
+
batch.mergeRequests = typeof store.getMergeRequestRecordsAsync === "function" ? await store.getMergeRequestRecordsAsync.call(this.runtime.getTaskStore(), ids) : void 0;
|
|
278380
|
+
} catch {
|
|
278381
|
+
batch.mergeRequests = void 0;
|
|
278382
|
+
}
|
|
278383
|
+
return batch;
|
|
278384
|
+
}
|
|
277955
278385
|
reconcileStaleMergeActive() {
|
|
277956
278386
|
let cleared = 0;
|
|
277957
278387
|
for (const taskId of [...this.mergeActive]) {
|
|
@@ -278270,6 +278700,11 @@ ${detail}`);
|
|
|
278270
278700
|
}
|
|
278271
278701
|
continue;
|
|
278272
278702
|
}
|
|
278703
|
+
if (!task.mergeDetails?.mergeConfirmed && !await this.isDispatchStillGraphAuthorized(task)) {
|
|
278704
|
+
runtimeLog.log(`Auto-merge dispatch skipped for ${taskId}: graph moved the card out of its merge region while it was queued`);
|
|
278705
|
+
this.clearMergeActive(taskId);
|
|
278706
|
+
continue;
|
|
278707
|
+
}
|
|
278273
278708
|
if (task.mergeDetails?.mergeConfirmed) {
|
|
278274
278709
|
const branchGroupForFastPathCandidate = isSharedBranchGroupMemberIntegration(task) ? store.getBranchGroup?.(task.branchContext?.groupId) : null;
|
|
278275
278710
|
const fastPathDefaultBranch = await resolveIntegrationBranch(this.config.workingDirectory, settings2);
|
|
@@ -278389,7 +278824,7 @@ ${detail}`);
|
|
|
278389
278824
|
continue;
|
|
278390
278825
|
}
|
|
278391
278826
|
}
|
|
278392
|
-
const blockerReason =
|
|
278827
|
+
const blockerReason = getMergeConfirmedFinalizationBlocker({
|
|
278393
278828
|
...task,
|
|
278394
278829
|
/*
|
|
278395
278830
|
FNXC:WorkflowResolvedColumns 2026-07-30-18:05 (this parked ALREADY-MERGED work as failed):
|
|
@@ -278403,7 +278838,13 @@ ${detail}`);
|
|
|
278403
278838
|
and for the reason recorded there: `"in-review"` is the review-eligible SENTINEL for this
|
|
278404
278839
|
helper, not a lifecycle column, so a merge-confirmed card evaluates the same blocker set
|
|
278405
278840
|
on a custom workflow as on the builtin one. The column identity of an already-landed card
|
|
278406
|
-
is not what this check is for
|
|
278841
|
+
is not what this check is for.
|
|
278842
|
+
|
|
278843
|
+
FNXC:MergeConfirmedFinalization 2026-08-23-21:40 (FN-9193): incomplete steps NO LONGER
|
|
278844
|
+
apply here. The fast path above already proved this merge landed, and holding a landed
|
|
278845
|
+
card out of `done` for unfinished steps is what left FN-9193 permanently unfinalizable —
|
|
278846
|
+
a restart then replanned fresh pending steps and re-created the very block it was meant
|
|
278847
|
+
to clear. Paused/error/pre-merge-step blockers still apply.
|
|
278407
278848
|
*/
|
|
278408
278849
|
column: REVIEW_ELIGIBLE_SENTINEL_COLUMN,
|
|
278409
278850
|
// Merge-confirmed tasks have already landed. Treat stale merge
|
|
@@ -278413,6 +278854,14 @@ ${detail}`);
|
|
|
278413
278854
|
status: clearMergeConfirmedTransientStatus(task.status),
|
|
278414
278855
|
error: void 0
|
|
278415
278856
|
});
|
|
278857
|
+
const unfinishedFastPathSteps = getUnfinishedStepTitles(task);
|
|
278858
|
+
if (unfinishedFastPathSteps.length > 0) {
|
|
278859
|
+
await store.logEntry(
|
|
278860
|
+
taskId,
|
|
278861
|
+
`Finalizing proven merge with ${unfinishedFastPathSteps.length} unfinished step(s) \u2014 the branch already landed, so these did not run: ${unfinishedFastPathSteps.slice(0, 8).join("; ")}`,
|
|
278862
|
+
"MergeConfirmedFinalizeUnfinishedSteps"
|
|
278863
|
+
).catch(() => void 0);
|
|
278864
|
+
}
|
|
278416
278865
|
if (blockerReason) {
|
|
278417
278866
|
await store.updateTask(taskId, {
|
|
278418
278867
|
status: "failed",
|
|
@@ -279564,6 +280013,11 @@ ${detail}`);
|
|
|
279564
280013
|
runtimeLog.warn(`Auto-merge handoff (${task.id}): clearing stale mergeActive before enqueue`);
|
|
279565
280014
|
this.clearMergeActive(task.id);
|
|
279566
280015
|
}
|
|
280016
|
+
const handoffAdmission = await this.classifyMergeSweepCandidate(latestTask, /* @__PURE__ */ new Map(), void 0, { ignoreOwnMergePipeline: true });
|
|
280017
|
+
if (!handoffAdmission.admit) {
|
|
280018
|
+
runtimeLog.log(`Auto-merge handoff (${task.id}) skipped: ${handoffAdmission.reason} (graph owns the merge decision)`);
|
|
280019
|
+
return;
|
|
280020
|
+
}
|
|
279567
280021
|
this.internalEnqueueMerge(task.id);
|
|
279568
280022
|
} catch (err) {
|
|
279569
280023
|
runtimeLog.warn(
|
|
@@ -279651,6 +280105,11 @@ ${detail}`);
|
|
|
279651
280105
|
if (this.options.getTaskMergeBlocker?.(task)) {
|
|
279652
280106
|
return;
|
|
279653
280107
|
}
|
|
280108
|
+
const unpauseAdmission = await this.classifyMergeSweepCandidate(task, /* @__PURE__ */ new Map(), void 0, { ignoreOwnMergePipeline: true });
|
|
280109
|
+
if (!unpauseAdmission.admit) {
|
|
280110
|
+
runtimeLog.log(`In-review unpause: ${task.id} not re-enqueued: ${unpauseAdmission.reason} (graph owns the merge decision)`);
|
|
280111
|
+
return;
|
|
280112
|
+
}
|
|
279654
280113
|
runtimeLog.log(`Unpaused in-review task re-enqueued for auto-merge: ${task.id}`);
|
|
279655
280114
|
this.internalEnqueueMerge(task.id);
|
|
279656
280115
|
} catch (err) {
|
|
@@ -279661,6 +280120,7 @@ ${detail}`);
|
|
|
279661
280120
|
};
|
|
279662
280121
|
this.taskDeletedHandler = (task) => {
|
|
279663
280122
|
this.pausedReviewTaskIds.delete(task.id);
|
|
280123
|
+
this.mergeSweepHoldReasons.delete(task.id);
|
|
279664
280124
|
const queueLengthBefore = this.mergeQueue.length;
|
|
279665
280125
|
this.mergeQueue = this.mergeQueue.filter((queuedTaskId) => queuedTaskId !== task.id);
|
|
279666
280126
|
const removedFromQueue = this.mergeQueue.length !== queueLengthBefore;
|