• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / go-mehrhof / 21521768385

30 Jan 2026 03:49PM UTC coverage: 40.116% (+0.003%) from 40.113%
21521768385

push

github

k0d3r1s
Update documentation for new features
Documentation updates across CLI and Web UI:
- README: Feature highlights and updates
- docs/cli: Command documentation for new features
- docs/web-ui: Web UI feature documentation
- docs/configuration: Config guide updates
- docs/quickstart: Getting started updates

CLI documentation:
- auto: Auto mode documentation
- commit: Commit command docs
- serve: Server options and settings
- submit: Submit command enhancements
- simplify: Simplify command updates
- review: Review command docs
- optimize: Optimize command updates

Web UI documentation:
- api: API endpoint documentation
- authentication: Auth and role-based access
- auto: Auto mode in Web UI
- commit: Commit page documentation
- quick-tasks: Quick tasks feature
- settings: Settings page documentation
- standalone-simplify: Standalone simplify page

Other:
- e2e tests: E2E test updates
- wrike provider: Minor cleanup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

28802 of 71796 relevant lines covered (40.12%)

34.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.47
/internal/storage/taskqueue.go
1
package storage
2

3
import (
4
        "errors"
5
        "fmt"
6
        "os"
7
        "path/filepath"
8
        "sort"
9
        "sync"
10
        "time"
11

12
        "gopkg.in/yaml.v3"
13
)
14

15
const (
16
        // TaskQueueVersion is the current version of the queue format.
17
        TaskQueueVersion = "1"
18
        // QueuesDir is the subdirectory for queue storage.
19
        QueuesDir = "queues"
20
)
21

22
// TaskQueue represents a collection of tasks for a project planning workflow.
23
// Tasks are stored locally for review/editing before submission to external providers.
24
type TaskQueue struct {
25
        Version   string        `yaml:"version"`
26
        ID        string        `yaml:"id"`
27
        Title     string        `yaml:"title,omitempty"`
28
        Source    string        `yaml:"source,omitempty"`    // Original source reference
29
        Status    QueueStatus   `yaml:"status"`              // draft, ready, submitted
30
        Tasks     []*QueuedTask `yaml:"tasks"`               // Ordered list of tasks
31
        Questions []string      `yaml:"questions,omitempty"` // Unresolved questions from AI
32
        Blockers  []string      `yaml:"blockers,omitempty"`  // Identified blockers
33
        CreatedAt time.Time     `yaml:"created_at"`
34
        UpdatedAt time.Time     `yaml:"updated_at"`
35

36
        mu   sync.RWMutex `yaml:"-"`
37
        path string       `yaml:"-"` // path to queue file
38
}
39

40
// QueueStatus represents the state of a task queue.
41
type QueueStatus string
42

43
const (
44
        QueueStatusDraft     QueueStatus = "draft"     // Being reviewed/edited
45
        QueueStatusReady     QueueStatus = "ready"     // Ready for submission
46
        QueueStatusSubmitted QueueStatus = "submitted" // Submitted to provider
47
)
48

49
// QueuedTask represents a single task within a queue.
50
type QueuedTask struct {
51
        ID          string     `yaml:"id"`                     // Local ID (task-1, task-2, etc.)
52
        Title       string     `yaml:"title"`                  // Task title
53
        Description string     `yaml:"description,omitempty"`  // Detailed description
54
        Status      TaskStatus `yaml:"status"`                 // pending, ready, blocked, submitted
55
        Priority    int        `yaml:"priority"`               // 1 = highest priority
56
        ParentID    string     `yaml:"parent_id,omitempty"`    // Local parent task ID (makes this a subtask)
57
        Subtasks    []string   `yaml:"subtasks,omitempty"`     // Child task IDs (computed from ParentID)
58
        DependsOn   []string   `yaml:"depends_on,omitempty"`   // Task IDs this depends on (execution order)
59
        Blocks      []string   `yaml:"blocks,omitempty"`       // Task IDs this blocks (computed)
60
        Labels      []string   `yaml:"labels,omitempty"`       // Labels/tags
61
        Assignee    string     `yaml:"assignee,omitempty"`     // Assignee identifier
62
        ExternalID  string     `yaml:"external_id,omitempty"`  // Provider ID after submission
63
        ExternalURL string     `yaml:"external_url,omitempty"` // Provider URL after submission
64
}
65

66
// TaskStatus represents the state of a single task.
67
type TaskStatus string
68

69
const (
70
        TaskStatusPending   TaskStatus = "pending"   // Not yet ready
71
        TaskStatusReady     TaskStatus = "ready"     // Ready to start (no blockers)
72
        TaskStatusBlocked   TaskStatus = "blocked"   // Waiting on dependencies
73
        TaskStatusSubmitted TaskStatus = "submitted" // Submitted to provider
74
)
75

76
// NewTaskQueue creates a new empty task queue.
77
func NewTaskQueue(id, title, source string) *TaskQueue {
18✔
78
        now := time.Now()
18✔
79

18✔
80
        return &TaskQueue{
18✔
81
                Version:   TaskQueueVersion,
18✔
82
                ID:        id,
18✔
83
                Title:     title,
18✔
84
                Source:    source,
18✔
85
                Status:    QueueStatusDraft,
18✔
86
                Tasks:     make([]*QueuedTask, 0),
18✔
87
                Questions: make([]string, 0),
18✔
88
                Blockers:  make([]string, 0),
18✔
89
                CreatedAt: now,
18✔
90
                UpdatedAt: now,
18✔
91
        }
18✔
92
}
18✔
93

94
// LoadTaskQueue loads a task queue from disk.
95
func LoadTaskQueue(ws *Workspace, queueID string) (*TaskQueue, error) {
3✔
96
        path := ws.QueuePath(queueID)
3✔
97
        if path == "" {
3✔
98
                return nil, errors.New("could not determine queue path")
×
99
        }
×
100

101
        data, err := os.ReadFile(path)
3✔
102
        if err != nil {
4✔
103
                if os.IsNotExist(err) {
2✔
104
                        return nil, fmt.Errorf("queue not found: %s", queueID)
1✔
105
                }
1✔
106

107
                return nil, fmt.Errorf("read queue file: %w", err)
×
108
        }
109

110
        var queue TaskQueue
2✔
111
        if err := yaml.Unmarshal(data, &queue); err != nil {
2✔
112
                return nil, fmt.Errorf("parse queue: %w", err)
×
113
        }
×
114

115
        queue.path = path
2✔
116
        if queue.Tasks == nil {
2✔
117
                queue.Tasks = make([]*QueuedTask, 0)
×
118
        }
×
119
        if queue.Questions == nil {
3✔
120
                queue.Questions = make([]string, 0)
1✔
121
        }
1✔
122
        if queue.Blockers == nil {
3✔
123
                queue.Blockers = make([]string, 0)
1✔
124
        }
1✔
125

126
        return &queue, nil
2✔
127
}
128

129
// Save writes the queue to disk using atomic write pattern.
130
func (q *TaskQueue) Save() error {
6✔
131
        q.mu.Lock()
6✔
132
        defer q.mu.Unlock()
6✔
133

6✔
134
        if q.path == "" {
6✔
135
                return errors.New("queue path not set")
×
136
        }
×
137

138
        q.UpdatedAt = time.Now()
6✔
139

6✔
140
        // Ensure directory exists
6✔
141
        dir := filepath.Dir(q.path)
6✔
142
        if err := os.MkdirAll(dir, 0o755); err != nil {
6✔
143
                return fmt.Errorf("create queue directory: %w", err)
×
144
        }
×
145

146
        data, err := yaml.Marshal(q)
6✔
147
        if err != nil {
6✔
148
                return fmt.Errorf("marshal queue: %w", err)
×
149
        }
×
150

151
        // Atomic write: temp file then rename
152
        tmpPath := q.path + ".tmp"
6✔
153
        if err := os.WriteFile(tmpPath, data, 0o644); err != nil {
6✔
154
                return fmt.Errorf("write queue: %w", err)
×
155
        }
×
156

157
        if err := os.Rename(tmpPath, q.path); err != nil {
6✔
158
                _ = os.Remove(tmpPath)
×
159

×
160
                return fmt.Errorf("save queue: %w", err)
×
161
        }
×
162

163
        return nil
6✔
164
}
165

166
// AddTask adds a task to the queue.
167
func (q *TaskQueue) AddTask(task *QueuedTask) {
539✔
168
        q.mu.Lock()
539✔
169
        defer q.mu.Unlock()
539✔
170

539✔
171
        q.Tasks = append(q.Tasks, task)
539✔
172
        q.UpdatedAt = time.Now()
539✔
173
}
539✔
174

175
// GetTask retrieves a task by ID.
176
func (q *TaskQueue) GetTask(taskID string) *QueuedTask {
514✔
177
        q.mu.RLock()
514✔
178
        defer q.mu.RUnlock()
514✔
179

514✔
180
        for _, task := range q.Tasks {
1,041✔
181
                if task.ID == taskID {
1,040✔
182
                        return task
513✔
183
                }
513✔
184
        }
185

186
        return nil
1✔
187
}
188

189
// UpdateTask updates a task in the queue.
190
func (q *TaskQueue) UpdateTask(taskID string, updater func(*QueuedTask)) error {
503✔
191
        q.mu.Lock()
503✔
192
        defer q.mu.Unlock()
503✔
193

503✔
194
        for _, task := range q.Tasks {
1,006✔
195
                if task.ID == taskID {
1,005✔
196
                        updater(task)
502✔
197
                        q.UpdatedAt = time.Now()
502✔
198

502✔
199
                        return nil
502✔
200
                }
502✔
201
        }
202

203
        return fmt.Errorf("task not found: %s", taskID)
1✔
204
}
205

206
// RemoveTask removes a task from the queue.
207
func (q *TaskQueue) RemoveTask(taskID string) bool {
2✔
208
        q.mu.Lock()
2✔
209
        defer q.mu.Unlock()
2✔
210

2✔
211
        for i, task := range q.Tasks {
4✔
212
                if task.ID == taskID {
3✔
213
                        q.Tasks = append(q.Tasks[:i], q.Tasks[i+1:]...)
1✔
214
                        q.UpdatedAt = time.Now()
1✔
215

1✔
216
                        return true
1✔
217
                }
1✔
218
        }
219

220
        return false
1✔
221
}
222

223
// ReorderTask moves a task to a new position.
224
func (q *TaskQueue) ReorderTask(taskID string, newIndex int) error {
4✔
225
        q.mu.Lock()
4✔
226
        defer q.mu.Unlock()
4✔
227

4✔
228
        taskIndex := -1
4✔
229
        for i, task := range q.Tasks {
14✔
230
                if task.ID == taskID {
13✔
231
                        taskIndex = i
3✔
232

3✔
233
                        break
3✔
234
                }
235
        }
236

237
        if taskIndex == -1 {
5✔
238
                return fmt.Errorf("task not found: %s", taskID)
1✔
239
        }
1✔
240

241
        if newIndex < 0 || newIndex >= len(q.Tasks) {
5✔
242
                return fmt.Errorf("invalid index: %d", newIndex)
2✔
243
        }
2✔
244

245
        // Remove task from current position
246
        task := q.Tasks[taskIndex]
1✔
247
        q.Tasks = append(q.Tasks[:taskIndex], q.Tasks[taskIndex+1:]...)
1✔
248

1✔
249
        // Insert at new position
1✔
250
        q.Tasks = append(q.Tasks[:newIndex], append([]*QueuedTask{task}, q.Tasks[newIndex:]...)...)
1✔
251
        q.UpdatedAt = time.Now()
1✔
252

1✔
253
        return nil
1✔
254
}
255

256
// ComputeBlocksRelations updates the Blocks field for all tasks based on DependsOn.
257
func (q *TaskQueue) ComputeBlocksRelations() {
1✔
258
        q.mu.Lock()
1✔
259
        defer q.mu.Unlock()
1✔
260

1✔
261
        // Clear existing Blocks
1✔
262
        for _, task := range q.Tasks {
4✔
263
                task.Blocks = nil
3✔
264
        }
3✔
265

266
        // Build reverse mapping
267
        for _, task := range q.Tasks {
4✔
268
                for _, depID := range task.DependsOn {
6✔
269
                        for _, depTask := range q.Tasks {
7✔
270
                                if depTask.ID == depID {
7✔
271
                                        depTask.Blocks = append(depTask.Blocks, task.ID)
3✔
272

3✔
273
                                        break
3✔
274
                                }
275
                        }
276
                }
277
        }
278
}
279

280
// ComputeSubtaskRelations updates the Subtasks field for all tasks based on ParentID.
281
// This mirrors ComputeBlocksRelations but for parent-child hierarchy.
282
func (q *TaskQueue) ComputeSubtaskRelations() {
1✔
283
        q.mu.Lock()
1✔
284
        defer q.mu.Unlock()
1✔
285

1✔
286
        // Clear existing Subtasks
1✔
287
        for _, task := range q.Tasks {
5✔
288
                task.Subtasks = nil
4✔
289
        }
4✔
290

291
        // Build reverse mapping from ParentID to Subtasks
292
        for _, task := range q.Tasks {
5✔
293
                if task.ParentID == "" {
5✔
294
                        continue
1✔
295
                }
296
                for _, parentTask := range q.Tasks {
7✔
297
                        if parentTask.ID == task.ParentID {
7✔
298
                                parentTask.Subtasks = append(parentTask.Subtasks, task.ID)
3✔
299

3✔
300
                                break
3✔
301
                        }
302
                }
303
        }
304
}
305

306
// ComputeTaskStatuses updates task statuses based on dependencies.
307
// Tasks with unsubmitted dependencies are marked as blocked.
308
func (q *TaskQueue) ComputeTaskStatuses() {
2✔
309
        q.mu.Lock()
2✔
310
        defer q.mu.Unlock()
2✔
311

2✔
312
        // Build a map of submitted tasks
2✔
313
        submitted := make(map[string]bool)
2✔
314
        for _, task := range q.Tasks {
8✔
315
                if task.Status == TaskStatusSubmitted {
7✔
316
                        submitted[task.ID] = true
1✔
317
                }
1✔
318
        }
319

320
        // Update statuses
321
        for _, task := range q.Tasks {
8✔
322
                if task.Status == TaskStatusSubmitted {
7✔
323
                        continue // Don't change submitted tasks
1✔
324
                }
325

326
                blocked := false
5✔
327
                for _, depID := range task.DependsOn {
9✔
328
                        if !submitted[depID] {
7✔
329
                                blocked = true
3✔
330

3✔
331
                                break
3✔
332
                        }
333
                }
334

335
                if blocked {
8✔
336
                        task.Status = TaskStatusBlocked
3✔
337
                } else if task.Status == TaskStatusBlocked {
6✔
338
                        task.Status = TaskStatusReady
1✔
339
                }
1✔
340
        }
341
}
342

343
// GetReadyTasks returns tasks that are ready to start (not blocked, not submitted).
344
func (q *TaskQueue) GetReadyTasks() []*QueuedTask {
501✔
345
        q.mu.RLock()
501✔
346
        defer q.mu.RUnlock()
501✔
347

501✔
348
        var ready []*QueuedTask
501✔
349
        for _, task := range q.Tasks {
9,812✔
350
                if task.Status == TaskStatusReady {
9,313✔
351
                        ready = append(ready, task)
2✔
352
                }
2✔
353
        }
354

355
        return ready
501✔
356
}
357

358
// GetBlockedTasks returns tasks that are blocked by dependencies.
359
func (q *TaskQueue) GetBlockedTasks() []*QueuedTask {
×
360
        q.mu.RLock()
×
361
        defer q.mu.RUnlock()
×
362

×
363
        var blocked []*QueuedTask
×
364
        for _, task := range q.Tasks {
×
365
                if task.Status == TaskStatusBlocked {
×
366
                        blocked = append(blocked, task)
×
367
                }
×
368
        }
369

370
        return blocked
×
371
}
372

373
// NextTaskID generates the next task ID (task-1, task-2, etc.).
374
func (q *TaskQueue) NextTaskID() string {
512✔
375
        q.mu.RLock()
512✔
376
        defer q.mu.RUnlock()
512✔
377

512✔
378
        maxNum := 0
512✔
379
        for _, task := range q.Tasks {
128,366✔
380
                var num int
127,854✔
381
                if _, err := fmt.Sscanf(task.ID, "task-%d", &num); err == nil {
255,708✔
382
                        if num > maxNum {
161,566✔
383
                                maxNum = num
33,712✔
384
                        }
33,712✔
385
                }
386
        }
387

388
        return fmt.Sprintf("task-%d", maxNum+1)
512✔
389
}
390

391
// TaskCount returns the number of tasks in the queue.
392
func (q *TaskQueue) TaskCount() int {
500✔
393
        q.mu.RLock()
500✔
394
        defer q.mu.RUnlock()
500✔
395

500✔
396
        return len(q.Tasks)
500✔
397
}
500✔
398

399
// QueuePath returns the path to a queue file within the workspace.
400
func (ws *Workspace) QueuePath(queueID string) string {
12✔
401
        return filepath.Join(ws.workspaceRoot, QueuesDir, queueID, "queue.yaml")
12✔
402
}
12✔
403

404
// SaveTaskQueue saves a task queue to the workspace.
405
func (ws *Workspace) SaveTaskQueue(queue *TaskQueue) error {
×
406
        if queue.path == "" {
×
407
                queue.path = ws.QueuePath(queue.ID)
×
408
        }
×
409

410
        return queue.Save()
×
411
}
412

413
// ListQueues returns all queue IDs in the workspace.
414
func (ws *Workspace) ListQueues() ([]string, error) {
2✔
415
        queuesDir := filepath.Join(ws.workspaceRoot, QueuesDir)
2✔
416

2✔
417
        entries, err := os.ReadDir(queuesDir)
2✔
418
        if os.IsNotExist(err) {
3✔
419
                return []string{}, nil
1✔
420
        }
1✔
421
        if err != nil {
1✔
422
                return nil, fmt.Errorf("read queues directory: %w", err)
×
423
        }
×
424

425
        var queueIDs []string
1✔
426
        for _, entry := range entries {
4✔
427
                if entry.IsDir() {
6✔
428
                        queuePath := filepath.Join(queuesDir, entry.Name(), "queue.yaml")
3✔
429
                        if _, err := os.Stat(queuePath); err == nil {
6✔
430
                                queueIDs = append(queueIDs, entry.Name())
3✔
431
                        }
3✔
432
                }
433
        }
434

435
        // Sort by name
436
        sort.Strings(queueIDs)
1✔
437

1✔
438
        return queueIDs, nil
1✔
439
}
440

441
// DeleteQueue removes a queue and its directory.
442
func (ws *Workspace) DeleteQueue(queueID string) error {
1✔
443
        queueDir := filepath.Join(ws.workspaceRoot, QueuesDir, queueID)
1✔
444

1✔
445
        return os.RemoveAll(queueDir)
1✔
446
}
1✔
447

448
// QueueExists checks if a queue exists.
449
func (ws *Workspace) QueueExists(queueID string) bool {
2✔
450
        path := ws.QueuePath(queueID)
2✔
451
        _, err := os.Stat(path)
2✔
452

2✔
453
        return err == nil
2✔
454
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc