#!/usr/bin/env python3 """ quicproquo AI Team ================== A multi-agent Claude team specialised for the quicproquo Rust workspace. Usage: python scripts/ai_team.py "" # orchestrator python scripts/ai_team.py --agent "" # single agent python scripts/ai_team.py --sprint # parallel sprint python scripts/ai_team.py --parallel \\ "rust-server-dev: Fix unwrap() in server" \\ "security-auditor: Audit quicproquo-core" # ad-hoc parallel python scripts/ai_team.py --list-agents python scripts/ai_team.py --list-sprints Requires: pip install claude-agent-sdk The ANTHROPIC_API_KEY environment variable must be set. """ import argparse import asyncio import sys import os from datetime import datetime from pathlib import Path try: from claude_agent_sdk import ( query, ClaudeAgentOptions, AgentDefinition, ResultMessage, SystemMessage, AssistantMessage, TextBlock, CLINotFoundError, CLIConnectionError, ) except ImportError: print("ERROR: claude-agent-sdk not found.") print("Install with: pip install claude-agent-sdk") sys.exit(1) # ── Project root ─────────────────────────────────────────────────────────────── PROJECT_ROOT = str(Path(__file__).parent.parent.resolve()) # ── Shared project context injected into every agent's system prompt ─────────── PROJECT_CONTEXT = """ ## Project: quicproquo A production-grade end-to-end encrypted group messenger written in Rust. ### Transport stack TCP → Noise_XX (snow) → ChaCha20-Poly1305 encrypted channel → Cap'n Proto RPC ### Workspace layout ``` quicproquo/ ├── Cargo.toml # workspace root ├── crates/ │ ├── quicproquo-core/ # crypto primitives, MLS wrapper, Noise codec │ ├── quicproquo-proto/ # Cap'n Proto schemas + generated types │ ├── quicproquo-server/ # Delivery Service (DS) + Authentication Service (AS) │ ├── quicproquo-client/ # CLI client (clap, REPL) │ ├── quicproquo-gui/ # GUI frontend (WIP) │ └── quicproquo-mobile/ # Mobile frontend (WIP) ├── schemas/ # .capnp schema files (canonical source) ├── docker/ + docker-compose.yml ├── docs/ ├── scripts/ ├── ROADMAP.md # phased milestone plan └── master-prompt.md # full architecture reference ``` ### Non-negotiable engineering standards - Production-ready only — no stubs, todo!(), unimplemented!(), or placeholder logic. - YAGNI / KISS / DRY. - Spec-first: doc comments before implementation. - Security-by-design: zeroize secrets, typed errors, no unwrap() on crypto paths. - Conventional commits: feat:, fix:, chore:, docs:, test:, refactor:. - No Co-authored-by trailers. GPG-signed commits only. ### Key dependencies (pinned majors) openmls 0.5, openmls_rust_crypto 0.2, ml-kem 0.2, x25519-dalek 2, ed25519-dalek 2, snow 0.9, chacha20poly1305 0.10, capnp 0.19, capnp-rpc 0.19, tokio 1, tokio-util 0.7, dashmap 5, rusqlite (SQLite), tracing 0.1, anyhow 1, thiserror 1, clap 4. Always read ROADMAP.md and master-prompt.md before making architectural decisions. """.strip() # ── Agent definitions ────────────────────────────────────────────────────────── AGENTS: dict[str, AgentDefinition] = { "rust-architect": AgentDefinition( description=( "Senior Rust architect for quicproquo. Designs new features, writes ADRs, " "reviews architecture decisions, analyses crate boundaries, and ensures the " "design conforms to master-prompt.md. Does NOT write implementation code." ), prompt=f"""{PROJECT_CONTEXT} You are the **Rust Architect** for quicproquo. Responsibilities: - Read ROADMAP.md and master-prompt.md to understand the current milestone and constraints. - Produce concise Architecture Decision Records (ADR format) when a significant decision is made. - Review proposed designs for correctness against MLS RFC 9420, Noise protocol spec, and Cap'n Proto semantics. - Identify crate-boundary violations (e.g. I/O in quicproquo-proto, crypto in quicproquo-server). - Flag when a feature would require a new crate dependency and evaluate it. - Never produce implementation code — your output is design documents and reviews. Output format: 1. One-sentence summary of the architectural concern. 2. ADR (if applicable): Context → Decision → Consequences. 3. Concrete list of action items for the development agents. """, tools=["Read", "Glob", "Grep"], ), "rust-core-dev": AgentDefinition( description=( "Implements quicproquo-core: Noise_XX handshake, Cap'n Proto frame codec, " "MLS group state machine, hybrid PQ KEM (X25519 + ML-KEM-768), key types " "with zeroize-on-drop, and all crypto primitives." ), prompt=f"""{PROJECT_CONTEXT} You are the **Core Developer** for quicproquo, responsible for the `quicproquo-core` crate. Crate responsibilities: - Noise_XX handshake initiator and responder (via `snow`). - Length-prefixed Cap'n Proto frame codec (Tokio Encoder/Decoder traits). - MLS group state machine wrapper around `openmls`. - Hybrid PQ ciphersuite: X25519 + ML-KEM-768 → HKDF-SHA256 → 32-byte shared secret. - Key generation, zeroize-on-drop key types. - OPAQUE password auth helper types. Before any edit: 1. Read the relevant source file(s) in full. 2. Check ROADMAP.md for the current milestone scope. 3. Confirm no new dependencies are needed or justify additions. After any edit: run `cargo check -p quicproquo-core` to verify compilation. Security requirements: - All crypto errors must be propagated as typed `Result` — never `.unwrap()`. - Key material structs must derive `Zeroize` and `ZeroizeOnDrop`. - No secret bytes in log output. """, tools=["Read", "Glob", "Grep", "Edit", "Write", "Bash"], ), "rust-server-dev": AgentDefinition( description=( "Implements quicproquo-server: TCP listener, Noise handshake per connection, " "Cap'n Proto RPC server for the Authentication Service (AS) and Delivery " "Service (DS), fan-out router, per-group message log, SQLite persistence." ), prompt=f"""{PROJECT_CONTEXT} You are the **Server Developer** for quicproquo, responsible for the `quicproquo-server` crate. Crate responsibilities: - Tokio TCP listener; one task per connection. - Noise_XX responder using quicproquo-core. - Cap'n Proto RPC server stubs (capnp-rpc) for AuthenticationService and DeliveryService. - Authentication Service: KeyPackage store (DashMap → SQLite at M6). - Delivery Service: fan-out router, per-group append-only message log. - Structured logging via `tracing`. Before any edit: 1. Read the relevant source file(s) in full. 2. Verify the Cap'n Proto schema in `schemas/` for the interface you are implementing. 3. Check ROADMAP.md for what is in scope. After any edit: run `cargo check -p quicproquo-server` to verify compilation. Security requirements: - No `.unwrap()` on any lock or I/O operation in production paths. - Auth tokens validated before any privileged operation. - `QPQ_PRODUCTION=true` check: reject weak/default tokens on startup. """, tools=["Read", "Glob", "Grep", "Edit", "Write", "Bash"], ), "rust-client-dev": AgentDefinition( description=( "Implements quicproquo-client: CLI (clap), interactive REPL, Noise handshake, " "Cap'n Proto RPC client stubs, OPAQUE login/register, encrypted local state " "(SQLCipher + Argon2id), conversation and session management." ), prompt=f"""{PROJECT_CONTEXT} You are the **Client Developer** for quicproquo, responsible for the `quicproquo-client` crate. Crate responsibilities: - Tokio TCP connection to server; Noise_XX initiator via quicproquo-core. - Cap'n Proto RPC client stubs. - OPAQUE password-authenticated key exchange (register + login). - CLI interface (clap) with subcommands and an interactive REPL. - Encrypted local state: SQLCipher + Argon2id + ChaCha20-Poly1305 for session tokens. - Conversation management, background polling, message history. Before any edit: 1. Read the relevant source file(s) in full. 2. Understand existing command handlers in `commands.rs` and state management in `state.rs`. 3. Check ROADMAP.md for the current milestone scope. After any edit: run `cargo check -p quicproquo-client` to verify compilation. UX requirements: - Clear error messages for the user — no raw Rust error types exposed in REPL output. - REPL prompt must show current context (server, active conversation). """, tools=["Read", "Glob", "Grep", "Edit", "Write", "Bash"], ), "security-auditor": AgentDefinition( description=( "Security-focused auditor for quicproquo. Reviews Rust source for: unwrap()/expect() " "on crypto paths, missing zeroize, secrets in logs, non-constant-time comparisons, " "improper error handling, and deviations from the security standards in master-prompt.md. " "Produces a prioritised finding report — does NOT edit files." ), prompt=f"""{PROJECT_CONTEXT} You are the **Security Auditor** for quicproquo. Your job is to read Rust source code and produce a prioritised security finding report. Audit checklist: 1. `.unwrap()` / `.expect()` in non-test code on crypto or I/O operations. 2. Key material types missing `Zeroize` / `ZeroizeOnDrop`. 3. Secret bytes (keys, passwords, tokens) potentially reaching `tracing`/`log` output. 4. Non-constant-time comparisons on authentication tags or tokens. 5. `panic!` / `unreachable!` in production paths. 6. `unsafe` blocks without documented safety invariants. 7. Missing `#[cfg(not(test))]` guards around debug-only logic. 8. Deviations from the engineering standards in master-prompt.md. 9. Dockerfile / docker-compose security issues (running as root, secrets in ENV, etc.). Output format (Markdown): ## Security Audit Report ### Critical - [file:line] Description. Remediation: ... ### High - ... ### Medium - ... ### Low / Informational - ... Do NOT edit any files. Findings only. """, tools=["Read", "Glob", "Grep"], ), "test-engineer": AgentDefinition( description=( "Writes and runs tests for quicproquo. Adds unit tests, integration tests, " "and property-based tests. Runs `cargo test` and interprets failures. " "Knows the milestone-by-milestone test requirements from ROADMAP.md." ), prompt=f"""{PROJECT_CONTEXT} You are the **Test Engineer** for quicproquo. Responsibilities: - Write unit tests inside `#[cfg(test)]` modules in the relevant crate. - Write integration tests in `crates//tests/`. - Run `cargo test --workspace` and interpret failures. - For crypto code, write property-based tests using `proptest` when applicable. - Verify test coverage against the milestone acceptance criteria in ROADMAP.md. Test naming convention: `test__` (snake_case). After writing tests, run them with Bash and report: - Which tests pass / fail. - Root cause of any failure. - Suggested fix (but do not edit non-test files without instruction). """, tools=["Read", "Glob", "Grep", "Edit", "Write", "Bash"], ), "roadmap-tracker": AgentDefinition( description=( "Reads ROADMAP.md and the codebase to determine: which milestones are complete, " "which are in progress, what the next actionable tasks are, and which ROADMAP items " "are blocked. Produces a concise status report — does NOT edit files." ), prompt=f"""{PROJECT_CONTEXT} You are the **Roadmap Tracker** for quicproquo. Your job is to read ROADMAP.md and grep/read the source code to assess progress and produce a status report. Steps: 1. Read ROADMAP.md in full. 2. For each unchecked `- [ ]` item, search the codebase for evidence of implementation. 3. Identify blockers (e.g. a later item depending on an incomplete earlier item). 4. Identify quick wins (small, self-contained tasks that can be done immediately). Output format (Markdown): ## Roadmap Status Report ### Completed ✅ - Phase X, item Y: ... ### In Progress 🔄 - Phase X, item Y: partial — what exists vs what's missing. ### Next Actionable Tasks (prioritised) 1. ... 2. ... ### Blockers - ... Do NOT edit any files. Analysis only. """, tools=["Read", "Glob", "Grep"], ), } # ── Parallel sprint definitions ──────────────────────────────────────────────── # Each sprint is a list of (agent_name, task) pairs run concurrently. # Independent tasks that touch different crates can always be parallelised. # Tasks that depend on each other (e.g. audit after code changes) should be # run as separate sprints. SPRINTS: dict[str, list[tuple[str, str]]] = { "audit": [ ("security-auditor", "Perform a full security audit of all production Rust source in quicproquo-core " "and quicproquo-server. Check every file for: .unwrap()/.expect() outside #[cfg(test)], " "key material types missing Zeroize/ZeroizeOnDrop, secrets potentially reaching tracing " "output, non-constant-time comparisons, unsafe blocks without safety docs, and Dockerfile " "security issues. Produce a prioritised finding report in Markdown."), ("roadmap-tracker", "Read ROADMAP.md and the full codebase. Assess which Phase 1 and Phase 2 items are " "complete, partially done, or not started. For each incomplete item search the source " "for relevant code. Produce a concise status report with prioritised next actions."), ], "phase1-hardening": [ ("rust-server-dev", "Fix Phase 1.1: eliminate all .unwrap() and .expect() in quicproquo-server production " "paths (anything outside #[cfg(test)]). Read every .rs file in crates/quicproquo-server/src/. " "Replace each .unwrap() with proper ? propagation or map_err. Replace .expect() with " "a typed error or explicit match. Run `cargo check -p quicproquo-server` after each file. " "Also check Phase 1.2 (QPQ_PRODUCTION=true startup validation) and implement if missing."), ("rust-client-dev", "Fix Phase 1.1: eliminate all .unwrap() and .expect() in quicproquo-client production " "paths (anything outside #[cfg(test)]). Read every .rs file in crates/quicproquo-client/src/. " "Replace each .unwrap() with proper ? propagation or map_err. Replace .expect() with " "a typed error or explicit match. Run `cargo check -p quicproquo-client` after each file. " "Pay special attention to AUTH_CONTEXT.read().expect() and any Mutex::lock().unwrap() calls."), ("rust-core-dev", "Fix Phase 1.1: check quicproquo-core for any .unwrap()/.expect() in non-test code. " "Read all files in crates/quicproquo-core/src/. Replace any found instances with typed " "Result propagation. Also review all key material types: ensure every struct holding " "secret bytes derives Zeroize and ZeroizeOnDrop. Run `cargo check -p quicproquo-core`."), ], "phase2-tests": [ ("test-engineer", "Implement Phase 2.1 E2E test coverage for auth failure scenarios. Add to " "crates/quicproquo-client/tests/e2e.rs: (1) wrong-password login returns error, " "(2) expired/invalid token is rejected by server, (3) message ordering: send 5 messages " "in sequence, verify seq numbers arrive in order. Read the existing e2e.rs first to " "match the test harness pattern (spawn_test_server, AUTH_LOCK). Run tests with " "`cargo test -p quicproquo-client --test e2e -- --test-threads 1` and fix any failures."), ("test-engineer", "Implement Phase 2.2 unit tests for untested paths. Add to quicproquo-client: " "(1) REPL input parsing edge cases — test parse_input() with empty string, whitespace-only, " "'/dm' with no args, '/send' with no args, unknown slash command. " "(2) Token cache expiry — test that an expired token is evicted on next access. " "Read repl.rs and token_cache.rs first to understand the APIs. " "Run `cargo test -p quicproquo-client` and fix any failures."), ], "phase1-infra": [ ("rust-server-dev", "Fix Phase 1.3 and 1.4. " "1.3 — Check .gitignore at project root. Add missing entries: data/, *.der, *.pem, " "*.db, *.bin, *.ks, qpq-state.*, target/. Verify with `git ls-files --error-unmatch` " "for each pattern to ensure no secrets are tracked. " "1.4 — Fix docker/Dockerfile: (a) add the p2p crate correctly to workspace, " "(b) create a dedicated non-root user instead of nobody, (c) set writable QPQ_DATA_DIR " "with correct permissions. Test with `docker build -f docker/Dockerfile .`"), ("rust-architect", "Design the TLS certificate lifecycle for Phase 1.5. Read crates/quicproquo-server/src/tls.rs " "and config.rs in full. Produce an ADR covering: (1) how CA-signed certs (Let's Encrypt / " "custom CA) should be configured, (2) what --tls-required flag behaviour should be, " "(3) how the server should warn when using self-signed certs, " "(4) certificate rotation procedure without downtime. " "Output: ADR + concrete action items for rust-server-dev."), ], "status": [ ("roadmap-tracker", "Full roadmap status report. Read ROADMAP.md completely. For every unchecked item " "across all phases, search the source to determine if it's implemented, partial, or missing. " "Produce a structured report: Completed / In Progress / Not Started / Blockers / " "Top 5 Quick Wins."), ("security-auditor", "Quick security sweep of all recent changes (git diff HEAD~5). Read the modified files " "in full. Focus on: any new .unwrap()/.expect() introduced, new code paths that handle " "key material, any new logging that might leak secrets, and any new external inputs that " "lack validation. Produce a concise finding report."), ], } # ── Orchestrator system prompt ───────────────────────────────────────────────── ORCHESTRATOR_PROMPT = f"""{PROJECT_CONTEXT} You are the **Orchestrator** for the quicproquo AI development team. Your team of specialist subagents: | Agent | Role | |-------|------| | rust-architect | Architecture design, ADRs, design reviews | | rust-core-dev | quicproquo-core crate: crypto, MLS, Noise codec | | rust-server-dev | quicproquo-server crate: AS, DS, RPC server | | rust-client-dev | quicproquo-client crate: CLI, REPL, local state | | security-auditor | Security review: unwrap(), zeroize, secrets in logs | | test-engineer | Unit/integration tests, cargo test runs | | roadmap-tracker | Roadmap progress assessment | Workflow: 1. Read the task carefully. 2. Decide which agent(s) are needed. For multi-step tasks, sequence them logically. 3. Call each required agent with a precise, scoped prompt. 4. Synthesise the agents' outputs into a final report or code deliverable. 5. Always end with: "Next suggested task: ..." based on the ROADMAP. Rules: - Read master-prompt.md and ROADMAP.md before delegating significant tasks. - Do NOT delegate everything to one agent — split by crate/concern. - If a task touches security, always invoke security-auditor after code changes. - If a task adds/modifies functionality, always invoke test-engineer last. - Keep your synthesis concise — prefer structured output (headers, bullet lists). """ # ── Parallel runner ──────────────────────────────────────────────────────────── async def run_agent_to_file( agent_name: str, task: str, max_turns: int, output_dir: Path, label: str, ) -> tuple[str, str, str | None]: """ Run a single agent and stream its result to an output file. Returns (agent_name, label, result_text_or_None). `result_text` is None if the agent produced no ResultMessage. """ output_file = output_dir / f"{label}.md" result_text: str | None = None agent = AGENTS[agent_name] options = ClaudeAgentOptions( cwd=PROJECT_ROOT, allowed_tools=agent.tools or ["Read", "Glob", "Grep"], system_prompt=agent.prompt, max_turns=max_turns, permission_mode="acceptEdits", setting_sources=["project"], ) with open(output_file, "w") as f: f.write(f"# Agent: {agent_name}\n\n") f.write(f"**Task:** {task}\n\n") f.write(f"**Started:** {datetime.now().isoformat()}\n\n---\n\n") async for message in query(prompt=task, options=options): if isinstance(message, ResultMessage): result_text = message.result f.write(f"## Result\n\n{result_text}\n") f.write(f"\n**Finished:** {datetime.now().isoformat()}\n") return agent_name, label, result_text async def run_parallel( agent_tasks: list[tuple[str, str]], max_turns: int, verbose: bool, sprint_name: str = "custom", ) -> None: """Launch all (agent, task) pairs concurrently and print a summary when done.""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_dir = Path(PROJECT_ROOT) / "logs" / "ai_team" / f"{sprint_name}_{timestamp}" output_dir.mkdir(parents=True, exist_ok=True) print(f"\n{'═' * 70}") print(f" quicproquo AI Team — Parallel Sprint: {sprint_name}") print(f" Agents: {len(agent_tasks)} | Max turns each: {max_turns}") print(f" Logs: {output_dir}/") print(f"{'═' * 70}\n") for i, (agent, task) in enumerate(agent_tasks, 1): label = f"{i:02d}_{agent}" print(f" [{i}] {agent}") print(f" {task[:80]}{'…' if len(task) > 80 else ''}") print() # Build coroutines with stable labels for output files. coros = [ run_agent_to_file(agent, task, max_turns, output_dir, f"{i:02d}_{agent}") for i, (agent, task) in enumerate(agent_tasks, 1) ] print(f" Starting {len(coros)} agents in parallel…\n") results = await asyncio.gather(*coros, return_exceptions=True) print(f"\n{'─' * 70}") print(" SPRINT RESULTS") print(f"{'─' * 70}") success = 0 for result in results: if isinstance(result, Exception): print(f"\n ❌ ERROR: {result}") else: agent_name, label, text = result if text is not None: success += 1 print(f"\n ✅ {agent_name} ({label}.md)") # Show first 300 chars of result as a preview. preview = text.strip()[:300] for line in preview.splitlines(): print(f" {line}") if len(text.strip()) > 300: print(" …") else: print(f"\n ⚠️ {agent_name}: no result produced") print(f"\n {success}/{len(agent_tasks)} agents completed successfully.") print(f" Full outputs: {output_dir}/\n") # ── Sequential runners ───────────────────────────────────────────────────────── async def run_orchestrator(task: str, max_turns: int, verbose: bool) -> None: """Run the full team via the orchestrator.""" print(f"\n{'═' * 70}") print(f" quicproquo AI Team — Orchestrator") print(f" Task: {task[:72]}{'…' if len(task) > 72 else ''}") print(f"{'═' * 70}\n") options = ClaudeAgentOptions( cwd=PROJECT_ROOT, allowed_tools=["Read", "Glob", "Grep", "Agent"], system_prompt=ORCHESTRATOR_PROMPT, agents=AGENTS, max_turns=max_turns, permission_mode="acceptEdits", setting_sources=["project"], ) async for message in query(prompt=task, options=options): if isinstance(message, ResultMessage): print("\n" + "─" * 70) print("RESULT") print("─" * 70) print(message.result) elif verbose: if isinstance(message, AssistantMessage): for block in message.content: if isinstance(block, TextBlock) and block.text.strip(): print(block.text, end="", flush=True) elif isinstance(message, SystemMessage) and message.subtype == "init": print(f"[Session: {message.session_id}]") async def run_single_agent( agent_name: str, task: str, max_turns: int, verbose: bool ) -> None: """Bypass the orchestrator and run a single specialist agent directly.""" agent = AGENTS[agent_name] print(f"\n{'═' * 70}") print(f" quicproquo AI Team — {agent_name}") print(f" Task: {task[:72]}{'…' if len(task) > 72 else ''}") print(f"{'═' * 70}\n") options = ClaudeAgentOptions( cwd=PROJECT_ROOT, allowed_tools=agent.tools or ["Read", "Glob", "Grep"], system_prompt=agent.prompt, max_turns=max_turns, permission_mode="acceptEdits", setting_sources=["project"], ) async for message in query(prompt=task, options=options): if isinstance(message, ResultMessage): print("\n" + "─" * 70) print("RESULT") print("─" * 70) print(message.result) elif verbose: if isinstance(message, AssistantMessage): for block in message.content: if isinstance(block, TextBlock) and block.text.strip(): print(block.text, end="", flush=True) # ── CLI ──────────────────────────────────────────────────────────────────────── def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="ai_team", description="quicproquo multi-agent Claude team", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument( "task", nargs="?", help="Task description for the orchestrator", ) parser.add_argument( "--agent", "-a", choices=list(AGENTS.keys()), default=None, help="Bypass orchestrator and send task directly to a specific agent", ) parser.add_argument( "--sprint", "-s", choices=list(SPRINTS.keys()), default=None, metavar="SPRINT", help="Run a predefined parallel sprint (see --list-sprints)", ) parser.add_argument( "--parallel", "-p", nargs="+", metavar="AGENT:TASK", default=None, help=( 'Ad-hoc parallel run. Each argument is "agent-name: task description". ' 'Example: --parallel "rust-server-dev: Fix unwrap() in server" ' '"security-auditor: Audit core crate"' ), ) parser.add_argument( "--list-agents", "-l", action="store_true", help="List available agents and exit", ) parser.add_argument( "--list-sprints", action="store_true", help="List predefined sprints and exit", ) parser.add_argument( "--max-turns", type=int, default=60, help="Maximum agentic turns per agent (default: 60)", ) parser.add_argument( "--verbose", "-v", action="store_true", help="Print all message types (not just results)", ) return parser def list_agents() -> None: print("Available agents:\n") for name, defn in AGENTS.items(): print(f" {name}") desc = defn.description wrapped = "\n ".join( desc[i : i + 72] for i in range(0, len(desc), 72) ) print(f" {wrapped}\n") def list_sprints() -> None: print("Predefined sprints:\n") for name, tasks in SPRINTS.items(): print(f" {name} ({len(tasks)} agents in parallel)") for agent, task in tasks: preview = task[:60] + ("…" if len(task) > 60 else "") print(f" [{agent}] {preview}") print() def parse_parallel_args(args: list[str]) -> list[tuple[str, str]]: """ Parse --parallel arguments of the form "agent-name: task description". The colon after the agent name is required. """ pairs: list[tuple[str, str]] = [] valid = set(AGENTS.keys()) for arg in args: if ":" not in arg: print(f"ERROR: --parallel argument missing colon separator: {arg!r}") print(" Expected format: \"agent-name: task description\"") sys.exit(1) agent, _, task = arg.partition(":") agent = agent.strip() task = task.strip() if agent not in valid: print(f"ERROR: unknown agent {agent!r}. Valid: {', '.join(sorted(valid))}") sys.exit(1) if not task: print(f"ERROR: empty task for agent {agent!r}") sys.exit(1) pairs.append((agent, task)) return pairs # ── Entry point ──────────────────────────────────────────────────────────────── async def main() -> None: parser = build_parser() args = parser.parse_args() if args.list_agents: list_agents() return if args.list_sprints: list_sprints() return if not os.environ.get("ANTHROPIC_API_KEY"): print("ERROR: ANTHROPIC_API_KEY environment variable is not set.") sys.exit(1) try: if args.sprint: agent_tasks = SPRINTS[args.sprint] await run_parallel( agent_tasks, args.max_turns, args.verbose, sprint_name=args.sprint ) elif args.parallel: agent_tasks = parse_parallel_args(args.parallel) await run_parallel( agent_tasks, args.max_turns, args.verbose, sprint_name="custom" ) elif args.agent: if not args.task: print("ERROR: --agent requires a task argument.") sys.exit(1) await run_single_agent( args.agent, args.task, args.max_turns, args.verbose ) elif args.task: await run_orchestrator(args.task, args.max_turns, args.verbose) else: parser.print_help() sys.exit(1) except CLINotFoundError: print( "\nERROR: Claude Code CLI not found.\n" "Install with: pip install claude-agent-sdk" ) sys.exit(1) except CLIConnectionError as e: print(f"\nERROR: Connection error: {e}") sys.exit(1) except KeyboardInterrupt: print("\n\nInterrupted.") sys.exit(0) if __name__ == "__main__": asyncio.run(main())