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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Node.js
frontend/node_modules/
**/node_modules/
npm-debug.log*
yarn-debug.log*

# Python
**/__pycache__/
*.py[cod]
backend/.venv/
backend/.mypy_cache/
backend/.ruff_cache/
backend/.pytest_cache/

# Git
.git/

# IDE
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db

# Temporary / build artifacts
tmp/
*.log
.cache/
dist/
build/

# K8s configs (not needed in image)
k8s/

# Environment files
.env
.env.*
77 changes: 75 additions & 2 deletions docker/worker-implement.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,47 @@ async def _process_messages(client: ClaudeSDKClient, phase: str) -> None:
_emit_tool_detail(phase, block.name, json.dumps(block.input))


async def kill_dev_servers(repo_dir: str) -> None:
"""Kill any lingering dev server processes (node, vite, next, etc.)."""
for pattern in ["node", "vite", "next"]:
subprocess.run(
["pkill", "-f", pattern],
capture_output=True,
)
Comment on lines +189 to +195

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kill_dev_servers() relies on calling pkill, but the current worker image build (docker/worker.Dockerfile) does not install procps, so pkill is likely missing and this will raise FileNotFoundError, breaking the retry flow. Either install the needed OS package in the worker image or handle the absence of pkill (e.g., fallback to another mechanism) so retries don’t fail immediately.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +195

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kill_dev_servers(repo_dir) doesn’t use repo_dir and runs pkill -f on very broad patterns (node, vite, next). That can terminate unrelated processes in the container (and makes the parameter misleading). Prefer scoping the kill to processes launched for this repo (e.g., match repo_dir in the command line, or better: capture and store the dev server PID when starting it and kill that PID/process group).

Copilot uses AI. Check for mistakes.
# Give processes time to terminate
await asyncio.sleep(2)


async def fix_with_claude(repo_dir: str, error_message: str) -> None:
"""Use Claude to diagnose and fix the dev server launch failure."""
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
cwd=repo_dir,
max_turns=20,
max_budget_usd=1.5,
include_partial_messages=True,
)

async with ClaudeSDKClient(options=options) as client:
await client.query(f"""The dev server failed to start in the project at {repo_dir}.

Here is the error output:

```
{error_message}
```

Please diagnose and fix the issue. Common causes include:
- Missing or incorrect dependencies (run `npm install` or similar)
- Build errors in the source code (syntax errors, import errors, type errors)
- Port conflicts (change the port configuration)
- Missing environment variables or config files

Fix the code so the dev server can start successfully. Make minimal, targeted changes.
""")
await _process_messages(client, "implementing")


async def launch_and_screenshot(repo_dir: str, screenshot_output: str) -> None:
"""Launch dev server and take screenshot in a single session.

Expand Down Expand Up @@ -228,6 +269,13 @@ async def launch_and_screenshot(repo_dir: str, screenshot_output: str) -> None:
""")
await _process_messages(client, "screenshot")

# Verify screenshot was actually created
if not Path(screenshot_output).exists():
raise RuntimeError(
f"Screenshot was not created at {screenshot_output}. "
"The dev server likely failed to start or the screenshot command failed."
)


async def main() -> None:
# Read environment variables
Expand Down Expand Up @@ -320,8 +368,33 @@ async def main() -> None:
emit_log("implementing", "Implementing design proposal")
await implement_changes(repo_dir, proposal_plan)

# Launch + screenshot in a single session (shares dev server URL context)
await launch_and_screenshot(repo_dir, screenshot_path)
# Launch + screenshot with retry loop
max_retries = 2 # up to 3 total attempts
last_error = None
for attempt in range(max_retries + 1):
try:
await launch_and_screenshot(repo_dir, screenshot_path)
last_error = None
break # success
except Exception as e:
last_error = str(e)
if attempt < max_retries:
emit_log(
"implementing",
f"Dev server launch failed (attempt {attempt + 1}/{max_retries + 1}), attempting fix...",
detail=last_error,
)
await kill_dev_servers(repo_dir)
await fix_with_claude(repo_dir, last_error)
Comment on lines +379 to +388

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On failure you pass str(e) into fix_with_claude(), but the most common exception here is the synthetic “Screenshot was not created...” error, which doesn’t include the actual dev server/stdout/stderr that Claude needs to fix the root cause. Consider capturing and surfacing the relevant launch/screenshot command outputs (or writing them to a file and including that content) so the retry/fix prompt contains actionable error details.

Copilot uses AI. Check for mistakes.
else:
emit_log(
"implementing",
f"Dev server launch failed after {max_retries + 1} attempts, proceeding without screenshot",
detail=last_error,
)

if last_error:
emit_log("implementing", "WARNING: Could not launch dev server, screenshot may be missing")

# Step 4: Generate cumulative patch (squash everything since base_branch)
emit_log("uploading", "Generating cumulative patch")
Expand Down