feat(FN-4912): complete Step 6 — wire env writes into acquisition

Fusion-Task-Id: FN-4912
Fusion-Task-Lineage: 943d0651-052a-41b5-8069-4c60f4db1ba7
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 22:49:06 -07:00
committed by gsxdsm
parent a8615ecf6c
commit c8d01ca80d
3 changed files with 38 additions and 3 deletions

View File

@@ -111,6 +111,7 @@ export interface HeartbeatMonitorOptions {
reflectionService?: AgentReflectionService; reflectionService?: AgentReflectionService;
/** Optional self-improvement service for periodic self-improve injection */ /** Optional self-improvement service for periodic self-improve injection */
selfImproveService?: SelfImproveServiceLike; selfImproveService?: SelfImproveServiceLike;
secretsStore?: Pick<import("@fusion/core").SecretsStore, "listEnvExportable">;
} }
/** Options for waking up an agent */ /** Options for waking up an agent */
@@ -2397,6 +2398,7 @@ export class HeartbeatMonitor {
audit, audit,
runContext, runContext,
runInitCommand: false, runInitCommand: false,
secretsStore: this.options.secretsStore,
}); });
sessionCwd = acquisition.worktreePath; sessionCwd = acquisition.worktreePath;
} catch (worktreeErr) { } catch (worktreeErr) {

View File

@@ -902,6 +902,7 @@ export interface TaskExecutorOptions {
/** MessageStore for sending messages to other agents. When provided, executor agents gain fn_send_message capability. */ /** MessageStore for sending messages to other agents. When provided, executor agents gain fn_send_message capability. */
messageStore?: import("@fusion/core").MessageStore; messageStore?: import("@fusion/core").MessageStore;
missionStore?: MissionStore; missionStore?: MissionStore;
secretsStore?: Pick<import("@fusion/core").SecretsStore, "listEnvExportable">;
onSliceComplete?: (slice: Slice) => void; onSliceComplete?: (slice: Slice) => void;
onStart?: (task: Task, worktreePath: string) => void; onStart?: (task: Task, worktreePath: string) => void;
onComplete?: (task: Task) => void; onComplete?: (task: Task) => void;
@@ -2860,6 +2861,7 @@ export class TaskExecutor {
createWorktree: this.createWorktree.bind(this), createWorktree: this.createWorktree.bind(this),
runConfiguredCommand, runConfiguredCommand,
taskEnv, taskEnv,
secretsStore: this.options.secretsStore,
}); });
worktreePath = acquisition.worktreePath; worktreePath = acquisition.worktreePath;

View File

@@ -1,7 +1,7 @@
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { exec } from "node:child_process"; import { exec } from "node:child_process";
import { promisify } from "node:util"; import { promisify } from "node:util";
import type { RunMutationContext, Settings, Task, TaskStore } from "@fusion/core"; import type { RunMutationContext, Settings, Task, TaskStore, SecretsStore } from "@fusion/core";
import { generateWorktreeName, slugify } from "./worktree-names.js"; import { generateWorktreeName, slugify } from "./worktree-names.js";
import { resolveTaskWorktreePathForBackend } from "./worktree-paths.js"; import { resolveTaskWorktreePathForBackend } from "./worktree-paths.js";
import { hydrateWorktreeDb } from "./worktree-db-hydrate.js"; import { hydrateWorktreeDb } from "./worktree-db-hydrate.js";
@@ -32,6 +32,7 @@ import {
type WorktrunkOpName, type WorktrunkOpName,
} from "./worktrunk-failure-handler.js"; } from "./worktrunk-failure-handler.js";
import type { RunAuditor } from "./run-audit.js"; import type { RunAuditor } from "./run-audit.js";
import { writeSecretsEnvFile } from "./secrets-env-writer.js";
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -48,9 +49,10 @@ export interface AcquireTaskWorktreeOptions {
settings: Partial<Settings>; settings: Partial<Settings>;
pool?: WorktreePool; pool?: WorktreePool;
logger?: { log: (m: string) => void; warn: (m: string) => void; error?: (m: string) => void }; logger?: { log: (m: string) => void; warn: (m: string) => void; error?: (m: string) => void };
audit?: RunAuditor; audit?: Pick<RunAuditor, "git" | "filesystem">;
runContext?: RunMutationContext; runContext?: RunMutationContext;
runInitCommand?: boolean; runInitCommand?: boolean;
secretsStore?: Pick<SecretsStore, "listEnvExportable">;
createWorktree?: ( createWorktree?: (
branch: string, branch: string,
path: string, path: string,
@@ -153,7 +155,7 @@ async function maybeWarnForeignTaskStartPoint(
} }
export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Promise<AcquireTaskWorktreeResult> { export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Promise<AcquireTaskWorktreeResult> {
const { task, rootDir, store, settings, pool, logger, audit, runContext, createWorktree, runConfiguredCommand, runInitCommand, taskEnv } = opts; const { task, rootDir, store, settings, pool, logger, audit, runContext, createWorktree, runConfiguredCommand, runInitCommand, taskEnv, secretsStore } = opts;
const notifyFallback = async (op: WorktrunkOpName, stderr?: string) => { const notifyFallback = async (op: WorktrunkOpName, stderr?: string) => {
await store.logEntry(task.id, `Worktrunk ${op} failed; continuing with native worktree backend (${stderr ?? "no stderr"})`, undefined, runContext); await store.logEntry(task.id, `Worktrunk ${op} failed; continuing with native worktree backend (${stderr ?? "no stderr"})`, undefined, runContext);
}; };
@@ -251,6 +253,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
if (task.worktree && isResume) { if (task.worktree && isResume) {
logger?.log(`Reusing existing worktree: ${worktreePath}`); logger?.log(`Reusing existing worktree: ${worktreePath}`);
const hydrated = await hydrate(worktreePath); const hydrated = await hydrate(worktreePath);
// FN-4912: resume path reuses the prior on-disk .env (and its fingerprint sidecar). Rewrite is owned by the next fresh acquisition.
return { worktreePath, branch: task.branch ?? branchName, source: "existing", hydrated, isResume: true }; return { worktreePath, branch: task.branch ?? branchName, source: "existing", hydrated, isResume: true };
} }
@@ -338,6 +341,20 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
runContext, runContext,
}); });
const hydrated = await hydrate(worktreePath); const hydrated = await hydrate(worktreePath);
try {
await writeSecretsEnvFile({
rootDir,
worktreePath,
taskId: task.id,
settings,
worktreeSource: "pool",
secretsStore,
audit,
logger,
});
} catch (err) {
logger?.warn?.(`${task.id}: secrets-env write failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
}
return { return {
worktreePath, worktreePath,
branch, branch,
@@ -449,5 +466,19 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
runContext, runContext,
}); });
const hydrated = await hydrate(worktreePath); const hydrated = await hydrate(worktreePath);
try {
await writeSecretsEnvFile({
rootDir,
worktreePath,
taskId: task.id,
settings,
worktreeSource: "fresh",
secretsStore,
audit,
logger,
});
} catch (err) {
logger?.warn?.(`${task.id}: secrets-env write failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
}
return { worktreePath, branch, source: acquiredFromPool ? "pool" : "fresh", hydrated, isResume: false }; return { worktreePath, branch, source: acquiredFromPool ? "pool" : "fresh", hydrated, isResume: false };
} }