steerdev 0.4.27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. steerdev-0.4.27.dist-info/METADATA +224 -0
  2. steerdev-0.4.27.dist-info/RECORD +57 -0
  3. steerdev-0.4.27.dist-info/WHEEL +4 -0
  4. steerdev-0.4.27.dist-info/entry_points.txt +2 -0
  5. steerdev_agent/__init__.py +10 -0
  6. steerdev_agent/api/__init__.py +32 -0
  7. steerdev_agent/api/activity.py +278 -0
  8. steerdev_agent/api/agents.py +145 -0
  9. steerdev_agent/api/client.py +158 -0
  10. steerdev_agent/api/commands.py +399 -0
  11. steerdev_agent/api/configs.py +238 -0
  12. steerdev_agent/api/context.py +306 -0
  13. steerdev_agent/api/events.py +294 -0
  14. steerdev_agent/api/hooks.py +178 -0
  15. steerdev_agent/api/implementation_plan.py +408 -0
  16. steerdev_agent/api/messages.py +231 -0
  17. steerdev_agent/api/prd.py +281 -0
  18. steerdev_agent/api/runs.py +526 -0
  19. steerdev_agent/api/sessions.py +403 -0
  20. steerdev_agent/api/specs.py +321 -0
  21. steerdev_agent/api/tasks.py +659 -0
  22. steerdev_agent/api/workflow_runs.py +351 -0
  23. steerdev_agent/api/workflows.py +191 -0
  24. steerdev_agent/cli.py +2254 -0
  25. steerdev_agent/config/__init__.py +19 -0
  26. steerdev_agent/config/models.py +236 -0
  27. steerdev_agent/config/platform.py +272 -0
  28. steerdev_agent/config/settings.py +62 -0
  29. steerdev_agent/daemon.py +675 -0
  30. steerdev_agent/executor/__init__.py +64 -0
  31. steerdev_agent/executor/base.py +121 -0
  32. steerdev_agent/executor/claude.py +328 -0
  33. steerdev_agent/executor/stream.py +163 -0
  34. steerdev_agent/git/__init__.py +1 -0
  35. steerdev_agent/handlers/__init__.py +5 -0
  36. steerdev_agent/handlers/prd.py +533 -0
  37. steerdev_agent/integration.py +334 -0
  38. steerdev_agent/prompt/__init__.py +10 -0
  39. steerdev_agent/prompt/builder.py +263 -0
  40. steerdev_agent/prompt/templates.py +422 -0
  41. steerdev_agent/py.typed +0 -0
  42. steerdev_agent/runner.py +829 -0
  43. steerdev_agent/setup/__init__.py +5 -0
  44. steerdev_agent/setup/claude_setup.py +560 -0
  45. steerdev_agent/setup/templates/claude_md_section.md +140 -0
  46. steerdev_agent/setup/templates/settings.json +69 -0
  47. steerdev_agent/setup/templates/skills/activity/SKILL.md +160 -0
  48. steerdev_agent/setup/templates/skills/context/SKILL.md +122 -0
  49. steerdev_agent/setup/templates/skills/git-workflow/SKILL.md +218 -0
  50. steerdev_agent/setup/templates/skills/progress-logging/SKILL.md +211 -0
  51. steerdev_agent/setup/templates/skills/specs-management/SKILL.md +161 -0
  52. steerdev_agent/setup/templates/skills/task-management/SKILL.md +343 -0
  53. steerdev_agent/setup/templates/steerdev.yaml +51 -0
  54. steerdev_agent/version.py +149 -0
  55. steerdev_agent/workflow/__init__.py +10 -0
  56. steerdev_agent/workflow/executor.py +494 -0
  57. steerdev_agent/workflow/memory.py +185 -0
@@ -0,0 +1,659 @@
1
+ """Task management API client."""
2
+
3
+ from typing import Any
4
+
5
+ from loguru import logger
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from steerdev_agent.api.client import SteerDevClient, get_project_id
11
+ from steerdev_agent.api.implementation_plan import (
12
+ display_implementation_plan,
13
+ extract_task_description,
14
+ parse_implementation_plan,
15
+ )
16
+
17
+ console = Console()
18
+
19
+ # Linear-native status_type values
20
+ VALID_STATUSES = [
21
+ "backlog",
22
+ "unstarted",
23
+ "started",
24
+ "completed",
25
+ "canceled",
26
+ ]
27
+
28
+ # ===== Status Transition Validation =====
29
+ # Uses Linear-native status_type values directly:
30
+ # backlog -> unstarted (ready) -> started (in progress) -> completed
31
+
32
+ VALID_STATUS_TRANSITIONS: dict[str, list[str]] = {
33
+ "backlog": ["unstarted", "canceled"],
34
+ "unstarted": ["started", "backlog", "canceled"],
35
+ "started": ["completed", "canceled"],
36
+ "completed": [], # Terminal state
37
+ "canceled": [], # Terminal state
38
+ }
39
+
40
+
41
+ def validate_status_transition(current: str, new: str) -> tuple[bool, str | None]:
42
+ """Validate if a status transition is allowed.
43
+
44
+ Args:
45
+ current: Current task status.
46
+ new: Desired new status.
47
+
48
+ Returns:
49
+ Tuple of (is_valid, error_message).
50
+ error_message is None if transition is valid.
51
+ """
52
+ if current == new:
53
+ return True, None
54
+
55
+ allowed = VALID_STATUS_TRANSITIONS.get(current, [])
56
+ if new not in allowed:
57
+ allowed_str = ", ".join(allowed) if allowed else "none (terminal state)"
58
+ return False, (
59
+ f"Cannot transition from '{current}' to '{new}'. Allowed transitions: {allowed_str}"
60
+ )
61
+
62
+ return True, None
63
+
64
+
65
+ class TasksClient(SteerDevClient):
66
+ """Client for task management operations."""
67
+
68
+ def get_next_task(self, project_id: str | None = None) -> dict[str, Any] | None:
69
+ """Fetch the next task to work on (single-task mode, no waves).
70
+
71
+ Returns the highest priority task in order:
72
+ 1. In Progress tasks (to continue work)
73
+ 2. Todo tasks (to start new work)
74
+ 3. Backlog tasks (if nothing else available)
75
+
76
+ Args:
77
+ project_id: Filter by project ID. Falls back to STEERDEV_PROJECT_ID env var.
78
+
79
+ Returns:
80
+ Task data dict or None if no tasks available.
81
+ """
82
+ effective_project_id = project_id or get_project_id()
83
+
84
+ params: dict[str, str] = {"waves": "false"}
85
+ if effective_project_id:
86
+ params["project_id"] = effective_project_id
87
+
88
+ console.print(f"Fetching next task from {self.api_base}/tasks/next")
89
+ response = self.get("/tasks/next", params=params if params else None)
90
+
91
+ if response.status_code == 404:
92
+ return None
93
+
94
+ if response.status_code != 200:
95
+ console.print(f"[red]API Error: {response.status_code} - {response.text}[/red]")
96
+ return None
97
+
98
+ return response.json()
99
+
100
+ def get_next_wave(self, project_id: str | None = None) -> dict[str, Any] | None:
101
+ """Fetch the next wave-aware task with full wave context.
102
+
103
+ Returns the full wave context including:
104
+ - Current wave info (number, description, total waves)
105
+ - All tasks in the current wave with statuses
106
+ - Completed waves summary
107
+ - The specific next task to work on
108
+
109
+ If no wave tasks exist, returns None (caller should fall back to get_next_task).
110
+
111
+ Args:
112
+ project_id: Filter by project ID. Falls back to STEERDEV_PROJECT_ID env var.
113
+
114
+ Returns:
115
+ Wave response dict or None if no wave tasks available.
116
+ """
117
+ effective_project_id = project_id or get_project_id()
118
+
119
+ params: dict[str, str] = {"waves": "true"}
120
+ if effective_project_id:
121
+ params["project_id"] = effective_project_id
122
+
123
+ console.print(f"Fetching next wave task from {self.api_base}/tasks/next")
124
+ response = self.get("/tasks/next", params=params)
125
+
126
+ if response.status_code == 404:
127
+ return None
128
+
129
+ if response.status_code != 200:
130
+ console.print(f"[red]API Error: {response.status_code} - {response.text}[/red]")
131
+ return None
132
+
133
+ data = response.json()
134
+ # Check if response is a wave response (has "wave" key) vs single task
135
+ if "wave" in data and "context" in data:
136
+ return data
137
+ # API returned a single task (no wave data) — return None so caller falls back
138
+ return None
139
+
140
+ def list_tasks(
141
+ self,
142
+ status: str | None = None,
143
+ project_id: str | None = None,
144
+ limit: int = 20,
145
+ ) -> list[dict[str, Any]]:
146
+ """List tasks with optional filters.
147
+
148
+ Args:
149
+ status: Filter by status.
150
+ project_id: Filter by project ID. Falls back to STEERDEV_PROJECT_ID env var.
151
+ limit: Maximum number of tasks to return.
152
+
153
+ Returns:
154
+ List of task dicts.
155
+ """
156
+ effective_project_id = project_id or get_project_id()
157
+
158
+ params: dict[str, str | int] = {"limit": limit}
159
+ if status:
160
+ params["status"] = status
161
+ if effective_project_id:
162
+ params["project_id"] = effective_project_id
163
+
164
+ console.print(f"Fetching tasks from {self.api_base}/tasks")
165
+ response = self.get("/tasks", params=params)
166
+
167
+ if response.status_code != 200:
168
+ console.print(f"[red]API Error: {response.status_code} - {response.text}[/red]")
169
+ return []
170
+
171
+ data = response.json()
172
+ return data.get("tasks", []) if isinstance(data, dict) else data
173
+
174
+ def get_task(self, task_id: str) -> dict[str, Any] | None:
175
+ """Get a specific task by ID.
176
+
177
+ Args:
178
+ task_id: Task ID (UUID) to fetch.
179
+
180
+ Returns:
181
+ Task data dict or None if not found.
182
+ """
183
+ console.print(f"Fetching task {task_id} from {self.api_base}/tasks/{task_id}")
184
+ response = self.get(f"/tasks/{task_id}")
185
+
186
+ if response.status_code == 404:
187
+ return None
188
+
189
+ if response.status_code != 200:
190
+ console.print(f"[red]API Error: {response.status_code} - {response.text}[/red]")
191
+ return None
192
+
193
+ return response.json()
194
+
195
+ def update_task(
196
+ self,
197
+ task_id: str,
198
+ status: str | None = None,
199
+ title: str | None = None,
200
+ prompt: str | None = None,
201
+ priority: int | None = None,
202
+ result_summary: str | None = None,
203
+ error_message: str | None = None,
204
+ comment: str | None = None,
205
+ ) -> bool:
206
+ """Update a task's fields.
207
+
208
+ Args:
209
+ task_id: Task ID to update.
210
+ status: New status.
211
+ title: New title.
212
+ prompt: New prompt.
213
+ priority: New priority (1=urgent, 2=high, 3=medium, 4=low, 0=none).
214
+ result_summary: Result summary for completed tasks.
215
+ error_message: Error message for failed tasks.
216
+ comment: Comment to add.
217
+
218
+ Returns:
219
+ True if update succeeded.
220
+ """
221
+ # If status is being updated, validate the transition first
222
+ if status:
223
+ current_task = self.get_task(task_id)
224
+ if current_task:
225
+ current_status = current_task.get("status", "")
226
+ is_valid, error = validate_status_transition(current_status, status)
227
+ if not is_valid:
228
+ logger.error(f"Invalid status transition: {error}")
229
+ console.print(f"[red]Error: {error}[/red]")
230
+ return False
231
+
232
+ # Build payload
233
+ payload: dict[str, str | int] = {}
234
+ if status:
235
+ payload["status"] = status
236
+ if title:
237
+ payload["title"] = title
238
+ if prompt:
239
+ payload["prompt"] = prompt
240
+ if priority is not None:
241
+ payload["priority"] = priority
242
+ if result_summary:
243
+ payload["result_summary"] = result_summary
244
+ if error_message:
245
+ payload["error_message"] = error_message
246
+ if comment:
247
+ payload["comment"] = comment
248
+
249
+ if not payload:
250
+ return False
251
+
252
+ console.print(f"Updating task {task_id} at {self.api_base}/tasks/{task_id}")
253
+ response = self.patch(f"/tasks/{task_id}", json=payload)
254
+
255
+ if response.status_code == 404:
256
+ console.print(f"[red]Error: Task '{task_id}' not found[/red]")
257
+ return False
258
+
259
+ if response.status_code == 400:
260
+ # Status transition validation error from API
261
+ console.print(f"[red]Validation Error: {response.text}[/red]")
262
+ return False
263
+
264
+ if response.status_code not in (200, 204):
265
+ console.print(f"[red]API Error: {response.status_code} - {response.text}[/red]")
266
+ return False
267
+
268
+ return True
269
+
270
+ def create_task(
271
+ self,
272
+ title: str,
273
+ prompt: str,
274
+ project_id: str | None = None,
275
+ priority: int = 3,
276
+ working_directory: str | None = None,
277
+ spec_id: str | None = None,
278
+ cycle_id: str | None = None,
279
+ ) -> dict[str, Any] | None:
280
+ """Create a new task.
281
+
282
+ Args:
283
+ title: Task title.
284
+ prompt: Task prompt/description.
285
+ project_id: Project ID. Falls back to STEERDEV_PROJECT_ID env var.
286
+ priority: Task priority (1=urgent, 2=high, 3=medium, 4=low, 0=none).
287
+ working_directory: Working directory for the task.
288
+ spec_id: Specification ID to link this task to.
289
+ cycle_id: Cycle ID to link this task to.
290
+
291
+ Returns:
292
+ Created task data dict or None on failure.
293
+ """
294
+ effective_project_id = project_id or get_project_id()
295
+ if not effective_project_id:
296
+ console.print(
297
+ "[red]Error: project_id is required. "
298
+ "Use --project-id or set STEERDEV_PROJECT_ID environment variable[/red]"
299
+ )
300
+ return None
301
+
302
+ payload: dict[str, str | int | None] = {
303
+ "project_id": effective_project_id,
304
+ "title": title,
305
+ "prompt": prompt,
306
+ "priority": priority,
307
+ "source": "api",
308
+ }
309
+ if working_directory:
310
+ payload["working_directory"] = working_directory
311
+ if spec_id:
312
+ payload["spec_id"] = spec_id
313
+ if cycle_id:
314
+ payload["cycle_id"] = cycle_id
315
+
316
+ console.print(f"Creating task at {self.api_base}/tasks")
317
+ response = self.post("/tasks", json=payload)
318
+
319
+ if response.status_code not in (200, 201):
320
+ console.print(f"[red]API Error: {response.status_code} - {response.text}[/red]")
321
+ return None
322
+
323
+ return response.json()
324
+
325
+ # ===== Workflow Helper Methods =====
326
+ # These methods make the workflow explicit and easy to use.
327
+
328
+ def get_backlog_tasks(self, project_id: str | None = None) -> list[dict[str, Any]]:
329
+ """Get tasks in the backlog.
330
+
331
+ Args:
332
+ project_id: Filter by project ID. Falls back to STEERDEV_PROJECT_ID env var.
333
+
334
+ Returns:
335
+ List of backlog tasks.
336
+ """
337
+ return self.list_tasks(status="backlog", project_id=project_id)
338
+
339
+ def get_unstarted_tasks(self, project_id: str | None = None) -> list[dict[str, Any]]:
340
+ """Get tasks that are ready for implementation (unstarted).
341
+
342
+ Args:
343
+ project_id: Filter by project ID. Falls back to STEERDEV_PROJECT_ID env var.
344
+
345
+ Returns:
346
+ List of unstarted tasks ready to be implemented.
347
+ """
348
+ return self.list_tasks(status="unstarted", project_id=project_id)
349
+
350
+ def mark_ready(self, task_id: str) -> bool:
351
+ """Mark a task as ready for implementation.
352
+
353
+ Transitions task from 'backlog' to 'unstarted'.
354
+
355
+ Args:
356
+ task_id: Task ID to update.
357
+
358
+ Returns:
359
+ True if transition succeeded.
360
+ """
361
+ return self.update_task(task_id, status="unstarted")
362
+
363
+ def start_task(self, task_id: str) -> bool:
364
+ """Start working on a task.
365
+
366
+ Transitions task from 'unstarted' to 'started'.
367
+ This will fail if the task is not in 'unstarted' status.
368
+
369
+ Args:
370
+ task_id: Task ID to update.
371
+
372
+ Returns:
373
+ True if transition succeeded.
374
+ """
375
+ task = self.get_task(task_id)
376
+ if not task:
377
+ console.print(f"[red]Error: Task '{task_id}' not found[/red]")
378
+ return False
379
+
380
+ if task.get("status") != "unstarted":
381
+ logger.error(
382
+ f"Cannot start task: task {task_id} is not ready "
383
+ f"(current status: {task.get('status')})"
384
+ )
385
+ console.print(
386
+ f"[red]Error: Cannot start task. "
387
+ f"Task must be in 'unstarted' status, but is '{task.get('status')}'.[/red]"
388
+ )
389
+ return False
390
+
391
+ return self.update_task(task_id, status="started")
392
+
393
+ def complete_task(self, task_id: str, result_summary: str | None = None) -> bool:
394
+ """Mark a task as completed.
395
+
396
+ Transitions task from 'started' to 'completed'.
397
+
398
+ Args:
399
+ task_id: Task ID to update.
400
+ result_summary: Optional summary of what was accomplished.
401
+
402
+ Returns:
403
+ True if transition succeeded.
404
+ """
405
+ return self.update_task(task_id, status="completed", result_summary=result_summary)
406
+
407
+ def cancel_task(self, task_id: str, error_message: str) -> bool:
408
+ """Mark a task as canceled.
409
+
410
+ Transitions task from 'started' to 'canceled'.
411
+
412
+ Args:
413
+ task_id: Task ID to update.
414
+ error_message: Description of what went wrong.
415
+
416
+ Returns:
417
+ True if transition succeeded.
418
+ """
419
+ return self.update_task(task_id, status="canceled", error_message=error_message)
420
+
421
+
422
+ def get_priority_display(priority: int | None) -> str:
423
+ """Get human-readable priority display.
424
+
425
+ Args:
426
+ priority: Priority number (1=urgent, 2=high, 3=medium, 4=low, 0=none).
427
+
428
+ Returns:
429
+ Formatted priority string like "Urgent (1)" or "N/A".
430
+ """
431
+ priority_names = {
432
+ 1: "Urgent",
433
+ 2: "High",
434
+ 3: "Medium",
435
+ 4: "Low",
436
+ 0: "None",
437
+ }
438
+ if priority is None:
439
+ return "N/A"
440
+ name = priority_names.get(priority, "Unknown")
441
+ return f"{name} ({priority})"
442
+
443
+
444
+ def display_task(task: dict[str, Any], title: str = "Task") -> None:
445
+ """Display a task in a formatted panel.
446
+
447
+ Args:
448
+ task: Task data dict.
449
+ title: Panel title.
450
+ """
451
+ priority_display = get_priority_display(task.get("priority"))
452
+ linear_id = task.get("linear_identifier", "N/A")
453
+ task_info = (
454
+ f"[bold cyan]Linear ID:[/bold cyan] {linear_id}\n"
455
+ f"[bold cyan]ID:[/bold cyan] {task.get('id', 'N/A')}\n"
456
+ f"[bold cyan]Title:[/bold cyan] {task.get('title', 'N/A')}\n"
457
+ f"[bold cyan]Status:[/bold cyan] {task.get('status', 'N/A')}\n"
458
+ f"[bold cyan]Priority:[/bold cyan] {priority_display}\n"
459
+ f"[bold cyan]Project ID:[/bold cyan] {task.get('project_id', 'N/A')}"
460
+ )
461
+
462
+ # Parse implementation plan from prompt
463
+ prompt = task.get("prompt", "")
464
+ impl_plan = parse_implementation_plan(prompt)
465
+ task_description = extract_task_description(prompt) if impl_plan else prompt
466
+
467
+ if task_description:
468
+ task_info += f"\n\n[bold cyan]Description:[/bold cyan]\n{task_description}"
469
+
470
+ if task.get("working_directory"):
471
+ task_info += f"\n\n[bold cyan]Working Directory:[/bold cyan] {task['working_directory']}"
472
+
473
+ if task.get("spec_id"):
474
+ task_info += f"\n[bold cyan]Spec ID:[/bold cyan] {task.get('spec_id')}"
475
+
476
+ if task.get("cycle_id"):
477
+ task_info += f"\n[bold cyan]Cycle ID:[/bold cyan] {task.get('cycle_id')}"
478
+
479
+ console.print(Panel(task_info, title=title, border_style="green"))
480
+
481
+ # Display implementation plan if present
482
+ if impl_plan and not impl_plan.is_empty():
483
+ display_implementation_plan(impl_plan)
484
+
485
+
486
+ def display_task_list(tasks: list[dict[str, Any]], full_ids: bool = True) -> None:
487
+ """Display a list of tasks in a formatted table.
488
+
489
+ Args:
490
+ tasks: List of task data dicts.
491
+ full_ids: If True, show full UUIDs. If False, truncate to 8 chars.
492
+ """
493
+ if not tasks:
494
+ console.print("[yellow]No tasks found[/yellow]")
495
+ return
496
+
497
+ table = Table(title="Tasks")
498
+ table.add_column("Linear ID", style="cyan", no_wrap=True)
499
+ table.add_column("Title", style="white")
500
+ table.add_column("Status", style="magenta")
501
+ table.add_column("Priority", style="yellow")
502
+ table.add_column("ID", style="dim")
503
+
504
+ for task in tasks:
505
+ # Linear-native status_type styling
506
+ status_style = {
507
+ "backlog": "dim",
508
+ "unstarted": "cyan",
509
+ "started": "yellow",
510
+ "completed": "green",
511
+ "canceled": "dim",
512
+ }.get(task.get("status", ""), "white")
513
+
514
+ task_id = str(task.get("id", "N/A"))
515
+ linear_id = task.get("linear_identifier", "N/A")
516
+
517
+ if not full_ids:
518
+ task_id = task_id[:8] + "..." if len(task_id) > 8 else task_id
519
+
520
+ table.add_row(
521
+ linear_id,
522
+ task.get("title", "N/A")[:50],
523
+ f"[{status_style}]{task.get('status', 'N/A')}[/{status_style}]",
524
+ get_priority_display(task.get("priority")),
525
+ task_id,
526
+ )
527
+
528
+ console.print(table)
529
+ console.print(f"\n[dim]Total: {len(tasks)} tasks[/dim]")
530
+
531
+
532
+ def display_wave_context(wave_response: dict[str, Any]) -> None:
533
+ """Display wave context in a formatted panel.
534
+
535
+ Shows the current wave overview, task statuses, completed waves,
536
+ and the next task to work on.
537
+
538
+ Args:
539
+ wave_response: Wave response dict from the API.
540
+ """
541
+ wave = wave_response.get("wave", {})
542
+ context = wave_response.get("context", {})
543
+ tasks = wave_response.get("tasks", [])
544
+ next_task = context.get("next_task", {})
545
+ completed_waves = context.get("completed_waves", [])
546
+
547
+ wave_number = wave.get("wave_number", "?")
548
+ total_waves = wave.get("total_waves", "?")
549
+ wave_desc = wave.get("description", "")
550
+
551
+ # Count task statuses in current wave
552
+ status_counts: dict[str, int] = {}
553
+ for t in tasks:
554
+ s = t.get("status", "unknown")
555
+ status_counts[s] = status_counts.get(s, 0) + 1
556
+
557
+ status_parts = []
558
+ for s in ["completed", "started", "unstarted", "backlog", "canceled"]:
559
+ if s in status_counts:
560
+ status_parts.append(f"{status_counts[s]} {s}")
561
+ status_summary = ", ".join(status_parts) if status_parts else "no tasks"
562
+
563
+ # Wave header
564
+ header = f"[bold blue]Wave {wave_number} of {total_waves}[/bold blue]"
565
+ if wave_desc:
566
+ header += f" — {wave_desc}"
567
+ header += f"\n[dim]{len(tasks)} tasks: {status_summary}[/dim]"
568
+
569
+ # Completed waves
570
+ if completed_waves:
571
+ header += "\n\n[green]Completed waves:[/green]"
572
+ for cw in completed_waves:
573
+ header += f"\n [green]✓[/green] Wave {cw.get('wave_number', '?')}: {cw.get('description', '')}"
574
+
575
+ console.print(Panel(header, title="Wave Context", border_style="blue"))
576
+
577
+ # Task table for current wave
578
+ table = Table(title=f"Wave {wave_number} Tasks")
579
+ table.add_column("", style="bold", width=3)
580
+ table.add_column("Linear ID", style="cyan", no_wrap=True)
581
+ table.add_column("Title", style="white")
582
+ table.add_column("Status", style="magenta")
583
+ table.add_column("Priority", style="yellow")
584
+
585
+ next_task_id = next_task.get("id", "")
586
+ for t in tasks:
587
+ is_next = t.get("id") == next_task_id
588
+ marker = ">>>" if is_next else ""
589
+
590
+ status = t.get("status", "unknown")
591
+ status_style = {
592
+ "backlog": "dim",
593
+ "unstarted": "cyan",
594
+ "started": "yellow",
595
+ "completed": "green",
596
+ "canceled": "dim",
597
+ }.get(status, "white")
598
+
599
+ table.add_row(
600
+ f"[bold yellow]{marker}[/bold yellow]" if is_next else "",
601
+ t.get("linear_identifier", "N/A"),
602
+ t.get("title", "N/A")[:50],
603
+ f"[{status_style}]{status}[/{status_style}]",
604
+ get_priority_display(t.get("priority")),
605
+ )
606
+
607
+ console.print(table)
608
+
609
+ # Next task detail
610
+ if next_task:
611
+ display_task(next_task, title="Next Task")
612
+
613
+
614
+ def display_update_success(
615
+ task_id: str,
616
+ status: str | None = None,
617
+ title: str | None = None,
618
+ prompt: str | None = None,
619
+ priority: int | None = None,
620
+ result_summary: str | None = None,
621
+ error_message: str | None = None,
622
+ comment: str | None = None,
623
+ ) -> None:
624
+ """Display update success message.
625
+
626
+ Args:
627
+ task_id: Updated task ID.
628
+ status: Updated status if any.
629
+ title: Updated title if any.
630
+ prompt: Updated prompt if any.
631
+ priority: Updated priority if any.
632
+ result_summary: Added result summary if any.
633
+ error_message: Added error message if any.
634
+ comment: Added comment if any.
635
+ """
636
+ updates = []
637
+ if status:
638
+ updates.append(f"Status -> {status}")
639
+ if title:
640
+ updates.append("Title updated")
641
+ if prompt:
642
+ updates.append("Prompt updated")
643
+ if priority is not None:
644
+ updates.append(f"Priority -> {priority}")
645
+ if result_summary:
646
+ updates.append("Result summary added")
647
+ if error_message:
648
+ updates.append("Error message added")
649
+ if comment:
650
+ updates.append("Comment added")
651
+
652
+ console.print(
653
+ Panel(
654
+ f"[bold green]Task {task_id} updated[/bold green]\n\n"
655
+ + "\n".join(f"* {u}" for u in updates),
656
+ title="Success",
657
+ border_style="green",
658
+ )
659
+ )