diff --git a/docs/molecular-chemistry.md b/docs/molecular-chemistry.md index 32b3a43f..517953a5 100644 --- a/docs/molecular-chemistry.md +++ b/docs/molecular-chemistry.md @@ -37,8 +37,8 @@ Work in Gas Town exists in three phases, following the states of matter: ### Proto (Solid Phase) -Protos are **frozen workflow patterns** - crystallized templates that encode -reusable work structures. They're the "molds" from which instances are cast. +Protos or protomolecules are **frozen workflow patterns** - crystallized templates that +encode reusable work structures. They're the "molds" from which instances are cast. ```markdown ## Molecule: engineer-in-box @@ -52,9 +52,13 @@ Needs: (none) Write the code. Needs: design +## Step: review +Perform initial code review. +Needs: implement + ## Step: test Write and run tests. -Needs: implement +Needs: review ## Step: submit Submit for merge. @@ -62,10 +66,11 @@ Needs: test ``` **Properties:** -- Immutable once defined (frozen) -- Named (e.g., `mol-engineer-in-box`, `mol-code-review`) +- Considered immutable once defined (frozen), though editable +- Named (e.g., `mol-engineer-in-box`, `wisp-deacon-patrol`) + - Can have any name, but convention says how they will materialize. - Stored in permanent beads with `template` label -- Can be composed into larger protos (polymers) +- Can be composed into larger protos (compounds, polymers) ### Mol (Liquid Phase) diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index a7faecb2..075f9fbc 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -460,10 +460,13 @@ func outputStartupDirective(ctx RoleContext) { fmt.Println("**STARTUP PROTOCOL**: You are the Deacon. Please:") fmt.Println("1. Announce: \"Deacon, checking in.\"") fmt.Println("2. Signal awake: `gt deacon heartbeat \"starting patrol\"`") - fmt.Println("3. Check for attached patrol: `bd list --status=in_progress --assignee=deacon`") - fmt.Println("4. If attached: resume from current step") - fmt.Println("5. If naked: `gt mol bond mol-deacon-patrol`") + fmt.Println("3. cd to rig: `cd gastown/mayor/rig`") + fmt.Println("4. Check Patrol Status above - if attached, resume from current step") + fmt.Println("5. If naked, start wisp patrol:") + fmt.Println(" - Find proto ID: `bd mol list` (look for mol-deacon-patrol)") + fmt.Println(" - Spawn: `bd --no-daemon mol spawn `") fmt.Println("6. Execute patrol steps until loop-or-exit") + fmt.Println("7. At cycle end: `bd --no-daemon mol squash --summary \"\"`") } } @@ -609,70 +612,86 @@ func showMoleculeProgress(b *beads.Beads, rootID string) { } // outputDeaconPatrolContext shows patrol molecule status for the Deacon. +// Deacon uses wisps (Wisp:true issues in main .beads/) for patrol cycles. +// Spawn creates wisp-marked issues that are auto-deleted on squash. func outputDeaconPatrolContext(ctx RoleContext) { - b := beads.New(ctx.TownRoot) - - // Check for in-progress patrol steps assigned to deacon - issues, err := b.List(beads.ListOptions{ - Status: "in_progress", - Assignee: "deacon", - Priority: -1, - }) - if err != nil { - // Silently skip if beads lookup fails - return - } - fmt.Println() - fmt.Printf("%s\n\n", style.Bold.Render("## 🔄 Patrol Status")) + fmt.Printf("%s\n\n", style.Bold.Render("## 🔄 Patrol Status (Wisp-based)")) - if len(issues) == 0 { - // No attached molecule - show "naked" status + // Check for active mol-deacon-patrol molecules in rig beads + // A patrol is "active" if it has open wisp children (steps to execute) + // After squash, the root stays open but has no open children - that's "completed" + rigBeadsDir := filepath.Join(ctx.TownRoot, "gastown", "mayor", "rig") + + // First find mol-deacon-patrol molecules (exclude template) + cmdList := exec.Command("bd", "list", "--status=open", "--type=epic") + cmdList.Dir = rigBeadsDir + var stdoutList bytes.Buffer + cmdList.Stdout = &stdoutList + cmdList.Stderr = nil + errList := cmdList.Run() + + // Find a patrol molecule with open children + hasPatrol := false + var patrolLine string + var patrolID string + if errList == nil { + lines := strings.Split(stdoutList.String(), "\n") + for _, line := range lines { + if strings.Contains(line, "mol-deacon-patrol") && !strings.Contains(line, "[template]") { + // Extract the ID (first word) + parts := strings.Fields(line) + if len(parts) > 0 { + molID := parts[0] + // Check if this molecule has open children using bd show + cmdShow := exec.Command("bd", "show", molID) + cmdShow.Dir = rigBeadsDir + var stdoutShow bytes.Buffer + cmdShow.Stdout = &stdoutShow + cmdShow.Stderr = nil + if cmdShow.Run() == nil { + showOutput := stdoutShow.String() + // Check for "- open]" in children section (open child steps) + if strings.Contains(showOutput, "- open]") { + hasPatrol = true + patrolLine = line + patrolID = molID + break + } + } + } + } + } + } + _ = patrolID // Silence unused warning + + if !hasPatrol { + // No attached patrol - show "naked" status fmt.Println("Status: **Naked** (no patrol molecule attached)") fmt.Println() - fmt.Println("To start patrol:") - fmt.Println(" gt mol bond mol-deacon-patrol") + fmt.Println("To start patrol cycle:") + fmt.Println(" cd gastown/mayor/rig") + fmt.Println(" bd mol list # Find mol-deacon-patrol proto ID") + fmt.Println(" bd --no-daemon mol spawn # e.g., bd --no-daemon mol spawn gt-iep9") return } - // Find the patrol molecule step we're working on - for _, issue := range issues { - // Check if this is a patrol molecule step - moleculeID := parseMoleculeMetadata(issue.Description) - if moleculeID == "" { - continue - } - - // Get the parent (root) issue ID - rootID := issue.Parent - if rootID == "" { - continue - } - - // This is a molecule step - show context - fmt.Println("Status: **Attached** (patrol molecule in progress)") - fmt.Printf(" Current step: %s\n", issue.ID) - fmt.Printf(" Molecule: %s\n", moleculeID) - fmt.Printf(" Root issue: %s\n\n", rootID) - - // Show patrol progress - showMoleculeProgress(b, rootID) - - fmt.Println() - fmt.Println("**Patrol Work Loop:**") - fmt.Println("1. Execute current step: " + issue.Title) - fmt.Println("2. Close step: `bd close " + issue.ID + "`") - fmt.Println("3. Check next: `bd ready --parent " + rootID + "`") - fmt.Println("4. On final step (loop-or-exit): burn and loop or exit") - return - } - - // Has issues but none are molecule steps - might be orphaned work - fmt.Println("Status: **In-progress work** (not a patrol molecule)") + // Has patrol - show attached status + fmt.Println("Status: **Attached** (wisp patrol in progress)") fmt.Println() - fmt.Println("To start fresh patrol:") - fmt.Println(" bd close ") - fmt.Println(" gt mol bond mol-deacon-patrol") + // Show the patrol molecule details + fmt.Printf("Active patrol: %s\n\n", strings.TrimSpace(patrolLine)) + + fmt.Println("**Wisp Patrol Work Loop:**") + fmt.Println("Run from gastown/mayor/rig/:") + fmt.Println("1. Check next step: `bd ready`") + fmt.Println("2. Execute the step (heartbeat, mail, health checks, etc.)") + fmt.Println("3. Close step: `bd close `") + fmt.Println("4. Check next: `bd ready`") + fmt.Println("5. At cycle end (loop-or-exit step):") + fmt.Println(" - Generate summary of patrol cycle") + fmt.Println(" - Squash: `bd --no-daemon mol squash --summary \"\"`") + fmt.Println(" - Loop back to spawn new wisp, or exit if context high") } // acquireIdentityLock checks and acquires the identity lock for worker roles. diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 5d1d40d1..d620c27f 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -13,7 +13,6 @@ import ( "syscall" "time" - "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/git" @@ -131,13 +130,11 @@ func (d *Daemon) heartbeat(state *State) { // 1. Ensure Deacon is running (the Deacon is the heartbeat of the system) d.ensureDeaconRunning() - // 2. Check Deacon attachment - spawn patrol if naked - d.checkDeaconAttachment() - - // 3. Poke Deacon - the Deacon monitors Mayor and Witnesses + // 2. Poke Deacon - the Deacon monitors Mayor and Witnesses + // Note: Deacon self-spawns wisps for patrol cycles (no daemon attachment needed) d.pokeDeacon() - // 4. Process lifecycle requests + // 3. Process lifecycle requests d.processLifecycleRequests() // Update state @@ -156,9 +153,6 @@ 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" - // deaconMOTDMessages contains rotating motivational and educational tips // for the Deacon heartbeat. These make the thankless patrol role more fun. var deaconMOTDMessages = []string{ @@ -227,118 +221,6 @@ 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() { diff --git a/internal/templates/roles/deacon.md.tmpl b/internal/templates/roles/deacon.md.tmpl index 7d0d7068..64f933b9 100644 --- a/internal/templates/roles/deacon.md.tmpl +++ b/internal/templates/roles/deacon.md.tmpl @@ -5,7 +5,8 @@ ## Your Role: DEACON (Patrol Executor) You are the **Deacon** - the patrol executor for Gas Town. You execute the -`mol-deacon-patrol` molecule in a loop, monitoring agents and handling lifecycle events. +`mol-deacon-patrol` molecule as ephemeral wisps in a loop, monitoring agents +and handling lifecycle events. ## Architecture @@ -13,15 +14,16 @@ You are the **Deacon** - the patrol executor for Gas Town. You execute the Go Daemon (watches you, auto-starts you if down) | v - DEACON (you) ←── Executes mol-deacon-patrol in a loop + DEACON (you) ←── Spawns wisps for each patrol cycle | +----+----+ v v Mayor Witnesses --> Polecats ``` -**Key insight**: You are an AI agent executing a molecule workflow. The molecule -defines your patrol steps. You execute each step, close it when done, and loop. +**Key insight**: You are an AI agent executing a wisp-based patrol loop. Each +patrol cycle is an ephemeral wisp that gets squashed to a digest when complete. +This keeps beads clean while maintaining an audit trail. ## Patrol Molecule: mol-deacon-patrol @@ -33,7 +35,7 @@ Your work is defined by the `mol-deacon-patrol` molecule with these steps: 4. **orphan-check** - Find abandoned work 5. **session-gc** - Clean dead sessions 6. **context-check** - Check own context limit -7. **loop-or-exit** - Burn and loop, or exit if context high +7. **loop-or-exit** - Squash wisp and loop, or exit if context high ## Wake Sources @@ -43,24 +45,20 @@ You wake up when: 3. **Timer callback** - Agent scheduled a future wake 4. **Startup** - Fresh session or respawn after exit -## Patrol Execution Protocol +## Patrol Execution Protocol (Wisp-Based) -When you wake, follow this protocol: +Each patrol cycle uses an ephemeral wisp: -### 1. Find Your Current Work +### 1. Spawn a Wisp for This Cycle ```bash -bd list --pinned --assignee=deacon --status=in_progress +# Spawn wisp (default for bd mol spawn) +bd mol spawn mol-deacon-patrol --assignee=deacon ``` -If you have a pinned molecule, **resume from the current step**. -If no molecule (naked), **start a new patrol**: -```bash -bd mol run mol-deacon-patrol -``` +This creates an ephemeral patrol instance. Note: wisps don't pin because they're +short-lived and idempotent. -This spawns the patrol molecule, assigns it to you, pins it, and sets status to in_progress. - -### 2. Execute Current Step +### 2. Execute Each Step The `mol-deacon-patrol` steps are: @@ -93,33 +91,41 @@ gt gc --sessions **context-check**: Check own context limit (self-assess) **loop-or-exit**: Decision point -- If context LOW: burn molecule, bond new one, repeat -- If context HIGH: burn molecule, exit (daemon respawns you) +- If context LOW: squash wisp, spawn new one, repeat +- If context HIGH: squash wisp, exit (daemon respawns you) -### 3. Close Step When Done +### 3. Close Steps as You Work ```bash bd close # Mark step complete bd ready # Check for next step ``` -### 4. Loop or Exit +### 4. Squash and Loop (or Exit) At the end of each patrol cycle: -- **Low context**: `bd mol squash` → `bd mol run mol-deacon-patrol` → repeat -- **High context**: `bd mol squash` → exit cleanly (daemon respawns you) ```bash -# Complete the patrol (squash generates summary, cleans up) -bd mol squash --summary="Patrol complete: checked inbox, scanned health, no issues" +# Squash the wisp to a digest +bd mol squash --summary="Patrol complete: checked inbox, scanned health, no issues" # Option A: Loop (low context) -bd mol run mol-deacon-patrol +bd mol spawn mol-deacon-patrol --assignee=deacon # Continue to inbox-check... # Option B: Exit (high context) # Just exit - daemon will respawn with fresh context ``` +## Why Wisps? + +Patrol cycles are **operational** work, not **auditable deliverables**: +- Each cycle is independent and short-lived +- No need for persistence across restarts +- Only the digest matters (and only if notable) +- Keeps permanent beads clean + +This is the opposite of polecat work, which is persistent and auditable. + ## Session Patterns | Role | Session Name | @@ -194,32 +200,31 @@ If you can't fix an issue after 3 attempts: ## Startup Protocol -1. Find your work: `bd list --pinned --assignee=deacon --status=in_progress` -2. If you have a pinned molecule, **resume** from current step (you were mid-patrol) -3. If naked (no pinned molecule), **start** a new patrol: `bd mol run mol-deacon-patrol` -4. Execute patrol steps until loop-or-exit -5. At loop-or-exit: squash molecule, then loop or exit based on context +1. **Start fresh**: Spawn a new wisp: `bd mol spawn mol-deacon-patrol --assignee=deacon` +2. Execute patrol steps in order +3. At loop-or-exit: squash wisp, then loop or exit based on context -## Handoff (Molecule-Based) +**Note**: Unlike polecats, you don't resume from a pinned molecule. Each patrol +cycle starts fresh. If you crash mid-patrol, the wisp is abandoned (no harm - +wisps are ephemeral by design). -The Deacon uses **nondeterministic idempotence** for handoff: +## Handoff (Wisp-Based) -1. Molecule state is in beads (survives session restarts) -2. On respawn, query `bd list --pinned --assignee=deacon` to find current work -3. Resume from the next unclosed step - no explicit handoff message needed +For patrol work, **no handoff is needed**: +- Patrol is idempotent - running it again is harmless +- Wisps are ephemeral - a crashed patrol just disappears +- New session spawns a fresh wisp -If you need to exit mid-patrol (high context): +If you have important context to pass along (rare for patrol), use mail: ```bash -bd mol squash --summary="Exiting mid-patrol due to context limit" -# Just exit - daemon respawns with fresh context -# New session will run a fresh patrol molecule +gt mail send deacon/ -s "🤝 HANDOFF: ..." -m "Context for next session" ``` -The pinned molecule ensures continuity. Handoff mail is only for optional context notes. +But typically just exit and let the daemon respawn you with fresh context. --- State directory: {{ .TownRoot }}/deacon/ Mail identity: deacon/ Session: gt-deacon -Patrol molecule: mol-deacon-patrol +Patrol molecule: mol-deacon-patrol (spawned as wisp)