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:
@@ -18,6 +18,7 @@ func BuiltinMolecules() []BuiltinMolecule {
|
|||||||
BootstrapGasTownMolecule(),
|
BootstrapGasTownMolecule(),
|
||||||
PolecatWorkMolecule(),
|
PolecatWorkMolecule(),
|
||||||
VersionBumpMolecule(),
|
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.
|
// SeedBuiltinMolecules creates all built-in molecules in the beads database.
|
||||||
// It skips molecules that already exist (by title match).
|
// It skips molecules that already exist (by title match).
|
||||||
// Returns the number of molecules created.
|
// Returns the number of molecules created.
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import "testing"
|
|||||||
func TestBuiltinMolecules(t *testing.T) {
|
func TestBuiltinMolecules(t *testing.T) {
|
||||||
molecules := BuiltinMolecules()
|
molecules := BuiltinMolecules()
|
||||||
|
|
||||||
if len(molecules) != 7 {
|
if len(molecules) != 8 {
|
||||||
t.Errorf("expected 7 built-in molecules, got %d", len(molecules))
|
t.Errorf("expected 8 built-in molecules, got %d", len(molecules))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify each molecule can be parsed and validated
|
// 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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user