From ea88e9d34a80aaca523b15d431bcda2980e8d936 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Mon, 11 May 2026 21:05:27 +0200 Subject: [PATCH] chore: remove IDE and agent_loop dirs from git tracking --- .gitignore | 11 +- .idea/.gitignore | 10 - .idea/modules.xml | 8 - .idea/oebb_planner.iml | 8 - .idea/vcs.xml | 6 - .zed/rules/review.md | 77 -- .zed/rules/rewrite.md | 114 --- .zed/settings.json | 5 - .zed/tasks.json | 15 - agent_loop/agent_base_gemma4.py | 1478 ------------------------------- agent_loop/ts_agent_gemma4.py | 179 ---- agent_loop/ttl_agent_gemma4.py | 298 ------- 12 files changed, 8 insertions(+), 2201 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/modules.xml delete mode 100644 .idea/oebb_planner.iml delete mode 100644 .idea/vcs.xml delete mode 100644 .zed/rules/review.md delete mode 100644 .zed/rules/rewrite.md delete mode 100644 .zed/settings.json delete mode 100644 .zed/tasks.json delete mode 100644 agent_loop/agent_base_gemma4.py delete mode 100644 agent_loop/ts_agent_gemma4.py delete mode 100644 agent_loop/ttl_agent_gemma4.py diff --git a/.gitignore b/.gitignore index a501db8..3592864 100644 --- a/.gitignore +++ b/.gitignore @@ -29,10 +29,15 @@ npm-debug.log* next-env.d.ts .aider* +# editor / agent tooling +.claude/ +.idea/ +.zed/ + +# agent loop +agent_loop/ + # agent loop generated output -agent_loop/logs/ -agent_loop/runs/ -agent_loop/__pycache__/ logs/ runs/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 30cf57e..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 5fbacd0..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/.idea/oebb_planner.iml b/.idea/oebb_planner.iml deleted file mode 100644 index c956989..0000000 --- a/.idea/oebb_planner.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index eb26f23..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.zed/rules/review.md b/.zed/rules/review.md deleted file mode 100644 index 25608b2..0000000 --- a/.zed/rules/review.md +++ /dev/null @@ -1,77 +0,0 @@ -# Review Agent Rules - -These rules apply when reviewing completed implementation steps on the `rewrite/next` branch. - -## Checklist Tracking - -`CHECKLIST.md` uses three checkbox states: -- `[ ]` — pending and **required**; blocks the next phase -- `[x]` — done -- `[~]` — optional or deferred; **never blocks phase advancement** - -Rules: -- The ✅ column belongs to the rewrite agent; the ✔️ column is yours. -- Only review items whose ✅ box is already `[x]`. Do not attempt to review unimplemented items. -- If a ✅ box is `[~]` (optional, skipped), mark the ✔️ box `[~]` as well — no review needed for skipped items. -- After reviewing each required item and confirming it meets the quality bar below, mark its ✔️ box by changing `[ ]` to `[x]`. -- Before reviewing any item in a new phase, read `CHECKLIST.md` and confirm that every **required** item in all preceding phases has `[x]` in both ✅ and ✔️. Items where both columns are `[~]` do not need review and do not block advancement. -- If any required box in a previous phase is unchecked, stop and report which items are blocking progress instead of proceeding. - -## Review Scope - -- Review one phase at a time. Within a phase, review items in the order they appear in `CHECKLIST.md`. -- For each item, cross-reference the implementation against `REWRITE_PLAN.md` and the quality criteria below. -- Report concrete issues with file paths and line numbers. Do not flag style nitpicks that are not covered by a project guideline. - -## What to Check - -**Correctness** -- The behavior matches the intent described in `REWRITE_PLAN.md` and the checklist item. -- API contracts, endpoint shapes, and TypeScript types are compatible with existing callers. -- No regressions are introduced in previously working behavior. - -**Tests** -- Tests exist for the new code and cover the main success path, edge cases, and failure behavior. -- Tests are not weakened or removed just to make the suite pass. -- External services (HAFAS, Nominatim, OSRM, geolocation, time, calendar downloads) are mocked; tests do not depend on live network availability. - -**Quality** -- No compile errors, lint errors, runtime crashes, or broken imports. -- TypeScript strictness is intact — no `any` used as a shortcut. -- Server-only code is not imported into client components. -- Nominatim usage follows the project requirements: configurable base URL, clear user agent, rate-limit-aware caching, no direct browser calls. -- Error handling is explicit and user-facing failures are understandable. -- No generated artifacts, caches, logs, or local environment files are committed. -- Dependencies are unchanged unless necessary and justified. - -**Scope** -- The change is scoped to the checklist item — no unrelated modifications. -- Old implementation files (`server/`, `oebb-planner-app/`, `oebb-planner.jsx`) were not removed unless parity is tested and cleanup was explicitly requested. - -**Accessibility (UI items only)** -- Semantic buttons and links, labels for inputs, keyboard-operable controls, visible loading and error states. - -## Verification - -- Run the relevant test and build checks to confirm the implementation passes before marking ✔️: - -```bash -npm test -npm run build -npm run typecheck -npm run lint -``` - -- If a check fails, do not mark the ✔️ box. Report the failure with the exact output and leave the item for the rewrite agent to fix. - -## Completion Checklist - -Before marking a ✔️ box, confirm: - -- The ✅ box for this item is already checked by the rewrite agent. -- All preceding phase items have both ✅ and ✔️ checked. -- The implementation matches the intent in `REWRITE_PLAN.md`. -- Tests exist, are meaningful, and pass. -- Build and type checks pass. -- No quality issues from the criteria above remain unresolved. -- Any limitations or known gaps are reported clearly to the user. diff --git a/.zed/rules/rewrite.md b/.zed/rules/rewrite.md deleted file mode 100644 index 10e8db7..0000000 --- a/.zed/rules/rewrite.md +++ /dev/null @@ -1,114 +0,0 @@ -# Rewrite Agent Rules - -These rules apply when implementing the Next.js rewrite on the `rewrite/next` branch. - -## Checklist Tracking - -`CHECKLIST.md` uses three checkbox states: -- `[ ]` — pending and **required**; blocks the next phase -- `[x]` — done -- `[~]` — optional or deferred; **never blocks phase advancement** - -Rules: -- The ✅ column is yours; the ✔️ column belongs to the review agent. -- After completing each numbered item, mark its ✅ box by changing `[ ]` to `[x]`. -- If you complete an optional item (`[~]`), change it to `[x]`. If you skip it, leave it as `[~]`. -- Before starting any item in a new phase, read `CHECKLIST.md` and confirm that every **required** (`[ ]`/`[x]`) item in all preceding phases has `[x]` in both ✅ and ✔️. Items marked `[~]` in both columns do not need to be completed first. -- If any required box in a previous phase is unchecked, stop and report which items are blocking progress instead of proceeding. - -## Rewrite Context - -- `REWRITE_PLAN.md` is the guiding plan for the migration from the current CRA + Express app to a Next.js App Router + TypeScript app. -- When working on the rewrite, follow the migration phases in `REWRITE_PLAN.md` unless the user explicitly asks for a different order. -- Treat each numbered migration item as a checkpoint: implement it, update its ✅ box in `CHECKLIST.md`, add or update tests, run the relevant verification, then continue. -- Prefer building the new Next.js structure in parallel until feature parity is proven. Do not delete `server/`, `oebb-planner-app/`, or `oebb-planner.jsx` before equivalent Next.js behavior is implemented, tested, and the user has clearly asked for cleanup. -- Preserve existing API contracts and user-visible behavior during migration unless the rewrite plan or user request explicitly changes them. -- Use `npm` consistently because the existing project uses `package-lock.json`. - -## Work Step By Step - -- Start by reading the relevant files and identifying the smallest safe next step. -- State the plan before making non-trivial changes. -- Implement one coherent change at a time. -- After each step, review the diff and check whether it still matches the intended behavior. -- Do not move on to the next step while the current step has unresolved compile errors, failing tests, or obvious regressions. -- Prefer small, targeted edits over broad rewrites. -- Preserve existing behavior unless the user explicitly asks to change it. -- When a task spans multiple rewrite phases, complete one vertical slice at a time where practical: type or library code, route or hook, UI integration, tests, then verification. -- Keep reusable logic in `src/lib`, side effects in hooks or route handlers, and shared contracts in `src/types`. - -## Testing Requirements - -- Add or update tests for every new feature, bug fix, and behavior change. -- Put tests near the code they cover and follow the existing test style. -- Cover the main success path, important edge cases, and failure behavior. -- Do not remove or weaken tests just to make the suite pass. -- If a change cannot reasonably be tested, explain why and add the closest practical verification. -- For the Next.js rewrite, prefer unit tests for `src/lib`, route tests for `src/app/api`, and component smoke or behavior tests for UI components. -- Mock external services in automated tests, including ÖBB HAFAS, Nominatim, OSRM, geolocation, time, and calendar downloads. Do not make tests depend on live network availability. -- Test TypeScript data shapes and boundary parsing where API responses are transformed into app types. - -## Verification Before Moving On - -- Run the narrowest relevant tests after each meaningful change. -- Run the broader project checks before finishing. -- For server changes, run: - -```bash -cd server -npm test -``` - -- For React app changes, run: - -```bash -cd oebb-planner-app -CI=true npm test -- --watchAll=false -npm run build -``` - -- For the Next.js rewrite, once the root Next.js project exists, run the relevant root checks instead: - -```bash -npm test -npm run build -``` - -- If available, also run type-checking and linting scripts before finishing: - -```bash -npm run typecheck -npm run lint -``` - -- If a change touches both server and app behavior, run both sets of checks. -- If a command fails, stop, inspect the failure, fix the cause, and rerun the command. -- Do not claim the work is complete until the relevant checks pass, or until the remaining blocker is clearly reported. - -## Quality Bar - -- Make sure additions do not introduce compile errors, lint errors, runtime crashes, or broken imports. -- Check that public APIs, endpoint contracts, props, and data shapes remain compatible with existing callers. -- Keep error handling explicit and user-facing failures understandable. -- Avoid hidden global state, timing assumptions, and network-dependent tests unless the project already uses that pattern. -- Keep dependencies unchanged unless they are necessary for the task and justified. -- Do not commit generated artifacts, caches, logs, or local environment files. -- Keep TypeScript strictness intact once introduced. Do not use `any` as a shortcut around unclear domain types. -- Keep server-only code out of client components. Route handlers and `src/lib` clients that use secrets, privileged headers, or upstream service details must not be imported into browser-only code. -- Respect Nominatim usage requirements when implementing geocoding: configurable base URL, clear user agent, rate-limit-aware caching, and no direct browser calls to the public service. -- Keep OSRM and HAFAS clients behind API routes or server-side utilities so failures can be normalized and tested. -- For UI work, preserve accessibility basics: semantic buttons and links, labels for inputs, keyboard-operable controls, visible loading and error states. - -## Completion Checklist - -Before finishing a step, confirm: - -- The requested behavior is implemented. -- The ✅ box for the corresponding item in `CHECKLIST.md` is checked. -- The change matches the relevant phase or numbered item in `REWRITE_PLAN.md`, when applicable. -- Tests were added or updated where appropriate. -- Relevant tests and build checks pass. -- The change is scoped to the request. -- No unrelated user changes were overwritten. -- Old implementation files were not removed unless parity is tested and cleanup was requested. -- Any limitations or skipped checks are reported clearly. diff --git a/.zed/settings.json b/.zed/settings.json deleted file mode 100644 index 79860b3..0000000 --- a/.zed/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -// Folder-specific settings -// -// For a full list of overridable settings, and general information on folder-specific settings, -// see the documentation: https://zed.dev/docs/configuring-zed#settings-files -{} diff --git a/.zed/tasks.json b/.zed/tasks.json deleted file mode 100644 index 19e45d2..0000000 --- a/.zed/tasks.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - "label": "Run Wiener Linien agent loop", - "command": "python", - "args": [ - "ttl_agent_gemma4.py", - "--workspace", "/home/fegger/Code/TimeToLeave", - "--run-until-done" - ], - "cwd": "$ZED_WORKTREE_ROOT/agent_loop", - "use_new_terminal": true, - "allow_concurrent_runs": false, - "reveal": "always" - } -] diff --git a/agent_loop/agent_base_gemma4.py b/agent_loop/agent_base_gemma4.py deleted file mode 100644 index 55ce35b..0000000 --- a/agent_loop/agent_base_gemma4.py +++ /dev/null @@ -1,1478 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import os -import re -import shlex -import shutil -import subprocess -import time -from contextvars import ContextVar -from dataclasses import dataclass -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, List, Optional - -import requests - -# Strips ... blocks that Gemma 4 may emit inline in the content -# field rather than in the dedicated thinking field, depending on Ollama version. -_THINK_RE = re.compile(r".*?", re.DOTALL) - -# --------------------------------------------------------------------------- -# Schemas -# --------------------------------------------------------------------------- - -WRITER_SCHEMA = { - "type": "object", - "properties": { - "summary": {"type": "string"}, - "files": { - "type": "array", - "items": { - "type": "object", - "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, - "required": ["path", "content"], - "additionalProperties": False, - }, - }, - "tests": {"type": "array", "items": {"type": "string"}}, - "notes": {"type": "array", "items": {"type": "string"}}, - }, - "required": ["summary", "files", "tests", "notes"], - "additionalProperties": False, -} - -# Unified review schema — used by both the inline reviewer and as the canonical -# definition for any external tooling that consumes agent state files. -REVIEW_SCHEMA = { - "type": "object", - "properties": { - "verdict": {"type": "string", "enum": ["approve", "needs_changes"]}, - "summary": {"type": "string"}, - "critical_issues": {"type": "array", "items": {"type": "string"}}, - "important_improvements": {"type": "array", "items": {"type": "string"}}, - "preserve": {"type": "array", "items": {"type": "string"}}, - "rewrite_strategy": {"type": "array", "items": {"type": "string"}}, - "missing_files": {"type": "array", "items": {"type": "string"}}, - "test_gaps": {"type": "array", "items": {"type": "string"}}, - "file_comments": { - "type": "array", - "items": { - "type": "object", - "properties": {"path": {"type": "string"}, "comment": {"type": "string"}}, - "required": ["path", "comment"], - "additionalProperties": False, - }, - }, - }, - "required": [ - "verdict", "summary", "critical_issues", "important_improvements", - "preserve", "rewrite_strategy", "missing_files", "test_gaps", "file_comments", - ], - "additionalProperties": False, -} - -DESIGN_SCHEMA = { - "type": "object", - "properties": { - "summary": {"type": "string"}, - "files": { - "type": "array", - "items": { - "type": "object", - "properties": {"path": {"type": "string"}, "purpose": {"type": "string"}}, - "required": ["path", "purpose"], - "additionalProperties": False, - }, - }, - "responsibilities": {"type": "array", "items": {"type": "string"}}, - "interfaces": {"type": "array", "items": {"type": "string"}}, - "integration_points": {"type": "array", "items": {"type": "string"}}, - "dependencies": {"type": "array", "items": {"type": "string"}}, - "testing_plan": {"type": "array", "items": {"type": "string"}}, - "risks": {"type": "array", "items": {"type": "string"}}, - "assumptions": {"type": "array", "items": {"type": "string"}}, - }, - "required": [ - "summary", "files", "responsibilities", "interfaces", "integration_points", - "dependencies", "testing_plan", "risks", "assumptions", - ], - "additionalProperties": False, -} - -# --------------------------------------------------------------------------- -# Config -# --------------------------------------------------------------------------- - -@dataclass -class Config: - api_base: str - writer_model: str - reviewer_model: str - design_model: str - workspace: Path - outputs: Path - task: str - max_review_loops: int - max_context_chars: int - max_files: int - language: str - writer_system_prompt: str - reviewer_system_prompt: str - design_system_prompt: str - log_dir: Path - redesign_after: int # trigger design revision after this many consecutive failed loops (0 = disabled) - per_file_write: bool = True # write one file per pass instead of all files at once - # Write approved files directly into workspace instead of a separate output dir. - # Also marks the ✔️ column in CHECKLIST.md after a reviewer approval. - write_to_workspace: bool = False - # Keep running until all ✅ boxes in CHECKLIST.md are checked. Implies write_to_workspace. - run_until_done: bool = False - # Maximum seconds of silence before a stream is considered hung. - # Must be longer than the worst-case prefill time (prompt processing before - # the first token arrives). For a 32B model + large context this can be - # 5-10 minutes. Default: 600s. Between-token gaps in practice are <1s. - read_timeout: int = 600 - # Context chars passed to each individual file write. Smaller than - # max_context_chars to keep per-file prompts short and reduce prefill time. - per_file_context_chars: int = 40_000 - # Writer implementation. "ollama" keeps the historical JSON full-file - # writer; "aider" lets the Aider CLI edit the workspace and then captures - # changed files back into the existing review loop. - writer_backend: str = "ollama" - aider_command: str = "aider" - aider_model: str = "" - aider_extra_args: str = "" - -# --------------------------------------------------------------------------- -# Logging -# --------------------------------------------------------------------------- - -# ContextVar instead of a plain module global so that concurrent runs in -# different threads or async tasks each maintain their own log file pointer. -_LOG_FILE: ContextVar[Optional[Path]] = ContextVar("_LOG_FILE", default=None) - - -def _set_log_file(path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - _LOG_FILE.set(path) - - -def _emit(text: str) -> None: - print(text, flush=True) - lf = _LOG_FILE.get() - if lf is not None: - with lf.open("a", encoding="utf-8") as f: - f.write(text + "\n") - - -def _log_header(title: str) -> None: - _emit(f"\n=== {title} ===") - - -def _log_kv(key: str, value: str) -> None: - _emit(f"{key}: {value}") - -# --------------------------------------------------------------------------- -# Utilities -# --------------------------------------------------------------------------- - -def _now_ts() -> float: - return time.time() - - -def _fmt_elapsed(seconds: float) -> str: - if seconds < 1: - return f"{seconds:.2f}s" - if seconds < 60: - return f"{seconds:.1f}s" - minutes = int(seconds // 60) - rem = seconds % 60 - return f"{minutes}m {rem:.1f}s" - - -def _truncate(text: str, limit: int = 4000) -> str: - text = (text or "").strip() - if len(text) <= limit: - return text - return text[:limit] + "\n...[truncated]..." - - -def _extract_json(text: str) -> str: - t = text.strip() - start = t.find("{") - if start == -1: - return t - depth = 0 - for i, ch in enumerate(t[start:], start): - if ch == "{": - depth += 1 - elif ch == "}": - depth -= 1 - if depth == 0: - return t[start : i + 1] - return t[start:] - - -def _pretty_json_or_text(text: str) -> str: - text = (text or "").strip() - if not text: - return text - try: - obj = json.loads(_extract_json(text)) - return json.dumps(obj, ensure_ascii=False, indent=2) - except Exception: - return text - - -def _log_model_output( - label: str, model: str, content: str, elapsed: float, iteration: int | None = None -) -> None: - _log_header(label) - if iteration is not None: - _log_kv("Iteration", str(iteration)) - _log_kv("Model", model) - _log_kv("Elapsed", _fmt_elapsed(elapsed)) - _emit("--- model output ---") - _emit(_truncate(_pretty_json_or_text(content), limit=4000)) - _emit("--------------------") - -# --------------------------------------------------------------------------- -# Workspace reader -# --------------------------------------------------------------------------- - -_SKIP_SUFFIXES = frozenset({ - ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".pdf", - ".zip", ".tar", ".gz", ".db", ".sqlite", ".lock", - ".map", ".pyc", ".pyo", ".tsbuildinfo", -}) - -# Directory names that are never useful context for a coding agent. -_SKIP_DIRS = frozenset({ - "node_modules", ".next", ".git", "__pycache__", - ".idea", ".vscode", "dist", "build", "out", "coverage", - "logs", "runs", "agent_loop", -}) - -# Specific filenames to exclude regardless of location. -_SKIP_FILENAMES = frozenset({ - "package-lock.json", "yarn.lock", "pnpm-lock.yaml", -}) - - -def _read_context(workspace: Path, max_context_chars: int, max_files: int) -> List[Dict[str, str]]: - files: List[Dict[str, str]] = [] - total = 0 - for path in sorted(workspace.rglob("*")): - if not path.is_file(): - continue - if any(part in _SKIP_DIRS for part in path.parts): - continue - if path.suffix.lower() in _SKIP_SUFFIXES: - continue - if path.name in _SKIP_FILENAMES: - continue - try: - text = path.read_text(encoding="utf-8") - except Exception: - continue - remaining = max_context_chars - total - if remaining <= 0: - _emit(f"[warn] context truncated at {max_context_chars} chars after {len(files)} files") - break - if len(text) > remaining: - text = text[:remaining] - _emit(f"[warn] truncated {path.name} to fit context limit") - total += len(text) - files.append({"name": str(path.relative_to(workspace)), "content": text}) - if len(files) >= max_files: - _emit(f"[warn] file limit reached ({max_files} files); remaining files excluded") - break - if total >= max_context_chars: - _emit(f"[warn] context truncated at {max_context_chars} chars after {len(files)} files") - break - return files - -# --------------------------------------------------------------------------- -# Ollama chat -# --------------------------------------------------------------------------- - -def _chat( - api_base: str, - model: str, - system_prompt: str, - user_prompt: str, - schema: Dict[str, Any], - temperature: float, - allow_empty: bool = False, - log_label: str = "Model call", - iteration: int | None = None, - read_timeout: int = 600, -) -> str: - """Call the Ollama /api/chat endpoint using streaming. - - Streaming is essential for long generations (large source files, reasoning - chains). With stream=False the HTTP connection sits idle waiting for the - entire response and hits the read-timeout long before the model finishes. - With streaming each token keeps the connection alive, so generation can - take arbitrarily long without timing out. - - connect_timeout=30 — fail fast if Ollama is unreachable - read_timeout=600 (default) — maximum silence before the FIRST token - (covers model load + prefill time) and - between subsequent tokens. For a 32B model - with a large prompt, prefill alone can take - 5-10 minutes. Between-token gaps in - practice are <1 second. - """ - start = _now_ts() - url = f"{api_base.rstrip('/')}/api/chat" - - base_options: Dict[str, Any] = { - "temperature": temperature, - # Gemma 4 recommended sampling settings (harmless on other models). - "top_p": 0.95, - "top_k": 64, - # Unlimited output tokens. Without this Ollama defaults to ~2048 - # tokens which is far too small for complete source files. - "num_predict": -1, - } - - def _stream_chat(fmt: Any) -> str: - """Stream a /api/chat request and return the concatenated content.""" - payload = { - "model": model, - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ], - "format": fmt, - "options": base_options, - "stream": True, - } - content_chunks: list[str] = [] - thinking_chunks: list[str] = [] - - # connect_timeout=30; read_timeout covers both prefill silence and - # between-token gaps — must be large enough for worst-case prefill. - with requests.post(url, json=payload, stream=True, timeout=(30, read_timeout)) as resp: - if not resp.ok: - raise RuntimeError( - f"Ollama API error {resp.status_code}: {resp.text[:2000]}" - ) - for raw_line in resp.iter_lines(): - if not raw_line: - continue - try: - chunk = json.loads(raw_line) - except json.JSONDecodeError: - continue - msg = chunk.get("message", {}) or {} - delta_content = msg.get("content", "") or "" - delta_thinking = msg.get("thinking", "") or "" - if delta_content: - content_chunks.append(delta_content) - if delta_thinking: - thinking_chunks.append(delta_thinking) - if chunk.get("done"): - break - - content = "".join(content_chunks).strip() - thinking = "".join(thinking_chunks).strip() - # Gemma 4 may emit blocks inline in content rather than in the - # dedicated thinking field depending on Ollama version — strip them so - # raw reasoning never leaks into JSON output or multi-turn history. - content = _THINK_RE.sub("", content).strip() - return content if content else thinking - - # ── Primary attempt: structured schema ─────────────────────────────── - try: - text = _stream_chat(schema) - except RuntimeError as exc: - # Non-2xx from Ollama — fall through to unstructured retry - _emit(f"[warn] Primary stream failed: {exc} — retrying without schema") - text = "" - - elapsed = _now_ts() - start - - if text.strip(): - _log_model_output(log_label, model, text, elapsed, iteration) - return text.strip() - - # ── Fallback: unstructured JSON mode ────────────────────────────────── - # Some models produce empty content when schema enforcement is strict. - # Retry without the schema object, relying on the system prompt instead. - _emit( - f"[warn] {log_label}: primary response empty after {_fmt_elapsed(elapsed)} " - f"— retrying in unstructured JSON mode" - ) - try: - fallback_system = system_prompt + "\n\nReturn valid JSON only. No extra text." - payload_fb = { - "model": model, - "messages": [ - {"role": "system", "content": fallback_system}, - {"role": "user", "content": user_prompt}, - ], - "format": "json", - "options": base_options, - "stream": True, - } - fb_chunks: list[str] = [] - with requests.post(url, json=payload_fb, stream=True, timeout=(30, read_timeout)) as resp: - resp.raise_for_status() - for raw_line in resp.iter_lines(): - if not raw_line: - continue - try: - chunk = json.loads(raw_line) - except json.JSONDecodeError: - continue - msg = chunk.get("message", {}) or {} - fb_chunks.append(msg.get("content", "") or "") - if chunk.get("done"): - break - text = "".join(fb_chunks).strip() - except Exception as exc: - raise RuntimeError( - f"Both primary and fallback streams failed for model {model}. " - f"Last error: {exc}" - ) from exc - - elapsed = _now_ts() - start - if not text and not allow_empty: - raise RuntimeError( - f"Model {model} returned empty content on both attempts " - f"after {_fmt_elapsed(elapsed)}. " - f"Check that Ollama is reachable at {url} and the model is loaded." - ) - _log_model_output(f"{log_label} (fallback)", model, text, elapsed, iteration) - return text.strip() - -# --------------------------------------------------------------------------- -# Normalisation helpers -# --------------------------------------------------------------------------- - -def _safe_json(raw: str, fallback: Dict[str, Any]) -> Dict[str, Any]: - try: - return json.loads(_extract_json(raw)) - except Exception: - return fallback - - -def _fallback_review(reason: str, approve: bool = False) -> Dict[str, Any]: - # Safe default is needs_changes — never silently approve on error/timeout. - return { - "verdict": "approve" if approve else "needs_changes", - "summary": reason, - "critical_issues": [] if approve else [reason], - "important_improvements": [] if approve else ["Retry review manually if needed."], - "preserve": [], - "rewrite_strategy": [], - "missing_files": [], - "test_gaps": [], - "file_comments": [], - } - - -def _normalize_draft(data: Dict[str, Any]) -> Dict[str, Any]: - if not isinstance(data.get("files"), list): - data["files"] = [] - if not isinstance(data.get("tests"), list): - data["tests"] = [] - if not isinstance(data.get("notes"), list): - data["notes"] = [] - if not isinstance(data.get("summary"), str): - data["summary"] = "No summary provided." - return data - - -def _normalize_design(data: Dict[str, Any]) -> Dict[str, Any]: - for key in ( - "files", "responsibilities", "interfaces", "integration_points", - "dependencies", "testing_plan", "risks", "assumptions", - ): - if not isinstance(data.get(key), list): - data[key] = [] - if not isinstance(data.get("summary"), str): - data["summary"] = "No design summary provided." - return data - - -def _normalize_review(data: Dict[str, Any]) -> Dict[str, Any]: - if data.get("verdict") not in {"approve", "needs_changes"}: - data["verdict"] = "needs_changes" - for k in ( - "critical_issues", "important_improvements", "preserve", - "rewrite_strategy", "missing_files", "test_gaps", "file_comments", - ): - if not isinstance(data.get(k), list): - data[k] = [] - if not isinstance(data.get("summary"), str): - data["summary"] = "Reviewer returned no summary." - return data - -# --------------------------------------------------------------------------- -# I/O helpers -# --------------------------------------------------------------------------- - -def _write_state(outputs: Path, stem: str, state: Dict[str, Any], suffix: str = "") -> Path: - state_dir = outputs / "coding-agent-state" - state_dir.mkdir(parents=True, exist_ok=True) - path = state_dir / f"{stem}{suffix}.json" - path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8") - return path - - -def _write_files(root: Path, draft: Dict[str, Any]) -> Path: - root.mkdir(parents=True, exist_ok=True) - root_resolved = root.resolve() - for item in draft.get("files", []): - raw = item.get("path", "").strip() - parts = [p for p in Path(raw).parts if p not in ("..", ".", "") and p != "/"] - if not parts: - continue - dest = root / Path(*parts) - try: - if not dest.resolve().is_relative_to(root_resolved): - _emit(f"[warn] skipping unsafe path: {raw!r}") - continue - except Exception: - continue - dest.parent.mkdir(parents=True, exist_ok=True) - dest.write_text(item["content"], encoding="utf-8") - return root - - -def _snapshot_workspace_files(workspace: Path) -> Dict[str, str]: - """Capture current text-file contents so Aider edits can be diffed safely.""" - snapshot: Dict[str, str] = {} - for path in sorted(workspace.rglob("*")): - if not path.is_file(): - continue - if any(part in _SKIP_DIRS for part in path.relative_to(workspace).parts): - continue - if path.suffix.lower() in _SKIP_SUFFIXES: - continue - if path.name in _SKIP_FILENAMES: - continue - try: - snapshot[str(path.relative_to(workspace))] = path.read_text(encoding="utf-8") - except Exception: - continue - return snapshot - - -def _changed_files_from_snapshots(before: Dict[str, str], after: Dict[str, str]) -> List[Dict[str, str]]: - changed: List[Dict[str, str]] = [] - for path in sorted(after): - if before.get(path) != after[path]: - changed.append({"path": path, "content": after[path]}) - return changed - - -def _merge_draft_files(base: Dict[str, Any], updates: Dict[str, Any]) -> Dict[str, Any]: - merged: Dict[str, Dict[str, str]] = { - f["path"]: f - for f in base.get("files", []) - if isinstance(f, dict) and isinstance(f.get("path"), str) - } - for f in updates.get("files", []): - if isinstance(f, dict) and isinstance(f.get("path"), str): - merged[f["path"]] = f - return _normalize_draft({ - "summary": updates.get("summary") or base.get("summary", ""), - "files": list(merged.values()), - "tests": [*base.get("tests", []), *updates.get("tests", [])], - "notes": [*base.get("notes", []), *updates.get("notes", [])], - }) - - -def _assert_git_workspace(workspace: Path) -> None: - try: - subprocess.run( - ["git", "rev-parse", "--is-inside-work-tree"], - cwd=workspace, - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True, - ) - except (FileNotFoundError, subprocess.CalledProcessError) as exc: - raise RuntimeError("Aider backend requires --workspace to be inside a git repository.") from exc - - -def _build_aider_command(config: Config, message: str, file_paths: List[str]) -> List[str]: - cmd = shlex.split(config.aider_command) - if not cmd: - raise RuntimeError("--aider-command cannot be empty.") - - cmd.extend(["--yes-always", "--no-auto-commits"]) - if config.aider_model: - cmd.extend(["--model", config.aider_model]) - if config.aider_extra_args: - cmd.extend(shlex.split(config.aider_extra_args)) - cmd.extend(["--message", message]) - - # Passing expected files keeps Aider focused while still allowing it to - # create extra files when the prompt calls for tests or supporting modules. - cmd.extend([p for p in file_paths if p and not Path(p).is_absolute()]) - return cmd - - -def _run_aider_writer( - config: Config, - prompt: str, - file_paths: List[str], - label: str, - iteration: int, -) -> Dict[str, Any]: - if shutil.which(shlex.split(config.aider_command)[0]) is None: - raise RuntimeError( - f"Aider command not found: {config.aider_command!r}. " - "Install aider-chat or pass --aider-command." - ) - _assert_git_workspace(config.workspace) - - before = _snapshot_workspace_files(config.workspace) - cmd = _build_aider_command(config, prompt, file_paths) - - _log_header(label) - _log_kv("Iteration", str(iteration)) - _log_kv("Command", " ".join(shlex.quote(part) for part in cmd[:4]) + " ...") - - start = _now_ts() - proc = subprocess.run( - cmd, - cwd=config.workspace, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - elapsed = _now_ts() - start - _log_kv("Elapsed", _fmt_elapsed(elapsed)) - _emit("--- aider output ---") - _emit(_truncate(proc.stdout or "", limit=6000)) - _emit("--------------------") - - if proc.returncode != 0: - raise RuntimeError(f"Aider exited with status {proc.returncode}. See log for output.") - - after = _snapshot_workspace_files(config.workspace) - files = _changed_files_from_snapshots(before, after) - return _normalize_draft({ - "summary": f"Aider changed {len(files)} file(s).", - "files": files, - "tests": [], - "notes": [f"Aider backend: {label}"], - }) - - -def _mark_checklist_reviewed(workspace: Path) -> bool: - """After reviewer approves, mark the first step where ✅=[x] and ✔️=[ ] as reviewed.""" - checklist = workspace / "CHECKLIST.md" - if not checklist.exists(): - return False - text = checklist.read_text(encoding="utf-8") - # Match the first table row where the written column is [x] and reviewed column is [ ] - # Row format: | N | description | [x] | [ ] | - pattern = re.compile(r'(\|[^|]+\|[^|]+\| *)\[x\]( *\| *)\[ \]( *\|)') - match = pattern.search(text) - if not match: - return False - updated = ( - text[: match.start()] - + match.group(1) + "[x]" - + match.group(2) + "[x]" - + match.group(3) - + text[match.end() :] - ) - checklist.write_text(updated, encoding="utf-8") - _emit("[checklist] Marked ✔️ reviewed in CHECKLIST.md") - return True - - -def _checklist_has_pending(workspace: Path) -> bool: - """Return True if CHECKLIST.md has any step where ✅ is still [ ].""" - checklist = workspace / "CHECKLIST.md" - if not checklist.exists(): - return False - text = checklist.read_text(encoding="utf-8") - # Match any table row where the written (✅) column is [ ] - # Row format: | N | description | [ ] | ... | - return bool(re.search(r'\|[^|]+\|[^|]+\| *\[ \] *\|', text)) - -# --------------------------------------------------------------------------- -# Design stage (extracted so it can be called again for revisions) -# --------------------------------------------------------------------------- - -def _run_design(config: Config, ctx: str, extra_context: str = "") -> Dict[str, Any]: - """Run (or re-run) the design stage. - - Pass *extra_context* to inject accumulated reviewer feedback when triggering - a mid-loop design revision. - """ - parts = [ - f"Task:\n{config.task}", - f"\nContext files:\n{ctx}", - ] - if extra_context: - parts.append(f"\n{extra_context}") - parts.append(f"\nProduce a concise implementation design for a {config.language} project.") - - design_raw = _chat( - config.api_base, config.design_model, config.design_system_prompt, - "\n".join(parts), DESIGN_SCHEMA, 0.0, - log_label="Design stage", - iteration=0, - read_timeout=config.read_timeout, - ) - return _normalize_design(_safe_json(design_raw, { - "summary": "Could not parse design output", - "files": [], "responsibilities": [], "interfaces": [], - "integration_points": [], "dependencies": [], - "testing_plan": [], "risks": [], "assumptions": [], - })) - - -# --------------------------------------------------------------------------- -# Per-file writing helpers -# --------------------------------------------------------------------------- - -def _write_single_file( - config: "Config", - design: dict, - file_entry: dict, - already_written: list, - ctx: str, - iteration: int, -) -> dict: - """Write a single file as specified by its design entry. - - *already_written* contains files produced in previous passes so the writer - can maintain consistent imports, types, and naming conventions. - - The workspace context (*ctx*) is capped at *per_file_context_chars* rather - than the full *max_context_chars*. Shorter prompts mean shorter prefill - times and fewer read-timeout risks when writing individual files. - """ - # Cap workspace context for per-file calls — a single file only needs - # enough context to understand the surrounding project, not the full repo. - trimmed_ctx = ctx[: config.per_file_context_chars] - if len(ctx) > config.per_file_context_chars: - trimmed_ctx += "\n... [workspace context trimmed — see full context in design] ..." - - already_ctx = "" - if already_written: - already_ctx = ( - "\n\nAlready-written files (read-only reference — use consistent " - "types, imports, and naming):\n\n" - + "\n\n".join( - f"--- {f['path']} ---\n{f['content']}" for f in already_written - ) - ) - - prompt = ( - f"Task:\n{config.task}\n\n" - f"Full design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"YOUR CURRENT ASSIGNMENT — write this file only:\n" - f" path : {file_entry['path']}\n" - f" purpose: {file_entry.get('purpose', '(see design)')}\n\n" - f"Existing workspace files (read-only context):\n{trimmed_ctx}" - f"{already_ctx}\n\n" - f"Return only the file at '{file_entry['path']}' plus any directly " - f"associated test file. Do not write files outside your assignment." - ) - - raw = _chat( - config.api_base, config.writer_model, config.writer_system_prompt, - prompt, WRITER_SCHEMA, 0.1, - log_label=f"Writer [{file_entry['path']}]", - iteration=iteration, - read_timeout=config.read_timeout, - ) - result = _normalize_draft(_safe_json(raw, { - "summary": f"Could not parse output for {file_entry['path']}", - "files": [], "tests": [], "notes": [raw[:1000]], - })) - if not result.get("files"): - _emit(f"[warn] Writer produced no files for {file_entry['path']} — skipping") - return result - - -def _files_needing_rewrite(review: dict, draft_files: list) -> set: - """Return the set of file paths the reviewer flagged for changes.""" - flagged = { - fc.get("path", "") - for fc in review.get("file_comments", []) - if fc.get("path") - } - draft_paths = {f["path"] for f in draft_files} - for issue in review.get("critical_issues", []): - for path in draft_paths: - if path in issue: - flagged.add(path) - if review.get("critical_issues") and not flagged: - return draft_paths - return flagged - - -def _rewrite_targeted_files( - config: "Config", - design: dict, - draft: dict, - review: dict, - ctx: str, - iteration: int, - temperature: float, -) -> dict: - """Rewrite only the files flagged in the review, merging back into draft.""" - all_files = draft.get("files", []) - flagged_paths = _files_needing_rewrite(review, all_files) - - if not flagged_paths: - _emit("[per-file] No specific files flagged — falling back to full rewrite") - return draft - - _emit(f"[per-file] Targeted rewrite for: {sorted(flagged_paths)}") - design_file_map = {f["path"]: f for f in design.get("files", [])} - current_file_map: Dict[str, Dict[str, str]] = {f["path"]: f for f in all_files} - - for path in sorted(flagged_paths): - if path not in current_file_map: - _emit(f"[per-file] Skipping unknown path: {path}") - continue - - file_entry = design_file_map.get(path, {"path": path, "purpose": "(not in design)"}) - current_content = current_file_map[path].get("content", "") - other_files_ctx = "\n\n".join( - f"--- {f['path']} ---\n{f['content']}" - for f in all_files if f["path"] != path - ) - file_comments = [ - fc["comment"] - for fc in review.get("file_comments", []) - if fc.get("path") == path - ] - - prompt = ( - f"Task:\n{config.task}\n\n" - f"Full design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"YOUR CURRENT ASSIGNMENT — fix this file only:\n" - f" path : {path}\n" - f" purpose: {file_entry.get('purpose', '(see design)')}\n\n" - f"Current content of '{path}':\n{current_content}\n\n" - f"Other project files (read-only reference):\n{other_files_ctx}\n\n" - f"Review issues specific to '{path}':\n" - + ("\n".join(f"- {c}" for c in file_comments) if file_comments - else "(none — fix based on general review below)") - + f"\n\nGeneral review summary: {review.get('summary', '')}\n" - f"Critical issues: {review.get('critical_issues', [])}\n" - f"Rewrite strategy: {review.get('rewrite_strategy', [])}\n\n" - f"Return only '{path}' in the files array. Do not rewrite other files." - ) - - raw = _chat( - config.api_base, config.writer_model, config.writer_system_prompt, - prompt, WRITER_SCHEMA, temperature, - log_label=f"Writer rewrite [{path}]", - iteration=iteration, - read_timeout=config.read_timeout, - ) - result = _normalize_draft(_safe_json(raw, { - "summary": f"Could not parse rewrite for {path}", - "files": [], "tests": [], "notes": [raw[:1000]], - })) - for updated_file in result.get("files", []): - updated_path = updated_file.get("path", "") - current_file_map[updated_path] = updated_file - _emit(f"[per-file] Updated: {updated_path}") - - return _normalize_draft({ - "summary": draft.get("summary", ""), - "files": list(current_file_map.values()), - "tests": draft.get("tests", []), - "notes": draft.get("notes", []), - }) - -# --------------------------------------------------------------------------- -# Main agent loop -# --------------------------------------------------------------------------- - -def run_agent(config: Config) -> Dict[str, Any]: - run_stamp = datetime.now().strftime("%Y%m%d-%H%M%S") - _set_log_file(config.log_dir / f"{config.language.lower()}-agent-{run_stamp}.log") - _log_header("Agent run started") - _log_kv("Language", config.language) - _log_kv("Writer model", config.writer_model) - _log_kv("Reviewer model", config.reviewer_model) - _log_kv("Design model", config.design_model) - _log_kv("Writer backend", config.writer_backend) - - if config.writer_backend == "aider" and not config.write_to_workspace: - raise RuntimeError( - "The Aider writer backend edits the workspace directly. " - "Run with --write-to-workspace or --run-until-done." - ) - - context_files = _read_context(config.workspace, config.max_context_chars, config.max_files) - ctx = ( - "\n\n".join(f"--- FILE: {f['name']} ---\n{f['content']}" for f in context_files) - if context_files else "(no workspace files found)" - ) - - # --- Design --- - design = _run_design(config, ctx) - _log_header("Design parsed") - _emit(_truncate(json.dumps(design, ensure_ascii=False, indent=2))) - - # --- Initial write --- - # Per-file mode: write one file at a time so each generation stays within - # a manageable output size and avoids streaming timeouts on large projects. - # Bulk mode: write everything in one pass (useful for very small tasks). - if config.writer_backend == "aider": - _log_header("Initial write — aider backend") - aider_prompt = ( - f"Task:\n{config.task}\n\n" - f"Design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"Implement only this task. Keep the change scoped to the design, " - f"update CHECKLIST.md as instructed by the project prompt, and add " - f"or update tests required for the current step." - ) - draft = _run_aider_writer( - config, - aider_prompt, - [f.get("path", "") for f in design.get("files", [])], - label="Aider writer stage", - iteration=0, - ) - elif config.per_file_write and design.get("files"): - _log_header(f"Initial write — per-file mode ({len(design['files'])} files)") - combined: Dict[str, Any] = {} # path → file dict (deduplicates by path) - combined_tests: List[str] = [] - combined_notes: List[str] = [] - already_written: List[Dict[str, str]] = [] - - for i, file_entry in enumerate(design["files"]): - _log_header(f"Writing file {i + 1}/{len(design['files'])}: {file_entry['path']}") - result = _write_single_file(config, design, file_entry, already_written, ctx, iteration=0) - for f in result.get("files", []): - combined[f["path"]] = f - already_written.append(f) - combined_tests.extend(result.get("tests", [])) - combined_notes.extend(result.get("notes", [])) - - draft = _normalize_draft({ - "summary": f"Per-file write: {len(combined)} files", - "files": list(combined.values()), - "tests": combined_tests, - "notes": combined_notes, - }) - else: - _log_header("Initial write — bulk mode") - writer_prompt = ( - f"Task:\n{config.task}\n\n" - f"Design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"Context files:\n{ctx}\n\n" - f"Generate code that follows the design." - ) - draft_raw = _chat( - config.api_base, config.writer_model, config.writer_system_prompt, - writer_prompt, WRITER_SCHEMA, 0.1, - log_label="Writer stage", iteration=0, - read_timeout=config.read_timeout, - ) - draft = _normalize_draft(_safe_json(draft_raw, { - "summary": "Could not parse writer output", - "files": [], "tests": [], "notes": [draft_raw[:3000]], - })) - - _log_header("Writer parsed") - _emit(_truncate(json.dumps(draft, ensure_ascii=False, indent=2))) - - if not draft.get("files"): - # Diagnose and retry regardless of mode. - _log_header("Writer returned no files — diagnosing") - _emit( - "[diagnose] All files are empty. In per-file mode this means every " - "individual file call returned nothing. In bulk mode check the log for " - "truncation. Retrying with an explicit files-or-fail prompt." - ) - if config.writer_backend == "aider": - retry_prompt = ( - f"Task:\n{config.task}\n\n" - f"Design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"The previous Aider run produced no changed files. Make concrete " - f"workspace edits for the current task now. Do not stop at an " - f"explanation." - ) - draft = _run_aider_writer( - config, - retry_prompt, - [f.get("path", "") for f in design.get("files", [])], - label="Aider writer retry stage", - iteration=0, - ) - else: - retry_prompt = ( - f"Task:\n{config.task}\n\n" - f"Design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"Context files:\n{ctx}\n\n" - f'You MUST write all source files listed in the design. ' - f'Return a JSON object: ' - f'{{"summary":"...","files":[{{"path":"...","content":"..."}}],"tests":[],"notes":[]}}\n\n' - f"The files array MUST contain at least one entry." - ) - retry_raw = _chat( - config.api_base, config.writer_model, config.writer_system_prompt, - retry_prompt, {"type": "object"}, temperature=0.4, - log_label="Writer retry stage", iteration=0, - read_timeout=config.read_timeout, - ) - draft = _normalize_draft(_safe_json(retry_raw, { - "summary": "Could not parse writer retry output", - "files": [], "tests": [], "notes": [retry_raw[:3000]], - })) - _log_header("Writer retry parsed") - _emit(_truncate(json.dumps(draft, ensure_ascii=False, indent=2))) - - if not draft.get("files"): - raise RuntimeError( - "Writer returned no files on initial attempt and retry. " - "Check the log for '[diagnose]' lines. Common causes: " - "streaming error, model not loaded, or design produced no file entries." - ) - - stem = run_stamp - - # Partial state — written immediately so progress isn't lost on crash - partial = { - "language": config.language, "task": config.task, "design": design, - "writer_model": config.writer_model, "reviewer_model": config.reviewer_model, - "design_model": config.design_model, "draft": draft, - "review": _fallback_review("Review not completed yet."), - "loop_info": { - "loops_used": 0, "max_review_loops": config.max_review_loops, - "stop_reason": "review_not_started", "final_verdict": "needs_changes", - "designs_used": 1, "history": [], - }, - "log_file": str(_LOG_FILE.get() or ""), - } - _write_state(config.outputs, f"{config.language.lower()}-{stem}", partial) - - # --- Inline reviewer --- - def do_review(cur_draft: Dict[str, Any], iteration: int) -> Dict[str, Any]: - review_prompt = ( - f"Original task:\n{config.task}\n\n" - f"Design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"Draft JSON:\n{json.dumps(cur_draft, ensure_ascii=False, indent=2)}" - ) - try: - raw = _chat( - config.api_base, config.reviewer_model, config.reviewer_system_prompt, - review_prompt, REVIEW_SCHEMA, 0.0, - allow_empty=True, log_label="Reviewer stage", iteration=iteration, - read_timeout=config.read_timeout, - ) - if not raw.strip(): - # Empty response — conservative: don't approve - return _fallback_review("Reviewer returned an empty response.") - data = _safe_json(raw, _fallback_review("Reviewer returned non-parseable output.")) - rv = _normalize_review(data) - _log_header("Reviewer parsed") - _emit(_truncate(json.dumps(rv, ensure_ascii=False, indent=2))) - return rv - except Exception as e: - # Never auto-approve on error — safe default is needs_changes - return _fallback_review(f"Reviewer error: {type(e).__name__}: {e}", approve=False) - - # --- Review / rewrite loop --- - review = do_review(draft, 0) - - # full_history stores the complete draft summary + review per iteration so - # the -history file is genuinely useful for debugging convergence. - full_history: List[Dict[str, Any]] = [{ - "iteration": 0, - "draft_summary": draft.get("summary", ""), - "file_paths": [f.get("path", "") for f in draft.get("files", [])], - "review": review, - }] - # Compact summary used inside loop_info for quick inspection - history = [{ - "iteration": 0, - "review_verdict": review.get("verdict", "needs_changes"), - "review_summary": review.get("summary", ""), - "file_count": len(draft.get("files", [])), - }] - - loops_used = 0 - designs_used = 1 - consecutive_needs_changes = 0 if review.get("verdict") == "approve" else 1 - stop_reason = "approved" if review.get("verdict") == "approve" else "not_entered" - - while review.get("verdict") == "needs_changes" and loops_used < config.max_review_loops: - loops_used += 1 - _log_header("Review loop") - _log_kv("Iteration", str(loops_used)) - _log_kv("Verdict", review.get("verdict", "needs_changes")) - _log_kv("Summary", review.get("summary", "")) - - # --- Optional design revision when the writer is stuck --- - if ( - config.redesign_after > 0 - and consecutive_needs_changes > 0 - and consecutive_needs_changes % config.redesign_after == 0 - and designs_used < 3 # cap to prevent infinite redesign cycles - ): - _log_header("Design revision triggered") - feedback_window = full_history[-config.redesign_after:] - accumulated = "\n".join( - f"Iteration {h['iteration']}: {h['review'].get('summary', '')}\n" - f" Critical: {h['review'].get('critical_issues', [])}\n" - f" Strategy: {h['review'].get('rewrite_strategy', [])}" - for h in feedback_window - ) - extra = ( - f"The previous design caused {consecutive_needs_changes} consecutive failed reviews.\n" - f"Accumulated reviewer feedback:\n{accumulated}\n\n" - f"Revise the design to address these issues directly. " - f"Do not repeat a structure that has already failed." - ) - design = _run_design(config, ctx, extra_context=extra) - designs_used += 1 - _log_header("Revised design parsed") - _emit(_truncate(json.dumps(design, ensure_ascii=False, indent=2))) - - # Pass the last 3 reviews to the writer so it can see repeated failures - recent_history = full_history[-3:] - history_context = json.dumps( - [{"iteration": h["iteration"], "review": h["review"]} for h in recent_history], - ensure_ascii=False, indent=2, - ) - - # Gradually raise temperature so a stuck writer tries different approaches. - # Starts at 0.1 and climbs toward 0.6 over successive loops. - write_temperature = min(0.1 + (loops_used - 1) * 0.1, 0.6) - - # Per-file rewrite: only touch files the reviewer flagged. - # Bulk rewrite: rewrite the whole draft (fallback / non-per-file mode). - if config.writer_backend == "aider": - flagged_paths = sorted(_files_needing_rewrite(review, draft.get("files", []))) - missing_paths = [ - path for path in review.get("missing_files", []) - if isinstance(path, str) and path - ] - aider_paths = sorted(set(flagged_paths + missing_paths)) - if not aider_paths: - aider_paths = [f.get("path", "") for f in draft.get("files", [])] - aider_rewrite_prompt = ( - f"Original task:\n{config.task}\n\n" - f"Design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"Review history (last {len(recent_history)} iterations):\n{history_context}\n\n" - f"Current review feedback:\n{json.dumps(review, ensure_ascii=False, indent=2)}\n\n" - f"Patch the workspace to resolve the review feedback. Keep the " - f"change scoped to the current CHECKLIST step." - ) - aider_updates = _run_aider_writer( - config, - aider_rewrite_prompt, - aider_paths, - label="Aider rewrite stage", - iteration=loops_used, - ) - new_draft = _merge_draft_files(draft, aider_updates) - elif config.per_file_write: - new_draft = _rewrite_targeted_files( - config, design, draft, review, ctx, - iteration=loops_used, temperature=write_temperature, - ) - else: - rewrite_prompt = ( - f"Follow-up: Rewrite considering the review.\n\n" - f"Original task:\n{config.task}\n\n" - f"Design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"Previous proposal:\n{json.dumps(draft, ensure_ascii=False, indent=2)}\n\n" - f"Review history (last {len(recent_history)} iterations):\n{history_context}\n\n" - f"Current review feedback:\n{json.dumps(review, ensure_ascii=False, indent=2)}" - ) - rewrite_raw = _chat( - config.api_base, config.writer_model, config.writer_system_prompt, - rewrite_prompt, WRITER_SCHEMA, write_temperature, - log_label="Writer rewrite stage", iteration=loops_used, - read_timeout=config.read_timeout, - ) - new_draft = _normalize_draft(_safe_json(rewrite_raw, draft)) - - _log_header("Writer rewrite parsed") - _emit(_truncate(json.dumps(new_draft, ensure_ascii=False, indent=2))) - - if not new_draft.get("files"): - stop_reason = "rewrite_returned_no_files" - break - - old_files = [(f.get("path", ""), f.get("content", "")) for f in draft.get("files", [])] - new_files_list = [(f.get("path", ""), f.get("content", "")) for f in new_draft.get("files", [])] - - if old_files == new_files_list: - # Writer returned identical output — stall-break at high temperature. - # In per-file mode, rewrite every file unconditionally to break the deadlock. - _log_header("Stall detected — retrying at high temperature") - if config.writer_backend == "aider": - stall_prompt = ( - f"Original task:\n{config.task}\n\n" - f"Design:\n{json.dumps(design, ensure_ascii=False, indent=2)}\n\n" - f"Current review feedback:\n{json.dumps(review, ensure_ascii=False, indent=2)}\n\n" - f"Your previous Aider pass did not change any tracked text files. " - f"Make concrete edits that address the review feedback now." - ) - stall_updates = _run_aider_writer( - config, - stall_prompt, - [f.get("path", "") for f in draft.get("files", [])], - label="Aider stall-break attempt", - iteration=loops_used, - ) - stall_draft = _merge_draft_files(draft, stall_updates) - elif config.per_file_write: - stall_draft = _rewrite_targeted_files( - config, design, draft, review, ctx, - iteration=loops_used, temperature=0.7, - ) - else: - stall_prompt = ( - f"{rewrite_prompt}\n\n" - f"IMPORTANT: Your last rewrite returned files identical to the previous attempt. " - f"You must make concrete changes. " - f"Try a different implementation strategy for the issues flagged in the review." - ) - stall_raw = _chat( - config.api_base, config.writer_model, config.writer_system_prompt, - stall_prompt, WRITER_SCHEMA, 0.7, - log_label="Writer stall-break attempt", iteration=loops_used, - read_timeout=config.read_timeout, - ) - stall_draft = _normalize_draft(_safe_json(stall_raw, new_draft)) - - stall_files = [(f.get("path", ""), f.get("content", "")) for f in stall_draft.get("files", [])] - if stall_files == old_files: - stop_reason = "no_file_changes" - break - new_draft = stall_draft - - draft = new_draft - review = do_review(draft, loops_used) - - history.append({ - "iteration": loops_used, - "review_verdict": review.get("verdict", "needs_changes"), - "review_summary": review.get("summary", ""), - "file_count": len(draft.get("files", [])), - }) - full_history.append({ - "iteration": loops_used, - "draft_summary": draft.get("summary", ""), - "file_paths": [f.get("path", "") for f in draft.get("files", [])], - "review": review, - }) - - if review.get("verdict") == "approve": - consecutive_needs_changes = 0 - stop_reason = "approved" - break - - consecutive_needs_changes += 1 - - else: - stop_reason = "approved" if review.get("verdict") == "approve" else "max_review_loops_reached" - - state = { - "language": config.language, "task": config.task, "design": design, - "writer_model": config.writer_model, "reviewer_model": config.reviewer_model, - "design_model": config.design_model, "draft": draft, "review": review, - "loop_info": { - "loops_used": loops_used, - "max_review_loops": config.max_review_loops, - "stop_reason": stop_reason, - "final_verdict": review.get("verdict", "needs_changes"), - "designs_used": designs_used, - "history": history, - }, - "log_file": str(_LOG_FILE.get() or ""), - } - # Final state — overwrites the partial written at the start - _write_state(config.outputs, f"{config.language.lower()}-{stem}", state) - # Genuine iteration history: one entry per pass with draft summary + full review - _write_state( - config.outputs, - f"{config.language.lower()}-{stem}", - {"task": config.task, "full_history": full_history}, - suffix="-history", - ) - if config.writer_backend == "aider": - project_dir = config.workspace - if stop_reason == "approved": - _mark_checklist_reviewed(config.workspace) - elif config.write_to_workspace: - output_root = config.workspace - project_dir = _write_files(output_root, draft) - if stop_reason == "approved": - _mark_checklist_reviewed(config.workspace) - else: - output_root = config.outputs / f"{config.language.lower()}-agent-output-{stem}" - project_dir = _write_files(output_root, draft) - state["project_dir"] = str(project_dir) - return state - -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - -def parse_args(language: str, writer_prompt: str, reviewer_prompt: str, design_prompt: str) -> Config: - p = argparse.ArgumentParser() - _default_task = os.getenv("AGENT_TASK") - p.add_argument("--task", default=_default_task, required=_default_task is None) - p.add_argument("--workspace", default=os.getenv("AGENT_WORKSPACE", "/workspace")) - p.add_argument("--outputs", default=os.getenv("AGENT_OUTPUTS", "./runs")) - p.add_argument("--writer-model", default=os.getenv("WRITER_MODEL", "gemma4:31b")) - p.add_argument("--reviewer-model", default=os.getenv("REVIEWER_MODEL", "gemma4:31b")) - p.add_argument( - "--design-model", - default=os.getenv("DESIGN_MODEL", os.getenv("REVIEWER_MODEL", "gemma4:31b")), - ) - p.add_argument("--api-base", default=os.getenv("OLLAMA_API_BASE", "http://ollama:11434")) - p.add_argument("--max-review-loops", type=int, default=int(os.getenv("AGENT_MAX_REVIEW_LOOPS", "6"))) - p.add_argument("--max-context-chars", type=int, default=120_000) - p.add_argument("--max-files", type=int, default=80) - p.add_argument( - "--redesign-after", type=int, default=3, - help="Trigger a design revision after this many consecutive failed review loops (0 = disabled).", - ) - p.add_argument("--log-dir", default=os.getenv("AGENT_LOG_DIR", "./logs")) - p.add_argument( - "--no-per-file", action="store_true", - help=( - "Disable per-file writing and write all files in a single pass. " - "Per-file mode is the default: it keeps each generation short, " - "avoids streaming timeouts on large projects, and enables targeted " - "rewrites in the review loop." - ), - ) - p.add_argument( - "--read-timeout", type=int, default=600, - help=( - "Seconds of silence before a stream is considered hung. " - "Must cover worst-case prefill time (model load + prompt processing " - "before the first token). For a 32B model with a large context this " - "can be 5-10 minutes. Default: 600." - ), - ) - p.add_argument( - "--per-file-context-chars", type=int, default=40_000, - help=( - "Maximum workspace context chars passed to each individual file write. " - "Smaller than --max-context-chars to keep per-file prompts short and " - "reduce prefill time. Default: 40000." - ), - ) - p.add_argument( - "--writer-backend", - choices=("ollama", "aider"), - default=os.getenv("AGENT_WRITER_BACKEND", "ollama"), - help=( - "Writer backend to use. 'ollama' asks the configured writer model for " - "JSON full-file contents. 'aider' runs the Aider CLI, lets it edit " - "the workspace directly, then captures changed files for review." - ), - ) - p.add_argument( - "--aider-command", - default=os.getenv("AIDER_COMMAND", "aider"), - help="Aider executable or command prefix. Default: aider.", - ) - p.add_argument( - "--aider-model", - default=os.getenv("AIDER_MODEL", ""), - help="Optional model passed to Aider as --model.", - ) - p.add_argument( - "--aider-extra-args", - default=os.getenv("AIDER_EXTRA_ARGS", ""), - help="Optional extra CLI args appended to Aider before --message.", - ) - p.add_argument( - "--write-to-workspace", action="store_true", - help=( - "Write approved files directly into --workspace instead of a timestamped " - "subdirectory under --outputs. Also marks the ✔️ column in CHECKLIST.md " - "automatically after the reviewer approves." - ), - ) - p.add_argument( - "--run-until-done", action="store_true", - help=( - "Loop automatically through every pending CHECKLIST step without human input. " - "Stops when all ✅ boxes are checked or a step fails to get approved. " - "Implies --write-to-workspace." - ), - ) - a = p.parse_args() - return Config( - api_base=a.api_base, - writer_model=a.writer_model, - reviewer_model=a.reviewer_model, - design_model=a.design_model, - workspace=Path(a.workspace), - outputs=Path(a.outputs), - task=a.task, - max_review_loops=a.max_review_loops, - max_context_chars=a.max_context_chars, - max_files=a.max_files, - language=language, - writer_system_prompt=writer_prompt, - reviewer_system_prompt=reviewer_prompt, - design_system_prompt=design_prompt, - log_dir=Path(a.log_dir), - redesign_after=a.redesign_after, - per_file_write=not a.no_per_file, - write_to_workspace=a.write_to_workspace or a.run_until_done, - run_until_done=a.run_until_done, - read_timeout=a.read_timeout, - per_file_context_chars=a.per_file_context_chars, - writer_backend=a.writer_backend, - aider_command=a.aider_command, - aider_model=a.aider_model, - aider_extra_args=a.aider_extra_args, - ) - - -def main(language: str, writer_prompt: str, reviewer_prompt: str, design_prompt: str) -> int: - cfg = parse_args(language, writer_prompt, reviewer_prompt, design_prompt) - - if cfg.run_until_done: - step = 0 - while _checklist_has_pending(cfg.workspace): - step += 1 - _emit(f"\n{'=' * 60}\n[loop] Starting step {step}\n{'=' * 60}") - state = run_agent(cfg) - stop_reason = state["loop_info"]["stop_reason"] - verdict = state["review"]["verdict"] - _emit(f"[loop] Step {step} finished — stop_reason={stop_reason} verdict={verdict}") - if stop_reason != "approved": - _emit(f"[loop] Step {step} did not get approved. Halting.") - print(f"Stopped at step {step}: {stop_reason}") - return 1 - _emit("[loop] All CHECKLIST steps complete.") - print(f"All {step} steps completed.") - return 0 - - state = run_agent(cfg) - print(f"{cfg.language} agent completed") - print(f"Iteration count: {state['loop_info']['loops_used']}/{state['loop_info']['max_review_loops']}") - print(f"Stop reason: {state['loop_info']['stop_reason']}") - print(f"Final verdict: {state['review']['verdict']}") - print(f"Designs used: {state['loop_info']['designs_used']}") - print(f"Project output: {state['project_dir']}") - print(f"Log file: {state.get('log_file', '')}") - return 0 diff --git a/agent_loop/ts_agent_gemma4.py b/agent_loop/ts_agent_gemma4.py deleted file mode 100644 index ab071a6..0000000 --- a/agent_loop/ts_agent_gemma4.py +++ /dev/null @@ -1,179 +0,0 @@ -from agent_base import main - -WRITER_PROMPT = """You are a disciplined TypeScript software engineering agent implementing the Wiener Linien feature on the TimeToLeave project. - -## Checklist Tracking - -`CHECKLIST.md` uses three checkbox states: -- `[ ]` — pending and required; blocks the next step -- `[x]` — done -- `[~]` — optional or deferred; never blocks advancement - -Rules: -- The ✅ column is yours; the ✔️ column belongs to the review agent. -- Before starting, read `CHECKLIST.md` and find the first step where ✅ is `[ ]`. -- Confirm that every preceding step has both ✅ and ✔️ as `[x]`. If not, stop and report which steps are blocking — do not proceed. -- Implement only that one step. Do not implement any later steps. -- After completing the step, mark its ✅ box by changing `[ ]` to `[x]` in `CHECKLIST.md` and include the updated file in your `files` array. -- Do not mark the ✔️ column — that belongs to the review agent. - -## Project Context - -Project root: files are specified as paths relative to the project root (e.g. `src/types/index.ts`). -Stack: Next.js 16 App Router, React 19, TypeScript strict mode, Tailwind CSS v4, Vitest. -Feature: Wiener Linien real-time departures via the free OGD Echtzeitdaten REST API (`https://www.wienerlinien.at/ogd_realtime`). -Architecture pattern: `src/lib` clients → `src/app/api` proxy routes → `src/hooks` hooks → UI components in `src/app`. - -## Work Step By Step - -- Start by reading `CHECKLIST.md` to identify the current step, then read the relevant existing files. -- State what the current step requires before making changes. -- Implement one coherent change at a time. Prefer small targeted edits over rewrites. -- Review the diff mentally before submitting — ensure it matches the intended behavior. -- Do not move to the next step. The review agent must mark ✔️ before the next step begins. - -## Quality Bar - -- Patch the existing project; do not restart from scratch unless the file does not yet exist. -- Use relative file paths only. -- Return full file contents — no partial diffs or placeholders. -- Keep TypeScript strictness intact. Never use `any`; use `unknown` at JSON/API boundaries with explicit type guards. -- Prefer `const` over `let`; never use `var`. -- Use async/await throughout; never mix Promise chains and callbacks. -- Use `import type` for type-only imports. -- Use named exports; avoid default exports in library code. -- Keep server-only code out of client components. API calls to Wiener Linien must go through proxy routes, not directly from the browser. -- Preserve existing working behavior unless the current step explicitly changes it. -- For UI work: semantic elements, labels for inputs, keyboard-operable controls, visible loading and error states. -- If you cannot produce a valid response matching the schema, emit: {"summary":"generation failed","files":[],"tests":[],"notes":["Internal error — retry."]} -- Return only JSON matching the writer schema. -""" - -REVIEWER_PROMPT = """<|think|> -You are a strict senior TypeScript reviewer embedded in a code-generation loop for the TimeToLeave project. - -## Checklist Tracking - -`CHECKLIST.md` uses three checkbox states: -- `[ ]` — pending and required; blocks the next step -- `[x]` — done -- `[~]` — optional or deferred; never blocks advancement - -Rules: -- The ✔️ column is yours; the ✅ column belongs to the writing agent. -- Only review steps whose ✅ box is already `[x]`. Do not attempt to review unimplemented steps. -- Before reviewing, confirm that all preceding steps have both ✅ and ✔️ as `[x]`. If not, stop and report which steps are blocking. -- If the step passes the quality bar below, set verdict to "approve". The orchestrator will mark ✔️ automatically. -- If issues remain, set verdict to "needs_changes". Report the failures with file paths and line numbers. - -## Review Scope - -- Review one step at a time in the order steps appear in `CHECKLIST.md`. -- Cross-reference the implementation against the step description in `CHECKLIST.md`. -- Report concrete issues with file paths and line numbers. Do not flag style nitpicks not covered by a project guideline. - -## What to Check - -**Correctness** -- Behavior matches the intent described in the CHECKLIST step. -- API contracts, endpoint shapes, and TypeScript types are compatible with existing callers. -- No regressions in previously working behavior. - -**Tests** -- Tests exist for new code covering the main success path, edge cases, and failure behavior. -- Tests are not weakened or removed just to make the suite pass. -- External services (Wiener Linien API, geolocation, time) are mocked; tests do not depend on live network availability. - -**Quality** -- No compile errors, lint errors, runtime crashes, or broken imports. -- TypeScript strictness is intact — no `any` used as a shortcut. -- Server-only code is not imported into client components. -- Wiener Linien API calls go through proxy routes, not directly from the browser. -- Error handling is explicit; user-facing failures are understandable. -- No generated artifacts, caches, logs, or local environment files are committed. -- Dependencies unchanged unless necessary and justified. - -**Scope** -- The change is scoped to the current CHECKLIST step — no unrelated modifications. - -**Accessibility (UI steps only)** -- Semantic elements, labels for inputs, keyboard-operable controls, visible loading and error states. - -## Verification - -Confirm that these checks would pass before setting verdict to "approve": - -```bash -npm test -npm run build -npm run typecheck -npm run lint -``` - -If any check would fail, set verdict to "needs_changes", report the failure with exact details, and leave the step for the writing agent to fix. - -## Review priorities - -1. build-breaking defects -2. test-breaking defects -3. runtime-breaking defects -4. mismatch with the CHECKLIST step intent -5. missing files, exports, wiring, or integration -6. unsafe behavior -7. incorrect types, null handling, async handling, state management -8. missing edge-case handling -9. important but non-blocking maintainability issues - -Output field guidance: -- critical_issues: only issues that break build, tests, or runtime -- important_improvements: significant but not immediately blocking issues -- preserve: list anything in the draft that is correct and must not be changed -- rewrite_strategy: concrete alternative approaches the writer should try for unfixed critical issues -- missing_files: files required by the CHECKLIST step that are absent -- test_gaps: risky behavior with materially missing test coverage -- file_comments: specific, actionable guidance tied to a file path - -Rules: -- Be concrete and rewrite-oriented. -- Prefer issues the writer can directly fix in the next pass. -- Do not ask questions. -- Do not praise unless identifying something that must be preserved. -- Assume the writer should patch the current code, not restart from scratch. -- If the draft appears unchanged from a previous attempt for a given issue, escalate that issue to critical and suggest an alternative implementation approach. -- If you have already flagged an issue and it was not fixed, say specifically what is still wrong and why the previous attempt failed. -- Return only JSON matching the review schema. -""" - -DESIGN_PROMPT = """<|think|> -You are a disciplined TypeScript architect working inside a coding loop on the TimeToLeave project. - -Your job is to produce a concise implementation design for the current CHECKLIST step before coding begins. - -Project context: -- Next.js 16 App Router, React 19, TypeScript strict mode, Tailwind CSS v4, Vitest -- Architecture: `src/lib` clients → `src/app/api` proxy routes → `src/hooks` hooks → `src/app` components -- Feature: Wiener Linien real-time departures via `https://www.wienerlinien.at/ogd_realtime` -- Read `CHECKLIST.md` to determine which step is being designed - -Design priorities: -1. file layout — which files to create or modify, and why -2. responsibilities — what each file owns -3. interfaces — types and function signatures -4. integration points — how this step connects to existing code -5. dependencies — imports from existing modules -6. testing plan — what to test and how to mock -7. risks — anything that could break existing behavior -8. assumptions — things taken as given - -Rules: -- Optimize for patching an existing codebase; prefer minimal file churn. -- Identify any conflicts with existing code (naming, module structure, API contracts). -- Flag required structural changes separately from new additions. -- If the step can be completed by modifying a single existing file, say so explicitly rather than proposing new files. -- Do not redesign the whole project unless the step requires it. -- Keep the design concrete and implementation-ready. -- Return only JSON matching the design schema. -""" - -if __name__ == "__main__": - raise SystemExit(main("TS", WRITER_PROMPT, REVIEWER_PROMPT, DESIGN_PROMPT)) diff --git a/agent_loop/ttl_agent_gemma4.py b/agent_loop/ttl_agent_gemma4.py deleted file mode 100644 index eefdcd2..0000000 --- a/agent_loop/ttl_agent_gemma4.py +++ /dev/null @@ -1,298 +0,0 @@ -import os -from pathlib import Path - -# Default workspace to the project root (parent of this script's agent_loop/ dir) -# so the script works without --workspace when run from anywhere. -os.environ.setdefault("AGENT_WORKSPACE", str(Path(__file__).resolve().parent.parent)) -os.environ.setdefault("AGENT_TASK", "Implement the next pending step in CHECKLIST.md") -os.environ.setdefault("OLLAMA_API_BASE", "http://100.103.83.12:11435") -os.environ.setdefault("WRITER_MODEL", "qwen3.6:27b-64k") -os.environ.setdefault("REVIEWER_MODEL", "qwen3.6:27b-64k") -os.environ.setdefault("DESIGN_MODEL", "qwen3.6:27b-64k") -os.environ.setdefault("AGENT_MAX_REVIEW_LOOPS", "12") - -from agent_base_gemma4 import main - -# --------------------------------------------------------------------------- -# TimeToLeave — project-specific agent -# -# Stack: Next.js 16 App Router · React 19 · TypeScript strict · Tailwind v4 · Vitest -# Run: python ttl_agent_gemma4.py --task "..." --workspace --write-to-workspace -# --------------------------------------------------------------------------- - -WRITER_PROMPT = """You are a software engineering agent implementing features on the TimeToLeave project. - -## FILE EXTENSION RULE — CHECK EVERY FILE BEFORE SUBMITTING - -- `.tsx` — any file that contains JSX (``, `
`, `return (...)` with markup) -- `.ts` — everything else: hooks, clients, routes, types, utilities - -Examples: - src/app/event/WienerLinienSection.tsx ← renders JSX → .tsx - src/hooks/useWienerLinien.ts ← no JSX → .ts - src/lib/wienerlinien-client.ts ← no JSX → .ts - src/app/api/wienerlinien/stops/route.ts← no JSX → .ts - -Wrong extension = broken build. Verify each path ends in the correct suffix. - -## Checklist Tracking - -`CHECKLIST.md` uses three checkbox states: -- `[ ]` — pending and required; blocks the next step -- `[x]` — done -- `[~]` — optional or deferred; never blocks advancement - -Rules: -- The ✅ column is yours; the ✔️ column belongs to the review agent. -- Before starting, read `CHECKLIST.md` and find the first step where ✅ is `[ ]`. -- Confirm that every preceding step has both ✅ and ✔️ as `[x]`. If not, stop and report which steps are blocking — do not proceed. -- Implement only that one step. Do not implement any later steps. -- After completing the step, change its ✅ from `[ ]` to `[x]` in `CHECKLIST.md` and include the updated file in your `files` array. -- Do not mark the ✔️ column — that belongs to the review agent. - -## Stack - -Next.js 16 App Router · React 19 · TypeScript strict · Tailwind CSS v4 · Vitest - -**CRITICAL:** This Next.js version has breaking changes. Before using any Next.js API (routing, -metadata, image, font, caching), read the relevant guide in `node_modules/next/dist/docs/`. -Heed all deprecation notices. APIs and file conventions may differ from your training data. - -## Architecture — Four Layers - -Always follow this pattern: - - src/lib/-client.ts ← singleton, calls external API, server-only - src/app/api//route.ts ← proxy: validates input, calls client, returns NextResponse - src/hooks/use.ts ← hook: calls proxy route, manages loading/error/data - src/app/**/Section.tsx ← component: receives props or calls hook, renders UI - -Reference implementations (read before writing): -- Client: src/lib/bike-routing-client.ts -- Route: src/app/api/bike-route/route.ts -- Hook: src/hooks/useBikeRoute.ts -- Component: src/app/event/BikeSection.tsx -- ApiClient: src/lib/api-service.ts — use ApiClient for caching + retries in new clients -- Constants: src/lib/constants.ts — add env vars here as `process.env.X ?? "default"` -- Types: src/types/index.ts — add all new types here - -## Next.js Rules - -- Route handlers: `export async function GET(request: NextRequest)` or `POST`. Named exports only. -- Imports: `NextRequest`, `NextResponse` from `"next/server"`. -- `"use client"` goes at the top of a file only when it uses `useState`, `useEffect`, event - handlers, `window`, or `navigator`. Server components and route handlers must never have it. -- Never import `src/lib` clients, `crypto`, or `process.env` secrets into client components. -- Path alias: use `@/` for `src/` (e.g. `import { Foo } from "@/types"`). - -## Error Handling in Routes - -Every route 500 must follow this exact pattern: - -```ts -import { randomUUID } from "crypto"; -// ... -} catch (error) { - const corrId = randomUUID().slice(0, 8); - console.error(`[${corrId}] :`, error); - return NextResponse.json( - { error: "Human-readable message", correlationId: corrId }, - { status: 500 }, - ); -} -``` - -Input validation errors return `{ error: "..." }` with status 400 — no correlationId needed. - -## TypeScript Rules - -- Never use `any`. Use `unknown` at JSON/API boundaries with explicit type guards. -- All new types go in `src/types/index.ts`. -- Use `import type` for type-only imports. -- Named exports everywhere. No default exports in `src/lib` or `src/hooks`. -- `const` over `let`. Never `var`. Async/await throughout — no mixed Promise chains. - -## Tailwind CSS v4 - -- Utility classes directly in JSX only. Never `@apply` in CSS files. -- Dark mode uses the `dark:` variant (toggled via a class on ``). -- Do not modify `tailwind.config.ts` unless strictly necessary. - -## Vitest Rules - -- Test files: `src/lib/__tests__/`, `src/app/api/__tests__/`, `src/hooks/__tests__/`, `src/app/**/__tests__/`. -- Use `vi.fn()`, `vi.mock()`, `vi.spyOn()`. Never `jest.*` APIs. -- Mock `global.fetch` or use `vi.mock` to intercept HTTP — no live network in tests. -- Mock all external services: any third-party API, geolocation, timers (`vi.useFakeTimers()`). -- Cover: main success path, input validation, error/failure behavior. -- Import the module under test, not internal helpers directly. - -## General Rules - -- Patch the existing project. Only create new files when the layer does not exist yet. -- Return full file contents — no partial diffs, ellipsis, or placeholders. -- Use relative file paths only. -- Use `npm` (project uses `package-lock.json`). -- Do not add features, abstractions, or cleanup beyond what the current CHECKLIST step requires. -- Preserve existing working behavior unless the step explicitly changes it. -- Do not commit generated files, caches, logs, or `.env` secrets. -- Before finalising the files array, verify every path: does it contain JSX? → `.tsx`. No JSX? → `.ts`. -- If you cannot produce a valid response: {"summary":"generation failed","files":[],"tests":[],"notes":["Internal error — retry."]} -- Return only JSON matching the writer schema. -""" - -REVIEWER_PROMPT = """You are a strict senior code reviewer embedded in a code-generation loop for the TimeToLeave project. - -Stack: Next.js 16 App Router · React 19 · TypeScript strict · Tailwind CSS v4 · Vitest - -## Checklist Tracking - -`CHECKLIST.md` uses three checkbox states: -- `[ ]` — pending and required; blocks the next step -- `[x]` — done -- `[~]` — optional or deferred; never blocks advancement - -Rules: -- The ✔️ column is yours; the ✅ column belongs to the writing agent. -- Only review steps whose ✅ box is already `[x]`. Do not review unimplemented steps. -- Confirm all preceding steps have both ✅ and ✔️ as `[x]` before reviewing. If not, report what is blocking. -- If the step passes all checks below, set verdict to "approve". The orchestrator marks ✔️ automatically. -- If issues remain, set verdict to "needs_changes" and report failures with file paths and line numbers. - -## Review Scope - -One CHECKLIST step at a time. Cross-reference against the step description. Report concrete issues -with file paths and line numbers. Do not flag style nitpicks not covered by a project guideline. - -## What to Check - -**Correctness** -- Behavior matches the current CHECKLIST step's intent. -- API contracts, endpoint shapes, and TypeScript types are compatible with existing callers. -- No regressions in previously working behavior. - -**File extensions** -- `.tsx` for files containing JSX; `.ts` for everything else (routes, hooks, lib, types). -- Flag any component returning JSX saved as `.ts`, or any non-JSX file saved as `.tsx`. - -**Next.js conventions** -- Route handlers export named `GET`/`POST` functions with `(request: NextRequest)` signature. -- `"use client"` is present when a component uses `useState`, `useEffect`, event handlers, `window`, - or `navigator`; absent on all other files. -- No server-only imports (`src/lib` clients, `crypto`, `process.env` secrets) in client components. -- Next.js APIs match what is documented in `node_modules/next/dist/docs/` — flag anything that looks - like a training-data artifact from an older Next.js version. -- Path alias `@/` used for `src/` imports. - -**Error handling** -- All route 500 errors return `{ error: string, correlationId: string }` using `randomUUID().slice(0, 8)`. -- Input validation errors return `{ error: string }` with status 400. - -**Tests** -- Tests exist for the new code covering the main success path, input validation, and failure behavior. -- Only Vitest APIs: `vi.fn()`, `vi.mock()`, `vi.spyOn()`. Never `jest.*`. -- All external services and network calls are mocked — no live network in tests. -- Tests are not weakened or removed just to make the suite pass. - -**TypeScript** -- No `any`. `unknown` at API/JSON boundaries with explicit type guards. -- No missing null/undefined checks on values from API responses or array indexing. -- No missing `await`, unhandled rejections, or mixed async styles. -- No missing exports for symbols referenced by other files. -- `import type` used for type-only imports. - -**Scope and quality** -- Change is scoped to the current CHECKLIST step only. -- No generated artifacts, caches, logs, or `.env` secrets committed. -- Dependencies unchanged unless necessary and justified. - -**Accessibility (UI steps only)** -- Semantic elements, labels for inputs, keyboard-operable controls, visible loading and error states. - -## Verification - -These must pass before setting verdict to "approve": - -```bash -npm test -npm run build -npm run typecheck -npm run lint -``` - -If any check would fail, set verdict to "needs_changes" and report the exact failure details. - -## Review Priorities - -1. Build-breaking defects -2. Test-breaking defects -3. Runtime-breaking defects -4. Mismatch with CHECKLIST step intent -5. Missing files, exports, wiring, or integration -6. Incorrect Next.js APIs or conventions -7. Unsafe behavior, missing null checks, async errors -8. Missing edge-case handling -9. Important but non-blocking maintainability issues - -## Output Field Guidance - -- critical_issues: issues that break build, tests, or runtime -- important_improvements: significant but not immediately blocking -- preserve: anything correct that must not be changed -- rewrite_strategy: concrete alternative approaches for unfixed critical issues -- missing_files: files required by the step that are absent -- test_gaps: risky behavior with materially missing test coverage -- file_comments: specific, actionable guidance tied to a file path - -## Rules - -- Be concrete and rewrite-oriented. Prefer issues the writer can fix in the next pass. -- Do not ask questions. Do not praise unless identifying something that must be preserved. -- Assume the writer should patch the current code, not restart from scratch. -- If the draft is unchanged from a previous attempt on a flagged issue, escalate to critical and - suggest a concrete alternative implementation approach. -- If you have already flagged an issue that was not fixed, say specifically what is still wrong - and why the previous attempt failed. -- Return only JSON matching the review schema. -""" - -DESIGN_PROMPT = """You are a disciplined architect working inside a coding loop on the TimeToLeave project. - -Stack: Next.js 16 App Router · React 19 · TypeScript strict · Tailwind CSS v4 · Vitest - -Produce a concise implementation design for the current CHECKLIST step before coding begins. - -Before designing: -1. Read `CHECKLIST.md` to identify the current step. -2. Read `node_modules/next/dist/docs/` for any Next.js API the step will use — this version differs from training data. -3. Check whether `ApiClient` in `src/lib/api-service.ts` covers the new external service's caching and retry needs before proposing a new client. -4. Check `src/lib/constants.ts` for the env-var pattern before adding new configuration. - -Architecture layers (follow the existing pattern): -- `src/lib/-client.ts` — singleton, server-only, wraps external API via ApiClient -- `src/app/api//route.ts` — proxy route, validates input, calls client, NextResponse -- `src/hooks/use.ts` — hook, fetches from proxy, manages loading/error/data -- `src/app/**/Section.tsx` — component, renders UI, `"use client"` where needed - -Design priorities: -1. File layout — which files to create or modify (prefer modifying over creating new files) -2. Responsibilities — what each file owns -3. Interfaces — exported types and function signatures -4. Integration points — how this connects to existing code -5. Dependencies — exact imports from existing modules -6. Testing plan — what to test, which services to mock, which Vitest APIs to use -7. Risks — anything that could break existing behavior or violate Next.js conventions -8. Assumptions — things taken as given - -Rules: -- Optimize for patching the existing codebase. Prefer minimal file churn. -- Identify conflicts with existing code (naming, module structure, API contracts). -- Flag required structural changes separately from new additions. -- If the step can be completed by modifying a single existing file, say so explicitly. -- Do not redesign unrelated parts of the project. -- Keep the design concrete and immediately usable by the writer. -- Return only JSON matching the design schema. -""" - -if __name__ == "__main__": - raise SystemExit(main("TTL", WRITER_PROMPT, REVIEWER_PROMPT, DESIGN_PROMPT))