@runfusion/fusion 0.74.0-beta.1 → 0.74.0-beta.3
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 +1246 -680
- package/dist/child-process-worker.js +810 -253
- package/dist/client/.vite/manifest.json +268 -268
- package/dist/client/assets/{AgentDetailView-Bopzxiqb.js → AgentDetailView-D0J_GDct.js} +1 -1
- package/dist/client/assets/{AgentPermissionPolicyEditor-i5N7rUlL.js → AgentPermissionPolicyEditor-BBAwprvO.js} +1 -1
- package/dist/client/assets/{AgentsView-M3GoJDHH.js → AgentsView-DTY5DIav.js} +3 -3
- package/dist/client/assets/{ChatView-CqiyA9Kt.js → ChatView-CRoW2tRJ.js} +1 -1
- package/dist/client/assets/{CommandCenter-CAvMGKhl.js → CommandCenter-BieyKvKb.js} +1 -1
- package/dist/client/assets/{DevServerView-wqM6h3nV.js → DevServerView-CLR1LmhK.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-C8OyXCE-.js → DirectoryPicker-B-olNAvT.js} +1 -1
- package/dist/client/assets/{DocumentsView-CjNMZQi8.js → DocumentsView-CG_-pnXU.js} +1 -1
- package/dist/client/assets/{EvalsView-BVlbuB5t.js → EvalsView-Xp0zCUlr.js} +1 -1
- package/dist/client/assets/{ExperimentalAgentOnboardingModal-CIwCg8sq.js → ExperimentalAgentOnboardingModal-z8m5V5yv.js} +1 -1
- package/dist/client/assets/{GoalsView-Cnw2igil.js → GoalsView-Bd-4g949.js} +1 -1
- package/dist/client/assets/{InsightsView-D7GmD-Kx.js → InsightsView-tCgg66wJ.js} +1 -1
- package/dist/client/assets/{MemoryView-BlqkfIE5.js → MemoryView-Dqgsd_0E.js} +1 -1
- package/dist/client/assets/{PiExtensionsManager-EC94zLMq.js → PiExtensionsManager-GzGoWgb5.js} +1 -1
- package/dist/client/assets/{PluginManager-pYyCD3uh.js → PluginManager-BWdqEqr9.js} +1 -1
- package/dist/client/assets/{PullRequestView-BAb0vVhu.js → PullRequestView-CQq1G1kY.js} +1 -1
- package/dist/client/assets/{ReportModal-COmReivA.js → ReportModal-C-3rBFhw.js} +1 -1
- package/dist/client/assets/{ResearchView-DLMo4rFw.js → ResearchView-CS1HwDg_.js} +1 -1
- package/dist/client/assets/{SecretsView-w_O8NLfw.js → SecretsView-C6RP9Rkw.js} +1 -1
- package/dist/client/assets/{SessionTerminal-CSPi-qWE.js → SessionTerminal-C9nCSefk.js} +1 -1
- package/dist/client/assets/SettingsModal-C1oEklYy.css +1 -0
- package/dist/client/assets/{SettingsModal-wilrGXvE.js → SettingsModal-D3DCvCfP.js} +1 -1
- package/dist/client/assets/{SettingsModal-C9bVvYQq.js → SettingsModal-D4mjT8hw.js} +4 -4
- package/dist/client/assets/{SettingsTextareaRow-Bd7_SxVv.js → SettingsTextareaRow-zBVXQbcC.js} +1 -1
- package/dist/client/assets/{SetupWizardModal-sPs6Lw3a.js → SetupWizardModal-C9r1dd-h.js} +2 -2
- package/dist/client/assets/{SkillsView-B9PvYS8t.js → SkillsView-DFyUBWd8.js} +1 -1
- package/dist/client/assets/{TodoView-Df0SD_JT.js → TodoView-BC8f952m.js} +1 -1
- package/dist/client/assets/{WorkflowNodeEditor-B3kFD-S0.js → WorkflowNodeEditor-UTX757WH.js} +1 -1
- package/dist/client/assets/{agent-import-generation-OXfU1-D1.js → agent-import-generation-DN0q1xEo.js} +1 -1
- package/dist/client/assets/{app-DJIiVHdA.js → app-BP57l_5h.js} +1 -1
- package/dist/client/assets/{app-hQw_x2KJ.js → app-D4frL0Ka.js} +1 -1
- package/dist/client/assets/{app-DaikOPcV.js → app-niWDkeV8.js} +3 -3
- package/dist/client/assets/{app-Dn1u7zJ9.js → app-sLOwmHAE.js} +1 -1
- package/dist/client/assets/{app-DMZ_RusZ.js → app-vRWwM-aD.js} +1 -1
- package/dist/client/assets/{app-DxTbBEMH.js → app-zPyey_fg.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-B_8zk4ag.js → architectureDiagram-3BPJPVTR-D-nqJ95-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-CpDKBJkO.js → blockDiagram-GPEHLZMM--OlkXlyS.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-D3wtfGzI.js → c4Diagram-AAUBKEIU-CH7PaWgs.js} +1 -1
- package/dist/client/assets/channel-FJNMzlkv.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DS9sHIMv.js → chunk-2J33WTMH-CpHDwqg2.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-B-xW6cfi.js → chunk-4BX2VUAB-BdW4tdnc.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-Ddxf-sPU.js → chunk-55IACEB6-b3tBFiuH.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-Bv_Z1w57.js → chunk-727SXJPM-DifknEtv.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DYHYN43E.js → chunk-AQP2D5EJ-DHIZNZ4Z.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-wuuFvvBk.js → chunk-FMBD7UC4-BLRLYW2y.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-DfVRkVZo.js → chunk-ND2GUHAM-Dh8f06Ld.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-D0t6ryuh.js → chunk-QZHKN3VN-CpzUiemt.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BOenhjVP.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BOenhjVP.js +1 -0
- package/dist/client/assets/common-13IRj_PO.js +1 -0
- package/dist/client/assets/common-BBynATth.js +1 -0
- package/dist/client/assets/common-BG-vw0tL.js +1 -0
- package/dist/client/assets/common-DSo46rnH.js +1 -0
- package/dist/client/assets/common-DbOCE-0i.js +1 -0
- package/dist/client/assets/common-DnXF4Jmj.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-DVlKqNFJ.js → cose-bilkent-S5V4N54A-CE2RJ_k0.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-DTtiGxz-.js → dagre-BM42HDAG-CCs_xlYw.js} +1 -1
- package/dist/client/assets/{dashboard-view-CLjYqvE9.js → dashboard-view-D5qv61fP.js} +1 -1
- package/dist/client/assets/{dashboard-view-D-nDgSmP.js → dashboard-view-Eg7Jyvnm.js} +1 -1
- package/dist/client/assets/{dashboard-view-DH99T6VJ.js → dashboard-view-Z6H-k0uR.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-Tv-YxCWk.js → diagram-2AECGRRQ-CF60Q-de.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-DlAAPkGu.js → diagram-5GNKFQAL-DDcGMlua.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DisatTBB.js → diagram-KO2AKTUF-YgvJ4DQf.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-CDrUxjzW.js → diagram-LMA3HP47-Bi-j9nGw.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BQ34a4Ej.js → diagram-OG6HWLK6-DEmMqnwc.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-s-YHwAEU.js → erDiagram-TEJ5UH35-DwmZwhiW.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-EpXzmG2d.js → flowDiagram-I6XJVG4X-B5ITa_HJ.js} +1 -1
- package/dist/client/assets/{folder-open-DN86xUly.js → folder-open-BVxA7YtM.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-Dk2WaoX8.js → ganttDiagram-6RSMTGT7-BgAPwh9w.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CjchnKW0.js → gitGraphDiagram-PVQCEYII-DTtGzZFj.js} +1 -1
- package/dist/client/assets/{index-aGZ3DHQw.js → index-idkERMsE.js} +289 -289
- package/dist/client/assets/{infoDiagram-5YYISTIA-CiU-twoj.js → infoDiagram-5YYISTIA-CkSnTje1.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BvylyUKK.js → ishikawaDiagram-YF4QCWOH-jn5VNpMh.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Ca6z84KB.js → journeyDiagram-JHISSGLW-BpaqoXaI.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-DXbw2-s-.js → kanban-definition-UN3LZRKU-CpfjJoBN.js} +1 -1
- package/dist/client/assets/{mermaid.core-DkS33D12.js → mermaid.core-DZNGPQzF.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgXuWPDD.js → mindmap-definition-RKZ34NQL-BdbX1EJa.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-CnOzTN6U.js → pieDiagram-4H26LBE5-Xn0322tJ.js} +1 -1
- package/dist/client/assets/{puzzle-Dxl1kcLF.js → puzzle-CPx1RP2g.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DI3s1u95.js → quadrantDiagram-W4KKPZXB-OOHqqw5y.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-Ck97mo-W.js → requirementDiagram-4Y6WPE33-Dvj6zvaB.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Z3w8NJac.js → sankeyDiagram-5OEKKPKP-DEkEqthK.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DLGtWgfy.js → sequenceDiagram-3UESZ5HK-BPc9ncPj.js} +1 -1
- package/dist/client/assets/{shield-alert-BCGxgBpM.js → shield-alert-baoKRyU6.js} +1 -1
- package/dist/client/assets/{standing-instructions-template-CUE6ItG6.js → standing-instructions-template-CkqsGC1L.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-C6Zl2EJW.js → stateDiagram-AJRCARHV-DywhjHIx.js} +1 -1
- package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-BA77mDAW.js → stateDiagram-v2-BHNVJYJU-D7t7x4uT.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-D9Cp15Hm.js → timeline-definition-PNZ67QCA-BH3CKgrg.js} +1 -1
- package/dist/client/assets/{upload-2jCicylS.js → upload-DtIvOGwM.js} +1 -1
- package/dist/client/assets/{users-C1TTGAfX.js → users-DcxsxPlj.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-BwFSh6Tr.js → vennDiagram-CIIHVFJN-CCDllBgJ.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-CQknAo24.js → wardley-L42UT6IY-C43hFLXh.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Dwc8f8bd.js → wardleyDiagram-YWT4CUSO-__f9rI8w.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-CPkeHv8g.js → xychartDiagram-2RQKCTM6-DY-fpQ_x.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/client/version.json +1 -1
- package/dist/extension.js +950 -393
- package/package.json +1 -1
- package/skill/fusion/references/engine-tools.md +1 -1
- package/dist/client/assets/SettingsModal-CMLHZBhX.css +0 -1
- package/dist/client/assets/channel-BLzjKIVz.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-DkwPy8u9.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DkwPy8u9.js +0 -1
- package/dist/client/assets/common-BDp2y6CF.js +0 -1
- package/dist/client/assets/common-BnEo5fLN.js +0 -1
- package/dist/client/assets/common-Bzr_Y8h4.js +0 -1
- package/dist/client/assets/common-CvwyFzfv.js +0 -1
- package/dist/client/assets/common-CztCXRkz.js +0 -1
- package/dist/client/assets/common-DPe2mbb7.js +0 -1
|
@@ -5603,7 +5603,7 @@ var init_sql = __esm({
|
|
|
5603
5603
|
return new SQL([new StringChunk(str)]);
|
|
5604
5604
|
}
|
|
5605
5605
|
sql2.raw = raw;
|
|
5606
|
-
function
|
|
5606
|
+
function join85(chunks, separator) {
|
|
5607
5607
|
const result = [];
|
|
5608
5608
|
for (const [i, chunk] of chunks.entries()) {
|
|
5609
5609
|
if (i > 0 && separator !== void 0) {
|
|
@@ -5613,7 +5613,7 @@ var init_sql = __esm({
|
|
|
5613
5613
|
}
|
|
5614
5614
|
return new SQL(result);
|
|
5615
5615
|
}
|
|
5616
|
-
sql2.join =
|
|
5616
|
+
sql2.join = join85;
|
|
5617
5617
|
function identifier(value) {
|
|
5618
5618
|
return new Name(value);
|
|
5619
5619
|
}
|
|
@@ -10261,7 +10261,7 @@ var init_select2 = __esm({
|
|
|
10261
10261
|
const baseTableName = this.tableName;
|
|
10262
10262
|
const tableName = getTableLikeName(table);
|
|
10263
10263
|
for (const item of extractUsedTable(table)) this.usedTables.add(item);
|
|
10264
|
-
if (typeof tableName === "string" && this.config.joins?.some((
|
|
10264
|
+
if (typeof tableName === "string" && this.config.joins?.some((join85) => join85.alias === tableName)) {
|
|
10265
10265
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
10266
10266
|
}
|
|
10267
10267
|
if (!this.isPartialSelect) {
|
|
@@ -11796,7 +11796,7 @@ var init_update = __esm({
|
|
|
11796
11796
|
createJoin(joinType) {
|
|
11797
11797
|
return (table, on) => {
|
|
11798
11798
|
const tableName = getTableLikeName(table);
|
|
11799
|
-
if (typeof tableName === "string" && this.config.joins.some((
|
|
11799
|
+
if (typeof tableName === "string" && this.config.joins.some((join85) => join85.alias === tableName)) {
|
|
11800
11800
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
11801
11801
|
}
|
|
11802
11802
|
if (typeof on === "function") {
|
|
@@ -11892,10 +11892,10 @@ var init_update = __esm({
|
|
|
11892
11892
|
const fromFields = this.getTableLikeFields(this.config.from);
|
|
11893
11893
|
fields[tableName] = fromFields;
|
|
11894
11894
|
}
|
|
11895
|
-
for (const
|
|
11896
|
-
const tableName2 = getTableLikeName(
|
|
11897
|
-
if (typeof tableName2 === "string" && !is(
|
|
11898
|
-
const fromFields = this.getTableLikeFields(
|
|
11895
|
+
for (const join85 of this.config.joins) {
|
|
11896
|
+
const tableName2 = getTableLikeName(join85.table);
|
|
11897
|
+
if (typeof tableName2 === "string" && !is(join85.table, SQL)) {
|
|
11898
|
+
const fromFields = this.getTableLikeFields(join85.table);
|
|
11899
11899
|
fields[tableName2] = fromFields;
|
|
11900
11900
|
}
|
|
11901
11901
|
}
|
|
@@ -20499,26 +20499,26 @@ function validateParallelism(nodes2, outgoing, nodesById) {
|
|
|
20499
20499
|
}
|
|
20500
20500
|
const joinsReached = /* @__PURE__ */ new Set();
|
|
20501
20501
|
for (const edge of branchEdges) {
|
|
20502
|
-
const
|
|
20503
|
-
if (!
|
|
20502
|
+
const join86 = walkBranchToJoin(edge.to, split.id, outgoing, nodesById);
|
|
20503
|
+
if (!join86) {
|
|
20504
20504
|
throw new WorkflowIrError(`split '${split.id}' has a branch with no reachable matching join`);
|
|
20505
20505
|
}
|
|
20506
|
-
joinsReached.add(
|
|
20506
|
+
joinsReached.add(join86);
|
|
20507
20507
|
}
|
|
20508
20508
|
if (joinsReached.size !== 1) {
|
|
20509
20509
|
throw new WorkflowIrError(`split '${split.id}' branches converge on more than one join`);
|
|
20510
20510
|
}
|
|
20511
20511
|
const joinId = [...joinsReached][0];
|
|
20512
|
-
const
|
|
20513
|
-
const mode =
|
|
20512
|
+
const join85 = nodesById.get(joinId);
|
|
20513
|
+
const mode = join85.config?.mode;
|
|
20514
20514
|
if (mode && typeof mode === "object" && "quorum" in mode) {
|
|
20515
20515
|
const n = mode.quorum;
|
|
20516
20516
|
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) {
|
|
20517
|
-
throw new WorkflowIrError(`join '${
|
|
20517
|
+
throw new WorkflowIrError(`join '${join85.id}' quorum must be a positive integer`);
|
|
20518
20518
|
}
|
|
20519
20519
|
if (n > branchEdges.length) {
|
|
20520
20520
|
throw new WorkflowIrError(
|
|
20521
|
-
`join '${
|
|
20521
|
+
`join '${join85.id}' quorum(${n}) exceeds the split's ${branchEdges.length} branches`
|
|
20522
20522
|
);
|
|
20523
20523
|
}
|
|
20524
20524
|
}
|
|
@@ -24074,14 +24074,14 @@ async function detectWorkspaceRepos(dir, runner = runGitCommand, timeout = DEFAU
|
|
|
24074
24074
|
return [];
|
|
24075
24075
|
}
|
|
24076
24076
|
const { stat: stat10 } = await import("node:fs/promises");
|
|
24077
|
-
const { join:
|
|
24077
|
+
const { join: join85 } = await import("node:path");
|
|
24078
24078
|
const found = [];
|
|
24079
24079
|
const EXCLUDED_ENTRIES = /* @__PURE__ */ new Set(["node_modules", ".fusion", ".git", ".pi"]);
|
|
24080
24080
|
for (const entry of entries) {
|
|
24081
24081
|
if (EXCLUDED_ENTRIES.has(entry)) continue;
|
|
24082
|
-
const childDir =
|
|
24082
|
+
const childDir = join85(dir, entry);
|
|
24083
24083
|
try {
|
|
24084
|
-
const s3 = await stat10(
|
|
24084
|
+
const s3 = await stat10(join85(childDir, ".git"));
|
|
24085
24085
|
if (!s3.isDirectory() && !s3.isFile()) continue;
|
|
24086
24086
|
} catch {
|
|
24087
24087
|
continue;
|
|
@@ -24094,9 +24094,9 @@ async function detectWorkspaceRepos(dir, runner = runGitCommand, timeout = DEFAU
|
|
|
24094
24094
|
}
|
|
24095
24095
|
async function isWorkspaceModeExplicitlyDisabled(projectPath) {
|
|
24096
24096
|
try {
|
|
24097
|
-
const { readFile:
|
|
24098
|
-
const { join:
|
|
24099
|
-
const raw = await
|
|
24097
|
+
const { readFile: readFile41 } = await import("node:fs/promises");
|
|
24098
|
+
const { join: join85 } = await import("node:path");
|
|
24099
|
+
const raw = await readFile41(join85(projectPath, ".fusion", "config.json"), "utf-8");
|
|
24100
24100
|
const config3 = JSON.parse(raw);
|
|
24101
24101
|
return config3.settings?.workspaceMode === false;
|
|
24102
24102
|
} catch {
|
|
@@ -24104,12 +24104,12 @@ async function isWorkspaceModeExplicitlyDisabled(projectPath) {
|
|
|
24104
24104
|
}
|
|
24105
24105
|
}
|
|
24106
24106
|
async function setWorkspaceModeInConfig(projectPath, value) {
|
|
24107
|
-
const { readFile:
|
|
24108
|
-
const { join:
|
|
24109
|
-
const configPath =
|
|
24107
|
+
const { readFile: readFile41, writeFile: writeFile31, mkdir: mkdir29 } = await import("node:fs/promises");
|
|
24108
|
+
const { join: join85 } = await import("node:path");
|
|
24109
|
+
const configPath = join85(projectPath, ".fusion", "config.json");
|
|
24110
24110
|
let config3 = {};
|
|
24111
24111
|
try {
|
|
24112
|
-
config3 = JSON.parse(await
|
|
24112
|
+
config3 = JSON.parse(await readFile41(configPath, "utf-8"));
|
|
24113
24113
|
} catch (err) {
|
|
24114
24114
|
if (err?.code !== "ENOENT") throw err;
|
|
24115
24115
|
}
|
|
@@ -24118,7 +24118,7 @@ async function setWorkspaceModeInConfig(projectPath, value) {
|
|
|
24118
24118
|
}
|
|
24119
24119
|
const settings = config3.settings;
|
|
24120
24120
|
settings.workspaceMode = value;
|
|
24121
|
-
await mkdir29(
|
|
24121
|
+
await mkdir29(join85(projectPath, ".fusion"), { recursive: true });
|
|
24122
24122
|
await writeFile31(configPath, JSON.stringify(config3, null, 2), "utf-8");
|
|
24123
24123
|
}
|
|
24124
24124
|
function isInRootRelativePath(entry, pathMod) {
|
|
@@ -24131,12 +24131,12 @@ function isInRootRelativePath(entry, pathMod) {
|
|
|
24131
24131
|
return true;
|
|
24132
24132
|
}
|
|
24133
24133
|
async function loadWorkspaceConfig(rootDir) {
|
|
24134
|
-
const { readFile:
|
|
24134
|
+
const { readFile: readFile41 } = await import("node:fs/promises");
|
|
24135
24135
|
const pathMod = await import("node:path");
|
|
24136
|
-
const { join:
|
|
24137
|
-
const configPath =
|
|
24136
|
+
const { join: join85 } = pathMod;
|
|
24137
|
+
const configPath = join85(rootDir, ".fusion", WORKSPACE_CONFIG_FILENAME);
|
|
24138
24138
|
try {
|
|
24139
|
-
const raw = await
|
|
24139
|
+
const raw = await readFile41(configPath, "utf-8");
|
|
24140
24140
|
const parsed = JSON.parse(raw);
|
|
24141
24141
|
if (parsed !== null && typeof parsed === "object" && "repos" in parsed && Array.isArray(parsed.repos) && parsed.repos.every((r) => typeof r === "string")) {
|
|
24142
24142
|
const rawRepos = parsed.repos;
|
|
@@ -24150,11 +24150,11 @@ async function loadWorkspaceConfig(rootDir) {
|
|
|
24150
24150
|
}
|
|
24151
24151
|
async function saveWorkspaceConfig(rootDir, config3) {
|
|
24152
24152
|
const { mkdir: mkdir29, writeFile: writeFile31 } = await import("node:fs/promises");
|
|
24153
|
-
const { join:
|
|
24154
|
-
const fusionDir =
|
|
24153
|
+
const { join: join85 } = await import("node:path");
|
|
24154
|
+
const fusionDir = join85(rootDir, ".fusion");
|
|
24155
24155
|
await mkdir29(fusionDir, { recursive: true });
|
|
24156
24156
|
await writeFile31(
|
|
24157
|
-
|
|
24157
|
+
join85(fusionDir, WORKSPACE_CONFIG_FILENAME),
|
|
24158
24158
|
JSON.stringify(config3, null, 2),
|
|
24159
24159
|
"utf-8"
|
|
24160
24160
|
);
|
|
@@ -38983,11 +38983,11 @@ async function rekeyFallbackProjectPartition(db, fallbackProjectId, registeredPr
|
|
|
38983
38983
|
const parentKey = `${fk.parentSchema}.${fk.parentTable}`;
|
|
38984
38984
|
if (!targetKeys.has(parentKey)) continue;
|
|
38985
38985
|
const childTarget = targetKeys.has(`${fk.childSchema}.${fk.childTable}`);
|
|
38986
|
-
const
|
|
38986
|
+
const join85 = fk.childColumns.map((column, index2) => `child.${quoteIdentifier(column)} IS NOT DISTINCT FROM parent.${quoteIdentifier(fk.parentColumns[index2])}`).join(" AND ");
|
|
38987
38987
|
const registeredChildScope = childTarget ? ` AND child.${quoteIdentifier("project_id")} = ${quoteLiteral(registeredProjectId)}` : "";
|
|
38988
38988
|
const unreplaced = await tx.execute(sql.raw(`SELECT EXISTS (
|
|
38989
38989
|
SELECT 1 FROM ${relationName({ schema: fk.childSchema, table: fk.childTable })} child
|
|
38990
|
-
JOIN ${relationName({ schema: fk.parentSchema, table: fk.parentTable })} parent ON ${
|
|
38990
|
+
JOIN ${relationName({ schema: fk.parentSchema, table: fk.parentTable })} parent ON ${join85}
|
|
38991
38991
|
JOIN fusion_rekey_conflict_candidates parent_candidate
|
|
38992
38992
|
ON parent_candidate.schema_name = ${quoteLiteral(fk.parentSchema)}
|
|
38993
38993
|
AND parent_candidate.table_name = ${quoteLiteral(fk.parentTable)}
|
|
@@ -49824,8 +49824,8 @@ var require_CronFileParser = __commonJS({
|
|
|
49824
49824
|
* @throws If file cannot be read
|
|
49825
49825
|
*/
|
|
49826
49826
|
static async parseFile(filePath) {
|
|
49827
|
-
const { readFile:
|
|
49828
|
-
const data = await
|
|
49827
|
+
const { readFile: readFile41 } = await Promise.resolve().then(() => __importStar(__require("fs/promises")));
|
|
49828
|
+
const data = await readFile41(filePath, "utf8");
|
|
49829
49829
|
return _CronFileParser.#parseContent(data);
|
|
49830
49830
|
}
|
|
49831
49831
|
/**
|
|
@@ -132897,6 +132897,23 @@ var init_worktrunk_installer = __esm({
|
|
|
132897
132897
|
});
|
|
132898
132898
|
|
|
132899
132899
|
// ../engine/src/active-session-registry.ts
|
|
132900
|
+
function acquireActiveSessionPath(registry3, path5, registration, options = {}) {
|
|
132901
|
+
const existing = registry3.lookupByPath(path5);
|
|
132902
|
+
if (!existing || existing.taskId === registration.taskId) {
|
|
132903
|
+
registry3.registerPath(path5, registration);
|
|
132904
|
+
return { action: "registered" };
|
|
132905
|
+
}
|
|
132906
|
+
const now = options.now?.() ?? Date.now();
|
|
132907
|
+
const ageMs = now - existing.registeredAt;
|
|
132908
|
+
const minIdleMs = options.minIdleMs ?? DEFAULT_SELF_OWNED_MIN_IDLE_MS;
|
|
132909
|
+
const holderIsLive = options.holderLiveProbe?.(existing.taskId, path5) ?? true;
|
|
132910
|
+
if (holderIsLive || ageMs < minIdleMs) {
|
|
132911
|
+
return { action: "contended", holderTaskId: existing.taskId, holderKind: existing.kind, ageMs };
|
|
132912
|
+
}
|
|
132913
|
+
registry3.unregisterPath(path5);
|
|
132914
|
+
registry3.registerPath(path5, registration);
|
|
132915
|
+
return { action: "reclaimed-stale-foreign", holderTaskId: existing.taskId, ageMs };
|
|
132916
|
+
}
|
|
132900
132917
|
function reconcileSelfOwnedActiveSessionForRemoval(registry3, worktreePath, requestingTaskId, liveBindingProbe, options = {}) {
|
|
132901
132918
|
const record2 = registry3.lookupByPath(worktreePath);
|
|
132902
132919
|
if (!record2) {
|
|
@@ -136283,10 +136300,10 @@ async function runHoldReleaseSweep(store, deps) {
|
|
|
136283
136300
|
}
|
|
136284
136301
|
return result;
|
|
136285
136302
|
}
|
|
136286
|
-
async function issueRelease(store, deps, task, target, ir2) {
|
|
136303
|
+
async function issueRelease(store, deps, task, target, ir2, options = {}) {
|
|
136287
136304
|
const targetColumn = findColumn2(ir2, target);
|
|
136288
136305
|
const targetIsProcessing = targetColumn ? resolveColumnFlags(targetColumn).countsTowardWip === true : false;
|
|
136289
|
-
if (targetIsProcessing && await isUnplannedForExecution(store, task, ir2)) {
|
|
136306
|
+
if (targetIsProcessing && !options.allowUnplanned && await isUnplannedForExecution(store, task, ir2)) {
|
|
136290
136307
|
schedulerLog.log(`Hold release for ${task.id} blocked \u2014 card is unplanned and cannot enter processing column ${target}`);
|
|
136291
136308
|
return false;
|
|
136292
136309
|
}
|
|
@@ -136328,7 +136345,7 @@ async function issueRelease(store, deps, task, target, ir2) {
|
|
|
136328
136345
|
return false;
|
|
136329
136346
|
}
|
|
136330
136347
|
}
|
|
136331
|
-
async function promoteHeldTask(store, taskId, deps = {}) {
|
|
136348
|
+
async function promoteHeldTask(store, taskId, deps = {}, options = {}) {
|
|
136332
136349
|
const task = await store.getTask(taskId);
|
|
136333
136350
|
if (!task) return { released: false, rejection: "task-not-found" };
|
|
136334
136351
|
const ir2 = await resolveWorkflowIrForTask(store, taskId);
|
|
@@ -136339,17 +136356,50 @@ async function promoteHeldTask(store, taskId, deps = {}) {
|
|
|
136339
136356
|
if (!target) return { released: false, rejection: "no-release-target" };
|
|
136340
136357
|
const targetColumn = findColumn2(ir2, target);
|
|
136341
136358
|
const targetIsProcessing = targetColumn ? resolveColumnFlags(targetColumn).countsTowardWip === true : false;
|
|
136342
|
-
|
|
136359
|
+
const unplanned = targetIsProcessing && await isUnplannedForExecution(store, task, ir2);
|
|
136360
|
+
if (unplanned && options.force !== true) {
|
|
136343
136361
|
return { released: false, rejection: "unplanned-for-execution", toColumn: target };
|
|
136344
136362
|
}
|
|
136363
|
+
let promoted = task;
|
|
136364
|
+
if (unplanned) {
|
|
136365
|
+
if (task.status === "needs-replan" || task.status === "plan-review-unavailable") {
|
|
136366
|
+
try {
|
|
136367
|
+
await store.updateTask(task.id, { status: null });
|
|
136368
|
+
promoted = { ...task, status: void 0 };
|
|
136369
|
+
} catch (error2) {
|
|
136370
|
+
schedulerLog.warn(
|
|
136371
|
+
`Force-promote for ${task.id} could not clear replan status: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
136372
|
+
);
|
|
136373
|
+
}
|
|
136374
|
+
}
|
|
136375
|
+
if (typeof store.recordRunAuditEvent === "function") {
|
|
136376
|
+
try {
|
|
136377
|
+
await store.recordRunAuditEvent({
|
|
136378
|
+
domain: "database",
|
|
136379
|
+
mutationType: "task:promote-forced-unplanned",
|
|
136380
|
+
target: task.id,
|
|
136381
|
+
taskId: task.id,
|
|
136382
|
+
agentId: "system",
|
|
136383
|
+
runId: `promote-force-${task.id}`,
|
|
136384
|
+
metadata: { fromColumn: task.column, toColumn: target, priorStatus: task.status ?? null }
|
|
136385
|
+
});
|
|
136386
|
+
} catch {
|
|
136387
|
+
}
|
|
136388
|
+
}
|
|
136389
|
+
schedulerLog.log(`Force-promote for ${task.id} bypassing unplanned gate into ${target} (operator override)`);
|
|
136390
|
+
}
|
|
136345
136391
|
const released = await issueRelease(
|
|
136346
136392
|
store,
|
|
136347
136393
|
{ now: () => Date.now(), reserveSlot: deps.reserveSlot, allocateWorktree: deps.allocateWorktree },
|
|
136348
|
-
|
|
136394
|
+
promoted,
|
|
136349
136395
|
target,
|
|
136350
|
-
ir2
|
|
136396
|
+
ir2,
|
|
136397
|
+
{ allowUnplanned: unplanned }
|
|
136351
136398
|
);
|
|
136352
|
-
|
|
136399
|
+
if (!released) {
|
|
136400
|
+
return { released: false, rejection: "capacity-exhausted-or-no-slot" };
|
|
136401
|
+
}
|
|
136402
|
+
return { released: true, toColumn: target, forcedUnplanned: unplanned || void 0 };
|
|
136353
136403
|
}
|
|
136354
136404
|
async function releaseHeldTaskByEvent(store, taskId, eventTag, deps = {}) {
|
|
136355
136405
|
const task = await store.getTask(taskId);
|
|
@@ -137273,8 +137323,8 @@ var require_graceful_fs = __commonJS({
|
|
|
137273
137323
|
fs10.createReadStream = createReadStream;
|
|
137274
137324
|
fs10.createWriteStream = createWriteStream;
|
|
137275
137325
|
var fs$readFile = fs10.readFile;
|
|
137276
|
-
fs10.readFile =
|
|
137277
|
-
function
|
|
137326
|
+
fs10.readFile = readFile41;
|
|
137327
|
+
function readFile41(path5, options, cb) {
|
|
137278
137328
|
if (typeof options === "function")
|
|
137279
137329
|
cb = options, options = null;
|
|
137280
137330
|
return go$readFile(path5, options, cb);
|
|
@@ -162576,6 +162626,12 @@ var init_usage_limit_detector = __esm({
|
|
|
162576
162626
|
});
|
|
162577
162627
|
|
|
162578
162628
|
// ../engine/src/transient-error-patterns.ts
|
|
162629
|
+
function isSessionContentionError(errorMessage) {
|
|
162630
|
+
if (!errorMessage || typeof errorMessage !== "string") {
|
|
162631
|
+
return false;
|
|
162632
|
+
}
|
|
162633
|
+
return SESSION_CONTENTION_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
162634
|
+
}
|
|
162579
162635
|
function isTransientError(errorMessage) {
|
|
162580
162636
|
if (!errorMessage || typeof errorMessage !== "string") {
|
|
162581
162637
|
return false;
|
|
@@ -162583,6 +162639,9 @@ function isTransientError(errorMessage) {
|
|
|
162583
162639
|
if (isTransientAuthCredentialError(errorMessage)) {
|
|
162584
162640
|
return true;
|
|
162585
162641
|
}
|
|
162642
|
+
if (isSessionContentionError(errorMessage)) {
|
|
162643
|
+
return true;
|
|
162644
|
+
}
|
|
162586
162645
|
return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
162587
162646
|
}
|
|
162588
162647
|
function isTransientAuthCredentialError(errorMessage) {
|
|
@@ -162594,7 +162653,7 @@ function isTransientAuthCredentialError(errorMessage) {
|
|
|
162594
162653
|
}
|
|
162595
162654
|
return TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN.test(errorMessage);
|
|
162596
162655
|
}
|
|
162597
|
-
var TRANSIENT_ERROR_PATTERNS, TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN, OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN;
|
|
162656
|
+
var TRANSIENT_ERROR_PATTERNS, SESSION_CONTENTION_PATTERNS, TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN, OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN;
|
|
162598
162657
|
var init_transient_error_patterns = __esm({
|
|
162599
162658
|
"../engine/src/transient-error-patterns.ts"() {
|
|
162600
162659
|
"use strict";
|
|
@@ -162655,6 +162714,14 @@ var init_transient_error_patterns = __esm({
|
|
|
162655
162714
|
/\bACP failed to start\b/i,
|
|
162656
162715
|
/\bACP session has no live connection\b/i
|
|
162657
162716
|
];
|
|
162717
|
+
SESSION_CONTENTION_PATTERNS = [
|
|
162718
|
+
// ActiveSessionPathHeldByForeignTaskError (active-session-registry.ts)
|
|
162719
|
+
/active-session path\s+\S+\s+is held by task\s+\S+/i,
|
|
162720
|
+
// WorkspaceRepoAcquireBusyError (worktree-acquisition.ts)
|
|
162721
|
+
/workspace sub-repo\s+\S+\s+acquisition is in progress for task\s+\S+/i,
|
|
162722
|
+
// WorkspaceRepoLandBusyError (merger-ai.ts)
|
|
162723
|
+
/workspace sub-repo\s+\S+\s+land is in progress for task\s+\S+/i
|
|
162724
|
+
];
|
|
162658
162725
|
TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN = /"type":\s*"authentication_error"|invalid authentication credentials|token[_\s]?expired/i;
|
|
162659
162726
|
OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN = /oauth token does not meet scope|insufficient[_\s-]?scope|invalid[_\s-]?scope|invalid (?:api[_\s-]?key|x-api-key)|missing\s+(?:\S+\s+)?(?:api[_\s-]?)?key|revoked|suspend(?:ed)?|disabled|deactivated|subscription|account (?:is )?(?:locked|closed|inactive)|access denied/i;
|
|
162660
162727
|
}
|
|
@@ -162663,6 +162730,9 @@ var init_transient_error_patterns = __esm({
|
|
|
162663
162730
|
// ../engine/src/transient-error-detector.ts
|
|
162664
162731
|
function isNonPlanDefectPlanReviewFailure(input) {
|
|
162665
162732
|
if (input.verdict === "REVISE") return false;
|
|
162733
|
+
if (input.failureValue?.trim().toLowerCase() === "session-contention-hold" || (input.errorMessage ? isSessionContentionError(input.errorMessage) : false)) {
|
|
162734
|
+
return false;
|
|
162735
|
+
}
|
|
162666
162736
|
const failureValue = input.failureValue?.trim().toLowerCase();
|
|
162667
162737
|
if (failureValue === "exception" || failureValue === "aborted") return true;
|
|
162668
162738
|
const errorMessage = input.errorMessage?.trim();
|
|
@@ -169753,6 +169823,86 @@ var init_research_step_runner = __esm({
|
|
|
169753
169823
|
}
|
|
169754
169824
|
});
|
|
169755
169825
|
|
|
169826
|
+
// ../engine/src/plan-artifact-writeback.ts
|
|
169827
|
+
import { readFile as readFile33 } from "node:fs/promises";
|
|
169828
|
+
import { join as join66 } from "node:path";
|
|
169829
|
+
function relativePromptPath(taskId) {
|
|
169830
|
+
return `.fusion/tasks/${taskId}/PROMPT.md`;
|
|
169831
|
+
}
|
|
169832
|
+
async function readIfPresent(path5) {
|
|
169833
|
+
try {
|
|
169834
|
+
return await readFile33(path5, "utf-8");
|
|
169835
|
+
} catch {
|
|
169836
|
+
return null;
|
|
169837
|
+
}
|
|
169838
|
+
}
|
|
169839
|
+
async function reconcileWorktreePlanArtifact(options) {
|
|
169840
|
+
const { store, taskId, rootDir, planningCwd, logger: logger3 } = options;
|
|
169841
|
+
const relPath = relativePromptPath(taskId);
|
|
169842
|
+
const rootContent = await readIfPresent(join66(rootDir, relPath));
|
|
169843
|
+
if (planningCwd === rootDir) {
|
|
169844
|
+
return { outcome: "not-worktree", content: rootContent ?? void 0 };
|
|
169845
|
+
}
|
|
169846
|
+
const worktreeContent = await readIfPresent(join66(planningCwd, relPath));
|
|
169847
|
+
if (worktreeContent === null) {
|
|
169848
|
+
return { outcome: "no-worktree-artifact", content: rootContent ?? void 0 };
|
|
169849
|
+
}
|
|
169850
|
+
if (worktreeContent.trim().length === 0) {
|
|
169851
|
+
return { outcome: "worktree-artifact-empty", content: rootContent ?? void 0 };
|
|
169852
|
+
}
|
|
169853
|
+
if (rootContent === worktreeContent) {
|
|
169854
|
+
return { outcome: "already-authoritative", content: rootContent };
|
|
169855
|
+
}
|
|
169856
|
+
try {
|
|
169857
|
+
await store.updateTask(taskId, { prompt: worktreeContent });
|
|
169858
|
+
logger3?.log?.(
|
|
169859
|
+
`${taskId}: recovered a worktree-local PROMPT.md into the project .fusion folder (${planningCwd})`
|
|
169860
|
+
);
|
|
169861
|
+
return { outcome: "recovered", content: worktreeContent };
|
|
169862
|
+
} catch (error2) {
|
|
169863
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
169864
|
+
logger3?.warn?.(
|
|
169865
|
+
`${taskId}: failed to copy the worktree-local PROMPT.md back into the project .fusion folder: ${message}`
|
|
169866
|
+
);
|
|
169867
|
+
return { outcome: "recovery-failed", content: rootContent ?? void 0 };
|
|
169868
|
+
}
|
|
169869
|
+
}
|
|
169870
|
+
async function mirrorPlanToProjectDb(store, taskId, content, options = {}) {
|
|
169871
|
+
if (content.trim().length === 0) return false;
|
|
169872
|
+
if (typeof store.upsertTaskDocument !== "function") return false;
|
|
169873
|
+
try {
|
|
169874
|
+
if (typeof store.getTaskDocument === "function") {
|
|
169875
|
+
const existing = await store.getTaskDocument(taskId, PLAN_DOCUMENT_KEY);
|
|
169876
|
+
if (typeof existing?.content === "string" && existing.content === content) return false;
|
|
169877
|
+
}
|
|
169878
|
+
await store.upsertTaskDocument(taskId, {
|
|
169879
|
+
key: PLAN_DOCUMENT_KEY,
|
|
169880
|
+
content,
|
|
169881
|
+
author: options.author ?? "agent"
|
|
169882
|
+
});
|
|
169883
|
+
return true;
|
|
169884
|
+
} catch (error2) {
|
|
169885
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
169886
|
+
options.logger?.warn?.(`${taskId}: failed to mirror the plan into the project database: ${message}`);
|
|
169887
|
+
return false;
|
|
169888
|
+
}
|
|
169889
|
+
}
|
|
169890
|
+
async function persistPlanArtifact(options) {
|
|
169891
|
+
const result = await reconcileWorktreePlanArtifact(options);
|
|
169892
|
+
const mirrored = result.content ? await mirrorPlanToProjectDb(options.store, options.taskId, result.content, {
|
|
169893
|
+
author: options.author,
|
|
169894
|
+
logger: options.logger
|
|
169895
|
+
}) : false;
|
|
169896
|
+
return { ...result, mirrored };
|
|
169897
|
+
}
|
|
169898
|
+
var PLAN_DOCUMENT_KEY;
|
|
169899
|
+
var init_plan_artifact_writeback = __esm({
|
|
169900
|
+
"../engine/src/plan-artifact-writeback.ts"() {
|
|
169901
|
+
"use strict";
|
|
169902
|
+
PLAN_DOCUMENT_KEY = "plan";
|
|
169903
|
+
}
|
|
169904
|
+
});
|
|
169905
|
+
|
|
169756
169906
|
// ../engine/src/auto-recovery-handlers/message-delivery.ts
|
|
169757
169907
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
169758
169908
|
var baseLog, MessageDeliveryAutoRecoveryHandler;
|
|
@@ -170780,9 +170930,9 @@ function assertInRootRepoRelPath(repoRelPath, sep12, isAbsolute22, normalize8) {
|
|
|
170780
170930
|
async function acquireWorkspaceRepoWorktree(opts) {
|
|
170781
170931
|
const { repoRelPath, workspaceRootDir, task, store, settings, logger: logger3, secretsStore, audit, runContext, runConfiguredCommand: runConfiguredCommand2, taskEnv } = opts;
|
|
170782
170932
|
const registry3 = opts.registry ?? activeSessionRegistry;
|
|
170783
|
-
const { join:
|
|
170933
|
+
const { join: join85, isAbsolute: isAbsolute22, normalize: normalize8, sep: sep12 } = await import("node:path");
|
|
170784
170934
|
assertInRootRepoRelPath(repoRelPath, sep12, isAbsolute22, normalize8);
|
|
170785
|
-
const repoAbsPath =
|
|
170935
|
+
const repoAbsPath = join85(workspaceRootDir, repoRelPath);
|
|
170786
170936
|
const existing = task.workspaceWorktrees?.[repoRelPath];
|
|
170787
170937
|
if (existing) {
|
|
170788
170938
|
let live = existsSync52(existing.worktreePath);
|
|
@@ -170971,11 +171121,11 @@ var init_worktree_acquisition = __esm({
|
|
|
170971
171121
|
});
|
|
170972
171122
|
|
|
170973
171123
|
// ../engine/src/agent-tools.ts
|
|
170974
|
-
import { appendFile as appendFile3, mkdir as mkdir26, readFile as
|
|
171124
|
+
import { appendFile as appendFile3, mkdir as mkdir26, readFile as readFile34, readdir as readdir9, realpath, stat as stat9, writeFile as writeFile25 } from "node:fs/promises";
|
|
170975
171125
|
import { existsSync as existsSync53 } from "node:fs";
|
|
170976
171126
|
import { createHash as createHash16, randomUUID as randomUUID45 } from "node:crypto";
|
|
170977
171127
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
170978
|
-
import { extname as extname3, isAbsolute as isAbsolute13, join as
|
|
171128
|
+
import { extname as extname3, isAbsolute as isAbsolute13, join as join67, relative as relative11, resolve as resolve32, sep as sep9 } from "node:path";
|
|
170979
171129
|
import { Type as Type2 } from "@earendil-works/pi-ai";
|
|
170980
171130
|
function sanitizeAgentMemoryId(agentId) {
|
|
170981
171131
|
return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
@@ -170987,16 +171137,16 @@ function agentDreamsDisplayPath(agentId) {
|
|
|
170987
171137
|
return `${AGENT_MEMORY_ROOT3}/${sanitizeAgentMemoryId(agentId)}/${AGENT_DREAMS_FILENAME2}`;
|
|
170988
171138
|
}
|
|
170989
171139
|
function agentMemoryDirectory(rootDir, agentId) {
|
|
170990
|
-
return
|
|
171140
|
+
return join67(rootDir, AGENT_MEMORY_ROOT3, sanitizeAgentMemoryId(agentId));
|
|
170991
171141
|
}
|
|
170992
171142
|
function agentMemoryFilePath(rootDir, agentId) {
|
|
170993
|
-
return
|
|
171143
|
+
return join67(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME2);
|
|
170994
171144
|
}
|
|
170995
171145
|
function agentDreamsFilePath(rootDir, agentId) {
|
|
170996
|
-
return
|
|
171146
|
+
return join67(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME2);
|
|
170997
171147
|
}
|
|
170998
171148
|
function agentDailyFilePath(rootDir, agentId, date5 = /* @__PURE__ */ new Date()) {
|
|
170999
|
-
return
|
|
171149
|
+
return join67(agentMemoryDirectory(rootDir, agentId), `${date5.toISOString().slice(0, 10)}.md`);
|
|
171000
171150
|
}
|
|
171001
171151
|
async function readAgentMemoryWorkspaceLongTerm(rootDir, agentId) {
|
|
171002
171152
|
const safeRoot = typeof rootDir === "string" ? rootDir.trim() : "";
|
|
@@ -171010,7 +171160,7 @@ async function readAgentMemoryWorkspaceLongTerm(rootDir, agentId) {
|
|
|
171010
171160
|
if (!fileStat.isFile()) {
|
|
171011
171161
|
return "";
|
|
171012
171162
|
}
|
|
171013
|
-
const content = await
|
|
171163
|
+
const content = await readFile34(filePath, "utf-8");
|
|
171014
171164
|
return typeof content === "string" ? content.trim() : "";
|
|
171015
171165
|
} catch {
|
|
171016
171166
|
return "";
|
|
@@ -171089,7 +171239,7 @@ async function listAgentMemoryFiles2(rootDir, agentMemory) {
|
|
|
171089
171239
|
}
|
|
171090
171240
|
for (const entry of entries) {
|
|
171091
171241
|
if (!DAILY_AGENT_MEMORY_RE2.test(entry)) continue;
|
|
171092
|
-
const absPath =
|
|
171242
|
+
const absPath = join67(dir, entry);
|
|
171093
171243
|
const fileStat = await stat9(absPath);
|
|
171094
171244
|
if (fileStat.isFile()) {
|
|
171095
171245
|
files.push({
|
|
@@ -171112,7 +171262,7 @@ async function searchAgentMemoryFile(rootDir, agentMemory, query, limit) {
|
|
|
171112
171262
|
}
|
|
171113
171263
|
const results = [];
|
|
171114
171264
|
for (const file of await listAgentMemoryFiles2(rootDir, agentMemory)) {
|
|
171115
|
-
const content = await
|
|
171265
|
+
const content = await readFile34(file.absPath, "utf-8");
|
|
171116
171266
|
const lines = content.split("\n");
|
|
171117
171267
|
for (let index2 = 0; index2 < lines.length; index2 += 8) {
|
|
171118
171268
|
const chunk = lines.slice(index2, index2 + 12).join("\n").trim();
|
|
@@ -171258,7 +171408,7 @@ function resolveAgentMemoryPath(rootDir, agentId, path5) {
|
|
|
171258
171408
|
return null;
|
|
171259
171409
|
}
|
|
171260
171410
|
return {
|
|
171261
|
-
absPath:
|
|
171411
|
+
absPath: join67(agentMemoryDirectory(rootDir, agentId), filename),
|
|
171262
171412
|
displayPath: `${prefix}${filename}`
|
|
171263
171413
|
};
|
|
171264
171414
|
}
|
|
@@ -171268,7 +171418,7 @@ async function getAgentMemoryWindow(rootDir, agentMemory, path5, startLine = 1,
|
|
|
171268
171418
|
return null;
|
|
171269
171419
|
}
|
|
171270
171420
|
await syncAgentMemoryFile(rootDir, agentMemory);
|
|
171271
|
-
const content = await
|
|
171421
|
+
const content = await readFile34(resolved.absPath, "utf-8");
|
|
171272
171422
|
const lines = content.split("\n");
|
|
171273
171423
|
const start = Math.max(1, Math.floor(startLine));
|
|
171274
171424
|
const count2 = Math.max(1, Math.min(Math.floor(lineCount), 200));
|
|
@@ -171918,6 +172068,9 @@ function createTaskPromptWriteTool(store, taskId, runContext) {
|
|
|
171918
172068
|
if (persisted?.prompt !== params.content) {
|
|
171919
172069
|
throw new Error("authoritative PROMPT.md read-back did not match the requested content; persistence could not be verified");
|
|
171920
172070
|
}
|
|
172071
|
+
await mirrorPlanToProjectDb(store, taskId, params.content, {
|
|
172072
|
+
author: runContext?.agentId ?? "agent"
|
|
172073
|
+
});
|
|
171921
172074
|
return {
|
|
171922
172075
|
content: [{ type: "text", text: `Updated PROMPT.md for ${taskId}.` }],
|
|
171923
172076
|
details: {}
|
|
@@ -172132,7 +172285,7 @@ async function readArtifactFileFromPath(params, baseDir) {
|
|
|
172132
172285
|
if (!mimeType) {
|
|
172133
172286
|
throw new Error(`Could not infer a MIME type from ${resolvedPath}; pass mimeType explicitly.`);
|
|
172134
172287
|
}
|
|
172135
|
-
const data = await
|
|
172288
|
+
const data = await readFile34(canonicalPath);
|
|
172136
172289
|
if (params.type === "image") {
|
|
172137
172290
|
if (!mimeType.startsWith("image/")) {
|
|
172138
172291
|
throw new Error(`image artifacts require an image/* mimeType, got ${mimeType}.`);
|
|
@@ -172475,25 +172628,33 @@ function createTaskPromoteTool(store, currentTaskId) {
|
|
|
172475
172628
|
return {
|
|
172476
172629
|
name: "fn_task_promote",
|
|
172477
172630
|
label: "Promote Held Task",
|
|
172478
|
-
description: "Manually promote a held task out of its hold column, releasing it regardless of the hold's release kind (the explicit operator action a 'manual' hold waits for). Defaults to the current task. Returns the destination column, or a rejection reason when the task is not held or the destination is full.",
|
|
172631
|
+
description: "Manually promote a held task out of its hold column, releasing it regardless of the hold's release kind (the explicit operator action a 'manual' hold waits for). Defaults to the current task. Returns the destination column, or a rejection reason when the task is not held or the destination is full. Pass force:true to start execution even when planning or plan review is still outstanding (that waives the plan gate and cancels the pending replan; capacity still applies).",
|
|
172479
172632
|
parameters: taskPromoteParams,
|
|
172480
172633
|
execute: async (_id, params) => {
|
|
172481
172634
|
const taskId = params.task_id?.trim() || currentTaskId;
|
|
172635
|
+
const force = params.force === true;
|
|
172482
172636
|
try {
|
|
172483
|
-
const outcome = await promoteHeldTask(store, taskId);
|
|
172637
|
+
const outcome = await promoteHeldTask(store, taskId, {}, { force });
|
|
172484
172638
|
if (outcome.released) {
|
|
172639
|
+
const forcedNote = outcome.forcedUnplanned ? " Forced past the outstanding planning/plan review \u2014 the pending replan was cancelled." : "";
|
|
172485
172640
|
return {
|
|
172486
172641
|
content: [{
|
|
172487
172642
|
type: "text",
|
|
172488
|
-
text: `Promoted ${taskId} to column '${outcome.toColumn}'
|
|
172643
|
+
text: `Promoted ${taskId} to column '${outcome.toColumn}'.${forcedNote}`
|
|
172489
172644
|
}],
|
|
172490
|
-
details: {
|
|
172645
|
+
details: {
|
|
172646
|
+
taskId,
|
|
172647
|
+
released: true,
|
|
172648
|
+
toColumn: outcome.toColumn,
|
|
172649
|
+
forcedUnplanned: outcome.forcedUnplanned === true
|
|
172650
|
+
}
|
|
172491
172651
|
};
|
|
172492
172652
|
}
|
|
172653
|
+
const forceHint = outcome.rejection === "unplanned-for-execution" ? " Pass force:true to start execution anyway." : "";
|
|
172493
172654
|
return {
|
|
172494
172655
|
content: [{
|
|
172495
172656
|
type: "text",
|
|
172496
|
-
text: `ERROR: Could not promote ${taskId}: ${outcome.rejection ?? "unknown"}
|
|
172657
|
+
text: `ERROR: Could not promote ${taskId}: ${outcome.rejection ?? "unknown"}.${forceHint}`
|
|
172497
172658
|
}],
|
|
172498
172659
|
details: { taskId, released: false, rejection: outcome.rejection },
|
|
172499
172660
|
isError: true
|
|
@@ -174676,6 +174837,7 @@ var init_agent_tools = __esm({
|
|
|
174676
174837
|
init_provider_registry();
|
|
174677
174838
|
init_research_step_runner();
|
|
174678
174839
|
init_logger();
|
|
174840
|
+
init_plan_artifact_writeback();
|
|
174679
174841
|
init_web_fetch();
|
|
174680
174842
|
init_agent_action_gate();
|
|
174681
174843
|
init_message_delivery();
|
|
@@ -174855,6 +175017,19 @@ var init_agent_tools = __esm({
|
|
|
174855
175017
|
taskPromoteParams = Type2.Object({
|
|
174856
175018
|
task_id: Type2.Optional(
|
|
174857
175019
|
Type2.String({ description: "Held task to promote. Defaults to the current task." })
|
|
175020
|
+
),
|
|
175021
|
+
/*
|
|
175022
|
+
FNXC:WorkflowScheduling 2026-07-25-05:40:
|
|
175023
|
+
Agent-native parity with the dashboard's force-promote override: an agent that
|
|
175024
|
+
has read the card and judged the pending replan / Plan Review not worth waiting
|
|
175025
|
+
for can start execution anyway. Opt-in per call and never defaulted on — the
|
|
175026
|
+
automatic surfaces (hold-release sweep, webhook release) still cannot force, so
|
|
175027
|
+
FN-7648 holds for everything that is not an explicit promote request.
|
|
175028
|
+
*/
|
|
175029
|
+
force: Type2.Optional(
|
|
175030
|
+
Type2.Boolean({
|
|
175031
|
+
description: "Start execution even when the task is still waiting on planning or plan review (rejection 'unplanned-for-execution'). Waives ONLY that gate \u2014 hold membership and downstream capacity still apply \u2014 and cancels the pending replan. Default false."
|
|
175032
|
+
})
|
|
174858
175033
|
)
|
|
174859
175034
|
});
|
|
174860
175035
|
workflowCreateParams = Type2.Object({
|
|
@@ -175239,8 +175414,8 @@ var init_agent_tools = __esm({
|
|
|
175239
175414
|
});
|
|
175240
175415
|
|
|
175241
175416
|
// ../engine/src/agent-memory-index.ts
|
|
175242
|
-
import { readFile as
|
|
175243
|
-
import { join as
|
|
175417
|
+
import { readFile as readFile35 } from "node:fs/promises";
|
|
175418
|
+
import { join as join68 } from "node:path";
|
|
175244
175419
|
function clampUtf8(input, maxBytes) {
|
|
175245
175420
|
if (Buffer.byteLength(input, "utf8") <= maxBytes) return input;
|
|
175246
175421
|
let out = "";
|
|
@@ -175272,7 +175447,7 @@ function parseHeadings(content) {
|
|
|
175272
175447
|
}
|
|
175273
175448
|
async function buildFileSection(sectionHeader, displayPath, fsPath) {
|
|
175274
175449
|
try {
|
|
175275
|
-
const content = await
|
|
175450
|
+
const content = await readFile35(fsPath, "utf-8");
|
|
175276
175451
|
const headings = parseHeadings(content);
|
|
175277
175452
|
const lines = [sectionHeader, `- ${displayPath}`];
|
|
175278
175453
|
for (const entry of headings) {
|
|
@@ -175288,9 +175463,9 @@ async function buildFileSection(sectionHeader, displayPath, fsPath) {
|
|
|
175288
175463
|
async function buildMemoryIndex({ rootDir, agentId }) {
|
|
175289
175464
|
const safeAgentId = agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
175290
175465
|
const agentDisplayPath = `.fusion/agent-memory/${safeAgentId}/MEMORY.md`;
|
|
175291
|
-
const agentFsPath =
|
|
175466
|
+
const agentFsPath = join68(rootDir, ".fusion", "agent-memory", safeAgentId, "MEMORY.md");
|
|
175292
175467
|
const projectDisplayPath = ".fusion/memory/MEMORY.md";
|
|
175293
|
-
const projectFsPath =
|
|
175468
|
+
const projectFsPath = join68(rootDir, ".fusion", "memory", "MEMORY.md");
|
|
175294
175469
|
const [agentSection, projectSection] = await Promise.all([
|
|
175295
175470
|
buildFileSection("## Agent Memory Index (use fn_memory_search / fn_memory_get to read)", agentDisplayPath, agentFsPath),
|
|
175296
175471
|
buildFileSection("## Project Memory Index", projectDisplayPath, projectFsPath)
|
|
@@ -175309,7 +175484,7 @@ var init_agent_memory_index = __esm({
|
|
|
175309
175484
|
});
|
|
175310
175485
|
|
|
175311
175486
|
// ../engine/src/agent-instructions.ts
|
|
175312
|
-
import { readFile as
|
|
175487
|
+
import { readFile as readFile36, writeFile as writeFile26, mkdir as mkdir27, access as access8 } from "node:fs/promises";
|
|
175313
175488
|
import { isAbsolute as isAbsolute14, resolve as resolve33, relative as relative12, normalize as normalize7, sep as sep10, dirname as dirname28 } from "node:path";
|
|
175314
175489
|
function trimAndClamp(value, maxLength, label, agentId) {
|
|
175315
175490
|
const trimmed = value.trim();
|
|
@@ -175370,7 +175545,7 @@ async function resolveAgentHeartbeatProcedure(agent, rootDir) {
|
|
|
175370
175545
|
return null;
|
|
175371
175546
|
}
|
|
175372
175547
|
try {
|
|
175373
|
-
const content = await
|
|
175548
|
+
const content = await readFile36(filePath, "utf-8");
|
|
175374
175549
|
const normalized = trimAndClamp(
|
|
175375
175550
|
content,
|
|
175376
175551
|
MAX_INSTRUCTIONS_TEXT_LENGTH2,
|
|
@@ -175496,7 +175671,7 @@ async function resolveAgentInstructions(agent, rootDir, ratingSummary, inclusion
|
|
|
175496
175671
|
const filePath = resolveValidatedInstructionsPath(agent.instructionsPath, rootDir, agent.id);
|
|
175497
175672
|
if (filePath) {
|
|
175498
175673
|
try {
|
|
175499
|
-
const content = await
|
|
175674
|
+
const content = await readFile36(filePath, "utf-8");
|
|
175500
175675
|
const normalizedContent = trimAndClamp(
|
|
175501
175676
|
content,
|
|
175502
175677
|
MAX_INSTRUCTIONS_TEXT_LENGTH2,
|
|
@@ -177329,10 +177504,10 @@ var init_verification_utils = __esm({
|
|
|
177329
177504
|
import { exec as exec19 } from "node:child_process";
|
|
177330
177505
|
import { createHash as createHash17 } from "node:crypto";
|
|
177331
177506
|
import { existsSync as existsSync54, readFileSync as readFileSync18, writeFileSync as writeFileSync7 } from "node:fs";
|
|
177332
|
-
import { join as
|
|
177507
|
+
import { join as join69 } from "node:path";
|
|
177333
177508
|
import { promisify as promisify24 } from "node:util";
|
|
177334
177509
|
function hasInstallState(rootDir) {
|
|
177335
|
-
return existsSync54(
|
|
177510
|
+
return existsSync54(join69(rootDir, "node_modules")) || existsSync54(join69(rootDir, ".pnp.cjs"));
|
|
177336
177511
|
}
|
|
177337
177512
|
function getConfiguredWorktreeInitCommand(settings) {
|
|
177338
177513
|
const trimmed = settings?.worktreeInitCommand?.trim();
|
|
@@ -177341,17 +177516,17 @@ function getConfiguredWorktreeInitCommand(settings) {
|
|
|
177341
177516
|
function getDependencySyncCommand(rootDir, settings) {
|
|
177342
177517
|
const configuredCommand = getConfiguredWorktreeInitCommand(settings);
|
|
177343
177518
|
if (configuredCommand) return configuredCommand;
|
|
177344
|
-
if (existsSync54(
|
|
177345
|
-
if (existsSync54(
|
|
177346
|
-
if (existsSync54(
|
|
177347
|
-
if (existsSync54(
|
|
177519
|
+
if (existsSync54(join69(rootDir, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
|
|
177520
|
+
if (existsSync54(join69(rootDir, "package-lock.json"))) return "npm install";
|
|
177521
|
+
if (existsSync54(join69(rootDir, "yarn.lock"))) return "yarn install --frozen-lockfile";
|
|
177522
|
+
if (existsSync54(join69(rootDir, "bun.lock")) || existsSync54(join69(rootDir, "bun.lockb"))) {
|
|
177348
177523
|
return "bun install --frozen-lockfile";
|
|
177349
177524
|
}
|
|
177350
177525
|
return null;
|
|
177351
177526
|
}
|
|
177352
177527
|
function computeLockfileHash(rootDir) {
|
|
177353
177528
|
for (const name2 of LOCKFILE_CANDIDATES) {
|
|
177354
|
-
const p2 =
|
|
177529
|
+
const p2 = join69(rootDir, name2);
|
|
177355
177530
|
if (existsSync54(p2)) {
|
|
177356
177531
|
try {
|
|
177357
177532
|
return createHash17("sha256").update(readFileSync18(p2)).digest("hex");
|
|
@@ -177364,7 +177539,7 @@ function computeLockfileHash(rootDir) {
|
|
|
177364
177539
|
}
|
|
177365
177540
|
function readInstallMarker(rootDir) {
|
|
177366
177541
|
try {
|
|
177367
|
-
const value = readFileSync18(
|
|
177542
|
+
const value = readFileSync18(join69(rootDir, INSTALL_MARKER_RELPATH), "utf-8").trim();
|
|
177368
177543
|
return value || null;
|
|
177369
177544
|
} catch {
|
|
177370
177545
|
return null;
|
|
@@ -177372,7 +177547,7 @@ function readInstallMarker(rootDir) {
|
|
|
177372
177547
|
}
|
|
177373
177548
|
function writeInstallMarker(rootDir, hash) {
|
|
177374
177549
|
try {
|
|
177375
|
-
writeFileSync7(
|
|
177550
|
+
writeFileSync7(join69(rootDir, INSTALL_MARKER_RELPATH), hash);
|
|
177376
177551
|
} catch {
|
|
177377
177552
|
}
|
|
177378
177553
|
}
|
|
@@ -177463,7 +177638,7 @@ var init_merge_dependency_sync = __esm({
|
|
|
177463
177638
|
"../engine/src/merge-dependency-sync.ts"() {
|
|
177464
177639
|
"use strict";
|
|
177465
177640
|
execAsync17 = promisify24(exec19);
|
|
177466
|
-
INSTALL_MARKER_RELPATH =
|
|
177641
|
+
INSTALL_MARKER_RELPATH = join69("node_modules", ".fusion-install-marker");
|
|
177467
177642
|
LOCKFILE_CANDIDATES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb", "bun.lock"];
|
|
177468
177643
|
INSTALL_TIMEOUT_MS = 3e5;
|
|
177469
177644
|
}
|
|
@@ -178084,7 +178259,7 @@ var init_merger_glob = __esm({
|
|
|
178084
178259
|
|
|
178085
178260
|
// ../engine/src/merger-workspace-test-commands.ts
|
|
178086
178261
|
import { existsSync as existsSync55, readdirSync as readdirSync9, readFileSync as readFileSync19 } from "node:fs";
|
|
178087
|
-
import { basename as basename18, dirname as dirname31, join as
|
|
178262
|
+
import { basename as basename18, dirname as dirname31, join as join70 } from "node:path";
|
|
178088
178263
|
import { execSync as execSync3 } from "node:child_process";
|
|
178089
178264
|
function quoteArg(value) {
|
|
178090
178265
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
@@ -178115,14 +178290,14 @@ function resolveWorkspacePackageRoots(rootDir, globs) {
|
|
|
178115
178290
|
for (const glob of globs) {
|
|
178116
178291
|
const starIdx = glob.indexOf("*");
|
|
178117
178292
|
if (starIdx === -1) {
|
|
178118
|
-
const candidate =
|
|
178119
|
-
if (existsSync55(
|
|
178293
|
+
const candidate = join70(rootDir, glob);
|
|
178294
|
+
if (existsSync55(join70(candidate, "package.json"))) {
|
|
178120
178295
|
roots.push(glob);
|
|
178121
178296
|
}
|
|
178122
178297
|
continue;
|
|
178123
178298
|
}
|
|
178124
178299
|
const prefix = glob.slice(0, starIdx);
|
|
178125
|
-
const parentDir =
|
|
178300
|
+
const parentDir = join70(rootDir, prefix.replace(/\/$/, ""));
|
|
178126
178301
|
let entries;
|
|
178127
178302
|
try {
|
|
178128
178303
|
entries = readdirSync9(parentDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
@@ -178131,8 +178306,8 @@ function resolveWorkspacePackageRoots(rootDir, globs) {
|
|
|
178131
178306
|
}
|
|
178132
178307
|
for (const entry of entries) {
|
|
178133
178308
|
const relPath = `${prefix.replace(/\/$/, "")}/${entry}`;
|
|
178134
|
-
const absPath =
|
|
178135
|
-
if (existsSync55(
|
|
178309
|
+
const absPath = join70(rootDir, relPath);
|
|
178310
|
+
if (existsSync55(join70(absPath, "package.json"))) {
|
|
178136
178311
|
roots.push(relPath);
|
|
178137
178312
|
}
|
|
178138
178313
|
}
|
|
@@ -178155,7 +178330,7 @@ function mapChangedFilesToPackageNames(changedFiles, packageRoots, rootDir) {
|
|
|
178155
178330
|
}
|
|
178156
178331
|
if (bestRoot !== null) {
|
|
178157
178332
|
try {
|
|
178158
|
-
const pkgJsonPath =
|
|
178333
|
+
const pkgJsonPath = join70(rootDir, bestRoot, "package.json");
|
|
178159
178334
|
const raw = readFileSync19(pkgJsonPath, "utf-8");
|
|
178160
178335
|
const parsed = JSON.parse(raw);
|
|
178161
178336
|
if (parsed.name) {
|
|
@@ -178172,7 +178347,7 @@ function packageNamesForFiles(rootDir, files) {
|
|
|
178172
178347
|
if (files.length === 0) return [];
|
|
178173
178348
|
let workspaceContent;
|
|
178174
178349
|
try {
|
|
178175
|
-
workspaceContent = readFileSync19(
|
|
178350
|
+
workspaceContent = readFileSync19(join70(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
178176
178351
|
} catch {
|
|
178177
178352
|
return [];
|
|
178178
178353
|
}
|
|
@@ -178183,7 +178358,7 @@ function packageNamesForFiles(rootDir, files) {
|
|
|
178183
178358
|
return mapChangedFilesToPackageNames(files, packageRoots, rootDir);
|
|
178184
178359
|
}
|
|
178185
178360
|
function deriveScopedPnpmTestCommand(rootDir, baseBranch, branch) {
|
|
178186
|
-
const workspacePath =
|
|
178361
|
+
const workspacePath = join70(rootDir, "pnpm-workspace.yaml");
|
|
178187
178362
|
let workspaceContent;
|
|
178188
178363
|
try {
|
|
178189
178364
|
workspaceContent = readFileSync19(workspacePath, "utf-8");
|
|
@@ -178221,7 +178396,7 @@ function deriveScopedPnpmTestCommand(rootDir, baseBranch, branch) {
|
|
|
178221
178396
|
function deriveFileScopedPnpmTestCommand(rootDir, baseBranch, branch) {
|
|
178222
178397
|
let workspaceContent;
|
|
178223
178398
|
try {
|
|
178224
|
-
workspaceContent = readFileSync19(
|
|
178399
|
+
workspaceContent = readFileSync19(join70(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
178225
178400
|
} catch {
|
|
178226
178401
|
return null;
|
|
178227
178402
|
}
|
|
@@ -178263,7 +178438,7 @@ function deriveFileScopedPnpmTestCommand(rootDir, baseBranch, branch) {
|
|
|
178263
178438
|
];
|
|
178264
178439
|
for (const candidate of candidates) {
|
|
178265
178440
|
const normalized = candidate.startsWith("./") ? candidate.slice(2) : candidate;
|
|
178266
|
-
if (existsSync55(
|
|
178441
|
+
if (existsSync55(join70(rootDir, normalized))) {
|
|
178267
178442
|
resolvedTests.add(normalized);
|
|
178268
178443
|
}
|
|
178269
178444
|
}
|
|
@@ -178291,7 +178466,7 @@ function deriveFileScopedPnpmTestCommand(rootDir, baseBranch, branch) {
|
|
|
178291
178466
|
let name2 = bestRoot;
|
|
178292
178467
|
try {
|
|
178293
178468
|
const parsed = JSON.parse(
|
|
178294
|
-
readFileSync19(
|
|
178469
|
+
readFileSync19(join70(rootDir, bestRoot, "package.json"), "utf-8")
|
|
178295
178470
|
);
|
|
178296
178471
|
if (parsed.name) name2 = parsed.name;
|
|
178297
178472
|
} catch {
|
|
@@ -178335,8 +178510,8 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
|
|
|
178335
178510
|
};
|
|
178336
178511
|
}
|
|
178337
178512
|
const buildSource = explicitBuildCommand?.trim() ? "explicit" : void 0;
|
|
178338
|
-
if (existsSync55(
|
|
178339
|
-
if (existsSync55(
|
|
178513
|
+
if (existsSync55(join70(rootDir, "pnpm-lock.yaml"))) {
|
|
178514
|
+
if (existsSync55(join70(rootDir, "pnpm-workspace.yaml"))) {
|
|
178340
178515
|
if (baseBranch?.trim() && branch?.trim()) {
|
|
178341
178516
|
try {
|
|
178342
178517
|
const scoped = deriveScopedPnpmTestCommand(rootDir, baseBranch.trim(), branch.trim());
|
|
@@ -178355,13 +178530,13 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
|
|
|
178355
178530
|
}
|
|
178356
178531
|
return { command: "pnpm test", testSource: "inferred", buildSource };
|
|
178357
178532
|
}
|
|
178358
|
-
if (existsSync55(
|
|
178533
|
+
if (existsSync55(join70(rootDir, "yarn.lock"))) {
|
|
178359
178534
|
return { command: "yarn test", testSource: "inferred", buildSource };
|
|
178360
178535
|
}
|
|
178361
|
-
if (existsSync55(
|
|
178536
|
+
if (existsSync55(join70(rootDir, "bun.lock")) || existsSync55(join70(rootDir, "bun.lockb"))) {
|
|
178362
178537
|
return { command: "bun test", testSource: "inferred", buildSource };
|
|
178363
178538
|
}
|
|
178364
|
-
if (existsSync55(
|
|
178539
|
+
if (existsSync55(join70(rootDir, "package-lock.json"))) {
|
|
178365
178540
|
return { command: "npm test", testSource: "inferred", buildSource };
|
|
178366
178541
|
}
|
|
178367
178542
|
return null;
|
|
@@ -181353,7 +181528,7 @@ var init_merger_ref_update_advance = __esm({
|
|
|
181353
181528
|
import { execFile as execFile21, spawn as spawn7 } from "node:child_process";
|
|
181354
181529
|
import { promisify as promisify39 } from "node:util";
|
|
181355
181530
|
import { copyFileSync, existsSync as existsSync56, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync20, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
181356
|
-
import { dirname as dirname32, join as
|
|
181531
|
+
import { dirname as dirname32, join as join71 } from "node:path";
|
|
181357
181532
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
181358
181533
|
async function runGit4(args, cwd, timeoutMs) {
|
|
181359
181534
|
const result = await execFileAsync14("git", ["-c", "core.quotePath=false", ...args], {
|
|
@@ -181439,9 +181614,9 @@ async function syncWorktreeToHead(input) {
|
|
|
181439
181614
|
if (mode === "ff-only") {
|
|
181440
181615
|
return { kind: "skipped-dirty", fromSha: previousSha, reason: "ff-only-mode-requires-clean-tree", dirtyFiles, untrackedFiles };
|
|
181441
181616
|
}
|
|
181442
|
-
const stageDir = mkdtempSync2(
|
|
181443
|
-
const patchPath =
|
|
181444
|
-
const untrackedDir =
|
|
181617
|
+
const stageDir = mkdtempSync2(join71(tmpdir7(), "fusion-worktree-sync-"));
|
|
181618
|
+
const patchPath = join71(stageDir, "edits.patch");
|
|
181619
|
+
const untrackedDir = join71(stageDir, "untracked");
|
|
181445
181620
|
let preserveStageDir = false;
|
|
181446
181621
|
try {
|
|
181447
181622
|
mkdirSync9(untrackedDir, { recursive: true });
|
|
@@ -181461,8 +181636,8 @@ async function syncWorktreeToHead(input) {
|
|
|
181461
181636
|
}
|
|
181462
181637
|
}
|
|
181463
181638
|
for (const rel of untrackedFiles) {
|
|
181464
|
-
const src =
|
|
181465
|
-
const dst =
|
|
181639
|
+
const src = join71(worktreePath, rel);
|
|
181640
|
+
const dst = join71(untrackedDir, rel);
|
|
181466
181641
|
try {
|
|
181467
181642
|
mkdirSync9(dirname32(dst), { recursive: true });
|
|
181468
181643
|
copyFileSync(src, dst);
|
|
@@ -181525,13 +181700,13 @@ async function syncWorktreeToHead(input) {
|
|
|
181525
181700
|
const restored = [];
|
|
181526
181701
|
const untrackedSkippedAsTracked = [];
|
|
181527
181702
|
for (const rel of untrackedFiles) {
|
|
181528
|
-
const src =
|
|
181703
|
+
const src = join71(untrackedDir, rel);
|
|
181529
181704
|
if (!existsSync56(src)) continue;
|
|
181530
181705
|
if (trackedAtHead.has(rel)) {
|
|
181531
181706
|
untrackedSkippedAsTracked.push(rel);
|
|
181532
181707
|
continue;
|
|
181533
181708
|
}
|
|
181534
|
-
const dst =
|
|
181709
|
+
const dst = join71(worktreePath, rel);
|
|
181535
181710
|
try {
|
|
181536
181711
|
mkdirSync9(dirname32(dst), { recursive: true });
|
|
181537
181712
|
const data = readFileSync20(src);
|
|
@@ -181600,8 +181775,8 @@ var init_worktree_ref_sync = __esm({
|
|
|
181600
181775
|
|
|
181601
181776
|
// ../engine/src/merger-scope-auto-widen.ts
|
|
181602
181777
|
import { exec as exec28 } from "node:child_process";
|
|
181603
|
-
import { readFile as
|
|
181604
|
-
import { join as
|
|
181778
|
+
import { readFile as readFile37, writeFile as writeFile27 } from "node:fs/promises";
|
|
181779
|
+
import { join as join72 } from "node:path";
|
|
181605
181780
|
import { promisify as promisify40 } from "node:util";
|
|
181606
181781
|
function quoteArg4(value) {
|
|
181607
181782
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -181740,8 +181915,8 @@ function parseScopeEntries(section) {
|
|
|
181740
181915
|
async function appendAutoWidenedScopeToPrompt(params) {
|
|
181741
181916
|
const { store, taskId, files } = params;
|
|
181742
181917
|
if (files.length === 0) return [];
|
|
181743
|
-
const promptPath =
|
|
181744
|
-
const prompt = await
|
|
181918
|
+
const promptPath = join72(store.getTaskDir(taskId), "PROMPT.md");
|
|
181919
|
+
const prompt = await readFile37(promptPath, "utf-8");
|
|
181745
181920
|
const { before, section, after } = splitPromptSections(prompt);
|
|
181746
181921
|
const existing = parseScopeEntries(section);
|
|
181747
181922
|
const toAdd = files.filter((file) => !existing.has(file));
|
|
@@ -181897,7 +182072,7 @@ import * as childProcess4 from "node:child_process";
|
|
|
181897
182072
|
import { promisify as promisify41 } from "node:util";
|
|
181898
182073
|
import { existsSync as existsSync57, readFileSync as readFileSync21, writeFileSync as writeFileSync9, unlinkSync as unlinkSync3, renameSync as renameSync5 } from "node:fs";
|
|
181899
182074
|
import { createHash as createHash19 } from "node:crypto";
|
|
181900
|
-
import { join as
|
|
182075
|
+
import { join as join73, resolve as resolve36 } from "node:path";
|
|
181901
182076
|
import { hostname as hostname2 } from "node:os";
|
|
181902
182077
|
import { Type as Type3 } from "typebox";
|
|
181903
182078
|
function mergerCommitEnv() {
|
|
@@ -182210,7 +182385,7 @@ async function runDeterministicVerification(store, rootDir, taskId, testCommand,
|
|
|
182210
182385
|
const deterministicVerificationMessage = "Running deterministic merge verification" + (hasTestCommand ? ` (test${testSourceDisplayLabel}: ${normalizedTestCommand})` : "") + (hasBuildCommand ? ` (build${buildSource === "inferred" ? " [inferred]" : ""}: ${normalizedBuildCommand})` : "");
|
|
182211
182386
|
await store.logEntry(taskId, deterministicVerificationMessage);
|
|
182212
182387
|
await store.appendAgentLog(taskId, deterministicVerificationMessage, "status", void 0, "merger");
|
|
182213
|
-
const bootstrapScriptPath =
|
|
182388
|
+
const bootstrapScriptPath = join73(rootDir, "scripts/ensure-test-artifacts.mjs");
|
|
182214
182389
|
if (hasTestCommand || hasBuildCommand) {
|
|
182215
182390
|
if (!existsSync57(bootstrapScriptPath)) {
|
|
182216
182391
|
const bootstrapMissingMessage = `${taskId}: [verification:bootstrap] script missing at scripts/ensure-test-artifacts.mjs \u2014 skipping preamble`;
|
|
@@ -182709,7 +182884,7 @@ async function stashTreesEqual(rootDir, aSha, bSha) {
|
|
|
182709
182884
|
}
|
|
182710
182885
|
function writeActiveMergerStatus(rootDir, taskId) {
|
|
182711
182886
|
try {
|
|
182712
|
-
const statusPath =
|
|
182887
|
+
const statusPath = join73(rootDir, ".git", ACTIVE_MERGER_STATUS_FILENAME);
|
|
182713
182888
|
const payload = {
|
|
182714
182889
|
taskId,
|
|
182715
182890
|
pid: process.pid,
|
|
@@ -182738,7 +182913,7 @@ function clearActiveMergerStatus(statusPath, taskId) {
|
|
|
182738
182913
|
}
|
|
182739
182914
|
function readActiveMergerStatus(rootDir) {
|
|
182740
182915
|
try {
|
|
182741
|
-
const statusPath =
|
|
182916
|
+
const statusPath = join73(rootDir, ".git", ACTIVE_MERGER_STATUS_FILENAME);
|
|
182742
182917
|
if (!existsSync57(statusPath)) return null;
|
|
182743
182918
|
const raw = readFileSync21(statusPath, "utf-8");
|
|
182744
182919
|
const parsed = JSON.parse(raw);
|
|
@@ -183311,7 +183486,7 @@ async function findFilesWithConflictMarkers(rootDir, files) {
|
|
|
183311
183486
|
const stillConflicted = [];
|
|
183312
183487
|
for (const file of files) {
|
|
183313
183488
|
try {
|
|
183314
|
-
const fullPath =
|
|
183489
|
+
const fullPath = join73(rootDir, file);
|
|
183315
183490
|
if (!existsSync57(fullPath)) continue;
|
|
183316
183491
|
const { stdout } = await execAsync27(
|
|
183317
183492
|
`git grep -l -e "^<<<<<<< " -e "^=======$" -e "^>>>>>>> " --no-index -- ${quoteArg2(fullPath)}`,
|
|
@@ -183338,7 +183513,7 @@ async function tryRecoverHardFailApply(params) {
|
|
|
183338
183513
|
if (!patchText.trim()) {
|
|
183339
183514
|
mergerLog.warn(`${taskId}: autostash ${sha.slice(0, 7)} produced empty patch; cannot 3-way recover`);
|
|
183340
183515
|
} else {
|
|
183341
|
-
const patchPath =
|
|
183516
|
+
const patchPath = join73(rootDir, ".git", `fusion-autostash-${sha.slice(0, 7)}.patch`);
|
|
183342
183517
|
writeFileSync9(patchPath, patchText, "utf-8");
|
|
183343
183518
|
try {
|
|
183344
183519
|
await execAsync27(`git apply --3way --whitespace=nowarn ${quoteArg2(patchPath)}`, { cwd: rootDir });
|
|
@@ -184826,13 +185001,13 @@ async function isCherryPickInProgress(rootDir) {
|
|
|
184826
185001
|
cwd: rootDir,
|
|
184827
185002
|
encoding: "utf-8"
|
|
184828
185003
|
});
|
|
184829
|
-
const cherryPickHeadPath =
|
|
185004
|
+
const cherryPickHeadPath = join73(rootDir, cherryPickHeadPathOut.trim());
|
|
184830
185005
|
if (existsSync57(cherryPickHeadPath)) return true;
|
|
184831
185006
|
const { stdout: sequencerPathOut } = await execAsync27("git rev-parse --git-path sequencer", {
|
|
184832
185007
|
cwd: rootDir,
|
|
184833
185008
|
encoding: "utf-8"
|
|
184834
185009
|
});
|
|
184835
|
-
const sequencerPath =
|
|
185010
|
+
const sequencerPath = join73(rootDir, sequencerPathOut.trim());
|
|
184836
185011
|
return existsSync57(sequencerPath);
|
|
184837
185012
|
} catch {
|
|
184838
185013
|
return false;
|
|
@@ -189874,7 +190049,7 @@ var agent_reflection_exports = {};
|
|
|
189874
190049
|
__export(agent_reflection_exports, {
|
|
189875
190050
|
AgentReflectionService: () => AgentReflectionService
|
|
189876
190051
|
});
|
|
189877
|
-
import { readFile as
|
|
190052
|
+
import { readFile as readFile40 } from "node:fs/promises";
|
|
189878
190053
|
import { isAbsolute as isAbsolute20, resolve as resolve38 } from "node:path";
|
|
189879
190054
|
var reflectionLog, REFLECTION_SYSTEM_PROMPT, DEFAULT_OUTCOME_LIMIT, AgentReflectionService;
|
|
189880
190055
|
var init_agent_reflection = __esm({
|
|
@@ -190433,7 +190608,7 @@ Rules:
|
|
|
190433
190608
|
if (agent.instructionsPath?.trim()) {
|
|
190434
190609
|
const resolvedPath = isAbsolute20(agent.instructionsPath) ? agent.instructionsPath : resolve38(this.rootDir, agent.instructionsPath);
|
|
190435
190610
|
try {
|
|
190436
|
-
const content = await
|
|
190611
|
+
const content = await readFile40(resolvedPath, "utf-8");
|
|
190437
190612
|
if (content.trim()) {
|
|
190438
190613
|
pieces.push(content.trim());
|
|
190439
190614
|
}
|
|
@@ -191347,7 +191522,7 @@ function hasAdvancedPastPlanning(task) {
|
|
|
191347
191522
|
if (task.column === "in-progress" || task.column === "in-review" || task.column === "done" || task.column === "archived") {
|
|
191348
191523
|
return true;
|
|
191349
191524
|
}
|
|
191350
|
-
if (task.
|
|
191525
|
+
if (task.firstExecutionAt != null || task.executionStartedAt != null) {
|
|
191351
191526
|
return true;
|
|
191352
191527
|
}
|
|
191353
191528
|
if (task.column === "triage") {
|
|
@@ -194147,9 +194322,9 @@ var Scheduler = class {
|
|
|
194147
194322
|
import { exec as exec37, execFile as execFile26, execSync as execSync7 } from "node:child_process";
|
|
194148
194323
|
import { promisify as promisify52 } from "node:util";
|
|
194149
194324
|
import { setImmediate as setImmediateCb3 } from "node:timers";
|
|
194150
|
-
import { basename as basename19, delimiter as delimiter2, isAbsolute as isAbsolute18, join as
|
|
194325
|
+
import { basename as basename19, delimiter as delimiter2, isAbsolute as isAbsolute18, join as join79, relative as relative16, resolve as resolvePath3 } from "node:path";
|
|
194151
194326
|
import { existsSync as existsSync65, lstatSync as lstatSync3, realpathSync as realpathSync9 } from "node:fs";
|
|
194152
|
-
import { readFile as
|
|
194327
|
+
import { readFile as readFile38, rm as rm12, writeFile as writeFile29 } from "node:fs/promises";
|
|
194153
194328
|
init_src2();
|
|
194154
194329
|
|
|
194155
194330
|
// ../engine/src/auto-merge-finalization.ts
|
|
@@ -194544,6 +194719,7 @@ init_src2();
|
|
|
194544
194719
|
// ../engine/src/workflow-graph-executor.ts
|
|
194545
194720
|
init_src2();
|
|
194546
194721
|
init_transient_error_detector();
|
|
194722
|
+
init_transient_error_patterns();
|
|
194547
194723
|
|
|
194548
194724
|
// ../engine/src/required-workflow-artifacts.ts
|
|
194549
194725
|
var REQUIRED_ARTIFACT_MISSING_PREFIX = "required-artifact-missing:";
|
|
@@ -194599,16 +194775,16 @@ async function persistBranchState(persistence, state) {
|
|
|
194599
194775
|
);
|
|
194600
194776
|
}
|
|
194601
194777
|
}
|
|
194602
|
-
function resolveJoinConfig(
|
|
194603
|
-
const rawMode =
|
|
194778
|
+
function resolveJoinConfig(join85) {
|
|
194779
|
+
const rawMode = join85.config?.mode;
|
|
194604
194780
|
let mode = "all";
|
|
194605
194781
|
if (rawMode === "all" || rawMode === "any") mode = rawMode;
|
|
194606
194782
|
else if (rawMode && typeof rawMode === "object" && "quorum" in rawMode) {
|
|
194607
194783
|
const n = rawMode.quorum;
|
|
194608
194784
|
if (typeof n === "number" && Number.isInteger(n) && n > 0) mode = { quorum: n };
|
|
194609
|
-
else throw new WorkflowIrError(`join '${
|
|
194785
|
+
else throw new WorkflowIrError(`join '${join85.id}' quorum must be a positive integer`);
|
|
194610
194786
|
}
|
|
194611
|
-
const rawFail =
|
|
194787
|
+
const rawFail = join85.config?.onBranchFailure;
|
|
194612
194788
|
const onBranchFailure = rawFail === "collect" ? "collect" : "fail-fast";
|
|
194613
194789
|
return { mode, onBranchFailure };
|
|
194614
194790
|
}
|
|
@@ -194624,9 +194800,9 @@ async function runSplitJoin(split, env) {
|
|
|
194624
194800
|
if (branchEdges.length === 0) {
|
|
194625
194801
|
throw new WorkflowIrError(`split '${split.id}' has no traversable branches`);
|
|
194626
194802
|
}
|
|
194627
|
-
const
|
|
194628
|
-
if (!
|
|
194629
|
-
const joinConfig = resolveJoinConfig(env.nodeMap.get(
|
|
194803
|
+
const join85 = findMatchingJoin(branchEdges[0].to, env);
|
|
194804
|
+
if (!join85) throw new WorkflowIrError(`split '${split.id}' has no reachable matching join`);
|
|
194805
|
+
const joinConfig = resolveJoinConfig(env.nodeMap.get(join85));
|
|
194630
194806
|
const controller = new AbortController();
|
|
194631
194807
|
const branchCount = branchEdges.length;
|
|
194632
194808
|
const required2 = requiredCompletions(joinConfig.mode, branchCount);
|
|
@@ -194662,7 +194838,7 @@ async function runSplitJoin(split, env) {
|
|
|
194662
194838
|
};
|
|
194663
194839
|
const branchPromises = branchEdges.map((edge) => {
|
|
194664
194840
|
const branchId = edge.to;
|
|
194665
|
-
return walkBranch(branchId,
|
|
194841
|
+
return walkBranch(branchId, join85, env, controller.signal, visitedNodeIds).then((result) => {
|
|
194666
194842
|
branchOutcomes.push({ branchId, outcome: result.outcome, nodeId: result.lastNodeId });
|
|
194667
194843
|
if (result.outcome === "success") succeeded += 1;
|
|
194668
194844
|
else failed += 1;
|
|
@@ -194680,7 +194856,7 @@ async function runSplitJoin(split, env) {
|
|
|
194680
194856
|
});
|
|
194681
194857
|
const outcome = await joinReached;
|
|
194682
194858
|
await Promise.allSettled(branchPromises);
|
|
194683
|
-
return { joinNodeId:
|
|
194859
|
+
return { joinNodeId: join85, outcome, branchOutcomes, visitedNodeIds };
|
|
194684
194860
|
}
|
|
194685
194861
|
async function walkBranch(startNodeId, joinId, env, signal, visitedNodeIds) {
|
|
194686
194862
|
let currentId = startNodeId;
|
|
@@ -195622,6 +195798,7 @@ function workflowNodeRequiresWorktree(node, { optionalGroupId, reviewerInlineFix
|
|
|
195622
195798
|
// ../engine/src/workflow-graph-executor.ts
|
|
195623
195799
|
var PLAN_REVIEW_PROVIDER_FAILURE_HOLD_VALUE = "plan-review-provider-failure-hold";
|
|
195624
195800
|
var PLAN_REVIEW_LEASE_HELD_VALUE = "plan-review-lease-held";
|
|
195801
|
+
var SESSION_CONTENTION_HOLD_VALUE = "session-contention-hold";
|
|
195625
195802
|
var WORKFLOW_INTERRUPTED_NODE_ID_CONTEXT_KEY = "workflow:interruptedNodeId";
|
|
195626
195803
|
var WORKFLOW_INTERRUPTED_NODE_ABORT_KIND_CONTEXT_KEY = "workflow:interruptedNodeAbortKind";
|
|
195627
195804
|
var WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY = "workflow:optionalGroupActive";
|
|
@@ -195991,6 +196168,15 @@ var WorkflowGraphExecutor = class {
|
|
|
195991
196168
|
};
|
|
195992
196169
|
context[`node:${node.id}:outcome`] = result2.outcome;
|
|
195993
196170
|
if (result2.value !== void 0) context[`node:${node.id}:value`] = result2.value;
|
|
196171
|
+
const sessionContentionFailure = stepStatus === "failed" && (verdictRaw === SESSION_CONTENTION_HOLD_VALUE || isSessionContentionError(stepOutput ?? stepNotes ?? ""));
|
|
196172
|
+
if (sessionContentionFailure) {
|
|
196173
|
+
this.deps.logTaskEntry?.(
|
|
196174
|
+
`${logPrefix} ${groupName} is waiting on another task to release its session path \u2014 holding in place`
|
|
196175
|
+
);
|
|
196176
|
+
context[`node:${node.id}:outcome`] = "failure";
|
|
196177
|
+
context[`node:${node.id}:value`] = SESSION_CONTENTION_HOLD_VALUE;
|
|
196178
|
+
return { outcome: "failure", value: SESSION_CONTENTION_HOLD_VALUE };
|
|
196179
|
+
}
|
|
195994
196180
|
const nonPlanDefectPlanReviewFailure = node.id === PLAN_REVIEW_GROUP_ID && stepStatus === "failed" && (isRequiredArtifactReadFailedValue(verdictRaw) || isNonPlanDefectPlanReviewFailure({
|
|
195995
196181
|
verdict,
|
|
195996
196182
|
errorMessage: stepOutput ?? stepNotes,
|
|
@@ -196374,6 +196560,7 @@ var WorkflowGraphExecutor = class {
|
|
|
196374
196560
|
} catch (error2) {
|
|
196375
196561
|
if (signal?.aborted) return this.withEnginePauseAbortContext(node, { outcome: "failure", value: "aborted" });
|
|
196376
196562
|
lastError = error2;
|
|
196563
|
+
if (isSessionContentionError(error2 instanceof Error ? error2.message : String(error2))) break;
|
|
196377
196564
|
}
|
|
196378
196565
|
}
|
|
196379
196566
|
if (signal?.aborted) {
|
|
@@ -196392,11 +196579,13 @@ var WorkflowGraphExecutor = class {
|
|
|
196392
196579
|
}
|
|
196393
196580
|
return degraded;
|
|
196394
196581
|
}
|
|
196582
|
+
const lastErrorText = lastError instanceof Error ? lastError.message : String(lastError);
|
|
196395
196583
|
const failureResult = {
|
|
196396
196584
|
outcome: "failure",
|
|
196397
|
-
|
|
196585
|
+
// FNXC:SessionContention 2026-07-25-21:30: contention is a retryable hold, not an exception.
|
|
196586
|
+
value: isSessionContentionError(lastErrorText) ? SESSION_CONTENTION_HOLD_VALUE : "exception",
|
|
196398
196587
|
contextPatch: {
|
|
196399
|
-
[`node:${node.id}:error`]:
|
|
196588
|
+
[`node:${node.id}:error`]: lastErrorText
|
|
196400
196589
|
}
|
|
196401
196590
|
};
|
|
196402
196591
|
if (recordProgress && this.shouldRecordNodeProgress(node)) {
|
|
@@ -198050,11 +198239,11 @@ init_active_session_registry();
|
|
|
198050
198239
|
// ../engine/src/cli-agent/task-session.ts
|
|
198051
198240
|
import { mkdtemp as mkdtemp3, rm as rm10 } from "node:fs/promises";
|
|
198052
198241
|
import { tmpdir as tmpdir8 } from "node:os";
|
|
198053
|
-
import { join as
|
|
198242
|
+
import { join as join75 } from "node:path";
|
|
198054
198243
|
|
|
198055
198244
|
// ../engine/src/cli-agent/hook-scripts.ts
|
|
198056
198245
|
import { mkdir as mkdir28, rm as rm9, writeFile as writeFile28 } from "node:fs/promises";
|
|
198057
|
-
import { join as
|
|
198246
|
+
import { join as join74 } from "node:path";
|
|
198058
198247
|
var HOOK_SCRIPT_NAMES = {
|
|
198059
198248
|
/** Main hook script: POSTs stdin payload (with the hook event name) to the route. */
|
|
198060
198249
|
hook: "fusion-hook.sh",
|
|
@@ -198136,8 +198325,8 @@ exit 0
|
|
|
198136
198325
|
async function writeSessionHookScripts(opts) {
|
|
198137
198326
|
const { sessionId, token, endpointUrl, dir } = opts;
|
|
198138
198327
|
await mkdir28(dir, { recursive: true, mode: 448 });
|
|
198139
|
-
const hookScriptPath =
|
|
198140
|
-
const notifyScriptPath =
|
|
198328
|
+
const hookScriptPath = join74(dir, HOOK_SCRIPT_NAMES.hook);
|
|
198329
|
+
const notifyScriptPath = join74(dir, HOOK_SCRIPT_NAMES.notify);
|
|
198141
198330
|
await writeFile28(hookScriptPath, buildHookScriptContent({ sessionId, token, endpointUrl }), {
|
|
198142
198331
|
mode: 448
|
|
198143
198332
|
});
|
|
@@ -198313,9 +198502,9 @@ var CliTaskSession = class _CliTaskSession {
|
|
|
198313
198502
|
isApproved: opts.isAutonomyApproved ?? (() => false)
|
|
198314
198503
|
});
|
|
198315
198504
|
const root = opts.hookDirRoot ?? tmpdir8();
|
|
198316
|
-
const hookDir = await mkdtemp3(
|
|
198317
|
-
const hookScriptPath =
|
|
198318
|
-
const settingsPath =
|
|
198505
|
+
const hookDir = await mkdtemp3(join75(root, "fusion-cli-hooks-"));
|
|
198506
|
+
const hookScriptPath = join75(hookDir, HOOK_SCRIPT_NAMES.hook);
|
|
198507
|
+
const settingsPath = join75(hookDir, "settings.json");
|
|
198319
198508
|
const cliAgentSettings = opts.config.cliAgentSettings ?? null;
|
|
198320
198509
|
const operatorEnvAdditions = (cliAgentSettings?.envAdditions ?? []).filter(
|
|
198321
198510
|
(k2) => !/^FUSION_/i.test(k2)
|
|
@@ -198568,7 +198757,7 @@ function killLiveTaskSessions(taskId, manager, store) {
|
|
|
198568
198757
|
// ../engine/src/pty-native.ts
|
|
198569
198758
|
import * as fs7 from "node:fs";
|
|
198570
198759
|
import { createRequire as createRequire7 } from "node:module";
|
|
198571
|
-
import { join as
|
|
198760
|
+
import { join as join76, dirname as dirname33 } from "node:path";
|
|
198572
198761
|
var isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
|
|
198573
198762
|
var ptyModule = null;
|
|
198574
198763
|
var ptyLoadError = null;
|
|
@@ -198582,12 +198771,12 @@ function findInstalledNodePtyNativeDir() {
|
|
|
198582
198771
|
try {
|
|
198583
198772
|
const packageJsonPath = require4.resolve("node-pty/package.json");
|
|
198584
198773
|
const pkgRoot = dirname33(packageJsonPath);
|
|
198585
|
-
const releaseDir =
|
|
198586
|
-
if (fs7.existsSync(
|
|
198774
|
+
const releaseDir = join76(pkgRoot, "build", "Release");
|
|
198775
|
+
if (fs7.existsSync(join76(releaseDir, "pty.node"))) {
|
|
198587
198776
|
return releaseDir;
|
|
198588
198777
|
}
|
|
198589
|
-
const prebuildDir =
|
|
198590
|
-
if (fs7.existsSync(
|
|
198778
|
+
const prebuildDir = join76(pkgRoot, "prebuilds", getNativePrebuildName());
|
|
198779
|
+
if (fs7.existsSync(join76(prebuildDir, "pty.node"))) {
|
|
198591
198780
|
return prebuildDir;
|
|
198592
198781
|
}
|
|
198593
198782
|
return null;
|
|
@@ -198598,14 +198787,14 @@ function findInstalledNodePtyNativeDir() {
|
|
|
198598
198787
|
function findStagedNativeDir() {
|
|
198599
198788
|
const prebuildName = getNativePrebuildName();
|
|
198600
198789
|
if (process.env.FUSION_RUNTIME_DIR) {
|
|
198601
|
-
const envPath =
|
|
198602
|
-
if (fs7.existsSync(
|
|
198790
|
+
const envPath = join76(process.env.FUSION_RUNTIME_DIR, prebuildName);
|
|
198791
|
+
if (fs7.existsSync(join76(envPath, "pty.node"))) {
|
|
198603
198792
|
return envPath;
|
|
198604
198793
|
}
|
|
198605
198794
|
}
|
|
198606
198795
|
const execDir = dirname33(process.execPath);
|
|
198607
|
-
const nextToBinary =
|
|
198608
|
-
if (fs7.existsSync(
|
|
198796
|
+
const nextToBinary = join76(execDir, "runtime", prebuildName);
|
|
198797
|
+
if (fs7.existsSync(join76(nextToBinary, "pty.node"))) {
|
|
198609
198798
|
return nextToBinary;
|
|
198610
198799
|
}
|
|
198611
198800
|
return null;
|
|
@@ -198628,8 +198817,8 @@ function ensureNodePtyNativePermissions() {
|
|
|
198628
198817
|
candidateDirs.add(installedNativeDir);
|
|
198629
198818
|
}
|
|
198630
198819
|
for (const nativeDir of candidateDirs) {
|
|
198631
|
-
const helperPath =
|
|
198632
|
-
const nativeModulePath =
|
|
198820
|
+
const helperPath = join76(nativeDir, "spawn-helper");
|
|
198821
|
+
const nativeModulePath = join76(nativeDir, "pty.node");
|
|
198633
198822
|
try {
|
|
198634
198823
|
fs7.chmodSync(helperPath, 493);
|
|
198635
198824
|
} catch {
|
|
@@ -198659,7 +198848,7 @@ async function loadPtyModule() {
|
|
|
198659
198848
|
process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
|
|
198660
198849
|
}
|
|
198661
198850
|
process.env.FUSION_NATIVE_ASSETS_PATH = nativeDir;
|
|
198662
|
-
const nativePath =
|
|
198851
|
+
const nativePath = join76(nativeDir, "pty.node");
|
|
198663
198852
|
if (fs7.existsSync(nativePath)) {
|
|
198664
198853
|
try {
|
|
198665
198854
|
const nativeModule = { exports: {} };
|
|
@@ -201779,7 +201968,7 @@ import { promisify as promisify49 } from "node:util";
|
|
|
201779
201968
|
import { setImmediate as setImmediateCb2 } from "node:timers";
|
|
201780
201969
|
import { existsSync as existsSync63, mkdirSync as mkdirSync10, readdirSync as readdirSync10, readFileSync as readFileSync22, realpathSync as realpathSync8, rmSync as rmSync8, statSync as statSync12, writeFileSync as writeFileSync10 } from "node:fs";
|
|
201781
201970
|
import { tmpdir as tmpdir9 } from "node:os";
|
|
201782
|
-
import { isAbsolute as isAbsolute15, join as
|
|
201971
|
+
import { isAbsolute as isAbsolute15, join as join77, relative as relative13, resolve as resolve37 } from "node:path";
|
|
201783
201972
|
init_transient_error_detector();
|
|
201784
201973
|
|
|
201785
201974
|
// ../engine/src/auto-claim-snapshot.ts
|
|
@@ -207202,9 +207391,9 @@ async function preserveWorktreeChanges(repoDir, worktreePath, taskId) {
|
|
|
207202
207391
|
return null;
|
|
207203
207392
|
}
|
|
207204
207393
|
const diff = (await execAsync32("git diff HEAD --binary", { cwd: worktreePath, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 })).stdout;
|
|
207205
|
-
const recoveryDir =
|
|
207394
|
+
const recoveryDir = join77(repoDir, ".fusion", "recovery");
|
|
207206
207395
|
mkdirSync10(recoveryDir, { recursive: true });
|
|
207207
|
-
const patchPath =
|
|
207396
|
+
const patchPath = join77(recoveryDir, `${taskId.toLowerCase()}-${formatRecoveryTimestamp()}.patch`);
|
|
207208
207397
|
writeFileSync10(patchPath, diff, "utf-8");
|
|
207209
207398
|
return patchPath;
|
|
207210
207399
|
} catch (error2) {
|
|
@@ -207212,6 +207401,7 @@ async function preserveWorktreeChanges(repoDir, worktreePath, taskId) {
|
|
|
207212
207401
|
return null;
|
|
207213
207402
|
}
|
|
207214
207403
|
}
|
|
207404
|
+
var PRE_EXECUTION_WORKTREE_MAX_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
207215
207405
|
var APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
|
|
207216
207406
|
var STARVED_REFINEMENT_RECOVERY_GRACE_MS = 10 * 6e4;
|
|
207217
207407
|
var STARVED_PEER_PROGRESS_THRESHOLD = 3;
|
|
@@ -208569,6 +208759,35 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
208569
208759
|
}
|
|
208570
208760
|
return false;
|
|
208571
208761
|
}
|
|
208762
|
+
/**
|
|
208763
|
+
* FNXC:WorkflowRecovery 2026-07-25-09:40:
|
|
208764
|
+
* Single shared decision for "is this branch tip's foreign `Fusion-Task-Id`/`Fusion-Task-Lineage` trailer real
|
|
208765
|
+
* evidence of cross-task contamination, or just an inherited start point?"
|
|
208766
|
+
*
|
|
208767
|
+
* A task branch cut from the integration branch points at whatever landed last. When the task has not committed
|
|
208768
|
+
* anything of its own (planning aborted, spec revise, operator move back to `todo`), the branch tip IS that
|
|
208769
|
+
* previous task's commit — a foreign trailer with zero foreign intent. FN-1406 hit exactly this: `fusion/fn-1406`
|
|
208770
|
+
* was cut from `main` at FN-1401's landed commit, planning ended without a commit, and the reclaim sweep logged
|
|
208771
|
+
* `already-merged rejected ... owner=FN-1401 reason=foreign-task-tip` every pass instead of clearing the stale
|
|
208772
|
+
* worktree/branch metadata.
|
|
208773
|
+
*
|
|
208774
|
+
* Merge-base-to-tip diff proof separates the two: no unique diff means the branch carries no task content, so the
|
|
208775
|
+
* foreign trailer is inherited and the caller may treat the row as stale metadata. The one exception is when the
|
|
208776
|
+
* base ALREADY has an explicit commit for this task — then the branch should have been sitting on the task's own
|
|
208777
|
+
* landed commit, and a foreign tip is genuine misbinding worth rejecting.
|
|
208778
|
+
*
|
|
208779
|
+
* Callers: already-merged recovery (`branchTipForeignOwnership`), branch-misbound recovery
|
|
208780
|
+
* (`isBranchTipMisboundToTask`), and the self-owned-branch reclaim sweep's `tip-already-merged` arm. Keeping the
|
|
208781
|
+
* three on one helper is the point — the reclaim arm drifted without the diff proof and that was the bug.
|
|
208782
|
+
*/
|
|
208783
|
+
async foreignTipRejection(input) {
|
|
208784
|
+
const { taskId, lineageId, branchTip, baseBranch, ownership } = input;
|
|
208785
|
+
if (ownership.rejectionReason !== "foreign-task" && ownership.rejectionReason !== "foreign-lineage") return null;
|
|
208786
|
+
const hasNoUniqueDiff = await this.branchHasNoUniqueDiff(branchTip, baseBranch).catch(() => false);
|
|
208787
|
+
const baseAlreadyHasCurrentTask = hasNoUniqueDiff ? await this.baseHasExplicitTaskOwnership(taskId, lineageId, baseBranch).catch(() => false) : false;
|
|
208788
|
+
if (hasNoUniqueDiff && !baseAlreadyHasCurrentTask) return null;
|
|
208789
|
+
return ownership.rejectionReason === "foreign-task" ? { reason: "foreign-task-tip", owner: ownership.ownerTaskId } : { reason: "foreign-lineage-tip", owner: ownership.ownerLineageId };
|
|
208790
|
+
}
|
|
208572
208791
|
async rejectForeignAlreadyMergedCandidate(input) {
|
|
208573
208792
|
const { task, candidateSha, candidateOwner, taskBranch, baseBranch, reason, phase } = input;
|
|
208574
208793
|
await this.store.logEntry(
|
|
@@ -208610,20 +208829,14 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
208610
208829
|
}
|
|
208611
208830
|
const sha = stdout.trim();
|
|
208612
208831
|
if (!sha) return null;
|
|
208613
|
-
const hasNoUniqueDiff = await this.branchHasNoUniqueDiff(sha, baseBranch).catch(() => false);
|
|
208614
208832
|
let ownership;
|
|
208615
208833
|
try {
|
|
208616
208834
|
ownership = await this.readCommitTaskOwnership(sha, taskId, lineageId);
|
|
208617
208835
|
} catch {
|
|
208618
208836
|
return { sha, reason: "ownership-unverifiable" };
|
|
208619
208837
|
}
|
|
208620
|
-
const
|
|
208621
|
-
if (
|
|
208622
|
-
return { sha, owner: ownership.ownerTaskId, reason: "foreign-task-tip" };
|
|
208623
|
-
}
|
|
208624
|
-
if ((!hasNoUniqueDiff || baseAlreadyHasCurrentTask) && ownership.rejectionReason === "foreign-lineage") {
|
|
208625
|
-
return { sha, owner: ownership.ownerLineageId, reason: "foreign-lineage-tip" };
|
|
208626
|
-
}
|
|
208838
|
+
const rejection = await this.foreignTipRejection({ taskId, lineageId, branchTip: sha, baseBranch, ownership });
|
|
208839
|
+
if (rejection) return { sha, owner: rejection.owner, reason: rejection.reason };
|
|
208627
208840
|
return null;
|
|
208628
208841
|
}
|
|
208629
208842
|
async resolveSelfHealingMergeTarget(task, settings, phase) {
|
|
@@ -208970,6 +209183,8 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
208970
209183
|
{ name: "reconcile-workspace-partial-lands", fn: () => this.reconcileWorkspacePartialLands() },
|
|
208971
209184
|
{ name: "reclaim-phantom-workspace-land-leases", fn: () => this.reclaimPhantomWorkspaceLandLeases() },
|
|
208972
209185
|
{ name: "reconcile-orphaned-workspace-worktrees", fn: () => this.reconcileOrphanedWorkspaceWorktrees() },
|
|
209186
|
+
// FNXC:PlanningEvacuation 2026-07-25-23:00: reclaim worktrees acquired at planning time by cards that never executed.
|
|
209187
|
+
{ name: "reconcile-pre-execution-worktrees", fn: () => this.reconcilePreExecutionWorktrees() },
|
|
208973
209188
|
{ name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
|
|
208974
209189
|
{ name: "recover-already-merged-review", fn: () => this.recoverAlreadyMergedReviewTasks() },
|
|
208975
209190
|
{ name: "recover-post-done-noncontinuable-wedge", fn: () => this.recoverPostDoneNonContinuableWedge() },
|
|
@@ -209837,14 +210052,21 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
209837
210052
|
if (!ownership) {
|
|
209838
210053
|
continue;
|
|
209839
210054
|
}
|
|
209840
|
-
|
|
210055
|
+
const foreignRejection = await this.foreignTipRejection({
|
|
210056
|
+
taskId: task.id,
|
|
210057
|
+
lineageId: task.lineageId,
|
|
210058
|
+
branchTip: inspection.tipSha,
|
|
210059
|
+
baseBranch: inspection.integrationRef,
|
|
210060
|
+
ownership
|
|
210061
|
+
});
|
|
210062
|
+
if (foreignRejection) {
|
|
209841
210063
|
await this.rejectForeignAlreadyMergedCandidate({
|
|
209842
210064
|
task,
|
|
209843
210065
|
candidateSha: inspection.tipSha,
|
|
209844
|
-
candidateOwner:
|
|
210066
|
+
candidateOwner: foreignRejection.owner,
|
|
209845
210067
|
taskBranch: branchName,
|
|
209846
210068
|
baseBranch: inspection.integrationRef,
|
|
209847
|
-
reason:
|
|
210069
|
+
reason: foreignRejection.reason,
|
|
209848
210070
|
phase: "tip-already-merged"
|
|
209849
210071
|
});
|
|
209850
210072
|
continue;
|
|
@@ -213784,7 +214006,7 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
213784
214006
|
const unrecoverableRepos = [];
|
|
213785
214007
|
for (const repoRel of repoKeys) {
|
|
213786
214008
|
const entry = workspaceWorktrees[repoRel];
|
|
213787
|
-
const repoRootDir =
|
|
214009
|
+
const repoRootDir = join77(this.options.rootDir, repoRel);
|
|
213788
214010
|
let integrationBranch;
|
|
213789
214011
|
try {
|
|
213790
214012
|
integrationBranch = await resolveIntegrationBranch(
|
|
@@ -213996,6 +214218,58 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
213996
214218
|
return 0;
|
|
213997
214219
|
}
|
|
213998
214220
|
}
|
|
214221
|
+
/*
|
|
214222
|
+
FNXC:PlanningEvacuation 2026-07-25-23:00 (pre-execution worktree sweep):
|
|
214223
|
+
Planning acquires the task's own worktree, so cards that never reach execution would accumulate
|
|
214224
|
+
worktrees on disk: withdrawn to an intake column, archived from a planner lane, or simply parked.
|
|
214225
|
+
This sweep reclaims them.
|
|
214226
|
+
|
|
214227
|
+
Candidates are addressed from task ROWS (never a directory walk — AGENTS.md forbids unbounded temp
|
|
214228
|
+
scans): tasks holding `worktree` that sit in a non-executing column and carry no execution
|
|
214229
|
+
timestamp. `todo` is deliberately EXCLUDED: a planned card waiting for a WIP slot legitimately
|
|
214230
|
+
keeps its worktree, and churning it would just force a re-acquire minutes later. Every real safety
|
|
214231
|
+
decision (live session, clean branch, execution evidence) belongs to
|
|
214232
|
+
`TaskExecutor.releasePreExecutionWorktree`, so this sweep cannot diverge from the event-driven path
|
|
214233
|
+
that runs on the move itself.
|
|
214234
|
+
*/
|
|
214235
|
+
async reconcilePreExecutionWorktrees() {
|
|
214236
|
+
try {
|
|
214237
|
+
const release2 = this.options.releasePreExecutionWorktree;
|
|
214238
|
+
if (!release2) return 0;
|
|
214239
|
+
const settings = await this.store.getSettings();
|
|
214240
|
+
if (settings.globalPause || settings.enginePaused) return 0;
|
|
214241
|
+
const now = Date.now();
|
|
214242
|
+
const parked = await this.store.listTasks({ slim: true });
|
|
214243
|
+
const candidates = parked.filter((task) => {
|
|
214244
|
+
if (!task.worktree || task.deletedAt) return false;
|
|
214245
|
+
if (task.firstExecutionAt || task.executionStartedAt) return false;
|
|
214246
|
+
if (task.column === "todo" || task.column === "in-progress" || task.column === "in-review" || task.column === "done") return false;
|
|
214247
|
+
if (task.paused || task.userPaused) return false;
|
|
214248
|
+
if (task.status != null) return false;
|
|
214249
|
+
if (task.blockedBy || task.overlapBlockedBy || task.nextRecoveryAt) return false;
|
|
214250
|
+
const lastTouchedMs = Math.max(
|
|
214251
|
+
Date.parse(task.columnMovedAt ?? "") || 0,
|
|
214252
|
+
Date.parse(task.updatedAt ?? "") || 0
|
|
214253
|
+
);
|
|
214254
|
+
if (!lastTouchedMs) return false;
|
|
214255
|
+
return now - lastTouchedMs >= PRE_EXECUTION_WORKTREE_MAX_IDLE_MS;
|
|
214256
|
+
});
|
|
214257
|
+
if (candidates.length === 0) return 0;
|
|
214258
|
+
let released = 0;
|
|
214259
|
+
for (const task of candidates) {
|
|
214260
|
+
try {
|
|
214261
|
+
if (await release2(task.id, `parked pre-execution in '${task.column}'`)) released++;
|
|
214262
|
+
} catch (err) {
|
|
214263
|
+
log28.warn(`reconcilePreExecutionWorktrees: release failed for ${task.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
214264
|
+
}
|
|
214265
|
+
}
|
|
214266
|
+
if (released > 0) log28.log(`reconcilePreExecutionWorktrees: released ${released} pre-execution worktree(s)`);
|
|
214267
|
+
return released;
|
|
214268
|
+
} catch (err) {
|
|
214269
|
+
log28.error(`reconcilePreExecutionWorktrees sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
214270
|
+
return 0;
|
|
214271
|
+
}
|
|
214272
|
+
}
|
|
213999
214273
|
/*
|
|
214000
214274
|
FNXC:Workspace 2026-06-22-09:30 (Phase D U1, KTD4 — per-repo worktree cleanup from STORED paths):
|
|
214001
214275
|
For done/dead workspace tasks, remove each recorded per-repo worktree. The paths are ADDRESSABLE
|
|
@@ -214026,7 +214300,7 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
214026
214300
|
if ((this.orphanWorktreeRemovalFailures.get(worktreePath) ?? 0) >= MAX_STARVATION_DROPS) {
|
|
214027
214301
|
continue;
|
|
214028
214302
|
}
|
|
214029
|
-
const repoRootDir =
|
|
214303
|
+
const repoRootDir = join77(this.options.rootDir, repoRel);
|
|
214030
214304
|
let success = false;
|
|
214031
214305
|
let reason = "removed";
|
|
214032
214306
|
try {
|
|
@@ -214947,7 +215221,7 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
214947
215221
|
if (testWorkerRoot) {
|
|
214948
215222
|
try {
|
|
214949
215223
|
for (const entry of readdirSync10(testWorkerRoot)) {
|
|
214950
|
-
if (entry.startsWith("redir-")) roots.push(
|
|
215224
|
+
if (entry.startsWith("redir-")) roots.push(join77(testWorkerRoot, entry));
|
|
214951
215225
|
}
|
|
214952
215226
|
} catch {
|
|
214953
215227
|
}
|
|
@@ -214961,7 +215235,7 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
214961
215235
|
} catch {
|
|
214962
215236
|
continue;
|
|
214963
215237
|
}
|
|
214964
|
-
for (const entry of entries) paths2.push(
|
|
215238
|
+
for (const entry of entries) paths2.push(join77(root, entry));
|
|
214965
215239
|
}
|
|
214966
215240
|
return paths2;
|
|
214967
215241
|
}
|
|
@@ -215197,14 +215471,10 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
215197
215471
|
maxBuffer: 1024 * 1024
|
|
215198
215472
|
});
|
|
215199
215473
|
const branchTip = tipOut.trim();
|
|
215200
|
-
const hasNoUniqueDiff = await this.branchHasNoUniqueDiff(branchTip, baseBranch).catch(() => false);
|
|
215201
215474
|
const ownership = await this.readCommitTaskOwnership(branchTip, taskId, lineageId);
|
|
215202
|
-
const
|
|
215203
|
-
if (
|
|
215204
|
-
return { misbound: false, branchTip, landed: null, rejection
|
|
215205
|
-
}
|
|
215206
|
-
if ((!hasNoUniqueDiff || baseAlreadyHasCurrentTask) && ownership.rejectionReason === "foreign-lineage") {
|
|
215207
|
-
return { misbound: false, branchTip, landed: null, rejection: { reason: "foreign-lineage-tip", owner: ownership.ownerLineageId } };
|
|
215475
|
+
const rejection = await this.foreignTipRejection({ taskId, lineageId, branchTip, baseBranch, ownership });
|
|
215476
|
+
if (rejection) {
|
|
215477
|
+
return { misbound: false, branchTip, landed: null, rejection };
|
|
215208
215478
|
}
|
|
215209
215479
|
const hasTaskId = ownership.ownerTaskId === taskId;
|
|
215210
215480
|
const hasLineage = lineageId ? ownership.ownerLineageId === lineageId : false;
|
|
@@ -216866,7 +217136,7 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
216866
217136
|
let processedMarkers = 0;
|
|
216867
217137
|
for (const task of candidates) {
|
|
216868
217138
|
try {
|
|
216869
|
-
const promptPath =
|
|
217139
|
+
const promptPath = join77(this.options.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
|
216870
217140
|
if (!existsSync63(promptPath)) {
|
|
216871
217141
|
continue;
|
|
216872
217142
|
}
|
|
@@ -217253,7 +217523,7 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
217253
217523
|
if (!existsSync63(worktreesDir)) return 0;
|
|
217254
217524
|
let dirs;
|
|
217255
217525
|
try {
|
|
217256
|
-
dirs = readdirSync10(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !isAiMergeContainerDir(e.name)).map((e) =>
|
|
217526
|
+
dirs = readdirSync10(worktreesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !isAiMergeContainerDir(e.name)).map((e) => join77(worktreesDir, e.name));
|
|
217257
217527
|
} catch (err) {
|
|
217258
217528
|
log28.warn(`Failed to read .worktrees/ for unregistered orphan reap: ${err instanceof Error ? err.message : String(err)}`);
|
|
217259
217529
|
return 0;
|
|
@@ -217323,7 +217593,7 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
217323
217593
|
}
|
|
217324
217594
|
if (entries.length === 0) continue;
|
|
217325
217595
|
for (const entry of entries) {
|
|
217326
|
-
const path5 =
|
|
217596
|
+
const path5 = join77(tempRoot, entry);
|
|
217327
217597
|
let canonicalPath = path5;
|
|
217328
217598
|
let cleanupReason = "stale";
|
|
217329
217599
|
try {
|
|
@@ -217715,7 +217985,7 @@ init_src2();
|
|
|
217715
217985
|
init_logger();
|
|
217716
217986
|
init_verification_concurrency();
|
|
217717
217987
|
import { existsSync as existsSync64, readdirSync as readdirSync11, readFileSync as readFileSync23 } from "node:fs";
|
|
217718
|
-
import { isAbsolute as isAbsolute16, join as
|
|
217988
|
+
import { isAbsolute as isAbsolute16, join as join78, relative as relative14 } from "node:path";
|
|
217719
217989
|
import { Type as Type5 } from "@earendil-works/pi-ai";
|
|
217720
217990
|
var MAX_OUTPUT_BYTES = 200 * 1024;
|
|
217721
217991
|
var QUIET_HEARTBEAT_INTERVAL_MS = 6e4;
|
|
@@ -217840,7 +218110,7 @@ function findWorkspacePackageDir(rootDir, packageName) {
|
|
|
217840
218110
|
for (const candidate of candidates) {
|
|
217841
218111
|
if (!candidate) continue;
|
|
217842
218112
|
try {
|
|
217843
|
-
const pkg = JSON.parse(readFileSync23(
|
|
218113
|
+
const pkg = JSON.parse(readFileSync23(join78(rootDir, candidate, "package.json"), "utf8"));
|
|
217844
218114
|
if (pkg.name === packageName) {
|
|
217845
218115
|
packageDirCache.set(cacheKey, candidate);
|
|
217846
218116
|
return candidate;
|
|
@@ -217855,7 +218125,7 @@ function findWorkspacePackageDir(rootDir, packageName) {
|
|
|
217855
218125
|
while (queue.length > 0) {
|
|
217856
218126
|
const current = queue.shift();
|
|
217857
218127
|
if (!current) break;
|
|
217858
|
-
const abs =
|
|
218128
|
+
const abs = join78(rootDir, current.dir);
|
|
217859
218129
|
let entries;
|
|
217860
218130
|
try {
|
|
217861
218131
|
entries = readdirSync11(abs, { withFileTypes: true });
|
|
@@ -217865,9 +218135,9 @@ function findWorkspacePackageDir(rootDir, packageName) {
|
|
|
217865
218135
|
for (const entry of entries) {
|
|
217866
218136
|
if (!entry.isDirectory()) continue;
|
|
217867
218137
|
if (entry.name === "node_modules" || entry.name === "dist") continue;
|
|
217868
|
-
const child =
|
|
218138
|
+
const child = join78(current.dir, entry.name);
|
|
217869
218139
|
try {
|
|
217870
|
-
const pkg = JSON.parse(readFileSync23(
|
|
218140
|
+
const pkg = JSON.parse(readFileSync23(join78(rootDir, child, "package.json"), "utf8"));
|
|
217871
218141
|
if (pkg.name === packageName) {
|
|
217872
218142
|
packageDirCache.set(cacheKey, child);
|
|
217873
218143
|
return child;
|
|
@@ -217887,7 +218157,7 @@ function toPackageRelativeFilter(token, rootDir, packageDir) {
|
|
|
217887
218157
|
return normalized.slice(normalizedPackageDir.length + 1);
|
|
217888
218158
|
}
|
|
217889
218159
|
if (isAbsolute16(token)) {
|
|
217890
|
-
const rel = relative14(
|
|
218160
|
+
const rel = relative14(join78(rootDir, packageDir), token).replace(/\\/g, "/");
|
|
217891
218161
|
if (!rel.startsWith("../") && rel !== "..") return rel;
|
|
217892
218162
|
}
|
|
217893
218163
|
return token;
|
|
@@ -218175,7 +218445,7 @@ function createRunVerificationTool(opts) {
|
|
|
218175
218445
|
if (params.cwd && isAbsolute16(params.cwd)) {
|
|
218176
218446
|
resolvedCwd = params.cwd;
|
|
218177
218447
|
} else if (params.cwd) {
|
|
218178
|
-
resolvedCwd =
|
|
218448
|
+
resolvedCwd = join78(worktreePath, params.cwd);
|
|
218179
218449
|
} else {
|
|
218180
218450
|
resolvedCwd = worktreePath;
|
|
218181
218451
|
}
|
|
@@ -218208,7 +218478,7 @@ function createRunVerificationTool(opts) {
|
|
|
218208
218478
|
warnings.push(...normalized.warnings);
|
|
218209
218479
|
}
|
|
218210
218480
|
if (effectiveCommand.trimStart().startsWith("pnpm --filter")) {
|
|
218211
|
-
const modulesYaml =
|
|
218481
|
+
const modulesYaml = join78(rootDir, "node_modules", ".modules.yaml");
|
|
218212
218482
|
if (!existsSync64(modulesYaml)) {
|
|
218213
218483
|
const installCmd = "pnpm install --prefer-offline";
|
|
218214
218484
|
const msg = `node_modules/.modules.yaml not found in workspace root \u2014 auto-prepending \`${installCmd}\` before running the command.`;
|
|
@@ -218498,7 +218768,7 @@ function isWorkflowStepSkillDiscoverable(skillName, additionalSkillPaths, ceSkil
|
|
|
218498
218768
|
return false;
|
|
218499
218769
|
}
|
|
218500
218770
|
return (additionalSkillPaths ?? []).some(
|
|
218501
|
-
(skillPath) => basename19(skillPath) === bareSkillName && existsSync65(
|
|
218771
|
+
(skillPath) => basename19(skillPath) === bareSkillName && existsSync65(join79(skillPath, "SKILL.md")) || existsSync65(join79(skillPath, bareSkillName, "SKILL.md"))
|
|
218502
218772
|
);
|
|
218503
218773
|
}
|
|
218504
218774
|
var yieldEventLoop3 = () => new Promise((resolve40) => setImmediateCb3(resolve40));
|
|
@@ -218601,6 +218871,9 @@ var MAX_TASK_DONE_SESSION_RETRIES = 3;
|
|
|
218601
218871
|
var MAX_TASK_DONE_REQUEUE_RETRIES = 3;
|
|
218602
218872
|
var MAX_TRANSIENT_GRAPH_RESUME_RETRIES = 2;
|
|
218603
218873
|
var TRANSIENT_GRAPH_RESUME_RETRY_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 1e3;
|
|
218874
|
+
var MAX_SESSION_CONTENTION_HOLD_RETRIES = 10;
|
|
218875
|
+
var SESSION_CONTENTION_HOLD_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 5e3;
|
|
218876
|
+
var SESSION_CONTENTION_HOLD_MAX_BACKOFF_MS = 6e4;
|
|
218604
218877
|
var COMPLETED_TASK_WATCHDOG_MS = 6e4;
|
|
218605
218878
|
var WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
|
|
218606
218879
|
var LOOP_COMPACTION_TIMEOUT_MS = 6e4;
|
|
@@ -218823,7 +219096,7 @@ function normalizeWorktreePath(pathValue) {
|
|
|
218823
219096
|
}
|
|
218824
219097
|
async function extractPersistedSessionWorktreePath(sessionFile, rootDir, settings) {
|
|
218825
219098
|
try {
|
|
218826
|
-
const content = await
|
|
219099
|
+
const content = await readFile38(sessionFile, "utf-8");
|
|
218827
219100
|
const matches = content.match(buildSessionWorktreePathRegex(rootDir, settings)) ?? [];
|
|
218828
219101
|
if (matches.length === 0) return null;
|
|
218829
219102
|
const normalizedCounts = /* @__PURE__ */ new Map();
|
|
@@ -219340,6 +219613,15 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219340
219613
|
}
|
|
219341
219614
|
})
|
|
219342
219615
|
);
|
|
219616
|
+
} else if ((from === "todo" || from === "triage") && to2 !== "in-progress" && to2 !== "in-review" && to2 !== "done") {
|
|
219617
|
+
this.trackTaskDisposal(
|
|
219618
|
+
task.id,
|
|
219619
|
+
this.awaitAbortInFlightTaskWork(task.id, `task moved out of planning to ${to2}`, {
|
|
219620
|
+
userCanceled: source === "user"
|
|
219621
|
+
}).then(async () => {
|
|
219622
|
+
await this.releasePreExecutionWorktree(task.id, `moved to ${to2}`);
|
|
219623
|
+
})
|
|
219624
|
+
);
|
|
219343
219625
|
} else if (from === "in-progress") {
|
|
219344
219626
|
if (this.workflowLifecycleMovesInFlight.has(task.id) && this.graphRouting.has(task.id)) {
|
|
219345
219627
|
executorLog.log(
|
|
@@ -219929,15 +220211,63 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219929
220211
|
getActiveWorktreePaths() consumers that cd into a path are unaffected; only the registry key changes.
|
|
219930
220212
|
Non-workspace tasks (unique worktree path != rootDir) are returned unchanged.
|
|
219931
220213
|
*/
|
|
220214
|
+
/*
|
|
220215
|
+
FNXC:PlanReviewWorktree 2026-07-25-20:40 (concurrent root-rooted step sessions — single-repo collision):
|
|
220216
|
+
The task-scoped key must apply to the shared repo root in EVERY project mode, not only workspace mode.
|
|
220217
|
+
Read-only graph nodes that need no worktree (Plan Review is the canonical one — it reviews the
|
|
220218
|
+
store-injected PROMPT.md, see FNXC:PlanReviewSpecInjection) run rooted at `this.rootDir`, and a todo
|
|
220219
|
+
task has no worktree of its own. With the bare root as the registry key, two tasks reaching Plan Review
|
|
220220
|
+
at the same time collided: the second failed with "active-session path <root> is held by task <other>;
|
|
220221
|
+
task <self> may not overwrite it", which surfaced as a Plan Review provider failure, burned the
|
|
220222
|
+
in-place retry budget against a hold that retrying can never clear, and left the task parked
|
|
220223
|
+
(reported: FN-1398 holding /home/ubuntu/dev/freemap-svelte while FN-1403 planned).
|
|
220224
|
+
Path-exclusivity on the shared root is not what keeps these sessions correct: write-capable nodes are
|
|
220225
|
+
refused at the root outright (no-worktree-for-write-node above), real per-sub-repo exclusivity is the
|
|
220226
|
+
workspace-repo-acquire lease, and every isPathActive consumer guards removable WORKTREE paths — the
|
|
220227
|
+
root is never one. Liveness still works because the synthetic key stays in the registry under the task.
|
|
220228
|
+
*/
|
|
219932
220229
|
sessionRegistryPath(taskId, worktreePath) {
|
|
219933
|
-
if (
|
|
220230
|
+
if (worktreePath === this.rootDir) {
|
|
219934
220231
|
return `${worktreePath}#session:${taskId}`;
|
|
219935
220232
|
}
|
|
219936
220233
|
return worktreePath;
|
|
219937
220234
|
}
|
|
220235
|
+
/*
|
|
220236
|
+
FNXC:SessionContention 2026-07-25-21:30 (contention prevention at the registration seam):
|
|
220237
|
+
Every executor session registration goes through `acquireActiveSessionPath` instead of the raw
|
|
220238
|
+
`registerPath`, so a LEAKED entry owned by a task with no live session surface in this process is
|
|
220239
|
+
RECLAIMED rather than throwing at the newcomer. That closes the second contention class (a dead
|
|
220240
|
+
holder can never release, so waiting on it is waiting forever). A genuinely live holder still throws
|
|
220241
|
+
the typed error — that case is real serialization, and callers classify it as a retryable contention
|
|
220242
|
+
hold (SESSION_CONTENTION_HOLD_VALUE), never as a provider/model failure.
|
|
220243
|
+
The probe reports LIVE on any uncertainty: an unknown holder with a fresh entry is treated as live by
|
|
220244
|
+
the staleness floor, so the reclaim only ever fires on proven-dead, aged entries.
|
|
220245
|
+
*/
|
|
220246
|
+
acquireSessionRegistryPath(taskId, registryPath, kind, ownerKey) {
|
|
220247
|
+
const outcome = acquireActiveSessionPath(activeSessionRegistry, registryPath, { taskId, kind, ownerKey }, {
|
|
220248
|
+
holderLiveProbe: (holderTaskId) => this.hasLiveTaskSessionSurface(holderTaskId) || executingTaskLock.has(holderTaskId)
|
|
220249
|
+
});
|
|
220250
|
+
if (outcome.action === "contended") {
|
|
220251
|
+
throw new ActiveSessionPathHeldByForeignTaskError(registryPath, outcome.holderTaskId, taskId);
|
|
220252
|
+
}
|
|
220253
|
+
if (outcome.action === "reclaimed-stale-foreign") {
|
|
220254
|
+
executorLog.warn(
|
|
220255
|
+
`${taskId}: reclaimed a stale active-session entry on ${registryPath} from dead task ${outcome.holderTaskId} (idle ${outcome.ageMs}ms)`
|
|
220256
|
+
);
|
|
220257
|
+
void this.store.recordRunAuditEvent?.({
|
|
220258
|
+
taskId,
|
|
220259
|
+
agentId: "executor",
|
|
220260
|
+
runId: generateSyntheticRunId("session-path-reclaim", taskId),
|
|
220261
|
+
domain: "database",
|
|
220262
|
+
mutationType: "session:reclaim-stale-foreign-path",
|
|
220263
|
+
target: taskId,
|
|
220264
|
+
metadata: { taskId, holderTaskId: outcome.holderTaskId, kind, ageMs: outcome.ageMs }
|
|
220265
|
+
})?.catch?.(() => void 0);
|
|
220266
|
+
}
|
|
220267
|
+
}
|
|
219938
220268
|
setActiveSession(taskId, sessionState, worktreePath) {
|
|
219939
220269
|
this.activeSessions.set(taskId, sessionState);
|
|
219940
|
-
|
|
220270
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "executor", taskId);
|
|
219941
220271
|
}
|
|
219942
220272
|
markGraphExecuteSelfRequeued(taskId) {
|
|
219943
220273
|
if (this.graphRouting.has(taskId)) {
|
|
@@ -219955,7 +220285,7 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219955
220285
|
setActiveStepExecutor(taskId, stepExecutor, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
|
|
219956
220286
|
this.activeStepExecutors.set(taskId, stepExecutor);
|
|
219957
220287
|
this.activeStepExecutorSeenSteeringIds.set(taskId, seenSteeringIds);
|
|
219958
|
-
|
|
220288
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "step-session", `${taskId}#step-session`);
|
|
219959
220289
|
}
|
|
219960
220290
|
deleteActiveStepExecutor(taskId, worktreePath) {
|
|
219961
220291
|
this.activeStepExecutors.delete(taskId);
|
|
@@ -219969,7 +220299,7 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219969
220299
|
setActiveWorkflowStepSession(taskId, session, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
|
|
219970
220300
|
this.activeWorkflowStepSessions.set(taskId, session);
|
|
219971
220301
|
this.activeWorkflowStepSessionSeenSteeringIds.set(taskId, seenSteeringIds);
|
|
219972
|
-
|
|
220302
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "workflow-step", `${taskId}#workflow-step`);
|
|
219973
220303
|
}
|
|
219974
220304
|
deleteActiveWorkflowStepSession(taskId, worktreePath) {
|
|
219975
220305
|
this.activeWorkflowStepSessions.delete(taskId);
|
|
@@ -221841,7 +222171,7 @@ ${info.feedback}`, revisionKey),
|
|
|
221841
222171
|
const authoritativeAgentLayer = this.store.getAsyncLayer();
|
|
221842
222172
|
if (!this.authoritativeAssignedAgentStore || authoritativeAgentLayer && !this.authoritativeAssignedAgentStore.backendMode) {
|
|
221843
222173
|
this.authoritativeAssignedAgentStore = new AgentStore({
|
|
221844
|
-
rootDir:
|
|
222174
|
+
rootDir: join79(this.rootDir, ".fusion"),
|
|
221845
222175
|
taskStore: this.store,
|
|
221846
222176
|
...authoritativeAgentLayer ? { asyncLayer: authoritativeAgentLayer } : {}
|
|
221847
222177
|
});
|
|
@@ -224288,6 +224618,87 @@ ${info.feedback}`, revisionKey),
|
|
|
224288
224618
|
this.unregisterConfiguredCommandController(task.id, commandAbortController);
|
|
224289
224619
|
}
|
|
224290
224620
|
}
|
|
224621
|
+
/*
|
|
224622
|
+
FNXC:NodeWorktreeIsolation 2026-07-25-22:10 (planning acquires the task worktree):
|
|
224623
|
+
Public seam for the planning/triage lane. Specification runs a CODING-tool session; pointing it at
|
|
224624
|
+
the shared main checkout meant every planning agent had write tools in the operator's tree and every
|
|
224625
|
+
concurrent planner shared one path. Acquire the task's own worktree up front and let the whole
|
|
224626
|
+
lifecycle — planning, Plan Review, implementation, code review — reuse that single worktree.
|
|
224627
|
+
Returns null (caller falls back to the root, unchanged behavior) when the project is a workspace, or
|
|
224628
|
+
when acquisition fails: planning must never be blocked by a worktree problem.
|
|
224629
|
+
*/
|
|
224630
|
+
/*
|
|
224631
|
+
FNXC:PlanningEvacuation 2026-07-25-23:00 (pre-execution worktree release):
|
|
224632
|
+
Planning now acquires a worktree, so a card that never reaches execution — withdrawn to Ideas,
|
|
224633
|
+
archived from a planner lane, or parked pre-execution — would otherwise hold one forever. Release
|
|
224634
|
+
it. Safety conditions, all required:
|
|
224635
|
+
- the task never executed (`firstExecutionAt`/`executionStartedAt` unset): execution evidence means
|
|
224636
|
+
the worktree may hold real work, and only the normal merge/archive lifecycle may remove it;
|
|
224637
|
+
- no live session registered on the path (the same isPathActive guard the other sweeps use);
|
|
224638
|
+
- the branch carries no commits beyond its base — planning writes its spec to the task store, not
|
|
224639
|
+
the worktree, so a clean branch means there is genuinely nothing to lose.
|
|
224640
|
+
Metadata (`worktree`/`branch`) is cleared with it, so a later promotion re-acquires cleanly.
|
|
224641
|
+
Fail-soft throughout: a cleanup problem must never block the lifecycle move that triggered it.
|
|
224642
|
+
*/
|
|
224643
|
+
async releasePreExecutionWorktree(taskId, reason) {
|
|
224644
|
+
try {
|
|
224645
|
+
const live = await this.store.getTask(taskId);
|
|
224646
|
+
if (!live?.worktree) return false;
|
|
224647
|
+
if (live.firstExecutionAt || live.executionStartedAt) return false;
|
|
224648
|
+
if (activeSessionRegistry.isPathActive(live.worktree) || activeSessionRegistry.isPathActive(resolvePath3(live.worktree))) return false;
|
|
224649
|
+
if (this.hasLiveTaskSessionSurface(taskId) || executingTaskLock.has(taskId)) return false;
|
|
224650
|
+
if (existsSync65(live.worktree)) {
|
|
224651
|
+
if (await this.preExecutionWorktreeHasWork(live.worktree)) {
|
|
224652
|
+
executorLog.log(`${taskId}: keeping pre-execution worktree ${live.worktree} \u2014 it carries commits or uncommitted changes`);
|
|
224653
|
+
return false;
|
|
224654
|
+
}
|
|
224655
|
+
const settings = await this.store.getSettings();
|
|
224656
|
+
await removeWorktree({
|
|
224657
|
+
rootDir: this.rootDir,
|
|
224658
|
+
worktreePath: live.worktree,
|
|
224659
|
+
settings,
|
|
224660
|
+
taskId,
|
|
224661
|
+
reason: RemovalReason.SelfHealingReclaim
|
|
224662
|
+
});
|
|
224663
|
+
}
|
|
224664
|
+
this.activeWorktrees.get(taskId)?.delete(live.worktree);
|
|
224665
|
+
await this.store.updateTask(taskId, { worktree: null, branch: null, baseCommitSha: null, sessionFile: null }, this.getRunContextFor(taskId));
|
|
224666
|
+
await this.store.logEntry(taskId, `Released the pre-execution worktree (${reason}) \u2014 it will be re-acquired when planning or execution resumes`, void 0, this.getRunContextFor(taskId)).catch(() => void 0);
|
|
224667
|
+
executorLog.log(`${taskId}: released pre-execution worktree ${live.worktree} (${reason})`);
|
|
224668
|
+
return true;
|
|
224669
|
+
} catch (error2) {
|
|
224670
|
+
executorLog.warn(`${taskId}: could not release the pre-execution worktree: ${formatError(error2).message}`);
|
|
224671
|
+
return false;
|
|
224672
|
+
}
|
|
224673
|
+
}
|
|
224674
|
+
/** True when a pre-execution worktree holds commits past its base or any uncommitted change. */
|
|
224675
|
+
async preExecutionWorktreeHasWork(worktreePath) {
|
|
224676
|
+
try {
|
|
224677
|
+
const { stdout: dirty } = await execFileAsync19("git", ["status", "--porcelain"], { cwd: worktreePath, timeout: 3e4 });
|
|
224678
|
+
if (dirty.trim()) return true;
|
|
224679
|
+
const { stdout: ahead } = await execFileAsync19("git", ["log", "--oneline", "@{upstream}..HEAD"], { cwd: worktreePath, timeout: 3e4 }).catch(async () => await execFileAsync19("git", ["log", "--oneline", "-1", "HEAD", "--not", "--remotes", "--branches=main", "--branches=master"], { cwd: worktreePath, timeout: 3e4 }));
|
|
224680
|
+
return Boolean(ahead.trim());
|
|
224681
|
+
} catch {
|
|
224682
|
+
return true;
|
|
224683
|
+
}
|
|
224684
|
+
}
|
|
224685
|
+
async ensureTaskWorktreeForPlanning(taskId) {
|
|
224686
|
+
try {
|
|
224687
|
+
if (this.workspaceConfig === void 0) {
|
|
224688
|
+
this.workspaceConfig = await loadWorkspaceConfig(this.rootDir);
|
|
224689
|
+
}
|
|
224690
|
+
if (this.workspaceConfig && (this.workspaceConfig.repos.length ?? 0) > 0) return null;
|
|
224691
|
+
const live = await this.store.getTask(taskId);
|
|
224692
|
+
if (live.worktree && existsSync65(live.worktree)) return live.worktree;
|
|
224693
|
+
const settings = await this.store.getSettings();
|
|
224694
|
+
const acquisitionTask = live.worktree ? { ...live, worktree: void 0, sessionFile: void 0 } : live;
|
|
224695
|
+
const acquired = await this.ensureGraphCustomNodeWorktree(acquisitionTask, settings, "planning");
|
|
224696
|
+
return acquired.worktree || null;
|
|
224697
|
+
} catch (error2) {
|
|
224698
|
+
executorLog.warn(`${taskId}: could not acquire a planning worktree \u2014 planning falls back to the repo root: ${formatError(error2)}`);
|
|
224699
|
+
return null;
|
|
224700
|
+
}
|
|
224701
|
+
}
|
|
224291
224702
|
async prepareGraphNodeExecution(node, nodeTask, settings, requirement) {
|
|
224292
224703
|
if (!requirement.requiresWorktree) return;
|
|
224293
224704
|
const live = await this.store.getTask(nodeTask.id);
|
|
@@ -224414,22 +224825,29 @@ ${info.feedback}`, revisionKey),
|
|
|
224414
224825
|
optionalGroupId,
|
|
224415
224826
|
reviewerInlineFixes: settings.reviewerInlineFixes
|
|
224416
224827
|
});
|
|
224417
|
-
|
|
224418
|
-
if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
|
|
224419
|
-
return { outcome: "failure", value: "no-worktree-for-write-node" };
|
|
224420
|
-
}
|
|
224828
|
+
let executionTarget = writeCapable ? await this.store.getTask(live.id) : live;
|
|
224421
224829
|
const nodeDisplayName = typeof cfg.name === "string" && cfg.name.trim() ? cfg.name.trim() : node.id;
|
|
224422
224830
|
const isPlanReviewNode = node.id === "plan-review-step" || nodeDisplayName === "Plan Review" || optionalGroupId === "plan-review";
|
|
224423
|
-
|
|
224424
|
-
|
|
224425
|
-
|
|
224426
|
-
|
|
224427
|
-
|
|
224428
|
-
|
|
224429
|
-
|
|
224430
|
-
|
|
224431
|
-
|
|
224831
|
+
if (!this.workspaceConfig) {
|
|
224832
|
+
const recordedWorktreeMissing = Boolean(executionTarget.worktree) && !existsSync65(executionTarget.worktree);
|
|
224833
|
+
const shouldAcquire = !executionTarget.worktree || recordedWorktreeMissing && isPlanReviewNode;
|
|
224834
|
+
if (shouldAcquire) {
|
|
224835
|
+
if (recordedWorktreeMissing) {
|
|
224836
|
+
await this.store.logEntry(
|
|
224837
|
+
live.id,
|
|
224838
|
+
`Plan Review worktree ${executionTarget.worktree} is missing on disk \u2014 re-acquiring a task worktree instead of running in the shared checkout`,
|
|
224839
|
+
void 0,
|
|
224840
|
+
this.getRunContextFor(live.id)
|
|
224841
|
+
);
|
|
224842
|
+
}
|
|
224843
|
+
const acquisitionTask = recordedWorktreeMissing ? { ...executionTarget, worktree: void 0, sessionFile: void 0 } : executionTarget;
|
|
224844
|
+
executionTarget = await this.ensureGraphCustomNodeWorktree(acquisitionTask, settings, node.id);
|
|
224845
|
+
}
|
|
224432
224846
|
}
|
|
224847
|
+
if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
|
|
224848
|
+
return { outcome: "failure", value: "no-worktree-for-write-node" };
|
|
224849
|
+
}
|
|
224850
|
+
const worktreePath = executionTarget.worktree || this.rootDir;
|
|
224433
224851
|
let prompt = typeof cfg.prompt === "string" ? cfg.prompt : "";
|
|
224434
224852
|
let modelProvider = typeof cfg.modelProvider === "string" && cfg.modelProvider.trim() ? cfg.modelProvider : void 0;
|
|
224435
224853
|
let modelId = typeof cfg.modelId === "string" && cfg.modelId.trim() ? cfg.modelId : void 0;
|
|
@@ -224726,6 +225144,77 @@ ${prompt}`;
|
|
|
224726
225144
|
const latestAction = live.log.at(-1)?.action;
|
|
224727
225145
|
return latestAction === "Resumed after engine restart" || latestAction === "Resuming execution after unpause";
|
|
224728
225146
|
}
|
|
225147
|
+
/*
|
|
225148
|
+
FNXC:SessionContention 2026-07-25-21:30:
|
|
225149
|
+
Two ways in, because contention must never slip through to a park:
|
|
225150
|
+
- the typed failure value the graph now publishes (SESSION_CONTENTION_HOLD_VALUE), and
|
|
225151
|
+
- a message-shape fallback over the run's `:error` context patches, so contention arriving from a
|
|
225152
|
+
path that has not been taught the typed value is still recognized.
|
|
225153
|
+
*/
|
|
225154
|
+
graphFailureErrorTexts(result) {
|
|
225155
|
+
if (!result.context) return [];
|
|
225156
|
+
const texts = [];
|
|
225157
|
+
for (const [key, value] of Object.entries(result.context)) {
|
|
225158
|
+
if (key.endsWith(":error") && typeof value === "string" && value.trim()) texts.push(value);
|
|
225159
|
+
}
|
|
225160
|
+
return texts;
|
|
225161
|
+
}
|
|
225162
|
+
isSessionContentionGraphFailure(result) {
|
|
225163
|
+
if (this.graphFailureValue(result) === SESSION_CONTENTION_HOLD_VALUE) return true;
|
|
225164
|
+
return this.graphFailureErrorTexts(result).some((text2) => isSessionContentionError(text2));
|
|
225165
|
+
}
|
|
225166
|
+
/*
|
|
225167
|
+
FNXC:SessionContention 2026-07-25-21:30 (self-recovering wait — the task is never parked):
|
|
225168
|
+
Retry the graph in place on an exponential backoff while the holder finishes. The counter is
|
|
225169
|
+
IN-MEMORY on purpose: it needs no schema change, and an engine restart resetting it is the desired
|
|
225170
|
+
behavior (a restart also drops the in-process registry, so the contention is gone anyway).
|
|
225171
|
+
When the ladder is exhausted the task is left cleanly dispatchable — status/error cleared, progress
|
|
225172
|
+
untouched — so ordinary scheduling picks it up later with a fresh budget. There is no terminal branch
|
|
225173
|
+
here by design: lease contention always ends (the holder finishes, or self-healing sweeps it), so
|
|
225174
|
+
parking the task would only require a human to press Retry on a condition that fixed itself.
|
|
225175
|
+
*/
|
|
225176
|
+
sessionContentionHoldAttempts = /* @__PURE__ */ new Map();
|
|
225177
|
+
clearSessionContentionHold(taskId) {
|
|
225178
|
+
this.sessionContentionHoldAttempts.delete(taskId);
|
|
225179
|
+
}
|
|
225180
|
+
async holdForSessionContention(task, live, result) {
|
|
225181
|
+
const detail = this.graphFailureErrorTexts(result).find((text2) => isSessionContentionError(text2));
|
|
225182
|
+
const priorAttempts = this.sessionContentionHoldAttempts.get(task.id) ?? 0;
|
|
225183
|
+
const attempt = priorAttempts + 1;
|
|
225184
|
+
if (attempt > MAX_SESSION_CONTENTION_HOLD_RETRIES) {
|
|
225185
|
+
this.clearSessionContentionHold(task.id);
|
|
225186
|
+
const message2 = `Still waiting on another task to release a shared session path after ${MAX_SESSION_CONTENTION_HOLD_RETRIES} attempts \u2014 leaving the task queued for normal re-dispatch (not a failure)${detail ? `: ${detail}` : ""}`;
|
|
225187
|
+
executorLog.warn(`${task.id}: ${message2}`);
|
|
225188
|
+
await this.store.logEntry(task.id, message2, void 0, this.getRunContextFor(task.id));
|
|
225189
|
+
if (live.status != null || live.error != null) {
|
|
225190
|
+
await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id));
|
|
225191
|
+
}
|
|
225192
|
+
return;
|
|
225193
|
+
}
|
|
225194
|
+
this.sessionContentionHoldAttempts.set(task.id, attempt);
|
|
225195
|
+
const message = `Waiting on another task to release a shared session path \u2014 retrying in place (${attempt}/${MAX_SESSION_CONTENTION_HOLD_RETRIES})${detail ? `: ${detail}` : ""}`;
|
|
225196
|
+
executorLog.warn(`${task.id}: ${message}`);
|
|
225197
|
+
await this.store.logEntry(task.id, message, void 0, this.getRunContextFor(task.id));
|
|
225198
|
+
if (live.status != null || live.error != null) {
|
|
225199
|
+
await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id));
|
|
225200
|
+
}
|
|
225201
|
+
const delayMs = SESSION_CONTENTION_HOLD_BACKOFF_MS === 0 ? 0 : Math.min(SESSION_CONTENTION_HOLD_MAX_BACKOFF_MS, SESSION_CONTENTION_HOLD_BACKOFF_MS * 2 ** (attempt - 1));
|
|
225202
|
+
const scheduleRetry = () => {
|
|
225203
|
+
void (async () => {
|
|
225204
|
+
try {
|
|
225205
|
+
const resume = await this.store.getTask(task.id);
|
|
225206
|
+
if (!resume || resume.deletedAt || resume.paused || resume.userPaused) {
|
|
225207
|
+
this.clearSessionContentionHold(task.id);
|
|
225208
|
+
return;
|
|
225209
|
+
}
|
|
225210
|
+
await this.execute(resume);
|
|
225211
|
+
} catch (err) {
|
|
225212
|
+
executorLog.error(`Failed session-contention retry for ${task.id}:`, err);
|
|
225213
|
+
}
|
|
225214
|
+
})();
|
|
225215
|
+
};
|
|
225216
|
+
setTimeout(scheduleRetry, delayMs).unref?.();
|
|
225217
|
+
}
|
|
224729
225218
|
graphFailureValue(result) {
|
|
224730
225219
|
const failedNode = result.visitedNodeIds[result.visitedNodeIds.length - 1];
|
|
224731
225220
|
if (!failedNode || !result.context) return void 0;
|
|
@@ -225317,6 +225806,11 @@ Graph value: ${failureValue}` : ""}`, budget.key),
|
|
|
225317
225806
|
await this.persistTokenUsage(task.id);
|
|
225318
225807
|
return;
|
|
225319
225808
|
}
|
|
225809
|
+
if (this.isSessionContentionGraphFailure(result)) {
|
|
225810
|
+
await this.holdForSessionContention(task, live, result);
|
|
225811
|
+
await this.persistTokenUsage(task.id);
|
|
225812
|
+
return;
|
|
225813
|
+
}
|
|
225320
225814
|
if (await this.routeUnusableWorktreeGraphFailureToRecovery(task, live, result)) {
|
|
225321
225815
|
await this.persistTokenUsage(task.id);
|
|
225322
225816
|
return;
|
|
@@ -226137,7 +226631,7 @@ Graph value: ${failureValue}` : ""}`, budget.key),
|
|
|
226137
226631
|
const activeMergeStatuses = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
|
|
226138
226632
|
const isActiveTask = activeColumns.has(task.column) || activeMergeStatuses.has(task.status ?? "");
|
|
226139
226633
|
if (!isActiveTask) {
|
|
226140
|
-
const tasksDir =
|
|
226634
|
+
const tasksDir = join79(this.store.getFusionDir(), "tasks");
|
|
226141
226635
|
const promptPath = getPromptPath(tasksDir, task.id);
|
|
226142
226636
|
const staleness = await evaluateSpecStaleness({ settings, promptPath, task });
|
|
226143
226637
|
if (staleness.isStale) {
|
|
@@ -229998,10 +230492,10 @@ Please fix the issues so the verification can pass on the next attempt.`,
|
|
|
229998
230492
|
* The section is replaced entirely to avoid accumulation of old feedback.
|
|
229999
230493
|
*/
|
|
230000
230494
|
async injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retry) {
|
|
230001
|
-
const promptPath =
|
|
230495
|
+
const promptPath = join79(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
|
|
230002
230496
|
let content;
|
|
230003
230497
|
try {
|
|
230004
|
-
content = await
|
|
230498
|
+
content = await readFile38(promptPath, "utf-8");
|
|
230005
230499
|
} catch {
|
|
230006
230500
|
executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping workflow failure injection`);
|
|
230007
230501
|
return;
|
|
@@ -241174,7 +241668,7 @@ init_effective_settings();
|
|
|
241174
241668
|
|
|
241175
241669
|
// ../engine/src/spec-validation/task-document-references.ts
|
|
241176
241670
|
import { access as access9 } from "node:fs/promises";
|
|
241177
|
-
import { join as
|
|
241671
|
+
import { join as join80 } from "node:path";
|
|
241178
241672
|
var TASK_PATH_REGEX = /\.fusion\/tasks\/[A-Z0-9]+(?:-[A-Z0-9]+)*\/[^\s`'"\]]+/g;
|
|
241179
241673
|
function normalizePathToken(token) {
|
|
241180
241674
|
return token.replace(/^[`([{"']+/, "").replace(/[`),.;:!?\]}'"]+$/, "");
|
|
@@ -241263,7 +241757,7 @@ async function detectDanglingTaskDocReferences(promptContent, opts) {
|
|
|
241263
241757
|
}
|
|
241264
241758
|
const dangling = [];
|
|
241265
241759
|
for (const [path5, sections] of references.entries()) {
|
|
241266
|
-
const absPath =
|
|
241760
|
+
const absPath = join80(opts.rootDir, path5);
|
|
241267
241761
|
if (await existsImpl(absPath)) continue;
|
|
241268
241762
|
dangling.push({ path: path5, sections: Array.from(sections).sort() });
|
|
241269
241763
|
}
|
|
@@ -241286,14 +241780,15 @@ init_agent_logger();
|
|
|
241286
241780
|
init_agent_instructions();
|
|
241287
241781
|
init_fallback_model_observer();
|
|
241288
241782
|
init_logger();
|
|
241783
|
+
init_plan_artifact_writeback();
|
|
241289
241784
|
init_mcp_resolution();
|
|
241290
241785
|
init_usage_limit_detector();
|
|
241291
241786
|
init_transient_error_detector();
|
|
241292
241787
|
init_rate_limit_retry();
|
|
241293
241788
|
init_agent_tools();
|
|
241294
241789
|
import { exec as exec41 } from "node:child_process";
|
|
241295
|
-
import { readFile as
|
|
241296
|
-
import { join as
|
|
241790
|
+
import { readFile as readFile39, writeFile as writeFile30, rm as rm13 } from "node:fs/promises";
|
|
241791
|
+
import { join as join81 } from "node:path";
|
|
241297
241792
|
import { promisify as promisify56 } from "node:util";
|
|
241298
241793
|
|
|
241299
241794
|
// ../engine/src/triage-preflight.ts
|
|
@@ -241523,6 +242018,33 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241523
242018
|
if (this.processing.has(task.id) || this.hasLivePlanningWork(task.id)) return;
|
|
241524
242019
|
this.requestImmediatePoll();
|
|
241525
242020
|
};
|
|
242021
|
+
this.taskEvacuatedFromPlanningHandler = (task) => {
|
|
242022
|
+
if (!task?.id) return;
|
|
242023
|
+
if (typeof task.column !== "string") return;
|
|
242024
|
+
if (task.column === "todo" || task.column === "triage" || task.column === "in-progress") return;
|
|
242025
|
+
if (this.activeSubagentSessions.has(task.id)) {
|
|
242026
|
+
this.disposeSubagentsForTask(task.id, `task moved to ${task.column}`);
|
|
242027
|
+
}
|
|
242028
|
+
const session = this.activeSessions.get(task.id);
|
|
242029
|
+
if (!session) return;
|
|
242030
|
+
planLog.log(`task moved out of planning to '${task.column}' \u2014 terminating triage session for ${task.id}`);
|
|
242031
|
+
this.pauseAborted.add(task.id);
|
|
242032
|
+
this.options.stuckTaskDetector?.untrackTask(task.id);
|
|
242033
|
+
const sessionWithAbort = session;
|
|
242034
|
+
if (typeof sessionWithAbort.abort === "function") {
|
|
242035
|
+
void sessionWithAbort.abort().catch((err) => {
|
|
242036
|
+
planLog.warn(`Failed to abort triage session for ${task.id}: ${err}`);
|
|
242037
|
+
});
|
|
242038
|
+
}
|
|
242039
|
+
this.recordTriageSessionTokenUsageSoon(task.id, session, { agentId: task.assignedAgentId ?? "triage" });
|
|
242040
|
+
session.dispose();
|
|
242041
|
+
this.activeSessions.delete(task.id);
|
|
242042
|
+
if (task.status === "planning") {
|
|
242043
|
+
void Promise.resolve(this.store.updateTask(task.id, { status: null })).catch((err) => {
|
|
242044
|
+
planLog.warn(`${task.id}: failed to clear planning status after evacuation: ${err instanceof Error ? err.message : String(err)}`);
|
|
242045
|
+
});
|
|
242046
|
+
}
|
|
242047
|
+
};
|
|
241526
242048
|
}
|
|
241527
242049
|
running = false;
|
|
241528
242050
|
polling = false;
|
|
@@ -241581,6 +242103,8 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241581
242103
|
taskPausedHandler;
|
|
241582
242104
|
/** FNXC:CodingIdeasWorkflow 2026-07-25-11:20: store-event wake for planning-eligible columns. */
|
|
241583
242105
|
taskColumnWakeHandler;
|
|
242106
|
+
/** FNXC:PlanningEvacuation 2026-07-25-23:00: stops planning when a card leaves the planner lanes. */
|
|
242107
|
+
taskEvacuatedFromPlanningHandler;
|
|
241584
242108
|
_approvalRequestStore;
|
|
241585
242109
|
/**
|
|
241586
242110
|
* @param store — Task store instance (also used to listen for `settings:updated` events)
|
|
@@ -241689,6 +242213,9 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241689
242213
|
this.store.on("task:updated", this.taskColumnWakeHandler);
|
|
241690
242214
|
this.store.on("task:created", this.taskColumnWakeHandler);
|
|
241691
242215
|
}
|
|
242216
|
+
if (this.taskEvacuatedFromPlanningHandler && typeof this.store.on === "function") {
|
|
242217
|
+
this.store.on("task:updated", this.taskEvacuatedFromPlanningHandler);
|
|
242218
|
+
}
|
|
241692
242219
|
this.clearStaleSpecifyingStatuses().catch((err) => {
|
|
241693
242220
|
planLog.error("Failed to clear stale planning statuses:", err);
|
|
241694
242221
|
});
|
|
@@ -241736,6 +242263,9 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241736
242263
|
this.store.off("task:updated", this.taskColumnWakeHandler);
|
|
241737
242264
|
this.store.off("task:created", this.taskColumnWakeHandler);
|
|
241738
242265
|
}
|
|
242266
|
+
if (this.taskEvacuatedFromPlanningHandler && typeof this.store.off === "function") {
|
|
242267
|
+
this.store.off("task:updated", this.taskEvacuatedFromPlanningHandler);
|
|
242268
|
+
}
|
|
241739
242269
|
this.abortAndDisposeActiveSessions("engine stop");
|
|
241740
242270
|
planLog.log("Processor stopped");
|
|
241741
242271
|
}
|
|
@@ -241934,8 +242464,8 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241934
242464
|
}
|
|
241935
242465
|
const settings = await mergeEffectiveSettings(this.store, task, await this.store.getSettings());
|
|
241936
242466
|
const approvalRequired = resolvePlanApprovalRequired(settings);
|
|
241937
|
-
const promptPath =
|
|
241938
|
-
const written = await
|
|
242467
|
+
const promptPath = join81(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
|
242468
|
+
const written = await readFile39(promptPath, "utf-8").catch((err) => {
|
|
241939
242469
|
const msg = err instanceof Error ? err.message : String(err);
|
|
241940
242470
|
planLog.warn(`${task.id}: failed to read PROMPT.md during planning recovery (${promptPath}): ${msg}`);
|
|
241941
242471
|
return "";
|
|
@@ -241973,8 +242503,8 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241973
242503
|
return true;
|
|
241974
242504
|
}
|
|
241975
242505
|
async readNonEmptyPromptDraft(taskId, context) {
|
|
241976
|
-
const promptPath =
|
|
241977
|
-
const written = await
|
|
242506
|
+
const promptPath = join81(this.rootDir, ".fusion", "tasks", taskId, "PROMPT.md");
|
|
242507
|
+
const written = await readFile39(promptPath, "utf-8").catch((err) => {
|
|
241978
242508
|
const msg = err instanceof Error ? err.message : String(err);
|
|
241979
242509
|
planLog.warn(`${taskId}: failed to read PROMPT.md during ${context} (${promptPath}): ${msg}`);
|
|
241980
242510
|
return "";
|
|
@@ -242138,8 +242668,8 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
242138
242668
|
continue;
|
|
242139
242669
|
}
|
|
242140
242670
|
try {
|
|
242141
|
-
const promptPath =
|
|
242142
|
-
const content = await
|
|
242671
|
+
const promptPath = join81(this.rootDir, ".fusion", "tasks", todoTask.id, "PROMPT.md");
|
|
242672
|
+
const content = await readFile39(promptPath, "utf-8");
|
|
242143
242673
|
if (isUnplannedSeedPrompt(content, todoTask.id, todoTask.title, todoTask.description)) {
|
|
242144
242674
|
eligibleTodoTasks.push(todoTask);
|
|
242145
242675
|
}
|
|
@@ -242350,7 +242880,7 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
242350
242880
|
const detail = await this.store.getTask(task.id);
|
|
242351
242881
|
const currentTask = detail ?? task;
|
|
242352
242882
|
const settings = await mergeEffectiveSettings(this.store, currentTask, await this.store.getSettings());
|
|
242353
|
-
const promptPath =
|
|
242883
|
+
const promptPath = relativePromptPath(task.id);
|
|
242354
242884
|
const isFast = task.executionMode === "fast";
|
|
242355
242885
|
const leanPlanning = settings.leanPlanning === true || isFast;
|
|
242356
242886
|
const agentWork = async () => {
|
|
@@ -242527,11 +243057,15 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
|
|
|
242527
243057
|
planningModel.modelId,
|
|
242528
243058
|
assignedAgent?.runtimeConfig
|
|
242529
243059
|
) : { provider: void 0, modelId: void 0 };
|
|
243060
|
+
const planningCwd = await this.options.acquirePlanningWorktree?.(task.id).catch(() => null) || this.rootDir;
|
|
243061
|
+
if (planningCwd !== this.rootDir) {
|
|
243062
|
+
await this.store.logEntry(task.id, `Planning session running in task worktree ${planningCwd}`).catch(() => void 0);
|
|
243063
|
+
}
|
|
242530
243064
|
const { session } = await createResolvedAgentSession({
|
|
242531
243065
|
sessionPurpose: "triage",
|
|
242532
243066
|
runtimeHint: triageRuntimeHint,
|
|
242533
243067
|
pluginRunner: this.options.pluginRunner,
|
|
242534
|
-
cwd:
|
|
243068
|
+
cwd: planningCwd,
|
|
242535
243069
|
systemPrompt: triageSystemPromptFinal,
|
|
242536
243070
|
systemPromptLayers: triageLayers,
|
|
242537
243071
|
tools: "coding",
|
|
@@ -242682,8 +243216,22 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
|
|
|
242682
243216
|
}
|
|
242683
243217
|
return;
|
|
242684
243218
|
}
|
|
242685
|
-
const
|
|
242686
|
-
|
|
243219
|
+
const planPersistence = await persistPlanArtifact({
|
|
243220
|
+
store: this.store,
|
|
243221
|
+
taskId: task.id,
|
|
243222
|
+
rootDir: this.rootDir,
|
|
243223
|
+
planningCwd,
|
|
243224
|
+
author: "triage",
|
|
243225
|
+
logger: { log: (m2) => planLog.log(m2), warn: (m2) => planLog.warn(m2) }
|
|
243226
|
+
});
|
|
243227
|
+
if (planPersistence.outcome === "recovered") {
|
|
243228
|
+
await this.store.logEntry(
|
|
243229
|
+
task.id,
|
|
243230
|
+
"Recovered the plan written inside the task worktree into the project .fusion folder"
|
|
243231
|
+
).catch(() => void 0);
|
|
243232
|
+
}
|
|
243233
|
+
const written = await readFile39(
|
|
243234
|
+
join81(this.rootDir, promptPath),
|
|
242687
243235
|
"utf-8"
|
|
242688
243236
|
).catch((err) => {
|
|
242689
243237
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -243300,7 +243848,7 @@ Remove or replace these ids and call fn_task_create again.`
|
|
|
243300
243848
|
if (isNearDuplicateCanonicalInactive(canonicalTask ?? void 0)) {
|
|
243301
243849
|
if (canClearInactiveMarker) {
|
|
243302
243850
|
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
|
243303
|
-
await rm13(
|
|
243851
|
+
await rm13(join81(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
|
|
243304
243852
|
})) return;
|
|
243305
243853
|
await this.updatePlanningStateIfStillCurrent(task, { paused: false, pausedReason: null, status: null });
|
|
243306
243854
|
}
|
|
@@ -243311,7 +243859,7 @@ Remove or replace these ids and call fn_task_create again.`
|
|
|
243311
243859
|
if (resolution === "prompt" && keepAcknowledged) {
|
|
243312
243860
|
if (canClearInactiveMarker) {
|
|
243313
243861
|
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
|
243314
|
-
await rm13(
|
|
243862
|
+
await rm13(join81(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
|
|
243315
243863
|
})) return;
|
|
243316
243864
|
await this.updatePlanningStateIfStillCurrent(task, {
|
|
243317
243865
|
paused: false,
|
|
@@ -243352,7 +243900,7 @@ Remove or replace these ids and call fn_task_create again.`
|
|
|
243352
243900
|
return;
|
|
243353
243901
|
}
|
|
243354
243902
|
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
|
243355
|
-
await rm13(
|
|
243903
|
+
await rm13(join81(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), { force: true });
|
|
243356
243904
|
})) return;
|
|
243357
243905
|
if (!await this.updatePlanningStateIfStillCurrent(task, {
|
|
243358
243906
|
paused: false,
|
|
@@ -243417,7 +243965,7 @@ Remove or replace these ids and call fn_task_create again.`
|
|
|
243417
243965
|
let nextPrompt = applyOriginalDescription(written, originalDescription);
|
|
243418
243966
|
nextPrompt = applyFrontendUxCriteria(nextPrompt, parsedFileScope);
|
|
243419
243967
|
if (nextPrompt !== written) {
|
|
243420
|
-
const promptPath =
|
|
243968
|
+
const promptPath = join81(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
|
243421
243969
|
try {
|
|
243422
243970
|
if (!await this.runIfStillPlanningUnderTaskLock(task, async () => {
|
|
243423
243971
|
await writeFile30(promptPath, nextPrompt, "utf-8");
|
|
@@ -243429,6 +243977,10 @@ Remove or replace these ids and call fn_task_create again.`
|
|
|
243429
243977
|
}
|
|
243430
243978
|
}
|
|
243431
243979
|
}
|
|
243980
|
+
await mirrorPlanToProjectDb(this.store, task.id, written, {
|
|
243981
|
+
author: "triage",
|
|
243982
|
+
logger: { warn: (m2) => planLog.warn(m2) }
|
|
243983
|
+
});
|
|
243432
243984
|
let taskIntentSignature = {
|
|
243433
243985
|
routePaths: [],
|
|
243434
243986
|
filePaths: [],
|
|
@@ -243705,10 +244257,10 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
|
|
|
243705
244257
|
if (!attachments || attachments.length === 0) {
|
|
243706
244258
|
return { attachmentContents, imageContents };
|
|
243707
244259
|
}
|
|
243708
|
-
const { readFile:
|
|
243709
|
-
const { join:
|
|
244260
|
+
const { readFile: readFile41 } = await import("node:fs/promises");
|
|
244261
|
+
const { join: join85 } = await import("node:path");
|
|
243710
244262
|
for (const att of attachments) {
|
|
243711
|
-
const filePath =
|
|
244263
|
+
const filePath = join85(
|
|
243712
244264
|
rootDir,
|
|
243713
244265
|
".fusion",
|
|
243714
244266
|
"tasks",
|
|
@@ -243718,7 +244270,7 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
|
|
|
243718
244270
|
);
|
|
243719
244271
|
try {
|
|
243720
244272
|
if (IMAGE_MIME_TYPES.has(att.mimeType)) {
|
|
243721
|
-
const data = await
|
|
244273
|
+
const data = await readFile41(filePath);
|
|
243722
244274
|
const detectedMimeType = detectImageMimeFromBytes(data);
|
|
243723
244275
|
const imageMimeType = detectedMimeType ?? att.mimeType;
|
|
243724
244276
|
if (detectedMimeType && detectedMimeType !== att.mimeType) {
|
|
@@ -243735,7 +244287,7 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
|
|
|
243735
244287
|
text: null
|
|
243736
244288
|
});
|
|
243737
244289
|
} else {
|
|
243738
|
-
const data = await
|
|
244290
|
+
const data = await readFile41(filePath, "utf-8");
|
|
243739
244291
|
const text2 = data.length > TEXT_INLINE_LIMIT ? data.slice(0, TEXT_INLINE_LIMIT) + "\n... (truncated at 50KB)" : data;
|
|
243740
244292
|
attachmentContents.push({
|
|
243741
244293
|
originalName: att.originalName,
|
|
@@ -244963,6 +245515,9 @@ var InProcessRuntime = class extends EventEmitter27 {
|
|
|
244963
245515
|
usageLimitPauser: this.usageLimitPauser,
|
|
244964
245516
|
agentStore: this.agentStore,
|
|
244965
245517
|
pluginRunner: this.pluginRunner,
|
|
245518
|
+
// FNXC:NodeWorktreeIsolation 2026-07-25-22:10: planning acquires (or reuses) the task's own
|
|
245519
|
+
// worktree through the executor's acquisition path, so no lane runs in the shared checkout.
|
|
245520
|
+
acquirePlanningWorktree: (taskId) => this.executor.ensureTaskWorktreeForPlanning(taskId),
|
|
244966
245521
|
onSpecifyStart: (t) => {
|
|
244967
245522
|
this.recordActivity();
|
|
244968
245523
|
runtimeLog.log(`Specifying ${t.id}...`);
|
|
@@ -245051,6 +245606,8 @@ var InProcessRuntime = class extends EventEmitter27 {
|
|
|
245051
245606
|
getExecutingTaskIds: () => this.executor?.getExecutingTaskIds() ?? /* @__PURE__ */ new Set(),
|
|
245052
245607
|
clearPhantomExecutorBinding: (taskId, options) => this.executor?.clearPhantomExecutorBinding(taskId, options),
|
|
245053
245608
|
listWorktreeHolders: () => this.executor?.listWorktreeHolders() ?? [],
|
|
245609
|
+
// FNXC:PlanningEvacuation 2026-07-25-23:00: the executor owns the release safety conditions.
|
|
245610
|
+
releasePreExecutionWorktree: (taskId, reason) => this.executor?.releasePreExecutionWorktree(taskId, reason) ?? Promise.resolve(false),
|
|
245054
245611
|
recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
|
|
245055
245612
|
getPlanningTaskIds: () => this.triageProcessor?.getPlanningTaskIds() ?? /* @__PURE__ */ new Set(),
|
|
245056
245613
|
// FNXC:TaskTiming 2026-08-01-12:00: orphan planning recovery must defer
|
|
@@ -247090,7 +247647,7 @@ function extractAdvisorAssistantText(session) {
|
|
|
247090
247647
|
// ../engine/src/overseer-watchdog.ts
|
|
247091
247648
|
init_logger();
|
|
247092
247649
|
import { existsSync as existsSync67, readFileSync as readFileSync24 } from "node:fs";
|
|
247093
|
-
import { dirname as dirname34, join as
|
|
247650
|
+
import { dirname as dirname34, join as join82, relative as relative17, resolve as resolve39, sep as sep11 } from "node:path";
|
|
247094
247651
|
import { homedir as homedir11 } from "node:os";
|
|
247095
247652
|
var log32 = createLogger("overseer-watchdog");
|
|
247096
247653
|
var WATCHDOG_FILENAMES = ["OVERSEER.md", "WATCHDOG.md"];
|
|
@@ -247121,7 +247678,7 @@ function discoverOverseerWatchdogFiles(options) {
|
|
|
247121
247678
|
};
|
|
247122
247679
|
if (agentDir) {
|
|
247123
247680
|
for (const name2 of WATCHDOG_FILENAMES) {
|
|
247124
|
-
tryAdd(
|
|
247681
|
+
tryAdd(join82(agentDir, name2), "user", 999);
|
|
247125
247682
|
}
|
|
247126
247683
|
}
|
|
247127
247684
|
let current = cwd;
|
|
@@ -247129,9 +247686,9 @@ function discoverOverseerWatchdogFiles(options) {
|
|
|
247129
247686
|
for (let hop = 0; hop < 64; hop++) {
|
|
247130
247687
|
const depth = relative17(cwd, current) === "" ? 0 : relative17(cwd, current).split(sep11).filter(Boolean).length;
|
|
247131
247688
|
for (const name2 of WATCHDOG_FILENAMES) {
|
|
247132
|
-
tryAdd(
|
|
247133
|
-
tryAdd(
|
|
247134
|
-
tryAdd(
|
|
247689
|
+
tryAdd(join82(current, name2), "project", depth);
|
|
247690
|
+
tryAdd(join82(current, ".fusion", name2), "project", depth);
|
|
247691
|
+
tryAdd(join82(current, ".omp", name2), "project", depth);
|
|
247135
247692
|
}
|
|
247136
247693
|
if (current === stopAt) break;
|
|
247137
247694
|
const parent = dirname34(current);
|
|
@@ -248096,7 +248653,7 @@ import { promisify as promisify58 } from "node:util";
|
|
|
248096
248653
|
import { realpathSync as realpathSync11, readdirSync as readdirSync13 } from "node:fs";
|
|
248097
248654
|
import { mkdtemp as mkdtemp4 } from "node:fs/promises";
|
|
248098
248655
|
import { tmpdir as tmpdir11 } from "node:os";
|
|
248099
|
-
import { join as
|
|
248656
|
+
import { join as join84 } from "node:path";
|
|
248100
248657
|
|
|
248101
248658
|
// ../engine/src/overseer-noop-finalize-veto.ts
|
|
248102
248659
|
init_src2();
|
|
@@ -248190,7 +248747,7 @@ import { execFile as execFile28 } from "node:child_process";
|
|
|
248190
248747
|
import { appendFileSync as appendFileSync2, existsSync as existsSync68, mkdirSync as mkdirSync11, readdirSync as readdirSync12, readFileSync as readFileSync25, realpathSync as realpathSync10, rmSync as rmSync9, statSync as statSync13 } from "node:fs";
|
|
248191
248748
|
import { rm as rm14 } from "node:fs/promises";
|
|
248192
248749
|
import { tmpdir as tmpdir10 } from "node:os";
|
|
248193
|
-
import { isAbsolute as isAbsolute21, join as
|
|
248750
|
+
import { isAbsolute as isAbsolute21, join as join83, relative as relative18 } from "node:path";
|
|
248194
248751
|
import { promisify as promisify57 } from "node:util";
|
|
248195
248752
|
init_worktree_paths();
|
|
248196
248753
|
var execFileAsync21 = promisify57(execFile28);
|
|
@@ -248223,7 +248780,7 @@ function isBenignAbsentWorktreeError(err) {
|
|
|
248223
248780
|
return /is not a working tree|No such file or directory|spawn\s+.*\bENOENT\b/i.test(description);
|
|
248224
248781
|
}
|
|
248225
248782
|
function ensureAiMergeRootIgnored(projectRootDir, settings) {
|
|
248226
|
-
const excludePath =
|
|
248783
|
+
const excludePath = join83(projectRootDir, ".git", "info", "exclude");
|
|
248227
248784
|
if (!existsSync68(excludePath)) return;
|
|
248228
248785
|
try {
|
|
248229
248786
|
const current = readFileSync25(excludePath, "utf-8");
|
|
@@ -248255,7 +248812,7 @@ function getAiMergeTempSearchRoots(projectRootDir, settings) {
|
|
|
248255
248812
|
if (testWorkerRoot) {
|
|
248256
248813
|
try {
|
|
248257
248814
|
for (const entry of readdirSync12(testWorkerRoot)) {
|
|
248258
|
-
if (entry.startsWith("redir-")) roots.push(
|
|
248815
|
+
if (entry.startsWith("redir-")) roots.push(join83(testWorkerRoot, entry));
|
|
248259
248816
|
}
|
|
248260
248817
|
} catch {
|
|
248261
248818
|
}
|
|
@@ -248278,7 +248835,7 @@ async function pruneExistingAiMergeWorktrees(taskId, projectRootDir, audit, log3
|
|
|
248278
248835
|
continue;
|
|
248279
248836
|
}
|
|
248280
248837
|
for (const entry of entries) {
|
|
248281
|
-
const candidatePath =
|
|
248838
|
+
const candidatePath = join83(tempRoot, entry);
|
|
248282
248839
|
let canonicalPath = candidatePath;
|
|
248283
248840
|
try {
|
|
248284
248841
|
canonicalPath = realpathSync10(candidatePath);
|
|
@@ -248754,7 +249311,7 @@ function listAiMergeWorktreeCandidates(taskId, projectRootDir, settings) {
|
|
|
248754
249311
|
if (testWorkerRoot) {
|
|
248755
249312
|
try {
|
|
248756
249313
|
for (const entry of readdirSync13(testWorkerRoot)) {
|
|
248757
|
-
if (entry.startsWith("redir-")) roots.push(
|
|
249314
|
+
if (entry.startsWith("redir-")) roots.push(join84(testWorkerRoot, entry));
|
|
248758
249315
|
}
|
|
248759
249316
|
} catch {
|
|
248760
249317
|
}
|
|
@@ -248767,7 +249324,7 @@ function listAiMergeWorktreeCandidates(taskId, projectRootDir, settings) {
|
|
|
248767
249324
|
} catch {
|
|
248768
249325
|
continue;
|
|
248769
249326
|
}
|
|
248770
|
-
for (const entry of entries) candidates.push(
|
|
249327
|
+
for (const entry of entries) candidates.push(join84(root, entry));
|
|
248771
249328
|
}
|
|
248772
249329
|
return candidates;
|
|
248773
249330
|
}
|
|
@@ -249111,7 +249668,7 @@ async function landOneRepo(repoRootDir, branch, integrationBranch, ctx) {
|
|
|
249111
249668
|
registeredMergePaths.add(pathToRegister);
|
|
249112
249669
|
};
|
|
249113
249670
|
try {
|
|
249114
|
-
mergeRoot = await mkdtemp4(
|
|
249671
|
+
mergeRoot = await mkdtemp4(join84(resolveAiMergeRoot(repoRootDir, settings), `fusion-ai-merge-${taskId.toLowerCase()}-`));
|
|
249115
249672
|
registerMergeRoot(mergeRoot);
|
|
249116
249673
|
await git5(["worktree", "add", "--detach", mergeRoot, tipSha], repoRootDir);
|
|
249117
249674
|
worktreeAdded = true;
|
|
@@ -249628,7 +250185,7 @@ async function landWorkspaceTask(store, task, workspaceRootDir, options = {}, de
|
|
|
249628
250185
|
for (const repoRel of repoKeys) {
|
|
249629
250186
|
throwIfAborted2(options.signal, taskId);
|
|
249630
250187
|
const entry = workspaceWorktrees[repoRel];
|
|
249631
|
-
const repoRootDir =
|
|
250188
|
+
const repoRootDir = join84(workspaceRootDir, repoRel);
|
|
249632
250189
|
let integrationBranch;
|
|
249633
250190
|
try {
|
|
249634
250191
|
integrationBranch = await resolveIntegrationBranch(
|
|
@@ -249945,7 +250502,7 @@ async function pushAfterMergeToRemote(input) {
|
|
|
249945
250502
|
let worktreeAdded = false;
|
|
249946
250503
|
const registeredPaths = /* @__PURE__ */ new Set();
|
|
249947
250504
|
try {
|
|
249948
|
-
pushRoot = await mkdtemp4(
|
|
250505
|
+
pushRoot = await mkdtemp4(join84(resolveAiMergeRoot(projectRootDir, settings), `fusion-ai-merge-push-${taskId.toLowerCase()}-`));
|
|
249949
250506
|
for (const p2 of [pushRoot]) {
|
|
249950
250507
|
activeSessionRegistry.registerPath(p2, { taskId, kind: "ai-merge", ownerKey: `ai-merge-push:${taskId}` });
|
|
249951
250508
|
registeredPaths.add(p2);
|