Add gt park/resume commands for async gate coordination (gt-twjr5.5)
Implements agent parking and resumption on gates: - gt park <gate-id>: Parks work on a gate, saves context - gt resume: Checks for cleared gates and restores work - gt gate wake: Sends wake mail to waiters when gate closes These commands enable agents to safely suspend work while waiting for external conditions (timers, CI, human approval) and resume when gates clear. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
186
internal/cmd/gate.go
Normal file
186
internal/cmd/gate.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/mail"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
// Gate command provides gt wrappers for gate operations.
|
||||
// Most gate commands are in beads (bd gate ...), but gt provides
|
||||
// integration with the Gas Town mail system for wake notifications.
|
||||
|
||||
var gateCmd = &cobra.Command{
|
||||
Use: "gate",
|
||||
GroupID: GroupWork,
|
||||
Short: "Gate coordination commands",
|
||||
Long: `Gate commands for async coordination.
|
||||
|
||||
Most gate commands are in beads:
|
||||
bd gate create - Create a gate (timer, gh:run, human, mail)
|
||||
bd gate show - Show gate details
|
||||
bd gate list - List open gates
|
||||
bd gate close - Close a gate
|
||||
bd gate approve - Approve a human gate
|
||||
bd gate eval - Evaluate and close elapsed gates
|
||||
|
||||
The gt gate command provides Gas Town integration:
|
||||
gt gate wake - Send wake mail to gate waiters after close`,
|
||||
}
|
||||
|
||||
var gateWakeCmd = &cobra.Command{
|
||||
Use: "wake <gate-id>",
|
||||
Short: "Send wake mail to gate waiters",
|
||||
Long: `Send wake mail to all waiters on a gate.
|
||||
|
||||
This command should be called after a gate closes to notify waiting agents.
|
||||
Typically called by Deacon after 'bd gate eval' or after manual gate close.
|
||||
|
||||
The wake mail includes:
|
||||
- Gate ID and close reason
|
||||
- Instructions to run 'gt resume'
|
||||
|
||||
Examples:
|
||||
# After manual gate close
|
||||
bd gate close gt-xxx --reason "Approved"
|
||||
gt gate wake gt-xxx
|
||||
|
||||
# In Deacon patrol after gate eval
|
||||
for gate in $(bd gate eval --json | jq -r '.closed[]'); do
|
||||
gt gate wake $gate
|
||||
done`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runGateWake,
|
||||
}
|
||||
|
||||
var (
|
||||
gateWakeJSON bool
|
||||
gateWakeDryRun bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
gateWakeCmd.Flags().BoolVar(&gateWakeJSON, "json", false, "Output as JSON")
|
||||
gateWakeCmd.Flags().BoolVarP(&gateWakeDryRun, "dry-run", "n", false, "Show what would be done")
|
||||
|
||||
gateCmd.AddCommand(gateWakeCmd)
|
||||
rootCmd.AddCommand(gateCmd)
|
||||
}
|
||||
|
||||
// GateWakeResult represents the result of sending wake mail.
|
||||
type GateWakeResult struct {
|
||||
GateID string `json:"gate_id"`
|
||||
CloseReason string `json:"close_reason"`
|
||||
Waiters []string `json:"waiters"`
|
||||
Notified []string `json:"notified"`
|
||||
Failed []string `json:"failed,omitempty"`
|
||||
}
|
||||
|
||||
func runGateWake(cmd *cobra.Command, args []string) error {
|
||||
gateID := args[0]
|
||||
|
||||
// Get gate info
|
||||
gateCheck := exec.Command("bd", "gate", "show", gateID, "--json")
|
||||
gateOutput, err := gateCheck.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("gate '%s' not found or not accessible", gateID)
|
||||
}
|
||||
|
||||
var gateInfo struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CloseReason string `json:"close_reason"`
|
||||
Waiters []string `json:"waiters"`
|
||||
}
|
||||
if err := json.Unmarshal(gateOutput, &gateInfo); err != nil {
|
||||
return fmt.Errorf("parsing gate info: %w", err)
|
||||
}
|
||||
|
||||
if gateInfo.Status != "closed" {
|
||||
return fmt.Errorf("gate '%s' is not closed (status: %s) - wake mail only sent for closed gates", gateID, gateInfo.Status)
|
||||
}
|
||||
|
||||
if len(gateInfo.Waiters) == 0 {
|
||||
if gateWakeJSON {
|
||||
result := GateWakeResult{
|
||||
GateID: gateID,
|
||||
CloseReason: gateInfo.CloseReason,
|
||||
Waiters: []string{},
|
||||
Notified: []string{},
|
||||
}
|
||||
return outputGateWakeResult(result)
|
||||
}
|
||||
fmt.Printf("%s Gate %s has no waiters to notify\n", style.Dim.Render("○"), gateID)
|
||||
return nil
|
||||
}
|
||||
|
||||
if gateWakeDryRun {
|
||||
fmt.Printf("Would send wake mail for gate %s to:\n", gateID)
|
||||
for _, w := range gateInfo.Waiters {
|
||||
fmt.Printf(" - %s\n", w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find town root for mail routing
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding town root: %w", err)
|
||||
}
|
||||
|
||||
router := mail.NewRouter(townRoot)
|
||||
|
||||
result := GateWakeResult{
|
||||
GateID: gateID,
|
||||
CloseReason: gateInfo.CloseReason,
|
||||
Waiters: gateInfo.Waiters,
|
||||
Notified: []string{},
|
||||
Failed: []string{},
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("🚦 GATE CLEARED: %s", gateID)
|
||||
body := fmt.Sprintf("Gate %s has closed.\n\nReason: %s\n\nRun 'gt resume' to continue your parked work.",
|
||||
gateID, gateInfo.CloseReason)
|
||||
|
||||
for _, waiter := range gateInfo.Waiters {
|
||||
msg := &mail.Message{
|
||||
From: "deacon/",
|
||||
To: waiter,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
Type: mail.TypeNotification,
|
||||
Priority: mail.PriorityHigh,
|
||||
Wisp: true,
|
||||
}
|
||||
if err := router.Send(msg); err != nil {
|
||||
result.Failed = append(result.Failed, waiter)
|
||||
} else {
|
||||
result.Notified = append(result.Notified, waiter)
|
||||
}
|
||||
}
|
||||
|
||||
if gateWakeJSON {
|
||||
return outputGateWakeResult(result)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Sent wake mail for gate %s\n", style.Bold.Render("🚦"), gateID)
|
||||
if len(result.Notified) > 0 {
|
||||
fmt.Printf(" Notified: %v\n", result.Notified)
|
||||
}
|
||||
if len(result.Failed) > 0 {
|
||||
fmt.Printf(" Failed: %v\n", result.Failed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputGateWakeResult(result GateWakeResult) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(result)
|
||||
}
|
||||
Reference in New Issue
Block a user