Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ function checkBuild(repoPath: string): GateResult {
const scripts = (pkg?.scripts ?? {}) as Record<string, string>;

if (hasTsConfig) {
// Only run tsc if typescript is actually installed (npx may fetch a wrong "tsc" package otherwise)
const hasTs = existsSync(resolve(repoPath, "node_modules", ".package-lock.json")) ||
existsSync(resolve(repoPath, "node_modules", "typescript"));
if (!hasTs) {
return { gate: "build", verdict: "pass", message: "tsconfig.json found but typescript not installed, skipped" };
}
const result = shell("npx tsc --noEmit", repoPath, 180);
if (!result.ok) {
return { gate: "build", verdict: "fail", message: "TypeScript compilation failed", details: result.output };
Expand Down
13 changes: 11 additions & 2 deletions src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,20 @@ export class LLM {

async isAvailable(): Promise<boolean> {
try {
const resp = await fetch(`${this.baseUrl}/models`, {
const resp = await fetch(`${this.baseUrl}/auth/key`, {
headers: { Authorization: `Bearer ${this.apiKey}` },
signal: AbortSignal.timeout(5000),
});
return resp.ok;
if (resp.ok) return true;
// Fallback: some providers don't have /auth/key
if (resp.status === 404) {
const modelsResp = await fetch(`${this.baseUrl}/models`, {
headers: { Authorization: `Bearer ${this.apiKey}` },
signal: AbortSignal.timeout(5000),
});
return modelsResp.ok;
}
return false;
} catch {
return false;
}
Expand Down
14 changes: 14 additions & 0 deletions src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,25 @@ async function loadOrCreatePlan(config: Config, llm: LLM): Promise<Plan> {

function findTask(taskId: string, plan: Plan, config: Config): Task | null {
const all = loadTasks(config.paths.queue);

// Direct ID match (queue task IDs match plan IDs)
const found = all.find(t => t.id === taskId);
if (found) return found;

// Planner often generates new IDs. Try to match by name against pending queue tasks.
const planTask = plan.tasks.find(t => t.id === taskId);
if (planTask) {
const pending = all.filter(t => t.status === "pending");
const nameMatch = pending.find(t =>
t.question.toLowerCase().includes(planTask.name.toLowerCase()) ||
planTask.name.toLowerCase().includes(t.question.toLowerCase().slice(0, 30)),
);
if (nameMatch) return nameMatch;

// Last resort: return first pending implement task if available
const firstPending = pending[0];
if (firstPending) return firstPending;

const type = (planTask.type ?? "feasibility") as Task["type"];
return createTask(planTask.name, type);
}
Expand Down
6 changes: 4 additions & 2 deletions src/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ export interface Plan {
export async function createPlan(tasks: Task[], config: Config, llm: LLM): Promise<Plan> {
const taskList = tasks.map(t => `- [${t.id}] (${t.type}) ${t.question} [${t.status}]`).join("\n");

const prompt = `You are a task planner. Given these tasks, create a plan.
const prompt = `You are a task planner. Given these tasks, create an execution plan.

CRITICAL: You MUST use the EXACT task IDs provided below. Do NOT generate new IDs.

Tasks:
${taskList || "(no tasks)"}

Output JSON only:
{"tasks": [{"id":"...","name":"...","type":"...","dependsOn":[],"estimatedHours":2,"status":"ready"}], "executionOrder": ["id1","id2"], "reasoning": "..."}`;
{"tasks": [{"id":"<use exact ID from above>","name":"...","type":"...","dependsOn":[],"estimatedHours":2,"status":"ready"}], "executionOrder": ["id1","id2"], "reasoning": "..."}`;

const response = await llm.chat(config.models.planner, [{ role: "user", content: prompt }], 0.3);
return parseJSON(response);
Expand Down
19 changes: 19 additions & 0 deletions src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,30 @@ export class Sandbox {
this.exec("cp -r /workspace /project");
this.exec("cd /project && (grep -q node_modules .gitignore 2>/dev/null || echo -e 'node_modules/\\ndist/\\ncoverage/' >> .gitignore)");
this.exec("cd /project && git add -A 2>/dev/null; git stash 2>/dev/null || true");

// Backup original tsconfig and replace with tsx-friendly version
// The project's tsconfig (e.g. "module": "Node16") can break tsx inside the sandbox.
// Original is restored before extracting diff so the PR won't include tsconfig changes.
this.exec("cp /project/tsconfig.json /project/tsconfig.original.json 2>/dev/null || true");
this.exec(`cat > /project/tsconfig.json << 'TSEOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
}
}
TSEOF`);
return id;
}

extractDiff(): string {
if (!this.containerId) throw new Error("Sandbox not created");
// Restore original tsconfig before diffing so the PR won't include sandbox tsconfig changes
this.exec("cp /project/tsconfig.original.json /project/tsconfig.json 2>/dev/null && rm /project/tsconfig.original.json 2>/dev/null || true");
this.exec("cd /project && git add -A 2>/dev/null");
const result = this.exec("cd /project && git diff --cached HEAD -- . ':!node_modules' ':!package-lock.json' ':!pnpm-lock.yaml'");
return result.stdout;
Expand Down
10 changes: 5 additions & 5 deletions src/scout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,15 @@ export async function runScout(task: Task, config: Config, llm: LLM): Promise<Sc
const response = await llm.chat(model, messages);
messages.push({ role: "assistant", content: response });

if (response.toUpperCase().includes("ESCALATE") && codeExecuted) {
if (response.toUpperCase().includes("ESCALATE") && codeExecuted && iterations >= 5) {
escalated = true;
escalationReason = response;
log(`[scout:${modeLabel}] Requests escalation`);
log(`[scout:${modeLabel}] Requests escalation at iteration ${iterations}`);
break;
}
if (response.toUpperCase().includes("ESCALATE") && !codeExecuted) {
log(`[scout:${modeLabel}] Ignoring premature ESCALATE (no code executed yet)`);
messages.push({ role: "user", content: "You must read the project code first before escalating. Start by listing files and reading the relevant source code. Use bash blocks: ```bash\nls /project/src/\n```" });
if (response.toUpperCase().includes("ESCALATE") && (!codeExecuted || iterations < 5)) {
log(`[scout:${modeLabel}] Ignoring premature ESCALATE (iteration ${iterations}/5, codeExecuted=${codeExecuted})`);
messages.push({ role: "user", content: "You must try harder before escalating. You need at least 5 iterations with code executed. Read the project code, write code, run it, and iterate. Use bash blocks: ```bash\nls /project/src/\n```" });
continue;
}

Expand Down