Files
gastown/docs/beads-data-plane.md

15 KiB

Beads as Data Plane (CLAW)

Status: Design documentation See also: pinned-beads-design.md, propulsion-principle.md

Overview

Gas Town agents coordinate through Beads - a git-backed issue tracker.

CLAW: Commits as Ledger of All Work. Git commits are the persistence mechanism. Every state change becomes a commit, giving us atomic updates, full history, and distributed sync for free.

We store agent state (work assignments, mail, molecules, hooks) as beads issues.

How We Use It

We're treating beads as more than an issue tracker:

  • Work molecules are issues with steps as child issues
  • Mail messages are issues with sender/recipient encoded in fields
  • Hooks are queries over issues (assignee = me AND pinned = true)
  • Inboxes are queries over issues (assignee = me AND status = open AND has from: label)

Everything is an issue. The semantics come from how fields are used.

The Unified Data Model

Every beads issue has these core fields:

Field Type Purpose
id string Unique identifier (e.g., gt-xxxx, hq-yyyy)
title string Brief summary
description string Full content
status enum open, in_progress, closed, pinned
assignee string Who this is assigned to
priority int 0=critical, 1=high, 2=normal, 3=low, 4=backlog
type string task, bug, feature, epic
labels []string Metadata tags
pinned bool Whether this is pinned to assignee's hook
parent string Parent issue ID (for hierarchies)
created_at timestamp Creation time

Field Reuse Patterns

Work Molecules

A molecule is the general abstraction for executable work in Gas Town. Molecules can take various shapes - structural patterns that encode different workflow semantics. The data plane stores all shapes the same way; the semantics come from how the structure is interpreted.

Common molecular shapes:

Shape Structure Use Case
Epic Parent + flat children (TODO list) Simple feature work, linear decomposition
Christmas Ornament Trunk + dynamic arms + base Patrol cycles with variable fanout
Compound Multiple bonded molecules Complex multi-phase workflows
Polymer Chained compound molecules Large-scale autonomous operation

All epics are molecules. Not all molecules are epics. For the full taxonomy of shapes, see molecular-chemistry.md.

Here's an epic (the simplest molecular shape) stored as beads data:

┌─────────────────────────────────────────────────────────────┐
│  Issue: gt-abc1                                             │
│  ─────────────────────────────────────────────────────────  │
│  title:       "Implement user authentication"               │
│  type:        epic                     ← SHAPE: TODO LIST   │
│  assignee:    "gastown/crew/max"                            │
│  pinned:      true                     ← ON MY HOOK         │
│  status:      in_progress                                   │
│  description: "Full auth flow with OAuth..."                │
│  children:    [gt-abc2, gt-abc3, gt-abc4]  ← FLAT CHILDREN  │
└─────────────────────────────────────────────────────────────┘

The hook query: WHERE assignee = me AND pinned = true

Why the data plane doesn't distinguish shapes: The shape lives in the structure (parent/child relationships, dependency edges), not in a separate field. An epic is recognized by its flat parent-children structure. A Christmas Ornament is recognized by dynamic arms bonded during execution. The beads database just stores issues with relationships - the chemistry layer interprets the shape.

Mail Messages

Mail reuses the same fields with different semantics:

MAIL FIELD          →    BEADS FIELD
═══════════════════════════════════════════════════════════

To (recipient)      →    assignee
Subject             →    title
Body                →    description
From (sender)       →    labels: ["from:mayor/"]
Thread ID           →    labels: ["thread:thread-xxx"]
Reply-To            →    labels: ["reply-to:hq-yyy"]
Message Type        →    labels: ["msg-type:task"]
Unread              →    status: open
Read                →    status: closed

Example mail message as a bead:

┌─────────────────────────────────────────────────────────────┐
│  Issue: hq-def2                                             │
│  ─────────────────────────────────────────────────────────  │
│  title:       "Fix the auth bug"           ← SUBJECT        │
│  assignee:    "gastown/crew/max"           ← TO             │
│  status:      open                         ← UNREAD         │
│  labels:      ["from:mayor/",              ← FROM           │
│                "thread:thread-abc",        ← THREAD         │
│                "msg-type:task"]            ← TYPE           │
│  description: "The OAuth flow is broken..."  ← BODY         │
└─────────────────────────────────────────────────────────────┘

The inbox query: WHERE assignee = me AND status = open AND has_label("from:*")

Distinguishing Mail from Work

How does the system know if an issue is mail or work?

Indicator Mail Work
Has from: label Yes No
Has pinned: true Rarely Yes (when on hook)
Parent is molecule No Often
ID prefix hq-* (town beads) gt-* (rig beads)

The from: label is the canonical discriminator. Regular issues don't have senders.

Two-Tier Beads Architecture

Gas Town uses beads at two levels, each with persistent and ephemeral components:

══════════════════════════════════════════════════════════════════════════
                            TOWN LEVEL
══════════════════════════════════════════════════════════════════════════

┌─────────────────────────────────┐  ┌─────────────────────────────────┐
│  PERSISTENT: ~/gt/.beads/       │  │  EPHEMERAL: ~/gt/.beads-wisp/   │
│  ─────────────────────────────  │  │  ─────────────────────────────  │
│  Prefix: hq-*                   │  │  Git tracked: NO                │
│  Git tracked: Yes               │  │  Contains:                      │
│  Contains:                      │  │    - Deacon patrol cycles       │
│    - All mail                   │  │    - Town-level ephemeral work  │
│    - Mayor coordination         │  │  Lifecycle:                     │
│    - Cross-rig work items       │  │    Created → Executed →         │
│  Sync: Direct commit to main    │  │    Squashed to digest → Deleted │
└─────────────────────────────────┘  └─────────────────────────────────┘

══════════════════════════════════════════════════════════════════════════
                             RIG LEVEL
══════════════════════════════════════════════════════════════════════════

┌─────────────────────────────────┐  ┌─────────────────────────────────┐
│  PERSISTENT: <clone>/.beads/    │  │  EPHEMERAL: <rig>/.beads-wisp/  │
│  ─────────────────────────────  │  │  ─────────────────────────────  │
│  Prefix: gt-* (rig-specific)    │  │  Git tracked: NO                │
│  Git tracked: Yes               │  │  Contains:                      │
│  Contains:                      │  │    - Witness patrol cycles      │
│    - Project issues             │  │    - Refinery patrol cycles     │
│    - Molecules (work patterns)  │  │    - Rig-level ephemeral work   │
│    - Agent hook states          │  │  Lifecycle:                     │
│  Sync: Via beads-sync branch    │  │    Created → Executed →         │
│                                 │  │    Squashed to digest → Deleted │
└─────────────────────────────────┘  └─────────────────────────────────┘

Key points:

  • Each level has persistent + ephemeral storage
  • Town persistent (~/gt/.beads/) - mail, mayor work, cross-rig coordination
  • Town ephemeral (~/gt/.beads-wisp/) - deacon patrols
  • Rig persistent (<clone>/.beads/) - project issues, molecules, hooks
  • Rig ephemeral (<rig>/.beads-wisp/) - witness/refinery patrols
  • Sync strategies: Persistent beads sync via git; ephemeral wisps never sync

The Query Model

Both hooks and inboxes are views (queries) over the flat beads collection:

BEADS DATABASE (flat collection)
══════════════════════════════════════════════════════════════

┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
│ hq-aaa   │  │ hq-bbb   │  │ gt-xxx   │  │ gt-yyy   │
│ mail     │  │ mail     │  │ task     │  │ molecule │
│ to: max  │  │ to: joe  │  │ assign:  │  │ assign:  │
│ open     │  │ closed   │  │   max    │  │   max    │
│          │  │          │  │ pinned:  │  │          │
│ ↓ INBOX  │  │          │  │   true   │  │          │
│          │  │          │  │ ↓ HOOK   │  │          │
└──────────┘  └──────────┘  └──────────┘  └──────────┘


INBOX QUERY (for max):
  SELECT * FROM beads
  WHERE assignee = 'gastown/crew/max'
    AND status = 'open'
    AND labels CONTAINS 'from:*'
  → Returns: [hq-aaa]


HOOK QUERY (for max):
  SELECT * FROM beads
  WHERE assignee = 'gastown/crew/max'
    AND pinned = true
  → Returns: [gt-xxx]

There is no container. No inbox bead. No hook bead. Just queries over issues.

Session Cycling Through the Data Lens

When an agent cycles (hands off to fresh session), the data model ensures continuity:

What Persists (in beads)

Data Location Survives Restart
Pinned molecule Rig beads Yes
Handoff mail Town beads Yes
Issue state Rig beads Yes
Git commits Git Yes

What Doesn't Persist

Data Why Not
Claude context Cleared on restart
In-memory state Process dies
Uncommitted changes Not in git
Unflushed beads Not synced

The Cycle

END OF SESSION                         START OF SESSION
══════════════════                     ══════════════════

1. Commit & push code                  1. gt prime (loads context)

2. bd sync (flush beads)               2. gt mol status
   └─ Molecule state saved                └─ Query: pinned = true
                                             ↓
3. gt handoff -s "..." -m "..."        3. Hook has molecule?
   └─ Creates mail in town beads          ├─ YES → Execute it
      (assignee = self)                   └─ NO  → Query inbox
                                                   ↓
4. Session dies                        4. Inbox has handoff mail?
                                          ├─ YES → Read context
                                          └─ NO  → Wait for work

The molecule is the source of truth for what you're working on. The handoff mail is supplementary context (optional but helpful).

Command to Data Mapping

Command Data Operation
gt mol status Query: assignee = me AND pinned = true
gt mail inbox Query: assignee = me AND status = open AND from:*
gt mail read X Read issue X, no status change
gt mail delete X Close issue X (status → closed)
gt sling X [Y] Hook X to Y (or self), inject start prompt
gt hook X Update X: pinned = true (assign without action)
gt handoff X Hook X, restart session (GUPP kicks in)
bd pin X Update X: pinned = true
bd close X Update X: status = closed

Why This Design?

1. Single Source of Truth

All agent state lives in beads. No separate databases, no config files, no hidden state. If you can read beads, you can understand the entire system.

2. Queryable Everything

Hooks, inboxes, work queues - all are just queries. Want to find all blocked work? Query. Want to see what's assigned to a role? Query. The data model supports arbitrary views.

3. Git-Native Persistence

Beads syncs through git. This gives you:

  • Version history
  • Branch-based isolation
  • Merge conflict resolution
  • Distributed replication

4. No Schema Lock-in

New use cases emerge by convention, not schema changes. Mail was added by reusing assignee for recipient and adding from: labels. No database migration needed.