Files
quicproquo/scripts/ai_team.py
Chris Nennemann b6483dedbc chore: ROADMAP Phase 8, parallel AI team script, docker and infra updates
- ROADMAP.md: add Phase 8 — Freifunk / Community Mesh Networking with
  F0-F8 checkboxes; F0-F2 marked complete
- scripts/ai_team.py: rewrite to support asyncio.gather parallel agent
  runs; add --sprint flag with predefined work packages (audit,
  phase1-hardening, phase2-tests, phase1-infra, status); add --parallel
  for ad-hoc concurrent agent invocations; output written to
  logs/ai_team/<sprint>_<timestamp>/<agent>.md
- scripts/dev-shell.sh: convenience development shell helper
- docker: update Dockerfiles for quicproquo rename and new server flags
- .gitignore: add qpq-state artifacts (*.bin, *.session, *.pending.ks,
  *.convdb*)
2026-03-03 14:42:21 +01:00

798 lines
31 KiB
Python
Executable File

#!/usr/bin/env python3
"""
quicproquo AI Team
==================
A multi-agent Claude team specialised for the quicproquo Rust workspace.
Usage:
python scripts/ai_team.py "<task>" # orchestrator
python scripts/ai_team.py --agent <name> "<task>" # single agent
python scripts/ai_team.py --sprint <name> # 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/<crate>/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_<what>_<expected_outcome>` (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())