Architecture¶
C4 Diagrams¶
See the C4 Diagrams page for all rendered diagrams.
Clean Architecture¶
The API follows Clean Architecture — dependencies point inward only.
┌─────────────────────────────────────────────────────┐
│ ConductorE.Api (Frameworks & Drivers) │
│ │
│ Program.cs DI wiring, endpoint routing │
│ Adapters/ │
│ MartenEventStore Implements IEventStore │
│ MartenIssueQuery Implements IIssueQuery │
│ MartenAgentQuery Implements IAgentQuery │
│ MartenProjections Marten-specific projections │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ ConductorE.Core (Domain + Use Cases) │ │
│ │ │ │
│ │ Domain/ │ │
│ │ Events.cs Pure event records │ │
│ │ ReadModels.cs Pure read model records │ │
│ │ │ │
│ │ Ports/ │ │
│ │ IEventStore Interface │ │
│ │ IIssueQuery Interface │ │
│ │ IAgentQuery Interface │ │
│ │ │ │
│ │ UseCases/ │ │
│ │ SubmitEvent Maps request → event │ │
│ │ │ │
│ │ ⚠ ZERO external dependencies │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Core has zero NuGet packages. No Marten, no ASP.NET, no framework code. Domain events, read models, ports, and use cases are pure C#.
Marten is only in the Api project (adapters). If we ever swap PostgreSQL for another event store, only the adapters change — Core stays untouched.
Two-Component Design¶
Conductor-E runs as two components in the same Kubernetes namespace:
┌──────────────────── conductor-e namespace ────────────────────┐
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Conductor-E │ HTTP │ Conductor-E API │ │
│ │ (Rig Agent) │───────▶│ (.NET 10) │ │
│ │ │ │ │ │
│ │ Discord bot │ │ Domain: │ │
│ │ GitHub MCP tools │ │ Events │ │
│ │ Claude Haiku │ │ ReadModels │ │
│ │ 1-year sub token │ │ Ports: │ │
│ └──────────────────┘ │ IEventStore │ │
│ │ IIssueQuery │ │
│ │ Adapters: │ │
│ │ Marten → PG │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌────────▼─────────┐ │
│ │ PostgreSQL 16 │ │
│ │ Marten schema │ │
│ └──────────────────┘ │
└───────────────────────────────────────────────────────────────┘
Event Flow¶
sequenceDiagram
participant H as Human/Agent
participant A as Rig Agent Runtime (Discord)
participant UC as SubmitEvent (Use Case)
participant P as IEventStore (Port)
participant M as MartenEventStore (Adapter)
participant PG as PostgreSQL
H->>A: Message in #conductor-e
A->>A: Claude processes message
A->>UC: SubmitEventRequest
UC->>UC: MapToEvent (domain logic)
UC->>P: AppendAsync(streamId, event)
P->>M: Marten session.Events.Append
M->>PG: INSERT + inline projection update
PG-->>M: Stored
M-->>UC: Done
UC-->>A: SubmitEventResponse
A-->>H: "Issue #547 queued"
Stream Identity¶
String-based (not Guid):
- Issue streams:
dashecorp/conductor-e#42 - Agent streams:
dev-e-1
Projections¶
Marten inline projections (update synchronously with event append):
| Projection | Source Events | Key Fields |
|---|---|---|
| IssueStatus | All lifecycle events | state, agentId, prNumber, priority, attempt |
| AgentStatus | Heartbeat, IssueAssigned, IssueDone, AgentStuck | status, currentIssue, completed/failed counts |
Dashboard¶
Dashboard.html is a single-page control plane UI served at /. It auto-refreshes every 30 seconds.
| Panel | Data source | Description |
|---|---|---|
| Agents | GET /api/agents |
Online/offline status, current task, heartbeat age |
| Queue | GET /api/queue, GET /api/issues?state=in_progress |
Pending and active issues |
| Queues | GET /api/streams/status, GET /api/streams/{agentId} |
Per-agent Valkey stream depths with last-assigned entry. Rows with >20 queued items are highlighted amber. Clicking a row expands to show the last 10 queued items with GitHub links. |
| Costs | GET /api/costs/summary |
30-day cost breakdown per agent |
| Logs | GET /api/agent-logs + SSE |
Real-time agent log stream |
Duplicate PR Auto-Close¶
When two agents claim the same issue in parallel (claim-race), each opens a PR. After the winning PR merges, Conductor-E automatically closes any sibling PRs that reference the same issue.
Trigger: MERGED event (from MergeGate or GitHub webhook pull_request.closed)
Behaviour:
1. Scans all open PRs in the target repo for bodies matching Closes/Fixes/Resolves #N (or cross-repo owner/repo#N).
2. Closes each sibling PR with a comment:
**Duplicate of #<MERGED_PR>** — closed automatically by Conductor-E.
PR #<MERGED_PR> covered the same issue #<N> and merged at <MERGED_AT>.
Two agents claimed the same issue; this one lost the race.
session:{repo}#{N} (the agent claim lock).
4. Emits a DUPLICATE_PR_CLOSED event with fields: repo, mergedPrNumber, closedPrNumber, issueNumber.
Implementation: src/ConductorE.Api/Services/DuplicateCloseService.cs, called from MergeGate.Merge() and the webhook pull_request.closed handler.
Note: This does not prevent the double-claim — atomic
SETNX claim:{repo}#{N}is tracked in a separate issue.
Test Coverage¶
| Suite | Tests | Line | Branch |
|---|---|---|---|
| Core (unit) | 44 | 70.3% | 90% |
| API (unit, DuplicateCloseService) | 13 | — | — |
| API (integration, Testcontainers PostgreSQL) | 11 | 32.9% | 25.4% |
| Total | 68 | — | — |
Core coverage gap is auto-generated record methods. API coverage gap is ASP.NET framework-generated code. All business logic is covered.