Contributing to Perseus
June 17, 2026 · View on GitHub
Thank you for your interest. Perseus is an open project — contributions of all kinds are welcome.
Ground Rules
Perseus ships as a single-file CLI (perseus.py). This constraint is intentional:
perseus.pyis a generated artifact. Do not edit it directly. The canonical source lives insrc/perseus/. Runpython scripts/build.pyto regenerateperseus.pyafter making changes.- The single-file design is about trust and inspectability — anyone can read,
audit, and
cpthe whole thing withoutpip install. pyyamlis the only runtime dependency. Do not add runtime deps. Dev/test deps inrequirements.txtare fine.- Use
patch, notwrite_fileor full rewrites. The file is ~10K lines; any whole-file replacement risks truncation and data loss. - All tests must pass before committing. Run
python -m pytest tests/ -q. - Spec follows code. If your change modifies behavior, update
spec/*.mdto match.
Development Setup
git clone https://github.com/Perseus-Computing-LLC/perseus.git
cd perseus
# Activate the pre-commit hook — auto-rebuilds perseus.py when src/ changes
git config core.hooksPath .githooks
pip install -r requirements.txt # pyyaml + pytest
python -m pytest tests/ -q # 1,030+ tests
python perseus.py --version # perseus v1.0.6
The single-file artifact
perseus.py is generated from the module tree in src/perseus/. The build script
stitches all modules together in dependency order, stripping from perseus.X import Y
internal imports (which are only needed when running modules individually during
development). The install story is unchanged: cp perseus.py ~/.local/bin/perseus.
To regenerate after editing src/:
python scripts/build.py
Contributors should edit src/perseus/ and run python scripts/build.py before
committing — the perseus.py in the repo root is always a build artifact.
See scripts/build.py for the full module order and strip logic.
Repo Layout
perseus.py ← generated single-file artifact (do not edit directly)
src/perseus/ ← canonical source split by module
__init__.py ← shebang, stdlib imports (no logic)
cli.py ← argparse, main(), command dispatch
config.py ← PERSEUS_HOME, DEFAULT_CONFIG, load_config
registry.py ← DirectiveSpec, DIRECTIVE_REGISTRY, _bind_registry
renderer.py ← cache layer, _render_lines, render pipeline
serve.py ← HTTP serve, LSP, cmd_render, cmd_synthesize, …
directives/ ← one file per directive resolver
… (see scripts/build.py for full module order)
scripts/
build.py ← concatenates src/ → perseus.py
release.sh ← builds dist artifacts (calls build.py first)
install.sh ← one-liner installer
requirements.txt ← pyyaml + dev deps (pytest, etc.)
tests/
conftest.py ← shared fixtures and module import wiring
test_renderer.py ← directive resolution, rendering
test_lsp.py ← LSP JSON-RPC subprocess harness
test_doctor.py ← doctor checks
test_build.py ← verifies build.py: clean run, determinism, --version
test_*.py ← subsystem suites; run all before committing
spec/
overview.md ← architecture start point
components.md ← component specs
directives.md ← full directive reference
integration.md ← adapter conformance matrix
data-model.md ← config/checkpoint/cache schemas
docs/ ← user-facing documentation
tasks/ ← Agora task board
ROADMAP.md ← living roadmap (rendered live by Perseus itself)
AGENTS.md ← contributor guide for AI agents
examples/ ← runnable demo workspaces
Adding a New Directive
All directives go through the DIRECTIVE_REGISTRY. Adding one requires exactly four touches — miss any one and the directive will be partially broken:
- Write a
resolve_*function — returns a rendered string. - Add a
DirectiveSpecentry toDIRECTIVE_REGISTRYwithname,kind,args,doc,safe_for_hover,resolver, andcall_convention. - Call
_bind_registry()after allresolve_*functions are defined (it's already called in the right place — just add your entry before the call site). - For block directives, add a
*_BLOCK_REregex and a handler in_render_lines.
See the directive reference and the DIRECTIVE_REGISTRY architecture section in AGENTS.md for the call-convention table.
Writing Tests
Follow these conventions when writing new tests:
- Shell-running resolvers are
resolve_query/resolve_agent— notexecute_*. - Every
cmd_*handler takes(args, cfg)— two args. load_config(workspace=...)has no path overload. Inject test config viamonkeypatch.setattr(perseus, "PERSEUS_HOME", tmp).- Mutating
DEFAULT_CONFIGneedsjson.loads(json.dumps(DEFAULT_CONFIG))deep-copy. - Every early-exit path must respect
--json. Audit allreturnstatements in any function that supports--json.
Tests live under tests/. There's one file per subsystem. Add new tests to the most relevant file; if you're adding a new subsystem, add tests/test_<subsystem>.py.
Running the Perseus Gauntlet
The Perseus Gauntlet (benchmark/gauntlet/) is the enterprise torture test — 12 orchestrated phases, 25 role profiles, and 15 registered gates. It requires Linux (the orchestrator uses Linux-specific filesystem features for adversarial scenarios). Smoke mode takes ~20 minutes; full mode takes ~6–8 hours. Full certification requires no failed hard gates, no skipped hard gates, and no unexplained budget overruns.
On Linux
pip install pyyaml
python benchmark/gauntlet/gauntlet_setup.py
python benchmark/gauntlet/gauntlet_orchestrator.py \
--nodes local \
--nfs-path /tmp/perseus-gauntlet \
--developers-per-node 500 \
--duration smoke \
--output-dir /tmp/perseus-gauntlet-output
On macOS / Non-Linux (via Docker)
# From repo root, using a Linux Python image:
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
python:3.12-slim \
bash -lc '
apt-get update >/dev/null &&
apt-get install -y git >/dev/null &&
pip install --no-cache-dir pyyaml >/dev/null &&
python benchmark/gauntlet/gauntlet_setup.py &&
mkdir -p /tmp/perseus-gauntlet-output &&
python benchmark/gauntlet/gauntlet_orchestrator.py \
--nodes local \
--nfs-path /tmp/perseus-gauntlet \
--developers-per-node 500 \
--duration smoke \
--output-dir /tmp/perseus-gauntlet-output
'
Verify results:
cat /tmp/perseus-gauntlet-output/gauntlet_score.txt
cat /tmp/perseus-gauntlet-output/gauntlet_report.md
Expected smoke target: ≥ 90/100.
The Agora (task workflow)
The tasks/ directory is the async coordination substrate. Any contributor can pick up a task:
python perseus.py agora list # see open tasks
python perseus.py agora claim task-N --agent your_agent_name # claim one
# ... do the work ...
# Add a ## Completed section to the task file
python perseus.py agora complete task-N # mark done
Task files have YAML frontmatter with status, depends_on, and blocks. Respect the dependency graph. Do not create new tasks without explicit owner sign-off.
AI Contributor Notes
Perseus is built with AI coding assistants as first-class contributors. The AGENTS.md file orients any AI agent at session start. Key rules for AI contributors:
- Executor, not architect. Implement the spec, don't propose changes to it.
- No unsolicited new tasks. If you spot something worth doing, note it in your completion summary — the owner decides.
- Edit
src/perseus/, notperseus.py.perseus.pyis a generated artifact; edits to it are overwritten by the next build. patchnotwrite_fileon any large source file. Recovery from a truncated file:git checkout HEAD -- <file>.
Issues and Pull Requests
- Open an issue to discuss significant changes before starting.
- PRs are welcome. Keep them focused — one feature or fix per PR.
- For security issues, please report privately.
License
Perseus is MIT licensed. See LICENSE for details.