From c81f02de5669bc83f889c2b4904fabd7c6fbe1cf Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 23 Dec 2025 22:28:20 -0800 Subject: [PATCH] feat: witness instantiates mol-witness-patrol on start (gt-59zd.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add patrol instance tracking to witness manager: - Add PatrolInstanceID to WitnessHandoffState for tracking - Add ArmID to WorkerState for per-polecat arm tracking - Add ensurePatrolInstance() method that: - Checks if patrol instance already exists (from previous session) - Creates new patrol instance if needed - Persists instance ID in handoff state This enables the molecule-based tracking ledger pattern where the Go-based witness creates beads issues to track its patrol state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/witness/manager.go | 87 +++++++++++++++++++++++++++++++++++++ internal/witness/types.go | 6 +++ 2 files changed, 93 insertions(+) diff --git a/internal/witness/manager.go b/internal/witness/manager.go index 1af74c47..f9e04e61 100644 --- a/internal/witness/manager.go +++ b/internal/witness/manager.go @@ -337,6 +337,11 @@ func (m *Manager) run(w *Witness) error { m.handoffState = handoffState fmt.Printf("Loaded handoff state with %d worker(s)\n", len(m.handoffState.WorkerStates)) + // Ensure mol-witness-patrol instance exists for tracking + if err := m.ensurePatrolInstance(); err != nil { + fmt.Printf("Warning: could not ensure patrol instance: %v\n", err) + } + // Initial check immediately m.checkAndProcess(w) @@ -349,6 +354,88 @@ func (m *Manager) run(w *Witness) error { return nil } +// ensurePatrolInstance ensures a mol-witness-patrol instance exists for tracking. +// If one already exists (from a previous session), it's reused. Otherwise, a new +// instance is created and pinned to the witness handoff bead. +func (m *Manager) ensurePatrolInstance() error { + // Check if we already have a patrol instance + if m.handoffState != nil && m.handoffState.PatrolInstanceID != "" { + // Verify it still exists + cmd := exec.Command("bd", "show", m.handoffState.PatrolInstanceID, "--json") + cmd.Dir = m.workDir + if err := cmd.Run(); err == nil { + fmt.Printf("Using existing patrol instance: %s\n", m.handoffState.PatrolInstanceID) + return nil + } + // Instance no longer exists, clear it + m.handoffState.PatrolInstanceID = "" + } + + // Create a new patrol instance + // First, create a root issue for the patrol + patrolTitle := fmt.Sprintf("Witness Patrol (%s)", m.rig.Name) + patrolDesc := fmt.Sprintf(`Active mol-witness-patrol instance for %s. + +rig: %s +started_at: %s +type: patrol-instance +`, m.rig.Name, m.rig.Name, time.Now().UTC().Format(time.RFC3339)) + + cmd := exec.Command("bd", "create", + "--title", patrolTitle, + "--type", "task", + "--priority", "3", + "--description", patrolDesc, + ) + cmd.Dir = m.workDir + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("creating patrol instance: %s", stderr.String()) + } + + // Parse the created issue ID from stdout + // Output format: "✓ Created issue: gt-xyz" + output := stdout.String() + var patrolID string + if _, err := fmt.Sscanf(output, "✓ Created issue: %s", &patrolID); err != nil { + // Try alternate parsing + lines := strings.Split(output, "\n") + for _, line := range lines { + if strings.Contains(line, "Created issue:") { + parts := strings.Split(line, ":") + if len(parts) >= 2 { + patrolID = strings.TrimSpace(parts[len(parts)-1]) + break + } + } + } + } + + if patrolID == "" { + return fmt.Errorf("could not parse patrol instance ID from: %s", output) + } + + // Store the patrol instance ID + if m.handoffState == nil { + m.handoffState = &WitnessHandoffState{ + WorkerStates: make(map[string]WorkerState), + } + } + m.handoffState.PatrolInstanceID = patrolID + + // Persist the updated handoff state + if err := m.saveHandoffState(m.handoffState); err != nil { + return fmt.Errorf("saving handoff state: %w", err) + } + + fmt.Printf("Created patrol instance: %s\n", patrolID) + return nil +} + // checkAndProcess performs health check, shutdown processing, and auto-spawn. func (m *Manager) checkAndProcess(w *Witness) { // Perform health check diff --git a/internal/witness/types.go b/internal/witness/types.go index 51c72983..ab5b8e01 100644 --- a/internal/witness/types.go +++ b/internal/witness/types.go @@ -90,6 +90,9 @@ type WorkerState struct { // Issue is the current issue the worker is assigned to. Issue string `json:"issue,omitempty"` + // ArmID is the mol-polecat-arm instance tracking this worker. + ArmID string `json:"arm_id,omitempty"` + // NudgeCount is how many times this worker has been nudged. NudgeCount int `json:"nudge_count"` @@ -106,6 +109,9 @@ type WitnessHandoffState struct { // WorkerStates maps polecat names to their state. WorkerStates map[string]WorkerState `json:"worker_states"` + // PatrolInstanceID is the mol-witness-patrol instance tracking this patrol. + PatrolInstanceID string `json:"patrol_instance_id,omitempty"` + // LastPatrol is when the last patrol cycle completed. LastPatrol *time.Time `json:"last_patrol,omitempty"` }