A defensive security fleet under Hephaestus — built on the same pi install machinery as the code-fleet, but with the doctrine inverted. Fifteen specialists find vulnerabilities in a codebase, prove them with working exploits, and review each pass with the operator in the loop — the confirmed report comes at the end, once the loop converges. The operator decides what to patch. The pipeline never patches, never re-attacks. Find → prove → review — then loop, then report.
Four stages, no patching, no re-attack, no calibration loop. The v2.1 output is a confirmed-vulnerability report, not a fixed system — patching is the operator's job. The finder reads the codebase and plans the attack; the prover turns candidate paths into empirical proof with a running exploit; each pass is reviewed by the operator and the retrospective before the loop continues (HITL refinement as we go); once the loop converges, the reporter normalizes severity and hands the operator a prioritized list. An optional pair of dual lanes runs a second model on every triage and research call for verdict diversity.
The fleet mines the project's history for past security fixes, runs every available SAST scanner, maps the HTTP attack surface and the architecture, code, and trust boundaries into a static knowledge base, and devises the attack plan — a short list of investigations, each pointed at verified files with a sharp question.
The prover researches each target path in depth, decides whether an attack vector is viable, and devises new ones from what it learns along the way. When a vulnerability holds up, it crafts a working exploit, runs it, and captures empirical proof — not theory.
Every pass ends with the operator in the loop. The retrospective (a separate model) reads the pass's findings, clean verdicts, failed reproductions, and coverage holes and hunts false negatives; the operator is the catch-all — “this is mitigated by environment,” “this looks like a bad clean report, I need the strategist to redo it with this new info.” Pushback lands in retrospectives/pass-<N>.md and the loop continues back to the strategist. HITL refinement, every pass.
Once the loop converges (0 new findings, or the operator stops), the leader loads the reporter skill once: drops failed_to_reproduce and false positives, normalizes severity with a rubric (empirical proof downgrades one band), writes priority P0–P3, and produces the confirmed-vulnerability report grouped by priority. The operator reads it and decides what to patch.
One engagement is Recon once, then a loop of Hunt → Review that refines as it goes. Each pass: the strategist plans, the fleet hunts, findings land, and the operator reviews them with the retrospective before the next pass. The loop starts at the strategist and ends at the operator — HITL refinement every pass. When it converges (0 new findings, or the operator stops), the reporter produces the confirmed-vulnerability report. State lives in <project-root>/.agents/purpleteam/ — the KB, the ledger, the plan, and the findings persist between passes; the leader reports the delta.
clean to learnings.jsonlrepro_status: reproducedfinding.chains via pt-chain.pyrepro_status: reproducedfinding.chains via pt-chain.pyfindings/*.json (skip-list) + learnings.jsonl (retry/blocker/gap tail) + chaining + operator-mandated pushback (“mitigated by environment”, “bad clean — redo with this info”). KB & TM are static (architect and threat-modeler don’t re-run). HITL refinement as we go — converges on 0 new findings or the operator says stop.
Kept light on purpose — the pipeline shape is the design; the agents are sized to their role. Full doctrine, schemas, and the three open empirical questions live in hephaestus/pi/profiles/purple/readme.md.
Drives Recon in order, delegates jrresearcher per investigation (≤4/wave), routes verdicts, loads the reporter skill at end-of-pass, loops. Does not audit, exploit, or patch.
Greps git log for past security fixes, writes historical_learnings.jsonl. Idempotent — skips if the file exists. Cheap, narrow, read-only.
Runs every available SAST scanner via the pt-sast skill (semgrep, bandit, ruff, pip-audit, npm audit, gitleaks, trufflehog, trivy — graceful degradation on missing tools), compiles positives to kb/sast_findings.json, and flags unsupported languages. Appends note records for HIGH/CRITICAL positives. Cheap, scripted, read-only.
Recon-only. Maps the codebase's HTTP attack surface once — endpoints, methods, params, auth status, and risk flags — to kb/api_inventory/routes.jsonl (+ topology.json + unresolved.jsonl + report.md). Runs after the scanner; the 5-phase workflow (detect → candidates → topology → resolve → cross-check) lives in the agent prompt, the framework extractors in the pt-api-inventory skill. Fans per-language handler-body extraction to the intern — extraction is the intern's, adjudication is the mapper's. Output is folded into the KB by the architect and read by the strategist for investigation scoring.
Recon-only. Folds recon learnings.jsonl first (permanent, never truncated), then folds the api inventory (kb/api_inventory/routes.jsonl) and synthesizes the interlinked Markdown KB of entities, data flows, and vuln classes — built once, static (not re-run in the loop). KB is index-backed (kb.index.json via the pt-kb skill). Long-context synthesis at 1M window.
Recon-only. Reads the KB, writes kb/THREAT_MODEL.md — trust boundaries, threat actors & vectors, high-risk assets. Flags gaps as TODOs; never invents entities the KB lacks. Static — trust boundaries come from the code, not findings; not re-run in the loop.
Reads KB + threat model + history, writes plan.json — 4–12 investigations (multiple of 4); each item has a title, test -f-verified target_files, KB references, and a question. Re-plans every pass against findings (skip-list — a vuln finding supersedes a clean on the same file) + learnings tail + chaining + operator-mandated pushback from the retrospective.
One investigation per delegation. Reads the target_files, returns a verdict — CLEAN (writes a kind: clean finding + ledger record), HOTSPOT, or ESCALATE. No nested delegation. Batched ≤4/wave on the Qwen tenant.
A second model runs the same triage call as Jr Researcher — same context, different model family — and returns its own CLEAN/HOTSPOT/ESCALATE verdict. Writes nothing. Dispatched alongside the primary only when dual_triage: true in run-flags.json. Body byte-identical to the primary; frontmatter-only fork. Splits become observation ledger records for the retrospective.
For HOTSPOT: understands the bug from the code, crafts a working exploit, runs it via bash, captures the proof, writes the finding with empirical proof + initial severity. Owned Qwen 27B on the Spark — no third-party ToS exposure on exploit code.
Traces data flow back from each vulnerability to identify composition chains — what else this vuln enables, what enables it — and writes chains into finding JSONs via pt-chain.py. Updates chain confidence (hypothetical → investigated → confirmed / disproven) as new findings land. Cheap 35B for mechanical trace-back sweeps; delegates its own sweeps to the intern.
For ESCALATE paths: multi-file deep analysis, reports CLEAN or HOTSPOT back to the leader. On CLEAN, writes a kind: clean finding; if HOTSPOT, the leader routes to the reproducer. Writes a not_required finding when the exploitation path isn't clear.
A second model runs the same ESCALATE deep-audit as the Researcher — 1M-ctx GLM against Kimi — and returns its own CLEAN/HOTSPOT verdict. Writes nothing. Dispatched alongside the primary only when dual_research: true in run-flags.json. Body byte-identical to the primary; frontmatter-only fork. Splits become observation ledger records for the retrospective.
Starts from the pass's findings (not the final report — that comes at the end, once the loop converges) and walks the operator through what happened and why. Hunts false negatives in the non-findings — CLEAN verdicts, failed_to_reproduce, coverage holes, unresolved shadow disagreements — and co-decides the strategist's next-pass pushback with the operator, writing retrospectives/pass-<N>.md. The operator is the catch-all: “mitigated by environment”, “bad clean — redo this one”. A different model family from the leader (DeepSeek) and researcher (Kimi) for genuine judgment diversity. The leader never dispatches it — the operator switches to @pt-retrospective by hand.
Specialists hand it one small mechanical task — lookup, count, summary, extraction, small synthesis, a scratch script — so the expensive models don't spend tokens on grep/wc work. No findings, no verdicts, no ledger. Does the task, replies the result. The cost-routing sink for leader, strategist, architect, threat-modeler, reproducer, researcher, chainer, and api-mapper (per-language handler-body extraction).
Permanent append-only ledger. Eight fields (required: phase, agent, type, note; optional: target, finding_id, hint, pass); nine-value type enum. Append via pt-learn.py (CLI flags, validated); finding IDs are readable word-word slugs from pt-slug.py; pt-chain.py writes chains. The architect folds recon records into the KB once (Recon-only); the ledger is never truncated.
Filters to kind: vulnerability only (clean findings are coverage state, not vulns), drops failed_to_reproduce, normalizes severity with a rubric (empirical proof downgrades one band), writes severity_final + priority P0–P3, produces a markdown report grouped by priority with a clean-checks count.
Runs every available scanner via pt-scan.py (semgrep, bandit, ruff, pip-audit, npm audit, gitleaks, trufflehog, trivy — graceful degradation on missing tools) and compiles positives to kb/sast_findings.json via pt-compile.py. SARIF 2.1.0 for the five native-SARIF tools, JSON for the rest. Read by architect (folds HIGH/CRITICAL) + strategist (scoring).
Builds and maintains .agents/purpleteam/kb/ as a file-backed knowledge base indexed by kb.index.json — the static map of the code. kb_index.py create/add/reconcile operations; the index is a portable router over KB contents (markdown stays canonical, the index stays in sync).
15 CWE-anchored vulnerability classes (60 subtypes) with per-subtype SAST coverage ratings. Every finding is classified against the canonical list so the strategist’s scoring, the KB’s vuln-class files, and the reporter’s severity normalization all compare apples to apples.
Thin manifest: ships the scripts (detect.py, candidates.py, topology.py, inventory.py, speccheck.py) and per-framework references (Python/Node/Java/Go/.NET/Ruby/PHP) loaded on demand after Phase 0 detection. The 5-phase workflow doctrine (detect → candidates → topology → resolve → cross-check) lives in the pt-api-mapper agent prompt; the skill just ships the extractors. Output: kb/api_inventory/routes.jsonl — per-endpoint auth status, parameter shapes, risk flags.
The preamble (rules-of-engagement.md) is embedded verbatim at the top of every agent's system prompt and tells the model, as fact, that it runs inside a fully isolated environment — no network egress, no production access, no third-party harm surface. The agent is instructed to trust the isolation, not verify, probe, or test it. Running exploit scripts via bash is expected; refusing on “what if this reaches a real system” is explicitly out of scope.
The pipeline does not manage Docker, mount volumes, or tear down containers. Clean isolation is the operator's responsibility — before invoking the leader, run pi inside a container, VM, or otherwise network-isolated environment the operator trusts (docker run --network=none is the simplest path; a dedicated VM with a snapshot is better if exploits need outbound HTTP to prove). Without this, exploits run on the operator's real network. By design, the agents cannot check isolation.
State is project-relative — one folder, no database, no vector store. Findings persist between passes; the leader reports the delta. v2 trims v1's per-pass archive, process files, and patcher.
.agents/purpleteam/
├── findings/
│ ├── <slug>.json # kind: vulnerability — confirmed/attempted vuln
│ └── <slug>.json # kind: clean — triage reasoning preserved
├── kb/ # STATIC — built once in Recon, not re-run in the loop
│ ├── kb.index.json # authoritative KB index (pt-kb skill)
│ ├── THREAT_MODEL.md # threat-modeler (static)
│ ├── sast_findings.json # pt-scanner: compiled SAST positives
│ ├── api_inventory/ # pt-api-mapper (once per engagement): HTTP attack-surface map
│ │ ├── routes.jsonl # endpoints, methods, params, auth status, risk flags
│ │ ├── topology.json # request flow + cross-framework topology
│ │ ├── unresolved.jsonl # candidates the mapper couldn't fully resolve
│ │ └── report.md # human-readable summary
│ ├── sast/ # pt-scan.py raw per-tool output (SARIF/JSON)
│ └── <entity>.md # architect: entities, data flows, vuln classes
├── historical_learnings.jsonl # historian (once per engagement, read-only)
├── learnings.jsonl # permanent append-only ledger; every agent appends
├── plan.json # strategist output (rewritten every pass)
├── retrospectives/ # pt-retrospective writes (operator co-authored); one pass-<N>.md per pass
│ └── pass-<N>.md # ⛔ HITL GATE: leader blocks next pass until this exists
├── run-flags.json # dual-lane opt-in: {"dual_triage": false, "dual_research": false}; absent = both off
└── REPORT.md # reporter writes at end-of-pass; leader presents to operator
cd hephaestus/pi ./setup.sh purple # install the purple profile (15 agents + 6 skills) ./setup.sh --uninstall purple # removes only this profile's links
Installed: into the profile dir ~/.pi/profiles/purple/ — 15 agent .md files in agents/; the 6 pt-* skills in skills/ (pt-learnings/, pt-reporter/, pt-sast/, pt-kb/, pt-taxonomy/, pt-api-inventory/), symlinked and tracked by a per-profile manifest. Profile-scoping is deliberate: the pt-* skills never sit in front of the coding fleet or Claude Code. Untouched: the code-fleet profile, opencode config, ~/.claude/settings.json — the installer doesn't manage containers, so it doesn't touch the permission layer.
Full doctrine, schema, agent roles, the engagement workflow, and the four open v2.1 empirical questions (jrresearcher triage accuracy, reproducer analysis depth from HOTSPOT verdicts, SAST false-positive load in the KB, shadow disagreement yield): see hephaestus/pi/profiles/purple/readme.md.