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.
This commit is contained in:
Peter Chanthamynavong
2026-01-01 20:06:13 -08:00
committed by GitHub
parent e4042e3e1a
commit d371baf2ca
26 changed files with 1593 additions and 56 deletions

View File

@@ -20,28 +20,18 @@ import (
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/timeparsing"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/util"
"github.com/steveyegge/beads/internal/validation"
)
// parseTimeFlag parses time strings in multiple formats
// parseTimeFlag parses time strings using the layered time parsing architecture.
// Supports compact durations (+6h, -1d), natural language (tomorrow, next monday),
// and absolute formats (2006-01-02, RFC3339).
func parseTimeFlag(s string) (time.Time, error) {
formats := []string{
time.RFC3339,
"2006-01-02",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05",
}
for _, format := range formats {
if t, err := time.Parse(format, s); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unable to parse time %q (try formats: 2006-01-02, 2006-01-02T15:04:05, or RFC3339)", s)
return timeparsing.ParseRelativeTime(s, time.Now())
}
// pinIndicator returns a pushpin emoji prefix for pinned issues
@@ -434,6 +424,14 @@ var listCmd = &cobra.Command{
molType = &mt
}
// Time-based scheduling filters (GH#820)
deferredFlag, _ := cmd.Flags().GetBool("deferred")
deferAfter, _ := cmd.Flags().GetString("defer-after")
deferBefore, _ := cmd.Flags().GetString("defer-before")
dueAfter, _ := cmd.Flags().GetString("due-after")
dueBefore, _ := cmd.Flags().GetString("due-before")
overdueFlag, _ := cmd.Flags().GetBool("overdue")
// Pretty and watch flags (GH#654)
prettyFormat, _ := cmd.Flags().GetBool("pretty")
watchMode, _ := cmd.Flags().GetBool("watch")
@@ -640,6 +638,46 @@ var listCmd = &cobra.Command{
filter.MolType = molType
}
// Time-based scheduling filters (GH#820)
if deferredFlag {
filter.Deferred = true
}
if deferAfter != "" {
t, err := parseTimeFlag(deferAfter)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing --defer-after: %v\n", err)
os.Exit(1)
}
filter.DeferAfter = &t
}
if deferBefore != "" {
t, err := parseTimeFlag(deferBefore)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing --defer-before: %v\n", err)
os.Exit(1)
}
filter.DeferBefore = &t
}
if dueAfter != "" {
t, err := parseTimeFlag(dueAfter)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing --due-after: %v\n", err)
os.Exit(1)
}
filter.DueAfter = &t
}
if dueBefore != "" {
t, err := parseTimeFlag(dueBefore)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing --due-before: %v\n", err)
os.Exit(1)
}
filter.DueBefore = &t
}
if overdueFlag {
filter.Overdue = true
}
// Check database freshness before reading
// Skip check when using daemon (daemon auto-imports on staleness)
ctx := rootCtx
@@ -738,6 +776,22 @@ var listCmd = &cobra.Command{
}
}
// Time-based scheduling filters (GH#820)
listArgs.Deferred = filter.Deferred
if filter.DeferAfter != nil {
listArgs.DeferAfter = filter.DeferAfter.Format(time.RFC3339)
}
if filter.DeferBefore != nil {
listArgs.DeferBefore = filter.DeferBefore.Format(time.RFC3339)
}
if filter.DueAfter != nil {
listArgs.DueAfter = filter.DueAfter.Format(time.RFC3339)
}
if filter.DueBefore != nil {
listArgs.DueBefore = filter.DueBefore.Format(time.RFC3339)
}
listArgs.Overdue = filter.Overdue
resp, err := daemonClient.List(listArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -987,6 +1041,14 @@ func init() {
// Molecule type filtering
listCmd.Flags().String("mol-type", "", "Filter by molecule type: swarm, patrol, or work")
// Time-based scheduling filters (GH#820)
listCmd.Flags().Bool("deferred", false, "Show only issues with defer_until set")
listCmd.Flags().String("defer-after", "", "Filter issues deferred after date (supports relative: +6h, tomorrow)")
listCmd.Flags().String("defer-before", "", "Filter issues deferred before date (supports relative: +6h, tomorrow)")
listCmd.Flags().String("due-after", "", "Filter issues due after date (supports relative: +6h, tomorrow)")
listCmd.Flags().String("due-before", "", "Filter issues due before date (supports relative: +6h, tomorrow)")
listCmd.Flags().Bool("overdue", false, "Show only issues with due_at in the past (not closed)")
// Pretty and watch flags (GH#654)
listCmd.Flags().Bool("pretty", false, "Display issues in a tree format with status/priority symbols")
listCmd.Flags().BoolP("watch", "w", false, "Watch for changes and auto-update display (implies --pretty)")