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:
committed by
GitHub
parent
e4042e3e1a
commit
d371baf2ca
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user