Add bd gate eval command for timer gate evaluation (gt-twjr5.2)

- New `bd gate eval` command evaluates all open gates and closes elapsed ones
- Timer gates: closes when elapsed time exceeds timeout duration
- For timer gates, parse duration from await_id if --timeout not explicitly set
- Supports --dry-run and --json output modes
- Idempotent and safe to run repeatedly (e.g., in Deacon patrol)

🤖 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 22:28:49 -08:00
parent 32c803dd16
commit 1d3a7f98df

View File

@@ -101,6 +101,16 @@ Examples:
}
}
// For timer gates, the await_id IS the duration - use it as timeout if not explicitly set
if awaitType == "timer" && timeout == 0 {
var err error
timeout, err = time.ParseDuration(awaitID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid timer duration %q: %v\n", awaitID, err)
os.Exit(1)
}
}
// Generate title if not provided
if title == "" {
title = fmt.Sprintf("Gate: %s:%s", awaitType, awaitID)
@@ -564,7 +574,142 @@ var gateWaitCmd = &cobra.Command{
},
}
var gateEvalCmd = &cobra.Command{
Use: "eval",
Short: "Evaluate pending gates and close elapsed ones",
Long: `Evaluate all open gates and close those whose conditions are met.
Currently supports:
- timer gates: closed when elapsed time exceeds timeout
Future:
- gh:run gates: closed when GitHub Actions run completes
- gh:pr gates: closed when PR is merged/closed
This command is idempotent and safe to run repeatedly.`,
Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("gate eval")
ctx := rootCtx
dryRun, _ := cmd.Flags().GetBool("dry-run")
var gates []*types.Issue
// Get all open gates
if daemonClient != nil {
resp, err := daemonClient.GateList(&rpc.GateListArgs{All: false})
if err != nil {
FatalError("gate eval: %v", err)
}
if err := json.Unmarshal(resp.Data, &gates); err != nil {
FatalError("failed to parse gates: %v", err)
}
} else if store != nil {
gateType := types.TypeGate
openStatus := types.StatusOpen
filter := types.IssueFilter{
IssueType: &gateType,
Status: &openStatus,
}
var err error
gates, err = store.SearchIssues(ctx, "", filter)
if err != nil {
FatalError("listing gates: %v", err)
}
} else {
FatalError("no database connection")
}
if len(gates) == 0 {
if !jsonOutput {
fmt.Println("No open gates to evaluate")
}
return
}
var closed []string
var timedOut []string
now := time.Now()
for _, gate := range gates {
// Only evaluate timer gates for now
if gate.AwaitType != "timer" {
continue
}
// Check if timer has elapsed
if gate.Timeout <= 0 {
continue // No timeout set
}
elapsed := now.Sub(gate.CreatedAt)
if elapsed < gate.Timeout {
continue // Not yet elapsed
}
// Timer has elapsed - close the gate
if dryRun {
fmt.Printf("Would close gate %s (timer elapsed: %v >= %v)\n",
gate.ID, elapsed.Round(time.Second), gate.Timeout)
closed = append(closed, gate.ID)
continue
}
reason := fmt.Sprintf("Timer elapsed (%v)", gate.Timeout)
if daemonClient != nil {
_, err := daemonClient.GateClose(&rpc.GateCloseArgs{
ID: gate.ID,
Reason: reason,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close gate %s: %v\n", gate.ID, err)
continue
}
} else if store != nil {
if err := store.CloseIssue(ctx, gate.ID, reason, actor); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close gate %s: %v\n", gate.ID, err)
continue
}
markDirtyAndScheduleFlush()
}
closed = append(closed, gate.ID)
// Check if gate exceeded its timeout (for reporting)
if elapsed > gate.Timeout*2 {
timedOut = append(timedOut, gate.ID)
}
}
if jsonOutput {
outputJSON(map[string]interface{}{
"evaluated": len(gates),
"closed": closed,
"timed_out": timedOut,
})
return
}
if len(closed) == 0 {
fmt.Printf("Evaluated %d gates, none ready to close\n", len(gates))
} else {
action := "Closed"
if dryRun {
action = "Would close"
}
fmt.Printf("%s %s %d timer gate(s)\n", ui.RenderPass("✓"), action, len(closed))
for _, id := range closed {
fmt.Printf(" %s\n", id)
}
}
},
}
func init() {
// Gate eval flags
gateEvalCmd.Flags().Bool("dry-run", false, "Show what would be closed without actually closing")
gateEvalCmd.Flags().Bool("json", false, "Output JSON format")
// Gate create flags
gateCreateCmd.Flags().String("await", "", "Await spec: gh:run:<id>, gh:pr:<id>, timer:<duration>, human:<prompt>, mail:<pattern> (required)")
gateCreateCmd.Flags().String("timeout", "", "Timeout duration (e.g., 30m, 1h)")
@@ -593,6 +738,7 @@ func init() {
gateCmd.AddCommand(gateListCmd)
gateCmd.AddCommand(gateCloseCmd)
gateCmd.AddCommand(gateWaitCmd)
gateCmd.AddCommand(gateEvalCmd)
// Add gate command to root
rootCmd.AddCommand(gateCmd)