feat(types): add StatusPinned for persistent beads (bd-6v2)
Add pinned status for beads that should stay open indefinitely: - Add StatusPinned constant and update IsValid() - Add PinnedIssues count to Statistics struct - Protect pinned issues from bd close (requires --force) - Show pinned count in bd stats output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -272,6 +272,9 @@ var statsCmd = &cobra.Command{
|
||||
if stats.TombstoneIssues > 0 {
|
||||
fmt.Printf("Deleted: %d (tombstones)\n", stats.TombstoneIssues)
|
||||
}
|
||||
if stats.PinnedIssues > 0 {
|
||||
fmt.Printf("Pinned: %d\n", stats.PinnedIssues)
|
||||
}
|
||||
if stats.AverageLeadTime > 0 {
|
||||
fmt.Printf("Avg Lead Time: %.1f hours\n", stats.AverageLeadTime)
|
||||
}
|
||||
@@ -313,6 +316,9 @@ var statsCmd = &cobra.Command{
|
||||
if stats.TombstoneIssues > 0 {
|
||||
fmt.Printf("Deleted: %d (tombstones)\n", stats.TombstoneIssues)
|
||||
}
|
||||
if stats.PinnedIssues > 0 {
|
||||
fmt.Printf("Pinned: %d\n", stats.PinnedIssues)
|
||||
}
|
||||
if stats.EpicsEligibleForClosure > 0 {
|
||||
fmt.Printf("Epics Ready to Close: %s\n", green(fmt.Sprintf("%d", stats.EpicsEligibleForClosure)))
|
||||
}
|
||||
|
||||
@@ -968,6 +968,7 @@ var closeCmd = &cobra.Command{
|
||||
reason = "Closed"
|
||||
}
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
ctx := rootCtx
|
||||
|
||||
@@ -1001,6 +1002,21 @@ var closeCmd = &cobra.Command{
|
||||
if daemonClient != nil {
|
||||
closedIssues := []*types.Issue{}
|
||||
for _, id := range resolvedIDs {
|
||||
// Check if issue is pinned (bd-6v2)
|
||||
if !force {
|
||||
showArgs := &rpc.ShowArgs{ID: id}
|
||||
showResp, showErr := daemonClient.Show(showArgs)
|
||||
if showErr == nil {
|
||||
var issue types.Issue
|
||||
if json.Unmarshal(showResp.Data, &issue) == nil {
|
||||
if issue.Status == types.StatusPinned {
|
||||
fmt.Fprintf(os.Stderr, "Error: cannot close pinned issue %s (use --force to override)\n", id)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeArgs := &rpc.CloseArgs{
|
||||
ID: id,
|
||||
Reason: reason,
|
||||
@@ -1036,6 +1052,15 @@ var closeCmd = &cobra.Command{
|
||||
// Direct mode
|
||||
closedIssues := []*types.Issue{}
|
||||
for _, id := range resolvedIDs {
|
||||
// Check if issue is pinned (bd-6v2)
|
||||
if !force {
|
||||
issue, _ := store.GetIssue(ctx, id)
|
||||
if issue != nil && issue.Status == types.StatusPinned {
|
||||
fmt.Fprintf(os.Stderr, "Error: cannot close pinned issue %s (use --force to override)\n", id)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.CloseIssue(ctx, id, reason, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", id, err)
|
||||
continue
|
||||
@@ -1335,5 +1360,6 @@ func init() {
|
||||
|
||||
closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
|
||||
closeCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
closeCmd.Flags().BoolP("force", "f", false, "Force close pinned issues")
|
||||
rootCmd.AddCommand(closeCmd)
|
||||
}
|
||||
|
||||
@@ -111,15 +111,17 @@ func (s *SQLiteStorage) GetStatistics(ctx context.Context) (*types.Statistics, e
|
||||
var stats types.Statistics
|
||||
|
||||
// Get counts (bd-nyt: exclude tombstones from TotalIssues, report separately)
|
||||
// (bd-6v2: also count pinned issues)
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN status != 'tombstone' THEN 1 ELSE 0 END), 0) as total,
|
||||
COALESCE(SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END), 0) as open,
|
||||
COALESCE(SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END), 0) as in_progress,
|
||||
COALESCE(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0) as closed,
|
||||
COALESCE(SUM(CASE WHEN status = 'tombstone' THEN 1 ELSE 0 END), 0) as tombstone
|
||||
COALESCE(SUM(CASE WHEN status = 'tombstone' THEN 1 ELSE 0 END), 0) as tombstone,
|
||||
COALESCE(SUM(CASE WHEN status = 'pinned' THEN 1 ELSE 0 END), 0) as pinned
|
||||
FROM issues
|
||||
`).Scan(&stats.TotalIssues, &stats.OpenIssues, &stats.InProgressIssues, &stats.ClosedIssues, &stats.TombstoneIssues)
|
||||
`).Scan(&stats.TotalIssues, &stats.OpenIssues, &stats.InProgressIssues, &stats.ClosedIssues, &stats.TombstoneIssues, &stats.PinnedIssues)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get issue counts: %w", err)
|
||||
}
|
||||
|
||||
@@ -186,12 +186,13 @@ const (
|
||||
StatusBlocked Status = "blocked"
|
||||
StatusClosed Status = "closed"
|
||||
StatusTombstone Status = "tombstone" // Soft-deleted issue (bd-vw8)
|
||||
StatusPinned Status = "pinned" // Persistent bead that stays open indefinitely (bd-6v2)
|
||||
)
|
||||
|
||||
// IsValid checks if the status value is valid (built-in statuses only)
|
||||
func (s Status) IsValid() bool {
|
||||
switch s {
|
||||
case StatusOpen, StatusInProgress, StatusBlocked, StatusClosed, StatusTombstone:
|
||||
case StatusOpen, StatusInProgress, StatusBlocked, StatusClosed, StatusTombstone, StatusPinned:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -390,6 +391,7 @@ type Statistics struct {
|
||||
BlockedIssues int `json:"blocked_issues"`
|
||||
ReadyIssues int `json:"ready_issues"`
|
||||
TombstoneIssues int `json:"tombstone_issues"` // Soft-deleted issues (bd-nyt)
|
||||
PinnedIssues int `json:"pinned_issues"` // Persistent issues (bd-6v2)
|
||||
EpicsEligibleForClosure int `json:"epics_eligible_for_closure"`
|
||||
AverageLeadTime float64 `json:"average_lead_time_hours"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user