Guides

Field guide

Step-by-step guides for getting Auriga running, integrating it with Pantheon, operating it in production, and extending it with new agents or routing rules.

Quickstart

Auriga's router has zero runtime dependencies. Clone the repo, run the tests, and optionally fire one dry-run cycle to see what decisions Auriga would make against your board.

Step 1: Clone and verify

# Requires Node 24+
git clone https://github.com/mdostal/auriga.git
cd auriga/src/router

npm test  # runs all unit tests — expect 26+ passing

Step 2: Configure your board

Edit src/router/lib/config-substrate.mjs to add your Multica project IDs and agent IDs. Or use the external override mechanism:

# Create an override file with your project/agent IDs
cat > /etc/auriga/local.json <<'EOF'
{
  "PROJECT_IDS": ["<your-multica-project-id>"],
  "AGENTS": {
    "<your-agent-uuid>": {
      "name": "my-build-agent",
      "runtime": "claude",
      "maxInflight": 3
    }
  }
}
EOF

export AURIGA_CONFIG=/etc/auriga/local.json

Step 3: Dry-run

# One cycle, compute decisions, assign NOTHING
npm run dry
# or: node auriga-router.mjs --once --dry-run

Step 4: Run for real

# One cycle then exit
npm run once

# Supervised: keep exactly ONE detached router alive, restart on death
nohup ./supervisor.sh >> /tmp/auriga-supervisor.log 2>&1 &

One-command install with MCP

For a faster path — install the auriga CLI and register the MCP server with your agent CLI in one command:

curl -fsSL https://mdostal.github.io/auriga/install.sh | bash

Operator dashboard

The operator dashboard is a local, read-only web view onto Auriga's own planning state (.pHive/ epics and stories). It's served by the JSON API in src/server/ and the Vite + Tailwind + shadcn/ui UI in src/ui/. Not required for production — purely for local development and visibility.

# Build and run the UI
cd src/ui
npm install
npm run build

cd ../server
npm install
node index.mjs
# → http://localhost:8787

The dashboard shows epics, stories (with dependency constellations, acceptance criteria, risk cards), and the activity log from .pHive/audits/. The server reads files directly — no database, no external services.


Pantheon integration

In a Pantheon deployment, Auriga runs as a Docker container service in the pantheon-v2 compose stack. The image is built from a git checkout of this repo. The image is not rebuilt automatically when the repo's main branch gets new commits.

Automatic redeploy via git hook (recommended)

Run this once in the plugins/auriga checkout on the deploy host (not in a standalone dev clone — the hook is a no-op there):

git config core.hooksPath install/lib/git-hooks

From then on, every git pull that lands a merge on main fires install/lib/git-hooks/post-merge, which discovers every live auriga* compose service and runs pantheon-v2's own bin/pantheon-redeploy script.

The hook owns nothing

The post-merge hook contains no redeploy logic of its own — it only decides when and for which services to call pantheon-v2's existing redeploy script. All build/health-check logic stays in pantheon-v2.

Manual redeploy

From the pantheon-v2 deployment root, after pulling new Auriga code:

cd plugins/auriga && git pull && cd ../..
bin/pantheon-redeploy $(docker compose config --services | grep -E '^auriga(-|$)')

Multi-tenant service naming

Pantheon supports multiple tenants, each with their own Auriga service instance. Services are named auriga or auriga-<tenant_id>. The post-merge hook uses docker compose config --services to discover all running auriga* services dynamically — no tenant list to maintain.


Production: reboot survival with launchd (macOS)

The supervisor script alone only survives as long as the shell session that launched it. To keep the router alive across logout and reboot on macOS, install it as a per-user launchd LaunchAgent.

# Install (from src/router/)
scripts/launchd/install.sh
# Fills in NODE/DIR/HOME from the current machine, installs to ~/Library/LaunchAgents/
# RunAtLoad + KeepAlive: starts immediately, restarts on death

# Check status
launchctl print gui/$(id -u)/com.mdostal.auriga-supervisor

# Uninstall
scripts/launchd/uninstall.sh

The launchd template is at scripts/launchd/com.mdostal.auriga-supervisor.plist.template. Customize NODE (path to node 24+) or DIR (path to src/router/) via env overrides before running install.sh.

Log locations (production)

FileContents
/tmp/auriga-router.logHuman-readable router log
/tmp/auriga-router.jsonlStructured JSONL log (one JSON object per line)
/tmp/auriga-router.pidRouter pidfile (single-instance lock)
/tmp/auriga-supervisor.logSupervisor stdout/stderr
/tmp/auriga-supervisor.pidSupervisor pidfile
/tmp/auriga-supervisor-launchd.loglaunchd's own stdout/stderr capture for the supervisor

Adding a new agent lane

To add a new agent to the dispatch pool:

1. Add the agent to AGENTS

In src/router/lib/config-substrate.mjs (or via an AURIGA_CONFIG override):

AGENTS['<new-agent-uuid>'] = {
  name: 'my-new-agent',
  runtime: 'claude',    // must match a key in RUNTIME_CAP
  maxInflight: 2,
};

2. Wire the agent to a lane

If the agent handles a specific project's stories, add a PROJECT_LANE entry:

PROJECT_LANE['<multica-project-id>'] = 'my-lane-name';

If the agent handles general hive stories, set HIVE_LANE to include it, or if it handles the default (non-hive) pool, set DEFAULT_LANE. Lane names are Multica-specific and must match what's configured in the Multica workspace.

3. Add a runtime cap bucket if needed

If the new agent needs its own cap independent of existing buckets:

// in config.mjs
RUNTIME_CAP['my-new-runtime'] = 2;

4. Verify

npm run dry  # confirm the new agent appears in routing decisions

Start with --dry-run

Always verify a new agent config with --dry-run before running a live cycle. Routing decisions are logged — confirm the right issues are being routed to the new agent before enabling live dispatch.


Human queue workflow

Issues flagged for human attention are excluded from agent dispatch. The human queue export collects them into a single YAML file for review.

Generating the queue

# From repo root
node scripts/export-human-queue.mjs
# Output: .pHive/human-queue.yaml

The export script queries the board for all in-scope issues where waiting_on matches a HUMAN_NAMES entry, or that carry the human-todo label. It writes them to .pHive/human-queue.yaml in a human-readable format.

Resolving a queued issue

  1. Review the issue and take the required human action (provide feedback, approve, add information).
  2. Remove or clear the waiting_on metadata, or remove the human-todo label.
  3. On the next cycle, Auriga will find the issue in the normal candidate pool and route it.

Dependency waiting_on

If an issue has waiting_on: PANT-1234 (an issue identifier, not a human name), it is NOT in the human queue — it just waits for that dependency. Once the referenced issue is done, the waiting issue becomes a normal candidate.


Writing tests

Auriga's test suite uses Node's built-in node:test runner (no external framework) and in-memory stub adapters to drive cycle() without any real Multica CLI calls.

Running the tests

# From src/router/
npm test                    # node --test test/*.test.mjs

# From the repo root
npm test                    # delegates to all subpackages

Test anatomy

Every test that exercises routing logic uses the stubs:

import { createStubBacklogAdapter } from '../lib/adapters/stub/backlog.mjs';
import { createStubSpawnAdapter }   from '../lib/adapters/stub/spawn.mjs';

// Seed the board with a fixture issue
const backlog = createStubBacklogAdapter({
  issues: [{ id: 'PANT-1', status: 'todo', assignee_id: null, ... }],
  runs: [],
  prs: [],
});
const spawn = createStubSpawnAdapter();

await cycle({ backlog, spawn, sleep: async () => {}, now: Date.now() });

// Assert on what was dispatched
assert.equal(spawn.calls.length, 1);
assert.equal(spawn.calls[0].op, 'assignAndRun');
assert.equal(spawn.calls[0].issueId, 'PANT-1');

Key test files

FileWhat it tests
test/core.test.mjsPure decision logic: filtering, state machine, squad plan, capacity checks.
test/router-cycle.e2e.test.mjsFull cycle() end-to-end against stub adapters. The canonical integration test.
test/standalone-smoke.test.mjsProves zero external process calls (execFileSync) when running on stubs.
test/squad.test.mjsReview squad tier classification against keyword fixtures.
test/cutover-e2e.test.mjsEnd-to-end cutover test for the adapter boundary.