Architecture

How Auriga is built

Auriga is a plain Node.js service with a zero-dependency routing core, an adapter-interface boundary between decision logic and I/O, and two independent UI subsystems for local observability.

Component overview

The repo contains three independent subsystems. None is a dependency of any other — each has its own package.json and npm test.

SubsystemPathRole
Router src/router/ The auto-router daemon. Plain Node, zero runtime dependencies. This is the production service.
Server src/server/ Read-only JSON API over the repo's own .pHive/ state. node:http, no framework.
UI src/ui/ Operator dashboard — Vite + Tailwind + shadcn/ui — served by the server. Build-only, not shipped to production.

Server + UI are local-only

The server and UI subsystems are for local observability only — they read this repo's own .pHive/ planning state and serve it as a dashboard. They are not part of the production Pantheon deployment. In production, only src/router/ runs.


The router

The router at src/router/ is the core of Auriga. It has zero runtime dependencies — plain Node.js ESM modules, no npm packages required to run. All decisions are pure functions; all I/O goes through injectable adapters.

Key files

FileRole
auriga-router.mjsEntrypoint and daemon loop. Exports cycle(). main() runs when executed directly (guarded by isMainModule).
lib/core.mjsPure decision logic — all routing decisions, state machine transitions, squad planning. No adapter calls.
lib/config.mjsPolicy configuration: CAPS, HUMAN_NAMES, REVIEW_SQUAD_RULES. Imports from config-substrate.mjs.
lib/config-substrate.mjsSubstrate configuration: AGENTS, PROJECT_IDS, lane maps, RUNTIME_CAP. Tenant-specific.
lib/config-loader.mjsLoads optional external JSON override file (AURIGA_CONFIG env var).
lib/adapters/The BacklogAdapter, SpawnAdapter, MemoryAdapter contracts + real and stub implementations.
bin/auriga.mjsCLI entrypoint for subcommands: auriga memory recall/remember, auriga agent init/status.
supervisor.shKeeps exactly one detached router process alive. Restarts on death.

No class declarations

The entire codebase has zero class declarations. All modules export plain factory functions (createXAdapter(cfg)) returning frozen object literals. This is a stated project convention in .pHive/CONTEXT.md.


Adapter interface

Auriga's routing core (lib/core.mjs) never calls Multica or any external system directly. All I/O goes through a typed adapter boundary. This makes the routing logic pure and testable, and makes it possible to swap the underlying board system without touching the routing policy.

// cycle() accepts injectable adapters — tests can pass stubs
export async function cycle({ backlog, spawn, cfg, core, log, sleep, ...opts }) {
  // backlog: BacklogAdapter — read/write the work board
  // spawn:   SpawnAdapter   — dispatch/assign/rerun agents
  // core, log, sleep: injectable for testing
}

Synchronous by design

BacklogAdapter and SpawnAdapter are synchronous — every method returns its result directly, never a Promise. This matches the real Multica-backed implementation (which uses execFileSync) and all existing call sites — several methods are called inside .some()/.filter() callbacks where await cannot be used. Do not make these adapters async without a concrete story that requires it.

MemoryAdapter is the deliberate exception: it has no synchronous consumer today and Mnemosyne's real transport is genuinely async.


Three adapters, not one

The backlog (where work items live), the runner (what actually executes work), and memory (what context informs a decision) are genuinely different concerns with different failure modes. A future implementation of any one should be swappable independently of the others.

BacklogAdapter

Read/write the work board: list issues, read runs/PRs, change status, post comments.

lib/adapters/backlog-adapter.mjs

SpawnAdapter

Dispatch/assign/rerun/unassign an agent against an issue.

lib/adapters/spawn-adapter.mjs

MemoryAdapter

Recall/remember context or knowledge from Mnemosyne.

lib/adapters/memory-adapter.mjs

Implementations

PathWhat it is
adapters/multica/Real, live-default implementation backed by the multica CLI (execFileSync).
adapters/stub/In-memory test doubles. createStubBacklogAdapter(seedData) + createStubSpawnAdapter(). Zero external calls.
adapters/pantheon-v2-l2/Intentionally-unbuilt stub — the only sanctioned future path from Auriga into a Pantheon L2 host. Not production code yet.
adapters/mnemosyne/Real Mnemosyne-backed MemoryAdapter (HTTP recall/remember).

No speculative methods

Neither BacklogAdapter nor SpawnAdapter has any method that cycle() doesn't actually consume today. Do not add methods speculatively. Every method in these contracts exists because the real dispatch loop calls the equivalent capability. When a new need arises, add the method in the story that needs it.


Configuration split

Configuration is split into two files to separate policy (what the router decides) from substrate (who it routes to and where):

FileContainsWho changes it
lib/config.mjs Policy: CAPS, HUMAN_NAMES, REVIEW_SQUAD_RULES, RUNTIME_CAP mutations Routing policy changes (cycle behavior, review sizing)
lib/config-substrate.mjs Substrate: AGENTS, PROJECT_IDS, PROJECT_NAMES, lane maps (PROJECT_LANE, HIVE_LANE, DEFAULT_LANE, REVIEW_LANE), REVIEW_REPO_OWNER, REVIEW_SEARCH_REPOS Tenant/deployment changes (new agents, new projects, lane rewiring)

An optional external JSON override file can replace any key in either module via the AURIGA_CONFIG environment variable. Only keys present in the override file are replaced — absent keys keep their defaults. This allows per-deployment overrides without forking the config source.


Pantheon integration

In a Pantheon deployment, Auriga runs as a Docker container service defined in the pantheon-v2 compose stack. The container image is built from a plain git checkout of this repo at ${AURIGA_LOCAL_PATH:-./plugins/auriga}.

Images don't auto-update

Nothing rebuilds the container image automatically when this repo's main branch gets a new commit. A merged fix sitting unreachable in a stale running container is a real incident that has already happened (see #76). Use the git hook or manual redeploy process — see Guides: Pantheon integration.

pantheon-v2-l2 stub

The adapters/pantheon-v2-l2/ directory is the intentionally-unbuilt stub for Auriga's future direct integration with the Pantheon L2 host (bypassing the external Multica CLI). It is the only sanctioned path for Auriga to call into Pantheon internals. It is not production code yet — the stub exists to hold the interface contract, not to be wired in.


Repo layout

auriga/
├── src/
│   ├── router/                 # The auto-router (production)
│   │   ├── auriga-router.mjs   # Entrypoint, exports cycle()
│   │   ├── supervisor.sh       # Single-instance supervisor
│   │   ├── bin/auriga.mjs      # CLI subcommands
│   │   ├── lib/
│   │   │   ├── core.mjs        # Pure decision logic
│   │   │   ├── config.mjs      # Policy config
│   │   │   ├── config-substrate.mjs  # Substrate config
│   │   │   ├── config-loader.mjs     # External JSON override
│   │   │   └── adapters/       # BacklogAdapter, SpawnAdapter, MemoryAdapter
│   │   ├── agents/             # Agent instruction files
│   │   ├── scripts/            # Export human queue, launchd install
│   │   └── test/               # Unit + e2e tests
│   ├── server/                 # Read-only API over .pHive/ state
│   └── ui/                     # Operator dashboard (Vite + Tailwind)
├── docs/                       # GitHub Pages source (this site)
│   ├── index.html              # Project page
│   └── docs/                   # Full documentation
├── scripts/                    # Repo-root convenience scripts
├── install/                    # Git hooks for Pantheon auto-redeploy
└── .pHive/                     # Planning state (epics, stories, audits)