Upgrade to v0.39.0: reapply fork patches on clean upstream base
Some checks failed
PR Checks / Lint (push) Has been cancelled
PR Checks / Typecheck (push) Has been cancelled
PR Checks / Build (push) Has been cancelled
PR Checks / Test shard 1/4 (push) Has been cancelled
PR Checks / Test shard 2/4 (push) Has been cancelled
PR Checks / Test shard 3/4 (push) Has been cancelled
PR Checks / Test shard 4/4 (push) Has been cancelled
Some checks failed
PR Checks / Lint (push) Has been cancelled
PR Checks / Typecheck (push) Has been cancelled
PR Checks / Build (push) Has been cancelled
PR Checks / Test shard 1/4 (push) Has been cancelled
PR Checks / Test shard 2/4 (push) Has been cancelled
PR Checks / Test shard 3/4 (push) Has been cancelled
PR Checks / Test shard 4/4 (push) Has been cancelled
Strategy: reset main to upstream/main (v0.39.0) and reapply only the fork-specific patches that survive — most older patches (plugin-loader routing, heartbeat resolver, autoload, cross-spawn, scope-resolution fixes) were absorbed by upstream between v0.28.1 → v0.39.0. Surviving patches: - Dockerfile: keep single-stage Coolify-compatible build (upstream went multi-stage which breaks our bind-mount strategy for /home/s/.codex, /home/s/.claude, /usr/lib/node_modules) - packages/engine/src/sandbox-tester-gate.ts: shared Phase-3 pre-merge gate module (Opus 4.7 + OpenSandbox + Playwright smoke) - packages/engine/src/executor.ts: call runSandboxTesterGate from finalizeAlreadyReviewedTask recovery path - packages/engine/src/project-engine.ts: call applySandboxTesterGate from primary direct-merge dispatch (the path that actually fires in prod — recovery path only catches stuck tasks) - packages/dashboard/src/auth-middleware.ts: registerPluginExemptPath for webhook endpoints (Grafana, Gitea) that must bypass daemon-token - packages/dashboard/src/plugin-routes.ts: auto-register plugin-defined routes as daemon-exempt so external plugins (telemetry-watcher, gitea-issues) work out of the box Tested locally: pnpm install + pnpm -r build green, no TS errors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
78
Dockerfile
78
Dockerfile
@@ -1,88 +1,40 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:22-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git build-essential python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY packages/cli/package.json ./packages/cli/package.json
|
||||
COPY packages/cli-alias/package.json ./packages/cli-alias/package.json
|
||||
COPY packages/core/package.json ./packages/core/package.json
|
||||
COPY packages/dashboard/package.json ./packages/dashboard/package.json
|
||||
COPY packages/desktop/package.json ./packages/desktop/package.json
|
||||
COPY packages/droid-cli/package.json ./packages/droid-cli/package.json
|
||||
COPY packages/engine/package.json ./packages/engine/package.json
|
||||
COPY packages/mobile/package.json ./packages/mobile/package.json
|
||||
COPY packages/pi-claude-cli/package.json ./packages/pi-claude-cli/package.json
|
||||
COPY packages/pi-llama-cpp/package.json ./packages/pi-llama-cpp/package.json
|
||||
COPY packages/plugin-sdk/package.json ./packages/plugin-sdk/package.json
|
||||
COPY plugins/examples/fusion-plugin-auto-label/package.json ./plugins/examples/fusion-plugin-auto-label/package.json
|
||||
COPY plugins/examples/fusion-plugin-ci-status/package.json ./plugins/examples/fusion-plugin-ci-status/package.json
|
||||
COPY plugins/examples/fusion-plugin-notification/package.json ./plugins/examples/fusion-plugin-notification/package.json
|
||||
COPY plugins/examples/fusion-plugin-settings-demo/package.json ./plugins/examples/fusion-plugin-settings-demo/package.json
|
||||
COPY plugins/fusion-plugin-paperclip-runtime/package.json ./plugins/fusion-plugin-paperclip-runtime/package.json
|
||||
COPY plugins/fusion-plugin-dependency-graph/package.json ./plugins/fusion-plugin-dependency-graph/package.json
|
||||
COPY plugins/fusion-plugin-cli-printing-press/package.json ./plugins/fusion-plugin-cli-printing-press/package.json
|
||||
COPY plugins/fusion-plugin-openclaw-runtime/package.json ./plugins/fusion-plugin-openclaw-runtime/package.json
|
||||
COPY plugins/fusion-plugin-hermes-runtime/package.json ./plugins/fusion-plugin-hermes-runtime/package.json
|
||||
COPY plugins/fusion-plugin-droid-runtime/package.json ./plugins/fusion-plugin-droid-runtime/package.json
|
||||
COPY plugins/fusion-plugin-cursor-runtime/package.json ./plugins/fusion-plugin-cursor-runtime/package.json
|
||||
COPY plugins/fusion-plugin-agent-browser/package.json ./plugins/fusion-plugin-agent-browser/package.json
|
||||
COPY plugins/fusion-plugin-whatsapp-chat/package.json ./plugins/fusion-plugin-whatsapp-chat/package.json
|
||||
COPY plugins/fusion-plugin-roadmap/package.json ./plugins/fusion-plugin-roadmap/package.json
|
||||
COPY plugins/fusion-plugin-even-realities-glasses/package.json ./plugins/fusion-plugin-even-realities-glasses/package.json
|
||||
COPY plugins/fusion-plugin-reports/package.json ./plugins/fusion-plugin-reports/package.json
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM node:22-slim AS runner
|
||||
LABEL org.opencontainers.image.source="https://github.com/gsxdsm/fusion"
|
||||
FROM node:22-slim
|
||||
LABEL org.opencontainers.image.source="https://github.com/Runfusion/Fusion"
|
||||
LABEL org.opencontainers.image.description="AI-orchestrated task board"
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=4040
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git \
|
||||
&& apt-get install -y --no-install-recommends git build-essential python3 curl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
WORKDIR /project
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY packages/cli/package.json ./packages/cli/package.json
|
||||
COPY packages/core/package.json ./packages/core/package.json
|
||||
COPY packages/dashboard/package.json ./packages/dashboard/package.json
|
||||
COPY packages/engine/package.json ./packages/engine/package.json
|
||||
COPY packages ./packages
|
||||
COPY plugins ./plugins
|
||||
|
||||
RUN pnpm install --frozen-lockfile --prod \
|
||||
--filter @runfusion/fusion
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY --from=builder /app/packages/core/dist ./packages/core/dist
|
||||
COPY --from=builder /app/packages/engine/dist ./packages/engine/dist
|
||||
COPY --from=builder /app/packages/dashboard/dist ./packages/dashboard/dist
|
||||
COPY --from=builder /app/packages/cli/dist ./packages/cli/dist
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
# @runfusion/fusion references typebox at runtime via the bundled CLI.
|
||||
COPY --from=builder /app/node_modules/.pnpm/typebox@*/node_modules/typebox /project/node_modules/typebox
|
||||
RUN mkdir -p /project && chown -R node:node /project /app
|
||||
|
||||
RUN chown node:node /project
|
||||
RUN ln -s /usr/lib/node_modules/@openai/codex/bin/codex.js /usr/local/bin/codex
|
||||
|
||||
USER node
|
||||
WORKDIR /project
|
||||
|
||||
EXPOSE 4040
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD node -e "fetch('http://localhost:4040/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||||
CMD curl -fsS -o /dev/null http://localhost:4040/ || exit 1
|
||||
|
||||
ENTRYPOINT ["node", "packages/cli/dist/bin.js"]
|
||||
ENTRYPOINT ["node", "/app/packages/cli/dist/bin.js"]
|
||||
CMD ["dashboard", "--host", "0.0.0.0"]
|
||||
|
||||
@@ -19,6 +19,24 @@ export const TOKEN_QUERY_PARAM = "fn_token";
|
||||
/** Paths that are exempt from authentication (liveness probes). */
|
||||
const EXEMPT_PATHS = ["/api/health"];
|
||||
|
||||
/**
|
||||
* Plugin-registered paths that should bypass daemon-token auth.
|
||||
* Used for webhook endpoints (Grafana, Gitea, GitHub, etc.) that need to be
|
||||
* reachable from external services without holding the daemon token.
|
||||
*/
|
||||
const PLUGIN_EXEMPT_PATHS = new Set<string>();
|
||||
|
||||
/**
|
||||
* Register a webhook path defined by a plugin so daemon auth lets it through.
|
||||
* Paths must start with `/api/plugins/`. Idempotent.
|
||||
*/
|
||||
export function registerPluginExemptPath(path: string): void {
|
||||
if (!path.startsWith("/api/plugins/")) {
|
||||
throw new Error(`registerPluginExemptPath: path must start with /api/plugins/ (got: ${path})`);
|
||||
}
|
||||
PLUGIN_EXEMPT_PATHS.add(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only /api/* paths are gated by this middleware. The SPA shell (index.html,
|
||||
* /assets/*, favicon, etc.) must load unauthenticated so the frontend JS can
|
||||
@@ -66,7 +84,13 @@ export function getDaemonToken(options?: { daemon?: { token: string }; noAuth?:
|
||||
* Check if a request path is exempt from authentication.
|
||||
*/
|
||||
function isExemptPath(path: string): boolean {
|
||||
return EXEMPT_PATHS.some((exempt) => path === exempt || path.startsWith(exempt + "/"));
|
||||
if (EXEMPT_PATHS.some((exempt) => path === exempt || path.startsWith(exempt + "/"))) {
|
||||
return true;
|
||||
}
|
||||
for (const exempt of PLUGIN_EXEMPT_PATHS) {
|
||||
if (path === exempt || path.startsWith(exempt + "/")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
notFound,
|
||||
} from "./api-error.js";
|
||||
import { getOrCreateProjectStore } from "./project-store-resolver.js";
|
||||
import { registerPluginExemptPath } from "./auth-middleware.js";
|
||||
|
||||
|
||||
// PluginRunner interface for optional plugin runner
|
||||
@@ -627,6 +628,15 @@ export function createPluginRouter(
|
||||
router.delete(fullPath, handler);
|
||||
break;
|
||||
}
|
||||
|
||||
// Webhook endpoints (Grafana, Gitea, GitHub) must be reachable without the
|
||||
// daemon token. Plugin-defined routes handle their own auth via webhook
|
||||
// secrets in plugin settings; bypass daemon auth here.
|
||||
try {
|
||||
registerPluginExemptPath(`/api/plugins${fullPath}`);
|
||||
} catch {
|
||||
// ignore — non-/api/plugins paths are rejected; safe to skip.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||
import { reviewStep, type ReviewVerdict } from "./reviewer.js";
|
||||
import { resolveSandboxBackend } from "./sandbox/index.js";
|
||||
import type { SandboxBackend } from "./sandbox/types.js";
|
||||
import { runSandboxTesterGate } from "./sandbox-tester-gate.js";
|
||||
import { ModelRegistry, SessionManager, type ToolDefinition, type AgentSession } from "@earendil-works/pi-coding-agent";
|
||||
import { PRIORITY_EXECUTE, type AgentSemaphore } from "./concurrency.js";
|
||||
import { RemovalReason, classifyTaskWorktree, describeRegisteredWorktrees, detectNestedWorktreeRoot, getRegisteredWorktreePaths, isGitRepository, isInsideWorktreesDir, isRegisteredGitWorktree, removeWorktree, type WorktreePool } from "./worktree-pool.js";
|
||||
@@ -1298,10 +1299,24 @@ export class TaskExecutor {
|
||||
return "blocked";
|
||||
}
|
||||
|
||||
const gateResult = await runSandboxTesterGate(this.rootDir, latestTask, executorLog);
|
||||
if (gateResult.verdict === "REVISE") {
|
||||
await this.store.logEntry(
|
||||
taskId,
|
||||
"Sandbox-tester REVISE — merge blocked, bouncing to in-progress",
|
||||
gateResult.report.slice(0, 4000),
|
||||
this.getRunContextFor(taskId),
|
||||
);
|
||||
await this.store.moveTask(taskId, "in-progress");
|
||||
return "blocked";
|
||||
}
|
||||
|
||||
await this.store.logEntry(
|
||||
taskId,
|
||||
"Task already in-review after completion — finalizing merge",
|
||||
undefined,
|
||||
gateResult.verdict === "APPROVE"
|
||||
? "Sandbox-tester APPROVE — finalizing merge"
|
||||
: "Task already in-review after completion — finalizing merge",
|
||||
gateResult.verdict === "APPROVE" ? gateResult.report.slice(0, 2000) : undefined,
|
||||
this.getRunContextFor(taskId),
|
||||
);
|
||||
await this.store.mergeTask(taskId);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { CronRunner, createAiPromptExecutor } from "./cron-runner.js";
|
||||
import type { RoutineRunner } from "./routine-runner.js";
|
||||
import { aiMergeTask, sweepStaleAutostashes, VerificationError } from "./merger.js";
|
||||
import { runAiMerge } from "./merger-ai.js";
|
||||
import { applySandboxTesterGate } from "./sandbox-tester-gate.js";
|
||||
import { promoteBranchGroup } from "./group-merge-coordinator.js";
|
||||
import { PRIORITY_MERGE } from "./concurrency.js";
|
||||
import { runtimeLog } from "./logger.js";
|
||||
@@ -1916,6 +1917,32 @@ export class ProjectEngine {
|
||||
// Direct merge via AI agent, gated by semaphore
|
||||
runtimeLog.log(`${manualResolver ? "Manual" : "Auto"}-merge merging ${taskId}...`);
|
||||
|
||||
// Sandbox-tester pre-merge gate (opt-in via FUSION_SANDBOX_GATE_ENABLED=true)
|
||||
// — see packages/engine/src/sandbox-tester-gate.ts. Returns false on REVISE,
|
||||
// which moves the task back to in-progress and aborts this merge attempt.
|
||||
try {
|
||||
const gateTask = await store.getTask(taskId).catch(() => null);
|
||||
if (gateTask) {
|
||||
const proceed = await applySandboxTesterGate(store, cwd, gateTask, runtimeLog);
|
||||
if (!proceed) {
|
||||
runtimeLog.log(`Sandbox-tester REVISE — merge aborted for ${taskId}, bounced to in-progress`);
|
||||
if (manualResolver) {
|
||||
this.manualMergeResolvers.delete(taskId);
|
||||
manualResolver.resolve({
|
||||
task: gateTask,
|
||||
branch: gateTask.branch ?? "",
|
||||
merged: false,
|
||||
worktreeRemoved: false,
|
||||
branchDeleted: false,
|
||||
} as MergeResult);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (gateErr) {
|
||||
runtimeLog.warn(`Sandbox-tester gate threw for ${taskId}: ${gateErr instanceof Error ? gateErr.message : String(gateErr)} — proceeding with merge`);
|
||||
}
|
||||
|
||||
const semaphore = (this.runtime as any).globalSemaphore;
|
||||
|
||||
const pool = (this.runtime as any).worktreePool;
|
||||
|
||||
157
packages/engine/src/sandbox-tester-gate.ts
Normal file
157
packages/engine/src/sandbox-tester-gate.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile, writeFile, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { Task, TaskStore } from "@fusion/core";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export type SandboxTesterVerdict = "APPROVE" | "REVISE" | "SKIP";
|
||||
|
||||
export interface SandboxTesterResult {
|
||||
verdict: SandboxTesterVerdict;
|
||||
report: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-merge sandbox-tester gate.
|
||||
*
|
||||
* Spawns an Opus 4.7 Claude subprocess with the `.fusion/agents/sandbox-tester/instructions.md`
|
||||
* persona and an MCP-attached OpenSandbox connection (configured in the host's ~/.claude.json).
|
||||
* The persona's responsibility: spin a sandbox, boot the worktree's dev server, run Playwright
|
||||
* smokes against acceptance criteria, emit a verdict ending with `Sandbox verdict: APPROVE | REVISE | SKIP`.
|
||||
*
|
||||
* Gate is OPT-IN via env var `FUSION_SANDBOX_GATE_ENABLED=true`. Disabled by default so existing
|
||||
* tasks merge through the normal path. Errors and timeouts default to APPROVE to avoid blocking
|
||||
* on infrastructure flakes — caller logs the error trail for investigation.
|
||||
*
|
||||
* @param rootDir Project root containing `.fusion/agents/sandbox-tester/instructions.md`
|
||||
* @param task The task object (used for id, title, description, worktree path)
|
||||
* @param logger Optional logger for executor-side trace output
|
||||
*/
|
||||
export async function runSandboxTesterGate(
|
||||
rootDir: string,
|
||||
task: Task,
|
||||
logger?: { log: (msg: string) => void; warn: (msg: string) => void },
|
||||
): Promise<SandboxTesterResult> {
|
||||
const log = logger ?? { log: (m: string) => console.log(`[sandbox-gate] ${m}`), warn: (m: string) => console.warn(`[sandbox-gate] ${m}`) };
|
||||
|
||||
if (process.env.FUSION_SANDBOX_GATE_ENABLED !== "true") {
|
||||
return { verdict: "SKIP", report: "FUSION_SANDBOX_GATE_ENABLED is not set" };
|
||||
}
|
||||
|
||||
const personaPath = join(rootDir, ".fusion/agents/sandbox-tester/instructions.md");
|
||||
if (!existsSync(personaPath)) {
|
||||
log.warn(`Sandbox-tester persona missing at ${personaPath} — defaulting to APPROVE`);
|
||||
return { verdict: "APPROVE", report: `persona missing at ${personaPath}` };
|
||||
}
|
||||
|
||||
const worktreePath = task.worktree;
|
||||
if (!worktreePath || !existsSync(worktreePath)) {
|
||||
return { verdict: "APPROVE", report: "no worktree available for sandbox testing" };
|
||||
}
|
||||
|
||||
let changedFiles = "";
|
||||
try {
|
||||
const { stdout } = await execAsync(`git -C "${worktreePath}" diff --name-only origin/dev...HEAD`, {
|
||||
timeout: 10000,
|
||||
});
|
||||
changedFiles = stdout.trim();
|
||||
} catch (err) {
|
||||
changedFiles = `(diff failed: ${err instanceof Error ? err.message : String(err)})`;
|
||||
}
|
||||
|
||||
let persona: string;
|
||||
try {
|
||||
persona = await readFile(personaPath, "utf-8");
|
||||
} catch (err) {
|
||||
log.warn(`Sandbox-tester persona read failed: ${err}; defaulting to APPROVE`);
|
||||
return { verdict: "APPROVE", report: `persona read failed: ${err}` };
|
||||
}
|
||||
|
||||
const promptFile = join("/tmp", `sandbox-tester-prompt-${task.id}-${Date.now()}.txt`);
|
||||
await writeFile(promptFile, persona);
|
||||
|
||||
const userPrompt = [
|
||||
`Task ID: ${task.id}`,
|
||||
`Title: ${task.title ?? "(untitled)"}`,
|
||||
"",
|
||||
"Description:",
|
||||
task.description ?? "(none)",
|
||||
"",
|
||||
`Worktree path: ${worktreePath}`,
|
||||
"Changed files vs origin/dev:",
|
||||
changedFiles || "(none detected)",
|
||||
"",
|
||||
"Run your standard sandbox-tester workflow. End your response with a line of the form `Sandbox verdict: APPROVE` or `Sandbox verdict: REVISE` or `Sandbox verdict: SKIP`.",
|
||||
].join("\n");
|
||||
|
||||
const escapedPrompt = userPrompt.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
||||
const claudeCmd = `claude --print --model claude-opus-4-7 --permission-mode bypassPermissions --append-system-prompt "$(cat "${promptFile}")" "${escapedPrompt}"`;
|
||||
|
||||
const timeoutMs = Number(process.env.FUSION_SANDBOX_GATE_TIMEOUT_MS ?? "600000");
|
||||
log.log(`Sandbox-tester gate ENGAGED for ${task.id} (timeout=${timeoutMs}ms)`);
|
||||
|
||||
let stdout = "";
|
||||
try {
|
||||
const result = await execAsync(claudeCmd, {
|
||||
cwd: "/project",
|
||||
timeout: timeoutMs,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
stdout = result.stdout;
|
||||
} catch (err) {
|
||||
log.warn(`Sandbox-tester gate FAILED for ${task.id}: ${err instanceof Error ? err.message : String(err)} — defaulting to APPROVE`);
|
||||
return { verdict: "APPROVE", report: `gate error: ${err instanceof Error ? err.message : String(err)}` };
|
||||
} finally {
|
||||
try { await unlink(promptFile); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const match = stdout.match(/Sandbox verdict:\s*(APPROVE|REVISE|SKIP)/i);
|
||||
if (!match) {
|
||||
log.warn(`Sandbox-tester gate produced no parseable verdict for ${task.id} — defaulting to APPROVE`);
|
||||
return { verdict: "APPROVE", report: `no verdict found in output (length=${stdout.length}):\n${stdout.slice(-2000)}` };
|
||||
}
|
||||
const verdict = match[1].toUpperCase() as SandboxTesterVerdict;
|
||||
log.log(`Sandbox-tester gate verdict for ${task.id}: ${verdict}`);
|
||||
return { verdict, report: stdout };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper: runs the gate and applies side-effects on REVISE (bounce task back to
|
||||
* in-progress, log the rejection). Returns true if merge should proceed, false if blocked.
|
||||
*
|
||||
* APPROVE / SKIP → returns true (proceed with merge). On APPROVE we log the truncated report.
|
||||
* REVISE → moves the task back to in-progress, logs the smoke report, returns false.
|
||||
*/
|
||||
export async function applySandboxTesterGate(
|
||||
store: TaskStore,
|
||||
rootDir: string,
|
||||
task: Task,
|
||||
logger?: { log: (msg: string) => void; warn: (msg: string) => void },
|
||||
): Promise<boolean> {
|
||||
const result = await runSandboxTesterGate(rootDir, task, logger);
|
||||
|
||||
if (result.verdict === "REVISE") {
|
||||
await store.logEntry(
|
||||
task.id,
|
||||
"Sandbox-tester REVISE — merge blocked, bouncing to in-progress",
|
||||
result.report.slice(0, 4000),
|
||||
);
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.verdict === "APPROVE") {
|
||||
await store.logEntry(
|
||||
task.id,
|
||||
"Sandbox-tester APPROVE — proceeding with merge",
|
||||
result.report.slice(0, 2000),
|
||||
);
|
||||
}
|
||||
// SKIP: no log noise (gate disabled or no smokeable surface)
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user