Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:28:42 +08:00
commit 8a4be47b6e
43 changed files with 10867 additions and 0 deletions

View File

@@ -0,0 +1,501 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "pydantic",
# "python-dotenv",
# "click",
# "rich",
# ]
# ///
"""
Run chore planning and implementation workflow.
This script runs two slash commands in sequence:
1. /chore - Creates a plan based on the prompt
2. /implement - Implements the plan created by /chore
Usage:
# Method 1: Direct execution (requires uv)
./adws/adw_chore_implement.py "Add error handling to all API endpoints"
# Method 2: Using uv run
uv run adws/adw_chore_implement.py "Refactor database connection logic"
Examples:
# Run with specific model
./adws/adw_chore_implement.py "Add logging to agent.py" --model opus
# Run from a different working directory
./adws/adw_chore_implement.py "Update documentation" --working-dir /path/to/project
# Run with verbose output
./adws/adw_chore_implement.py "Add tests" --verbose
"""
import os
import sys
import json
import re
from pathlib import Path
import click
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.rule import Rule
# Add the adw_modules directory to the path so we can import agent
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "adw_modules"))
from agent import (
AgentTemplateRequest,
AgentPromptResponse,
execute_template,
generate_short_id,
)
# Output file name constants
OUTPUT_JSONL = "cc_raw_output.jsonl"
OUTPUT_JSON = "cc_raw_output.json"
FINAL_OBJECT_JSON = "cc_final_object.json"
SUMMARY_JSON = "custom_summary_output.json"
def extract_plan_path(output: str) -> str:
"""Extract the plan file path from the chore command output.
Looks for patterns like:
- specs/chore-12345678-update-readme.md
- Created plan at: specs/chore-...
- Plan file: specs/chore-...
"""
# Try multiple patterns to find the plan path
patterns = [
r"specs/chore-[a-zA-Z0-9\-]+\.md",
r"Created plan at:\s*(specs/chore-[a-zA-Z0-9\-]+\.md)",
r"Plan file:\s*(specs/chore-[a-zA-Z0-9\-]+\.md)",
r"path.*?:\s*(specs/chore-[a-zA-Z0-9\-]+\.md)",
]
for pattern in patterns:
match = re.search(pattern, output, re.IGNORECASE | re.MULTILINE)
if match:
return match.group(1) if match.groups() else match.group(0)
# If no match found, raise an error
raise ValueError("Could not find plan file path in chore output")
@click.command()
@click.argument("prompt", required=True)
@click.option(
"--model",
type=click.Choice(["sonnet", "opus", "haiku"]),
default="sonnet",
help="Claude model to use (sonnet=balanced, opus=max intelligence, haiku=fast & economical)",
)
@click.option(
"--working-dir",
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
help="Working directory for command execution (default: current directory)",
)
def main(
prompt: str,
model: str,
working_dir: str,
):
"""Run chore planning and implementation workflow."""
console = Console()
# Generate a unique ID for this workflow
adw_id = generate_short_id()
# Use current directory if no working directory specified
if not working_dir:
working_dir = os.getcwd()
# Set default agent names
planner_name = "planner"
builder_name = "builder"
console.print(
Panel(
f"[bold blue]ADW Chore & Implement Workflow[/bold blue]\n\n"
f"[cyan]ADW ID:[/cyan] {adw_id}\n"
f"[cyan]Model:[/cyan] {model}\n"
f"[cyan]Working Dir:[/cyan] {working_dir}",
title="[bold blue]🚀 Workflow Configuration[/bold blue]",
border_style="blue",
)
)
console.print()
# Phase 1: Run /chore command
console.print(Rule("[bold yellow]Phase 1: Planning (/chore)[/bold yellow]"))
console.print()
# Create the chore request
chore_request = AgentTemplateRequest(
agent_name=planner_name,
slash_command="/chore",
args=[adw_id, prompt],
adw_id=adw_id,
model=model,
working_dir=working_dir,
)
# Display chore execution info
chore_info_table = Table(show_header=False, box=None, padding=(0, 1))
chore_info_table.add_column(style="bold cyan")
chore_info_table.add_column()
chore_info_table.add_row("ADW ID", adw_id)
chore_info_table.add_row("ADW Name", "adw_chore_implement (planning)")
chore_info_table.add_row("Command", "/chore")
chore_info_table.add_row("Args", f'{adw_id} "{prompt}"')
chore_info_table.add_row("Model", model)
chore_info_table.add_row("Agent", planner_name)
console.print(
Panel(
chore_info_table,
title="[bold blue]🚀 Chore Inputs[/bold blue]",
border_style="blue",
)
)
console.print()
plan_path = None
try:
# Execute the chore command
with console.status("[bold yellow]Creating plan...[/bold yellow]"):
chore_response = execute_template(chore_request)
# Display the chore result
if chore_response.success:
# Success panel
console.print(
Panel(
chore_response.output,
title="[bold green]✅ Planning Success[/bold green]",
border_style="green",
padding=(1, 2),
)
)
# Extract the plan path from the output
try:
plan_path = extract_plan_path(chore_response.output)
console.print(f"\n[bold cyan]Plan created at:[/bold cyan] {plan_path}")
except ValueError as e:
console.print(
Panel(
f"[bold red]Could not extract plan path: {str(e)}[/bold red]\n\n"
"The chore command succeeded but the plan file path could not be found in the output.",
title="[bold red]❌ Parse Error[/bold red]",
border_style="red",
)
)
sys.exit(3)
else:
# Error panel
console.print(
Panel(
chore_response.output,
title="[bold red]❌ Planning Failed[/bold red]",
border_style="red",
padding=(1, 2),
)
)
console.print(
"\n[bold red]Workflow aborted: Planning phase failed[/bold red]"
)
sys.exit(1)
# Save chore phase summary
chore_output_dir = f"./agents/{adw_id}/{planner_name}"
chore_summary_path = f"{chore_output_dir}/{SUMMARY_JSON}"
with open(chore_summary_path, "w") as f:
json.dump(
{
"phase": "planning",
"adw_id": adw_id,
"slash_command": "/chore",
"args": [adw_id, prompt],
"path_to_slash_command_prompt": ".claude/commands/chore.md",
"model": model,
"working_dir": working_dir,
"success": chore_response.success,
"session_id": chore_response.session_id,
"retry_code": chore_response.retry_code,
"output": chore_response.output,
"plan_path": plan_path,
},
f,
indent=2,
)
# Show chore output files
console.print()
# Files saved panel for chore phase
chore_files_table = Table(show_header=True, box=None)
chore_files_table.add_column("File Type", style="bold cyan")
chore_files_table.add_column("Path", style="dim")
chore_files_table.add_column("Description", style="italic")
chore_files_table.add_row(
"JSONL Stream",
f"{chore_output_dir}/{OUTPUT_JSONL}",
"Raw streaming output from Claude Code",
)
chore_files_table.add_row(
"JSON Array",
f"{chore_output_dir}/{OUTPUT_JSON}",
"All messages as a JSON array",
)
chore_files_table.add_row(
"Final Object",
f"{chore_output_dir}/{FINAL_OBJECT_JSON}",
"Last message entry (final result)",
)
chore_files_table.add_row(
"Summary",
chore_summary_path,
"High-level execution summary with metadata",
)
console.print(
Panel(
chore_files_table,
title="[bold blue]📄 Planning Output Files[/bold blue]",
border_style="blue",
)
)
console.print()
# Phase 2: Run /implement command
console.print(
Rule("[bold yellow]Phase 2: Implementation (/implement)[/bold yellow]")
)
console.print()
# Create the implement request
implement_request = AgentTemplateRequest(
agent_name=builder_name,
slash_command="/implement",
args=[plan_path],
adw_id=adw_id,
model=model,
working_dir=working_dir,
)
# Display implement execution info
implement_info_table = Table(show_header=False, box=None, padding=(0, 1))
implement_info_table.add_column(style="bold cyan")
implement_info_table.add_column()
implement_info_table.add_row("ADW ID", adw_id)
implement_info_table.add_row("ADW Name", "adw_chore_implement (building)")
implement_info_table.add_row("Command", "/implement")
implement_info_table.add_row("Args", plan_path)
implement_info_table.add_row("Model", model)
implement_info_table.add_row("Agent", builder_name)
console.print(
Panel(
implement_info_table,
title="[bold blue]🚀 Implement Inputs[/bold blue]",
border_style="blue",
)
)
console.print()
# Execute the implement command
with console.status("[bold yellow]Implementing plan...[/bold yellow]"):
implement_response = execute_template(implement_request)
# Display the implement result
if implement_response.success:
# Success panel
console.print(
Panel(
implement_response.output,
title="[bold green]✅ Implementation Success[/bold green]",
border_style="green",
padding=(1, 2),
)
)
if implement_response.session_id:
console.print(
f"\n[bold cyan]Session ID:[/bold cyan] {implement_response.session_id}"
)
else:
# Error panel
console.print(
Panel(
implement_response.output,
title="[bold red]❌ Implementation Failed[/bold red]",
border_style="red",
padding=(1, 2),
)
)
# Save implement phase summary
implement_output_dir = f"./agents/{adw_id}/{builder_name}"
implement_summary_path = f"{implement_output_dir}/{SUMMARY_JSON}"
with open(implement_summary_path, "w") as f:
json.dump(
{
"phase": "implementation",
"adw_id": adw_id,
"slash_command": "/implement",
"args": [plan_path],
"path_to_slash_command_prompt": ".claude/commands/implement.md",
"model": model,
"working_dir": working_dir,
"success": implement_response.success,
"session_id": implement_response.session_id,
"retry_code": implement_response.retry_code,
"output": implement_response.output,
},
f,
indent=2,
)
# Show implement output files
console.print()
# Files saved panel for implement phase
implement_files_table = Table(show_header=True, box=None)
implement_files_table.add_column("File Type", style="bold cyan")
implement_files_table.add_column("Path", style="dim")
implement_files_table.add_column("Description", style="italic")
implement_files_table.add_row(
"JSONL Stream",
f"{implement_output_dir}/{OUTPUT_JSONL}",
"Raw streaming output from Claude Code",
)
implement_files_table.add_row(
"JSON Array",
f"{implement_output_dir}/{OUTPUT_JSON}",
"All messages as a JSON array",
)
implement_files_table.add_row(
"Final Object",
f"{implement_output_dir}/{FINAL_OBJECT_JSON}",
"Last message entry (final result)",
)
implement_files_table.add_row(
"Summary",
implement_summary_path,
"High-level execution summary with metadata",
)
console.print(
Panel(
implement_files_table,
title="[bold blue]📄 Implementation Output Files[/bold blue]",
border_style="blue",
)
)
# Show workflow summary
console.print()
console.print(Rule("[bold blue]Workflow Summary[/bold blue]"))
console.print()
summary_table = Table(show_header=True, box=None)
summary_table.add_column("Phase", style="bold cyan")
summary_table.add_column("Status", style="bold")
summary_table.add_column("Output Directory", style="dim")
# Planning phase row
planning_status = "✅ Success" if chore_response.success else "❌ Failed"
summary_table.add_row(
"Planning (/chore)",
planning_status,
f"./agents/{adw_id}/{planner_name}/",
)
# Implementation phase row
implement_status = "✅ Success" if implement_response.success else "❌ Failed"
summary_table.add_row(
"Implementation (/implement)",
implement_status,
f"./agents/{adw_id}/{builder_name}/",
)
console.print(summary_table)
# Create overall workflow summary
workflow_summary_path = f"./agents/{adw_id}/workflow_summary.json"
os.makedirs(f"./agents/{adw_id}", exist_ok=True)
with open(workflow_summary_path, "w") as f:
json.dump(
{
"workflow": "chore_implement",
"adw_id": adw_id,
"prompt": prompt,
"model": model,
"working_dir": working_dir,
"plan_path": plan_path,
"phases": {
"planning": {
"success": chore_response.success,
"session_id": chore_response.session_id,
"agent": planner_name,
"output_dir": f"./agents/{adw_id}/{planner_name}/",
},
"implementation": {
"success": implement_response.success,
"session_id": implement_response.session_id,
"agent": builder_name,
"output_dir": f"./agents/{adw_id}/{builder_name}/",
},
},
"overall_success": chore_response.success
and implement_response.success,
},
f,
indent=2,
)
console.print(
f"\n[bold cyan]Workflow summary:[/bold cyan] {workflow_summary_path}"
)
console.print()
# Exit with appropriate code
if chore_response.success and implement_response.success:
console.print(
"[bold green]✅ Workflow completed successfully![/bold green]"
)
sys.exit(0)
else:
console.print(
"[bold yellow]⚠️ Workflow completed with errors[/bold yellow]"
)
sys.exit(1)
except Exception as e:
console.print(
Panel(
f"[bold red]{str(e)}[/bold red]",
title="[bold red]❌ Unexpected Error[/bold red]",
border_style="red",
)
)
sys.exit(2)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,436 @@
"""
Claude Code SDK - The SDK Way
This module demonstrates the idiomatic way to use the Claude Code Python SDK
for programmatic agent interactions. It focuses on clean, type-safe patterns
using the SDK's native abstractions.
Key Concepts:
- Use `query()` for one-shot operations
- Use `ClaudeSDKClient` for interactive sessions
- Work directly with SDK message types
- Leverage async/await for clean concurrency
- Configure options for your use case
Example Usage:
# Simple query
async for message in query(prompt="What is 2 + 2?"):
if isinstance(message, AssistantMessage):
print(extract_text(message))
# With options
options = ClaudeCodeOptions(
model="claude-sonnet-4-20250514",
allowed_tools=["Read", "Write"],
permission_mode="bypassPermissions"
)
async for message in query(prompt="Create hello.py", options=options):
process_message(message)
# Interactive session
async with create_session() as client:
await client.query("Debug this error")
async for msg in client.receive_response():
handle_message(msg)
"""
import logging
from pathlib import Path
from typing import AsyncIterator, Optional, List
from contextlib import asynccontextmanager
# Import all SDK components we'll use
from claude_code_sdk import (
# Main functions
query,
ClaudeSDKClient,
# Configuration
ClaudeCodeOptions,
PermissionMode,
# Message types
Message,
AssistantMessage,
UserMessage,
SystemMessage,
ResultMessage,
# Content blocks
ContentBlock,
TextBlock,
ToolUseBlock,
ToolResultBlock,
# Errors
ClaudeSDKError,
CLIConnectionError,
CLINotFoundError,
ProcessError,
)
# Set up logging
logger = logging.getLogger(__name__)
# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================
def extract_text(message: AssistantMessage) -> str:
"""Extract all text content from an assistant message.
The SDK way: Work directly with typed message objects.
Args:
message: AssistantMessage with content blocks
Returns:
Concatenated text from all text blocks
"""
texts = []
for block in message.content:
if isinstance(block, TextBlock):
texts.append(block.text)
return "\n".join(texts)
def extract_tool_uses(message: AssistantMessage) -> List[ToolUseBlock]:
"""Extract all tool use blocks from an assistant message.
Args:
message: AssistantMessage with content blocks
Returns:
List of ToolUseBlock objects
"""
return [
block for block in message.content
if isinstance(block, ToolUseBlock)
]
def get_result_text(messages: List[Message]) -> str:
"""Extract final result text from a list of messages.
Args:
messages: List of messages from a query
Returns:
Result text or assistant responses
"""
# First check for ResultMessage
for msg in reversed(messages):
if isinstance(msg, ResultMessage) and msg.result:
return msg.result
# Otherwise collect assistant text
texts = []
for msg in messages:
if isinstance(msg, AssistantMessage):
text = extract_text(msg)
if text:
texts.append(text)
return "\n".join(texts)
# ============================================================================
# ONE-SHOT QUERIES (The Simple SDK Way)
# ============================================================================
async def simple_query(prompt: str, model: str = "claude-sonnet-4-5-20250929") -> str:
"""Simple one-shot query with text response.
The SDK way: Direct use of query() with minimal setup.
Args:
prompt: What to ask Claude
model: Which model to use
Returns:
Text response from Claude
Example:
response = await simple_query("What is 2 + 2?")
print(response) # "4" or "2 + 2 equals 4"
"""
options = ClaudeCodeOptions(model=model)
texts = []
async for message in query(prompt=prompt, options=options):
if isinstance(message, AssistantMessage):
text = extract_text(message)
if text:
texts.append(text)
return "\n".join(texts) if texts else "No response"
async def query_with_tools(
prompt: str,
allowed_tools: List[str],
working_dir: Optional[Path] = None
) -> AsyncIterator[Message]:
"""Query with specific tools enabled.
The SDK way: Configure options for your use case.
Args:
prompt: What to ask Claude
allowed_tools: List of tool names to allow
working_dir: Optional working directory
Yields:
SDK message objects
Example:
async for msg in query_with_tools(
"Create a Python script",
allowed_tools=["Write", "Read"]
):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, ToolUseBlock):
print(f"Using tool: {block.name}")
"""
options = ClaudeCodeOptions(
allowed_tools=allowed_tools,
cwd=str(working_dir) if working_dir else None,
permission_mode="bypassPermissions" # For automated workflows
)
async for message in query(prompt=prompt, options=options):
yield message
async def collect_query_response(
prompt: str,
options: Optional[ClaudeCodeOptions] = None
) -> tuple[List[Message], Optional[ResultMessage]]:
"""Collect all messages from a query.
The SDK way: Async iteration with type checking.
Args:
prompt: What to ask Claude
options: Optional configuration
Returns:
Tuple of (all_messages, result_message)
Example:
messages, result = await collect_query_response("List files")
if result and not result.is_error:
print("Success!")
for msg in messages:
process_message(msg)
"""
if options is None:
options = ClaudeCodeOptions()
messages = []
result = None
async for message in query(prompt=prompt, options=options):
messages.append(message)
if isinstance(message, ResultMessage):
result = message
return messages, result
# ============================================================================
# INTERACTIVE SESSIONS (The SDK Client Way)
# ============================================================================
@asynccontextmanager
async def create_session(
model: str = "claude-sonnet-4-5-20250929",
working_dir: Optional[Path] = None
):
"""Create an interactive session with Claude.
The SDK way: Use context managers for resource management.
Args:
model: Which model to use
working_dir: Optional working directory
Yields:
Connected ClaudeSDKClient
Example:
async with create_session() as client:
await client.query("Hello")
async for msg in client.receive_response():
print(msg)
"""
options = ClaudeCodeOptions(
model=model,
cwd=str(working_dir) if working_dir else None,
permission_mode="bypassPermissions"
)
client = ClaudeSDKClient(options=options)
await client.connect()
try:
yield client
finally:
await client.disconnect()
async def interactive_conversation(prompts: List[str]) -> List[Message]:
"""Have an interactive conversation with Claude.
The SDK way: Bidirectional communication with the client.
Args:
prompts: List of prompts to send in sequence
Returns:
All messages from the conversation
Example:
messages = await interactive_conversation([
"What's the weather like?",
"Tell me more about clouds",
"How do they form?"
])
"""
all_messages = []
async with create_session() as client:
for prompt in prompts:
# Send prompt
await client.query(prompt)
# Collect response
async for msg in client.receive_response():
all_messages.append(msg)
if isinstance(msg, ResultMessage):
break
return all_messages
# ============================================================================
# ERROR HANDLING (The SDK Way)
# ============================================================================
async def safe_query(prompt: str) -> tuple[Optional[str], Optional[str]]:
"""Query with comprehensive error handling.
The SDK way: Handle specific SDK exceptions.
Args:
prompt: What to ask Claude
Returns:
Tuple of (response_text, error_message)
Example:
response, error = await safe_query("Help me debug this")
if error:
print(f"Error: {error}")
else:
print(f"Response: {response}")
"""
try:
response = await simple_query(prompt)
return response, None
except CLINotFoundError:
return None, "Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code"
except CLIConnectionError as e:
return None, f"Connection error: {str(e)}"
except ProcessError as e:
return None, f"Process error (exit code {e.exit_code}): {str(e)}"
except ClaudeSDKError as e:
return None, f"SDK error: {str(e)}"
except Exception as e:
return None, f"Unexpected error: {str(e)}"
# ============================================================================
# ADVANCED PATTERNS (The SDK Way)
# ============================================================================
async def stream_with_progress(
prompt: str,
on_text: Optional[callable] = None,
on_tool: Optional[callable] = None
) -> ResultMessage:
"""Stream query with progress callbacks.
The SDK way: Process messages as they arrive.
Args:
prompt: What to ask Claude
on_text: Callback for text blocks (optional)
on_tool: Callback for tool use blocks (optional)
Returns:
Final ResultMessage
Example:
result = await stream_with_progress(
"Analyze this codebase",
on_text=lambda text: print(f"Claude: {text}"),
on_tool=lambda tool: print(f"Using: {tool.name}")
)
print(f"Cost: ${result.total_cost_usd:.4f}")
"""
result = None
async for message in query(prompt=prompt):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock) and on_text:
on_text(block.text)
elif isinstance(block, ToolUseBlock) and on_tool:
on_tool(block)
elif isinstance(message, ResultMessage):
result = message
return result
async def query_with_timeout(prompt: str, timeout_seconds: float = 30) -> Optional[str]:
"""Query with timeout protection.
The SDK way: Use asyncio for timeout control.
Args:
prompt: What to ask Claude
timeout_seconds: Maximum time to wait
Returns:
Response text or None if timeout
Example:
response = await query_with_timeout("Complex analysis", timeout_seconds=60)
if response is None:
print("Query timed out")
"""
import asyncio
try:
# Create the query task
async def _query():
return await simple_query(prompt)
# Run with timeout
response = await asyncio.wait_for(_query(), timeout=timeout_seconds)
return response
except asyncio.TimeoutError:
logger.warning(f"Query timed out after {timeout_seconds} seconds")
return None

View File

@@ -0,0 +1,470 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "pydantic",
# "python-dotenv",
# "click",
# "rich",
# "claude-code-sdk",
# "anyio",
# ]
# ///
"""
Run Claude Code prompts using the official Python SDK.
This ADW demonstrates using the Claude Code Python SDK for both one-shot
and interactive sessions. The SDK provides better type safety, error handling,
and a more Pythonic interface compared to subprocess-based implementations.
Usage:
# One-shot query (default)
./adws/adw_sdk_prompt.py "Hello Claude Code"
# Interactive session
./adws/adw_sdk_prompt.py --interactive
# Resume a previous session
./adws/adw_sdk_prompt.py --interactive --session-id abc123
# With specific model
./adws/adw_sdk_prompt.py "Create a FastAPI app" --model opus
# From different directory
./adws/adw_sdk_prompt.py "List files here" --working-dir /path/to/project
Examples:
# Simple query
./adws/adw_sdk_prompt.py "Explain async/await in Python"
# Interactive debugging session
./adws/adw_sdk_prompt.py --interactive --context "Debugging a memory leak"
# Resume session with context
./adws/adw_sdk_prompt.py --interactive --session-id abc123 --context "Continue debugging"
# Query with tools
./adws/adw_sdk_prompt.py "Create a Python web server" --tools Read,Write,Bash
Key Features:
- Uses official Claude Code Python SDK
- Supports both one-shot and interactive modes
- Better error handling with typed exceptions
- Native async/await support
- Clean message type handling
"""
import os
import sys
import json
import asyncio
from pathlib import Path
from typing import Optional, List
import click
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.live import Live
from rich.spinner import Spinner
from rich.text import Text
from rich.prompt import Prompt
# Add the adw_modules directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "adw_modules"))
# Import SDK functions from our clean module
from agent_sdk import (
simple_query,
query_with_tools,
collect_query_response,
create_session,
safe_query,
stream_with_progress,
extract_text,
extract_tool_uses,
)
# Import SDK types
from claude_code_sdk import (
ClaudeCodeOptions,
AssistantMessage,
ResultMessage,
TextBlock,
ToolUseBlock,
)
def generate_short_id() -> str:
"""Generate a short ID for tracking."""
import uuid
return str(uuid.uuid4())[:8]
async def run_one_shot_query(
prompt: str,
model: str,
working_dir: str,
allowed_tools: Optional[List[str]] = None,
session_id: Optional[str] = None,
) -> None:
"""Run a one-shot query using the SDK."""
console = Console()
adw_id = generate_short_id()
# Display execution info
info_table = Table(show_header=False, box=None, padding=(0, 1))
info_table.add_column(style="bold cyan")
info_table.add_column()
info_table.add_row("ADW ID", adw_id)
info_table.add_row("Mode", "One-shot Query")
info_table.add_row("Prompt", prompt)
info_table.add_row("Model", model)
info_table.add_row("Working Dir", working_dir)
if allowed_tools:
info_table.add_row("Tools", ", ".join(allowed_tools))
if session_id:
info_table.add_row("Session ID", session_id)
info_table.add_row("[bold green]SDK[/bold green]", "Claude Code Python SDK")
console.print(
Panel(
info_table,
title="[bold blue]🚀 SDK Query Execution[/bold blue]",
border_style="blue",
)
)
console.print()
try:
# Execute query based on whether tools are needed
with console.status("[bold yellow]Executing via SDK...[/bold yellow]"):
if allowed_tools:
# Query with tools
options = ClaudeCodeOptions(
model=model,
allowed_tools=allowed_tools,
cwd=working_dir,
permission_mode="bypassPermissions",
)
if session_id:
options.resume = session_id
messages, result = await collect_query_response(prompt, options=options)
# Extract response text
response_text = ""
tool_uses = []
for msg in messages:
if isinstance(msg, AssistantMessage):
text = extract_text(msg)
if text:
response_text += text + "\n"
for tool in extract_tool_uses(msg):
tool_uses.append(f"{tool.name} ({tool.id[:8]}...)")
success = result and not result.is_error if result else False
else:
# Simple query
response_text, error = await safe_query(prompt)
success = error is None
tool_uses = []
if error:
response_text = error
# Display result
if success:
console.print(
Panel(
response_text.strip(),
title="[bold green]✅ SDK Success[/bold green]",
border_style="green",
padding=(1, 2),
)
)
if tool_uses:
console.print(
f"\n[bold cyan]Tools used:[/bold cyan] {', '.join(tool_uses)}"
)
else:
console.print(
Panel(
response_text,
title="[bold red]❌ SDK Error[/bold red]",
border_style="red",
padding=(1, 2),
)
)
# Show cost and session info if available
if "result" in locals() and result:
if result.total_cost_usd:
console.print(
f"\n[bold cyan]Cost:[/bold cyan] ${result.total_cost_usd:.4f}"
)
if hasattr(result, 'session_id') and result.session_id:
console.print(
f"[bold cyan]Session ID:[/bold cyan] {result.session_id}"
)
console.print(
f"[dim]Resume with: --session-id {result.session_id}[/dim]"
)
except Exception as e:
console.print(
Panel(
f"[bold red]{str(e)}[/bold red]",
title="[bold red]❌ Unexpected Error[/bold red]",
border_style="red",
)
)
async def run_interactive_session(
model: str,
working_dir: str,
context: Optional[str] = None,
session_id: Optional[str] = None,
) -> None:
"""Run an interactive session using the SDK."""
console = Console()
adw_id = generate_short_id()
# Display session info
info_table = Table(show_header=False, box=None, padding=(0, 1))
info_table.add_column(style="bold cyan")
info_table.add_column()
info_table.add_row("ADW ID", adw_id)
info_table.add_row("Mode", "Interactive Session")
info_table.add_row("Model", model)
info_table.add_row("Working Dir", working_dir)
if context:
info_table.add_row("Context", context)
if session_id:
info_table.add_row("Session ID", session_id)
info_table.add_row("[bold green]SDK[/bold green]", "Claude Code Python SDK")
console.print(
Panel(
info_table,
title="[bold blue]💬 SDK Interactive Session[/bold blue]",
border_style="blue",
)
)
console.print()
# Instructions
console.print("[bold yellow]Interactive Mode[/bold yellow]")
console.print("Commands: 'exit' or 'quit' to end session")
console.print("Just type your questions or requests\n")
# Start session
options = ClaudeCodeOptions(
model=model,
cwd=working_dir,
permission_mode="bypassPermissions",
)
if session_id:
options.resume = session_id
from claude_code_sdk import ClaudeSDKClient
client = ClaudeSDKClient(options=options)
await client.connect()
# Track session ID from results throughout the session
session_id_from_result = None
try:
# Send initial context if provided
if context:
console.print(f"[dim]Setting context: {context}[/dim]\n")
await client.query(f"Context: {context}")
# Consume the context response
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
text = extract_text(msg)
if text:
console.print(f"[dim]Claude: {text}[/dim]\n")
# Interactive loop
while True:
# Get user input
try:
user_input = Prompt.ask("[bold cyan]You[/bold cyan]")
except (EOFError, KeyboardInterrupt):
console.print("\n[yellow]Session interrupted[/yellow]")
break
if user_input.lower() in ["exit", "quit"]:
break
# Send to Claude
await client.query(user_input)
# Show response with progress
console.print()
response_parts = []
tool_uses = []
cost = None
session_id_from_result = None
with Live(
Spinner("dots", text="Thinking..."),
console=console,
refresh_per_second=4,
):
async for msg in client.receive_response():
if isinstance(msg, AssistantMessage):
text = extract_text(msg)
if text:
response_parts.append(text)
for tool in extract_tool_uses(msg):
tool_uses.append(f"{tool.name}")
elif isinstance(msg, ResultMessage):
if msg.total_cost_usd:
cost = msg.total_cost_usd
if hasattr(msg, 'session_id') and msg.session_id:
session_id_from_result = msg.session_id
# Display response
if response_parts:
console.print("[bold green]Claude:[/bold green]")
for part in response_parts:
console.print(part)
if tool_uses:
console.print(f"\n[dim]Tools used: {', '.join(tool_uses)}[/dim]")
if cost:
console.print(f"[dim]Cost: ${cost:.4f}[/dim]")
if session_id_from_result:
console.print(f"[dim]Session ID: {session_id_from_result}[/dim]")
console.print()
finally:
await client.disconnect()
console.print("\n[bold green]Session ended[/bold green]")
console.print(f"[dim]ADW ID: {adw_id}[/dim]")
if 'session_id_from_result' in locals() and session_id_from_result:
console.print(f"[bold cyan]Session ID:[/bold cyan] {session_id_from_result}")
console.print(f"[dim]Resume with: ./adws/adw_sdk_prompt.py --interactive --session-id {session_id_from_result}[/dim]")
@click.command()
@click.argument("prompt", required=False)
@click.option(
"--interactive",
"-i",
is_flag=True,
help="Start an interactive session instead of one-shot query",
)
@click.option(
"--model",
type=click.Choice(["sonnet", "opus", "haiku"]),
default="sonnet",
help="Claude model to use (sonnet=balanced, opus=max intelligence, haiku=fast & economical)",
)
@click.option(
"--working-dir",
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
help="Working directory (default: current directory)",
)
@click.option(
"--tools",
help="Comma-separated list of allowed tools (e.g., Read,Write,Bash)",
)
@click.option(
"--context",
help="Context for interactive session (e.g., 'Debugging a memory leak')",
)
@click.option(
"--session-id",
help="Resume a previous session by its ID",
)
def main(
prompt: Optional[str],
interactive: bool,
model: str,
working_dir: Optional[str],
tools: Optional[str],
context: Optional[str],
session_id: Optional[str],
):
"""Run Claude Code prompts using the Python SDK.
Examples:
# One-shot query
adw_sdk_prompt.py "What is 2 + 2?"
# Interactive session
adw_sdk_prompt.py --interactive
# Resume session
adw_sdk_prompt.py --interactive --session-id abc123
# Query with tools
adw_sdk_prompt.py "Create hello.py" --tools Write,Read
"""
if not working_dir:
working_dir = os.getcwd()
# Convert model names
model_map = {
"sonnet": "claude-sonnet-4-5-20250929",
"opus": "claude-opus-4-20250514",
"haiku": "claude-haiku-4-5-20251001"
}
full_model = model_map.get(model, model)
# Parse tools if provided
allowed_tools = None
if tools:
allowed_tools = [t.strip() for t in tools.split(",")]
# Run appropriate mode
if interactive:
if prompt:
console = Console()
console.print(
"[yellow]Warning: Prompt ignored in interactive mode[/yellow]\n"
)
asyncio.run(
run_interactive_session(
model=full_model,
working_dir=working_dir,
context=context,
session_id=session_id,
)
)
else:
if not prompt:
console = Console()
console.print("[red]Error: Prompt required for one-shot mode[/red]")
console.print("Use --interactive for interactive session")
sys.exit(1)
asyncio.run(
run_one_shot_query(
prompt=prompt,
model=full_model,
working_dir=working_dir,
allowed_tools=allowed_tools,
session_id=session_id,
)
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,247 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "pydantic",
# "python-dotenv",
# "click",
# "rich",
# ]
# ///
"""
Run Claude Code slash commands from the command line.
Usage:
# Method 1: Direct execution (requires uv)
./adws/adw_slash_command.py /chore "Update documentation"
# Method 2: Using uv run
uv run adws/adw_slash_command.py /implement specs/<name-of-spec>.md
uv run adws/adw_slash_command.py /start
Examples:
# Run a slash command
./adws/adw_slash_command.py /chore "Add logging to agent.py"
# Run with specific model
./adws/adw_slash_command.py /implement plan.md --model opus
# Run from a different working directory
./adws/adw_slash_command.py /test --working-dir /path/to/project
# Use custom agent name
./adws/adw_slash_command.py /review --agent-name reviewer
"""
import os
import sys
import json
from pathlib import Path
import click
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
# Add the adw_modules directory to the path so we can import agent
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "adw_modules"))
from agent import (
AgentTemplateRequest,
AgentPromptResponse,
execute_template,
generate_short_id,
)
# Output file name constants
OUTPUT_JSONL = "cc_raw_output.jsonl"
OUTPUT_JSON = "cc_raw_output.json"
FINAL_OBJECT_JSON = "cc_final_object.json"
SUMMARY_JSON = "custom_summary_output.json"
@click.command()
@click.argument("slash_command", required=True)
@click.argument("args", nargs=-1) # Accept multiple optional arguments
@click.option(
"--model",
type=click.Choice(["sonnet", "opus", "haiku"]),
default="sonnet",
help="Claude model to use (sonnet=balanced, opus=max intelligence, haiku=fast & economical)",
)
@click.option(
"--working-dir",
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
help="Working directory for command execution (default: current directory)",
)
@click.option(
"--agent-name",
default="executor",
help="Agent name for tracking (default: executor)",
)
def main(
slash_command: str,
args: tuple,
model: str,
working_dir: str,
agent_name: str,
):
"""Run Claude Code slash commands from the command line."""
console = Console()
# Generate a unique ID for this execution
adw_id = generate_short_id()
# Use current directory if no working directory specified
if not working_dir:
working_dir = os.getcwd()
# Create the template request
request = AgentTemplateRequest(
agent_name=agent_name,
slash_command=slash_command,
args=list(args), # Convert tuple to list
adw_id=adw_id,
model=model,
working_dir=working_dir,
)
# Create execution info table
info_table = Table(show_header=False, box=None, padding=(0, 1))
info_table.add_column(style="bold cyan")
info_table.add_column()
info_table.add_row("ADW ID", adw_id)
info_table.add_row("ADW Name", "adw_slash_command")
info_table.add_row("Command", slash_command)
info_table.add_row("Args", " ".join(args) if args else "(none)")
info_table.add_row("Model", model)
info_table.add_row("Working Dir", working_dir)
console.print(
Panel(
info_table,
title="[bold blue]🚀 Inputs[/bold blue]",
border_style="blue",
)
)
console.print()
try:
# Execute the slash command
with console.status("[bold yellow]Executing command...[/bold yellow]"):
response = execute_template(request)
# Display the result
if response.success:
# Success panel
result_panel = Panel(
response.output,
title="[bold green]✅ Success[/bold green]",
border_style="green",
padding=(1, 2),
)
console.print(result_panel)
if response.session_id:
console.print(
f"\n[bold cyan]Session ID:[/bold cyan] {response.session_id}"
)
else:
# Error panel
error_panel = Panel(
response.output,
title="[bold red]❌ Failed[/bold red]",
border_style="red",
padding=(1, 2),
)
console.print(error_panel)
if response.retry_code != "none":
console.print(
f"\n[bold yellow]Retry code:[/bold yellow] {response.retry_code}"
)
# Show output file info
console.print()
# Output files are in agents/<adw_id>/<agent_name>/
output_dir = f"./agents/{adw_id}/{agent_name}"
# Create the simple JSON summary file
simple_json_output = f"{output_dir}/{SUMMARY_JSON}"
# Determine the template file path
command_name = slash_command.lstrip("/") # Remove leading slash
path_to_slash_command_prompt = f".claude/commands/{command_name}.md"
with open(simple_json_output, "w") as f:
json.dump(
{
"adw_id": adw_id,
"slash_command": slash_command,
"args": list(args),
"path_to_slash_command_prompt": path_to_slash_command_prompt,
"model": model,
"working_dir": working_dir,
"success": response.success,
"session_id": response.session_id,
"retry_code": response.retry_code,
"output": response.output,
},
f,
indent=2,
)
# Files saved panel
files_table = Table(show_header=True, box=None)
files_table.add_column("File Type", style="bold cyan")
files_table.add_column("Path", style="dim")
files_table.add_column("Description", style="italic")
files_table.add_row(
"JSONL Stream",
f"{output_dir}/{OUTPUT_JSONL}",
"Raw streaming output from Claude Code",
)
files_table.add_row(
"JSON Array",
f"{output_dir}/{OUTPUT_JSON}",
"All messages as a JSON array",
)
files_table.add_row(
"Final Object",
f"{output_dir}/{FINAL_OBJECT_JSON}",
"Last message entry (final result)",
)
files_table.add_row(
"Summary",
simple_json_output,
"High-level execution summary with metadata",
)
console.print(
Panel(
files_table,
title="[bold blue]📄 Output Files[/bold blue]",
border_style="blue",
)
)
# Exit with appropriate code
sys.exit(0 if response.success else 1)
except Exception as e:
console.print(
Panel(
f"[bold red]{str(e)}[/bold red]",
title="[bold red]❌ Unexpected Error[/bold red]",
border_style="red",
)
)
sys.exit(2)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,109 @@
# Feature Planning
Create a plan to implement the feature using the specified markdown `Plan Format`. Research the codebase and create a thorough plan.
## Variables
adw_id: $1
prompt: $2
## Instructions
- If the adw_id or prompt is not provided, stop and ask the user to provide them.
- Create a plan to implement the feature described in the `prompt`
- The plan should be comprehensive, well-designed, and follow existing patterns
- Create the plan in the `specs/` directory with filename: `feature-{adw_id}-{descriptive-name}.md`
- Replace `{descriptive-name}` with a short, descriptive name based on the feature (e.g., "add-agent-logging", "implement-retry-logic", "create-workflow-api")
- Research the codebase starting with `README.md`
- Replace every <placeholder> in the `Plan Format` with the requested value
- Use your reasoning model: THINK HARD about the feature requirements, design, and implementation approach
- Follow existing patterns and conventions in the codebase
- Design for extensibility and maintainability
## Codebase Structure
- `README.md` - Project overview and instructions (start here)
- `adws/` - AI Developer Workflow scripts and modules
- `apps/` - Application layer you'll be working in
- `.claude/commands/` - Claude command templates
- `specs/` - Specification and plan documents
## Plan Format
```md
# Feature: <feature name>
## Metadata
adw_id: `{adw_id}`
prompt: `{prompt}`
## Feature Description
<describe the feature in detail, including its purpose and value to users>
## User Story
As a <type of user>
I want to <action/goal>
So that <benefit/value>
## Problem Statement
<clearly define the specific problem or opportunity this feature addresses>
## Solution Statement
<describe the proposed solution approach and how it solves the problem>
## Relevant Files
Use these files to implement the feature:
<list files relevant to the feature with bullet points explaining why. Include new files to be created under an h3 'New Files' section if needed>
## Implementation Plan
### Phase 1: Foundation
<describe the foundational work needed before implementing the main feature>
### Phase 2: Core Implementation
<describe the main implementation work for the feature>
### Phase 3: Integration
<describe how the feature will integrate with existing functionality>
## Step by Step Tasks
IMPORTANT: Execute every step in order, top to bottom.
<list step by step tasks as h3 headers with bullet points. Start with foundational changes then move to specific changes. Include creating tests throughout the implementation process>
### 1. <First Task Name>
- <specific action>
- <specific action>
### 2. <Second Task Name>
- <specific action>
- <specific action>
<continue with additional tasks as needed>
## Testing Strategy
### Unit Tests
<describe unit tests needed for the feature>
### Edge Cases
<list edge cases that need to be tested>
## Acceptance Criteria
<list specific, measurable criteria that must be met for the feature to be considered complete>
## Validation Commands
Execute these commands to validate the feature is complete:
<list specific commands to validate the work. Be precise about what to run>
- Example: `uv run python -m py_compile apps/*.py` - Test to ensure the code compiles
## Notes
<optional additional context, future considerations, or dependencies. If new libraries are needed, specify using `uv add`>
```
## Feature
Use the feature description from the `prompt` variable.
## Report
Return the path to the plan file created.

View File

@@ -0,0 +1,12 @@
# Prime
Execute the `Run`, `Read` and `Report` sections to understand the codebase then summarize your understanding.
## Run
git ls-files
## Read
README.md
adws/README.md
## Report
Summarize your understanding of the codebase.

View File

@@ -0,0 +1,9 @@
# Start your applications
## Instructions
- Run the commands in the `Run` section below top to bottom.
## Run
uv run apps/main.py
bun run apps/main.ts