diff --git a/src/gate.ts b/src/gate.ts index 528de07..1645faf 100644 --- a/src/gate.ts +++ b/src/gate.ts @@ -55,6 +55,12 @@ function checkBuild(repoPath: string): GateResult { const scripts = (pkg?.scripts ?? {}) as Record; 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 }; diff --git a/src/llm.ts b/src/llm.ts index 620ec71..6cbd974 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -69,11 +69,20 @@ export class LLM { async isAvailable(): Promise { 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; } diff --git a/src/orchestrator.ts b/src/orchestrator.ts index 80c1725..235f69b 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -177,11 +177,25 @@ async function loadOrCreatePlan(config: Config, llm: LLM): Promise { 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); } diff --git a/src/planner.ts b/src/planner.ts index 2775690..8673079 100644 --- a/src/planner.ts +++ b/src/planner.ts @@ -20,13 +20,15 @@ export interface Plan { export async function createPlan(tasks: Task[], config: Config, llm: LLM): Promise { 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":"","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); diff --git a/src/sandbox.ts b/src/sandbox.ts index 0c5b0ed..650d2c3 100644 --- a/src/sandbox.ts +++ b/src/sandbox.ts @@ -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; diff --git a/src/scout.ts b/src/scout.ts index abefd0a..1fe354e 100644 --- a/src/scout.ts +++ b/src/scout.ts @@ -185,15 +185,15 @@ export async function runScout(task: Task, config: Config, llm: LLM): Promise= 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; }