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

@@ -108,6 +108,9 @@ type CreateArgs struct {
EventActor string `json:"event_actor,omitempty"` // Entity URI who caused this event
EventTarget string `json:"event_target,omitempty"` // Entity URI or bead ID affected
EventPayload string `json:"event_payload,omitempty"` // Event-specific JSON data
// Time-based scheduling fields (GH#820)
DueAt string `json:"due_at,omitempty"` // Relative or ISO format due date
DeferUntil string `json:"defer_until,omitempty"` // Relative or ISO format defer date
}
// UpdateArgs represents arguments for the update operation
@@ -155,6 +158,9 @@ type UpdateArgs struct {
EventPayload *string `json:"event_payload,omitempty"` // Event-specific JSON data
// Work queue claim operation
Claim bool `json:"claim,omitempty"` // If true, atomically claim issue (set assignee+status, fail if already claimed)
// Time-based scheduling fields (GH#820)
DueAt *string `json:"due_at,omitempty"` // Relative or ISO format due date
DeferUntil *string `json:"defer_until,omitempty"` // Relative or ISO format defer date
}
// CloseArgs represents arguments for the close operation
@@ -236,6 +242,14 @@ type ListArgs struct {
// Type exclusion (for hiding internal types like gates, bd-7zka.2)
ExcludeTypes []string `json:"exclude_types,omitempty"`
// Time-based scheduling filters (GH#820)
Deferred bool `json:"deferred,omitempty"` // Filter issues with defer_until set
DeferAfter string `json:"defer_after,omitempty"` // ISO 8601 format
DeferBefore string `json:"defer_before,omitempty"` // ISO 8601 format
DueAfter string `json:"due_after,omitempty"` // ISO 8601 format
DueBefore string `json:"due_before,omitempty"` // ISO 8601 format
Overdue bool `json:"overdue,omitempty"` // Filter issues where due_at < now
}
// CountArgs represents arguments for the count operation
@@ -296,8 +310,9 @@ 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
MolType string `json:"mol_type,omitempty"` // Filter by molecule type: swarm, patrol, or work
ParentID string `json:"parent_id,omitempty"` // Filter to descendants of this bead/epic
MolType string `json:"mol_type,omitempty"` // Filter by molecule type: swarm, patrol, or work
IncludeDeferred bool `json:"include_deferred,omitempty"` // Include issues with future defer_until (GH#820)
}
// BlockedArgs represents arguments for the blocked operation

View File

@@ -200,6 +200,23 @@ func (s *Server) handleCreate(req *Request) Response {
externalRef = &createArgs.ExternalRef
}
// Parse DueAt if provided (GH#820)
var dueAt *time.Time
if createArgs.DueAt != "" {
// Try date-only format first (YYYY-MM-DD)
if t, err := time.ParseInLocation("2006-01-02", createArgs.DueAt, time.Local); err == nil {
dueAt = &t
} else if t, err := time.Parse(time.RFC3339, createArgs.DueAt); err == nil {
// Try RFC3339 format (2025-01-15T10:00:00Z)
dueAt = &t
} else {
return Response{
Success: false,
Error: fmt.Sprintf("invalid due_at format %q. Examples: 2025-01-15, 2025-01-15T10:00:00Z", createArgs.DueAt),
}
}
}
issue := &types.Issue{
ID: issueID,
Title: createArgs.Title,
@@ -230,6 +247,8 @@ func (s *Server) handleCreate(req *Request) Response {
Actor: createArgs.EventActor,
Target: createArgs.EventTarget,
Payload: createArgs.EventPayload,
// Time-based scheduling (GH#820)
DueAt: dueAt,
}
// Check if any dependencies are discovered-from type
@@ -1124,6 +1143,50 @@ func (s *Server) handleList(req *Request) Response {
}
}
// Time-based scheduling filters (GH#820)
filter.Deferred = listArgs.Deferred
if listArgs.DeferAfter != "" {
t, err := parseTimeRPC(listArgs.DeferAfter)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid --defer-after date: %v", err),
}
}
filter.DeferAfter = &t
}
if listArgs.DeferBefore != "" {
t, err := parseTimeRPC(listArgs.DeferBefore)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid --defer-before date: %v", err),
}
}
filter.DeferBefore = &t
}
if listArgs.DueAfter != "" {
t, err := parseTimeRPC(listArgs.DueAfter)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid --due-after date: %v", err),
}
}
filter.DueAfter = &t
}
if listArgs.DueBefore != "" {
t, err := parseTimeRPC(listArgs.DueBefore)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("invalid --due-before date: %v", err),
}
}
filter.DueBefore = &t
}
filter.Overdue = listArgs.Overdue
// Guard against excessive ID lists to avoid SQLite parameter limits
const maxIDs = 1000
if len(filter.IDs) > maxIDs {
@@ -1536,14 +1599,15 @@ 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,
SortPolicy: types.SortPolicy(readyArgs.SortPolicy),
Labels: util.NormalizeLabels(readyArgs.Labels),
LabelsAny: util.NormalizeLabels(readyArgs.LabelsAny),
Status: types.StatusOpen,
Type: readyArgs.Type,
Priority: readyArgs.Priority,
Unassigned: readyArgs.Unassigned,
Limit: readyArgs.Limit,
SortPolicy: types.SortPolicy(readyArgs.SortPolicy),
Labels: util.NormalizeLabels(readyArgs.Labels),
LabelsAny: util.NormalizeLabels(readyArgs.LabelsAny),
IncludeDeferred: readyArgs.IncludeDeferred, // GH#820
}
if readyArgs.Assignee != "" && !readyArgs.Unassigned {
wf.Assignee = &readyArgs.Assignee