Core Concepts

How Auriga thinks

The routing cycle, state machine transitions, capacity model, human-todo filter, review squad, and zombie recovery — the core mechanisms that drive every dispatch decision.

The routing cycle

cycle() is the core function exported from auriga-router.mjs. It runs one complete scan-and-dispatch pass. In production, main() calls it in a while(true) loop separated by sleep(cfg.CAPS.cycleMs). Each pass runs exactly this sequence:

1. acquireLock()                  // pidfile — single-instance safety
2. detectZombies()                 // find stale in_progress → requeue or give up
3. detectRunCompletions()          // in_progress → in_review on run done
4. detectVerifiedDone()            // in_review → done on PR merged
5. detectCascadeCompletions()      // enqueue dependents of newly-done issues
6. detectFalseDone()               // wrongly-done → in_review correction
7. reviewDispatch()                // fire review squad for in_review + open PR
8. selectAssignments()             // pick unassigned todos for this cycle
9. assignAndVerify() per pick      // assign, check a run actually started

Steps 2–6 are pure status-machine passes derived entirely from live board state. Steps 7–9 are the dispatch passes. The cycle is always bounded: each step has an explicit cap on how many mutations it can make per cycle, so a single bad run can never mass-flip the board.

Pure inputs, pure outputs

cycle() accepts injectable adapters (backlog, spawn, cfg, core, log, sleep) so tests can drive the entire cycle against mocked board state without any real Multica CLI calls. The decision logic in lib/core.mjs is always pure — no adapter calls inside core, ever.


State machine

Issue status transitions are driven entirely by verifiable board facts — not by trusting what a run or agent reports about itself. This is a deliberate constraint from a past incident where run-status trust caused false completions.

Transitions Auriga drives

FromToConditionSource
todo in_progress Auriga assigns the issue to an agent and verifies a run started assignAndVerify
in_progress in_review Issue's latest run is done and not failed (classifyRun().done) detectRunCompletions
in_review done A PR linked to the issue has state === 'merged' or non-null merged_at detectVerifiedDone
in_progress blocked Zombie recovery exhausted max attempts detectZombies
in_review in_review Review squad sends story back (changes required) reviewDispatch
done in_review Issue marked done without a merged PR (detectFalseDone) detectFalseDone

Run status is never trusted alone

in_review → done requires a merged PR. A completed run alone does not mark an issue done — this constraint prevents false completions when a run finishes but the PR is not yet merged or was closed without merging.

Idempotency

Every transition scan re-derives its candidate set from live board state each cycle. A transitioned issue falls out of its source filter next cycle by definition — no explicit "already processed" tracking is needed.


Capacity model

Auriga enforces a layered capacity model to prevent mass-dispatch and to keep the Claude account from being saturated by a burst of simultaneous agent runs.

Cap layers

CapWhat it limitsConfig key
perCycleTotal Max new dispatches per full cycle CAPS.perCycleTotal (default: 5)
perCyclePerAgent Max dispatches to a single agent per cycle CAPS.perCyclePerAgent (default: 2)
maxInflight (agent) Max concurrently in_progress issues per agent AGENTS[id].maxInflight
RUNTIME_CAP Max concurrently in_progress issues per runtime bucket RUNTIME_CAP[bucket]
perCycleReview Max review dispatches per cycle (back-half) CAPS.perCycleReview (default: 1)
perCycleFalseDone Max false-done corrections per cycle CAPS.perCycleFalseDone (default: 3)
perCycleCascade Max cascade (dependent) enqueues per cycle CAPS.perCycleCascade (default: 5)

Runtime buckets

Agents are grouped into runtime buckets (e.g. claude, claude-review, claude-planning). Each bucket has its own RUNTIME_CAP so review dispatches don't compete with build-lane dispatches for the same Claude account slots.

// Example: review agent gets its own bucket, capped at 1
RUNTIME_CAP['claude-review'] = 1;

// Build lane agents share the 'claude' bucket (default cap)
RUNTIME_CAP['claude'] = 4; // set in config-substrate.mjs

Capability routing

Not every agent can run every kind of work. Auriga classifies each issue as a "hive story" or not, and routes accordingly.

Hive stories

A hive story is one authored for the plugin-hive SDLC workflow (/hive:execute, /hive:review, /hive:test). Only Claude+plugin-hive lanes can run hive stories — Codex/Opencode lanes do not have the plugin-hive install. isHiveStory(issue, cfg) detects this from the issue's content.

Lane selection

Issue typeLaneConfig key
Hive storyClaude + plugin-hiveHIVE_LANE
Non-hive storyDefault (Codex/Opencode)DEFAULT_LANE
in_review + open PRReview squadREVIEW_LANE

Lane maps are configured in config-substrate.mjs as PROJECT_LANE — a map from Multica project ID to the lane for that project's stories. This lets different Pantheon tenants or gods route to different lane pools.


Human-todo filter

Some issues are flagged for human attention, not agent dispatch. Auriga's priority-1 filter excludes these from the dispatch pool before any lane or capacity logic runs.

How an issue becomes human-todo

An issue is a human-todo if it carries the label human-todo, or if its metadata includes waiting_on: <human name> where the name matches one of the configured HUMAN_NAMES (matched case-insensitively, substring OK).

Dependency waiting_on

waiting_on values that don't match a known human name (e.g. an issue identifier like PAN-1234, meaning "waiting on that dependency to complete") are not treated as human-todo — the issue stays in the normal dispatch pool.

Human queue export

Human-todo issues are never just silently dropped. Run the export script to write them to .pHive/human-queue.yaml for a human to triage:

# from repo root
node scripts/export-human-queue.mjs

# or from src/router/
node ../../scripts/export-human-queue.mjs

Review squad

When an issue enters in_review with an open PR, Auriga fires the review squad — the "back-half" of the loop. The squad runs four perspectives and either ships the PR (merge to dev + story done) or sends it back with concrete per-perspective feedback.

The four perspectives

PerspectiveWhat it judgesHow
product (PO) Does the diff satisfy the story's intent and acceptance criteria? Reads ticket vs delivered diff
technical Correctness, conventions, security, maintainability /hive:review on the real diff
qa True verification — not a diff read Checks out branch, runs real build + tests, Playwright/E2E
ux User-facing surface quality + accessibility /hive:design-review / visual QA against running UI

Squad sizing (scale by ticket type)

Running the full four perspectives on every ticket wastes resources. Auriga's reviewSquadPlan(issue, cfg) classifies the issue from REVIEW_SQUAD_RULES and drops inapplicable perspectives:

TierSignalPerspectivesPlaywright
fullUser-facing / UI signals (react, page, dashboard, css…)All fourOn
backendHeadless API/service signals (endpoint, router, service…)product + technical + qaOff
lightDocs/chore/config signals (readme, typo, chore…)technical + qa-smokeOff
standardNo decisive signal (safe default)All fourOn

The plan (tier, perspectives, playwright flag) is logged and posted as a comment on the ticket before the squad runs — so what the squad will do is always visible up front.

Never force-merges

The review squad merges only when every enabled perspective is PASS and QA actually ran. Any CHANGES verdict sends the story back to todo with per-perspective feedback. It never force-merges a failing PR.


Zombie recovery

A zombie is an in_progress issue whose run has stalled, failed, or simply gone quiet for longer than zombieStaleMs (default: 20 minutes). Auriga's detectZombies() pass recovers them each cycle.

Recovery behavior

  1. Detect — find in_progress issues where the latest run is done/failed or stale past the threshold.
  2. Requeue — if attempts < zombieMaxAttempts (default: 3), re-assign and re-run the issue.
  3. Give up — if max attempts exhausted, set status to blocked and post a comment explaining why. Never silently drops — always leaves a visible record.

Bounded retries

Without a cap, a genuinely broken story would be re-queued indefinitely. zombieMaxAttempts bounds this — after the cap is hit, the issue becomes blocked and requires human attention to unblock.