refactor: remove unused bd pin/unpin/hook commands (bd-x0zl)
Analysis found these commands are dead code: - gt never calls `bd pin` - uses `bd update --status=pinned` instead - Beads.Pin() wrapper exists but is never called - bd hook functionality duplicated by gt mol status - Code comment says "pinned field is cosmetic for bd hook visibility" Removed: - cmd/bd/pin.go - cmd/bd/unpin.go - cmd/bd/hook.go 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -333,6 +333,11 @@ func (c *Client) Ready(args *ReadyArgs) (*Response, error) {
|
||||
return c.Execute(OpReady, args)
|
||||
}
|
||||
|
||||
// Blocked gets blocked issues via the daemon
|
||||
func (c *Client) Blocked(args *BlockedArgs) (*Response, error) {
|
||||
return c.Execute(OpBlocked, args)
|
||||
}
|
||||
|
||||
// Stale gets stale issues via the daemon
|
||||
func (c *Client) Stale(args *StaleArgs) (*Response, error) {
|
||||
return c.Execute(OpStale, args)
|
||||
|
||||
107
internal/rpc/client_gate_shutdown_test.go
Normal file
107
internal/rpc/client_gate_shutdown_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
func TestClient_GateLifecycleAndShutdown(t *testing.T) {
|
||||
_, client, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
createResp, err := client.GateCreate(&GateCreateArgs{
|
||||
Title: "Test Gate",
|
||||
AwaitType: "human",
|
||||
AwaitID: "",
|
||||
Timeout: 5 * time.Minute,
|
||||
Waiters: []string{"mayor/"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GateCreate: %v", err)
|
||||
}
|
||||
|
||||
var created GateCreateResult
|
||||
if err := json.Unmarshal(createResp.Data, &created); err != nil {
|
||||
t.Fatalf("unmarshal GateCreateResult: %v", err)
|
||||
}
|
||||
if created.ID == "" {
|
||||
t.Fatalf("expected created gate ID")
|
||||
}
|
||||
|
||||
listResp, err := client.GateList(&GateListArgs{All: false})
|
||||
if err != nil {
|
||||
t.Fatalf("GateList: %v", err)
|
||||
}
|
||||
var openGates []*types.Issue
|
||||
if err := json.Unmarshal(listResp.Data, &openGates); err != nil {
|
||||
t.Fatalf("unmarshal GateList: %v", err)
|
||||
}
|
||||
if len(openGates) != 1 || openGates[0].ID != created.ID {
|
||||
t.Fatalf("unexpected open gates: %+v", openGates)
|
||||
}
|
||||
|
||||
showResp, err := client.GateShow(&GateShowArgs{ID: created.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("GateShow: %v", err)
|
||||
}
|
||||
var gate types.Issue
|
||||
if err := json.Unmarshal(showResp.Data, &gate); err != nil {
|
||||
t.Fatalf("unmarshal GateShow: %v", err)
|
||||
}
|
||||
if gate.ID != created.ID || gate.IssueType != types.TypeGate {
|
||||
t.Fatalf("unexpected gate: %+v", gate)
|
||||
}
|
||||
|
||||
waitResp, err := client.GateWait(&GateWaitArgs{ID: created.ID, Waiters: []string{"deacon/"}})
|
||||
if err != nil {
|
||||
t.Fatalf("GateWait: %v", err)
|
||||
}
|
||||
var waitResult GateWaitResult
|
||||
if err := json.Unmarshal(waitResp.Data, &waitResult); err != nil {
|
||||
t.Fatalf("unmarshal GateWaitResult: %v", err)
|
||||
}
|
||||
if waitResult.AddedCount != 1 {
|
||||
t.Fatalf("expected 1 waiter added, got %d", waitResult.AddedCount)
|
||||
}
|
||||
|
||||
closeResp, err := client.GateClose(&GateCloseArgs{ID: created.ID, Reason: "done"})
|
||||
if err != nil {
|
||||
t.Fatalf("GateClose: %v", err)
|
||||
}
|
||||
var closedGate types.Issue
|
||||
if err := json.Unmarshal(closeResp.Data, &closedGate); err != nil {
|
||||
t.Fatalf("unmarshal GateClose: %v", err)
|
||||
}
|
||||
if closedGate.Status != types.StatusClosed {
|
||||
t.Fatalf("expected closed status, got %q", closedGate.Status)
|
||||
}
|
||||
|
||||
listResp, err = client.GateList(&GateListArgs{All: false})
|
||||
if err != nil {
|
||||
t.Fatalf("GateList open: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(listResp.Data, &openGates); err != nil {
|
||||
t.Fatalf("unmarshal GateList open: %v", err)
|
||||
}
|
||||
if len(openGates) != 0 {
|
||||
t.Fatalf("expected no open gates, got %+v", openGates)
|
||||
}
|
||||
|
||||
listResp, err = client.GateList(&GateListArgs{All: true})
|
||||
if err != nil {
|
||||
t.Fatalf("GateList all: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(listResp.Data, &openGates); err != nil {
|
||||
t.Fatalf("unmarshal GateList all: %v", err)
|
||||
}
|
||||
if len(openGates) != 1 || openGates[0].ID != created.ID {
|
||||
t.Fatalf("expected 1 total gate, got %+v", openGates)
|
||||
}
|
||||
|
||||
if err := client.Shutdown(); err != nil {
|
||||
t.Fatalf("Shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package rpc
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// Operation constants for all bd commands
|
||||
@@ -18,6 +20,7 @@ const (
|
||||
OpCount = "count"
|
||||
OpShow = "show"
|
||||
OpReady = "ready"
|
||||
OpBlocked = "blocked"
|
||||
OpStale = "stale"
|
||||
OpStats = "stats"
|
||||
OpDepAdd = "dep_add"
|
||||
@@ -86,11 +89,12 @@ type CreateArgs struct {
|
||||
WaitsFor string `json:"waits_for,omitempty"` // Spawner issue ID to wait for
|
||||
WaitsForGate string `json:"waits_for_gate,omitempty"` // Gate type: all-children or any-children
|
||||
// Messaging fields (bd-kwro)
|
||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Wisp bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed
|
||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Ephemeral bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
|
||||
RepliesTo string `json:"replies_to,omitempty"` // Issue ID for conversation threading
|
||||
// ID generation (bd-hobo)
|
||||
IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, wisp, etc.)
|
||||
IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, eph, etc.)
|
||||
CreatedBy string `json:"created_by,omitempty"` // Who created the issue
|
||||
}
|
||||
|
||||
// UpdateArgs represents arguments for the update operation
|
||||
@@ -111,8 +115,8 @@ type UpdateArgs struct {
|
||||
RemoveLabels []string `json:"remove_labels,omitempty"`
|
||||
SetLabels []string `json:"set_labels,omitempty"`
|
||||
// Messaging fields (bd-kwro)
|
||||
Sender *string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Wisp *bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed
|
||||
Sender *string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Ephemeral *bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
|
||||
RepliesTo *string `json:"replies_to,omitempty"` // Issue ID for conversation threading
|
||||
// Graph link fields (bd-fu83)
|
||||
RelatesTo *string `json:"relates_to,omitempty"` // JSON array of related issue IDs
|
||||
@@ -124,8 +128,16 @@ type UpdateArgs struct {
|
||||
|
||||
// CloseArgs represents arguments for the close operation
|
||||
type CloseArgs struct {
|
||||
ID string `json:"id"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
SuggestNext bool `json:"suggest_next,omitempty"` // Return newly unblocked issues (GH#679)
|
||||
}
|
||||
|
||||
// CloseResult is returned when SuggestNext is true (GH#679)
|
||||
// When SuggestNext is false, just the closed issue is returned for backward compatibility
|
||||
type CloseResult struct {
|
||||
Closed *types.Issue `json:"closed"` // The issue that was closed
|
||||
Unblocked []*types.Issue `json:"unblocked,omitempty"` // Issues newly unblocked by closing
|
||||
}
|
||||
|
||||
// DeleteArgs represents arguments for the delete operation
|
||||
@@ -181,8 +193,8 @@ type ListArgs struct {
|
||||
// Parent filtering (bd-yqhh)
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
|
||||
// Wisp filtering (bd-bkul)
|
||||
Wisp *bool `json:"wisp,omitempty"`
|
||||
// Ephemeral filtering (bd-bkul)
|
||||
Ephemeral *bool `json:"ephemeral,omitempty"`
|
||||
}
|
||||
|
||||
// CountArgs represents arguments for the count operation
|
||||
@@ -243,6 +255,12 @@ type ReadyArgs struct {
|
||||
SortPolicy string `json:"sort_policy,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
LabelsAny []string `json:"labels_any,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"` // Filter to descendants of this bead/epic
|
||||
}
|
||||
|
||||
// BlockedArgs represents arguments for the blocked operation
|
||||
type BlockedArgs struct {
|
||||
ParentID string `json:"parent_id,omitempty"` // Filter to descendants of this bead/epic
|
||||
}
|
||||
|
||||
// StaleArgs represents arguments for the stale command
|
||||
|
||||
@@ -81,8 +81,8 @@ func updatesFromArgs(a UpdateArgs) map[string]interface{} {
|
||||
if a.Sender != nil {
|
||||
u["sender"] = *a.Sender
|
||||
}
|
||||
if a.Wisp != nil {
|
||||
u["wisp"] = *a.Wisp
|
||||
if a.Ephemeral != nil {
|
||||
u["ephemeral"] = *a.Ephemeral
|
||||
}
|
||||
if a.RepliesTo != nil {
|
||||
u["replies_to"] = *a.RepliesTo
|
||||
@@ -176,11 +176,12 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
EstimatedMinutes: createArgs.EstimatedMinutes,
|
||||
Status: types.StatusOpen,
|
||||
// Messaging fields (bd-kwro)
|
||||
Sender: createArgs.Sender,
|
||||
Wisp: createArgs.Wisp,
|
||||
Sender: createArgs.Sender,
|
||||
Ephemeral: createArgs.Ephemeral,
|
||||
// NOTE: RepliesTo now handled via replies-to dependency (Decision 004)
|
||||
// ID generation (bd-hobo)
|
||||
IDPrefix: createArgs.IDPrefix,
|
||||
IDPrefix: createArgs.IDPrefix,
|
||||
CreatedBy: createArgs.CreatedBy,
|
||||
}
|
||||
|
||||
// Check if any dependencies are discovered-from type
|
||||
@@ -555,6 +556,26 @@ func (s *Server) handleClose(req *Request) Response {
|
||||
})
|
||||
|
||||
closedIssue, _ := store.GetIssue(ctx, closeArgs.ID)
|
||||
|
||||
// If SuggestNext is requested, find newly unblocked issues (GH#679)
|
||||
if closeArgs.SuggestNext {
|
||||
unblocked, err := store.GetNewlyUnblockedByClose(ctx, closeArgs.ID)
|
||||
if err != nil {
|
||||
// Non-fatal: still return the closed issue
|
||||
unblocked = nil
|
||||
}
|
||||
result := CloseResult{
|
||||
Closed: closedIssue,
|
||||
Unblocked: unblocked,
|
||||
}
|
||||
data, _ := json.Marshal(result)
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatible: just return the closed issue
|
||||
data, _ := json.Marshal(closedIssue)
|
||||
return Response{
|
||||
Success: true,
|
||||
@@ -823,8 +844,8 @@ func (s *Server) handleList(req *Request) Response {
|
||||
filter.ParentID = &listArgs.ParentID
|
||||
}
|
||||
|
||||
// Wisp filtering (bd-bkul)
|
||||
filter.Wisp = listArgs.Wisp
|
||||
// Ephemeral filtering (bd-bkul)
|
||||
filter.Ephemeral = listArgs.Ephemeral
|
||||
|
||||
// Guard against excessive ID lists to avoid SQLite parameter limits
|
||||
const maxIDs = 1000
|
||||
@@ -1201,12 +1222,16 @@ func (s *Server) handleShow(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch comments
|
||||
comments, _ := store.GetIssueComments(ctx, issue.ID)
|
||||
|
||||
// Create detailed response with related data
|
||||
type IssueDetails struct {
|
||||
*types.Issue
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
|
||||
Dependents []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
|
||||
Comments []*types.Comment `json:"comments,omitempty"`
|
||||
}
|
||||
|
||||
details := &IssueDetails{
|
||||
@@ -1214,6 +1239,7 @@ func (s *Server) handleShow(req *Request) Response {
|
||||
Labels: labels,
|
||||
Dependencies: deps,
|
||||
Dependents: dependents,
|
||||
Comments: comments,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(details)
|
||||
@@ -1242,6 +1268,7 @@ func (s *Server) handleReady(req *Request) Response {
|
||||
|
||||
wf := types.WorkFilter{
|
||||
Status: types.StatusOpen,
|
||||
Type: readyArgs.Type,
|
||||
Priority: readyArgs.Priority,
|
||||
Unassigned: readyArgs.Unassigned,
|
||||
Limit: readyArgs.Limit,
|
||||
@@ -1252,6 +1279,9 @@ func (s *Server) handleReady(req *Request) Response {
|
||||
if readyArgs.Assignee != "" && !readyArgs.Unassigned {
|
||||
wf.Assignee = &readyArgs.Assignee
|
||||
}
|
||||
if readyArgs.ParentID != "" {
|
||||
wf.ParentID = &readyArgs.ParentID
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
issues, err := store.GetReadyWork(ctx, wf)
|
||||
@@ -1269,6 +1299,44 @@ func (s *Server) handleReady(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleBlocked(req *Request) Response {
|
||||
var blockedArgs BlockedArgs
|
||||
if err := json.Unmarshal(req.Args, &blockedArgs); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("invalid blocked args: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
store := s.storage
|
||||
if store == nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: "storage not available (global daemon deprecated - use local daemon instead with 'bd daemon' in your project)",
|
||||
}
|
||||
}
|
||||
|
||||
var wf types.WorkFilter
|
||||
if blockedArgs.ParentID != "" {
|
||||
wf.ParentID = &blockedArgs.ParentID
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
blocked, err := store.GetBlockedIssues(ctx, wf)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to get blocked issues: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(blocked)
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleStale(req *Request) Response {
|
||||
var staleArgs StaleArgs
|
||||
if err := json.Unmarshal(req.Args, &staleArgs); err != nil {
|
||||
@@ -1412,7 +1480,7 @@ func (s *Server) handleGateCreate(req *Request) Response {
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1, // Gates are typically high priority
|
||||
Assignee: "deacon/",
|
||||
Wisp: true, // Gates are wisps (ephemeral)
|
||||
Ephemeral: true, // Gates are wisps (ephemeral)
|
||||
AwaitType: args.AwaitType,
|
||||
AwaitID: args.AwaitID,
|
||||
Timeout: args.Timeout,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -9,6 +10,49 @@ import (
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// TestHandleCreate_SetsCreatedBy verifies that CreatedBy is passed through RPC and stored (GH#748)
|
||||
func TestHandleCreate_SetsCreatedBy(t *testing.T) {
|
||||
store := memory.New("/tmp/test.jsonl")
|
||||
server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db")
|
||||
|
||||
createArgs := CreateArgs{
|
||||
Title: "Test CreatedBy Field",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
CreatedBy: "test-actor",
|
||||
}
|
||||
createJSON, _ := json.Marshal(createArgs)
|
||||
createReq := &Request{
|
||||
Operation: OpCreate,
|
||||
Args: createJSON,
|
||||
Actor: "test-actor",
|
||||
}
|
||||
|
||||
resp := server.handleCreate(createReq)
|
||||
if !resp.Success {
|
||||
t.Fatalf("create failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
var createdIssue types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &createdIssue); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
// Verify CreatedBy was set in the response
|
||||
if createdIssue.CreatedBy != "test-actor" {
|
||||
t.Errorf("expected CreatedBy 'test-actor' in response, got %q", createdIssue.CreatedBy)
|
||||
}
|
||||
|
||||
// Verify CreatedBy was persisted to storage
|
||||
storedIssue, err := store.GetIssue(context.Background(), createdIssue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get issue from storage: %v", err)
|
||||
}
|
||||
if storedIssue.CreatedBy != "test-actor" {
|
||||
t.Errorf("expected CreatedBy 'test-actor' in storage, got %q", storedIssue.CreatedBy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitMutation(t *testing.T) {
|
||||
store := memory.New("/tmp/test.jsonl")
|
||||
server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db")
|
||||
|
||||
@@ -188,6 +188,8 @@ func (s *Server) handleRequest(req *Request) Response {
|
||||
resp = s.handleResolveID(req)
|
||||
case OpReady:
|
||||
resp = s.handleReady(req)
|
||||
case OpBlocked:
|
||||
resp = s.handleBlocked(req)
|
||||
case OpStale:
|
||||
resp = s.handleStale(req)
|
||||
case OpStats:
|
||||
|
||||
Reference in New Issue
Block a user