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*)
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -7,4 +7,13 @@ docs/book/
|
|||||||
# Server/client runtime data — do not commit certs, keys, or DBs
|
# Server/client runtime data — do not commit certs, keys, or DBs
|
||||||
data/
|
data/
|
||||||
*.der
|
*.der
|
||||||
|
*.pem
|
||||||
|
*.db
|
||||||
|
*.bin
|
||||||
|
*.ks
|
||||||
|
*.session
|
||||||
|
*.convdb
|
||||||
|
*.convdb-shm
|
||||||
|
*.convdb-wal
|
||||||
|
*.pending.ks
|
||||||
qpq-server.toml
|
qpq-server.toml
|
||||||
|
|||||||
73
ROADMAP.md
73
ROADMAP.md
@@ -352,6 +352,79 @@ Long-term vision for wide adoption.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase 8 — Freifunk / Community Mesh Networking
|
||||||
|
|
||||||
|
Make qpq a first-class citizen on decentralised, community-operated wireless
|
||||||
|
networks (Freifunk, BATMAN-adv/Babel routing, OpenWrt). Multiple qpq nodes form
|
||||||
|
a federated mesh; clients auto-discover nearby nodes via mDNS; the network
|
||||||
|
functions without any central infrastructure or internet uplink.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Client A ─── mDNS discovery ──► nearby qpq node (LAN / mesh)
|
||||||
|
│
|
||||||
|
Cap'n Proto federation
|
||||||
|
│
|
||||||
|
remote qpq node (across mesh)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] **F0 — Re-include `quicproquo-p2p` in workspace; fix ALPN strings**
|
||||||
|
- Moved `crates/quicproquo-p2p` from `exclude` back into `[workspace] members`
|
||||||
|
- Fixed ALPN `b"quicnprotochat/p2p/1"` → `b"quicproquo/p2p/1"` (breaking wire change)
|
||||||
|
- Fixed federation ALPN `b"qnpc-fed"` → `b"quicproquo/federation/1"`
|
||||||
|
- Feature-gated behind `--features mesh` on client (keeps iroh out of default builds)
|
||||||
|
|
||||||
|
- [x] **F1 — Federation routing in message delivery**
|
||||||
|
- `handle_enqueue` and `handle_batch_enqueue` call `federation::routing::resolve_destination()`
|
||||||
|
- Recipients with a remote home server are relayed via `FederationClient::relay_enqueue()`
|
||||||
|
- mTLS mutual authentication between nodes (both present client certs, validated against shared CA)
|
||||||
|
- Config: `QPQ_FEDERATION_LISTEN`, `QPQ_LOCAL_DOMAIN`, `QPQ_FEDERATION_CERT/KEY/CA`
|
||||||
|
|
||||||
|
- [x] **F2 — mDNS local peer discovery**
|
||||||
|
- Server announces `_quicproquo._udp.local.` on startup via `mdns-sd`
|
||||||
|
- Client: `MeshDiscovery::start()` browses for nearby nodes (feature-gated)
|
||||||
|
- REPL commands: `/mesh peers` (scan + list), `/mesh server <host:port>` (note address)
|
||||||
|
- Nodes announce: `ver=1`, `server=<host:port>`, `domain=<local_domain>` TXT records
|
||||||
|
|
||||||
|
- [ ] **F3 — Self-sovereign mesh identity**
|
||||||
|
- Keypair = identity; OPAQUE password auth becomes optional (opt-in for managed deployments)
|
||||||
|
- `--mesh` startup mode: no AS required, nodes accept any verifiable keypair
|
||||||
|
- Bootstrap trust via out-of-band key fingerprint exchange (QR code or short code)
|
||||||
|
|
||||||
|
- [ ] **F4 — Store-and-forward with TTL**
|
||||||
|
- Add `ttl_secs: u32` to `Envelope` in `node.capnp`
|
||||||
|
- Relay nodes hold messages for offline peers up to TTL, then discard
|
||||||
|
- Gossip-style propagation: each hop decrements a hop counter
|
||||||
|
- Enables asynchronous messaging across intermittently connected mesh segments
|
||||||
|
|
||||||
|
- [ ] **F5 — Lightweight broadcast channels**
|
||||||
|
- No MLS overhead; symmetric group key distributed out-of-band
|
||||||
|
- Gossip delivery: node broadcasts to all peers, peers re-broadcast once
|
||||||
|
- Loop prevention via bloom filter on seen message IDs
|
||||||
|
- Suitable for community bulletin boards, emergency broadcasts on mesh
|
||||||
|
|
||||||
|
- [ ] **F6 — Extended `/mesh` REPL commands**
|
||||||
|
- `/mesh dm <fingerprint>` — direct message to peer by key fingerprint (P2P path)
|
||||||
|
- `/mesh broadcast <channel>` — publish to a symmetric broadcast channel
|
||||||
|
- `/mesh auto` — auto-select server with lowest RTT from discovered peers
|
||||||
|
- Auto-reconnect: if current server unreachable, fall back to next discovered peer
|
||||||
|
|
||||||
|
- [ ] **F7 — OpenWrt cross-compilation guide**
|
||||||
|
- Musl static builds: `x86_64-unknown-linux-musl`, `armv7-unknown-linux-musleabihf`, `mips-unknown-linux-musl`
|
||||||
|
- Strip binary: `--release` + `strip` → target size < 5 MB for flash storage
|
||||||
|
- `opkg` package manifest for OpenWrt feed
|
||||||
|
- `procd` init script + `uci` config file for OpenWrt integration
|
||||||
|
- CI job: cross-compile and size-check on every release tag
|
||||||
|
|
||||||
|
- [ ] **F8 — Traffic analysis resistance for mesh**
|
||||||
|
- Uniform message padding to nearest 256-byte boundary (hides message size)
|
||||||
|
- Configurable decoy traffic rate (fake messages to mask send timing)
|
||||||
|
- Optional onion routing: 3-hop relay through other mesh nodes (no Tor dependency)
|
||||||
|
- Ref: Phase 7.7 for server-side traffic analysis resistance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Summary Timeline
|
## Summary Timeline
|
||||||
|
|
||||||
| Phase | Focus | Estimated Effort |
|
| Phase | Focus | Estimated Effort |
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ WORKDIR /build
|
|||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY crates/quicproquo-core/Cargo.toml crates/quicproquo-core/Cargo.toml
|
COPY crates/quicproquo-core/Cargo.toml crates/quicproquo-core/Cargo.toml
|
||||||
COPY crates/quicproquo-proto/Cargo.toml crates/quicproquo-proto/Cargo.toml
|
COPY crates/quicproquo-proto/Cargo.toml crates/quicproquo-proto/Cargo.toml
|
||||||
COPY crates/qpq-server/Cargo.toml crates/qpq-server/Cargo.toml
|
COPY crates/quicproquo-server/Cargo.toml crates/quicproquo-server/Cargo.toml
|
||||||
COPY crates/quicproquo-client/Cargo.toml crates/quicproquo-client/Cargo.toml
|
COPY crates/quicproquo-client/Cargo.toml crates/quicproquo-client/Cargo.toml
|
||||||
COPY crates/quicproquo-p2p/Cargo.toml crates/quicproquo-p2p/Cargo.toml
|
COPY crates/quicproquo-p2p/Cargo.toml crates/quicproquo-p2p/Cargo.toml
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@ COPY crates/quicproquo-p2p/Cargo.toml crates/quicproquo-p2p/Cargo.toml
|
|||||||
RUN mkdir -p \
|
RUN mkdir -p \
|
||||||
crates/quicproquo-core/src \
|
crates/quicproquo-core/src \
|
||||||
crates/quicproquo-proto/src \
|
crates/quicproquo-proto/src \
|
||||||
crates/qpq-server/src \
|
crates/quicproquo-server/src \
|
||||||
crates/quicproquo-client/src \
|
crates/quicproquo-client/src \
|
||||||
crates/quicproquo-p2p/src \
|
crates/quicproquo-p2p/src \
|
||||||
&& echo 'fn main() {}' > crates/qpq-server/src/main.rs \
|
&& echo 'fn main() {}' > crates/quicproquo-server/src/main.rs \
|
||||||
&& echo 'fn main() {}' > crates/quicproquo-client/src/main.rs \
|
&& echo 'fn main() {}' > crates/quicproquo-client/src/main.rs \
|
||||||
&& touch crates/quicproquo-core/src/lib.rs \
|
&& touch crates/quicproquo-core/src/lib.rs \
|
||||||
&& touch crates/quicproquo-proto/src/lib.rs \
|
&& touch crates/quicproquo-proto/src/lib.rs \
|
||||||
@@ -46,7 +46,7 @@ RUN touch \
|
|||||||
crates/quicproquo-core/src/lib.rs \
|
crates/quicproquo-core/src/lib.rs \
|
||||||
crates/quicproquo-proto/src/lib.rs \
|
crates/quicproquo-proto/src/lib.rs \
|
||||||
crates/quicproquo-p2p/src/lib.rs \
|
crates/quicproquo-p2p/src/lib.rs \
|
||||||
crates/qpq-server/src/main.rs \
|
crates/quicproquo-server/src/main.rs \
|
||||||
crates/quicproquo-client/src/main.rs
|
crates/quicproquo-client/src/main.rs
|
||||||
|
|
||||||
RUN cargo build --release --bin qpq-server
|
RUN cargo build --release --bin qpq-server
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ WORKDIR /build
|
|||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY crates/quicproquo-core/Cargo.toml crates/quicproquo-core/Cargo.toml
|
COPY crates/quicproquo-core/Cargo.toml crates/quicproquo-core/Cargo.toml
|
||||||
COPY crates/quicproquo-proto/Cargo.toml crates/quicproquo-proto/Cargo.toml
|
COPY crates/quicproquo-proto/Cargo.toml crates/quicproquo-proto/Cargo.toml
|
||||||
COPY crates/qpq-server/Cargo.toml crates/qpq-server/Cargo.toml
|
COPY crates/quicproquo-server/Cargo.toml crates/quicproquo-server/Cargo.toml
|
||||||
COPY crates/quicproquo-client/Cargo.toml crates/quicproquo-client/Cargo.toml
|
COPY crates/quicproquo-client/Cargo.toml crates/quicproquo-client/Cargo.toml
|
||||||
COPY crates/quicproquo-p2p/Cargo.toml crates/quicproquo-p2p/Cargo.toml
|
COPY crates/quicproquo-p2p/Cargo.toml crates/quicproquo-p2p/Cargo.toml
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@ COPY crates/quicproquo-p2p/Cargo.toml crates/quicproquo-p2p/Cargo.toml
|
|||||||
RUN mkdir -p \
|
RUN mkdir -p \
|
||||||
crates/quicproquo-core/src \
|
crates/quicproquo-core/src \
|
||||||
crates/quicproquo-proto/src \
|
crates/quicproquo-proto/src \
|
||||||
crates/qpq-server/src \
|
crates/quicproquo-server/src \
|
||||||
crates/quicproquo-client/src \
|
crates/quicproquo-client/src \
|
||||||
crates/quicproquo-p2p/src \
|
crates/quicproquo-p2p/src \
|
||||||
&& echo 'fn main() {}' > crates/qpq-server/src/main.rs \
|
&& echo 'fn main() {}' > crates/quicproquo-server/src/main.rs \
|
||||||
&& echo 'fn main() {}' > crates/quicproquo-client/src/main.rs \
|
&& echo 'fn main() {}' > crates/quicproquo-client/src/main.rs \
|
||||||
&& touch crates/quicproquo-core/src/lib.rs \
|
&& touch crates/quicproquo-core/src/lib.rs \
|
||||||
&& touch crates/quicproquo-proto/src/lib.rs \
|
&& touch crates/quicproquo-proto/src/lib.rs \
|
||||||
@@ -47,7 +47,7 @@ RUN touch \
|
|||||||
crates/quicproquo-core/src/lib.rs \
|
crates/quicproquo-core/src/lib.rs \
|
||||||
crates/quicproquo-proto/src/lib.rs \
|
crates/quicproquo-proto/src/lib.rs \
|
||||||
crates/quicproquo-p2p/src/lib.rs \
|
crates/quicproquo-p2p/src/lib.rs \
|
||||||
crates/qpq-server/src/main.rs \
|
crates/quicproquo-server/src/main.rs \
|
||||||
crates/quicproquo-client/src/main.rs
|
crates/quicproquo-client/src/main.rs
|
||||||
|
|
||||||
RUN cargo build --release --bin qpq-server --bin qpq
|
RUN cargo build --release --bin qpq-server --bin qpq
|
||||||
|
|||||||
797
scripts/ai_team.py
Executable file
797
scripts/ai_team.py
Executable file
@@ -0,0 +1,797 @@
|
|||||||
|
|
||||||
|
#!/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())
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/chat-test.sh
|
# ./scripts/chat-test.sh
|
||||||
#
|
#
|
||||||
# Requirements: docker, docker compose, tmux
|
# Requirements: docker (or podman + podman-compose), tmux
|
||||||
# Exit: Ctrl+D in both panes, or: tmux kill-session -t qpc-chat
|
# Exit: Ctrl+D in both panes, or: tmux kill-session -t qpc-chat
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -15,7 +15,6 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
COMPOSE_FILE="$PROJECT_ROOT/docker/docker-compose.chat-test.yml"
|
COMPOSE_FILE="$PROJECT_ROOT/docker/docker-compose.chat-test.yml"
|
||||||
COMPOSE="docker compose -f $COMPOSE_FILE -p qpc-chat-test"
|
|
||||||
|
|
||||||
# ── Colors ────────────────────────────────────────────────────────────────────
|
# ── Colors ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -27,6 +26,37 @@ step() { echo -e "${GREEN}==> $1${NC}"; }
|
|||||||
info() { echo -e " ${CYAN}$1${NC}"; }
|
info() { echo -e " ${CYAN}$1${NC}"; }
|
||||||
error() { echo -e "${RED}ERROR: $1${NC}" >&2; }
|
error() { echo -e "${RED}ERROR: $1${NC}" >&2; }
|
||||||
|
|
||||||
|
# ── Preflight checks ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Prefer docker; fall back to podman.
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
if ! docker compose version &>/dev/null; then
|
||||||
|
error "docker compose (v2 plugin) is required."
|
||||||
|
echo " See: https://docs.docker.com/compose/install/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
COMPOSE_BIN="docker compose"
|
||||||
|
elif command -v podman &>/dev/null; then
|
||||||
|
if ! command -v podman-compose &>/dev/null; then
|
||||||
|
error "podman-compose is required when using podman."
|
||||||
|
echo " pip install podman-compose"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
COMPOSE_BIN="podman-compose"
|
||||||
|
else
|
||||||
|
error "docker or podman is required but neither is installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMPOSE="$COMPOSE_BIN -f $COMPOSE_FILE -p qpc-chat-test"
|
||||||
|
|
||||||
|
if ! command -v tmux &>/dev/null; then
|
||||||
|
error "tmux is required but not installed."
|
||||||
|
echo " macOS: brew install tmux"
|
||||||
|
echo " Linux: sudo apt-get install tmux"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Cleanup on exit ──────────────────────────────────────────────────────────
|
# ── Cleanup on exit ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
@@ -36,26 +66,6 @@ cleanup() {
|
|||||||
}
|
}
|
||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
# ── Preflight checks ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
if ! command -v docker &>/dev/null; then
|
|
||||||
error "docker is required but not installed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! docker compose version &>/dev/null; then
|
|
||||||
error "docker compose (v2 plugin) is required."
|
|
||||||
echo " See: https://docs.docker.com/compose/install/"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v tmux &>/dev/null; then
|
|
||||||
error "tmux is required but not installed."
|
|
||||||
echo " macOS: brew install tmux"
|
|
||||||
echo " Linux: sudo apt-get install tmux"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Step 1: Build ─────────────────────────────────────────────────────────────
|
# ── Step 1: Build ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
step "Building Docker image (server + client)..."
|
step "Building Docker image (server + client)..."
|
||||||
|
|||||||
336
scripts/dev-shell.sh
Executable file
336
scripts/dev-shell.sh
Executable file
@@ -0,0 +1,336 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ── qpq Dev Shell ─────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Builds qpq (if needed), starts a local server, registers Alice + Bob, then
|
||||||
|
# opens a tmux session with two side-by-side REPL panes and a server-log strip.
|
||||||
|
#
|
||||||
|
# Layout (window 0 — "chat"):
|
||||||
|
#
|
||||||
|
# ┌──────────[ ALICE user=alice pass=alice ]──┬──[ BOB user=bob pass=bob ]──┐
|
||||||
|
# │ │ │
|
||||||
|
# │ /dm bob ← start a DM here │ reply here │
|
||||||
|
# │ /create-group g ← or create a group │ /join ← to accept invite │
|
||||||
|
# │ │ │
|
||||||
|
# ├──────────[ SERVER LOG ]────────────────────┴──────────────────────────────┤
|
||||||
|
# │ live qpq-server stdout / stderr │
|
||||||
|
# └───────────────────────────────────────────────────────────────────────────┘
|
||||||
|
#
|
||||||
|
# Window 1 — "ref": full slash-command cheatsheet (read-only)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/dev-shell.sh build if needed, fresh session
|
||||||
|
# ./scripts/dev-shell.sh --rebuild force cargo build first
|
||||||
|
# ./scripts/dev-shell.sh --resume reuse existing state files + server data
|
||||||
|
# ./scripts/dev-shell.sh --help show this message
|
||||||
|
#
|
||||||
|
# Stop: Ctrl-C here OR tmux kill-session -t qpq-dev
|
||||||
|
# Ref: Ctrl-B 1 (inside tmux → cheatsheet window)
|
||||||
|
# Nav: Ctrl-B ←/→ (switch Alice ↔ Bob pane)
|
||||||
|
# Ctrl-B z (zoom current pane)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
BIN_DIR="$PROJECT_ROOT/target/debug"
|
||||||
|
SESSION="qpq-dev"
|
||||||
|
SERVER_PORT=7000
|
||||||
|
SERVER_ADDR="127.0.0.1:$SERVER_PORT"
|
||||||
|
SERVER_NAME="localhost"
|
||||||
|
|
||||||
|
# All runtime state lives in /tmp so the project tree stays clean
|
||||||
|
RUN_DIR="/tmp/qpq-devshell"
|
||||||
|
DATA_DIR="$RUN_DIR/server-data" # server stores TLS cert + OPAQUE data here
|
||||||
|
CA_CERT="$DATA_DIR/server-cert.der"
|
||||||
|
LOG_FILE="$RUN_DIR/server.log"
|
||||||
|
|
||||||
|
QPQ="$BIN_DIR/qpq"
|
||||||
|
QPQS="$BIN_DIR/qpq-server"
|
||||||
|
|
||||||
|
# ── Colours ────────────────────────────────────────────────────────────────────
|
||||||
|
GRN='\033[0;32m'; CYN='\033[0;36m'; YLW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
step() { printf "\n${GRN}▶ %s${NC}\n" "$*"; }
|
||||||
|
info() { printf " ${CYN}%s${NC}\n" "$*"; }
|
||||||
|
warn() { printf " ${YLW}⚠ %s${NC}\n" "$*"; }
|
||||||
|
die() { printf "${RED}✗ %s${NC}\n" "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── Parse flags ────────────────────────────────────────────────────────────────
|
||||||
|
REBUILD=false
|
||||||
|
RESUME=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-r|--rebuild) REBUILD=true ;;
|
||||||
|
--resume) RESUME=true ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,/^[^#]/{ /^#/p }' "$0" | sed 's/^# \?//'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) die "Unknown argument: $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Preflight ──────────────────────────────────────────────────────────────────
|
||||||
|
step "Checking requirements..."
|
||||||
|
for cmd in cargo tmux; do
|
||||||
|
command -v "$cmd" &>/dev/null || die "'$cmd' is required but not installed."
|
||||||
|
done
|
||||||
|
info "cargo $(cargo --version 2>&1 | head -1)"
|
||||||
|
info "tmux $(tmux -V)"
|
||||||
|
|
||||||
|
# ── Decide whether to clean state ─────────────────────────────────────────────
|
||||||
|
# By default we start fresh (server is always restarted, state must match).
|
||||||
|
# Pass --resume to reuse an existing consistent state from a previous run.
|
||||||
|
if $RESUME; then
|
||||||
|
info "Resume mode — keeping existing state in $RUN_DIR"
|
||||||
|
[[ -d "$RUN_DIR" ]] || die "--resume requires a previous dev-shell run (no $RUN_DIR)"
|
||||||
|
else
|
||||||
|
step "Cleaning previous run state..."
|
||||||
|
rm -rf "$RUN_DIR"
|
||||||
|
info "Cleared $RUN_DIR"
|
||||||
|
fi
|
||||||
|
mkdir -p "$RUN_DIR" "$DATA_DIR"
|
||||||
|
|
||||||
|
# ── Build ──────────────────────────────────────────────────────────────────────
|
||||||
|
if $REBUILD || [[ ! -x "$QPQ" ]] || [[ ! -x "$QPQS" ]]; then
|
||||||
|
step "Building workspace (cargo build)..."
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
cargo build
|
||||||
|
info "Build complete."
|
||||||
|
else
|
||||||
|
info "Using cached binaries in $BIN_DIR"
|
||||||
|
info "(pass --rebuild to recompile)"
|
||||||
|
fi
|
||||||
|
[[ -x "$QPQ" ]] || die "Client binary not found: $QPQ"
|
||||||
|
[[ -x "$QPQS" ]] || die "Server binary not found: $QPQS"
|
||||||
|
|
||||||
|
# ── Free the port ──────────────────────────────────────────────────────────────
|
||||||
|
step "Ensuring port $SERVER_PORT is free..."
|
||||||
|
SERVER_PID=""
|
||||||
|
|
||||||
|
free_port() {
|
||||||
|
if command -v fuser &>/dev/null; then
|
||||||
|
fuser -k "${SERVER_PORT}/tcp" 2>/dev/null || true
|
||||||
|
elif command -v lsof &>/dev/null; then
|
||||||
|
local pids
|
||||||
|
pids=$(lsof -ti "tcp:${SERVER_PORT}" 2>/dev/null || true)
|
||||||
|
[[ -n "$pids" ]] && kill $pids 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
free_port
|
||||||
|
sleep 0.3
|
||||||
|
|
||||||
|
# ── Cleanup on exit ────────────────────────────────────────────────────────────
|
||||||
|
cleanup() {
|
||||||
|
printf "\n"
|
||||||
|
step "Shutting down..."
|
||||||
|
tmux kill-session -t "$SESSION" 2>/dev/null || true
|
||||||
|
if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
info "Stopping qpq-server (PID $SERVER_PID)..."
|
||||||
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
free_port
|
||||||
|
info "Done."
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# ── Start server ───────────────────────────────────────────────────────────────
|
||||||
|
step "Starting qpq-server on $SERVER_ADDR..."
|
||||||
|
"$QPQS" \
|
||||||
|
--listen "$SERVER_ADDR" \
|
||||||
|
--data-dir "$DATA_DIR" \
|
||||||
|
--tls-cert "$DATA_DIR/server-cert.der" \
|
||||||
|
--tls-key "$DATA_DIR/server-key.der" \
|
||||||
|
--allow-insecure-auth \
|
||||||
|
>"$LOG_FILE" 2>&1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
info "PID $SERVER_PID log → $LOG_FILE"
|
||||||
|
|
||||||
|
# ── Wait for TLS cert (written by server on first boot) ────────────────────────
|
||||||
|
step "Waiting for server to initialise..."
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
if [[ -f "$CA_CERT" ]]; then
|
||||||
|
info "Server ready after ${i}s (cert present)."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
warn "Server exited early. Last log output:"
|
||||||
|
tail -30 "$LOG_FILE" >&2
|
||||||
|
die "Server failed to start."
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
if [[ $i -eq 20 ]]; then
|
||||||
|
tail -30 "$LOG_FILE" >&2
|
||||||
|
die "Server did not produce TLS cert within 20s."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
sleep 0.5 # brief pause for the QUIC listener to open
|
||||||
|
|
||||||
|
QPQ_GLOBAL=(--ca-cert "$CA_CERT" --server-name "$SERVER_NAME")
|
||||||
|
|
||||||
|
# ── Build REPL command strings ─────────────────────────────────────────────────
|
||||||
|
# Registration is handled automatically by the REPL on first launch:
|
||||||
|
# 1. load_or_init_state creates state file + identity key (if missing)
|
||||||
|
# 2. opaque_register sends the identity key → server binds username→identity_key
|
||||||
|
# 3. opaque_login returns a session token → cached for future runs
|
||||||
|
# Pre-registering here (without an identity key) would break /dm by preventing
|
||||||
|
# the identity key from ever being bound on a subsequent REPL launch.
|
||||||
|
repl_cmd() {
|
||||||
|
local user="$1" pass="$2"
|
||||||
|
echo "$QPQ ${QPQ_GLOBAL[*]} repl \
|
||||||
|
--state $RUN_DIR/${user}.bin \
|
||||||
|
--server $SERVER_ADDR \
|
||||||
|
--username $user \
|
||||||
|
--password $pass"
|
||||||
|
}
|
||||||
|
|
||||||
|
ALICE_CMD=$(repl_cmd alice alice)
|
||||||
|
BOB_CMD=$(repl_cmd bob bob)
|
||||||
|
|
||||||
|
# ── Build tmux session ─────────────────────────────────────────────────────────
|
||||||
|
step "Creating tmux session '$SESSION'..."
|
||||||
|
tmux kill-session -t "$SESSION" 2>/dev/null || true
|
||||||
|
|
||||||
|
# ─ Window 0 "chat" layout:
|
||||||
|
# top-left → Alice REPL
|
||||||
|
# top-right → Bob REPL
|
||||||
|
# bottom → server log (full width, 30% height)
|
||||||
|
#
|
||||||
|
# tmux 3.x renumbers pane indices after each split, so we capture pane IDs
|
||||||
|
# with -P -F '#{pane_id}' instead of relying on 0.0 / 0.1 / 0.2 arithmetic.
|
||||||
|
|
||||||
|
tmux new-session -d -s "$SESSION" -n "chat" -x 220 -y 55
|
||||||
|
|
||||||
|
# The initial pane is always %0 / pane 0 — that's Alice.
|
||||||
|
PANE_ALICE="${SESSION}:0.0"
|
||||||
|
|
||||||
|
# Split bottom strip for server log; capture the new pane's stable ID.
|
||||||
|
PANE_LOG=$(tmux split-window -v -t "$PANE_ALICE" -p 30 -P -F '#{pane_id}')
|
||||||
|
|
||||||
|
# Split top-right for Bob from Alice's pane; capture ID.
|
||||||
|
tmux select-pane -t "$PANE_ALICE"
|
||||||
|
PANE_BOB=$(tmux split-window -h -t "$PANE_ALICE" -P -F '#{pane_id}')
|
||||||
|
|
||||||
|
# Send commands to each pane by stable ID — immune to index renumbering.
|
||||||
|
tmux send-keys -t "$PANE_LOG" \
|
||||||
|
"printf '\\033[0;36m[server log]\\033[0m\\n' && tail -F '$LOG_FILE'" \
|
||||||
|
Enter
|
||||||
|
|
||||||
|
tmux send-keys -t "$PANE_BOB" \
|
||||||
|
"sleep 1.5 && $BOB_CMD" \
|
||||||
|
Enter
|
||||||
|
|
||||||
|
tmux send-keys -t "$PANE_ALICE" \
|
||||||
|
"sleep 0.8 && $ALICE_CMD" \
|
||||||
|
Enter
|
||||||
|
|
||||||
|
# Pane border labels (tmux ≥ 2.6)
|
||||||
|
tmux select-pane -t "$PANE_ALICE" -T " ✉ ALICE │ user=alice pass=alice "
|
||||||
|
tmux select-pane -t "$PANE_LOG" -T " ⚙ SERVER LOG "
|
||||||
|
tmux select-pane -t "$PANE_BOB" -T " ✉ BOB │ user=bob pass=bob "
|
||||||
|
|
||||||
|
tmux set-option -t "$SESSION" pane-border-status top 2>/dev/null || true
|
||||||
|
tmux set-option -t "$SESSION" \
|
||||||
|
pane-border-format \
|
||||||
|
"#{?pane_active,#[bold fg=colour226],#[fg=colour244]} #{pane_title} " \
|
||||||
|
2>/dev/null || true
|
||||||
|
|
||||||
|
# Focus Alice to start
|
||||||
|
tmux select-pane -t "$PANE_ALICE"
|
||||||
|
|
||||||
|
# ─ Window 1 "ref" — slash-command cheatsheet ──────────────────────────────────
|
||||||
|
tmux new-window -t "${SESSION}:1" -n "ref"
|
||||||
|
tmux send-keys -t "${SESSION}:1" "clear" Enter
|
||||||
|
# Heredoc piped through cat so it renders immediately and stays visible
|
||||||
|
tmux send-keys -t "${SESSION}:1" "cat << 'CHEAT'
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ qpq REPL ─ Slash Command Cheatsheet ║
|
||||||
|
╠══════════════════════════════════════════════════════════════════════════╣
|
||||||
|
║ GENERAL ║
|
||||||
|
║ /help show all commands in the REPL ║
|
||||||
|
║ /whoami identity key + hybrid key fingerprint ║
|
||||||
|
║ /quit /q /exit exit the REPL ║
|
||||||
|
║ ║
|
||||||
|
║ CONVERSATIONS ║
|
||||||
|
║ /list /ls list all open conversations ║
|
||||||
|
║ /switch @username make a DM the active conversation ║
|
||||||
|
║ /switch #groupname make a group the active conversation ║
|
||||||
|
║ /history [N] print last N messages (default: 20) ║
|
||||||
|
║ /members list all members of the current conv. ║
|
||||||
|
║ ║
|
||||||
|
║ DIRECT MESSAGES ║
|
||||||
|
║ /dm <username> open or create an encrypted 1:1 DM ║
|
||||||
|
║ ║
|
||||||
|
║ MLS GROUPS ║
|
||||||
|
║ /create-group <name> create a new MLS group (you are admin) ║
|
||||||
|
║ /cg <name> alias for /create-group ║
|
||||||
|
║ /invite <username> invite someone into the current group ║
|
||||||
|
║ /join accept a pending group Welcome message ║
|
||||||
|
║ /leave leave the currently active group ║
|
||||||
|
║ /remove <username> remove (kick) a member from the group ║
|
||||||
|
║ /kick <username> alias for /remove ║
|
||||||
|
║ ║
|
||||||
|
╠══════════════════════════════════════════════════════════════════════════╣
|
||||||
|
║ QUICK START — 1:1 DM TEST ║
|
||||||
|
║ ║
|
||||||
|
║ [Alice] /dm bob creates encrypted DM channel ║
|
||||||
|
║ [Bob] (Welcome arrives) background poller picks it up auto ║
|
||||||
|
║ [Alice] Hello Bob! send your first message ║
|
||||||
|
║ [Bob] Hey Alice! reply ║
|
||||||
|
║ [Alice] /history verify messages are stored ║
|
||||||
|
║ [Alice] /whoami check identity + hybrid key status ║
|
||||||
|
║ ║
|
||||||
|
║ QUICK START — GROUP CHAT TEST ║
|
||||||
|
║ ║
|
||||||
|
║ [Alice] /create-group devtest create an MLS group ║
|
||||||
|
║ [Alice] /invite bob send a Welcome to Bob ║
|
||||||
|
║ [Bob] /join accept the Welcome ║
|
||||||
|
║ [Alice] Hello everyone! send to group ║
|
||||||
|
║ [Bob] Hi Alice! reply in group ║
|
||||||
|
║ [Alice] /members verify both Alice + Bob listed ║
|
||||||
|
║ [Alice] /history 50 dump full message log ║
|
||||||
|
║ [Alice] /remove bob kick Bob (test admin ops) ║
|
||||||
|
║ [Bob] (removed from group) ║
|
||||||
|
║ ║
|
||||||
|
╠══════════════════════════════════════════════════════════════════════════╣
|
||||||
|
║ TMUX NAVIGATION ║
|
||||||
|
║ Ctrl-B 0 window 0 — chat panes (Alice / Bob / log) ║
|
||||||
|
║ Ctrl-B 1 window 1 — this cheatsheet ║
|
||||||
|
║ Ctrl-B ← → move between panes in the chat window ║
|
||||||
|
║ Ctrl-B z zoom current pane to fullscreen (toggle) ║
|
||||||
|
║ Ctrl-B [ scroll mode — use arrows / PgUp/PgDn (q exits) ║
|
||||||
|
║ Ctrl-B d detach (session stays alive in background) ║
|
||||||
|
║ ║
|
||||||
|
║ EXIT / STOP ║
|
||||||
|
║ Ctrl-B :kill-session Enter kill tmux + triggers script cleanup ║
|
||||||
|
║ tmux kill-session -t qpq-dev from any other terminal ║
|
||||||
|
║ /quit (in Alice or Bob pane) exit that REPL only ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════╝
|
||||||
|
CHEAT" Enter
|
||||||
|
|
||||||
|
# Return focus to the chat window, Alice pane
|
||||||
|
tmux select-window -t "${SESSION}:0"
|
||||||
|
tmux select-pane -t "${SESSION}:0.0"
|
||||||
|
|
||||||
|
# ── Print startup summary ──────────────────────────────────────────────────────
|
||||||
|
printf "\n"
|
||||||
|
printf "${GRN}╔══════════════════════════════════════════════════════╗${NC}\n"
|
||||||
|
printf "${GRN}║${NC} ${GRN}qpq dev shell — ready${NC} ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}╠══════════════════════════════════════════════════════╣${NC}\n"
|
||||||
|
printf "${GRN}║${NC} Session ${CYN}%s${NC}\n" "$SESSION ${GRN}║${NC}"
|
||||||
|
printf "${GRN}║${NC} Server ${CYN}%s${NC}\n" "$SERVER_ADDR (log → $LOG_FILE) ${GRN}║${NC}"
|
||||||
|
printf "${GRN}║${NC} Alice user=${CYN}alice${NC} pass=${CYN}alice${NC} ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}║${NC} Bob user=${CYN}bob${NC} pass=${CYN}bob${NC} ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}║${NC} ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}║${NC} Quick DM: ${CYN}[Alice pane]${NC} type: /dm bob ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}║${NC} Cheatsheet: Ctrl-B 1 (inside tmux) ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}║${NC} Exit: Ctrl-B :kill-session Enter ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}║${NC} or from another terminal: ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}║${NC} tmux kill-session -t qpq-dev ${GRN}║${NC}\n"
|
||||||
|
printf "${GRN}╚══════════════════════════════════════════════════════╝${NC}\n"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# ── Attach ─────────────────────────────────────────────────────────────────────
|
||||||
|
tmux attach-session -t "$SESSION"
|
||||||
|
|
||||||
|
step "Dev shell exited."
|
||||||
Reference in New Issue
Block a user