Add mail gate evaluation in bd gate eval (gt-twjr5.4)

Mail gates check for messages matching the pattern (await_id):
- Case-insensitive substring match on message subjects
- If waiters specified, only matches messages to those recipients
- Closes gate when matching message found

Note: Full testing blocked by bd-70c4 (await fields being cleared).
The code is correct but gate fields get corrupted by auto-import.

Also adds context and storage imports for mail gate function.

🤖 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-25 23:42:05 -08:00
parent a0e4267aa0
commit e0403c2d31

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
@@ -10,6 +11,7 @@ import (
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
@@ -770,9 +772,14 @@ This command is idempotent and safe to run repeatedly.`,
awaitingHuman = append(awaitingHuman, gate.ID)
continue
case "mail":
// Mail gates will be evaluated when mail gate support is added
awaitingMail = append(awaitingMail, gate.ID)
continue
// Mail gates check for messages matching the pattern
if store != nil {
shouldClose, reason = evalMailGate(ctx, store, gate)
} else {
// Daemon mode - can't evaluate mail gates without store access
awaitingMail = append(awaitingMail, gate.ID)
continue
}
default:
// Unsupported gate type - skip
skipped = append(skipped, gate.ID)
@@ -943,6 +950,59 @@ func evalGHPRGate(gate *types.Issue) (bool, string) {
}
}
// evalMailGate checks if any message matching the pattern exists.
// The pattern (await_id) is matched as a case-insensitive substring of message subjects.
// If waiters are specified, only messages addressed to those waiters are considered.
func evalMailGate(ctx context.Context, store storage.Storage, gate *types.Issue) (bool, string) {
pattern := gate.AwaitID
if pattern == "" {
return false, ""
}
// Search for messages
msgType := types.TypeMessage
openStatus := types.StatusOpen
filter := types.IssueFilter{
IssueType: &msgType,
Status: &openStatus,
}
messages, err := store.SearchIssues(ctx, "", filter)
if err != nil {
return false, ""
}
// Convert pattern to lowercase for case-insensitive matching
patternLower := strings.ToLower(pattern)
// Build waiter set for efficient lookup (if waiters specified)
waiterSet := make(map[string]bool)
for _, w := range gate.Waiters {
waiterSet[w] = true
}
// Check each message
for _, msg := range messages {
// Check subject contains pattern (case-insensitive)
if !strings.Contains(strings.ToLower(msg.Title), patternLower) {
continue
}
// If waiters specified, check if message is addressed to a waiter
// Messages use Assignee field for recipient
if len(waiterSet) > 0 {
if !waiterSet[msg.Assignee] {
continue
}
}
// Found a matching message
return true, fmt.Sprintf("Mail received: %s", msg.Title)
}
return false, ""
}
func init() {
// Gate eval flags
gateEvalCmd.Flags().Bool("dry-run", false, "Show what would be closed without actually closing")