From 33f8584e77b497e1d154b157117d9f237ccd03f4 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 21:22:03 -0800 Subject: [PATCH] feat: implement Christmas Ornament pattern for mol-witness-patrol (gt-tnow.1, gt-tnow.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update WitnessPatrolMolecule() with dynamic bonding pattern: - PREFLIGHT: inbox-check, check-refinery, load-state - DISCOVERY: survey-workers (bonds mol-polecat-arm per polecat) - CLEANUP: aggregate (WaitsFor: all-children), save-state, generate-summary, context-check, burn-or-loop - Reduced from 11 sequential steps to 9 with parallel arm execution - Add PolecatArmMolecule() for per-polecat inspection: - 5 sequential steps: capture, assess, load-history, decide, execute - Uses {{polecat_name}} and {{rig}} variable substitution - Bonded dynamically by survey-workers step - Update tests: 14 builtin molecules, new test cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/beads/builtin_molecules.go | 1 + internal/beads/builtin_molecules_test.go | 86 ++++++- internal/beads/molecules_patrol.go | 278 +++++++++++++++-------- 3 files changed, 259 insertions(+), 106 deletions(-) diff --git a/internal/beads/builtin_molecules.go b/internal/beads/builtin_molecules.go index 187f27a2..f8d20705 100644 --- a/internal/beads/builtin_molecules.go +++ b/internal/beads/builtin_molecules.go @@ -26,6 +26,7 @@ func BuiltinMolecules() []BuiltinMolecule { DeaconPatrolMolecule(), RefineryPatrolMolecule(), WitnessPatrolMolecule(), + PolecatArmMolecule(), // Session and utility molecules CrewSessionMolecule(), diff --git a/internal/beads/builtin_molecules_test.go b/internal/beads/builtin_molecules_test.go index f4f1e43f..97611abe 100644 --- a/internal/beads/builtin_molecules_test.go +++ b/internal/beads/builtin_molecules_test.go @@ -5,8 +5,8 @@ import "testing" func TestBuiltinMolecules(t *testing.T) { molecules := BuiltinMolecules() - if len(molecules) != 13 { - t.Errorf("expected 13 built-in molecules, got %d", len(molecules)) + if len(molecules) != 14 { + t.Errorf("expected 14 built-in molecules, got %d", len(molecules)) } // Verify each molecule can be parsed and validated @@ -300,17 +300,17 @@ func TestWitnessPatrolMolecule(t *testing.T) { t.Fatalf("failed to parse: %v", err) } - // Should have 11 steps: inbox-check, check-refinery, load-state, survey-workers, - // inspect-workers, decide-actions, execute-actions, save-state, generate-summary, - // context-check, burn-or-loop - if len(steps) != 11 { - t.Errorf("expected 11 steps, got %d", len(steps)) + // Should have 9 steps using Christmas Ornament pattern: + // PREFLIGHT: inbox-check, check-refinery, load-state + // DISCOVERY: survey-workers (bonds mol-polecat-arm dynamically) + // CLEANUP: aggregate, save-state, generate-summary, context-check, burn-or-loop + if len(steps) != 9 { + t.Errorf("expected 9 steps, got %d", len(steps)) } expectedRefs := []string{ "inbox-check", "check-refinery", "load-state", "survey-workers", - "inspect-workers", "decide-actions", "execute-actions", "save-state", - "generate-summary", "context-check", "burn-or-loop", + "aggregate", "save-state", "generate-summary", "context-check", "burn-or-loop", } for i, expected := range expectedRefs { if i >= len(steps) { @@ -338,8 +338,72 @@ func TestWitnessPatrolMolecule(t *testing.T) { t.Errorf("load-state should need check-refinery, got %v", steps[2].Needs) } + // aggregate needs survey-workers (fanout gate) + if len(steps[4].Needs) != 1 || steps[4].Needs[0] != "survey-workers" { + t.Errorf("aggregate should need survey-workers, got %v", steps[4].Needs) + } + // burn-or-loop needs context-check - if len(steps[10].Needs) != 1 || steps[10].Needs[0] != "context-check" { - t.Errorf("burn-or-loop should need context-check, got %v", steps[10].Needs) + if len(steps[8].Needs) != 1 || steps[8].Needs[0] != "context-check" { + t.Errorf("burn-or-loop should need context-check, got %v", steps[8].Needs) + } +} + +func TestPolecatArmMolecule(t *testing.T) { + mol := PolecatArmMolecule() + + if mol.ID != "mol-polecat-arm" { + t.Errorf("expected ID 'mol-polecat-arm', got %q", mol.ID) + } + + if mol.Title != "Polecat Arm" { + t.Errorf("expected Title 'Polecat Arm', got %q", mol.Title) + } + + steps, err := ParseMoleculeSteps(mol.Description) + if err != nil { + t.Fatalf("failed to parse: %v", err) + } + + // Should have 5 steps: capture, assess, load-history, decide, execute + if len(steps) != 5 { + t.Errorf("expected 5 steps, got %d", len(steps)) + } + + expectedRefs := []string{"capture", "assess", "load-history", "decide", "execute"} + 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 dependencies form a chain + // capture has no deps (first step) + if len(steps[0].Needs) != 0 { + t.Errorf("capture should have no deps, got %v", steps[0].Needs) + } + + // assess needs capture + if len(steps[1].Needs) != 1 || steps[1].Needs[0] != "capture" { + t.Errorf("assess should need capture, got %v", steps[1].Needs) + } + + // load-history needs assess + if len(steps[2].Needs) != 1 || steps[2].Needs[0] != "assess" { + t.Errorf("load-history should need assess, got %v", steps[2].Needs) + } + + // decide needs load-history + if len(steps[3].Needs) != 1 || steps[3].Needs[0] != "load-history" { + t.Errorf("decide should need load-history, got %v", steps[3].Needs) + } + + // execute needs decide + if len(steps[4].Needs) != 1 || steps[4].Needs[0] != "decide" { + t.Errorf("execute should need decide, got %v", steps[4].Needs) } } diff --git a/internal/beads/molecules_patrol.go b/internal/beads/molecules_patrol.go index b1e8ae28..35cb1750 100644 --- a/internal/beads/molecules_patrol.go +++ b/internal/beads/molecules_patrol.go @@ -182,20 +182,41 @@ Needs: context-check`, } // WitnessPatrolMolecule returns the witness-patrol molecule definition. -// This is the per-rig worker monitor's patrol loop with progressive nudging. +// This is the per-rig worker monitor's patrol loop using the Christmas Ornament pattern. func WitnessPatrolMolecule() BuiltinMolecule { return BuiltinMolecule{ ID: "mol-witness-patrol", Title: "Witness Patrol", - Description: `Per-rig worker monitor patrol loop. + Description: `Per-rig worker monitor patrol loop using the Christmas Ornament pattern. The Witness is the Pit Boss for your rig. You watch polecats, nudge them toward completion, verify clean git state before kills, and escalate stuck workers. **You do NOT do implementation work.** Your job is oversight, not coding. -This molecule uses wisp storage (.beads-wisp/) for ephemeral patrol state. -Persistent state (nudge counts, handoffs) is stored in a witness handoff bead. +This molecule uses dynamic bonding to spawn mol-polecat-arm for each worker, +enabling parallel inspection with a fanout gate for aggregation. + +## The Christmas Ornament Shape + +` + "```" + ` + ★ mol-witness-patrol (trunk) + /|\ + ┌────────┘ │ └────────┐ + PREFLIGHT DISCOVERY CLEANUP + │ │ │ + inbox-check survey aggregate (WaitsFor: all-children) + check-refnry │ save-state + load-state │ generate-summary + ↓ context-check + ┌───────┼───────┐ burn-or-loop + ● ● ● mol-polecat-arm (dynamic) + ace nux toast +` + "```" + ` + +--- +# PREFLIGHT PHASE +--- ## Step: inbox-check Process witness mail: lifecycle requests, help requests. @@ -254,114 +275,64 @@ If no handoff exists (fresh start), initialize empty state. This state persists across wisp burns and session cycles. Needs: check-refinery +--- +# DISCOVERY PHASE (Dynamic Bonding) +--- + ## Step: survey-workers -List polecats and categorize by status. +List polecats and bond mol-polecat-arm for each one. ` + "```" + `bash +# Get list of polecats gt polecat list ` + "```" + ` -Categorize each polecat: -- **working**: Actively processing (needs inspection) -- **idle**: At prompt, not active (may need nudge) -- **pending_shutdown**: Requested termination (needs pre-kill) -- **error**: Showing errors (needs assessment) +For each polecat discovered, dynamically bond an inspection arm: -Build action queue for next steps. +` + "```" + `bash +# Bond mol-polecat-arm for each polecat +for polecat in $(gt polecat list --names); do + bd mol bond mol-polecat-arm $PATROL_WISP_ID \ + --ref arm-$polecat \ + --var polecat_name=$polecat \ + --var rig= +done +` + "```" + ` + +This creates child wisps like: +- patrol-x7k.arm-ace (5 steps) +- patrol-x7k.arm-nux (5 steps) +- patrol-x7k.arm-toast (5 steps) + +Each arm runs in PARALLEL. The aggregate step will wait for all to complete. + +If no polecats are found, this step completes immediately with no children. Needs: load-state -## Step: inspect-workers -Capture output for each 'working' polecat. +--- +# CLEANUP PHASE (Gate + Fixed Steps) +--- -For each polecat showing "working" status: -` + "```" + `bash -tmux capture-pane -t gt-- -p | tail -40 -` + "```" + ` +## Step: aggregate +Collect outcomes from all polecat inspection arms. +WaitsFor: all-children -Look for: -- Recent tool calls (good - actively working) -- Prompt waiting for input (may be stuck) -- Error messages or stack traces -- "Done" or completion indicators -- Time since last activity +This is a **fanout gate** - it cannot proceed until ALL dynamically-bonded +polecat arms have completed their inspection cycles. -Update worker status based on inspection. +Once all arms complete, collect their outcomes: +- Actions taken per polecat (nudge, kill, escalate, none) +- Updated nudge counts +- Any errors or issues discovered + +Build the consolidated state for save-state. Needs: survey-workers -## Step: decide-actions -Apply nudge matrix and queue actions. - -For each worker, apply decision rules: - -**Progressing normally**: No action needed -**Idle <10 min**: Continue monitoring -**Idle 10-15 min**: Queue first nudge (gentle) -**Idle 15-20 min with no progress since nudge 1**: Queue second nudge (direct) -**Idle 20+ min with no progress since nudge 2**: Queue third nudge (final warning) -**No response after 3 nudges**: Queue escalation to Mayor -**Requesting shutdown**: Queue pre-kill verification -**Showing errors**: Assess severity, queue nudge or escalation - -Progressive nudge text: -1. "How's progress on ? Need any help?" -2. "Please wrap up soon. What's blocking you?" -3. "Final check on . Will escalate in 5 min if no response." - -Track nudge counts in state - never exceed 3 per issue. -Needs: inspect-workers - -## Step: execute-actions -Nudge, kill, or escalate as decided. - -Process action queue in order: - -**Nudges:** -` + "```" + `bash -tmux send-keys -t gt-- "" Enter -` + "```" + ` -Update nudge count and timestamp in state. - -**Pre-kill verification:** -` + "```" + `bash -cd polecats/ && git status # Must be clean -git log origin/main..HEAD # Check for unpushed commits -bd show # Verify issue closed -` + "```" + ` - -If clean: -` + "```" + `bash -tmux kill-session -t gt-- -git worktree remove polecats/ # If transient -git branch -d polecat/ # If transient -` + "```" + ` - -If dirty: nudge worker to clean up, wait for retry. -If dirty after 3 attempts: escalate to Mayor. - -**Escalations:** -` + "```" + `bash -gt mail send mayor/ -s "Escalation: stuck on " -m " -Worker: -Issue: -Problem: - -Timeline: -- Nudge 1: