Skip to content
Open
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
43 changes: 25 additions & 18 deletions actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
from pathlib import Path
from urllib.parse import quote

from secondary_agent import (
SECONDARY_AGENT,
build_secondary_agent_batch_shell_command,
build_secondary_agent_interactive_command,
)

log = logging.getLogger("jarvis.actions")

DESKTOP_PATH = Path.home() / "Desktop"
Expand Down Expand Up @@ -154,22 +160,19 @@ async def open_chrome(url: str) -> dict:
return await open_browser(url, "chrome")


async def open_claude_in_project(project_dir: str, prompt: str) -> dict:
"""Open Terminal, cd to project dir, run Claude Code interactively.

Writes the prompt to CLAUDE.md (which claude reads automatically on startup)
then launches claude in interactive mode with --dangerously-skip-permissions.
No prompt escaping needed — CLAUDE.md handles context delivery.
"""
# Write prompt to CLAUDE.md — claude reads this automatically
async def open_agent_in_project(project_dir: str, prompt: str) -> dict:
"""Open Terminal, cd to project dir, and launch the active coding agent."""
claude_md = Path(project_dir) / "CLAUDE.md"
claude_md.write_text(f"# Task\n\n{prompt}\n\nBuild this completely. If web app, make index.html work standalone.\n")

# Launch claude interactive — it reads CLAUDE.md on its own
launch_prompt = (
f"{prompt}\n\n"
"Build this completely. If web app, make index.html work standalone."
)
script = (
'tell application "Terminal"\n'
" activate\n"
f' do script "cd {project_dir} && claude --dangerously-skip-permissions"\n'
f' do script "{build_secondary_agent_interactive_command(project_dir, prompt=launch_prompt).replace(chr(34), r"\\\"")}"\n'
"end tell"
)
proc = await asyncio.create_subprocess_exec(
Expand All @@ -180,21 +183,25 @@ async def open_claude_in_project(project_dir: str, prompt: str) -> dict:
_, stderr = await proc.communicate()
success = proc.returncode == 0
if not success:
log.error(f"open_claude_in_project failed: {stderr.decode()}")
log.error(f"open_agent_in_project failed: {stderr.decode()}")
else:
await _mark_terminal_as_jarvis()
return {
"success": success,
"confirmation": "Claude Code is running in Terminal, sir. You can watch the progress."
"confirmation": f"{SECONDARY_AGENT.display_name} is running in Terminal, sir. You can watch the progress."
if success
else "Had trouble spawning Claude Code, sir.",
else f"Had trouble spawning {SECONDARY_AGENT.display_name}, sir.",
}


# Backward compatibility for older imports.
open_claude_in_project = open_agent_in_project


async def prompt_existing_terminal(project_name: str, prompt: str) -> dict:
"""Find a Terminal window matching a project name and type a prompt into it.

Uses System Events keystroke to type into an active Claude Code session
Uses System Events keystroke to type into an active coding-agent session
rather than `do script` which would open a new shell.
"""
escaped_name = project_name.replace('"', '\\"')
Expand Down Expand Up @@ -303,7 +310,7 @@ async def get_chrome_tab_info() -> dict:


async def monitor_build(project_dir: str, ws=None, synthesize_fn=None) -> None:
"""Monitor a Claude Code build for completion. Notify via WebSocket when done."""
"""Monitor a coding-agent build for completion. Notify via WebSocket when done."""
import base64

output_file = Path(project_dir) / ".jarvis_output.txt"
Expand Down Expand Up @@ -345,7 +352,7 @@ async def execute_action(intent: dict, projects: list = None) -> dict:
target = intent.get("target", "")

if action == "open_terminal":
result = await open_terminal("claude --dangerously-skip-permissions")
result = await open_terminal(build_secondary_agent_interactive_command())
result["project_dir"] = None
return result

Expand All @@ -367,11 +374,11 @@ async def execute_action(intent: dict, projects: list = None) -> dict:
return result

elif action == "build":
# Create project folder on Desktop, spawn Claude Code
# Create project folder on Desktop, spawn the active coding agent
project_name = _generate_project_name(target)
project_dir = str(DESKTOP_PATH / project_name)
os.makedirs(project_dir, exist_ok=True)
result = await open_claude_in_project(project_dir, target)
result = await open_agent_in_project(project_dir, target)
result["project_dir"] = project_dir
return result

Expand Down
59 changes: 19 additions & 40 deletions qa.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""
JARVIS QA Agent — Verifies Claude Code task output.
JARVIS QA Agent — Verifies coding-agent task output.

Spawns a claude -p subprocess to check completed work, auto-retries on failure.
Spawns the active secondary agent to check completed work, auto-retries on failure.
"""

import asyncio
import json
import logging
from dataclasses import dataclass, asdict
from datetime import datetime
from typing import Optional

from secondary_agent import SECONDARY_AGENT, run_secondary_agent_prompt

log = logging.getLogger("jarvis.qa")

Expand All @@ -28,10 +28,10 @@ def to_dict(self) -> dict:


class QAAgent:
"""Verifies Claude Code task output."""
"""Verifies coding-agent task output."""

async def verify(self, task_prompt: str, task_result: str, working_dir: str = ".") -> QAResult:
"""Run QA on a completed task by spawning claude -p with a verification prompt."""
"""Run QA on a completed task via the active secondary agent."""
qa_prompt = (
"You are a QA agent. Verify the following completed task.\n\n"
f"ORIGINAL TASK:\n{task_prompt}\n\n"
Expand All @@ -45,22 +45,12 @@ async def verify(self, task_prompt: str, task_result: str, working_dir: str = ".
)

try:
process = await asyncio.create_subprocess_exec(
"claude", "-p",
"--output-format", "text",
"--dangerously-skip-permissions",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=working_dir,
)

stdout, stderr = await asyncio.wait_for(
process.communicate(input=qa_prompt.encode()),
result = await run_secondary_agent_prompt(
prompt=qa_prompt,
working_dir=working_dir,
timeout=120.0,
)

raw = stdout.decode().strip()
raw = result.message.strip()

# Try to parse JSON from the response
try:
Expand Down Expand Up @@ -94,10 +84,10 @@ async def verify(self, task_prompt: str, task_result: str, working_dir: str = ".
summary="QA timed out",
)
except FileNotFoundError:
log.error("claude CLI not found for QA")
log.error(f"{SECONDARY_AGENT.display_name} not found for QA")
return QAResult(
passed=True,
issues=["claude CLI not available for QA"],
issues=[f"{SECONDARY_AGENT.display_name} not available for QA"],
summary="QA skipped — CLI not found",
)
except Exception as e:
Expand Down Expand Up @@ -133,34 +123,23 @@ async def auto_retry(
)

try:
process = await asyncio.create_subprocess_exec(
"claude", "-p",
"--output-format", "text",
"--dangerously-skip-permissions",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=working_dir,
)

stdout, stderr = await asyncio.wait_for(
process.communicate(input=retry_prompt.encode()),
result = await run_secondary_agent_prompt(
prompt=retry_prompt,
working_dir=working_dir,
timeout=300.0,
)

if process.returncode == 0:
result = stdout.decode().strip()
if result.returncode == 0:
return {
"status": "completed",
"result": result,
"result": result.message.strip(),
"error": "",
"attempt": attempt + 1,
}
else:
return {
"status": "failed",
"result": stdout.decode().strip(),
"error": stderr.decode().strip(),
"result": result.message.strip(),
"error": result.stderr.strip() or result.stdout.strip(),
"attempt": attempt + 1,
}

Expand Down
Loading