Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ CREATE TABLE tasks (
pr_url TEXT DEFAULT '', -- Pull request URL
pr_number INTEGER DEFAULT 0, -- Pull request number
scheduled_at DATETIME, -- When to next run
recurrence TEXT DEFAULT '', -- Recurrence pattern
last_run_at DATETIME, -- Last execution time
recurrence TEXT DEFAULT '', -- Deprecated recurrence pattern (kept for legacy display)
last_run_at DATETIME, -- Last scheduled execution time
created_at DATETIME,
updated_at DATETIME,
started_at DATETIME,
Expand Down Expand Up @@ -230,7 +230,7 @@ backlog → queued → processing → done
↘ blocked (needs input)

blocked can return to processing via retry
done triggers memory extraction + recurring task reset
done triggers memory extraction
```

1. **backlog** - Created but not yet started
Expand All @@ -239,6 +239,8 @@ done triggers memory extraction + recurring task reset
4. **blocked** - Waiting for user input/clarification
5. **done** - Completed successfully

Recurring scheduling has been removed from TaskYou. Use external schedulers (cron, calendar apps, etc.) to invoke the CLI whenever a task should repeat.

## Key Bindings (TUI)

| Key | Action |
Expand Down Expand Up @@ -286,7 +288,7 @@ The background executor (`internal/executor/executor.go`):
- Supports real-time watching via subscriptions
- Handles task suspension and resumption
- Extracts memories from successful tasks
- Manages scheduled and recurring tasks
- Manages scheduled tasks (recurring runs must now be handled externally via CLI automation)

### Claude Code Integration

Expand Down
30 changes: 13 additions & 17 deletions cmd/task/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,6 @@ Examples:
if t.ScheduledAt != nil {
item["scheduled_at"] = t.ScheduledAt.Time.Format(time.RFC3339)
}
if t.Recurrence != "" {
item["recurrence"] = t.Recurrence
}
if t.LastRunAt != nil {
item["last_run_at"] = t.LastRunAt.Time.Format(time.RFC3339)
}
Expand Down Expand Up @@ -623,10 +620,11 @@ Examples:
}
// Schedule indicator
scheduleIndicator := ""
if t.IsRecurring() {
scheduleIndicator = lipgloss.NewStyle().Foreground(lipgloss.Color("#F59E0B")).Render("🔁 ")
} else if t.IsScheduled() {
scheduleIndicator = lipgloss.NewStyle().Foreground(lipgloss.Color("#F59E0B")).Render("⏰ ")
scheduleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#F59E0B"))
if t.IsScheduled() {
scheduleIndicator = scheduleStyle.Render("⏰ ")
} else if t.Recurrence != "" {
scheduleIndicator = scheduleStyle.Render("⚠ ")
}
prStatus := ""
if showPR {
Expand Down Expand Up @@ -721,9 +719,6 @@ Examples:
if task.ScheduledAt != nil {
output["scheduled_at"] = task.ScheduledAt.Time.Format(time.RFC3339)
}
if task.Recurrence != "" {
output["recurrence"] = task.Recurrence
}
if task.LastRunAt != nil {
output["last_run_at"] = task.LastRunAt.Time.Format(time.RFC3339)
}
Expand Down Expand Up @@ -822,18 +817,19 @@ Examples:

// Schedule info
scheduleColor := lipgloss.Color("#F59E0B") // Orange for schedule
if task.IsRecurring() || task.IsScheduled() {
scheduleStyle := lipgloss.NewStyle().Foreground(scheduleColor)
if task.IsScheduled() || task.LastRunAt != nil || task.Recurrence != "" {
fmt.Println()
fmt.Println(boldStyle.Render("Schedule:"))
if task.Recurrence != "" {
recurrenceStyled := lipgloss.NewStyle().Foreground(scheduleColor).Render(task.Recurrence)
fmt.Printf(" Recurrence: %s\n", recurrenceStyled)
}
if task.ScheduledAt != nil {
fmt.Printf(" Next run: %s\n", task.ScheduledAt.Time.Format("2006-01-02 15:04:05"))
fmt.Printf(" Next run: %s\n", scheduleStyle.Render(task.ScheduledAt.Time.Format("2006-01-02 15:04:05")))
}
if task.LastRunAt != nil {
fmt.Printf(" Last run: %s\n", task.LastRunAt.Time.Format("2006-01-02 15:04:05"))
fmt.Printf(" Last run: %s\n", scheduleStyle.Render(task.LastRunAt.Time.Format("2006-01-02 15:04:05")))
}
if task.Recurrence != "" {
note := fmt.Sprintf("Recurring schedules inside TaskYou were removed (legacy value: %s)", task.Recurrence)
fmt.Printf(" Note: %s\n", scheduleStyle.Render(note))
}
}

Expand Down
4 changes: 2 additions & 2 deletions internal/db/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ func (db *DB) migrate() error {
`ALTER TABLE tasks ADD COLUMN port INTEGER DEFAULT 0`,
// Scheduled task columns
`ALTER TABLE tasks ADD COLUMN scheduled_at DATETIME`, // When to next run (null = not scheduled)
`ALTER TABLE tasks ADD COLUMN recurrence TEXT DEFAULT ''`, // Recurrence pattern (empty = one-time)
`ALTER TABLE tasks ADD COLUMN last_run_at DATETIME`, // When last executed (for recurring tasks)
`ALTER TABLE tasks ADD COLUMN recurrence TEXT DEFAULT ''`, // Deprecated recurrence pattern (empty = one-time)
`ALTER TABLE tasks ADD COLUMN last_run_at DATETIME`, // When last executed (for scheduled tasks)
// Claude session tracking
`ALTER TABLE tasks ADD COLUMN claude_session_id TEXT DEFAULT ''`, // Claude session ID for resuming conversations
// Project color column
Expand Down
81 changes: 11 additions & 70 deletions internal/db/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ type Task struct {
UpdatedAt LocalTime
StartedAt *LocalTime
CompletedAt *LocalTime
// Schedule fields for recurring/scheduled tasks
// Schedule fields for tasks with delayed execution
ScheduledAt *LocalTime // When to next run (nil = not scheduled)
Recurrence string // Recurrence pattern: "", "hourly", "daily", "weekly", "monthly", or cron expression
LastRunAt *LocalTime // When last executed (for recurring tasks)
Recurrence string // Deprecated: no longer used (kept for backward compatibility)
LastRunAt *LocalTime // When last executed (for scheduled tasks)
}

// Task statuses
Expand Down Expand Up @@ -75,25 +75,11 @@ func DefaultExecutor() string {
return ExecutorClaude
}

// Recurrence patterns for scheduled tasks
const (
RecurrenceNone = "" // One-time scheduled task (or not scheduled)
RecurrenceHourly = "hourly" // Run every hour
RecurrenceDaily = "daily" // Run every day
RecurrenceWeekly = "weekly" // Run every week
RecurrenceMonthly = "monthly" // Run every month
)

// IsScheduled returns true if the task has a scheduled time set.
func (t *Task) IsScheduled() bool {
return t.ScheduledAt != nil && !t.ScheduledAt.Time.IsZero()
}

// IsRecurring returns true if the task has a recurrence pattern.
func (t *Task) IsRecurring() bool {
return t.Recurrence != ""
}

// Port allocation constants
const (
PortRangeStart = 3100 // First port in the allocation range
Expand Down Expand Up @@ -696,7 +682,6 @@ func (db *DB) GetTasksWithBranches() ([]*Task, error) {
// A task is due if:
// - It has a scheduled_at time that is <= now
// - It is not currently queued or processing (to avoid double-queueing)
// For recurring tasks, this allows blocked/done tasks to run again on schedule.
func (db *DB) GetDueScheduledTasks() ([]*Task, error) {
rows, err := db.Query(`
SELECT id, title, body, status, type, project, COALESCE(executor, 'claude'),
Expand Down Expand Up @@ -776,49 +761,8 @@ func (db *DB) GetScheduledTasks() ([]*Task, error) {
return tasks, nil
}

// CalculateNextRunTime calculates the next scheduled time based on recurrence pattern.
// Returns nil if recurrence is empty (one-time task).
func CalculateNextRunTime(recurrence string, fromTime time.Time) *LocalTime {
if recurrence == "" {
return nil
}

var nextTime time.Time
switch recurrence {
case RecurrenceHourly:
nextTime = fromTime.Add(1 * time.Hour)
case RecurrenceDaily:
nextTime = fromTime.AddDate(0, 0, 1)
case RecurrenceWeekly:
nextTime = fromTime.AddDate(0, 0, 7)
case RecurrenceMonthly:
nextTime = fromTime.AddDate(0, 1, 0)
default:
// Unknown recurrence pattern
return nil
}

return &LocalTime{Time: nextTime}
}

// UpdateTaskSchedule updates only the schedule-related fields of a task.
func (db *DB) UpdateTaskSchedule(taskID int64, scheduledAt *LocalTime, recurrence string, lastRunAt *LocalTime) error {
_, err := db.Exec(`
UPDATE tasks SET
scheduled_at = ?,
recurrence = ?,
last_run_at = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, scheduledAt, recurrence, lastRunAt, taskID)
if err != nil {
return fmt.Errorf("update task schedule: %w", err)
}
return nil
}

// QueueScheduledTask queues a scheduled task and updates its schedule for recurring tasks.
// For recurring tasks, it sets the next scheduled_at time; for one-time tasks, it clears scheduled_at.
// QueueScheduledTask queues a scheduled task.
// The scheduled time is always cleared so the run happens once.
func (db *DB) QueueScheduledTask(taskID int64) error {
// Get the task first
task, err := db.GetTask(taskID)
Expand All @@ -831,15 +775,12 @@ func (db *DB) QueueScheduledTask(taskID int64) error {

now := time.Now()

// Calculate next run time for recurring tasks
if task.IsRecurring() {
nextRun := CalculateNextRunTime(task.Recurrence, now)
task.ScheduledAt = nextRun
task.LastRunAt = &LocalTime{Time: now}
} else {
// One-time task - clear the schedule after queuing
task.ScheduledAt = nil
task.LastRunAt = &LocalTime{Time: now}
// Recurring tasks are no longer supported; treat everything as one-time
task.ScheduledAt = nil
task.LastRunAt = &LocalTime{Time: now}
if task.Recurrence != "" {
// Clear any legacy recurrence data so the UI no longer highlights it
task.Recurrence = ""
}

// Update status to queued
Expand Down
Loading