Skip to content

Architecture Overview

The Big Picture

config.yml

Bootstrap (load config → Wire DI → Kernel)

Registry DSL (declare tasks, multiply by partitions)

Planner (split by Phase)

Scheduler
  ├── Phase 1: Preflights (one-shot, all must pass)
  └── Phase 2: Workers (interval tasks)

          longrun.Runner (per-error retry, backoff, degraded mode)

Directory Structure

cmd/                    CLI entry points (cobra)
internal/
├── bootstrap/          App init (Bootstrap → Wire → Kernel)
├── config/             Typed config structs (viper-backed)
├── domain/             Business logic
│   ├── repositories.go RepositoryStore interface (domain contract)
│   ├── entities/       Issue, Cursor, User
│   └── spec/           Task specs
│       ├── repository/ Partition, TaskSpec, Validator, IssuePoller, OutboxRelay
│       └── global/     IssueExplainer
├── application/        Orchestration layer
│   └── schedule/
│       ├── schedule.go     Two-phase Scheduler
│       ├── planner.go      Phase-based task splitting
│       ├── registry.go     Declarative task registry (DSL)
│       ├── repository_tasks.go  Per-repo task multiplication
│       └── global.go       Global task helpers
└── infrastructure/     External dependencies
    ├── github/         GitHub API client + rate limiter
    ├── dal/            Data access (sqlc-generated)
    ├── database/       SQLite + goose migrations
    └── telemetry/      OTEL SDK bootstrap
pkg/
└── longrun/            Task orchestration (standalone package)

Key Design Decisions

Domain Is Naive

Domain types (RepositoryValidator, IssuePoller) declare what to do via TaskSpec. They receive their partition as an honest function argument — no context injection, no lifecycle awareness. They don't know about retry, backoff, multi-repo multiplication, or error classification. That's all handled by the application layer.

Registry DSL Owns the Manifest

The registry (NewTasks) reads like a table of contents: what runs, under which partition, in which phase. repos.Pack() multiplies per-repo tasks. globalTasks() wraps partition-free tasks. Preflight() marks one-shot setup tasks. Adding a task = one line.

Partition Providers Own Multiplication

RepositoryTasks.Pack() takes domain specs and multiplies them by configured repositories. Each repo gets its own closure with a captured partition. Domain code receives a repository.Partition and does its job — it never knows how many repos exist.

Error Classification Pipeline

err from work()
  ├─ Built-in transport classify (net errors → Node)
  ├─ User classifier (apierr interfaces → Service)
  └─ Not classified → Unknown
       Degraded policy set → retry with loud logging
       Degraded policy nil → permanent error (crash)

Preflights have no Degraded policy — unknown errors crash the app. Workers have Degraded — they survive and retry.

Wire DI — No Magic

All dependencies are wired via Google Wire. Constructors are pure: accept deps, assign fields, return. No side effects, no goroutines in constructors.

Dive Deeper

Apache 2.0 · Built in public · Contributions welcome