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