""" AutoDev - Planner Reads description + manuals, queries LLM to produce a structured development plan. Approaches the task like a principal engineer — thorough, skeptical, complete. """ import json import os from .llm import LLM from .logger import Logger from .context import ContextManager from . import config SYSTEM_PROMPT = config.EXPERT_IDENTITY + """ You are now in PLANNING mode. You have been given a project description and optional reference manuals. Your job is to produce a complete, actionable development plan that another engineer could follow blindly. Think through this carefully: 1. What EXACTLY is being asked for? Read the description multiple times. Identify every requirement. 2. What language, frameworks, and tools are needed? 3. What is the correct project structure? Think about separation of concerns. 4. What are the build steps? What compiler flags, linker flags, dependencies? 5. What could go wrong? Plan for error handling and edge cases. 6. What is the correct order of implementation? Dependencies between files matter. Output ONLY valid JSON with this structure: { "project_name": "string", "language": "primary language", "summary": "2-3 sentence summary of what we're building and why", "dependencies": ["every tool, compiler, library needed"], "structure": ["every file and directory to create, in order"], "steps": [ { "id": 1, "phase": "setup|implement|test|debug|finalize", "description": "detailed description of what to do and WHY", "files": ["files this step creates or modifies"], "commands": ["exact shell commands to run, if any"], "acceptance": "how to verify this step succeeded" } ] } Rules: - Every file that needs to exist MUST have a step that creates it. - Implementation steps MUST be ordered so dependencies are created before dependents. - Include a setup step for directory structure and dependency checks. - Include test/compile steps after implementation. - Include a finalize step that does a clean build and verification. - The "acceptance" field is critical — it defines what success looks like for each step. - Be specific in commands. Not "compile the code" but "gcc -Wall -Wextra -o main main.c util.c -lm". """ class Planner: def __init__(self, llm: LLM, logger: Logger, ctx: ContextManager, workdir: str): self.llm = llm self.logger = logger self.ctx = ctx self.workdir = workdir self.plan_path = os.path.join(workdir, config.PLAN_FILE) def read_description(self) -> str | None: path = os.path.join(self.workdir, config.DESCRIPTION_FILE) if not os.path.exists(path): return None with open(path, "r") as f: return f.read().strip() def read_manuals(self) -> dict[str, str]: manuals = {} mdir = os.path.join(self.workdir, config.MANUALS_DIR) if not os.path.isdir(mdir): return manuals for fname in sorted(os.listdir(mdir)): fpath = os.path.join(mdir, fname) if os.path.isfile(fpath): try: with open(fpath, "r") as f: manuals[fname] = f.read() except (IOError, UnicodeDecodeError): self.logger.log("manual_skip", f"Could not read {fname}", "warn") return manuals def create_plan(self, description: str, manuals: dict[str, str], existing_work: str = "") -> dict: prompt_parts = [ "## Project Description\n" "Read this VERY carefully. Every sentence is a requirement.\n\n" f"{description}" ] for name, content in manuals.items(): prompt_parts.append(f"## Reference Manual: {name}\n{content[:6000]}") if existing_work: prompt_parts.append( "## Existing Work\n" "The following files already exist from a previous iteration. " "Your plan should BUILD ON what exists — only add steps for NEW or CHANGED requirements. " "Do NOT recreate files that already satisfy the requirements.\n\n" f"{existing_work}" ) prompt = "\n\n".join(prompt_parts) self.ctx.add("user", prompt, priority=9) self.logger.log("planning", "Querying LLM for development plan") response = self.llm.query(prompt, system=SYSTEM_PROMPT, temperature=0.3) self.ctx.add("assistant", response, priority=8) plan = self._parse_plan(response) # Validate plan has required fields if not plan.get("steps"): # Retry once with more explicit instruction self.logger.log("plan_retry", "First plan had no steps, retrying", "warn") retry_prompt = ( prompt + "\n\nIMPORTANT: Your previous response could not be parsed. " "You MUST output ONLY a JSON object. No text before or after. " "Start your response with { and end with }." ) response = self.llm.query(retry_prompt, system=SYSTEM_PROMPT, temperature=0.1) plan = self._parse_plan(response) self._save_plan(plan) self.logger.log("plan_created", f"{len(plan.get('steps', []))} steps") return plan def load_existing_plan(self) -> dict | None: if os.path.exists(self.plan_path): try: with open(self.plan_path, "r") as f: return json.load(f) except (json.JSONDecodeError, IOError): return None return None def _parse_plan(self, response: str) -> dict: text = response.strip() # Try progressively more aggressive extraction for extracted in self._extract_json_candidates(text): try: plan = json.loads(extracted) if isinstance(plan, dict): return plan except json.JSONDecodeError: continue self.logger.log("plan_parse_fail", "Could not parse plan JSON", "error") return {"steps": [], "error": "Failed to parse plan", "raw": response[:1000]} def _extract_json_candidates(self, text: str) -> list[str]: """Yield JSON candidates from most to least likely.""" candidates = [] # 1. Markdown fenced JSON if "```json" in text: block = text.split("```json", 1)[1].split("```", 1)[0].strip() candidates.append(block) elif "```" in text: block = text.split("```", 1)[1].split("```", 1)[0].strip() candidates.append(block) # 2. Raw text (entire response) candidates.append(text) # 3. Find outermost { ... } with brace matching start = text.find("{") if start >= 0: depth = 0 for i in range(start, len(text)): if text[i] == "{": depth += 1 elif text[i] == "}": depth -= 1 if depth == 0: candidates.append(text[start:i + 1]) break return candidates def _save_plan(self, plan: dict): with open(self.plan_path, "w") as f: json.dump(plan, f, indent=2)