596 lines
24 KiB
JavaScript
596 lines
24 KiB
JavaScript
"use strict";
|
|
/**
|
|
* agent-session-runner.ts
|
|
*
|
|
* Upgraded Cloud Agent Executor for VibnCode.
|
|
* Implements 4-level Smart Concurrency (parallel reads/lookups) and the
|
|
* Ralph Loop (autonomous self-correction) entirely inside your secure Cloud VM.
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.runSessionAgent = runSessionAgent;
|
|
const child_process_1 = require("child_process");
|
|
const vibn_chat_model_1 = require("./llm/vibn-chat-model");
|
|
const tools_1 = require("./tools");
|
|
const loader_1 = require("./prompts/loader");
|
|
const MAX_TURNS = 80;
|
|
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}`;
|
|
try {
|
|
await fetch(url, {
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-agent-runner-secret": process.env.AGENT_RUNNER_SECRET ?? "",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
catch (err) {
|
|
console.warn("[session-runner] PATCH failed:", err instanceof Error ? err.message : err);
|
|
}
|
|
}
|
|
function now() {
|
|
return new Date().toISOString();
|
|
}
|
|
// ── File change tracking ──────────────────────────────────────────────────────
|
|
const FILE_WRITE_TOOLS = new Set([
|
|
"write_file",
|
|
"replace_in_file",
|
|
"create_file",
|
|
"fs_write",
|
|
"fs_edit",
|
|
]);
|
|
function extractChangedFile(toolName, args, workspaceRoot, appPath) {
|
|
if (!FILE_WRITE_TOOLS.has(toolName))
|
|
return null;
|
|
const rawPath = String(args.path ?? args.file_path ?? "");
|
|
if (!rawPath)
|
|
return null;
|
|
// Make path relative to appPath for display
|
|
const fullPrefix = `${workspaceRoot}/${appPath}/`;
|
|
const appPrefix = `${appPath}/`;
|
|
let displayPath = rawPath.replace(fullPrefix, "").replace(appPrefix, "");
|
|
const fileStatus = toolName === "write_file" || toolName === "fs_write" ? "added" : "modified";
|
|
return { path: displayPath, status: fileStatus };
|
|
}
|
|
// ── Auto-commit helper ────────────────────────────────────────────────────────
|
|
async function autoCommitAndDeploy(opts, task, emit) {
|
|
const repoRoot = opts.repoRoot;
|
|
if (!repoRoot || !opts.giteaRepo) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: "Auto-approve skipped — no repo root available.",
|
|
});
|
|
return;
|
|
}
|
|
const gitOpts = { cwd: repoRoot, stdio: "pipe" };
|
|
const giteaApiUrl = process.env.GITEA_API_URL || "";
|
|
const giteaUsername = process.env.GITEA_USERNAME || "agent";
|
|
const giteaToken = process.env.GITEA_API_TOKEN || "";
|
|
try {
|
|
try {
|
|
(0, child_process_1.execSync)('git config user.email "agent@vibnai.com"', gitOpts);
|
|
(0, child_process_1.execSync)('git config user.name "VIBN Agent"', gitOpts);
|
|
}
|
|
catch {
|
|
/* already set */
|
|
}
|
|
(0, child_process_1.execSync)("git add -A", gitOpts);
|
|
const status = (0, child_process_1.execSync)("git status --porcelain", gitOpts)
|
|
.toString()
|
|
.trim();
|
|
if (!status) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: "✓ No file changes to commit.",
|
|
});
|
|
await patchSession(opts, { status: "approved" });
|
|
return;
|
|
}
|
|
const commitMsg = `agent: ${task.slice(0, 72)}`;
|
|
const msgFile = require("path").join(opts.repoRoot || process.cwd(), ".git", "COMMIT_EDITMSG");
|
|
require("fs").writeFileSync(msgFile, commitMsg, "utf8");
|
|
(0, child_process_1.execSync)("git commit -F .git/COMMIT_EDITMSG", gitOpts);
|
|
try {
|
|
require("fs").unlinkSync(msgFile);
|
|
}
|
|
catch { }
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: `✓ Committed: "${commitMsg}"`,
|
|
});
|
|
const authedUrl = `${giteaApiUrl}/${opts.giteaRepo}.git`.replace("https://", `https://${giteaUsername}:${giteaToken}@`);
|
|
(0, child_process_1.execSync)(`git push "${authedUrl}" HEAD:main`, gitOpts);
|
|
await emit({ ts: now(), type: "info", text: "✓ Pushed to Gitea." });
|
|
// Optional Coolify deploy
|
|
let deployed = false;
|
|
if (opts.coolifyApiUrl && opts.coolifyApiToken && opts.coolifyAppUuid) {
|
|
try {
|
|
const deployRes = await fetch(`${opts.coolifyApiUrl}/api/v1/applications/${opts.coolifyAppUuid}/start`, {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${opts.coolifyApiToken}` },
|
|
});
|
|
deployed = deployRes.ok;
|
|
if (deployed)
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: "✓ Deployment triggered.",
|
|
});
|
|
}
|
|
catch {
|
|
/* best-effort */
|
|
}
|
|
}
|
|
await patchSession(opts, {
|
|
status: "approved",
|
|
outputLine: {
|
|
ts: now(),
|
|
type: "done",
|
|
text: `✓ Auto-committed & ${deployed ? "deployed" : "pushed"}. No approval needed.`,
|
|
},
|
|
});
|
|
}
|
|
catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
await emit({
|
|
ts: now(),
|
|
type: "error",
|
|
text: `Auto-commit failed: ${msg}`,
|
|
});
|
|
// Fall back to done so user can manually approve
|
|
await patchSession(opts, { status: "done" });
|
|
}
|
|
}
|
|
function parseTaskItems(repoRoot) {
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const tasksDir = path.join(repoRoot, ".vibncode", "tasks");
|
|
if (!fs.existsSync(tasksDir))
|
|
return [];
|
|
const items = [];
|
|
try {
|
|
const files = fs
|
|
.readdirSync(tasksDir)
|
|
.filter((f) => f.endsWith(".md"));
|
|
files.sort();
|
|
for (const file of files) {
|
|
const filePath = path.join(tasksDir, file);
|
|
const content = fs.readFileSync(filePath, "utf8");
|
|
const lines = content.split("\n");
|
|
lines.forEach((line, lineIndex) => {
|
|
const match = line.match(/^(\s*)-\s*\[([ xX])\]\s+(.+)$/);
|
|
if (match && match[2] !== undefined && match[3] !== undefined) {
|
|
items.push({
|
|
text: match[3].trim(),
|
|
filePath,
|
|
lineIndex,
|
|
isChecked: match[2].toLowerCase() === "x",
|
|
fileName: file,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
catch (err) {
|
|
console.error("[Orchestrator] Error parsing task items:", err);
|
|
}
|
|
return items;
|
|
}
|
|
function toggleTaskOnDisk(task) {
|
|
const fs = require("fs");
|
|
const content = fs.readFileSync(task.filePath, "utf8");
|
|
const lines = content.split("\n");
|
|
const line = lines[task.lineIndex];
|
|
if (line) {
|
|
const match = line.match(/^(\s*)-\s*\[([ xX])\]\s+(.+)$/);
|
|
if (match && match[1] !== undefined && match[3] !== undefined) {
|
|
lines[task.lineIndex] = `${match[1]}- [x] ${match[3]}`;
|
|
fs.writeFileSync(task.filePath, lines.join("\n"), "utf8");
|
|
}
|
|
}
|
|
}
|
|
async function generateBacklogFromPrompt(taskPrompt, repoRoot) {
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const tasksDir = path.join(repoRoot, ".vibncode", "tasks");
|
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
const prompt = `You are an elite Software Engineering Orchestrator.
|
|
Your goal is to break down the user's high-level objective into a highly detailed, sequential checklist of concrete, atomic, self-contained implementation tasks.
|
|
|
|
High-Level Objective:
|
|
"${taskPrompt}"
|
|
|
|
Please output a standard Markdown file containing:
|
|
1. A brief 1-sentence overview.
|
|
2. A list of tasks, where each task MUST be formatted as a standard Markdown checkbox starting with "- [ ] ":
|
|
- [ ] Implement database schema changes for ...
|
|
- [ ] Add endpoint handler for ...
|
|
- [ ] Write tests ...
|
|
|
|
Be extremely thorough and break the objective down into small, digestible units of work (e.g. 5-15 tasks).
|
|
Do NOT include any extra conversational text or explanations. Just output the clean markdown.`;
|
|
const resp = await (0, vibn_chat_model_1.callVibnChat)({
|
|
systemPrompt: "You are a precise technical orchestrator who only outputs markdown checklist files.",
|
|
messages: [{ role: "user", content: prompt }],
|
|
temperature: 0.1,
|
|
});
|
|
const content = resp.text || `# Delegated Backlog\n\n- [ ] ${taskPrompt}`;
|
|
const backlogPath = path.join(tasksDir, "00-delegated-backlog.md");
|
|
fs.writeFileSync(backlogPath, content, "utf8");
|
|
}
|
|
function commitTaskProgress(task, repoRoot) {
|
|
const { execSync } = require("child_process");
|
|
try {
|
|
console.log(`[Orchestrator] Committing task progress: ${task.text}`);
|
|
execSync("git add -A", { cwd: repoRoot, stdio: "pipe" });
|
|
const msg = `feat(tasks): [Completed] ${task.text}`;
|
|
execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, {
|
|
cwd: repoRoot,
|
|
stdio: "pipe",
|
|
});
|
|
}
|
|
catch (err) {
|
|
// If nothing to commit, that's fine
|
|
}
|
|
}
|
|
async function runSingleSubTask(task, config, ctx, opts, emit) {
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const basePrompt = (0, loader_1.resolvePrompt)(config.promptId);
|
|
const scopedPrompt = `${basePrompt}
|
|
|
|
## ACTIVE SUBTASK OBJECTIVE
|
|
You are working on a single task in your task queue:
|
|
TASK: "${task.text}"
|
|
File: "${path.relative(opts.repoRoot ?? ctx.workspaceRoot, task.filePath)}" (line ${task.lineIndex + 1})
|
|
|
|
## CRITICAL EXECUTION CONSTRAINTS
|
|
1. 🎯 STAY HIGHLY FOCUSED: Your only objective is to implement this specific task. Do NOT wander, do NOT explore other unrelated parts of the codebase, and do NOT attempt unrelated tasks.
|
|
2. 🚫 NO EXPLORATION COMMANDS: DO NOT execute generic orientation/search commands like 'ls', 'find', 'pwd', 'grep', 'git diff', 'git status'. You already know the repository structure. Go straight to editing or reading the targeted files.
|
|
3. 🛠️ TOGGLE CHECKBOX: Once your implementation is done, you MUST read and rewrite "${path.relative(opts.repoRoot ?? ctx.workspaceRoot, task.filePath)}" at line ${task.lineIndex + 1} to change "- [ ]" to "- [x]".
|
|
4. 🔴 NO COMMITS: Do NOT run 'git commit' or 'git push'. The platform handles committing automatically after you finish.
|
|
5. 🟢 COMPLETED SIGNAL: When you are finished, verify the build compiles clean using the Ralph Loop checks. If successful, stop executing tools and end your response.
|
|
`;
|
|
const userPrompt = `Please implement the following task: "${task.text}" and then check it off in the task list.`;
|
|
const history = [{ role: "user", content: userPrompt }];
|
|
let subTurn = 0;
|
|
const SUB_MAX_TURNS = 40;
|
|
let toolCallsSinceText = 0;
|
|
let roundsSinceText = 0;
|
|
const toolFingerprints = [];
|
|
let ralphIteration = 0;
|
|
function fingerprintToolCall(tc) {
|
|
const name = tc.name;
|
|
const args = tc.args ?? {};
|
|
if (name === "shell_exec") {
|
|
const cmd = String(args.command ?? "").trim();
|
|
const firstWord = cmd.split(/\s+/)[0] ?? "shell";
|
|
return `shell_exec:${firstWord}`;
|
|
}
|
|
// Determine target based on most common descriptive parameter keys
|
|
const target = args.path ??
|
|
args.pattern ??
|
|
args.command ??
|
|
args.commandId ??
|
|
args.appUuid ??
|
|
args.uuid ??
|
|
"";
|
|
if (target) {
|
|
return `${name}:${target}`;
|
|
}
|
|
// Filter out common metadata like projectId, and use first real argument
|
|
const keys = Object.keys(args).filter((k) => k !== "projectId");
|
|
if (keys.length > 0) {
|
|
return `${name}:${args[keys[0]]}`;
|
|
}
|
|
return `${name}:default`;
|
|
}
|
|
while (subTurn < SUB_MAX_TURNS) {
|
|
if (opts.isStopped()) {
|
|
await emit({ ts: now(), type: "info", text: "Stopped by user." });
|
|
return false;
|
|
}
|
|
subTurn++;
|
|
const isSilent = roundsSinceText >= 8 || toolCallsSinceText >= 12;
|
|
const extraSystem = isSilent
|
|
? "\n\n[STATUS NUDGE] Focus on completing the current task. Do not make any more tool calls without a short sentence explaining what you are working on."
|
|
: "";
|
|
let resp;
|
|
try {
|
|
resp = await (0, vibn_chat_model_1.callVibnChat)({
|
|
systemPrompt: scopedPrompt + extraSystem,
|
|
messages: history,
|
|
tools: config.tools,
|
|
temperature: 0.1,
|
|
});
|
|
}
|
|
catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
await emit({
|
|
ts: now(),
|
|
type: "error",
|
|
text: `LLM sub-session error: ${msg}`,
|
|
});
|
|
return false;
|
|
}
|
|
if (resp.error) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "error",
|
|
text: `LLM sub-session error: ${resp.error}`,
|
|
});
|
|
return false;
|
|
}
|
|
if (resp.text) {
|
|
await emit({ ts: now(), type: "info", text: resp.text });
|
|
roundsSinceText = 0;
|
|
toolCallsSinceText = 0;
|
|
}
|
|
else if (resp.toolCalls.length) {
|
|
roundsSinceText++;
|
|
toolCallsSinceText += resp.toolCalls.length;
|
|
}
|
|
if (!resp.toolCalls.length) {
|
|
if (opts.repoRoot && ralphIteration < 3) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: "🔍 [Ralph Loop] Verifying build for this task...",
|
|
});
|
|
const verification = runBuildVerification(opts.repoRoot, opts.appPath);
|
|
if (!verification.success) {
|
|
ralphIteration++;
|
|
await emit({
|
|
ts: now(),
|
|
type: "error",
|
|
text: `❌ [Ralph Loop] Build failed (iteration ${ralphIteration}/3) for this task.`,
|
|
});
|
|
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 passed successfully! 0 errors.",
|
|
});
|
|
}
|
|
}
|
|
let diskChecked = false;
|
|
try {
|
|
const fileContent = fs.readFileSync(task.filePath, "utf8");
|
|
const lines = fileContent.split("\n");
|
|
const line = lines[task.lineIndex];
|
|
if (line) {
|
|
const match = line.match(/^(\s*)-\s*\[([ xX])\]\s+(.+)$/);
|
|
if (match && match[2].toLowerCase() === "x") {
|
|
diskChecked = true;
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
if (!diskChecked) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: `✍️ [Orchestrator] Task implementation completed. Automatically checking off task on disk.`,
|
|
});
|
|
toggleTaskOnDisk(task);
|
|
}
|
|
return true;
|
|
}
|
|
for (const tc of resp.toolCalls) {
|
|
toolFingerprints.push(fingerprintToolCall(tc));
|
|
}
|
|
const window = toolFingerprints.slice(-12);
|
|
const counts = new Map();
|
|
for (const fp of window)
|
|
counts.set(fp, (counts.get(fp) ?? 0) + 1);
|
|
let maxRepeats = 0;
|
|
let repeatedCmd = "";
|
|
for (const [fp, n] of counts.entries()) {
|
|
if (n > maxRepeats) {
|
|
maxRepeats = n;
|
|
repeatedCmd = fp;
|
|
}
|
|
}
|
|
if (maxRepeats >= 6) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "error",
|
|
text: `Loop detected in subtask execution (repeated "${repeatedCmd}" ${maxRepeats}x in last 12 calls), breaking loop.`,
|
|
});
|
|
return false;
|
|
}
|
|
history.push({
|
|
role: "assistant",
|
|
content: resp.text,
|
|
toolCalls: resp.toolCalls,
|
|
});
|
|
for (const tc of resp.toolCalls) {
|
|
if (opts.isStopped())
|
|
return false;
|
|
await emit({
|
|
ts: now(),
|
|
type: "step",
|
|
text: `Running ${tc.name}...`,
|
|
});
|
|
let result;
|
|
try {
|
|
result = await (0, tools_1.executeTool)(tc.name, tc.args, ctx);
|
|
}
|
|
catch (err) {
|
|
result = { error: err instanceof Error ? err.message : String(err) };
|
|
}
|
|
const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
history.push({
|
|
role: "tool",
|
|
content: resultStr,
|
|
toolCallId: tc.id,
|
|
toolName: tc.name,
|
|
});
|
|
}
|
|
}
|
|
await emit({
|
|
ts: now(),
|
|
type: "error",
|
|
text: `Subtask exceeded maximum turns limit of ${SUB_MAX_TURNS}.`,
|
|
});
|
|
return false;
|
|
}
|
|
async function runSessionAgent(config, task, ctx, opts) {
|
|
const emit = async (line) => {
|
|
console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`);
|
|
await patchSession(opts, { outputLine: line });
|
|
};
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: `Agent started offline delegation orchestrator in ${opts.appPath}`,
|
|
});
|
|
const repoRoot = opts.repoRoot ?? ctx.workspaceRoot;
|
|
let tasks = parseTaskItems(repoRoot);
|
|
if (tasks.length === 0) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: "🤖 [Orchestrator] No active tasks backlog found on disk. Analyzing prompt to plan atomic execution backlog...",
|
|
});
|
|
try {
|
|
await generateBacklogFromPrompt(task, repoRoot);
|
|
tasks = parseTaskItems(repoRoot);
|
|
}
|
|
catch (err) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "error",
|
|
text: `❌ [Orchestrator] Failed to generate backlog: ${err.message || String(err)}`,
|
|
});
|
|
await patchSession(opts, {
|
|
status: "failed",
|
|
error: "Backlog generation failed",
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
const openTasks = tasks.filter((t) => !t.isChecked);
|
|
if (openTasks.length === 0) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: "🟢 [Orchestrator] All tasks in the queue are already completed!",
|
|
});
|
|
await patchSession(opts, { status: "completed" });
|
|
return;
|
|
}
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: `🤖 [Orchestrator] Found ${openTasks.length} open tasks. Executing task-by-task Meta-Loop...`,
|
|
});
|
|
for (let i = 0; i < openTasks.length; i++) {
|
|
const currentTask = openTasks[i];
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: `🚀 [Orchestrator] Task ${i + 1}/${openTasks.length}: "${currentTask.text}"`,
|
|
});
|
|
const success = await runSingleSubTask(currentTask, config, ctx, opts, emit);
|
|
if (!success) {
|
|
await emit({
|
|
ts: now(),
|
|
type: "error",
|
|
text: `❌ [Orchestrator] Bailed out! Task execution failed on: "${currentTask.text}". Rolling back modifications for this task to keep the repository green...`,
|
|
});
|
|
try {
|
|
const { execSync } = require("child_process");
|
|
execSync("git checkout -- . && git clean -fd", {
|
|
cwd: repoRoot,
|
|
stdio: "pipe",
|
|
});
|
|
}
|
|
catch (rollbackErr) {
|
|
console.error("Rollback failed:", rollbackErr.message || rollbackErr);
|
|
}
|
|
await patchSession(opts, {
|
|
status: "failed",
|
|
error: `Delegation loop halted at task: "${currentTask.text}"`,
|
|
});
|
|
return;
|
|
}
|
|
commitTaskProgress(currentTask, repoRoot);
|
|
}
|
|
await emit({
|
|
ts: now(),
|
|
type: "info",
|
|
text: `🎉 [Orchestrator] All delegated tasks completed successfully with green compilation builds!`,
|
|
});
|
|
if (opts.autoApprove) {
|
|
await autoCommitAndDeploy(opts, task, emit);
|
|
}
|
|
else {
|
|
await patchSession(opts, { status: "completed" });
|
|
}
|
|
}
|