pea is an open source orchestrator for lightweight AI agents in software development, built around SDD (Specs–Design–Development) and designed to make the SDLC more governable, reviewable, and traceable.
We aim to close the gap between "vibe coding" and a truly professional agent implementation: agents that don't just generate code, but produce documentation, quality gates, and clear evidence of how decisions are made together with the human (human-in-the-loop), so every change can be reviewed with technical criteria, auditability, and accountability.
Our goal is genuine: contribute to an ethical and responsible adoption of AI agents — friendly and accessible for all types of organizations, from small teams to companies with strict governance processes.
pea is in active development and testing. It's not perfect yet, but we're building it with passion and continuous improvement. If you use it and test it, your feedback (issues, logs, PRs) directly helps us detect bugs, fix them, and strengthen the tool for the community.
Most AI agent tools focus on how fast code gets generated. pea focuses on how reliably it gets delivered:
- Spec-first: every run starts from a human-authored
SRS.md. Agents don't invent requirements. - Governed pipeline: 8 specialized agents, 4 mandatory human checkpoints. The pipeline stops until a human approves.
- Zero auto-retries:
max_auto_retries = 0. Nothing runs again automatically — every retry is an explicit human decision. - Full traceability: each agent phase records
prompt_ref,prompt_hash(SHA-256),claude_model, timestamp, and exit code. You can always audit what ran and when. - Quality gates built in: QA (ruff + eslint) and Security (pip-audit + npm audit) run as separate agents. A
FAILreport pauses the pipeline and waits for a human.
┌─────────────────────────────────────────────────────┐
│ pea Orchestrator (namespace: pea-system) │
│ │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────┐ │
│ │ FastAPI │ │ State │ │ Job │ │
│ │ Backend │◄►│ Machine │◄►│ Manager │ │
│ │ :8000 │ │ (10 states) │ │ (K8s) │ │
│ └──────┬───────┘ └───────────────┘ └──────────┘ │
│ │ │
│ ┌──────▼───────┐ ┌───────────────┐ │
│ │ Angular │ │ PostgreSQL │ │
│ │ Frontend │ │ (PVC) │ │
│ │ (GUI) │ │ │ │
│ └──────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────┘
│ creates K8s Jobs
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ 8-phase pipeline (namespace: pod-XX) │
│ │
│ ┌───────────┐ ┌───────────┐ ┌──────────────────┐ ┌───────────┐ │
│ │pm-analyst │───►│ architect │───►│ guardrails-design│───►│documenta- │ │
│ │ [K8s Job] │[*] │ [K8s Job] │[*] │ [K8s Job] │[*] │tion[Job] │ │
│ └───────────┘ └───────────┘ └──────────────────┘ └───────────┘ │
│ │ [*] │
│ ┌───────────┐ ┌───────────┐ ┌──────────────────┐ ▼ │
│ │pr-release │◄───│ security │◄───│ qa │◄──────[dev] │
│ │ [K8s Job] │ │ [K8s Job] │ │ [K8s Job] │ [Job] │
│ └───────────┘ └───────────┘ └──────────────────┘ │
│ │
│ [*] = mandatory human checkpoint — pipeline pauses at WAITING_HUMAN │
│ every Job: backoffLimit=0 restartPolicy=Never max_auto_retries=0 │
└───────────────────────────────────────────────────────────────────────────┘
INIT
└─► REQUIREMENTS_READY ─► WAITING_HUMAN ─► DESIGN_READY
└─► WAITING_HUMAN ─► DOCS_VALIDATED
└─► WAITING_HUMAN ─► CODE_READY
└─► WAITING_HUMAN ─► VALIDATED
└─► PR_READY ─► COMPLETED ─► ARCHIVED
Each WAITING_HUMAN state requires an explicit human action: approve (continue) or reject (archive the run). A QA or Security FAIL also transitions to WAITING_HUMAN, letting the operator order a dev retry.
| Component | Technology | Version |
|---|---|---|
| Backend | Python + FastAPI + Uvicorn | 3.11 / 0.115.0 / 0.30.0 |
| ORM + migrations | SQLAlchemy + Alembic | 2.0.31 / 1.13.2 |
| Database | PostgreSQL 15 (PVC in K8s) | 15-alpine |
| Frontend | Angular (checkpoint GUI) | see orchestrator/frontend/ |
| CLI | HTTP wrapper — Typer + Rich | 0.12.3 / 13.7.1 |
| Orchestration | Kubernetes Jobs (no CRDs) | kubernetes-python 30.1.0 |
| Environment | Conda (environment.yml) |
>= 23 |
| AI model | Claude Code CLI | claude-sonnet-4-6 |
| Validation | Pydantic v2 | 2.8.2 |
| Component | Technology |
|---|---|
| Frontend | React |
| Backend | Python FastAPI |
| Database | SQLite |
| Agent runtime | Claude Code CLI (--dangerously-skip-permissions) |
Each phase is a Kubernetes Job running Claude Code CLI with a specialized prompt. Work persists on a shared PVC (workspace-{run_id}).
| # | Agent | Role | Inputs | Outputs | Checkpoint |
|---|---|---|---|---|---|
| 1 | pm-analyst |
Product Manager Analyst | spec/SRS.md |
spec/SRS.md (refined), artifacts/reports/pm_summary.md |
YES |
| 2 | architect |
Software Architect | spec/SRS.md, pm_summary.md |
docs/sdd/SDD-*.md, docs/adr/ADR-*.md |
YES |
| 3 | guardrails-design |
Design Reviewer | docs/sdd/*.md, docs/adr/*.md |
artifacts/reports/design_check.json |
YES |
| 4 | documentation |
Documentation Engineer | Approved design docs | docs/ (docs-as-code), artifacts/reports/docs_check.json |
YES |
| 5 | dev |
Full-Stack Developer | All approved documentation | React + FastAPI + SQLite source code | NO |
| 6 | qa |
QA Engineer | Source code (backend/, frontend/) |
artifacts/reports/qa_report.json |
NO |
| 7 | security |
Security Engineer | requirements.txt, package.json |
artifacts/reports/sec_report.json |
NO |
| 8 | pr-release |
Release Manager | Fully validated repo | Pull Request created on GitHub | NO |
Phases pm-analyst, architect, guardrails-design, and documentation create a Checkpoint with status PENDING upon completion. The pipeline transitions to WAITING_HUMAN and does not advance until the operator approves or rejects.
If qa or security report status: "FAIL", the Orchestrator:
- Transitions to
WAITING_HUMAN - The operator can order a
devretry viaPOST /api/v1/runs/{id}/phases/dev/retry - The pipeline resumes:
dev→qa→security→pr-release
| Tool | Min version | Purpose |
|---|---|---|
| Minikube | 1.32 | Local K8s cluster (or any compatible cluster) |
| Docker | 24 | Build agent images |
| Conda (Miniconda/Anaconda) | 23 | Orchestrator Python environment |
| Node.js | 20 LTS | Angular frontend + Claude Code CLI |
| kubectl | cluster-compatible | K8s resource management |
| Python | 3.11 | Orchestrator runtime |
git clone <repo-url> pea
cd peaminikube start --memory=4g --cpus=4orchestrator/environment.yml declares all Python dependencies (FastAPI, SQLAlchemy, kubernetes-python, Typer, etc.).
conda env create -f orchestrator/environment.yml
conda activate pea-orchestratorTo update after changes to environment.yml:
conda env update -f orchestrator/environment.yml --prunepip install -e orchestrator/This registers pea-cli as a shell command in your active Conda environment.
kubectl apply -f orchestrator/k8s/namespace.yaml
kubectl apply -f orchestrator/k8s/pvc.yaml
kubectl apply -f orchestrator/k8s/rbac.yaml
# Create the PostgreSQL password secret
kubectl create secret generic orchestrator-secrets \
--from-literal=POSTGRES_PASSWORD=<your-secure-password> \
-n pea-system
# Deploy PostgreSQL + Orchestrator
kubectl apply -f orchestrator/k8s/deployment-orchestrator.yamlCredentials are mounted at /run/secrets/credentials.env inside each K8s Job. Claude Code is configured with deny: ["Read(/run/secrets/*)", "Read(.env*)"] to prevent agents from reading their own credentials.
# Copy the template and fill in real values
cp k8s/secrets/agent-credentials.yaml.template k8s/secrets/agent-credentials.yaml
# Edit agent-credentials.yaml — set real values for:
# ANTHROPIC_API_KEY=sk-ant-...
# GIT_USER_NAME=...
# GIT_USER_EMAIL=...
# GIT_TOKEN=...
# GIT_REPO_URL=https://github.com/your-org/your-repo.git
# GIT_DEFAULT_BRANCH=main
# Apply in the run namespace (created automatically by the Orchestrator)
kubectl apply -f k8s/secrets/agent-credentials.yaml -n pod-<POD_ID>IMPORTANT:
k8s/secrets/agent-credentials.yaml(without.template) is in.gitignore. Never commit this file with real credentials.
# Point Docker to the Minikube daemon (avoids pushing to an external registry)
eval $(minikube docker-env)
# Build base image (python:3.11-slim + Node.js 20 + Claude Code CLI)
docker build -f images/base/Dockerfile.base -t pea/base:latest images/base/
# Build all 8 agent images
for agent in pm-analyst architect guardrails-design documentation dev qa security pr-release; do
docker build -t pea/${agent}:latest images/${agent}/
doneconda activate pea-orchestrator
# Port-forward PostgreSQL if it's running in K8s
kubectl port-forward svc/postgres-service 5432:5432 -n pea-system &
# Set environment variables
export DATABASE_URL="postgresql://pea:<password>@localhost:5432/pea_orchestrator"
export KUBECONFIG="$HOME/.kube/config"
export CLAUDE_MODEL="claude-sonnet-4-6"
# Start the server
uvicorn orchestrator.src.api.app:app --host 0.0.0.0 --port 8000 --reloadcd orchestrator/frontend
npm install
ng serve --port 4200
# Open http://localhost:4200The Angular frontend shows active runs, pending checkpoints, and the "Go" button to approve and continue the pipeline.
# Via CLI
pea-cli runs create --srs-path spec/SRS.md --project-name "My Web App"
# Via API
curl -X POST http://localhost:8000/api/v1/runs \
-H "Content-Type: application/json" \
-d '{"srs_path": "spec/SRS.md", "project_name": "My Web App"}'SRS.md must exist in the shared workspace (PVC). The response includes the run id.
pea-cli runs status <run-id>
# Via API
curl http://localhost:8000/api/v1/runs/<run-id>The response includes state (current RunState), a list of phases (with status, exit_code, prompt_hash), and any pending or resolved checkpoints.
The pipeline pauses automatically at WAITING_HUMAN after checkpoint phases. The operator reviews the generated artifacts and decides:
# Approve — continues to the next agent
pea-cli runs approve <run-id>
pea-cli runs approve <run-id> --notes "SRS reviewed, approved with minor observations"
# Reject — archives the run (ARCHIVED state, terminal)
pea-cli runs reject <run-id> --reason "Incomplete requirements, needs client review"Via Angular GUI: "Go" button (approve) or "Reject" on the active checkpoint panel.
Via API:
# Approve
curl -X POST http://localhost:8000/api/v1/runs/<run-id>/checkpoint/approve \
-H "Content-Type: application/json" \
-d '{"notes": "Approved"}'
# Reject
curl -X POST http://localhost:8000/api/v1/runs/<run-id>/checkpoint/reject \
-H "Content-Type: application/json" \
-d '{"reason": "Rejected — incomplete design"}'When QA or Security report FAIL, the run enters WAITING_HUMAN. To relaunch the dev → qa → security → pr-release cycle:
pea-cli runs retry-dev <run-id>
pea-cli runs retry-dev <run-id> --notes "Linting fixes applied manually"
# Via API
curl -X POST http://localhost:8000/api/v1/runs/<run-id>/phases/dev/retry \
-H "Content-Type: application/json" \
-d '{"notes": "Fixes applied"}'pea-cli runs logs <run-id>
pea-cli runs logs <run-id> --phase qa
pea-cli runs artifacts <run-id>| Method | Endpoint | Description | Expected status |
|---|---|---|---|
POST |
/api/v1/runs |
Create a new run, launches pm-analyst | 201 Created |
GET |
/api/v1/runs/{id} |
Full state (run + phases + checkpoints) | 200 OK |
POST |
/api/v1/runs/{id}/checkpoint/approve |
Approve pending checkpoint | 200 OK |
POST |
/api/v1/runs/{id}/checkpoint/reject |
Reject checkpoint → ARCHIVED | 200 OK |
POST |
/api/v1/runs/{id}/phases/dev/retry |
Retry dev from WAITING_HUMAN | 202 Accepted |
GET |
/api/v1/runs/{id}/logs |
Execution logs (filterable by phase) | 200 OK |
GET |
/api/v1/runs/{id}/artifacts |
Artifacts generated in workspace | 200 OK |
GET |
/health |
Health check (liveness/readiness probe) | 200 OK |
Interactive docs available at http://localhost:8000/docs (Swagger UI) and http://localhost:8000/redoc.
pea/
├── README.md # This file
├── LICENSE # MIT License
├── BITACORA.md # Project history log
├── 00_orchestrator_blueprint_aaep_k8s_v2.1.md # Architectural blueprint
├── frd01_orchestrator_aaep_k8s_v2.1.md # FRD01: Orchestrator
├── frd02_work_pods_images_v1.1.md # FRD02: Work Pod images
│
├── orchestrator/ # Orchestrator (FRD01)
│ ├── environment.yml # Conda environment (Python 3.11)
│ ├── pyproject.toml # Package definition + pea-cli entry point
│ ├── src/
│ │ ├── main.py # Entry point (uvicorn)
│ │ ├── api/
│ │ │ ├── app.py # FastAPI app + router + CORS
│ │ │ └── runs.py # 7 pipeline endpoints
│ │ ├── core/
│ │ │ ├── state_machine.py # 10 states, valid transitions
│ │ │ ├── job_manager.py # Create/monitor K8s Jobs + SHA-256 hashing
│ │ │ └── validators/
│ │ │ ├── srs_validator.py # Validates SRS.md before creating a Run
│ │ │ └── report_validator.py # Validates qa_report.json, sec_report.json
│ │ ├── models/
│ │ │ ├── database.py # SQLAlchemy engine + SessionLocal
│ │ │ ├── run.py # Run model
│ │ │ ├── phase_run.py # PhaseRun model (traceability)
│ │ │ ├── checkpoint.py # Checkpoint model
│ │ │ └── base.py # Declarative base
│ │ └── cli/
│ │ └── main.py # pea-cli: create, status, approve, reject, retry-dev, logs, artifacts
│ ├── prompts/ # SHA-256 versioned prompts (one per agent)
│ │ ├── pm-analyst/prompt.md
│ │ ├── architect/prompt.md
│ │ ├── guardrails-design/prompt.md
│ │ ├── documentation/prompt.md
│ │ ├── dev/prompt.md
│ │ ├── qa/prompt.md
│ │ ├── security/prompt.md
│ │ └── pr-release/prompt.md
│ ├── frontend/ # Angular checkpoint GUI
│ │ └── src/app/
│ │ ├── runs/ # Run list view
│ │ └── checkpoints/ # Checkpoint panel + "Go" button
│ ├── k8s/ # Orchestrator K8s manifests
│ │ ├── namespace.yaml
│ │ ├── pvc.yaml
│ │ ├── rbac.yaml
│ │ └── deployment-orchestrator.yaml
│ └── tests/
│ ├── test_state_machine.py # State machine unit tests
│ ├── test_job_manager.py # Job manager tests (mocked K8s API)
│ └── test_api.py # FastAPI endpoint tests
│
├── images/ # Work Pod images (FRD02)
│ ├── base/
│ │ └── Dockerfile.base # python:3.11-slim + Node.js 20 + Claude Code CLI
│ ├── pm-analyst/ # Agent 1: PM Analyst
│ │ ├── Dockerfile
│ │ ├── entrypoint.sh
│ │ ├── prompt.md
│ │ └── .claude/settings.json # bypassPermissions + deny /run/secrets/*
│ ├── architect/ # Agent 2: Architect
│ ├── guardrails-design/ # Agent 3: Design Reviewer
│ ├── documentation/ # Agent 4: Documentation Engineer
│ ├── dev/ # Agent 5: Full-Stack Developer (+ React tooling + pytest)
│ ├── qa/ # Agent 6: QA Engineer (+ ruff + eslint)
│ ├── security/ # Agent 7: Security Engineer (+ pip-audit)
│ └── pr-release/ # Agent 8: Release Manager
│
├── k8s/ # Work Pod K8s manifests
│ ├── jobs/ # One Job YAML per agent
│ │ ├── job-pm-analyst.yaml
│ │ ├── job-architect.yaml
│ │ ├── job-guardrails-design.yaml
│ │ ├── job-documentation.yaml
│ │ ├── job-dev.yaml
│ │ ├── job-qa.yaml
│ │ ├── job-security.yaml
│ │ └── job-pr-release.yaml
│ └── secrets/
│ └── agent-credentials.yaml.template
│
├── specs/work-pods/ # SPEC.md for each agent
├── docs/
│ ├── PROJECT_STRUCTURE.md
│ └── plans/
└── tests/
└── test_all_images.py # Global structure validation for all 8 images
All agents share the same base architecture:
- Docker image built from
python:3.11-slim+ Node.js 20 + Claude Code CLI - Non-root user
agent(UID 1000),fsGroup: 1000in Job spec for PVC permissions entrypoint.shsources/run/secrets/credentials.envand launches Claude Code CLI- Resources: requests 512Mi/250m CPU, limits 2Gi/1000m CPU
backoffLimit: 0,restartPolicy: Never— no automatic retries
{
"permissions": {
"defaultMode": "bypassPermissions",
"allow": ["Bash", "Read", "Write", "Edit", "MultiEdit"],
"deny": ["Read(/run/secrets/*)", "Read(.env*)"]
}
}bypassPermissions enables autonomous execution. The deny rules prevent the agent from reading its own K8s Secret credentials.
| Agent | Extra tools | Main input | Main output |
|---|---|---|---|
pm-analyst |
— | spec/SRS.md |
artifacts/reports/pm_summary.md |
architect |
— | spec/SRS.md, pm_summary.md |
docs/sdd/SDD-*.md, docs/adr/ADR-*.md |
guardrails-design |
— | docs/sdd/*.md, docs/adr/*.md |
artifacts/reports/design_check.json |
documentation |
— | Approved design docs | Full docs/ + docs_check.json |
dev |
React tooling, pytest | All approved docs + reports (if any) | backend/ + frontend/ source |
qa |
ruff, eslint |
Source code | artifacts/reports/qa_report.json |
security |
pip-audit, npm audit |
requirements.txt, package.json |
artifacts/reports/sec_report.json |
pr-release |
git |
Fully validated repo | PR created on GitHub |
qa_report.json:
{
"status": "PASS | FAIL",
"evaluated_at": "2026-02-20T12:00:00Z",
"tool_versions": { "ruff": "X.Y.Z", "eslint": "X.Y.Z" },
"bugs": [
{
"id": "QA-001",
"tool": "ruff | eslint",
"severity": "error | warning",
"file": "backend/main.py",
"line": 42,
"description": "...",
"suggestion": "..."
}
]
}sec_report.json:
{
"status": "PASS | FAIL",
"evaluated_at": "2026-02-20T12:00:00Z",
"tool_versions": { "pip_audit": "X.Y.Z", "npm_audit": "X.Y.Z" },
"vulnerabilities": [
{
"id": "SEC-001",
"tool": "pip-audit | npm-audit",
"severity": "critical | high | medium | low",
"package": "package-name",
"installed_version": "X.Y.Z",
"fixed_version": "X.Y.Z",
"description": "...",
"suggestion": "upgrade to version X.Y.Z"
}
]
}Rule for security: pip-audit fails on any CVE; npm audit fails only on HIGH or CRITICAL severity.
Agent credentials (ANTHROPIC_API_KEY, Git tokens) are never stored in Docker images or source code. The flow is:
- Operator creates a K8s Secret from
k8s/secrets/agent-credentials.yaml.template - Secret is mounted at
/run/secrets/credentials.env(read-only) inside each Job entrypoint.shsources the file withset +xbefore thesourcecommand to prevent credentials from appearing in shell logs.claude/settings.jsonenforcesdeny: ["Read(/run/secrets/*)", "Read(.env*)"]— the agent cannot read its own credential file
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key for Claude Code CLI |
GIT_USER_NAME |
Commit author name |
GIT_USER_EMAIL |
Commit author email |
GIT_TOKEN |
GitHub personal access token (for creating PRs) |
GIT_REPO_URL |
Target repository URL for the generated PoC |
GIT_DEFAULT_BRANCH |
Base branch for the PR (typically main) |
max_auto_retries = 0 across the entire pipeline. No Job ever retries automatically. The only retry mechanism is explicit via pea-cli runs retry-dev <run-id> (or the API), available only from WAITING_HUMAN state and only for the dev phase.
The Orchestrator uses a ServiceAccount with minimal permissions to create/list/monitor Jobs and Namespaces. Agents in their Pods use a separate ServiceAccount (agent-sa) with read-only permissions within the run namespace.
conda activate pea-orchestrator
cd pea
pytest orchestrator/tests/ -v
pytest orchestrator/tests/test_state_machine.py -v
pytest orchestrator/tests/test_job_manager.py -v
pytest orchestrator/tests/test_api.py -vpytest tests/test_all_images.py -vThis verifies that each image has Dockerfile, entrypoint.sh, prompt.md, and .claude/settings.json with the correct deny rules.
Before launching each Job, job_manager calculates:
prompt_ref: relative path of the prompt in the workspace (images/{agent}/prompt.md)prompt_hash: SHA-256 of the prompt content (normalized to\nline endings)
Both values are stored in the corresponding PhaseRun, enabling a full audit of exactly which prompt version each agent ran.
| Variable | Default | Description |
|---|---|---|
PEA_API_URL |
http://localhost:8000 |
Base URL for the Orchestrator API (used by pea-cli) |
DATABASE_URL |
— | PostgreSQL connection URL |
CLAUDE_MODEL |
claude-sonnet-4-6 |
Claude model used by agents |
WORKSPACE_ROOT |
/workspace/repo |
Root of the shared workspace in K8s |
KUBECONFIG |
(in-cluster) | Path to kubeconfig (optional outside K8s) |
Error: cannot connect to Kubernetes API
Verify the cluster is running and kubeconfig is accessible:
kubectl cluster-info
kubectl get nodesIf running outside K8s, export KUBECONFIG explicitly:
export KUBECONFIG="$HOME/.kube/config"# Check Job status
kubectl get jobs -n pod-<run-id-prefix>
# View agent Pod logs
kubectl logs -n pod-<run-id-prefix> -l agent=pm-analyst
# Describe the Job to see events
kubectl describe job pm-analyst-<run-id> -n pod-<run-id-prefix>ERROR: Cannot connect to pea API at http://localhost:8000. Is the server running?
ps aux | grep uvicorn
# Or port-forward if the Orchestrator is in K8s
kubectl port-forward svc/orchestrator-service 8000:8000 -n pea-systemThe run is not in WAITING_HUMAN state, or the checkpoint was already resolved. Check the current state:
pea-cli runs status <run-id>kubectl get secret orchestrator-secrets -n pea-system
kubectl describe pod -l app=postgres -n pea-system# Make sure you're using Minikube's Docker daemon when building
eval $(minikube docker-env)
docker build -t pea/pm-analyst:latest images/pm-analyst/
# Verify image is available inside Minikube
minikube ssh docker images | grep peapea is in active development. Feedback in any form is valuable:
- Issues: bugs, unexpected behavior, documentation gaps
- PRs: fixes, improvements, new agent specs
- Logs: if an agent run fails unexpectedly, sharing the logs helps us reproduce and fix it
Please open an issue before submitting large PRs so we can align on the approach first.
- Omar Francisco Velazquez Juarez — ovelazquezj@gmail.com
- Salvador Agredano Herrera — salvador.agredano@outlook.com
This project is licensed under the MIT License. See the LICENSE file for details.
pea — actively developed. Last updated: 2026-02-20
