Skip to main content

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
StateMeaning
blockedWaiting for dependencies to complete
readyAll dependencies satisfied, can be started
in_progressAn agent is actively working on this group
doneGroup 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:
TablePurpose
boardsBoard definitions with project scope
board_templatesReusable 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:
TablePurpose
projectsNamed 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
StateMeaning
spawningAgent process being created
workingActively producing output
idleAt prompt, waiting for input
permissionWaiting for permission approval
questionWaiting for user answer
doneTask completed
errorEncountered an error
suspendedPane 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.

Message Format

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.