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.py is a generated artifact. Do not edit it directly. The canonical source lives in src/perseus/. Run python scripts/build.py to regenerate perseus.py after making changes.
  • The single-file design is about trust and inspectability — anyone can read, audit, and cp the whole thing without pip install.
  • pyyaml is the only runtime dependency. Do not add runtime deps. Dev/test deps in requirements.txt are fine.
  • Use patch, not write_file or 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/*.md to 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:

  1. Write a resolve_* function — returns a rendered string.
  2. Add a DirectiveSpec entry to DIRECTIVE_REGISTRY with name, kind, args, doc, safe_for_hover, resolver, and call_convention.
  3. Call _bind_registry() after all resolve_* functions are defined (it's already called in the right place — just add your entry before the call site).
  4. For block directives, add a *_BLOCK_RE regex 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 — not execute_*.
  • Every cmd_* handler takes (args, cfg) — two args.
  • load_config(workspace=...) has no path overload. Inject test config via monkeypatch.setattr(perseus, "PERSEUS_HOME", tmp).
  • Mutating DEFAULT_CONFIG needs json.loads(json.dumps(DEFAULT_CONFIG)) deep-copy.
  • Every early-exit path must respect --json. Audit all return statements 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/, not perseus.py. perseus.py is a generated artifact; edits to it are overwritten by the next build.
  • patch not write_file on 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.