Kanban for AI agent work.
Dispatch is a harness-agnostic Kanban and work dispatch layer for AI agents. It turns GitHub Issues and PR follow-ups into claimable queues, tracks agent runs, manages status transitions, and keeps an audit trail while GitHub remains the source of truth.
- Next.js: 16.2.6 (App Router)
- React: v19
- Prisma: v7
- Node: v24 (Dockerfile uses
node:24-bookworm-slim) - TypeScript: v6
- Tailwind CSS: v4
- GitHub is the authoritative source for all issue/PR data
- Dispatch Postgres stores:
- Cached issue metadata (not the authoritative source)
- Local project metadata
- Agent runs
- Audit logs
- Dispatch does NOT:
- Mount agent harness configuration files
- Require access to an agent harness local workspace
- Use GitHub Projects
- Require cluster-admin or broad Kubernetes RBAC
- Automatically close or complete tasks
GitHub API → Dispatch (cache) → UI
GitHub Labels ↔ Kanban Board ↔ Audit Log
Agent Runs → Dispatch → Agent Activity Page
status/backlog- Issue needs triage/grooming; not yet ready for agentsstatus/ready- Issue is groomed and actionable; available for agents to claimstatus/in-progress- Issue is being worked onstatus/in-review- Issue has an open PR pending review/mergestatus/done- Issue is completed (closed)
owner/*- Issue is owned by a specific person (e.g.,owner/alice,owner/bob). The Board Owner filter is derived only from syncedowner/*labels, not GitHub assignees.
agent/*- Issue is assigned to or being worked on by an agent (e.g.,agent/alpha,agent/beta). The Board Agent filter is derived only from syncedagent/*labels, notAgentRunnames, configured agents, or GitHub assignees.
- Optional:
project/*labels may exist on issues, but Dispatch Projects groups issues by repository by default.
priority/p0- Criticalpriority/p1- Highpriority/p2- Mediumpriority/p3- Low
type/bugtype/featuretype/choretype/researchtype/security
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string (canonical) |
GITHUB_TOKEN |
Yes | GitHub Personal Access Token or GitHub App token |
DISPATCH_AGENT_TOKEN |
Yes | Bearer token for agent API authentication |
GITHUB_REPOSITORIES |
Yes | Bootstrap seed config for repos to track. Accepts comma-separated or newline-separated values (e.g., myorg/repo1,myorg/repo2 or myorg/repo1 on separate lines). Repos can also be managed via Dispatch UI or /api/automation/repos after initial setup. |
DISPATCH_URL |
No | Base URL of your Dispatch instance (used by outbound clients and MCP bridge) |
DISPATCH_DATABASE_URL |
No | Alternative database URL alias — used if DATABASE_URL is not set |
NEXTAUTH_SECRET |
No | Secret for NextAuth.js (stub in Phase 1) |
NEXTAUTH_URL |
No | URL for NextAuth.js (stub in Phase 1) |
Resolution order: DATABASE_URL > DISPATCH_DATABASE_URL (for database URLs). DISPATCH_AGENT_TOKEN (for agent tokens). DISPATCH_URL (for instance URL).
To get started with Dispatch:
- Configure repos: Set
GITHUB_REPOSITORIESenv var (comma or newline separated) or add tracked repos via the UI after boot.GITHUB_REPOSITORIESis a one-time bootstrap seed — it is read only when the tracked-repos table is empty. Once any repo exists (seeded or added via UI), the env var is not consulted again, so updates must go through the UI or canonical API (POST /api/automation/repos). Env-seeded repos are taggedsource: "env"and shown with aseedbadge in/automation; user-added repos are taggedsource: "user". - Deploy with your database and GitHub token configured.
- Sync automation data:
POST /api/automation/sync(or use the Sync button on the Automation page). - Sync issues:
POST /api/sync(or use the Sync Issues action in the board UI). Agent harnesses or worker heartbeats may also trigger best-effort issue sync automatically. - View results: The Kanban Board shows synced issues; the Projects view groups them by repository.
Production database migrations (prisma migrate deploy) run automatically on container startup — no manual intervention needed.
Dispatch is designed as an internal-only application. It does not include public-facing authentication in Phase 1 (NextAuth.js is a stub for local development). The app remains internal unless external auth integration (e.g., OIDC/Authentik) is added later.
Ensure it is only accessible within your trusted network via ingress rules.
-
GitHub Repository Configuration
- Support configuring multiple GitHub repos to sync
- Use env vars for GitHub auth (PAT support)
-
Issue Sync
- Fetch open GitHub issues from configured repos
- Cache issue metadata in Postgres
- Store: number, repo, title, state, labels, assignees, URL, timestamps
-
Kanban Board
- Columns: Backlog, Ready, In Progress, In Review, Done
- Drag-and-drop between columns updates GitHub labels
- Audit log entry on every mutation
- Visible error on GitHub mutation failure
-
Filtering
- Filter by repo and priority
- Filter by agent and owner labels; empty agent/owner dropdowns mean no synced issues currently carry
agent/*orowner/*labels
-
Project View
- Group synced issues by repository
- Show issues by status per repository project
-
Agent Activity Ingestion
POST /api/agent-runswith bearer token auth- Store agent name, run type, status, timestamps, summary, touched issues
-
Overview Page
- Open issues by status
- Issues per agent
- Stale in-progress issues
- Recent agent runs
- Recent audit log entries
-
Audit Log
- Record every board mutation with actor, action, before/after labels, success/failure
-
Deployment
- Dockerfile for app
- Kubernetes manifests (bjw-s/app-template style)
- ExternalSecret placeholders for all secrets
- Internal-only ingress
- GitHub Projects integration
- Full OIDC/Authentik authentication (stub for local dev only)
- Automatic task completion
- Broad Kubernetes RBAC
- S3/PVC storage
- Redis/Dragonfly caching
- Kanban card editing beyond status
- Complex project management features
# Install dependencies
npm install
# Generate Prisma client
npm run db:generate
# Push schema to database (local dev only - no migrations exist yet)
npm run db:push
# Start development server
npm run devRun the smoke/regression suite locally with:
npm run testThe tests are intentionally lightweight and do not require Postgres, GitHub API access, or secrets. They cover:
- repository env parsing and validation
- BigInt-safe JSON serialization for automation API responses
- critical API route file presence
- issue sync response shaping and per-repo error handling
- Kanban status grouping, including no-status issues appearing in Backlog
- project grouping by repository boundaries
- dark mode toggle class and localStorage behavior
The CI workflow runs lint, typecheck, tests, and build. The image workflow builds the Docker image, publishes GHCR images on main and v* tags, and uploads advisory Trivy scan results.
Recommended Renovate flow:
- Merge the test harness first.
- Let Renovate PRs rebase onto the new checks.
- Merge low-risk dependency PRs first.
- Handle framework, Prisma, and React updates separately.
Dispatch uses Prisma with PostgreSQL. Migrations are used for production; db push is for local development only.
- Use
npm run db:pushto apply schema changes without migrations.
- Production automatically runs
prisma migrate deployon container startup. - First deploy against an empty database creates all tables automatically.
- No manual
kubectl execordb pushis required. - Set
SKIP_DB_MIGRATIONS=trueto skip migrations if needed.
# Production (automatic on startup):
npm run db:deploy
# Local dev only:
npm run db:pushThe app uses the bjw-s/app-template Helm chart. See home-ops/kubernetes/apps/base/llm/dispatch/ for manifests.
Required secrets (via ExternalSecret):
DATABASE_URL- PostgreSQL connection string (canonical)GITHUB_TOKEN- GitHub authenticationDISPATCH_AGENT_TOKEN- Agent API bearer token
List cached issues. Query params: repo, agent, owner, project, priority
Move issue between status columns. Body: { issueId, repoFullName, issueNumber, oldLabels, newLabels }
List configured repositories for the board/issue-sync view (Repository rows). This is not the tracked repository management API.
Deprecated compatibility endpoint for adding a tracked repository. Prefer POST /api/automation/repos; this endpoint delegates to the same behavior and returns deprecation headers.
Canonical tracked repository list for automation. Returns AutomationRepo rows with workflow/release summary fields used by /automation.
Canonical tracked repository add endpoint. Body: { fullName: "owner/repo" }. Creates an AutomationRepo row with source: "user" and a mirror enabled Repository row so issue sync and the board see it immediately. Returns 409 if the repo is already tracked. Writes an add_tracked_repo AuditLog entry.
Stop tracking a repository. [repo] is the URL-encoded owner/repo fullName. Hard-deletes the AutomationRepo row (cascading workflow/run/release history) and soft-disables the mirror Repository row (enabled = false) so cached issues remain visible for history but are excluded from active board filters. Writes a remove_tracked_repo AuditLog entry.
/api/automation/repositories and /api/automation/repositories/[id] were legacy duplicate routes and have been removed. Use /api/automation/repos for tracked repository management.
Sync all issues from configured repositories. Intended callers are:
- the board UI's manual Sync Issues action
- agent harness or worker heartbeat best-effort cache refresh (see Scheduled Issue Sync Strategy below)
List agent runs. Query params: limit
Create agent run. Requires DISPATCH_AGENT_TOKEN bearer auth.
List audit logs. Query params: limit, repo
Note: Issue sync and automation sync are separate concerns.
- Issue sync (
POST /api/sync) refreshes GitHub issues into the Kanban board. It is heartbeat-driven (best-effort via agent harness) or manual via UI. - Automation sync (
POST /api/automation/sync) refreshes CI/CD, workflow runs, releases, and packages data. It is managed independently on the Automation page. - Each sync operates on its own data models and caching layer.
Dispatch includes an Automation section that discovers and visualizes CI/CD, builds, tests, security scans, releases, and scheduled workflows from GitHub repositories.
- GitHub REST API (
/repos,/actions/workflows,/actions/runs,/actions/jobs,/releases,/pulls,/packages) - Local repository scanning (for workflow path discovery)
For read-only automation visibility:
metadata:read- Repository metadatacontents:read- Workflow file accessactions:read- Workflow runs and jobspull_requests:read- PR associations with runspackages:read- Container/package metadata (if applicable)
For control actions (rerun, dispatch):
actions:write- Re-run workflows, trigger workflow_dispatch
| Variable | Required | Description |
|---|---|---|
GITHUB_REPOSITORIES |
Yes | Bootstrap seed config for tracked repos. Accepts comma-separated or newline-separated values (e.g., myorg/repo1,myorg/repo2). Managed repos can also be added/removed via UI at /automation. |
-
Automation Overview (
/automation)- One card per tracked repo
- Shows: repo name, default branch, latest commit SHA, workflow status, failing/running counts, latest release, open PR count
- Sync button to refresh data
- Link to GitHub repo
- Add/remove tracked repos via UI
-
Repo Automation Detail (
/automation/repos/[repo])- Workflow list with recent runs per workflow
- Release history
- Package/image tags
- Recent activity feed
- Sync status and error display
-
Workflow Detail (
/automation/workflows/[id])- Workflow name, path, state
- Recent runs with status, branch, SHA, actor, duration
- Success rate and average duration
- Jobs breakdown for latest run
- Link to GitHub workflow page
-
Activity Feed (
/automation/activity)- Unified event feed across all repos
- Events include: workflow runs, releases, PRs, sync completions
- Filterable by event type
-
Re-run failed workflow: POST to
/api/automation/runs/[runId]?action=rerun- Audited in AuditLog
- Requires:
repoFullNamequery param,runIdpath param - Requires GitHub token with
actions:writepermission
-
Trigger workflow dispatch: POST to
/api/automation/runs/[runId]?action=dispatch- Audited in AuditLog
- Triggers
workflow_dispatchon the workflow associated with the run's branch - Requires GitHub token with
actions:writepermission
- All GitHub automation state is cached in Postgres
lastSyncedAttimestamp onAutomationReposhows cache freshnesssyncErrorfield stores last sync failure for visibility- UI shows stale warnings when
lastSyncedAt> 1 hour ago - Sync runs are recorded in
AutomationSyncRuntable with stats
Run this checklist before pointing an agent harness at Dispatch as its task-visibility layer (instead of a GitHub Project board). Every step should pass; stop and investigate on the first failure.
Set BASE to your Dispatch URL (e.g. BASE=https://dispatch.internal) before running.
| # | Check | Expected |
|---|---|---|
| 1 | curl -fsS "$BASE/api/health" |
{"ok":true,"database":"ok",...} |
| 2 | curl -fsS -X POST "$BASE/api/automation/sync" |
2xx, no error body |
| 3 | curl -fsS "$BASE/api/automation/repos" |
JSON array, non-empty if repos are configured |
| 4 | curl -fsS -X POST "$BASE/api/sync" |
syncedCount > 0 |
| 5 | curl -fsS "$BASE/api/issues" |
JSON array, length > 0 |
| 6 | Open /board in a browser |
Issues render — no "no issues synced yet" empty state |
| 7 | Open /projects |
Repo groups render |
| 8 | Open /agents |
Recent agent heartbeat visible with agent name |
| 9 | Move a low-risk test issue between columns | GitHub label changes; AuditLog row appears in GET /api/audit |
| 10 | kubectl logs -n <ns> <pod> (or equivalent) |
No Prisma / BigInt / FK errors |
Only flip the agent's workflow over once all ten steps pass.
Dispatch keeps GitHub as the source of truth and stores issues only as a local cache. Cache freshness is owned by agent harness heartbeat sync rather than by a Dispatch background worker or new cluster CronJob.
Decision:
- At the start of each heartbeat, the agent harness should make a best-effort
POSTrequest to Dispatch's/api/syncendpoint. - The request must be non-blocking for heartbeat work: log/report a warning if the sync fails or times out, then continue the heartbeat.
- Manual UI sync remains supported for immediate refreshes and troubleshooting.
Rationale:
- Reuses the existing heartbeat that already reports to Dispatch, so no new Kubernetes manifests, images, queues, or background scheduler are required.
- Keeps cache freshness close to the agent workflow that consumes the board.
- Preserves Dispatch's simple app model: it serves API/UI requests and does not need long-running in-process scheduling state.
Rejected alternatives for the first implementation:
- Kubernetes CronJob: valid later if heartbeat-driven sync is too sparse, but it adds deployment and auth plumbing for little immediate benefit.
- Dispatch internal scheduler: would couple cache freshness to app process lifetime and introduces timer/queue behavior that Phase 1 intentionally avoids.
Operational notes:
- Configure the agent harness with
DISPATCH_URLand any required network access to reach Dispatch. /api/synccurrently does not requireDISPATCH_AGENT_TOKEN; if auth is added later, update the heartbeat caller and this section together.- Treat sync failures as freshness warnings, not heartbeat failures, unless the heartbeat itself cannot complete.
- No workflow YAML parsing: Trigger types (push, PR, schedule, manual) are not parsed from workflow files. GitHub shows workflow state (active/inactive) but not trigger configuration.
- No branch-specific runs: Only the most recent runs per workflow are fetched, not runs across all branches historically.
- No check runs/check suites: Job-level visibility is limited to Actions jobs; separate status checks from other integrations are not fetched.
- No Secrets scanning: Secret detection results from GitHub's secret scanning are not fetched (requires additional API endpoint).
- Package visibility: GitHub packages require the user to have appropriate permissions to view; private packages may not be visible.
- No webhook receiver for real-time updates: Issue cache refresh is heartbeat-driven and best-effort, not push-based from GitHub.
- No workflow run logs: Full logs are not stored; only run metadata and job status.
- Webhook receiver for real-time automation updates
- Workflow trigger parsing from YAML to show push/PR/schedule/dispatch triggers
- Branch-specific run history with filtering
- Check runs integration for non-Actions status checks
- Secret scanning results visibility
- Artifact listing for workflow runs
- Deployment status correlation (link runs to environments)
- Caching improvements with Redis if sync load becomes problematic
- Webhook-based issue sync if heartbeat freshness is not enough
The Dispatch Docker image is built and published via GitHub Actions CI/CD.
ghcr.io/misospace/dispatch
.github/workflows/image.yaml - Build Dispatch Image
- Push to
mainbranch - Pull requests targeting
main - Version tags (
v*) - Manual workflow dispatch
| Event | Tags |
|---|---|
Push to main |
main, sha-<shortsha> |
Version tag v1.2.3 |
1.2.3, 1.2, latest, sha-<shortsha> |
| Pull request | Build only, no push |
# Via GitHub CLI
gh workflow run image.yaml
# Via web: Actions > Build Dispatch Image > Run workflow-
GHCR Package Visibility: The package is published to
ghcr.io. Ensure the repository's GHCR package visibility is set to appropriate level (public or private with OCI registry access). -
Workflow Permissions: The workflow requires:
contents: read- for checkoutpackages: write- for pushing to GHCRpull-requests: read- for PR trigger context
These are set via
GITHUB_TOKENwhich is automatically granted. No additional secrets needed. -
OIDC: No cloud credentials required. Uses
GITHUB_TOKENfor GHCR authentication.
The Kubernetes deployment references the image:
spec:
containers:
- image: ghcr.io/misospace/dispatch:mainThe image tag main is updated on each push to the main branch via the CI workflow.
# Build locally
docker build -t ghcr.io/misospace/dispatch:local .
# Run locally
docker run -p 3000:3000 \
-e DATABASE_URL="postgresql://..." \
-e GITHUB_TOKEN="ghp_..." \
-e DISPATCH_AGENT_TOKEN="..." \
ghcr.io/misospace/dispatch:local