diff --git a/vibn-agent-runner/dist/agent-session-runner.js b/vibn-agent-runner/dist/agent-session-runner.js index b52c6649..26b0418d 100644 --- a/vibn-agent-runner/dist/agent-session-runner.js +++ b/vibn-agent-runner/dist/agent-session-runner.js @@ -13,6 +13,41 @@ const vibn_chat_model_1 = require("./llm/vibn-chat-model"); const tools_1 = require("./tools"); const loader_1 = require("./prompts/loader"); const MAX_TURNS = 45; +function runBuildVerification(repoRoot, appPath) { + const fs = require("fs"); + const path = require("path"); + const { execSync } = require("child_process"); + const absoluteAppPath = path.join(repoRoot, appPath); + const pkgJsonPath = path.join(absoluteAppPath, "package.json"); + if (!fs.existsSync(pkgJsonPath)) { + return { success: true }; // No package.json, skip build check + } + try { + const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")); + // Only verify if there is an explicit build script + if (!pkg.scripts || !pkg.scripts.build) { + return { success: true }; + } + console.log(`[Ralph Loop] Running automatic build verification: npm run build inside ${absoluteAppPath}...`); + // Run npm run build with a 45s timeout to prevent hanging + execSync("npm run build", { + cwd: absoluteAppPath, + stdio: "pipe", + timeout: 45000, + }); + return { success: true }; + } + catch (err) { + const stderr = err.stderr + ? err.stderr.toString() + : err.message || String(err); + console.warn(`[Ralph Loop] Build verification failed:`, stderr); + return { + success: false, + error: stderr.slice(-3000), // Cap the log length to avoid flooding the prompt context + }; + } +} // ── VIBN DB bridge ──────────────────────────────────────────────────────────── async function patchSession(opts, payload) { const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`; @@ -265,6 +300,53 @@ Do NOT run git commit or git push — the platform handles committing after you }); continue; } + // ── Cloud Build Verification (Ralph Loop integration) ── + if (opts.repoRoot && ralphIteration < 3) { + await emit({ + ts: now(), + type: "info", + text: "🔍 [Ralph Loop] Initiating automatic build verification...", + }); + const verification = runBuildVerification(opts.repoRoot, opts.appPath); + if (!verification.success) { + ralphIteration++; + await emit({ + ts: now(), + type: "error", + text: `❌ [Ralph Loop] Build verification failed (iteration ${ralphIteration}/3). Feeding compilation errors back to the model...`, + }); + history.push({ + role: "user", + content: `Your previous edits completed, but the project's build check failed with compilation errors. + +========================================= +🚨 SURGICAL HEALING PROTOCOL ACTIVE 🚨 +========================================= +The project's compilation/build has failed. You are currently in an autonomous, auto-correcting healing loop and must fix this compilation error immediately. + +To prevent cognitive loop spirals and command limits, you MUST follow this strict, non-negotiable troubleshooting protocol: + +1. 🚫 STRICTLY BLOCK EXPLORATION: DO NOT execute general directory exploration or orientation commands such as 'ls', 'find', 'pwd', 'grep', 'git status', 'git diff', or other search commands. You do not need to look around. +2. 🎯 SURGICAL TARGETING: Scan the compiler error logs below to locate the EXACT filename, line number, and column where the compilation failed. +3. 🛠️ IMMEDIATE CORRECTION: Read that file immediately using your specific file-reading tool (using precise start/end lines if it is large) and apply a targeted, surgical edit to correct the exact syntax or type error. Do not write a placeholder or partial fix. + +Here are the precise compilation errors from the compiler: +\`\`\`text +${verification.error} +\`\`\` + +Implement the exact fix directly in the code now.`, + }); + continue; + } + else { + await emit({ + ts: now(), + type: "info", + text: "🟢 [Ralph Loop] Build verification passed successfully! 0 errors.", + }); + } + } // If fully complete, trigger auto-commit and finish if (opts.autoApprove) { await autoCommitAndDeploy(opts, task, emit); diff --git a/vibn-agent-runner/dist/server.js b/vibn-agent-runner/dist/server.js index a6bbb3f1..fea5c0c6 100644 --- a/vibn-agent-runner/dist/server.js +++ b/vibn-agent-runner/dist/server.js @@ -47,160 +47,257 @@ const app = (0, express_1.default)(); app.use((0, cors_1.default)()); const startTime = new Date(); // Raw body capture for webhook HMAC — must come before express.json() -app.use('/webhook/gitea', express_1.default.raw({ type: '*/*' })); +app.use("/webhook/gitea", express_1.default.raw({ type: "*/*" })); app.use(express_1.default.json()); const PORT = process.env.PORT || 3333; // --------------------------------------------------------------------------- // Build ToolContext from environment variables // --------------------------------------------------------------------------- -function ensureWorkspace(repo) { - const base = process.env.WORKSPACE_BASE || '/workspaces'; +function ensureWorkspace(repo, sessionId) { + const base = process.env.WORKSPACE_BASE || "/workspaces"; if (!repo) { - const dir = path.join(base, 'default'); + const dir = path.join(base, "default"); fs.mkdirSync(dir, { recursive: true }); return dir; } - const dir = path.join(base, repo.replace('/', '_')); + const mainRepoDir = path.join(base, repo.replace("/", "_")); const gitea = { - apiUrl: process.env.GITEA_API_URL || '', - apiToken: process.env.GITEA_API_TOKEN || '', - username: process.env.GITEA_USERNAME || '' + apiUrl: process.env.GITEA_API_URL || "", + apiToken: process.env.GITEA_API_TOKEN || "", + username: process.env.GITEA_USERNAME || "", }; - if (!fs.existsSync(path.join(dir, '.git'))) { - fs.mkdirSync(dir, { recursive: true }); - const authedUrl = `${gitea.apiUrl}/${repo}.git` - .replace('https://', `https://${gitea.username}:${gitea.apiToken}@`); + // 1. Ensure main repo clone exists + if (!fs.existsSync(path.join(mainRepoDir, ".git"))) { + fs.mkdirSync(mainRepoDir, { recursive: true }); + const authedUrl = `${gitea.apiUrl}/${repo}.git`.replace("https://", `https://${gitea.username}:${gitea.apiToken}@`); try { - (0, child_process_1.execSync)(`git clone "${authedUrl}" "${dir}"`, { stdio: 'pipe' }); + (0, child_process_1.execSync)(`git clone "${authedUrl}" "${mainRepoDir}"`, { stdio: "pipe" }); } catch { // Repo may not exist yet — just init - (0, child_process_1.execSync)(`git init`, { cwd: dir, stdio: 'pipe' }); - (0, child_process_1.execSync)(`git remote add origin "${authedUrl}"`, { cwd: dir, stdio: 'pipe' }); + (0, child_process_1.execSync)(`git init`, { cwd: mainRepoDir, stdio: "pipe" }); + (0, child_process_1.execSync)(`git remote add origin "${authedUrl}"`, { + cwd: mainRepoDir, + stdio: "pipe", + }); } } - return dir; + // 2. If no sessionId, fall back to main repo clone directly + if (!sessionId) { + return mainRepoDir; + } + // 3. Isolated Worktree Directory per task session + const taskWorktreePath = path.join(base, "tasks", sessionId); + fs.mkdirSync(path.join(base, "tasks"), { recursive: true }); + // 4. Create isolated worktree if not yet active + if (!fs.existsSync(path.join(taskWorktreePath, ".git"))) { + // Clean up any stale directory from previous failed runs before adding worktree + if (fs.existsSync(taskWorktreePath)) { + try { + fs.rmSync(taskWorktreePath, { recursive: true, force: true }); + } + catch { } + } + try { + console.log(`[worktree] Adding isolated git worktree for session ${sessionId} at ${taskWorktreePath}...`); + // Check if the branch task-sessionId already exists in the main repository + let branchExists = false; + try { + const branches = (0, child_process_1.execSync)(`git branch --list "task-${sessionId}"`, { + cwd: mainRepoDir, + }).toString(); + branchExists = branches.trim().length > 0; + } + catch { + branchExists = false; + } + if (branchExists) { + // Checkout the existing branch into the new worktree path + (0, child_process_1.execSync)(`git worktree add -f "${taskWorktreePath}" "task-${sessionId}"`, { cwd: mainRepoDir, stdio: "pipe" }); + } + else { + // Create and checkout a new isolated branch + (0, child_process_1.execSync)(`git worktree add -f -b "task-${sessionId}" "${taskWorktreePath}"`, { cwd: mainRepoDir, stdio: "pipe" }); + } + } + catch (e) { + console.error("[worktree] Failed to add git worktree, falling back to main clone:", e.message || String(e)); + return mainRepoDir; + } + } + return taskWorktreePath; } -function buildContext(repo) { - const workspaceRoot = ensureWorkspace(repo); +function buildContext(repo, sessionId) { + const workspaceRoot = ensureWorkspace(repo, sessionId); return { workspaceRoot, gitea: { - apiUrl: process.env.GITEA_API_URL || '', - apiToken: process.env.GITEA_API_TOKEN || '', - username: process.env.GITEA_USERNAME || '' + apiUrl: process.env.GITEA_API_URL || "", + apiToken: process.env.GITEA_API_TOKEN || "", + username: process.env.GITEA_USERNAME || "", }, coolify: { - apiUrl: process.env.COOLIFY_API_URL || '', - apiToken: process.env.COOLIFY_API_TOKEN || '' + apiUrl: process.env.COOLIFY_API_URL || "", + apiToken: process.env.COOLIFY_API_TOKEN || "", }, - mcpToken: '', - vibnApiUrl: process.env.VIBN_API_URL ?? 'https://vibnai.com', - memoryUpdates: [] + mcpToken: "", + vibnApiUrl: process.env.VIBN_API_URL ?? "https://vibnai.com", + memoryUpdates: [], }; } +function cleanupWorkspace(repo, sessionId) { + const base = process.env.WORKSPACE_BASE || "/workspaces"; + const mainRepoDir = path.join(base, repo.replace("/", "_")); + const taskWorktreePath = path.join(base, "tasks", sessionId); + if (fs.existsSync(taskWorktreePath)) { + try { + console.log(`[worktree] Pruning and removing git worktree for session ${sessionId}...`); + // 1. Tell git to remove the worktree references + (0, child_process_1.execSync)(`git worktree remove --force "${taskWorktreePath}"`, { + cwd: mainRepoDir, + stdio: "pipe", + }); + // 2. Delete the temporary branch from the main repository index + (0, child_process_1.execSync)(`git branch -D "task-${sessionId}"`, { + cwd: mainRepoDir, + stdio: "pipe", + }); + // 3. Force clean directory + if (fs.existsSync(taskWorktreePath)) { + fs.rmSync(taskWorktreePath, { recursive: true, force: true }); + } + } + catch (e) { + console.warn(`[worktree] Non-fatal cleanup error for session ${sessionId}:`, e.message || String(e)); + } + } +} // --------------------------------------------------------------------------- // Routes // --------------------------------------------------------------------------- // Health check -app.get('/health', (_req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); +app.get("/health", (_req, res) => { + res.json({ status: "ok", timestamp: new Date().toISOString() }); }); // --------------------------------------------------------------------------- // GitHub mirror — clone a public GitHub repo and push to Gitea as-is // --------------------------------------------------------------------------- -app.post('/api/mirror', async (req, res) => { +app.post("/api/mirror", async (req, res) => { const { github_url, gitea_repo, project_name, github_token } = req.body; if (!github_url || !gitea_repo) { - res.status(400).json({ error: '"github_url" and "gitea_repo" are required' }); + res + .status(400) + .json({ error: '"github_url" and "gitea_repo" are required' }); return; } - const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process'))); - const fs = await Promise.resolve().then(() => __importStar(require('fs'))); - const path = await Promise.resolve().then(() => __importStar(require('path'))); - const os = await Promise.resolve().then(() => __importStar(require('os'))); + const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process"))); + const fs = await Promise.resolve().then(() => __importStar(require("fs"))); + const path = await Promise.resolve().then(() => __importStar(require("path"))); + const os = await Promise.resolve().then(() => __importStar(require("os"))); const mirrorId = `mirror_${Date.now()}`; const tmpDir = path.join(os.tmpdir(), mirrorId); const gitea = { - apiUrl: process.env.GITEA_API_URL || '', - apiToken: process.env.GITEA_API_TOKEN || '', - username: process.env.GITEA_USERNAME || '' + apiUrl: process.env.GITEA_API_URL || "", + apiToken: process.env.GITEA_API_TOKEN || "", + username: process.env.GITEA_USERNAME || "", }; try { // Build authenticated Gitea push URL // GITEA_API_URL is like https://git.vibnai.com — strip /api/v1 if present - const giteaBase = gitea.apiUrl.replace(/\/api\/v1\/?$/, ''); - const authedPushUrl = `${giteaBase}/${gitea_repo}.git` - .replace('https://', `https://${gitea.username}:${gitea.apiToken}@`); + const giteaBase = gitea.apiUrl.replace(/\/api\/v1\/?$/, ""); + const authedPushUrl = `${giteaBase}/${gitea_repo}.git`.replace("https://", `https://${gitea.username}:${gitea.apiToken}@`); console.log(`[mirror] Cloning ${github_url} → ${tmpDir}`); fs.mkdirSync(tmpDir, { recursive: true }); // Build authenticated clone URL for private repos let cloneUrl = github_url; if (github_token) { - cloneUrl = github_url.replace('https://', `https://${github_token}@`); + cloneUrl = github_url.replace("https://", `https://${github_token}@`); } // Mirror-clone the GitHub repo (preserves all branches + tags) execSync(`git clone --mirror "${cloneUrl}" "${tmpDir}/.git"`, { - stdio: 'pipe', - timeout: 120000 + stdio: "pipe", + timeout: 120000, }); - execSync(`git config --bool core.bare false`, { cwd: tmpDir, stdio: 'pipe' }); - execSync(`git checkout`, { cwd: tmpDir, stdio: 'pipe' }); + execSync(`git config --bool core.bare false`, { + cwd: tmpDir, + stdio: "pipe", + }); + execSync(`git checkout`, { cwd: tmpDir, stdio: "pipe" }); // Point origin at Gitea and push all refs - execSync(`git remote set-url origin "${authedPushUrl}"`, { cwd: tmpDir, stdio: 'pipe' }); - execSync(`git push --mirror origin`, { cwd: tmpDir, stdio: 'pipe', timeout: 120000 }); + execSync(`git remote set-url origin "${authedPushUrl}"`, { + cwd: tmpDir, + stdio: "pipe", + }); + execSync(`git push --mirror origin`, { + cwd: tmpDir, + stdio: "pipe", + timeout: 120000, + }); console.log(`[mirror] Pushed ${gitea_repo} successfully`); res.json({ success: true, gitea_repo, github_url }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`[mirror] Failed:`, msg); - res.status(500).json({ error: 'Mirror failed', details: msg }); + res.status(500).json({ error: "Mirror failed", details: msg }); } finally { // Clean up temp dir try { - const { execSync: rm } = await Promise.resolve().then(() => __importStar(require('child_process'))); - rm(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); + const { execSync: rm } = await Promise.resolve().then(() => __importStar(require("child_process"))); + rm(`rm -rf "${tmpDir}"`, { stdio: "pipe" }); + } + catch { + /* best effort */ } - catch { /* best effort */ } } }); // List available agents -app.get('/api/agents', (_req, res) => { - const agents = Object.values(agents_1.AGENTS).map(a => ({ +app.get("/api/agents", (_req, res) => { + const agents = Object.values(agents_1.AGENTS).map((a) => ({ name: a.name, description: a.description, - tools: a.tools.map(t => t.name) + tools: a.tools.map((t) => t.name), })); res.json(agents); }); const activeSessions = new Map(); -app.post('/agent/execute', async (req, res) => { - const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, mcpToken, } = req.body; +app.post("/agent/execute", async (req, res) => { + const { sessionId, projectId, appName, appPath: rawAppPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, mcpToken, } = req.body; + const appPath = rawAppPath === undefined || rawAppPath === null || rawAppPath === "" + ? "." + : rawAppPath; if (!sessionId || !projectId || !appPath || !task) { - res.status(400).json({ error: 'sessionId, projectId, appPath and task are required' }); + res + .status(400) + .json({ error: "sessionId, projectId, appPath and task are required" }); return; } - const vibnApiUrl = process.env.VIBN_API_URL ?? 'https://vibnai.com'; + const vibnApiUrl = process.env.VIBN_API_URL ?? "https://vibnai.com"; + const patchHeaders = { + "Content-Type": "application/json", + ...(process.env.AGENT_RUNNER_SECRET + ? { "x-agent-runner-secret": process.env.AGENT_RUNNER_SECRET } + : {}), + }; // Register session as active const sessionState = { stopped: false }; activeSessions.set(sessionId, sessionState); // Respond immediately — execution is async - res.status(202).json({ sessionId, status: 'running' }); + res.status(202).json({ sessionId, status: "running" }); // Build workspace context — clone/update the Gitea repo if provided let ctx; try { - ctx = buildContext(giteaRepo); + ctx = buildContext(giteaRepo, sessionId); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - console.error('[agent/execute] buildContext failed:', msg); + console.error("[agent/execute] buildContext failed:", msg); // Notify VIBN DB of failure fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ status: 'failed', error: msg }), + method: "PATCH", + headers: patchHeaders, + body: JSON.stringify({ status: "failed", error: msg }), }).catch(() => { }); activeSessions.delete(sessionId); return; @@ -217,17 +314,20 @@ app.post('/agent/execute', async (req, res) => { ctx.projectId = projectId; // Scope workspace to the app subdirectory so the agent works there naturally if (appPath) { - const path = require('path'); + const path = require("path"); ctx.workspaceRoot = path.join(ctx.workspaceRoot, appPath); - const fs = require('fs'); + const fs = require("fs"); fs.mkdirSync(ctx.workspaceRoot, { recursive: true }); } - const agentConfig = agents_1.AGENTS['Coder']; + const agentConfig = agents_1.AGENTS["Coder"]; if (!agentConfig) { fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ status: 'failed', error: 'Coder agent not registered' }), + method: "PATCH", + headers: patchHeaders, + body: JSON.stringify({ + status: "failed", + error: "Coder agent not registered", + }), }).catch(() => { }); activeSessions.delete(sessionId); return; @@ -251,38 +351,41 @@ app.post('/agent/execute', async (req, res) => { coolifyApiUrl: process.env.COOLIFY_API_URL, coolifyApiToken: process.env.COOLIFY_API_TOKEN, }) - .catch(err => { + .catch((err) => { const msg = err instanceof Error ? err.message : String(err); console.error(`[agent/execute] session ${sessionId} crashed:`, msg); fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ status: 'failed', error: msg }), + method: "PATCH", + headers: patchHeaders, + body: JSON.stringify({ status: "failed", error: msg }), }).catch(() => { }); }) .finally(() => { activeSessions.delete(sessionId); + if (giteaRepo && sessionId) { + cleanupWorkspace(giteaRepo, sessionId); + } }); }); -app.post('/agent/stop', (req, res) => { +app.post("/agent/stop", (req, res) => { const { sessionId } = req.body; if (!sessionId) { - res.status(400).json({ error: 'sessionId required' }); + res.status(400).json({ error: "sessionId required" }); return; } const session = activeSessions.get(sessionId); if (session) { session.stopped = true; - res.json({ status: 'stopped' }); + res.json({ status: "stopped" }); } else { - res.status(404).json({ error: 'session not found or not running' }); + res.status(404).json({ error: "session not found or not running" }); } }); app.listen(PORT, () => { console.log(`AgentRunner listening on port ${PORT}`); - console.log(`Agents available: ${Object.keys(agents_1.AGENTS).join(', ')}`); + console.log(`Agents available: ${Object.keys(agents_1.AGENTS).join(", ")}`); if (!process.env.GOOGLE_API_KEY) { - console.warn('WARNING: GOOGLE_API_KEY is not set — agents will fail'); + console.warn("WARNING: GOOGLE_API_KEY is not set — agents will fail"); } }); diff --git a/vibn-agent-runner/dist/test-execute-hardening.d.ts b/vibn-agent-runner/dist/test-execute-hardening.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/vibn-agent-runner/dist/test-execute-hardening.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/vibn-agent-runner/dist/test-execute-hardening.js b/vibn-agent-runner/dist/test-execute-hardening.js new file mode 100644 index 00000000..d5dfde31 --- /dev/null +++ b/vibn-agent-runner/dist/test-execute-hardening.js @@ -0,0 +1,139 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const child_process_1 = require("child_process"); +const http_1 = __importDefault(require("http")); +// We will start the runner server on port 3334 +const PORT = 3334; +const BASE_URL = `http://localhost:${PORT}`; +console.log("🧪 Starting AgentRunner Hardening Test Suite..."); +// Set up environment variables +const env = { + ...process.env, + PORT: String(PORT), + AGENT_RUNNER_SECRET: "test-secret-123", + GOOGLE_API_KEY: "dummy-key-for-testing", // Pass dummy key to avoid Gemini API initialization crash + VIBN_API_URL: "http://localhost:3335", // Mock backend +}; +// Start mock backend on port 3335 to catch PATCH callbacks and verify headers +let receivedHeaders = null; +let receivedBody = null; +const mockBackend = http_1.default.createServer((req, res) => { + receivedHeaders = req.headers; + let body = ""; + req.on("data", (chunk) => { + body += chunk; + }); + req.on("end", () => { + try { + receivedBody = JSON.parse(body); + } + catch { + receivedBody = body; + } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true })); + }); +}); +mockBackend.listen(3335, () => { + console.log("✓ Mock backend server listening on port 3335"); +}); +// Spawn the runner server +const serverProcess = (0, child_process_1.spawn)("npx", ["ts-node", "src/server.ts"], { + env, + stdio: "pipe", +}); +// Wait for server to start +serverProcess.stdout.on("data", (data) => { + const output = data.toString(); + console.log(`[Server Out] ${output.trim()}`); +}); +serverProcess.stderr.on("data", (data) => { + console.error(`[Server Err] ${data.toString()}`); +}); +// Helper function to sleep +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +async function runTests() { + // Wait 4 seconds for server to boot + await sleep(4000); + let passed = 0; + let failed = 0; + const assert = (condition, message) => { + if (condition) { + console.log(` 🟢 PASSED: ${message}`); + passed++; + } + else { + console.error(` 🔴 FAILED: ${message}`); + failed++; + } + }; + try { + // Test 1: Empty appPath should be accepted and fall back to "." + console.log("\n1️⃣ Testing appPath empty string fallback..."); + const res1 = await fetch(`${BASE_URL}/agent/execute`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + sessionId: "test-session-1", + projectId: "test-project-1", + task: "Test empty appPath", + appPath: "", // Empty string! + giteaRepo: "test-repo", + }), + }); + assert(res1.status === 202, `Should return 202, got ${res1.status}`); + const data1 = (await res1.json()); + assert(data1.sessionId === "test-session-1", `Should return correct sessionId, got ${data1.sessionId}`); + // Test 2: Missing sessionId should return 400 + console.log("\n2️⃣ Testing missing required parameters (sessionId)..."); + const res2 = await fetch(`${BASE_URL}/agent/execute`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + projectId: "test-project-1", + task: "Test missing sessionId", + appPath: ".", + }), + }); + assert(res2.status === 400, `Should return 400, got ${res2.status}`); + // Test 3: Emergency callback headers should include x-agent-runner-secret + console.log("\n3️⃣ Testing early failure callback headers..."); + // Trigger a clone failure by passing a malformed giteaRepo containing slash, + // which triggers clone instead of default workspace but will fail clone. + console.log("Triggering clone failure on mock Gitea..."); + const res3 = await fetch(`${BASE_URL}/agent/execute`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + sessionId: "test-session-3", + projectId: "test-project-3", + task: "Trigger crash", + appPath: ".", + giteaRepo: "invalid_owner/invalid_repo", + }), + }); + assert(res3.status === 202, `Should return 202 Accepted, got ${res3.status}`); + // Wait for server to process async task and fail, calling our mock backend PATCH + console.log("Waiting for runner callback on mock backend..."); + await sleep(4000); + assert(receivedHeaders !== null, "Should call mock backend PATCH endpoint"); + if (receivedHeaders) { + assert(receivedHeaders["x-agent-runner-secret"] === "test-secret-123", `Callback should include secret header 'test-secret-123', got '${receivedHeaders["x-agent-runner-secret"]}'`); + assert(receivedBody && receivedBody.status === "failed", `Callback body should have status 'failed', got '${receivedBody?.status}'`); + } + } + catch (err) { + console.error("Test execution failed with exception:", err); + } + finally { + console.log("\n🧹 Cleaning up test servers..."); + serverProcess.kill(); + mockBackend.close(); + console.log(`\n📊 Tests complete. Passed: ${passed}, Failed: ${failed}`); + process.exit(failed > 0 ? 1 : 0); + } +} +runTests(); diff --git a/vibn-agent-runner/package.json b/vibn-agent-runner/package.json index 0cacb6f0..415b585a 100644 --- a/vibn-agent-runner/package.json +++ b/vibn-agent-runner/package.json @@ -37,5 +37,6 @@ "@types/uuid": "^9.0.8", "ts-node": "^10.9.2", "typescript": "^5.4.5" - } + }, + "packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8" } diff --git a/vibn-agent-runner/src/agent-session-runner.ts b/vibn-agent-runner/src/agent-session-runner.ts index 9f0fe06e..0a4d1b0f 100644 --- a/vibn-agent-runner/src/agent-session-runner.ts +++ b/vibn-agent-runner/src/agent-session-runner.ts @@ -417,7 +417,25 @@ Do NOT run git commit or git push — the platform handles committing after you history.push({ role: "user", - content: `Your previous edits completed, but the project's build check failed with compilation errors. Please fix these errors immediately so the build compiles clean:\n\n\`\`\`text\n${verification.error}\n\`\`\``, + content: `Your previous edits completed, but the project's build check failed with compilation errors. + +========================================= +🚨 SURGICAL HEALING PROTOCOL ACTIVE 🚨 +========================================= +The project's compilation/build has failed. You are currently in an autonomous, auto-correcting healing loop and must fix this compilation error immediately. + +To prevent cognitive loop spirals and command limits, you MUST follow this strict, non-negotiable troubleshooting protocol: + +1. 🚫 STRICTLY BLOCK EXPLORATION: DO NOT execute general directory exploration or orientation commands such as 'ls', 'find', 'pwd', 'grep', 'git status', 'git diff', or other search commands. You do not need to look around. +2. 🎯 SURGICAL TARGETING: Scan the compiler error logs below to locate the EXACT filename, line number, and column where the compilation failed. +3. 🛠️ IMMEDIATE CORRECTION: Read that file immediately using your specific file-reading tool (using precise start/end lines if it is large) and apply a targeted, surgical edit to correct the exact syntax or type error. Do not write a placeholder or partial fix. + +Here are the precise compilation errors from the compiler: +\`\`\`text +${verification.error} +\`\`\` + +Implement the exact fix directly in the code now.`, }); continue; } else {