Files
beads/cmd/bd/defer.go
Peter Chanthamynavong d371baf2ca feat(dates): add --due and --defer timestamp options with natural language parsing (#847)
* feat(dates): add due date schema and --due flag

- Add due_at and defer_until columns to issues table via migration 035
- Implement --due flag on create command with ISO date parsing
- Extend RPC protocol and daemon to pass DueAt from CLI to storage
- Display DueAt and DeferUntil in show command output
- Update Issue type with new date fields

Users can now set due dates when creating issues, enabling deadline-based
task management.

* feat(dates): add compact duration parser (+6h, +1d, +2w)

- Create internal/timeparsing package with layered parser architecture
- Implement parseCompactDuration with regex pattern [+-]?\d+[hdwmy]
- Add comprehensive test suite (22 cases) for duration parsing
- Integrate into create.go with fallback to ISO format

Supports hours (h), days (d), weeks (w), months (m), and years (y).
Negative values allowed for past dates.

* feat(dates): add NLP parsing for natural language dates

Integrate olebedev/when library for natural language time expressions.
The layered parser now handles: compact duration → absolute formats → NLP.

Changes:
- Add olebedev/when dependency for NLP parsing
- Implement ParseNaturalLanguage and ParseRelativeTime functions
- Reorder layers: absolute formats before NLP to avoid misinterpretation
- Simplify create.go to use unified ParseRelativeTime
- Add comprehensive NLP test coverage (22 test cases)

Supports: tomorrow, next monday, in 3 days, 3 days ago

* feat(dates): add --defer flag to create/update/defer commands

Add time-based deferral support alongside existing status-based defer.
Issues can now be hidden from bd ready until a specific time.

Changes:
- Add --defer flag to bd create (sets defer_until on creation)
- Add --due and --defer flags to bd update (modify existing issues)
- Add --until flag to bd defer (combines status=deferred with defer_until)
- Add DueAt/DeferUntil fields to UpdateArgs in protocol.go

Supports: +1h, tomorrow, next monday, 2025-01-15

* feat(dates): add defer_until filtering to ready command

Add time-based deferral support to bd ready:

- Add --include-deferred flag to show issues with future defer_until
- Filter out issues where defer_until > now by default
- Update undefer to clear defer_until alongside status change
- Add IncludeDeferred to WorkFilter and RPC ReadyArgs

Part of GH#820: Relative Date Parsing (Phase 5)

* feat(dates): add polish and tests for relative date parsing

Add user-facing warnings when defer date is in the past to help catch
common mistakes. Expand help text with format examples and document
the olebedev/when September parsing quirk.

Tests:
- TestCreateSuite/WithDueAt, WithDeferUntil, WithBothDueAndDefer
- TestReadyWorkDeferUntil (ExcludesFutureDeferredByDefault, IncludeDeferredShowsAll)

Docs:
- CLAUDE.md quick reference updated with new flags
- Help text examples for --due, --defer on create/update

Closes: Phase 6 of beads-820-relative-dates spec

* feat(list): add time-based query filters for defer/due dates

Add --deferred, --defer-before, --defer-after, --due-before, --due-after,
and --overdue flags to bd list command. All date filters now support
relative time expressions (+6h, tomorrow, next monday) via the
timeparsing package.

Filters:
- --deferred: issues with defer_until set
- --defer-before/after: filter by defer_until date range
- --due-before/after: filter by due_at date range
- --overdue: due_at in past AND status != closed

Existing date filters (--created-after, etc.) now also support relative
time expressions through updated parseTimeFlag().

* build(nix): update vendorHash for olebedev/when dependency

The olebedev/when library was added for natural language date parsing
(GH#820). This changes go.sum, requiring an updated vendorHash in the
Nix flake configuration.
2026-01-01 20:06:13 -08:00

167 lines
4.4 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/timeparsing"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils"
)
var deferCmd = &cobra.Command{
Use: "defer [id...]",
Short: "Defer one or more issues for later",
Long: `Defer issues to put them on ice for later.
Deferred issues are deliberately set aside - not blocked by anything specific,
just postponed for future consideration. Unlike blocked issues, there's no
dependency keeping them from being worked. Unlike closed issues, they will
be revisited.
Deferred issues don't show in 'bd ready' but remain visible in 'bd list'.
Examples:
bd defer bd-abc # Defer a single issue (status-based)
bd defer bd-abc --until=tomorrow # Defer until specific time
bd defer bd-abc bd-def # Defer multiple issues`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("defer")
// Parse --until flag (GH#820)
var deferUntil *time.Time
untilStr, _ := cmd.Flags().GetString("until")
if untilStr != "" {
t, err := timeparsing.ParseRelativeTime(untilStr, time.Now())
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid --until format %q. Examples: +1h, tomorrow, next monday, 2025-01-15\n", untilStr)
os.Exit(1)
}
deferUntil = &t
}
ctx := rootCtx
// Resolve partial IDs first
var resolvedIDs []string
if daemonClient != nil {
for _, id := range args {
resolveArgs := &rpc.ResolveIDArgs{ID: id}
resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
os.Exit(1)
}
var resolvedID string
if err := json.Unmarshal(resp.Data, &resolvedID); err != nil {
fmt.Fprintf(os.Stderr, "Error unmarshaling resolved ID: %v\n", err)
os.Exit(1)
}
resolvedIDs = append(resolvedIDs, resolvedID)
}
} else {
var err error
resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
deferredIssues := []*types.Issue{}
// If daemon is running, use RPC
if daemonClient != nil {
for _, id := range resolvedIDs {
status := string(types.StatusDeferred)
updateArgs := &rpc.UpdateArgs{
ID: id,
Status: &status,
}
// Add defer_until if --until specified (GH#820)
if deferUntil != nil {
s := deferUntil.Format(time.RFC3339)
updateArgs.DeferUntil = &s
}
resp, err := daemonClient.Update(updateArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error deferring %s: %v\n", id, err)
continue
}
if jsonOutput {
var issue types.Issue
if err := json.Unmarshal(resp.Data, &issue); err == nil {
deferredIssues = append(deferredIssues, &issue)
}
} else {
fmt.Printf("%s Deferred %s\n", ui.RenderAccent("*"), id)
}
}
if jsonOutput && len(deferredIssues) > 0 {
outputJSON(deferredIssues)
}
return
}
// Fall back to direct storage access
if store == nil {
fmt.Fprintln(os.Stderr, "Error: database not initialized")
os.Exit(1)
}
for _, id := range args {
fullID, err := utils.ResolvePartialID(ctx, store, id)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
continue
}
updates := map[string]interface{}{
"status": string(types.StatusDeferred),
}
// Add defer_until if --until specified (GH#820)
if deferUntil != nil {
updates["defer_until"] = *deferUntil
}
if err := store.UpdateIssue(ctx, fullID, updates, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error deferring %s: %v\n", fullID, err)
continue
}
if jsonOutput {
issue, _ := store.GetIssue(ctx, fullID)
if issue != nil {
deferredIssues = append(deferredIssues, issue)
}
} else {
fmt.Printf("%s Deferred %s\n", ui.RenderAccent("*"), fullID)
}
}
// Schedule auto-flush if any issues were deferred
if len(args) > 0 {
markDirtyAndScheduleFlush()
}
if jsonOutput && len(deferredIssues) > 0 {
outputJSON(deferredIssues)
}
},
}
func init() {
// Time-based scheduling flag (GH#820)
deferCmd.Flags().String("until", "", "Defer until specific time (e.g., +1h, tomorrow, next monday)")
rootCmd.AddCommand(deferCmd)
}