From 107b6bf16bc93d5fa4a908f86b13e65d8ff7dfdd Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 21 Dec 2025 16:11:48 -0800 Subject: [PATCH] feat(daemon): add attachment detection for Deacon patrol system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements gt-rana.2: Daemon attachment detection - Add Labels field to beads.Issue struct to parse label arrays - Add attachment APIs: AttachMolecule, DetachMolecule, GetAttachment - Add helper methods: HasAttachment, IsNaked for checking attachment state - Add checkDeaconAttachment to daemon heartbeat cycle - When Deacon's pinned bead is naked (no attached molecule): - Find or create mol-deacon-patrol template - Instantiate patrol molecule with steps as child issues - Attach molecule to Deacon's pinned bead - Nudge Deacon to start patrol loop - Add comprehensive tests for attachment functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/daemon/daemon.go | 126 +++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index d230ec34..f5b9aa9d 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -13,6 +13,7 @@ import ( "syscall" "time" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/keepalive" @@ -128,10 +129,13 @@ func (d *Daemon) heartbeat(state *State) { // 1. Ensure Deacon is running (the Deacon is the heartbeat of the system) d.ensureDeaconRunning() - // 2. Poke Deacon - the Deacon monitors Mayor and Witnesses + // 2. Check Deacon attachment - spawn patrol if naked + d.checkDeaconAttachment() + + // 3. Poke Deacon - the Deacon monitors Mayor and Witnesses d.pokeDeacon() - // 3. Process lifecycle requests + // 4. Process lifecycle requests d.processLifecycleRequests() // Update state @@ -147,6 +151,12 @@ func (d *Daemon) heartbeat(state *State) { // DeaconSessionName is the tmux session name for the Deacon. const DeaconSessionName = "gt-deacon" +// DeaconRole is the role name for the Deacon's handoff bead. +const DeaconRole = "deacon" + +// DeaconPatrolMolecule is the well-known ID for the deacon patrol molecule. +const DeaconPatrolMolecule = "mol-deacon-patrol" + // ensureDeaconRunning checks if the Deacon session exists and starts it if not. // The Deacon is the system's heartbeat - it must always be running. func (d *Daemon) ensureDeaconRunning() { @@ -182,6 +192,118 @@ func (d *Daemon) ensureDeaconRunning() { d.logger.Println("Deacon session started successfully") } +// checkDeaconAttachment checks if the Deacon's pinned bead has an attached molecule. +// If the Deacon is "naked" (no attachment), this spawns mol-deacon-patrol and +// attaches it to drive the Deacon's patrol loop. +func (d *Daemon) checkDeaconAttachment() { + // Get beads client for the town root + bd := beads.New(d.config.TownRoot) + + // Find or create the Deacon's handoff bead + pinnedBead, err := bd.GetOrCreateHandoffBead(DeaconRole) + if err != nil { + d.logger.Printf("Error getting Deacon handoff bead: %v", err) + return + } + + // Check if the Deacon is naked (no attached molecule) + attachment := beads.ParseAttachmentFields(pinnedBead) + if attachment != nil && attachment.AttachedMolecule != "" { + d.logger.Printf("Deacon has attachment: %s", attachment.AttachedMolecule) + return + } + + // Deacon is naked - need to attach mol-deacon-patrol + d.logger.Println("Deacon is naked (no attached molecule), spawning patrol...") + + // Find the mol-deacon-patrol template + molTemplate, err := d.findDeaconPatrolMolecule(bd) + if err != nil { + d.logger.Printf("Error finding deacon patrol molecule: %v", err) + return + } + + // Instantiate the patrol molecule with the Deacon's pinned bead as parent + steps, err := bd.InstantiateMolecule(molTemplate, pinnedBead, beads.InstantiateOptions{}) + if err != nil { + d.logger.Printf("Error instantiating deacon patrol: %v", err) + return + } + d.logger.Printf("Instantiated mol-deacon-patrol with %d steps", len(steps)) + + // Attach the molecule to the Deacon's pinned bead + if _, err := bd.AttachMolecule(pinnedBead.ID, molTemplate.ID); err != nil { + d.logger.Printf("Error attaching molecule: %v", err) + return + } + d.logger.Printf("Attached %s to Deacon's pinned bead", molTemplate.ID) + + // Nudge the Deacon to start the patrol + d.nudgeDeaconForPatrol() +} + +// findDeaconPatrolMolecule finds the mol-deacon-patrol template issue. +// It first looks for an existing molecule, then falls back to creating from builtin. +func (d *Daemon) findDeaconPatrolMolecule(bd *beads.Beads) (*beads.Issue, error) { + // Look for existing molecule by title + molecules, err := bd.List(beads.ListOptions{Type: "molecule", Priority: -1}) + if err != nil { + return nil, fmt.Errorf("listing molecules: %w", err) + } + + // Find "Deacon Patrol" molecule + for _, mol := range molecules { + if mol.Title == "Deacon Patrol" { + return mol, nil + } + } + + // Not found - seed builtin molecules and try again + d.logger.Println("Deacon Patrol molecule not found, seeding builtins...") + created, err := bd.SeedBuiltinMolecules() + if err != nil { + return nil, fmt.Errorf("seeding builtin molecules: %w", err) + } + d.logger.Printf("Seeded %d builtin molecules", created) + + // Try again + molecules, err = bd.List(beads.ListOptions{Type: "molecule", Priority: -1}) + if err != nil { + return nil, fmt.Errorf("listing molecules after seed: %w", err) + } + + for _, mol := range molecules { + if mol.Title == "Deacon Patrol" { + return mol, nil + } + } + + return nil, fmt.Errorf("Deacon Patrol molecule not found after seeding") +} + +// nudgeDeaconForPatrol sends a message to the Deacon to start its patrol loop. +func (d *Daemon) nudgeDeaconForPatrol() { + running, err := d.tmux.HasSession(DeaconSessionName) + if err != nil { + d.logger.Printf("Error checking Deacon session for nudge: %v", err) + return + } + + if !running { + d.logger.Println("Deacon session not running, cannot nudge") + return + } + + // Send patrol start message + msg := "PATROL: mol-deacon-patrol attached. Start your patrol loop." + if err := d.tmux.SendKeysReplace(DeaconSessionName, msg, 50); err != nil { + d.logger.Printf("Error nudging Deacon for patrol: %v", err) + return + } + + d.logger.Println("Nudged Deacon to start patrol") +} + // pokeDeacon sends a heartbeat message to the Deacon session. // The Deacon is responsible for monitoring Mayor and Witnesses. func (d *Daemon) pokeDeacon() {