fix(RUFU-018): add noCommitsExpected dep-sync skip and corepack/pnpm env passthrough (#2501)
Manually land RUFU-018 fix bypassing the AI merge pipeline. ## Summary - Add `noCommitsExpected` flag to `LandRepoContext`; skip dependency sync when set - Forward `COREPACK_HOME`/`PNPM_HOME`/`npm_config_registry` in `installWorktreeDependencies` - Add comprehensive tests for both changes This unblocks all downstream RUFU audit tasks. ## Surface Enumeration - Providers/bridges: `installWorktreeDependencies` called from `landOneRepo` (AI merge) and legacy `merger.ts`; `landOneRepo` called from `runAiMerge` and `landWorkspaceTask` - Data states: `noCommitsExpected` can be `true`, `false`, or `undefined` — both callers use `=== true` strict check <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Improved support for tasks that do not produce commits by skipping unnecessary dependency installation during merges. - Preserved normal merge and review behavior when dependency installation is skipped. - **Bug Fixes** - Dependency installation now correctly preserves relevant package-manager and system environment settings. - Reduced installation failures caused by missing or unavailable package-manager configuration. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Fusion <noreply@runfusion.ai> Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
This commit is contained in:
@@ -135,3 +135,127 @@ describe("installWorktreeDependencies lockfile auto-heal", () => {
|
||||
expect(readLog(logPath)).toEqual([["install", "--frozen-lockfile"]]);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:MergeDeps 2026-07-17-12:00:
|
||||
Env passthrough coverage for installWorktreeDependencies. The explicit forwarding of corepack/pnpm
|
||||
env vars mirrors mission-verification.ts VERIFICATION_ENV_ALLOWLIST so pnpm is resolvable even when
|
||||
the engine process starts without full shell initialization.
|
||||
*/
|
||||
describe("installWorktreeDependencies env passthrough", () => {
|
||||
/**
|
||||
* Install a fake `pnpm` that logs selected env vars to a file so we can assert
|
||||
* the child process receives the expected environment. Writes a JSON object with
|
||||
* the requested env var values.
|
||||
*/
|
||||
function installEnvLoggingPnpm(envVars: string[], logPath: string): string {
|
||||
const binDir = tmp("fusion-env-fake-bin-");
|
||||
const script = join(binDir, "pnpm");
|
||||
const varsJson = JSON.stringify(envVars);
|
||||
writeFileSync(
|
||||
script,
|
||||
`#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const vars = ${varsJson};
|
||||
const env = {};
|
||||
for (let v of vars) env[v] = process.env[v];
|
||||
fs.writeFileSync(${JSON.stringify(logPath)}, JSON.stringify(env));
|
||||
`,
|
||||
);
|
||||
chmodSync(script, 0o755);
|
||||
const previousPath = process.env.PATH ?? "";
|
||||
process.env.PATH = `${binDir}${delimiter}${previousPath}`;
|
||||
return previousPath;
|
||||
}
|
||||
|
||||
it("passes COREPACK_HOME, PNPM_HOME, and npm_config_registry through to exec", async () => {
|
||||
// Set the env vars so the passthrough has values to forward
|
||||
const origCorepackHome = process.env.COREPACK_HOME;
|
||||
const origPnpmHome = process.env.PNPM_HOME;
|
||||
const origNpmRegistry = process.env.npm_config_registry;
|
||||
process.env.COREPACK_HOME = "/tmp/fake-corepack";
|
||||
process.env.PNPM_HOME = "/tmp/fake-pnpm";
|
||||
process.env.npm_config_registry = "https://fake.registry/";
|
||||
|
||||
const dir = tmp("fusion-env-repo-");
|
||||
writeFileSync(join(dir, "pnpm-lock.yaml"), "lockfile: {}\n");
|
||||
|
||||
const logPath = join(tmp("fusion-env-log-"), "env.json");
|
||||
const previousPath = installEnvLoggingPnpm(
|
||||
["COREPACK_HOME", "PNPM_HOME", "npm_config_registry"],
|
||||
logPath,
|
||||
);
|
||||
try {
|
||||
await installWorktreeDependencies({ cwd: dir, taskId: "FN-1" });
|
||||
|
||||
const captured = JSON.parse(readFileSync(logPath, "utf-8"));
|
||||
expect(captured.COREPACK_HOME).toBe("/tmp/fake-corepack");
|
||||
expect(captured.PNPM_HOME).toBe("/tmp/fake-pnpm");
|
||||
expect(captured.npm_config_registry).toBe("https://fake.registry/");
|
||||
} finally {
|
||||
process.env.PATH = previousPath;
|
||||
process.env.COREPACK_HOME = origCorepackHome;
|
||||
process.env.PNPM_HOME = origPnpmHome;
|
||||
process.env.npm_config_registry = origNpmRegistry;
|
||||
}
|
||||
});
|
||||
|
||||
it("does NOT override or strip existing env vars like PATH", async () => {
|
||||
const dir = tmp("fusion-env-repo2-");
|
||||
writeFileSync(join(dir, "pnpm-lock.yaml"), "lockfile: {}\n");
|
||||
|
||||
const logPath = join(tmp("fusion-env-log2-"), "env.json");
|
||||
const previousPath = installEnvLoggingPnpm(
|
||||
["PATH", "HOME", "SHELL"],
|
||||
logPath,
|
||||
);
|
||||
try {
|
||||
await installWorktreeDependencies({ cwd: dir, taskId: "FN-1" });
|
||||
|
||||
const captured = JSON.parse(readFileSync(logPath, "utf-8"));
|
||||
// PATH should still contain the fake bin dir AND the real PATH
|
||||
expect(captured.PATH).toContain("fusion-env-fake-bin-");
|
||||
// HOME and SHELL should be preserved from process.env
|
||||
expect(captured.HOME).toBe(process.env.HOME);
|
||||
expect(captured.SHELL).toBe(process.env.SHELL);
|
||||
} finally {
|
||||
process.env.PATH = previousPath;
|
||||
}
|
||||
});
|
||||
|
||||
it("handles undefined corepack/pnpm env vars gracefully", async () => {
|
||||
// Clear the env vars
|
||||
const origCorepackHome = process.env.COREPACK_HOME;
|
||||
const origPnpmHome = process.env.PNPM_HOME;
|
||||
const origNpmRegistry = process.env.npm_config_registry;
|
||||
delete process.env.COREPACK_HOME;
|
||||
delete process.env.PNPM_HOME;
|
||||
delete process.env.npm_config_registry;
|
||||
|
||||
const dir = tmp("fusion-env-repo3-");
|
||||
writeFileSync(join(dir, "pnpm-lock.yaml"), "lockfile: {}\n");
|
||||
|
||||
const logPath = join(tmp("fusion-env-log3-"), "env.json");
|
||||
const previousPath = installEnvLoggingPnpm(
|
||||
["COREPACK_HOME", "PNPM_HOME", "npm_config_registry", "PATH"],
|
||||
logPath,
|
||||
);
|
||||
try {
|
||||
await installWorktreeDependencies({ cwd: dir, taskId: "FN-1" });
|
||||
|
||||
const captured = JSON.parse(readFileSync(logPath, "utf-8"));
|
||||
// When the env vars are undefined, they should be undefined in the child too
|
||||
// (not set to empty string or some sentinel)
|
||||
expect(captured.COREPACK_HOME).toBeUndefined();
|
||||
expect(captured.PNPM_HOME).toBeUndefined();
|
||||
expect(captured.npm_config_registry).toBeUndefined();
|
||||
// PATH should still be present
|
||||
expect(captured.PATH).toBeDefined();
|
||||
} finally {
|
||||
process.env.PATH = previousPath;
|
||||
process.env.COREPACK_HOME = origCorepackHome;
|
||||
process.env.PNPM_HOME = origPnpmHome;
|
||||
process.env.npm_config_registry = origNpmRegistry;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
FNXC:MergeNoCommits 2026-07-17-12:00:
|
||||
No-commits tasks (audit, documentation, decision-only) have no code changes to install or build.
|
||||
The clean-room dependency sync must be skipped entirely to avoid "pnpm: command not found" failures
|
||||
when pnpm is not resolvable in the engine process environment. We drive the REAL landOneRepo against
|
||||
a REAL git fixture with injected agents (the squash is a plain `git merge --squash`, no AI), and
|
||||
MOCK installWorktreeDependencies so we can assert call counts without real/slow/networked npm runs
|
||||
(FN-5048).
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { execSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Task, TaskStore } from "@fusion/core";
|
||||
|
||||
vi.mock("../merge-dependency-sync.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../merge-dependency-sync.js")>();
|
||||
return { ...actual, installWorktreeDependencies: vi.fn() };
|
||||
});
|
||||
|
||||
import { installWorktreeDependencies } from "../merge-dependency-sync.js";
|
||||
import { landOneRepo } from "../merger-ai.js";
|
||||
import { createRunAuditor, generateSyntheticRunId } from "../run-audit.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function hasGitInstall(): boolean {
|
||||
try {
|
||||
execSync("git --version", { stdio: "pipe" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const describeIfGit = hasGitInstall() ? describe : describe.skip;
|
||||
const TASK_ID = "RUFU-018";
|
||||
const BRANCH = "fusion/rufu-018";
|
||||
|
||||
function configureIdentity(dir: string): void {
|
||||
execSync('git config user.email "test@example.com"', { cwd: dir, stdio: "pipe" });
|
||||
execSync('git config user.name "Test"', { cwd: dir, stdio: "pipe" });
|
||||
}
|
||||
|
||||
interface RepoFixture {
|
||||
rootDir: string;
|
||||
mainSha: string;
|
||||
cleanup(): void;
|
||||
}
|
||||
|
||||
/** Create a single real git repo with an initial commit on main, then a branch
|
||||
* with one commit. Returns the fixture. */
|
||||
function createRepoFixture(withChanges: boolean): RepoFixture {
|
||||
const rootDir = mkdtempSync(path.join(os.tmpdir(), "fusion-no-commits-"));
|
||||
execSync("git init -b main", { cwd: rootDir, stdio: "pipe" });
|
||||
configureIdentity(rootDir);
|
||||
writeFileSync(path.join(rootDir, "README.md"), "# Test\n", "utf-8");
|
||||
execSync("git add README.md && git commit -m 'init'", { cwd: rootDir, stdio: "pipe" });
|
||||
|
||||
// Create the task branch
|
||||
execSync(`git checkout -b ${BRANCH}`, { cwd: rootDir, stdio: "pipe" });
|
||||
if (withChanges) {
|
||||
writeFileSync(path.join(rootDir, "feature.txt"), "feature work\n", "utf-8");
|
||||
execSync("git add feature.txt && git commit -m 'feat: add feature'", { cwd: rootDir, stdio: "pipe" });
|
||||
} else {
|
||||
// Commit that adds no code (e.g. a readme-only doc change), so the branch is ahead
|
||||
writeFileSync(path.join(rootDir, "README.md"), "# Test\n\nUpdated\n", "utf-8");
|
||||
execSync("git add README.md && git commit -m 'docs: update readme'", { cwd: rootDir, stdio: "pipe" });
|
||||
}
|
||||
const mainSha = execSync("git rev-parse main", { cwd: rootDir, encoding: "utf-8" }).trim();
|
||||
execSync("git checkout main", { cwd: rootDir, stdio: "pipe" });
|
||||
|
||||
return {
|
||||
rootDir,
|
||||
mainSha,
|
||||
cleanup: () => { try { rmSync(rootDir, { recursive: true, force: true }); } catch { /* best effort */ } },
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Store
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createStore(): TaskStore & { logs: string[] } {
|
||||
const emitter = new EventEmitter();
|
||||
const logs: string[] = [];
|
||||
return Object.assign(emitter, {
|
||||
logs,
|
||||
getSettings: vi.fn().mockResolvedValue({ autoMerge: false }),
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
logEntry: vi.fn((_id: string, message: string) => { logs.push(message); return Promise.resolve(undefined); }),
|
||||
appendAgentLog: vi.fn().mockResolvedValue(undefined),
|
||||
getTask: vi.fn().mockResolvedValue({
|
||||
id: TASK_ID, column: "in-review", branch: BRANCH,
|
||||
comments: [], steeringComments: [], steps: [], log: [],
|
||||
}),
|
||||
moveTask: vi.fn().mockResolvedValue({ id: TASK_ID, column: "done" } as Task),
|
||||
upsertTaskCommitAssociation: vi.fn().mockResolvedValue(undefined),
|
||||
accumulateTokenUsage: vi.fn().mockResolvedValue(undefined),
|
||||
}) as unknown as TaskStore & { logs: string[] };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Injected agents (plain git merge, no AI)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const squashMergeAgent = async (cwd: string): Promise<void> => {
|
||||
configureIdentity(cwd);
|
||||
execSync(`git merge --squash ${BRANCH}`, { cwd, stdio: "pipe" });
|
||||
const staged = execSync("git diff --cached --name-only", { cwd, encoding: "utf-8" }).trim();
|
||||
if (staged.length === 0) return;
|
||||
execSync(`git commit -m "${BRANCH}: squashed"`, { cwd, stdio: "pipe" });
|
||||
};
|
||||
const approveReviewAgent = async (): Promise<string> => "REVIEW_VERDICT: approve";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describeIfGit("landOneRepo no-commits dep-sync skip", () => {
|
||||
let fx: RepoFixture;
|
||||
let store: TaskStore & { logs: string[] };
|
||||
let audit: ReturnType<typeof createRunAuditor>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fx?.cleanup();
|
||||
});
|
||||
|
||||
it("skips installWorktreeDependencies entirely when noCommitsExpected: true", async () => {
|
||||
fx = createRepoFixture(true); // branch has actual changes
|
||||
store = createStore();
|
||||
audit = createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("ai-merge", TASK_ID),
|
||||
agentId: "merger",
|
||||
taskId: TASK_ID,
|
||||
phase: "merge",
|
||||
});
|
||||
|
||||
const result = await landOneRepo(fx.rootDir, BRANCH, "main", {
|
||||
taskId: TASK_ID,
|
||||
settings: { autoMerge: false } as never,
|
||||
audit,
|
||||
log: async () => undefined,
|
||||
setStatus: async () => undefined,
|
||||
maxPasses: 1,
|
||||
mergeAgent: squashMergeAgent,
|
||||
reviewAgent: approveReviewAgent,
|
||||
stashResolveAgent: async () => undefined,
|
||||
includeTaskId: true,
|
||||
trailers: [],
|
||||
store,
|
||||
noCommitsExpected: true,
|
||||
});
|
||||
|
||||
// Dep sync was never called
|
||||
expect(vi.mocked(installWorktreeDependencies)).not.toHaveBeenCalled();
|
||||
// The land still succeeds
|
||||
expect(result.outcome).toBe("landed");
|
||||
});
|
||||
|
||||
it("still calls installWorktreeDependencies when noCommitsExpected is false", async () => {
|
||||
fx = createRepoFixture(true); // branch has actual changes
|
||||
store = createStore();
|
||||
audit = createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("ai-merge", TASK_ID),
|
||||
agentId: "merger",
|
||||
taskId: TASK_ID,
|
||||
phase: "merge",
|
||||
});
|
||||
|
||||
const result = await landOneRepo(fx.rootDir, BRANCH, "main", {
|
||||
taskId: TASK_ID,
|
||||
settings: { autoMerge: false } as never,
|
||||
audit,
|
||||
log: async () => undefined,
|
||||
setStatus: async () => undefined,
|
||||
maxPasses: 1,
|
||||
mergeAgent: squashMergeAgent,
|
||||
reviewAgent: approveReviewAgent,
|
||||
stashResolveAgent: async () => undefined,
|
||||
includeTaskId: true,
|
||||
trailers: [],
|
||||
store,
|
||||
// noCommitsExpected intentionally omitted (defaults to undefined → false)
|
||||
});
|
||||
|
||||
// Dep sync WAS called
|
||||
expect(vi.mocked(installWorktreeDependencies)).toHaveBeenCalled();
|
||||
// The land still succeeds
|
||||
expect(result.outcome).toBe("landed");
|
||||
});
|
||||
|
||||
it("still calls installWorktreeDependencies when noCommitsExpected is explicitly false", async () => {
|
||||
fx = createRepoFixture(true); // branch has actual changes
|
||||
store = createStore();
|
||||
audit = createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("ai-merge", TASK_ID),
|
||||
agentId: "merger",
|
||||
taskId: TASK_ID,
|
||||
phase: "merge",
|
||||
});
|
||||
|
||||
const result = await landOneRepo(fx.rootDir, BRANCH, "main", {
|
||||
taskId: TASK_ID,
|
||||
settings: { autoMerge: false } as never,
|
||||
audit,
|
||||
log: async () => undefined,
|
||||
setStatus: async () => undefined,
|
||||
maxPasses: 1,
|
||||
mergeAgent: squashMergeAgent,
|
||||
reviewAgent: approveReviewAgent,
|
||||
stashResolveAgent: async () => undefined,
|
||||
includeTaskId: true,
|
||||
trailers: [],
|
||||
store,
|
||||
noCommitsExpected: false,
|
||||
});
|
||||
|
||||
// Dep sync WAS called
|
||||
expect(vi.mocked(installWorktreeDependencies)).toHaveBeenCalled();
|
||||
expect(result.outcome).toBe("landed");
|
||||
});
|
||||
|
||||
it("lands successfully with noCommitsExpected: true and actual changes", async () => {
|
||||
fx = createRepoFixture(true); // branch has actual file changes
|
||||
store = createStore();
|
||||
audit = createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("ai-merge", TASK_ID),
|
||||
agentId: "merger",
|
||||
taskId: TASK_ID,
|
||||
phase: "merge",
|
||||
});
|
||||
|
||||
const result = await landOneRepo(fx.rootDir, BRANCH, "main", {
|
||||
taskId: TASK_ID,
|
||||
settings: { autoMerge: false } as never,
|
||||
audit,
|
||||
log: async () => undefined,
|
||||
setStatus: async () => undefined,
|
||||
maxPasses: 1,
|
||||
mergeAgent: squashMergeAgent,
|
||||
reviewAgent: approveReviewAgent,
|
||||
stashResolveAgent: async () => undefined,
|
||||
includeTaskId: true,
|
||||
trailers: [],
|
||||
store,
|
||||
noCommitsExpected: true,
|
||||
});
|
||||
|
||||
expect(result.outcome).toBe("landed");
|
||||
expect(vi.mocked(installWorktreeDependencies)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -166,9 +166,25 @@ export async function installWorktreeDependencies(options: InstallWorktreeDepend
|
||||
logger?.log?.(`${taskId}: syncing dependencies ${context}`);
|
||||
await log?.(`Syncing dependencies ${context}: ${installCommand}`);
|
||||
|
||||
/*
|
||||
FNXC:MergeDeps 2026-07-17-12:00:
|
||||
Pass corepack/pnpm env vars (PNPM_HOME, COREPACK_HOME, npm_config_registry) to the exec
|
||||
child process — mirrors mission-verification.ts VERIFICATION_ENV_ALLOWLIST so pnpm is
|
||||
resolvable even when the engine process starts without full shell initialization. Without
|
||||
these vars, corepack cannot locate its pnpm shim and "pnpm: command not found" occurs.
|
||||
*/
|
||||
const resolvedEnv: NodeJS.ProcessEnv = { ...process.env };
|
||||
const PNPM_ENV_VARS = ["COREPACK_HOME", "PNPM_HOME", "npm_config_registry"] as const;
|
||||
for (const key of PNPM_ENV_VARS) {
|
||||
const value = process.env[key];
|
||||
if (value !== undefined) {
|
||||
resolvedEnv[key] = value;
|
||||
}
|
||||
}
|
||||
const runInstall = (command: string): Promise<unknown> =>
|
||||
execAsync(command, {
|
||||
cwd,
|
||||
env: resolvedEnv,
|
||||
encoding: "utf-8",
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: INSTALL_TIMEOUT_MS,
|
||||
|
||||
@@ -763,6 +763,14 @@ export interface LandRepoContext {
|
||||
off, preserving the documented hard-fail for the single-repo land path.
|
||||
*/
|
||||
nonFatalDependencySync?: boolean;
|
||||
/*
|
||||
FNXC:MergeNoCommits 2026-07-17-12:00:
|
||||
When true, the task is expected to produce no code changes (audit, documentation, decision-only).
|
||||
The clean-room dependency sync is skipped entirely because there are no source changes to install
|
||||
or build. Avoiding the dep-sync prevents "pnpm: command not found" failures when pnpm is not
|
||||
resolvable in the engine process environment, and avoids unnecessary work.
|
||||
*/
|
||||
noCommitsExpected?: boolean;
|
||||
store: TaskStore;
|
||||
}
|
||||
|
||||
@@ -883,6 +891,17 @@ export async function landOneRepo(
|
||||
* FNXC:AIMerge 2026-06-13-20:32:
|
||||
* The detached AI-merge clean room is rebuilt from the integration tip and starts without workspace dependencies. Hard-fail configured or inferred install failures so verification cannot silently run against an uninstalled checkout; aborts propagate before merge agents run.
|
||||
*/
|
||||
/*
|
||||
FNXC:MergeNoCommits 2026-07-17-12:00:
|
||||
No-commits tasks (audit, documentation, decision-only) have no code changes to install or
|
||||
build. Skip the entire dependency-sync step in the clean-room worktree to avoid "pnpm: command
|
||||
not found" when pnpm is not resolvable in the engine process environment. The merge/review
|
||||
agents still run (they may verify documentation or produce merge metadata); only the
|
||||
dependency install is skipped.
|
||||
*/
|
||||
if (ctx.noCommitsExpected === true) {
|
||||
await log(`AI merge: skipping dependency sync — no-commits task (no code changes expected)`);
|
||||
} else {
|
||||
const depsSyncStartedAt = Date.now();
|
||||
let depsSyncResult: Awaited<ReturnType<typeof installWorktreeDependencies>> | null = null;
|
||||
try {
|
||||
@@ -937,6 +956,7 @@ export async function landOneRepo(
|
||||
});
|
||||
}
|
||||
await log(`[timing] AI merge dependency sync completed in ${Date.now() - depsSyncStartedAt}ms${depsSyncResult ? (depsSyncResult.installCommand ? ` (${depsSyncResult.skipped ? "skipped" : "ran"}: ${depsSyncResult.installCommand})` : " (no command)") : " (failed — non-fatal, deps unavailable)"}`);
|
||||
}
|
||||
|
||||
// 2 + 3. Merge + review loop (corrective passes).
|
||||
const reviewResult = await mergeAndReview({
|
||||
@@ -1281,6 +1301,8 @@ export async function runAiMerge(
|
||||
mergeAgent, reviewAgent, stashResolveAgent,
|
||||
includeTaskId, trailers, taskTitle, signal: options.signal,
|
||||
allowDirtyLocalCheckoutSync,
|
||||
// FNXC:MergeNoCommits 2026-07-17-12:00: no-commits tasks skip dependency sync in the clean room
|
||||
noCommitsExpected: task.noCommitsExpected === true,
|
||||
store,
|
||||
});
|
||||
|
||||
@@ -1877,6 +1899,8 @@ export async function landWorkspaceTask(
|
||||
// FNXC:Workspace 2026-06-24-23:50: one sub-repo's dependency-sync failure must not block
|
||||
// landing the others — degrade verification for that repo, still land the git squash.
|
||||
nonFatalDependencySync: true,
|
||||
// FNXC:MergeNoCommits 2026-07-17-12:00: no-commits tasks skip dependency sync in the clean room
|
||||
noCommitsExpected: task.noCommitsExpected === true,
|
||||
store,
|
||||
});
|
||||
if (landResult.outcome === "landed") {
|
||||
|
||||
Reference in New Issue
Block a user