feat(mol): add deacon-patrol molecule definition

Define the Deacon patrol molecule in builtin_molecules.go with 7 steps:
1. inbox-check - Handle callbacks from agents
2. health-scan - Ping Witnesses and Refineries
3. plugin-run - Execute registered plugins
4. orphan-check - Find abandoned work (uses wisp terminology)
5. session-gc - Clean dead sessions and wisp artifacts
6. context-check - Check own context limit
7. loop-or-exit - Burn and let daemon respawn, or exit if context high

Added DeaconPatrolMolecule() to BuiltinMolecules() list and added
corresponding test in builtin_molecules_test.go.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-21 15:39:28 -08:00
parent 0fc74de3a3
commit fb0d8c1bb9
2 changed files with 199 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ func BuiltinMolecules() []BuiltinMolecule {
BootstrapGasTownMolecule(),
PolecatWorkMolecule(),
VersionBumpMolecule(),
DeaconPatrolMolecule(),
}
}
@@ -511,6 +512,149 @@ Needs: verify-release`,
}
}
// DeaconPatrolMolecule returns the deacon-patrol molecule definition.
// This is the Mayor's daemon loop for handling callbacks, health checks, and cleanup.
func DeaconPatrolMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-deacon-patrol",
Title: "Deacon Patrol",
Description: `Mayor's daemon patrol loop.
The Deacon is the Mayor's background process that runs continuously,
handling callbacks, monitoring rig health, and performing cleanup.
Each patrol cycle runs these steps in sequence, then loops or exits.
## Step: inbox-check
Handle callbacks from agents.
Check the Mayor's inbox for messages from:
- Witnesses reporting polecat status
- Refineries reporting merge results
- Polecats requesting help or escalation
- External triggers (webhooks, timers)
Process each message:
` + "```" + `bash
gt mail inbox
# For each message:
gt mail read <id>
# Handle based on message type
` + "```" + `
Callbacks may spawn new polecats, update issue state, or trigger other actions.
## Step: health-scan
Ping Witnesses and Refineries.
For each rig, verify:
- Witness is responsive
- Refinery is processing queue
- No stalled operations
` + "```" + `bash
gt status --health
# Check each rig
for rig in $(gt rigs); do
gt rig status $rig
done
` + "```" + `
Report any issues found. Restart unresponsive components if needed.
Needs: inbox-check
## Step: plugin-run
Execute registered plugins.
Run any plugins registered with the Deacon:
- Custom health checks
- Integration hooks (Slack, GitHub, etc.)
- Metrics collection
- External system sync
Plugins are defined in the Mayor's config and run on each patrol cycle.
Skip this step if no plugins are registered.
Needs: health-scan
## Step: orphan-check
Find abandoned work.
Scan for orphaned state:
- Issues marked in_progress with no active polecat
- Polecats that stopped responding mid-work
- Merge queue entries with no polecat owner
- Wisp sessions that outlived their spawner
` + "```" + `bash
bd list --status=in_progress
gt polecats --all --orphan
` + "```" + `
For each orphan:
- Check if polecat session still exists
- If not, mark issue for reassignment or retry
- File incident beads if data loss occurred
Needs: health-scan
## Step: session-gc
Clean dead sessions.
Garbage collect terminated sessions:
- Remove stale polecat directories
- Clean up wisp session artifacts
- Prune old logs and temp files
- Archive completed molecule state
` + "```" + `bash
gt gc --sessions
gt gc --wisps --age=1h
` + "```" + `
Preserve audit trail. Only clean sessions confirmed dead.
Needs: orphan-check
## Step: context-check
Check own context limit.
The Deacon runs in a Claude session with finite context.
Check if approaching the limit:
` + "```" + `bash
gt context --usage
` + "```" + `
If context is high (>80%), prepare for handoff:
- Summarize current state
- Note any pending work
- Write handoff to molecule state
This enables the Deacon to burn and respawn cleanly.
Needs: session-gc
## Step: loop-or-exit
Burn and let daemon respawn, or exit if context high.
Decision point at end of patrol cycle:
If context is LOW:
- Sleep briefly (avoid tight loop)
- Return to inbox-check step
If context is HIGH:
- Write state to persistent storage
- Exit cleanly
- Let the daemon orchestrator respawn a fresh Deacon
The daemon ensures Deacon is always running:
` + "```" + `bash
# Daemon respawns on exit
gt daemon status
` + "```" + `
This enables infinite patrol duration via context-aware respawning.
Needs: context-check`,
}
}
// SeedBuiltinMolecules creates all built-in molecules in the beads database.
// It skips molecules that already exist (by title match).
// Returns the number of molecules created.

View File

@@ -5,8 +5,8 @@ import "testing"
func TestBuiltinMolecules(t *testing.T) {
molecules := BuiltinMolecules()
if len(molecules) != 7 {
t.Errorf("expected 7 built-in molecules, got %d", len(molecules))
if len(molecules) != 8 {
t.Errorf("expected 8 built-in molecules, got %d", len(molecules))
}
// Verify each molecule can be parsed and validated
@@ -230,3 +230,56 @@ func TestPolecatWorkMolecule(t *testing.T) {
t.Errorf("request-shutdown should need update-handoff, got %v", steps[7].Needs)
}
}
func TestDeaconPatrolMolecule(t *testing.T) {
mol := DeaconPatrolMolecule()
if mol.ID != "mol-deacon-patrol" {
t.Errorf("expected ID 'mol-deacon-patrol', got %q", mol.ID)
}
if mol.Title != "Deacon Patrol" {
t.Errorf("expected Title 'Deacon Patrol', got %q", mol.Title)
}
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
// Should have 7 steps: inbox-check, health-scan, plugin-run, orphan-check,
// session-gc, context-check, loop-or-exit
if len(steps) != 7 {
t.Errorf("expected 7 steps, got %d", len(steps))
}
expectedRefs := []string{
"inbox-check", "health-scan", "plugin-run", "orphan-check",
"session-gc", "context-check", "loop-or-exit",
}
for i, expected := range expectedRefs {
if i >= len(steps) {
t.Errorf("missing step %d: expected %q", i, expected)
continue
}
if steps[i].Ref != expected {
t.Errorf("step %d: expected ref %q, got %q", i, expected, steps[i].Ref)
}
}
// Verify key dependencies
// inbox-check has no deps (first step)
if len(steps[0].Needs) != 0 {
t.Errorf("inbox-check should have no deps, got %v", steps[0].Needs)
}
// health-scan needs inbox-check
if len(steps[1].Needs) != 1 || steps[1].Needs[0] != "inbox-check" {
t.Errorf("health-scan should need inbox-check, got %v", steps[1].Needs)
}
// loop-or-exit needs context-check
if len(steps[6].Needs) != 1 || steps[6].Needs[0] != "context-check" {
t.Errorf("loop-or-exit should need context-check, got %v", steps[6].Needs)
}
}