feat: implement Christmas Ornament pattern for mol-witness-patrol (gt-tnow.1, gt-tnow.2)

- 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 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-23 21:22:03 -08:00
parent 6a7662ba6b
commit 33f8584e77
3 changed files with 259 additions and 106 deletions

View File

@@ -26,6 +26,7 @@ func BuiltinMolecules() []BuiltinMolecule {
DeaconPatrolMolecule(),
RefineryPatrolMolecule(),
WitnessPatrolMolecule(),
PolecatArmMolecule(),
// Session and utility molecules
CrewSessionMolecule(),

View File

@@ -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)
}
}

View File

@@ -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 <rig>
` + "```" + `
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 <rig> --names); do
bd mol bond mol-polecat-arm $PATROL_WISP_ID \
--ref arm-$polecat \
--var polecat_name=$polecat \
--var rig=<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-<rig>-<name> -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 <issue>? Need any help?"
2. "Please wrap up <issue> soon. What's blocking you?"
3. "Final check on <issue>. 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-<rig>-<name> "<nudge text>" Enter
` + "```" + `
Update nudge count and timestamp in state.
**Pre-kill verification:**
` + "```" + `bash
cd polecats/<name> && git status # Must be clean
git log origin/main..HEAD # Check for unpushed commits
bd show <issue-id> # Verify issue closed
` + "```" + `
If clean:
` + "```" + `bash
tmux kill-session -t gt-<rig>-<name>
git worktree remove polecats/<name> # If transient
git branch -d polecat/<name> # 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: <polecat> stuck on <issue>" -m "
Worker: <polecat>
Issue: <issue-id>
Problem: <description>
Timeline:
- Nudge 1: <time> - <response>
- Nudge 2: <time> - <response>
- Nudge 3: <time> - <response>
Git state: <clean/dirty>
My assessment: <what's happening>
Recommendation: <what should happen>
"
` + "```" + `
Needs: decide-actions
## Step: save-state
Update handoff bead with new states.
Persist state to the witness handoff bead:
- Updated worker statuses
- Updated worker statuses from all arms
- Current nudge counts per worker
- Nudge timestamps
- Actions taken this cycle
@@ -372,7 +343,7 @@ bd update <handoff-bead-id> --description="<serialized state>"
` + "```" + `
This state survives wisp burns and session cycles.
Needs: execute-actions
Needs: aggregate
## Step: generate-summary
Summarize this patrol cycle for digest.
@@ -420,6 +391,123 @@ Needs: context-check`,
}
}
// PolecatArmMolecule returns the polecat-arm molecule definition.
// This is dynamically bonded by mol-witness-patrol for each polecat being monitored.
func PolecatArmMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-polecat-arm",
Title: "Polecat Arm",
Description: `Single polecat inspection and action cycle.
This molecule is bonded dynamically by mol-witness-patrol's survey-workers step.
Each polecat being monitored gets one arm that runs in parallel with other arms.
## Variables
| Variable | Required | Description |
|----------|----------|-------------|
| polecat_name | Yes | Name of the polecat to inspect |
| rig | Yes | Rig containing the polecat |
## Step: capture
Capture recent tmux output for {{polecat_name}}.
` + "```" + `bash
tmux capture-pane -t gt-{{rig}}-{{polecat_name}} -p | tail -50
` + "```" + `
Record:
- Last activity timestamp (when was last tool call?)
- Visible errors or stack traces
- Completion indicators ("Done", "Finished", etc.)
## Step: assess
Categorize polecat state based on captured output.
States:
- **working**: Recent tool calls, active processing
- **idle**: At prompt, no recent activity
- **error**: Showing errors or stack traces
- **requesting_shutdown**: Sent LIFECYCLE/Shutdown mail
- **done**: Showing completion indicators
Calculate: minutes since last activity.
Needs: capture
## Step: load-history
Read nudge history for {{polecat_name}} from patrol state.
` + "```" + `
nudge_count = state.nudges[{{polecat_name}}].count
last_nudge_time = state.nudges[{{polecat_name}}].timestamp
` + "```" + `
This data was loaded by the parent patrol's load-state step and passed
to the arm via the bonding context.
Needs: assess
## Step: decide
Apply the nudge matrix to determine action for {{polecat_name}}.
| State | Idle Time | Nudge Count | Action |
|-------|-----------|-------------|--------|
| working | any | any | none |
| idle | <10min | any | none |
| idle | 10-15min | 0 | nudge-1 (gentle) |
| idle | 15-20min | 1 | nudge-2 (direct) |
| idle | 20+min | 2 | nudge-3 (final) |
| idle | any | 3 | escalate |
| error | any | any | assess-severity |
| requesting_shutdown | any | any | pre-kill-verify |
| done | any | any | pre-kill-verify |
Nudge text:
1. "How's progress? Need any help?"
2. "Please wrap up soon. What's blocking you?"
3. "Final check. Will escalate in 5 min if no response."
Record decision and rationale.
Needs: load-history
## Step: execute
Take the decided action for {{polecat_name}}.
**nudge-N**:
` + "```" + `bash
tmux send-keys -t gt-{{rig}}-{{polecat_name}} "{{nudge_text}}" Enter
` + "```" + `
**pre-kill-verify**:
` + "```" + `bash
cd polecats/{{polecat_name}}
git status # Must be clean
git log origin/main..HEAD # Check for unpushed
bd show <assigned-issue> # Verify closed/deferred
` + "```" + `
If clean: kill session, remove worktree, delete branch
If dirty: record failure, retry next cycle
**escalate**:
` + "```" + `bash
gt mail send mayor/ -s "Escalation: {{polecat_name}} stuck" -m "..."
` + "```" + `
**none**: No action needed.
Record: action taken, result, updated nudge count.
Needs: decide
## Output
The arm completes with:
- action_taken: none | nudge-1 | nudge-2 | nudge-3 | killed | escalated
- result: success | failed | pending
- updated_state: New nudge count and timestamp for {{polecat_name}}
This data feeds back to the parent patrol's aggregate step.`,
}
}
// RefineryPatrolMolecule returns the refinery-patrol molecule definition.
// This is the merge queue processor's patrol loop with verification gates.
func RefineryPatrolMolecule() BuiltinMolecule {