Adds the ability to filter ready work for issues with no assignee, which is useful for the SCAVENGE protocol in Gas Town where polecats need to query the "Salvage Yard" for unclaimed work. Changes: - Add Unassigned bool field to types.WorkFilter - Add --unassigned/-u flag to bd ready command - Update SQL query in GetReadyWork to filter for NULL/empty assignee - Add Unassigned field to RPC ReadyArgs for daemon support - Add tests for the new functionality Closes: gt-3rp 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
369 lines
14 KiB
Go
369 lines
14 KiB
Go
package rpc
|
|
|
|
import (
|
|
"encoding/json"
|
|
)
|
|
|
|
// Operation constants for all bd commands
|
|
const (
|
|
OpPing = "ping"
|
|
OpStatus = "status"
|
|
OpHealth = "health"
|
|
OpMetrics = "metrics"
|
|
OpCreate = "create"
|
|
OpUpdate = "update"
|
|
OpClose = "close"
|
|
OpList = "list"
|
|
OpCount = "count"
|
|
OpShow = "show"
|
|
OpReady = "ready"
|
|
OpStale = "stale"
|
|
OpStats = "stats"
|
|
OpDepAdd = "dep_add"
|
|
OpDepRemove = "dep_remove"
|
|
OpDepTree = "dep_tree"
|
|
OpLabelAdd = "label_add"
|
|
OpLabelRemove = "label_remove"
|
|
OpCommentList = "comment_list"
|
|
OpCommentAdd = "comment_add"
|
|
OpBatch = "batch"
|
|
OpResolveID = "resolve_id"
|
|
|
|
OpCompact = "compact"
|
|
OpCompactStats = "compact_stats"
|
|
OpExport = "export"
|
|
OpImport = "import"
|
|
OpEpicStatus = "epic_status"
|
|
OpGetMutations = "get_mutations"
|
|
OpShutdown = "shutdown"
|
|
)
|
|
|
|
// Request represents an RPC request from client to daemon
|
|
type Request struct {
|
|
Operation string `json:"operation"`
|
|
Args json.RawMessage `json:"args"`
|
|
Actor string `json:"actor,omitempty"`
|
|
RequestID string `json:"request_id,omitempty"`
|
|
Cwd string `json:"cwd,omitempty"` // Working directory for database discovery
|
|
ClientVersion string `json:"client_version,omitempty"` // Client version for compatibility checks
|
|
ExpectedDB string `json:"expected_db,omitempty"` // Expected database path for validation (absolute)
|
|
}
|
|
|
|
// Response represents an RPC response from daemon to client
|
|
type Response struct {
|
|
Success bool `json:"success"`
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// CreateArgs represents arguments for the create operation
|
|
type CreateArgs struct {
|
|
ID string `json:"id,omitempty"`
|
|
Parent string `json:"parent,omitempty"` // Parent ID for hierarchical issues
|
|
Title string `json:"title"`
|
|
Description string `json:"description,omitempty"`
|
|
IssueType string `json:"issue_type"`
|
|
Priority int `json:"priority"`
|
|
Design string `json:"design,omitempty"`
|
|
AcceptanceCriteria string `json:"acceptance_criteria,omitempty"`
|
|
Assignee string `json:"assignee,omitempty"`
|
|
ExternalRef string `json:"external_ref,omitempty"` // Link to external issue trackers
|
|
Labels []string `json:"labels,omitempty"`
|
|
Dependencies []string `json:"dependencies,omitempty"`
|
|
}
|
|
|
|
// UpdateArgs represents arguments for the update operation
|
|
type UpdateArgs struct {
|
|
ID string `json:"id"`
|
|
Title *string `json:"title,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
Status *string `json:"status,omitempty"`
|
|
Priority *int `json:"priority,omitempty"`
|
|
Design *string `json:"design,omitempty"`
|
|
AcceptanceCriteria *string `json:"acceptance_criteria,omitempty"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
Assignee *string `json:"assignee,omitempty"`
|
|
ExternalRef *string `json:"external_ref,omitempty"` // Link to external issue trackers
|
|
AddLabels []string `json:"add_labels,omitempty"`
|
|
RemoveLabels []string `json:"remove_labels,omitempty"`
|
|
SetLabels []string `json:"set_labels,omitempty"`
|
|
}
|
|
|
|
// CloseArgs represents arguments for the close operation
|
|
type CloseArgs struct {
|
|
ID string `json:"id"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// ListArgs represents arguments for the list operation
|
|
type ListArgs struct {
|
|
Query string `json:"query,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Priority *int `json:"priority,omitempty"`
|
|
IssueType string `json:"issue_type,omitempty"`
|
|
Assignee string `json:"assignee,omitempty"`
|
|
Label string `json:"label,omitempty"` // Deprecated: use Labels
|
|
Labels []string `json:"labels,omitempty"` // AND semantics
|
|
LabelsAny []string `json:"labels_any,omitempty"` // OR semantics
|
|
IDs []string `json:"ids,omitempty"` // Filter by specific issue IDs
|
|
Limit int `json:"limit,omitempty"`
|
|
|
|
// Pattern matching
|
|
TitleContains string `json:"title_contains,omitempty"`
|
|
DescriptionContains string `json:"description_contains,omitempty"`
|
|
NotesContains string `json:"notes_contains,omitempty"`
|
|
|
|
// Date ranges (ISO 8601 format)
|
|
CreatedAfter string `json:"created_after,omitempty"`
|
|
CreatedBefore string `json:"created_before,omitempty"`
|
|
UpdatedAfter string `json:"updated_after,omitempty"`
|
|
UpdatedBefore string `json:"updated_before,omitempty"`
|
|
ClosedAfter string `json:"closed_after,omitempty"`
|
|
ClosedBefore string `json:"closed_before,omitempty"`
|
|
|
|
// Empty/null checks
|
|
EmptyDescription bool `json:"empty_description,omitempty"`
|
|
NoAssignee bool `json:"no_assignee,omitempty"`
|
|
NoLabels bool `json:"no_labels,omitempty"`
|
|
|
|
// Priority range
|
|
PriorityMin *int `json:"priority_min,omitempty"`
|
|
PriorityMax *int `json:"priority_max,omitempty"`
|
|
}
|
|
|
|
// CountArgs represents arguments for the count operation
|
|
type CountArgs struct {
|
|
// Supports all the same filters as ListArgs
|
|
Query string `json:"query,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Priority *int `json:"priority,omitempty"`
|
|
IssueType string `json:"issue_type,omitempty"`
|
|
Assignee string `json:"assignee,omitempty"`
|
|
Labels []string `json:"labels,omitempty"`
|
|
LabelsAny []string `json:"labels_any,omitempty"`
|
|
IDs []string `json:"ids,omitempty"`
|
|
|
|
// Pattern matching
|
|
TitleContains string `json:"title_contains,omitempty"`
|
|
DescriptionContains string `json:"description_contains,omitempty"`
|
|
NotesContains string `json:"notes_contains,omitempty"`
|
|
|
|
// Date ranges
|
|
CreatedAfter string `json:"created_after,omitempty"`
|
|
CreatedBefore string `json:"created_before,omitempty"`
|
|
UpdatedAfter string `json:"updated_after,omitempty"`
|
|
UpdatedBefore string `json:"updated_before,omitempty"`
|
|
ClosedAfter string `json:"closed_after,omitempty"`
|
|
ClosedBefore string `json:"closed_before,omitempty"`
|
|
|
|
// Empty/null checks
|
|
EmptyDescription bool `json:"empty_description,omitempty"`
|
|
NoAssignee bool `json:"no_assignee,omitempty"`
|
|
NoLabels bool `json:"no_labels,omitempty"`
|
|
|
|
// Priority range
|
|
PriorityMin *int `json:"priority_min,omitempty"`
|
|
PriorityMax *int `json:"priority_max,omitempty"`
|
|
|
|
// Grouping option (only one can be specified)
|
|
GroupBy string `json:"group_by,omitempty"` // "status", "priority", "type", "assignee", "label"
|
|
}
|
|
|
|
// ShowArgs represents arguments for the show operation
|
|
type ShowArgs struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// ResolveIDArgs represents arguments for the resolve_id operation
|
|
type ResolveIDArgs struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// ReadyArgs represents arguments for the ready operation
|
|
type ReadyArgs struct {
|
|
Assignee string `json:"assignee,omitempty"`
|
|
Unassigned bool `json:"unassigned,omitempty"`
|
|
Priority *int `json:"priority,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
SortPolicy string `json:"sort_policy,omitempty"`
|
|
Labels []string `json:"labels,omitempty"`
|
|
LabelsAny []string `json:"labels_any,omitempty"`
|
|
}
|
|
|
|
// StaleArgs represents arguments for the stale command
|
|
type StaleArgs struct {
|
|
Days int `json:"days,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
|
|
// DepAddArgs represents arguments for adding a dependency
|
|
type DepAddArgs struct {
|
|
FromID string `json:"from_id"`
|
|
ToID string `json:"to_id"`
|
|
DepType string `json:"dep_type"`
|
|
}
|
|
|
|
// DepRemoveArgs represents arguments for removing a dependency
|
|
type DepRemoveArgs struct {
|
|
FromID string `json:"from_id"`
|
|
ToID string `json:"to_id"`
|
|
DepType string `json:"dep_type,omitempty"`
|
|
}
|
|
|
|
// DepTreeArgs represents arguments for the dep tree operation
|
|
type DepTreeArgs struct {
|
|
ID string `json:"id"`
|
|
MaxDepth int `json:"max_depth,omitempty"`
|
|
}
|
|
|
|
// LabelAddArgs represents arguments for adding a label
|
|
type LabelAddArgs struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
// LabelRemoveArgs represents arguments for removing a label
|
|
type LabelRemoveArgs struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
// CommentListArgs represents arguments for listing comments on an issue
|
|
type CommentListArgs struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// CommentAddArgs represents arguments for adding a comment to an issue
|
|
type CommentAddArgs struct {
|
|
ID string `json:"id"`
|
|
Author string `json:"author"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// EpicStatusArgs represents arguments for the epic status operation
|
|
type EpicStatusArgs struct {
|
|
EligibleOnly bool `json:"eligible_only,omitempty"`
|
|
}
|
|
|
|
// PingResponse is the response for a ping operation
|
|
type PingResponse struct {
|
|
Message string `json:"message"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// StatusResponse represents the daemon status metadata
|
|
type StatusResponse struct {
|
|
Version string `json:"version"` // Server/daemon version
|
|
WorkspacePath string `json:"workspace_path"` // Absolute path to workspace root
|
|
DatabasePath string `json:"database_path"` // Absolute path to database file
|
|
SocketPath string `json:"socket_path"` // Path to Unix socket
|
|
PID int `json:"pid"` // Process ID
|
|
UptimeSeconds float64 `json:"uptime_seconds"` // Time since daemon started
|
|
LastActivityTime string `json:"last_activity_time"` // ISO 8601 timestamp of last request
|
|
ExclusiveLockActive bool `json:"exclusive_lock_active"` // Whether an exclusive lock is held
|
|
ExclusiveLockHolder string `json:"exclusive_lock_holder,omitempty"` // Lock holder name if active
|
|
}
|
|
|
|
// HealthResponse is the response for a health check operation
|
|
type HealthResponse struct {
|
|
Status string `json:"status"` // "healthy", "degraded", "unhealthy"
|
|
Version string `json:"version"` // Server/daemon version
|
|
ClientVersion string `json:"client_version,omitempty"` // Client version from request
|
|
Compatible bool `json:"compatible"` // Whether versions are compatible
|
|
Uptime float64 `json:"uptime_seconds"`
|
|
DBResponseTime float64 `json:"db_response_ms"`
|
|
ActiveConns int32 `json:"active_connections"`
|
|
MaxConns int `json:"max_connections"`
|
|
MemoryAllocMB uint64 `json:"memory_alloc_mb"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// BatchArgs represents arguments for batch operations
|
|
type BatchArgs struct {
|
|
Operations []BatchOperation `json:"operations"`
|
|
}
|
|
|
|
// BatchOperation represents a single operation in a batch
|
|
type BatchOperation struct {
|
|
Operation string `json:"operation"`
|
|
Args json.RawMessage `json:"args"`
|
|
}
|
|
|
|
// BatchResponse contains the results of a batch operation
|
|
type BatchResponse struct {
|
|
Results []BatchResult `json:"results"`
|
|
}
|
|
|
|
// BatchResult represents the result of a single operation in a batch
|
|
type BatchResult struct {
|
|
Success bool `json:"success"`
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// CompactArgs represents arguments for the compact operation
|
|
type CompactArgs struct {
|
|
IssueID string `json:"issue_id,omitempty"` // Empty for --all
|
|
Tier int `json:"tier"` // 1 or 2
|
|
DryRun bool `json:"dry_run"`
|
|
Force bool `json:"force"`
|
|
All bool `json:"all"`
|
|
APIKey string `json:"api_key,omitempty"`
|
|
Workers int `json:"workers,omitempty"`
|
|
BatchSize int `json:"batch_size,omitempty"`
|
|
}
|
|
|
|
// CompactStatsArgs represents arguments for compact stats operation
|
|
type CompactStatsArgs struct {
|
|
Tier int `json:"tier,omitempty"`
|
|
}
|
|
|
|
// CompactResponse represents the response from a compact operation
|
|
type CompactResponse struct {
|
|
Success bool `json:"success"`
|
|
IssueID string `json:"issue_id,omitempty"`
|
|
Results []CompactResult `json:"results,omitempty"` // For batch operations
|
|
Stats *CompactStatsData `json:"stats,omitempty"` // For stats operation
|
|
OriginalSize int `json:"original_size,omitempty"`
|
|
CompactedSize int `json:"compacted_size,omitempty"`
|
|
Reduction string `json:"reduction,omitempty"`
|
|
Duration string `json:"duration,omitempty"`
|
|
DryRun bool `json:"dry_run,omitempty"`
|
|
}
|
|
|
|
// CompactResult represents the result of compacting a single issue
|
|
type CompactResult struct {
|
|
IssueID string `json:"issue_id"`
|
|
Success bool `json:"success"`
|
|
Error string `json:"error,omitempty"`
|
|
OriginalSize int `json:"original_size,omitempty"`
|
|
CompactedSize int `json:"compacted_size,omitempty"`
|
|
Reduction string `json:"reduction,omitempty"`
|
|
}
|
|
|
|
// CompactStatsData represents compaction statistics
|
|
type CompactStatsData struct {
|
|
Tier1Candidates int `json:"tier1_candidates"`
|
|
Tier2Candidates int `json:"tier2_candidates"`
|
|
TotalClosed int `json:"total_closed"`
|
|
Tier1MinAge string `json:"tier1_min_age"`
|
|
Tier2MinAge string `json:"tier2_min_age"`
|
|
EstimatedSavings string `json:"estimated_savings,omitempty"`
|
|
}
|
|
|
|
// ExportArgs represents arguments for the export operation
|
|
type ExportArgs struct {
|
|
JSONLPath string `json:"jsonl_path"` // Path to export JSONL file
|
|
}
|
|
|
|
// ImportArgs represents arguments for the import operation
|
|
type ImportArgs struct {
|
|
JSONLPath string `json:"jsonl_path"` // Path to import JSONL file
|
|
}
|
|
|
|
// GetMutationsArgs represents arguments for retrieving recent mutations
|
|
type GetMutationsArgs struct {
|
|
Since int64 `json:"since"` // Unix timestamp in milliseconds (0 for all recent)
|
|
}
|