feat(gate): add add-waiter and show commands for phase handoff

- Add `bd gate add-waiter <gate-id> <waiter>` command to register
  agents as waiters on a gate bead using the native Waiters field
- Add `bd gate show <gate-id>` command to display gate details
  including waiters (used by gt gate wake)
- Add Waiters field to UpdateArgs in RPC protocol
- Update server to handle waiters field in updates

This is part of the polecat phase handoff feature (bd-quw1).
The corresponding gastown changes (gt done --phase-complete) are
tracked separately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jasper
2026-01-02 16:36:36 -08:00
committed by Steve Yegge
parent a89f47cdf2
commit d7246a1f2d
4 changed files with 227 additions and 18 deletions

View File

@@ -167,6 +167,175 @@ func displayGates(gates []*types.Issue) {
fmt.Printf("To resolve a gate: bd close <gate-id>\n")
}
// gateAddWaiterCmd adds a waiter to a gate
var gateAddWaiterCmd = &cobra.Command{
Use: "add-waiter <gate-id> <waiter>",
Short: "Add a waiter to a gate",
Long: `Register an agent as a waiter on a gate bead.
When the gate closes, the waiter will receive a wake notification via 'gt gate wake'.
The waiter is typically the polecat's address (e.g., "gastown/polecats/Toast").
This is used by 'gt done --phase-complete' to register for gate wake notifications.`,
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("gate add-waiter")
gateID := args[0]
waiter := args[1]
ctx := rootCtx
// Get the gate issue
var issue *types.Issue
var err error
if daemonClient != nil {
resp, rerr := daemonClient.Show(&rpc.ShowArgs{ID: gateID})
if rerr != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", rerr)
os.Exit(1)
}
if !resp.Success {
fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Error)
os.Exit(1)
}
var details types.IssueDetails
if uerr := json.Unmarshal(resp.Data, &details); uerr != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", uerr)
os.Exit(1)
}
issue = &details.Issue
} else {
issue, err = store.GetIssue(ctx, gateID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: gate not found: %s\n", gateID)
os.Exit(1)
}
}
if issue.IssueType != types.TypeGate {
fmt.Fprintf(os.Stderr, "Error: %s is not a gate issue (type=%s)\n", gateID, issue.IssueType)
os.Exit(1)
}
// Check if waiter is already registered
for _, w := range issue.Waiters {
if w == waiter {
fmt.Printf("Waiter already registered on gate %s\n", gateID)
return
}
}
// Add waiter to the waiters list
newWaiters := append(issue.Waiters, waiter)
// Update the gate
if daemonClient != nil {
updateArgs := &rpc.UpdateArgs{
ID: gateID,
Waiters: newWaiters,
}
resp, uerr := daemonClient.Update(updateArgs)
if uerr != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", uerr)
os.Exit(1)
}
if !resp.Success {
fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Error)
os.Exit(1)
}
} else {
updates := map[string]interface{}{
"waiters": newWaiters,
}
if err := store.UpdateIssue(ctx, gateID, updates, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error updating gate: %v\n", err)
os.Exit(1)
}
markDirtyAndScheduleFlush()
}
fmt.Printf("%s Added waiter to gate %s: %s\n", ui.RenderPass("✓"), gateID, waiter)
},
}
// gateShowCmd shows a gate issue
var gateShowCmd = &cobra.Command{
Use: "show <gate-id>",
Short: "Show a gate issue",
Long: `Display details of a gate issue including its waiters.
This is similar to 'bd show' but validates that the issue is a gate.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
gateID := args[0]
ctx := rootCtx
// Get the gate issue
var issue *types.Issue
var err error
if daemonClient != nil {
resp, rerr := daemonClient.Show(&rpc.ShowArgs{ID: gateID})
if rerr != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", rerr)
os.Exit(1)
}
if !resp.Success {
fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Error)
os.Exit(1)
}
var details types.IssueDetails
if uerr := json.Unmarshal(resp.Data, &details); uerr != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", uerr)
os.Exit(1)
}
issue = &details.Issue
} else {
issue, err = store.GetIssue(ctx, gateID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: gate not found: %s\n", gateID)
os.Exit(1)
}
}
if issue.IssueType != types.TypeGate {
fmt.Fprintf(os.Stderr, "Error: %s is not a gate issue (type=%s)\n", gateID, issue.IssueType)
os.Exit(1)
}
if jsonOutput {
outputJSON(issue)
return
}
// Display gate details
statusSym := "○"
if issue.Status == types.StatusClosed {
statusSym = "●"
}
fmt.Printf("%s %s - %s\n", statusSym, ui.RenderID(issue.ID), issue.Title)
fmt.Printf(" Status: %s\n", issue.Status)
fmt.Printf(" Await Type: %s\n", issue.AwaitType)
if issue.AwaitID != "" {
fmt.Printf(" Await ID: %s\n", issue.AwaitID)
}
if issue.Timeout > 0 {
fmt.Printf(" Timeout: %s\n", issue.Timeout)
}
if len(issue.Waiters) > 0 {
fmt.Printf(" Waiters:\n")
for _, w := range issue.Waiters {
fmt.Printf(" - %s\n", w)
}
}
if issue.Description != "" {
fmt.Printf(" Description: %s\n", issue.Description)
}
},
}
// gateResolveCmd manually closes a gate
var gateResolveCmd = &cobra.Command{
Use: "resolve <gate-id>",
@@ -703,8 +872,10 @@ func init() {
// Add subcommands
gateCmd.AddCommand(gateListCmd)
gateCmd.AddCommand(gateShowCmd)
gateCmd.AddCommand(gateResolveCmd)
gateCmd.AddCommand(gateCheckCmd)
gateCmd.AddCommand(gateAddWaiterCmd)
rootCmd.AddCommand(gateCmd)
}