agentic-devtools 0.2.0__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.
- agdt_ai_helpers/__init__.py +34 -0
- agentic_devtools/__init__.py +8 -0
- agentic_devtools/background_tasks.py +598 -0
- agentic_devtools/cli/__init__.py +1 -0
- agentic_devtools/cli/azure_devops/__init__.py +222 -0
- agentic_devtools/cli/azure_devops/async_commands.py +1218 -0
- agentic_devtools/cli/azure_devops/auth.py +34 -0
- agentic_devtools/cli/azure_devops/commands.py +728 -0
- agentic_devtools/cli/azure_devops/config.py +49 -0
- agentic_devtools/cli/azure_devops/file_review_commands.py +1038 -0
- agentic_devtools/cli/azure_devops/helpers.py +561 -0
- agentic_devtools/cli/azure_devops/mark_reviewed.py +756 -0
- agentic_devtools/cli/azure_devops/pipeline_commands.py +724 -0
- agentic_devtools/cli/azure_devops/pr_summary_commands.py +579 -0
- agentic_devtools/cli/azure_devops/pull_request_details_commands.py +596 -0
- agentic_devtools/cli/azure_devops/review_commands.py +700 -0
- agentic_devtools/cli/azure_devops/review_helpers.py +191 -0
- agentic_devtools/cli/azure_devops/review_jira.py +308 -0
- agentic_devtools/cli/azure_devops/review_prompts.py +263 -0
- agentic_devtools/cli/azure_devops/run_details_commands.py +935 -0
- agentic_devtools/cli/azure_devops/vpn_toggle.py +1220 -0
- agentic_devtools/cli/git/__init__.py +91 -0
- agentic_devtools/cli/git/async_commands.py +294 -0
- agentic_devtools/cli/git/commands.py +399 -0
- agentic_devtools/cli/git/core.py +152 -0
- agentic_devtools/cli/git/diff.py +210 -0
- agentic_devtools/cli/git/operations.py +737 -0
- agentic_devtools/cli/jira/__init__.py +114 -0
- agentic_devtools/cli/jira/adf.py +105 -0
- agentic_devtools/cli/jira/async_commands.py +439 -0
- agentic_devtools/cli/jira/async_status.py +27 -0
- agentic_devtools/cli/jira/commands.py +28 -0
- agentic_devtools/cli/jira/comment_commands.py +141 -0
- agentic_devtools/cli/jira/config.py +69 -0
- agentic_devtools/cli/jira/create_commands.py +293 -0
- agentic_devtools/cli/jira/formatting.py +131 -0
- agentic_devtools/cli/jira/get_commands.py +287 -0
- agentic_devtools/cli/jira/helpers.py +278 -0
- agentic_devtools/cli/jira/parse_error_report.py +352 -0
- agentic_devtools/cli/jira/role_commands.py +560 -0
- agentic_devtools/cli/jira/state_helpers.py +39 -0
- agentic_devtools/cli/jira/update_commands.py +222 -0
- agentic_devtools/cli/jira/vpn_wrapper.py +58 -0
- agentic_devtools/cli/release/__init__.py +5 -0
- agentic_devtools/cli/release/commands.py +113 -0
- agentic_devtools/cli/release/helpers.py +113 -0
- agentic_devtools/cli/runner.py +318 -0
- agentic_devtools/cli/state.py +174 -0
- agentic_devtools/cli/subprocess_utils.py +109 -0
- agentic_devtools/cli/tasks/__init__.py +28 -0
- agentic_devtools/cli/tasks/commands.py +851 -0
- agentic_devtools/cli/testing.py +442 -0
- agentic_devtools/cli/workflows/__init__.py +80 -0
- agentic_devtools/cli/workflows/advancement.py +204 -0
- agentic_devtools/cli/workflows/base.py +240 -0
- agentic_devtools/cli/workflows/checklist.py +278 -0
- agentic_devtools/cli/workflows/commands.py +1610 -0
- agentic_devtools/cli/workflows/manager.py +802 -0
- agentic_devtools/cli/workflows/preflight.py +323 -0
- agentic_devtools/cli/workflows/worktree_setup.py +1110 -0
- agentic_devtools/dispatcher.py +704 -0
- agentic_devtools/file_locking.py +203 -0
- agentic_devtools/prompts/__init__.py +38 -0
- agentic_devtools/prompts/apply-pull-request-review-suggestions/default-initiate-prompt.md +82 -0
- agentic_devtools/prompts/create-jira-epic/default-initiate-prompt.md +63 -0
- agentic_devtools/prompts/create-jira-issue/default-initiate-prompt.md +306 -0
- agentic_devtools/prompts/create-jira-subtask/default-initiate-prompt.md +57 -0
- agentic_devtools/prompts/loader.py +377 -0
- agentic_devtools/prompts/pull-request-review/default-completion-prompt.md +45 -0
- agentic_devtools/prompts/pull-request-review/default-decision-prompt.md +63 -0
- agentic_devtools/prompts/pull-request-review/default-file-review-prompt.md +69 -0
- agentic_devtools/prompts/pull-request-review/default-initiate-prompt.md +50 -0
- agentic_devtools/prompts/pull-request-review/default-summary-prompt.md +40 -0
- agentic_devtools/prompts/update-jira-issue/default-initiate-prompt.md +78 -0
- agentic_devtools/prompts/work-on-jira-issue/default-checklist-creation-prompt.md +58 -0
- agentic_devtools/prompts/work-on-jira-issue/default-commit-prompt.md +47 -0
- agentic_devtools/prompts/work-on-jira-issue/default-completion-prompt.md +65 -0
- agentic_devtools/prompts/work-on-jira-issue/default-implementation-prompt.md +66 -0
- agentic_devtools/prompts/work-on-jira-issue/default-implementation-review-prompt.md +60 -0
- agentic_devtools/prompts/work-on-jira-issue/default-initiate-prompt.md +67 -0
- agentic_devtools/prompts/work-on-jira-issue/default-planning-prompt.md +50 -0
- agentic_devtools/prompts/work-on-jira-issue/default-pull-request-prompt.md +56 -0
- agentic_devtools/prompts/work-on-jira-issue/default-retrieve-prompt.md +29 -0
- agentic_devtools/prompts/work-on-jira-issue/default-setup-prompt.md +19 -0
- agentic_devtools/prompts/work-on-jira-issue/default-verification-prompt.md +73 -0
- agentic_devtools/state.py +754 -0
- agentic_devtools/task_state.py +902 -0
- agentic_devtools-0.2.0.dist-info/METADATA +544 -0
- agentic_devtools-0.2.0.dist-info/RECORD +92 -0
- agentic_devtools-0.2.0.dist-info/WHEEL +4 -0
- agentic_devtools-0.2.0.dist-info/entry_points.txt +79 -0
- agentic_devtools-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task monitoring commands.
|
|
3
|
+
|
|
4
|
+
CLI commands for monitoring and managing background tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from ...background_tasks import get_task_log_content
|
|
13
|
+
from ...task_state import (
|
|
14
|
+
BackgroundTask,
|
|
15
|
+
TaskStatus,
|
|
16
|
+
cleanup_expired_tasks,
|
|
17
|
+
get_background_tasks,
|
|
18
|
+
get_other_incomplete_tasks,
|
|
19
|
+
get_task_by_id,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_task_id_from_args_or_state(_argv: Optional[List[str]] = None) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Get task ID from CLI args or state.
|
|
26
|
+
|
|
27
|
+
If --id is provided, updates state with the new task_id.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
_argv: Optional list of CLI arguments (for testing)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Task ID string
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
SystemExit: If no task ID is available
|
|
37
|
+
"""
|
|
38
|
+
from ...state import get_value, set_value
|
|
39
|
+
|
|
40
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
41
|
+
parser.add_argument("--id", type=str, default=None, help="Task ID to track")
|
|
42
|
+
|
|
43
|
+
args, _ = parser.parse_known_args(_argv)
|
|
44
|
+
|
|
45
|
+
if args.id:
|
|
46
|
+
# Update state with the provided task ID
|
|
47
|
+
set_value("background.task_id", args.id)
|
|
48
|
+
return args.id
|
|
49
|
+
|
|
50
|
+
task_id = get_value("background.task_id")
|
|
51
|
+
|
|
52
|
+
if not task_id:
|
|
53
|
+
print("Error: No task ID specified.")
|
|
54
|
+
print('Set a task ID with: agdt-set background.task_id <task-id> or use --id "<task-id>"')
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
return task_id
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _safe_print(text: str) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Print text safely, handling unicode encoding errors.
|
|
63
|
+
|
|
64
|
+
On Windows with cp1252 encoding, emoji characters may fail to encode.
|
|
65
|
+
This function replaces non-encodable characters with ASCII equivalents.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
print(text)
|
|
69
|
+
except UnicodeEncodeError:
|
|
70
|
+
# Replace emoji with ASCII equivalents
|
|
71
|
+
ascii_replacements = {
|
|
72
|
+
"\u2705": "[OK]", # ā
|
|
73
|
+
"\u274c": "[FAIL]", # ā
|
|
74
|
+
"\u23f3": "[...]", # ā³
|
|
75
|
+
"\U0001f504": "[~]", # š
|
|
76
|
+
"\u2753": "[?]", # ā
|
|
77
|
+
"\u23f0": "[TIMEOUT]", # ā°
|
|
78
|
+
"\U0001f4cb": "[LIST]", # š
|
|
79
|
+
"\u2022": "-", # ā¢
|
|
80
|
+
}
|
|
81
|
+
safe_text = text
|
|
82
|
+
for emoji, replacement in ascii_replacements.items():
|
|
83
|
+
safe_text = safe_text.replace(emoji, replacement)
|
|
84
|
+
# Final fallback: encode with replace
|
|
85
|
+
print(safe_text.encode(sys.stdout.encoding or "utf-8", errors="replace").decode())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _format_timestamp(ts: Optional[str]) -> str:
|
|
89
|
+
"""Format an ISO timestamp for display."""
|
|
90
|
+
if not ts:
|
|
91
|
+
return "N/A"
|
|
92
|
+
try:
|
|
93
|
+
dt = datetime.fromisoformat(ts)
|
|
94
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
95
|
+
except (ValueError, TypeError):
|
|
96
|
+
return ts
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _format_duration(task: BackgroundTask) -> str:
|
|
100
|
+
"""Calculate and format task duration."""
|
|
101
|
+
if not task.start_time:
|
|
102
|
+
return "Not started"
|
|
103
|
+
|
|
104
|
+
start = datetime.fromisoformat(task.start_time)
|
|
105
|
+
|
|
106
|
+
if task.end_time:
|
|
107
|
+
end = datetime.fromisoformat(task.end_time)
|
|
108
|
+
elif task.status == TaskStatus.RUNNING:
|
|
109
|
+
end = datetime.now(timezone.utc)
|
|
110
|
+
else:
|
|
111
|
+
return "Unknown"
|
|
112
|
+
|
|
113
|
+
duration = end - start
|
|
114
|
+
total_seconds = int(duration.total_seconds())
|
|
115
|
+
|
|
116
|
+
if total_seconds < 60:
|
|
117
|
+
return f"{total_seconds}s"
|
|
118
|
+
elif total_seconds < 3600:
|
|
119
|
+
minutes = total_seconds // 60
|
|
120
|
+
seconds = total_seconds % 60
|
|
121
|
+
return f"{minutes}m {seconds}s"
|
|
122
|
+
else:
|
|
123
|
+
hours = total_seconds // 3600
|
|
124
|
+
minutes = (total_seconds % 3600) // 60
|
|
125
|
+
return f"{hours}h {minutes}m"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _status_indicator(status: TaskStatus) -> str:
|
|
129
|
+
"""Get a status indicator symbol."""
|
|
130
|
+
indicators = {
|
|
131
|
+
TaskStatus.PENDING: "ā³",
|
|
132
|
+
TaskStatus.RUNNING: "š",
|
|
133
|
+
TaskStatus.COMPLETED: "ā
",
|
|
134
|
+
TaskStatus.FAILED: "ā",
|
|
135
|
+
}
|
|
136
|
+
return indicators.get(status, "ā")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def list_tasks() -> None:
|
|
140
|
+
"""
|
|
141
|
+
List all background tasks.
|
|
142
|
+
|
|
143
|
+
Entry point: agdt-tasks
|
|
144
|
+
"""
|
|
145
|
+
tasks = get_background_tasks()
|
|
146
|
+
|
|
147
|
+
if not tasks:
|
|
148
|
+
print("No background tasks found.")
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# Sort by start_time descending (most recent first)
|
|
152
|
+
sorted_tasks = sorted(
|
|
153
|
+
tasks,
|
|
154
|
+
key=lambda t: t.start_time or "",
|
|
155
|
+
reverse=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
print(f"\n{'ID':<36} {'Status':<12} {'Command':<30} {'Duration':<10} Created")
|
|
159
|
+
print("-" * 110)
|
|
160
|
+
|
|
161
|
+
for task in sorted_tasks:
|
|
162
|
+
status_str = f"{_status_indicator(task.status)} {task.status.value}"
|
|
163
|
+
command_display = task.command[:27] + "..." if len(task.command) > 30 else task.command
|
|
164
|
+
duration = _format_duration(task)
|
|
165
|
+
created = _format_timestamp(task.start_time)
|
|
166
|
+
|
|
167
|
+
_safe_print(f"{task.id:<36} {status_str:<12} {command_display:<30} {duration:<10} {created}")
|
|
168
|
+
|
|
169
|
+
print(f"\nTotal: {len(tasks)} task(s)")
|
|
170
|
+
|
|
171
|
+
# Count by status
|
|
172
|
+
status_counts = {}
|
|
173
|
+
for task in tasks:
|
|
174
|
+
status_counts[task.status] = status_counts.get(task.status, 0) + 1
|
|
175
|
+
|
|
176
|
+
status_summary = ", ".join(
|
|
177
|
+
f"{status.value}: {count}" for status, count in sorted(status_counts.items(), key=lambda x: x[0].value)
|
|
178
|
+
)
|
|
179
|
+
print(f"By status: {status_summary}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def task_status(_argv: Optional[List[str]] = None) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Show detailed status of a specific task.
|
|
185
|
+
|
|
186
|
+
Entry point: agdt-task-status
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
_argv: Optional list of CLI arguments (for testing)
|
|
190
|
+
|
|
191
|
+
CLI args:
|
|
192
|
+
--id: Task ID to show status for (overrides state, updates background.task_id)
|
|
193
|
+
|
|
194
|
+
Reads task ID from state: background.task_id (if --id not provided)
|
|
195
|
+
"""
|
|
196
|
+
task_id = _get_task_id_from_args_or_state(_argv)
|
|
197
|
+
|
|
198
|
+
task = get_task_by_id(task_id)
|
|
199
|
+
|
|
200
|
+
if not task:
|
|
201
|
+
print(f"Error: Task '{task_id}' not found.")
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
|
|
204
|
+
print(f"\n{'=' * 60}")
|
|
205
|
+
print(f"Task Details: {task.id}")
|
|
206
|
+
print(f"{'=' * 60}")
|
|
207
|
+
print(f" Command: {task.command}")
|
|
208
|
+
_safe_print(f" Status: {_status_indicator(task.status)} {task.status.value}")
|
|
209
|
+
print(f" Started: {_format_timestamp(task.start_time)}")
|
|
210
|
+
print(f" Completed: {_format_timestamp(task.end_time)}")
|
|
211
|
+
print(f" Duration: {_format_duration(task)}")
|
|
212
|
+
|
|
213
|
+
if task.exit_code is not None:
|
|
214
|
+
print(f" Exit Code: {task.exit_code}")
|
|
215
|
+
|
|
216
|
+
if task.log_file:
|
|
217
|
+
print(f" Log File: {task.log_file}")
|
|
218
|
+
|
|
219
|
+
if task.error_message:
|
|
220
|
+
print(f" Error: {task.error_message}")
|
|
221
|
+
|
|
222
|
+
print(f"{'=' * 60}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def task_log(_argv: Optional[List[str]] = None) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Display task log contents.
|
|
228
|
+
|
|
229
|
+
Entry point: agdt-task-log
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
_argv: Optional list of CLI arguments (for testing)
|
|
233
|
+
|
|
234
|
+
CLI args:
|
|
235
|
+
--id: Task ID to show log for (overrides state, updates background.task_id)
|
|
236
|
+
|
|
237
|
+
Reads task ID from state: background.task_id (if --id not provided)
|
|
238
|
+
Optional state keys:
|
|
239
|
+
- background.log_lines: Number of lines to show (default: all, use negative for tail)
|
|
240
|
+
"""
|
|
241
|
+
from ...state import get_value
|
|
242
|
+
|
|
243
|
+
task_id = _get_task_id_from_args_or_state(_argv)
|
|
244
|
+
|
|
245
|
+
task = get_task_by_id(task_id)
|
|
246
|
+
|
|
247
|
+
if not task:
|
|
248
|
+
print(f"Error: Task '{task_id}' not found.")
|
|
249
|
+
sys.exit(1)
|
|
250
|
+
|
|
251
|
+
log_content = get_task_log_content(task_id)
|
|
252
|
+
|
|
253
|
+
if log_content is None:
|
|
254
|
+
print(f"No log file available for task '{task_id}'.")
|
|
255
|
+
if task.log_file:
|
|
256
|
+
print(f"Expected log file: {task.log_file}")
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
|
|
259
|
+
# Check for line limit
|
|
260
|
+
lines_str = get_value("background.log_lines")
|
|
261
|
+
if lines_str:
|
|
262
|
+
try:
|
|
263
|
+
lines_limit = int(lines_str)
|
|
264
|
+
log_lines = log_content.splitlines()
|
|
265
|
+
|
|
266
|
+
if lines_limit < 0:
|
|
267
|
+
# Tail mode: show last N lines
|
|
268
|
+
log_lines = log_lines[lines_limit:]
|
|
269
|
+
elif lines_limit > 0:
|
|
270
|
+
# Head mode: show first N lines
|
|
271
|
+
log_lines = log_lines[:lines_limit]
|
|
272
|
+
|
|
273
|
+
log_content = "\n".join(log_lines)
|
|
274
|
+
except ValueError:
|
|
275
|
+
pass # Ignore invalid line count
|
|
276
|
+
|
|
277
|
+
print(f"\n--- Log for task {task_id} ---")
|
|
278
|
+
print(f"Command: {task.command}")
|
|
279
|
+
_safe_print(f"Status: {_status_indicator(task.status)} {task.status.value}")
|
|
280
|
+
print("-" * 50)
|
|
281
|
+
print(log_content)
|
|
282
|
+
print("-" * 50)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _parse_wait_args(_argv: Optional[List[str]] = None) -> argparse.Namespace:
|
|
286
|
+
"""
|
|
287
|
+
Parse arguments for task_wait command.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
_argv: Optional list of CLI arguments (for testing)
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Parsed arguments namespace
|
|
294
|
+
"""
|
|
295
|
+
parser = argparse.ArgumentParser(
|
|
296
|
+
prog="agdt-task-wait",
|
|
297
|
+
description="Wait for a background task to complete",
|
|
298
|
+
add_help=True,
|
|
299
|
+
)
|
|
300
|
+
parser.add_argument("--id", type=str, default=None, help="Task ID to wait for")
|
|
301
|
+
parser.add_argument(
|
|
302
|
+
"--wait-interval",
|
|
303
|
+
type=float,
|
|
304
|
+
default=1.0,
|
|
305
|
+
help="Seconds to wait between status checks (default: 1.0)",
|
|
306
|
+
)
|
|
307
|
+
parser.add_argument(
|
|
308
|
+
"--timeout",
|
|
309
|
+
type=float,
|
|
310
|
+
default=300.0,
|
|
311
|
+
help="Maximum seconds since task start before timeout (default: 300)",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return parser.parse_args(_argv or [])
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _check_task_timeout(task: BackgroundTask, timeout: float) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Check if task has exceeded timeout based on its start_time.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
task: The background task to check
|
|
323
|
+
timeout: Maximum seconds since task start
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
True if task has timed out, False otherwise
|
|
327
|
+
"""
|
|
328
|
+
if not task.start_time:
|
|
329
|
+
return False # Can't determine timeout without start time
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
# Parse start_time (ISO format)
|
|
333
|
+
start_dt = datetime.fromisoformat(task.start_time.replace("Z", "+00:00"))
|
|
334
|
+
elapsed = (datetime.now(timezone.utc) - start_dt).total_seconds()
|
|
335
|
+
return elapsed > timeout
|
|
336
|
+
except (ValueError, TypeError):
|
|
337
|
+
return False # Can't determine timeout with invalid start time
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def task_wait(_argv: Optional[List[str]] = None) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Wait for task completion and auto-progress workflow.
|
|
343
|
+
|
|
344
|
+
Entry point: agdt-task-wait
|
|
345
|
+
|
|
346
|
+
This command checks task status twice (with a configurable wait between checks):
|
|
347
|
+
1. If task completed ā Handles success/failure as appropriate
|
|
348
|
+
2. If task still running ā Tells AI agent to call agdt-task-wait again
|
|
349
|
+
3. If task timed out (based on start_time) ā Reports timeout
|
|
350
|
+
|
|
351
|
+
After task completion:
|
|
352
|
+
1. If task failed ā Shows log with fix instructions
|
|
353
|
+
2. If other incomplete most-recent-per-command tasks exist ā Instructs to wait for them
|
|
354
|
+
3. If any most-recent-per-command task failed ā Instructs to review it
|
|
355
|
+
4. If all clear ā Automatically runs agdt-get-next-workflow-prompt
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
_argv: Optional list of CLI arguments (for testing)
|
|
359
|
+
|
|
360
|
+
CLI args:
|
|
361
|
+
--id: Task ID to wait for (overrides state, updates background.task_id)
|
|
362
|
+
--wait-interval: Seconds to wait between checks (default: 1.0)
|
|
363
|
+
--timeout: Max seconds since task start before timeout (default: 300)
|
|
364
|
+
|
|
365
|
+
Reads task ID from state: background.task_id (if --id not provided)
|
|
366
|
+
"""
|
|
367
|
+
import time
|
|
368
|
+
|
|
369
|
+
from ...state import get_value, set_value
|
|
370
|
+
|
|
371
|
+
# Parse args
|
|
372
|
+
args = _parse_wait_args(_argv)
|
|
373
|
+
|
|
374
|
+
# Handle task ID (from arg or state)
|
|
375
|
+
if args.id:
|
|
376
|
+
set_value("background.task_id", args.id)
|
|
377
|
+
task_id = args.id
|
|
378
|
+
else:
|
|
379
|
+
task_id = get_value("background.task_id")
|
|
380
|
+
if not task_id:
|
|
381
|
+
print("Error: No task ID specified.")
|
|
382
|
+
print('Set a task ID with: agdt-set background.task_id <task-id> or use --id "<task-id>"')
|
|
383
|
+
sys.exit(1)
|
|
384
|
+
|
|
385
|
+
# Allow state overrides for wait_interval and timeout
|
|
386
|
+
wait_interval = args.wait_interval
|
|
387
|
+
timeout = args.timeout
|
|
388
|
+
|
|
389
|
+
wait_interval_str = get_value("background.wait_interval")
|
|
390
|
+
if wait_interval_str:
|
|
391
|
+
try:
|
|
392
|
+
wait_interval = float(wait_interval_str)
|
|
393
|
+
except ValueError:
|
|
394
|
+
pass # Keep CLI arg value
|
|
395
|
+
|
|
396
|
+
timeout_str = get_value("background.timeout")
|
|
397
|
+
if timeout_str:
|
|
398
|
+
try:
|
|
399
|
+
timeout = float(timeout_str)
|
|
400
|
+
except ValueError:
|
|
401
|
+
pass # Keep CLI arg value
|
|
402
|
+
|
|
403
|
+
# Get task details
|
|
404
|
+
task = get_task_by_id(task_id)
|
|
405
|
+
if task is None:
|
|
406
|
+
print(f"Error: Task '{task_id}' not found.")
|
|
407
|
+
sys.exit(1)
|
|
408
|
+
|
|
409
|
+
print(f"Checking task (command: {task.command}, id: {task_id})...")
|
|
410
|
+
|
|
411
|
+
# Check 1: Is task already complete?
|
|
412
|
+
if task.is_terminal():
|
|
413
|
+
_handle_task_completed(task, task_id, timeout)
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
# Check timeout based on start_time
|
|
417
|
+
if _check_task_timeout(task, timeout):
|
|
418
|
+
_handle_task_timeout(task, task_id, timeout)
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
# Task still running - wait and check again
|
|
422
|
+
print(f"Task still running, waiting {wait_interval}s...")
|
|
423
|
+
time.sleep(wait_interval)
|
|
424
|
+
|
|
425
|
+
# Check 2: Re-fetch and check status
|
|
426
|
+
task = get_task_by_id(task_id)
|
|
427
|
+
if task is None:
|
|
428
|
+
print(f"Error: Task '{task_id}' disappeared during wait.")
|
|
429
|
+
sys.exit(1)
|
|
430
|
+
|
|
431
|
+
if task.is_terminal():
|
|
432
|
+
_handle_task_completed(task, task_id, timeout)
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
# Check timeout again
|
|
436
|
+
if _check_task_timeout(task, timeout):
|
|
437
|
+
_handle_task_timeout(task, task_id, timeout)
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# Task still running after 2 checks - tell AI to wait again
|
|
441
|
+
_handle_task_still_running(task, task_id, wait_interval)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _handle_task_still_running(task: BackgroundTask, task_id: str, wait_interval: float) -> None:
|
|
445
|
+
"""Handle case where task is still running after checks."""
|
|
446
|
+
elapsed = _get_task_elapsed_time(task)
|
|
447
|
+
|
|
448
|
+
print()
|
|
449
|
+
print("=" * 70)
|
|
450
|
+
_safe_print("ā³ TASK STILL IN PROGRESS")
|
|
451
|
+
print("=" * 70)
|
|
452
|
+
_safe_print(f"\nTask: {task.command}")
|
|
453
|
+
_safe_print(f"Status: {_status_indicator(task.status)} {task.status.value}")
|
|
454
|
+
if elapsed is not None:
|
|
455
|
+
print(f"Running for: {elapsed:.1f}s")
|
|
456
|
+
|
|
457
|
+
_safe_print("\nš Next Step:")
|
|
458
|
+
print(" Run agdt-task-wait again to continue waiting for completion.")
|
|
459
|
+
print()
|
|
460
|
+
print("Alternative actions:")
|
|
461
|
+
print(" ⢠Check status: agdt-task-status")
|
|
462
|
+
print(" ⢠View log: agdt-task-log")
|
|
463
|
+
sys.exit(0)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _handle_task_timeout(task: BackgroundTask, task_id: str, timeout: float) -> None:
|
|
467
|
+
"""Handle case where task has exceeded timeout."""
|
|
468
|
+
elapsed = _get_task_elapsed_time(task)
|
|
469
|
+
|
|
470
|
+
print()
|
|
471
|
+
print("=" * 70)
|
|
472
|
+
_safe_print("ā° TASK TIMEOUT")
|
|
473
|
+
print("=" * 70)
|
|
474
|
+
_safe_print(f"\nTask: {task.command}")
|
|
475
|
+
_safe_print(f"Status: {_status_indicator(task.status)} {task.status.value}")
|
|
476
|
+
if elapsed is not None:
|
|
477
|
+
print(f"Running for: {elapsed:.1f}s (timeout: {timeout}s)")
|
|
478
|
+
|
|
479
|
+
_safe_print("\nš Next Steps:")
|
|
480
|
+
print(" 1. Check if task is stuck: agdt-task-log")
|
|
481
|
+
print(" 2. If stuck, consider canceling and retrying")
|
|
482
|
+
print(f" 3. Or increase timeout: agdt-set background.timeout {int(timeout * 2)}")
|
|
483
|
+
sys.exit(2)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _get_task_elapsed_time(task: BackgroundTask) -> Optional[float]:
|
|
487
|
+
"""Get elapsed time in seconds since task started."""
|
|
488
|
+
if not task.start_time:
|
|
489
|
+
return None
|
|
490
|
+
try:
|
|
491
|
+
start_dt = datetime.fromisoformat(task.start_time.replace("Z", "+00:00"))
|
|
492
|
+
return (datetime.now(timezone.utc) - start_dt).total_seconds()
|
|
493
|
+
except (ValueError, TypeError):
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _try_advance_pr_review_to_summary() -> bool:
|
|
498
|
+
"""
|
|
499
|
+
Check if pull-request-review workflow should auto-advance to summary step.
|
|
500
|
+
|
|
501
|
+
This function checks if:
|
|
502
|
+
1. Current workflow is pull-request-review
|
|
503
|
+
2. Current step is file-review
|
|
504
|
+
3. All files have been reviewed (queue all_complete is True)
|
|
505
|
+
|
|
506
|
+
If all conditions are met:
|
|
507
|
+
- Advances workflow to summary step
|
|
508
|
+
- Triggers generate_pr_summary_async() as background task
|
|
509
|
+
- Sets background.task_id to the new summary task
|
|
510
|
+
- Prints message instructing AI to run agdt-task-wait
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
True if workflow was advanced and summary task started
|
|
514
|
+
False if conditions not met or workflow not applicable
|
|
515
|
+
"""
|
|
516
|
+
from ...state import get_value, get_workflow_state, set_value
|
|
517
|
+
|
|
518
|
+
# Check if we're in pull-request-review workflow at file-review step
|
|
519
|
+
workflow = get_workflow_state()
|
|
520
|
+
if not workflow:
|
|
521
|
+
return False
|
|
522
|
+
|
|
523
|
+
workflow_name = workflow.get("active", "")
|
|
524
|
+
current_step = workflow.get("step", "")
|
|
525
|
+
|
|
526
|
+
if workflow_name != "pull-request-review" or current_step != "file-review":
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
# Get PR ID to check queue status
|
|
530
|
+
pr_id_str = get_value("pull_request_id")
|
|
531
|
+
if not pr_id_str:
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
pr_id = int(pr_id_str)
|
|
536
|
+
except ValueError:
|
|
537
|
+
return False
|
|
538
|
+
|
|
539
|
+
# Check if all files are complete
|
|
540
|
+
from ..azure_devops.file_review_commands import get_queue_status
|
|
541
|
+
|
|
542
|
+
queue_status = get_queue_status(pr_id)
|
|
543
|
+
if not queue_status.get("all_complete", False):
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
# Also wait for all submissions to complete before generating summary
|
|
547
|
+
if queue_status.get("submission_pending_count", 0) > 0:
|
|
548
|
+
return False
|
|
549
|
+
|
|
550
|
+
# All conditions met - advance workflow to summary step
|
|
551
|
+
from ..workflows.base import set_workflow_state
|
|
552
|
+
|
|
553
|
+
context = workflow.get("context", {})
|
|
554
|
+
set_workflow_state(
|
|
555
|
+
name="pull-request-review",
|
|
556
|
+
status="in-progress",
|
|
557
|
+
step="summary",
|
|
558
|
+
context=context,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# Start summary generation as background task (without printing tracking info)
|
|
562
|
+
from ...background_tasks import run_function_in_background
|
|
563
|
+
from ...task_state import BackgroundTask
|
|
564
|
+
|
|
565
|
+
_PR_SUMMARY_MODULE = "agentic_devtools.cli.azure_devops.pr_summary_commands"
|
|
566
|
+
task: BackgroundTask = run_function_in_background(
|
|
567
|
+
_PR_SUMMARY_MODULE,
|
|
568
|
+
"generate_overarching_pr_comments_cli",
|
|
569
|
+
command_display_name="agdt-generate-pr-summary",
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# Update background.task_id so next agdt-task-wait tracks this task
|
|
573
|
+
set_value("background.task_id", task.id)
|
|
574
|
+
|
|
575
|
+
# Print clear, unambiguous message for AI agent
|
|
576
|
+
print()
|
|
577
|
+
print("=" * 70)
|
|
578
|
+
_safe_print("ā
ALL FILE REVIEWS COMPLETE - AUTO-GENERATING PR SUMMARY")
|
|
579
|
+
print("=" * 70)
|
|
580
|
+
print()
|
|
581
|
+
print("PR summary generation has been AUTOMATICALLY started in the background.")
|
|
582
|
+
print(f"Task ID: {task.id}")
|
|
583
|
+
print()
|
|
584
|
+
print("IMPORTANT: The summary will be posted automatically. Do NOT manually")
|
|
585
|
+
print("trigger dfly-generate-pr-summary - it is already running.")
|
|
586
|
+
print()
|
|
587
|
+
_safe_print("š YOUR ONLY ACTION: Run agdt-task-wait")
|
|
588
|
+
print()
|
|
589
|
+
print("This will wait for the summary to complete and then provide next steps.")
|
|
590
|
+
|
|
591
|
+
return True
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _try_complete_pr_review_workflow(task: BackgroundTask) -> bool:
|
|
595
|
+
"""
|
|
596
|
+
Check if pull-request-review workflow should complete after summary generation.
|
|
597
|
+
|
|
598
|
+
This function checks if:
|
|
599
|
+
1. Current workflow is pull-request-review
|
|
600
|
+
2. Current step is summary
|
|
601
|
+
3. The completed task was dfly-generate-pr-summary
|
|
602
|
+
|
|
603
|
+
If all conditions are met:
|
|
604
|
+
- Advances workflow to completion step
|
|
605
|
+
- Prints completion message
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
task: The background task that just completed
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
True if workflow was completed
|
|
612
|
+
False if conditions not met or workflow not applicable
|
|
613
|
+
"""
|
|
614
|
+
from ...state import get_workflow_state
|
|
615
|
+
|
|
616
|
+
# Check if we're in pull-request-review workflow at summary step
|
|
617
|
+
workflow = get_workflow_state()
|
|
618
|
+
if not workflow:
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
workflow_name = workflow.get("active", "")
|
|
622
|
+
current_step = workflow.get("step", "")
|
|
623
|
+
|
|
624
|
+
if workflow_name != "pull-request-review" or current_step != "summary":
|
|
625
|
+
return False
|
|
626
|
+
|
|
627
|
+
# Check if the completed task was the summary generation
|
|
628
|
+
if task.command != "agdt-generate-pr-summary":
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
# All conditions met - advance workflow to completion
|
|
632
|
+
from ..workflows.base import set_workflow_state
|
|
633
|
+
|
|
634
|
+
context = workflow.get("context", {})
|
|
635
|
+
set_workflow_state(
|
|
636
|
+
name="pull-request-review",
|
|
637
|
+
status="completed",
|
|
638
|
+
step="completion",
|
|
639
|
+
context=context,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
# Print completion message
|
|
643
|
+
print()
|
|
644
|
+
print("=" * 70)
|
|
645
|
+
_safe_print("ā
PR REVIEW WORKFLOW COMPLETE")
|
|
646
|
+
print("=" * 70)
|
|
647
|
+
print()
|
|
648
|
+
print("All file reviews have been submitted and summary comments posted.")
|
|
649
|
+
print("The pull-request-review workflow has completed successfully.")
|
|
650
|
+
print()
|
|
651
|
+
print("NEXT STEPS:")
|
|
652
|
+
print(" 1. Review the PR summary comments that were posted")
|
|
653
|
+
print(" 2. Make a final decision on the PR:")
|
|
654
|
+
print(" - To approve: agdt-approve-pull-request")
|
|
655
|
+
print(" - Or provide feedback and request changes as needed")
|
|
656
|
+
|
|
657
|
+
return True
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _handle_task_completed(task: BackgroundTask, task_id: str, timeout: float) -> None:
|
|
661
|
+
"""
|
|
662
|
+
Handle a completed task (success or failure).
|
|
663
|
+
|
|
664
|
+
This contains the original post-completion logic from task_wait.
|
|
665
|
+
"""
|
|
666
|
+
from ...task_state import (
|
|
667
|
+
get_failed_most_recent_per_command,
|
|
668
|
+
get_incomplete_most_recent_per_command,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
_safe_print(f"\nTask completed with status: {_status_indicator(task.status)} {task.status.value}")
|
|
672
|
+
print(f"Duration: {_format_duration(task)}")
|
|
673
|
+
|
|
674
|
+
if task.exit_code is not None:
|
|
675
|
+
print(f"Exit code: {task.exit_code}")
|
|
676
|
+
|
|
677
|
+
# Handle task failure - show log with fix instructions
|
|
678
|
+
if task.status == TaskStatus.FAILED:
|
|
679
|
+
print("\n" + "=" * 70)
|
|
680
|
+
_safe_print("ā TASK FAILED - Review log and fix the issue")
|
|
681
|
+
print("=" * 70)
|
|
682
|
+
|
|
683
|
+
if task.error_message:
|
|
684
|
+
print(f"\nError: {task.error_message}")
|
|
685
|
+
|
|
686
|
+
# Show the log
|
|
687
|
+
print("\n--- Task Log ---")
|
|
688
|
+
log_content = get_task_log_content(task_id)
|
|
689
|
+
if log_content:
|
|
690
|
+
print(log_content)
|
|
691
|
+
else:
|
|
692
|
+
print("(No log content available)")
|
|
693
|
+
print("--- End Log ---")
|
|
694
|
+
|
|
695
|
+
_safe_print("\nš Next Steps:")
|
|
696
|
+
print(" 1. Review the error above")
|
|
697
|
+
print(" 2. Fix the underlying issue")
|
|
698
|
+
print(f" 3. Re-run the command: {task.command}")
|
|
699
|
+
print(" 4. Run agdt-task-wait again")
|
|
700
|
+
sys.exit(task.exit_code if task.exit_code is not None else 1)
|
|
701
|
+
|
|
702
|
+
# Task succeeded - now check for other tasks
|
|
703
|
+
print()
|
|
704
|
+
|
|
705
|
+
# Check for other incomplete most-recent-per-command tasks
|
|
706
|
+
other_incomplete = get_incomplete_most_recent_per_command(exclude_task_id=task_id)
|
|
707
|
+
if other_incomplete:
|
|
708
|
+
print("=" * 70)
|
|
709
|
+
_safe_print("ā³ OTHER TASKS STILL RUNNING")
|
|
710
|
+
print("=" * 70)
|
|
711
|
+
print(f"\n{len(other_incomplete)} other task(s) still in progress:")
|
|
712
|
+
for other_task in other_incomplete:
|
|
713
|
+
_safe_print(f" ⢠{other_task.command} (id: {other_task.id}, status: {other_task.status.value})")
|
|
714
|
+
|
|
715
|
+
# Pick the first one to wait for
|
|
716
|
+
next_task = other_incomplete[0]
|
|
717
|
+
_safe_print("\nš Next Step:")
|
|
718
|
+
print(f' agdt-task-wait --id "{next_task.id}"')
|
|
719
|
+
sys.exit(0)
|
|
720
|
+
|
|
721
|
+
# Check for failed most-recent-per-command tasks
|
|
722
|
+
# Exclude the current task's command since it just succeeded - we don't want to
|
|
723
|
+
# report an older failed task for the same command
|
|
724
|
+
failed_tasks = get_failed_most_recent_per_command(
|
|
725
|
+
exclude_task_id=task_id,
|
|
726
|
+
exclude_commands=[task.command],
|
|
727
|
+
)
|
|
728
|
+
if failed_tasks:
|
|
729
|
+
print("=" * 70)
|
|
730
|
+
_safe_print("ā OTHER TASKS FAILED - Review and fix")
|
|
731
|
+
print("=" * 70)
|
|
732
|
+
print(f"\n{len(failed_tasks)} task(s) failed:")
|
|
733
|
+
for failed_task in failed_tasks:
|
|
734
|
+
_safe_print(f" ⢠{failed_task.command} (id: {failed_task.id})")
|
|
735
|
+
|
|
736
|
+
# Pick the first one to review
|
|
737
|
+
next_task = failed_tasks[0]
|
|
738
|
+
_safe_print("\nš Next Steps:")
|
|
739
|
+
print(f' 1. Review the failed task: agdt-task-log --id "{next_task.id}"')
|
|
740
|
+
print(" 2. Fix the underlying issue")
|
|
741
|
+
print(f" 3. Re-run the command: {next_task.command}")
|
|
742
|
+
sys.exit(0)
|
|
743
|
+
|
|
744
|
+
# All tasks succeeded - check for special workflow handling
|
|
745
|
+
# Check if we need to auto-advance pull-request-review workflow
|
|
746
|
+
if _try_advance_pr_review_to_summary():
|
|
747
|
+
# Successfully advanced and triggered summary - AI agent should run agdt-task-wait
|
|
748
|
+
sys.exit(0)
|
|
749
|
+
|
|
750
|
+
# Check if pull-request-review workflow should complete after summary
|
|
751
|
+
if _try_complete_pr_review_workflow(task):
|
|
752
|
+
# Workflow completed successfully
|
|
753
|
+
sys.exit(0)
|
|
754
|
+
|
|
755
|
+
# Standard workflow progression
|
|
756
|
+
print("=" * 70)
|
|
757
|
+
_safe_print("ā
ALL TASKS COMPLETED SUCCESSFULLY")
|
|
758
|
+
print("=" * 70)
|
|
759
|
+
|
|
760
|
+
# Try to get next workflow prompt
|
|
761
|
+
try:
|
|
762
|
+
from ..workflows import get_next_workflow_prompt_cmd
|
|
763
|
+
|
|
764
|
+
print("\nAuto-progressing workflow...\n")
|
|
765
|
+
get_next_workflow_prompt_cmd()
|
|
766
|
+
except ImportError:
|
|
767
|
+
# Workflow module not available
|
|
768
|
+
print("\nAll background tasks complete.")
|
|
769
|
+
except Exception as e:
|
|
770
|
+
# Workflow command failed - still report success
|
|
771
|
+
print(f"\nNote: Could not auto-progress workflow: {e}")
|
|
772
|
+
print("Run manually: agdt-get-next-workflow-prompt")
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def tasks_clean() -> None:
|
|
776
|
+
"""
|
|
777
|
+
Clean up expired tasks and old log files.
|
|
778
|
+
|
|
779
|
+
Entry point: agdt-tasks-clean
|
|
780
|
+
Optional state keys:
|
|
781
|
+
- background.expiry_hours: Hours before tasks expire (default: 24)
|
|
782
|
+
"""
|
|
783
|
+
from ...background_tasks import cleanup_old_logs
|
|
784
|
+
from ...state import load_state
|
|
785
|
+
|
|
786
|
+
state = load_state()
|
|
787
|
+
|
|
788
|
+
# Parse optional expiry hours
|
|
789
|
+
expiry_hours = 24 # Default 24 hours
|
|
790
|
+
|
|
791
|
+
expiry_str = state.get("background.expiry_hours")
|
|
792
|
+
if expiry_str:
|
|
793
|
+
try:
|
|
794
|
+
expiry_hours = int(expiry_str)
|
|
795
|
+
except ValueError:
|
|
796
|
+
print(f"Warning: Invalid expiry hours '{expiry_str}', using default {expiry_hours}h")
|
|
797
|
+
|
|
798
|
+
print(f"Cleaning up tasks older than {expiry_hours} hours...")
|
|
799
|
+
|
|
800
|
+
# Get task count before cleanup
|
|
801
|
+
tasks_before = get_background_tasks()
|
|
802
|
+
count_before = len(tasks_before)
|
|
803
|
+
|
|
804
|
+
# Clean up expired tasks
|
|
805
|
+
removed_count = cleanup_expired_tasks(retention_hours=expiry_hours)
|
|
806
|
+
|
|
807
|
+
# Clean up orphaned log files
|
|
808
|
+
logs_removed = cleanup_old_logs(max_age_hours=expiry_hours)
|
|
809
|
+
|
|
810
|
+
print("\nCleanup complete:")
|
|
811
|
+
print(f" Tasks before: {count_before}")
|
|
812
|
+
print(f" Tasks removed: {removed_count}")
|
|
813
|
+
print(f" Tasks remaining: {count_before - removed_count}")
|
|
814
|
+
print(f" Log files removed: {logs_removed}")
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def show_other_incomplete_tasks() -> None:
|
|
818
|
+
"""
|
|
819
|
+
Show other incomplete background tasks (not the current task_id).
|
|
820
|
+
|
|
821
|
+
Entry point: agdt-show-other-incomplete-tasks
|
|
822
|
+
|
|
823
|
+
This command shows any tasks in background.recentTasks that:
|
|
824
|
+
- Are not in status 'completed' or 'failed'
|
|
825
|
+
- Do not match the current background.task_id
|
|
826
|
+
|
|
827
|
+
Useful for seeing if there are other tasks still running.
|
|
828
|
+
"""
|
|
829
|
+
from ...state import get_value
|
|
830
|
+
|
|
831
|
+
current_task_id = get_value("background.task_id") or ""
|
|
832
|
+
|
|
833
|
+
other_incomplete = get_other_incomplete_tasks(current_task_id)
|
|
834
|
+
|
|
835
|
+
if not other_incomplete:
|
|
836
|
+
print("No other recent incomplete background tasks.")
|
|
837
|
+
return
|
|
838
|
+
|
|
839
|
+
print(f"\nOther incomplete background tasks ({len(other_incomplete)} found):")
|
|
840
|
+
print(f"{'ID':<36} {'Status':<12} {'Command':<30} {'Duration':<10}")
|
|
841
|
+
print("-" * 95)
|
|
842
|
+
|
|
843
|
+
for task in other_incomplete:
|
|
844
|
+
status_str = f"{_status_indicator(task.status)} {task.status.value}"
|
|
845
|
+
command_display = task.command[:27] + "..." if len(task.command) > 30 else task.command
|
|
846
|
+
duration = _format_duration(task)
|
|
847
|
+
|
|
848
|
+
_safe_print(f"{task.id:<36} {status_str:<12} {command_display:<30} {duration:<10}")
|
|
849
|
+
|
|
850
|
+
print()
|
|
851
|
+
print('To track a specific task: agdt-task-status --id "<task-id>"')
|