--- name: beads-state description: Git-friendly, crash-recoverable state persistence for the Gastown orchestration chipset. Manages agent identities, work items, hooks, convoys, and merge requests as JSON files with atomic write operations. type: skill category: state status: stable origin: tibsfox modified: false first_seen: 2026-03-06 first_path: .claude/skills/beads-state/SKILL.md superseded_by: null --- # Beads State Persistence Filesystem-backed state management for multi-agent orchestration. All state is stored as individual JSON files using atomic write operations. No database dependencies. ## State Entities ### Agent Identity **Path:** `.chipset/state/agents/{id}.json` Persistent record of an agent in the topology. Contains role, rig assignment, hook pointer, lifecycle status, and optional ephemeral session ID. ```typescript interface AgentIdentity { id: string; // Unique identifier (e.g., 'polecat-alpha') role: AgentRole; // mayor | witness | refinery | polecat | crew rig: string; // Parent rig name hookId: string; // Pointer to hook bead in state/hooks/ status: AgentStatus; // idle | active | stalled | terminated sessionId?: string; // Present only while agent is active } ``` ### Work Item **Path:** `.chipset/state/work/{bead-id}.json` A unit of work flowing through the dispatch pipeline. Created by the mayor, assigned via hooks, tracked through lifecycle. ```typescript interface WorkItem { beadId: string; // Bead-style ID (prefix-xxxxx) title: string; description: string; status: WorkStatus; // open | hooked | in_progress | done | merged assignee?: string; // Agent ID (undefined if unassigned) hookStatus: HookStatus; // empty | pending | active | completed priority: 'P1' | 'P2' | 'P3'; } ``` ### Hook **Path:** `.chipset/state/hooks/{agent-id}.json` Current work assignment for an agent. Enforces GUPP (Get Up and Push Protocol) -- one agent, one work item at a time. ```typescript interface HookState { agentId: string; status: HookStatus; workItem?: WorkItem; // Present when hook is pending/active lastActivity: string; // ISO 8601 timestamp } ``` **Constraint:** An agent can hold at most one hook at a time. Setting a new hook on an agent that already has an active hook is rejected. The caller must clear the existing hook first. ### Convoy **Path:** `.chipset/state/convoys/{id}.json` Groups related work items for batch tracking. The mayor creates convoys to organize beads and monitor aggregate progress. ```typescript interface Convoy { id: string; name: string; beadIds: string[]; progress: number; // 0.0 to 1.0 createdAt: string; // ISO 8601 } ``` ### Merge Request **Path:** `.chipset/state/merge-queue/{id}.json` Queued for the refinery. Processed strictly sequentially -- no parallel merges. Conflicts block the queue and escalate. ```typescript interface MergeRequest { id: string; sourceBranch: string; targetBranch: string; status: 'pending' | 'merging' | 'merged' | 'conflicted'; beadId: string; } ``` ## Filesystem Layout ``` .chipset/state/ agents/ Agent identity JSON files hooks/ GUPP hook state per agent work/ Work item beads convoys/ Batch tracking merge-queue/ Refinery merge requests ``` Each entity is a single JSON file. The directory structure acts as the "table" -- listing files is the equivalent of a database scan. ## Durability Contract ### Atomic Writes All mutations follow a three-step atomic write protocol: 1. **Write** content to a temporary file in the same directory (`.{name}.tmp`) 2. **Fsync** the temporary file to ensure data reaches disk 3. **Rename** the temporary file to the target path (atomic on POSIX filesystems) This guarantees that a reader always sees either the complete old state or the complete new state, never a partial write. If the process crashes between steps 1-2, only the temp file is left (cleaned up on next startup). If it crashes between steps 2-3, the temp file contains the full new state and can be recovered. ### JSON Format All JSON output uses `JSON.stringify` with sorted keys. This produces deterministic output that creates clean, minimal diffs when tracked by git. Sorted keys also make manual inspection easier -- fields appear in a predictable order. ```typescript // Sorted-key serialization function serialize(data: unknown): string { return JSON.stringify(data, Object.keys(data as object).sort(), 2); } ``` ### No Database Dependencies State is filesystem-only. No SQLite, no LevelDB, no external services. This means: - State is readable with `cat` and editable with any text editor - State is trackable by git (JSON diffs show exactly what changed) - State survives any process crash (atomic writes prevent corruption) - State works across all platforms (POSIX rename semantics) - State requires no setup beyond `mkdir -p` ## StateManager API The StateManager class provides typed CRUD operations for all state entities. ### Construction ```typescript const manager = new StateManager({ stateDir: '.chipset/state/' }); ``` The `stateDir` parameter is configurable. The manager creates subdirectories on first use. ### Agent Operations | Method | Signature | Description | |--------|-----------|-------------| | `createAgent` | `(role, rig) => AgentIdentity` | Generate unique ID, write agent JSON | | `getAgent` | `(id) => AgentIdentity \| null` | Read agent by ID, null if not found | | `updateAgentStatus` | `(id, status) => void` | Atomic status update | | `listAgents` | `(filter?) => AgentIdentity[]` | List all agents, optional role/rig filter | ### Work Item Operations | Method | Signature | Description | |--------|-----------|-------------| | `createWorkItem` | `(title, description, priority?) => WorkItem` | Generate bead ID, write work JSON | | `getWorkItem` | `(beadId) => WorkItem \| null` | Read work item by bead ID | | `updateWorkStatus` | `(beadId, status) => void` | Atomic status update | ### Hook Operations | Method | Signature | Description | |--------|-----------|-------------| | `setHook` | `(agentId, beadId) => void` | Assign work to agent (single assignment enforced) | | `getHook` | `(agentId) => HookState \| null` | Read hook state for agent | | `clearHook` | `(agentId) => void` | Remove hook assignment | ### Convoy Operations | Method | Signature | Description | |--------|-----------|-------------| | `createConvoy` | `(name, beadIds) => Convoy` | Create batch with member beads | | `getConvoy` | `(id) => Convoy \| null` | Read convoy by ID | | `updateConvoyProgress` | `(id) => void` | Recalculate progress from member bead statuses | ## Error Handling - **File not found:** Returns `null` for get operations. Never throws on missing state. - **Concurrent writes:** Last writer wins (rename is atomic). For coordination, use the convoy or hook layer. - **Corrupt JSON:** Log warning, return `null`. Caller decides recovery strategy. - **Disk full:** Propagates OS error. Temp file cleanup is best-effort. ## Usage Patterns ### Create and Assign Work ```typescript const agent = await manager.createAgent('polecat', 'my-rig'); const item = await manager.createWorkItem('Fix auth bug', 'JWT expiry not handled', 'P1'); await manager.setHook(agent.id, item.beadId); ``` ### Track Convoy Progress ```typescript const convoy = await manager.createConvoy('Sprint 1', [item1.beadId, item2.beadId]); // ... after some work completes ... await manager.updateConvoyProgress(convoy.id); const updated = await manager.getConvoy(convoy.id); console.log(`Progress: ${(updated!.progress * 100).toFixed(0)}%`); ``` ### Filter Agents by Role ```typescript const polecats = await manager.listAgents({ role: 'polecat' }); const idle = polecats.filter(a => a.status === 'idle'); ```