State Management
Genie tracks state primarily in PostgreSQL via the embedded pgserve database. Wishes, tasks, boards, projects, agents, teams, mailboxes, and team chat all live in PG tables. Legacy JSON file state has been replaced by database-backed equivalents.
Wish State Machine
Wish execution is tracked in PostgreSQL via the task service. A wish becomes a parent task, and each execution group becomes a child task with dependency edges.
State Transitions
blocked → ready → in_progress → done
↓
failed
| State | Meaning |
|---|
blocked | Waiting for dependencies to complete |
ready | All dependencies satisfied, can be started |
in_progress | An agent is actively working on this group |
done | Group completed successfully |
Dependency Resolution
When a group completes, completeGroup() recalculates all dependent groups. If every dependency of a blocked group is now done, that group transitions to ready.
// Schemas from wish-state.ts
const GroupStatusSchema = z.enum(['blocked', 'ready', 'in_progress', 'done']);
const GroupStateSchema = z.object({
status: GroupStatusSchema,
assignee: z.string().optional(),
dependsOn: z.array(z.string()).default([]),
startedAt: z.string().optional(),
completedAt: z.string().optional(),
});
The wish state machine stores state in the PG tasks + task_dependencies tables. PG handles concurrency natively — no file locks are needed for wish state.
Board State
Boards provide project-scoped kanban pipelines. Each board has ordered columns with gate types and action skills:
| Table | Purpose |
|---|
boards | Board definitions with project scope |
board_templates | Reusable pipeline blueprints |
Tasks are assigned to board columns via column_id. Use genie board reconcile to fix orphaned column references after pipeline changes.
Project State
Projects group boards and tasks into named scopes:
| Table | Purpose |
|---|
projects | Named task boards for multi-repo organization |
Tasks scope to projects via project_id. When running genie task list inside a repo, it auto-scopes to that repo’s project.
Worker Registry
Every spawned agent is tracked in the PostgreSQL agents table. The registry stores provider metadata, transport info, and lifecycle state.
Agent States
spawning → working → idle → done
↓ ↓
error suspended
↓
permission
↓
question
| State | Meaning |
|---|
spawning | Agent process being created |
working | Actively producing output |
idle | At prompt, waiting for input |
permission | Waiting for permission approval |
question | Waiting for user answer |
done | Task completed |
error | Encountered an error |
suspended | Pane killed, session preserved for resume |
Agent Record
Each agent record includes:
interface Agent {
id: string; // Unique ID (e.g., "wish-42")
paneId: string; // tmux pane ID (e.g., "%16")
session: string; // tmux session name
worktree: string | null; // Git worktree path
state: AgentState; // Current lifecycle state
repoPath: string; // Repository this agent operates in
provider?: ProviderName; // "claude" or "codex"
transport?: TransportType; // Always "tmux"
role?: string; // "engineer", "reviewer", "qa", "fix"
team?: string; // Team membership
claudeSessionId?: string; // For session resume
wishSlug?: string; // Associated wish
groupNumber?: number; // Execution group
}
PostgreSQL handles concurrency natively — no file locks needed.
Team Management
Teams are stored in the PostgreSQL teams table. Each team owns an isolated clone of the repository.
Team Lifecycle
created → in_progress → done
↓
blocked
Isolation Model
Teams use git clone --shared instead of git worktree to avoid a known bug where Claude Code workers can flip core.bare=true on the parent repo via shared .git metadata, silently corrupting it.
interface TeamConfig {
name: string; // Also the git branch name
repo: string; // Source repository path
baseBranch: string; // Branch created from (e.g., "dev")
worktreePath: string; // Absolute path to clone
leader?: string; // Team leader agent name
members: string[]; // Member agent names
status: TeamStatus; // "in_progress" | "done" | "blocked"
nativeTeamsEnabled?: boolean; // Claude Code native IPC
}
Mailbox
Messages persist to the PostgreSQL mailbox table before any push delivery attempt. This ensures durability — even if tmux delivery fails, the message is stored in the database.
interface MailboxMessage {
id: string; // Unique message ID
from: string; // Sender worker ID or "operator"
to: string; // Recipient worker ID
body: string; // Message text
createdAt: string; // ISO timestamp
read: boolean; // Whether recipient has read this
deliveredAt: string | null; // null = pending delivery
}
Delivery is state-aware: messages are queued and pushed to tmux panes only when the worker is idle (not mid-turn). A sent message log is also kept in an append-only JSONL outbox file.
Mailbox delivery is best-effort. Messages are persisted to PostgreSQL (durable), but tmux pane injection is not retried. If a pane dies, the message stays with deliveredAt: null.
Team Chat
Each team has a group chat stored in the PostgreSQL team_chat table.
interface ChatMessage {
id: string; // Unique message ID
sender: string; // Agent name
body: string; // Message text
timestamp: string; // ISO timestamp
}
Team chat is scoped per team — each team maintains its own chat history in the database.