claude-task-master 0.1.1__py3-none-any.whl → 0.1.3__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.
- claude_task_master/__init__.py +1 -1
- claude_task_master/api/__init__.py +98 -0
- claude_task_master/api/models.py +553 -0
- claude_task_master/api/routes.py +1135 -0
- claude_task_master/api/routes_config.py +160 -0
- claude_task_master/api/routes_control.py +278 -0
- claude_task_master/api/routes_webhooks.py +980 -0
- claude_task_master/api/server.py +551 -0
- claude_task_master/auth/__init__.py +89 -0
- claude_task_master/auth/middleware.py +448 -0
- claude_task_master/auth/password.py +332 -0
- claude_task_master/bin/claudetm +1 -1
- claude_task_master/cli.py +4 -0
- claude_task_master/cli_commands/__init__.py +2 -0
- claude_task_master/cli_commands/ci_helpers.py +114 -0
- claude_task_master/cli_commands/control.py +191 -0
- claude_task_master/cli_commands/fix_pr.py +260 -0
- claude_task_master/cli_commands/fix_session.py +174 -0
- claude_task_master/cli_commands/workflow.py +51 -3
- claude_task_master/core/__init__.py +13 -0
- claude_task_master/core/agent_message.py +27 -5
- claude_task_master/core/control.py +466 -0
- claude_task_master/core/orchestrator.py +316 -4
- claude_task_master/core/pr_context.py +7 -2
- claude_task_master/core/prompts_working.py +32 -12
- claude_task_master/core/state.py +84 -2
- claude_task_master/core/state_exceptions.py +9 -6
- claude_task_master/core/workflow_stages.py +160 -21
- claude_task_master/github/client_pr.py +43 -1
- claude_task_master/mcp/auth.py +153 -0
- claude_task_master/mcp/server.py +268 -10
- claude_task_master/mcp/tools.py +281 -0
- claude_task_master/server.py +489 -0
- claude_task_master/webhooks/__init__.py +73 -0
- claude_task_master/webhooks/client.py +703 -0
- claude_task_master/webhooks/config.py +565 -0
- claude_task_master/webhooks/events.py +639 -0
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/METADATA +144 -6
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/RECORD +42 -21
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/entry_points.txt +2 -0
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/WHEEL +0 -0
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
"""REST API routes for Claude Task Master.
|
|
2
|
+
|
|
3
|
+
This module defines API endpoint routes that can be registered with a FastAPI app.
|
|
4
|
+
Each route function takes the necessary dependencies and returns the configured router.
|
|
5
|
+
|
|
6
|
+
Endpoints:
|
|
7
|
+
- GET /status: Get current task status
|
|
8
|
+
- GET /plan: Get task plan content
|
|
9
|
+
- GET /logs: Get log content
|
|
10
|
+
- GET /progress: Get progress summary
|
|
11
|
+
- GET /context: Get accumulated context/learnings
|
|
12
|
+
- GET /health: Health check endpoint
|
|
13
|
+
- POST /task/init: Initialize a new task
|
|
14
|
+
- DELETE /task: Delete/cleanup current task
|
|
15
|
+
- POST /control/stop: Stop a running task with optional cleanup
|
|
16
|
+
- POST /control/resume: Resume a paused or blocked task
|
|
17
|
+
- PATCH /config: Update runtime configuration options
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
from claude_task_master.api.routes import (
|
|
21
|
+
create_info_router,
|
|
22
|
+
create_control_router,
|
|
23
|
+
create_task_router,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
router = create_info_router()
|
|
27
|
+
app.include_router(router)
|
|
28
|
+
|
|
29
|
+
control_router = create_control_router()
|
|
30
|
+
app.include_router(control_router)
|
|
31
|
+
|
|
32
|
+
task_router = create_task_router()
|
|
33
|
+
app.include_router(task_router)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import logging
|
|
40
|
+
import shutil
|
|
41
|
+
import time
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import TYPE_CHECKING, Any
|
|
44
|
+
|
|
45
|
+
from claude_task_master import __version__
|
|
46
|
+
from claude_task_master.api.models import (
|
|
47
|
+
ConfigUpdateRequest,
|
|
48
|
+
ContextResponse,
|
|
49
|
+
ControlResponse,
|
|
50
|
+
ErrorResponse,
|
|
51
|
+
HealthResponse,
|
|
52
|
+
LogsResponse,
|
|
53
|
+
PlanResponse,
|
|
54
|
+
ProgressResponse,
|
|
55
|
+
ResumeRequest,
|
|
56
|
+
StopRequest,
|
|
57
|
+
TaskDeleteResponse,
|
|
58
|
+
TaskInitRequest,
|
|
59
|
+
TaskInitResponse,
|
|
60
|
+
TaskOptionsResponse,
|
|
61
|
+
TaskProgressInfo,
|
|
62
|
+
TaskStatus,
|
|
63
|
+
TaskStatusResponse,
|
|
64
|
+
WebhookStatusInfo,
|
|
65
|
+
WorkflowStage,
|
|
66
|
+
)
|
|
67
|
+
from claude_task_master.api.routes_webhooks import create_webhooks_router
|
|
68
|
+
from claude_task_master.core.agent import ModelType
|
|
69
|
+
from claude_task_master.core.control import ControlManager
|
|
70
|
+
from claude_task_master.core.credentials import CredentialManager
|
|
71
|
+
from claude_task_master.core.state import StateManager, TaskOptions
|
|
72
|
+
|
|
73
|
+
if TYPE_CHECKING:
|
|
74
|
+
from fastapi import APIRouter, FastAPI, Query, Request
|
|
75
|
+
from fastapi.responses import JSONResponse
|
|
76
|
+
|
|
77
|
+
# Import FastAPI - using try/except for graceful degradation
|
|
78
|
+
try:
|
|
79
|
+
from fastapi import APIRouter, Query, Request
|
|
80
|
+
from fastapi.responses import JSONResponse
|
|
81
|
+
|
|
82
|
+
FASTAPI_AVAILABLE = True
|
|
83
|
+
except ImportError:
|
|
84
|
+
FASTAPI_AVAILABLE = False
|
|
85
|
+
|
|
86
|
+
logger = logging.getLogger(__name__)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# =============================================================================
|
|
90
|
+
# Helper Functions
|
|
91
|
+
# =============================================================================
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_plan_tasks(plan: str) -> list[tuple[str, bool]]:
|
|
95
|
+
"""Parse task checkboxes from plan markdown.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
plan: The plan content in markdown format.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of (task_description, is_completed) tuples.
|
|
102
|
+
"""
|
|
103
|
+
tasks: list[tuple[str, bool]] = []
|
|
104
|
+
for line in plan.splitlines():
|
|
105
|
+
line = line.strip()
|
|
106
|
+
if line.startswith("- [ ] "):
|
|
107
|
+
tasks.append((line[6:], False))
|
|
108
|
+
elif line.startswith("- [x] ") or line.startswith("- [X] "):
|
|
109
|
+
tasks.append((line[6:], True))
|
|
110
|
+
return tasks
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _get_state_manager(request: Request) -> StateManager:
|
|
114
|
+
"""Get state manager from request, using working directory from app state.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
request: The FastAPI request object.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
StateManager instance configured for the app's working directory.
|
|
121
|
+
"""
|
|
122
|
+
working_dir: Path = getattr(request.app.state, "working_dir", Path.cwd())
|
|
123
|
+
state_dir = working_dir / ".claude-task-master"
|
|
124
|
+
return StateManager(state_dir=state_dir)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _get_webhook_status(request: Request) -> WebhookStatusInfo | None:
|
|
128
|
+
"""Get webhook configuration status summary.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
request: The FastAPI request object.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
WebhookStatusInfo with counts of total/enabled/disabled webhooks,
|
|
135
|
+
or None if webhooks file doesn't exist or can't be loaded.
|
|
136
|
+
"""
|
|
137
|
+
working_dir: Path = getattr(request.app.state, "working_dir", Path.cwd())
|
|
138
|
+
webhooks_file = working_dir / ".claude-task-master" / "webhooks.json"
|
|
139
|
+
|
|
140
|
+
if not webhooks_file.exists():
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
with open(webhooks_file) as f:
|
|
145
|
+
data = json.load(f)
|
|
146
|
+
webhooks: dict[str, dict[str, Any]] = data.get("webhooks", {})
|
|
147
|
+
|
|
148
|
+
total = len(webhooks)
|
|
149
|
+
enabled = sum(1 for wh in webhooks.values() if wh.get("enabled", True))
|
|
150
|
+
disabled = total - enabled
|
|
151
|
+
|
|
152
|
+
return WebhookStatusInfo(
|
|
153
|
+
total=total,
|
|
154
|
+
enabled=enabled,
|
|
155
|
+
disabled=disabled,
|
|
156
|
+
)
|
|
157
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
158
|
+
logger.warning(f"Failed to load webhook status: {e}")
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# =============================================================================
|
|
163
|
+
# Info Router (Status, Plan, Logs, Progress, Context, Health)
|
|
164
|
+
# =============================================================================
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def create_info_router() -> APIRouter:
|
|
168
|
+
"""Create router for info endpoints.
|
|
169
|
+
|
|
170
|
+
These are read-only endpoints that provide information about the
|
|
171
|
+
current task state without modifying anything.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
APIRouter configured with info endpoints.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ImportError: If FastAPI is not installed.
|
|
178
|
+
"""
|
|
179
|
+
if not FASTAPI_AVAILABLE:
|
|
180
|
+
raise ImportError(
|
|
181
|
+
"FastAPI not installed. Install with: pip install claude-task-master[api]"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
router = APIRouter(tags=["Info"])
|
|
185
|
+
|
|
186
|
+
@router.get(
|
|
187
|
+
"/status",
|
|
188
|
+
response_model=TaskStatusResponse,
|
|
189
|
+
responses={
|
|
190
|
+
404: {"model": ErrorResponse, "description": "No active task found"},
|
|
191
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
192
|
+
},
|
|
193
|
+
summary="Get Task Status",
|
|
194
|
+
description="Get comprehensive status information about the current task.",
|
|
195
|
+
)
|
|
196
|
+
async def get_status(request: Request) -> TaskStatusResponse | JSONResponse:
|
|
197
|
+
"""Get current task status.
|
|
198
|
+
|
|
199
|
+
Returns comprehensive information about the current task including:
|
|
200
|
+
- Goal and current status
|
|
201
|
+
- Model being used
|
|
202
|
+
- Session count and current task index
|
|
203
|
+
- PR information (if applicable)
|
|
204
|
+
- Task options/configuration
|
|
205
|
+
- Task progress (completed/total)
|
|
206
|
+
- Webhook configuration status (total/enabled/disabled counts)
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
TaskStatusResponse with full task status information.
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
404: If no active task exists.
|
|
213
|
+
500: If an error occurs loading state.
|
|
214
|
+
"""
|
|
215
|
+
state_manager = _get_state_manager(request)
|
|
216
|
+
|
|
217
|
+
if not state_manager.exists():
|
|
218
|
+
return JSONResponse(
|
|
219
|
+
status_code=404,
|
|
220
|
+
content=ErrorResponse(
|
|
221
|
+
error="not_found",
|
|
222
|
+
message="No active task found",
|
|
223
|
+
suggestion="Start a new task with 'claudetm start <goal>'",
|
|
224
|
+
).model_dump(),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
state = state_manager.load_state()
|
|
229
|
+
goal = state_manager.load_goal()
|
|
230
|
+
|
|
231
|
+
# Calculate task progress from plan
|
|
232
|
+
tasks_info: TaskProgressInfo | None = None
|
|
233
|
+
plan = state_manager.load_plan()
|
|
234
|
+
if plan:
|
|
235
|
+
tasks = _parse_plan_tasks(plan)
|
|
236
|
+
completed = sum(1 for _, done in tasks if done)
|
|
237
|
+
total = len(tasks)
|
|
238
|
+
tasks_info = TaskProgressInfo(
|
|
239
|
+
completed=completed,
|
|
240
|
+
total=total,
|
|
241
|
+
progress=f"{completed}/{total}" if total > 0 else "No tasks",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Convert status and workflow_stage to enums with defensive error handling
|
|
245
|
+
try:
|
|
246
|
+
status_enum = TaskStatus(state.status)
|
|
247
|
+
except ValueError as e:
|
|
248
|
+
logger.error(f"Invalid status value '{state.status}' in persisted state")
|
|
249
|
+
raise ValueError(f"Corrupted state: invalid status '{state.status}'") from e
|
|
250
|
+
|
|
251
|
+
workflow_stage_enum = None
|
|
252
|
+
if state.workflow_stage:
|
|
253
|
+
try:
|
|
254
|
+
workflow_stage_enum = WorkflowStage(state.workflow_stage)
|
|
255
|
+
except ValueError as e:
|
|
256
|
+
logger.error(
|
|
257
|
+
f"Invalid workflow_stage value '{state.workflow_stage}' in persisted state"
|
|
258
|
+
)
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"Corrupted state: invalid workflow_stage '{state.workflow_stage}'"
|
|
261
|
+
) from e
|
|
262
|
+
|
|
263
|
+
# Load webhook status
|
|
264
|
+
webhooks_info = _get_webhook_status(request)
|
|
265
|
+
|
|
266
|
+
return TaskStatusResponse(
|
|
267
|
+
success=True,
|
|
268
|
+
goal=goal,
|
|
269
|
+
status=status_enum,
|
|
270
|
+
model=state.model,
|
|
271
|
+
current_task_index=state.current_task_index,
|
|
272
|
+
session_count=state.session_count,
|
|
273
|
+
run_id=state.run_id,
|
|
274
|
+
current_pr=state.current_pr,
|
|
275
|
+
workflow_stage=workflow_stage_enum,
|
|
276
|
+
options=TaskOptionsResponse(
|
|
277
|
+
auto_merge=state.options.auto_merge,
|
|
278
|
+
max_sessions=state.options.max_sessions,
|
|
279
|
+
pause_on_pr=state.options.pause_on_pr,
|
|
280
|
+
enable_checkpointing=state.options.enable_checkpointing,
|
|
281
|
+
log_level=state.options.log_level,
|
|
282
|
+
log_format=state.options.log_format,
|
|
283
|
+
pr_per_task=state.options.pr_per_task,
|
|
284
|
+
),
|
|
285
|
+
created_at=state.created_at,
|
|
286
|
+
updated_at=state.updated_at,
|
|
287
|
+
tasks=tasks_info,
|
|
288
|
+
webhooks=webhooks_info,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.exception("Error loading task status")
|
|
293
|
+
return JSONResponse(
|
|
294
|
+
status_code=500,
|
|
295
|
+
content=ErrorResponse(
|
|
296
|
+
error="internal_error",
|
|
297
|
+
message="Failed to load task status",
|
|
298
|
+
detail=str(e),
|
|
299
|
+
).model_dump(),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@router.get(
|
|
303
|
+
"/plan",
|
|
304
|
+
response_model=PlanResponse,
|
|
305
|
+
responses={
|
|
306
|
+
404: {"model": ErrorResponse, "description": "No active task or plan found"},
|
|
307
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
308
|
+
},
|
|
309
|
+
summary="Get Task Plan",
|
|
310
|
+
description="Get the current task plan with markdown checkboxes.",
|
|
311
|
+
)
|
|
312
|
+
async def get_plan(request: Request) -> PlanResponse | JSONResponse:
|
|
313
|
+
"""Get task plan content.
|
|
314
|
+
|
|
315
|
+
Returns the plan markdown content with task checkboxes
|
|
316
|
+
indicating completion status.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
PlanResponse with plan content.
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
404: If no active task or plan exists.
|
|
323
|
+
500: If an error occurs loading the plan.
|
|
324
|
+
"""
|
|
325
|
+
state_manager = _get_state_manager(request)
|
|
326
|
+
|
|
327
|
+
if not state_manager.exists():
|
|
328
|
+
return JSONResponse(
|
|
329
|
+
status_code=404,
|
|
330
|
+
content=ErrorResponse(
|
|
331
|
+
error="not_found",
|
|
332
|
+
message="No active task found",
|
|
333
|
+
suggestion="Start a new task with 'claudetm start <goal>'",
|
|
334
|
+
).model_dump(),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
plan = state_manager.load_plan()
|
|
339
|
+
|
|
340
|
+
if not plan:
|
|
341
|
+
return JSONResponse(
|
|
342
|
+
status_code=404,
|
|
343
|
+
content=ErrorResponse(
|
|
344
|
+
error="not_found",
|
|
345
|
+
message="No plan found",
|
|
346
|
+
suggestion="Task may still be in planning phase",
|
|
347
|
+
).model_dump(),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return PlanResponse(success=True, plan=plan)
|
|
351
|
+
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.exception("Error loading task plan")
|
|
354
|
+
return JSONResponse(
|
|
355
|
+
status_code=500,
|
|
356
|
+
content=ErrorResponse(
|
|
357
|
+
error="internal_error",
|
|
358
|
+
message="Failed to load task plan",
|
|
359
|
+
detail=str(e),
|
|
360
|
+
).model_dump(),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
@router.get(
|
|
364
|
+
"/logs",
|
|
365
|
+
response_model=LogsResponse,
|
|
366
|
+
responses={
|
|
367
|
+
404: {"model": ErrorResponse, "description": "No active task or logs found"},
|
|
368
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
369
|
+
},
|
|
370
|
+
summary="Get Logs",
|
|
371
|
+
description="Get log content from the current run.",
|
|
372
|
+
)
|
|
373
|
+
async def get_logs(
|
|
374
|
+
request: Request,
|
|
375
|
+
tail: int = Query(
|
|
376
|
+
default=100,
|
|
377
|
+
ge=1,
|
|
378
|
+
le=10000,
|
|
379
|
+
description="Number of lines to return from the end of the log",
|
|
380
|
+
),
|
|
381
|
+
) -> LogsResponse | JSONResponse:
|
|
382
|
+
"""Get log content.
|
|
383
|
+
|
|
384
|
+
Returns the last N lines from the current run's log file.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
tail: Number of lines to return (default: 100, max: 10000).
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
LogsResponse with log content and file path.
|
|
391
|
+
|
|
392
|
+
Raises:
|
|
393
|
+
404: If no active task or log file exists.
|
|
394
|
+
500: If an error occurs reading logs.
|
|
395
|
+
"""
|
|
396
|
+
state_manager = _get_state_manager(request)
|
|
397
|
+
|
|
398
|
+
if not state_manager.exists():
|
|
399
|
+
return JSONResponse(
|
|
400
|
+
status_code=404,
|
|
401
|
+
content=ErrorResponse(
|
|
402
|
+
error="not_found",
|
|
403
|
+
message="No active task found",
|
|
404
|
+
suggestion="Start a new task with 'claudetm start <goal>'",
|
|
405
|
+
).model_dump(),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
state = state_manager.load_state()
|
|
410
|
+
log_file = state_manager.get_log_file(state.run_id)
|
|
411
|
+
|
|
412
|
+
if not log_file.exists():
|
|
413
|
+
return JSONResponse(
|
|
414
|
+
status_code=404,
|
|
415
|
+
content=ErrorResponse(
|
|
416
|
+
error="not_found",
|
|
417
|
+
message="No log file found",
|
|
418
|
+
suggestion="Task may not have started execution yet",
|
|
419
|
+
).model_dump(),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
with open(log_file) as f:
|
|
423
|
+
lines = f.readlines()
|
|
424
|
+
|
|
425
|
+
# Return last N lines
|
|
426
|
+
log_content = "".join(lines[-tail:])
|
|
427
|
+
|
|
428
|
+
return LogsResponse(
|
|
429
|
+
success=True,
|
|
430
|
+
log_content=log_content,
|
|
431
|
+
log_file=str(log_file),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
except Exception as e:
|
|
435
|
+
logger.exception("Error loading logs")
|
|
436
|
+
return JSONResponse(
|
|
437
|
+
status_code=500,
|
|
438
|
+
content=ErrorResponse(
|
|
439
|
+
error="internal_error",
|
|
440
|
+
message="Failed to load logs",
|
|
441
|
+
detail=str(e),
|
|
442
|
+
).model_dump(),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
@router.get(
|
|
446
|
+
"/progress",
|
|
447
|
+
response_model=ProgressResponse,
|
|
448
|
+
responses={
|
|
449
|
+
404: {"model": ErrorResponse, "description": "No active task found"},
|
|
450
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
451
|
+
},
|
|
452
|
+
summary="Get Progress",
|
|
453
|
+
description="Get human-readable progress summary.",
|
|
454
|
+
)
|
|
455
|
+
async def get_progress(request: Request) -> ProgressResponse | JSONResponse:
|
|
456
|
+
"""Get progress summary.
|
|
457
|
+
|
|
458
|
+
Returns the human-readable progress summary showing what has been
|
|
459
|
+
accomplished and what remains.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
ProgressResponse with progress content.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
404: If no active task exists.
|
|
466
|
+
500: If an error occurs loading progress.
|
|
467
|
+
"""
|
|
468
|
+
state_manager = _get_state_manager(request)
|
|
469
|
+
|
|
470
|
+
if not state_manager.exists():
|
|
471
|
+
return JSONResponse(
|
|
472
|
+
status_code=404,
|
|
473
|
+
content=ErrorResponse(
|
|
474
|
+
error="not_found",
|
|
475
|
+
message="No active task found",
|
|
476
|
+
suggestion="Start a new task with 'claudetm start <goal>'",
|
|
477
|
+
).model_dump(),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
progress = state_manager.load_progress()
|
|
482
|
+
|
|
483
|
+
if not progress:
|
|
484
|
+
return ProgressResponse(
|
|
485
|
+
success=True,
|
|
486
|
+
progress=None,
|
|
487
|
+
message="No progress recorded yet",
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
return ProgressResponse(success=True, progress=progress)
|
|
491
|
+
|
|
492
|
+
except Exception as e:
|
|
493
|
+
logger.exception("Error loading progress")
|
|
494
|
+
return JSONResponse(
|
|
495
|
+
status_code=500,
|
|
496
|
+
content=ErrorResponse(
|
|
497
|
+
error="internal_error",
|
|
498
|
+
message="Failed to load progress",
|
|
499
|
+
detail=str(e),
|
|
500
|
+
).model_dump(),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
@router.get(
|
|
504
|
+
"/context",
|
|
505
|
+
response_model=ContextResponse,
|
|
506
|
+
responses={
|
|
507
|
+
404: {"model": ErrorResponse, "description": "No active task found"},
|
|
508
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
509
|
+
},
|
|
510
|
+
summary="Get Context",
|
|
511
|
+
description="Get accumulated context and learnings.",
|
|
512
|
+
)
|
|
513
|
+
async def get_context(request: Request) -> ContextResponse | JSONResponse:
|
|
514
|
+
"""Get accumulated context.
|
|
515
|
+
|
|
516
|
+
Returns the accumulated context and learnings that inform
|
|
517
|
+
future sessions.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
ContextResponse with context content.
|
|
521
|
+
|
|
522
|
+
Raises:
|
|
523
|
+
404: If no active task exists.
|
|
524
|
+
500: If an error occurs loading context.
|
|
525
|
+
"""
|
|
526
|
+
state_manager = _get_state_manager(request)
|
|
527
|
+
|
|
528
|
+
if not state_manager.exists():
|
|
529
|
+
return JSONResponse(
|
|
530
|
+
status_code=404,
|
|
531
|
+
content=ErrorResponse(
|
|
532
|
+
error="not_found",
|
|
533
|
+
message="No active task found",
|
|
534
|
+
suggestion="Start a new task with 'claudetm start <goal>'",
|
|
535
|
+
).model_dump(),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
try:
|
|
539
|
+
context = state_manager.load_context()
|
|
540
|
+
|
|
541
|
+
if not context:
|
|
542
|
+
return ContextResponse(
|
|
543
|
+
success=True,
|
|
544
|
+
context=None,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return ContextResponse(success=True, context=context)
|
|
548
|
+
|
|
549
|
+
except Exception as e:
|
|
550
|
+
logger.exception("Error loading context")
|
|
551
|
+
return JSONResponse(
|
|
552
|
+
status_code=500,
|
|
553
|
+
content=ErrorResponse(
|
|
554
|
+
error="internal_error",
|
|
555
|
+
message="Failed to load context",
|
|
556
|
+
detail=str(e),
|
|
557
|
+
).model_dump(),
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
@router.get(
|
|
561
|
+
"/health",
|
|
562
|
+
response_model=HealthResponse,
|
|
563
|
+
summary="Health Check",
|
|
564
|
+
description="Health check endpoint for monitoring and load balancers.",
|
|
565
|
+
)
|
|
566
|
+
async def get_health(request: Request) -> HealthResponse:
|
|
567
|
+
"""Health check endpoint.
|
|
568
|
+
|
|
569
|
+
Returns server health information including:
|
|
570
|
+
- Server status (healthy, degraded, unhealthy)
|
|
571
|
+
- Version information
|
|
572
|
+
- Uptime in seconds
|
|
573
|
+
- Number of active tasks
|
|
574
|
+
|
|
575
|
+
This endpoint is suitable for load balancer health checks
|
|
576
|
+
and monitoring systems.
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
HealthResponse with health status.
|
|
580
|
+
"""
|
|
581
|
+
uptime: float | None = None
|
|
582
|
+
if hasattr(request.app.state, "start_time"):
|
|
583
|
+
uptime = time.time() - request.app.state.start_time
|
|
584
|
+
|
|
585
|
+
active_tasks: int = getattr(request.app.state, "active_tasks", 0)
|
|
586
|
+
|
|
587
|
+
# Check if state directory exists to determine if a task is active
|
|
588
|
+
state_manager = _get_state_manager(request)
|
|
589
|
+
status = "healthy"
|
|
590
|
+
if state_manager.exists():
|
|
591
|
+
try:
|
|
592
|
+
state = state_manager.load_state()
|
|
593
|
+
if state.status in ("blocked", "failed"):
|
|
594
|
+
status = "degraded"
|
|
595
|
+
except Exception:
|
|
596
|
+
# Can't load state - might be degraded
|
|
597
|
+
status = "degraded"
|
|
598
|
+
|
|
599
|
+
return HealthResponse(
|
|
600
|
+
status=status,
|
|
601
|
+
version=__version__,
|
|
602
|
+
server_name="claude-task-master-api",
|
|
603
|
+
uptime_seconds=uptime,
|
|
604
|
+
active_tasks=active_tasks,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
return router
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# =============================================================================
|
|
611
|
+
# Control Router (Stop)
|
|
612
|
+
# =============================================================================
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def create_control_router() -> APIRouter:
|
|
616
|
+
"""Create router for control endpoints.
|
|
617
|
+
|
|
618
|
+
These endpoints allow runtime control of task execution including
|
|
619
|
+
stopping and resuming tasks.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
APIRouter configured with control endpoints.
|
|
623
|
+
|
|
624
|
+
Raises:
|
|
625
|
+
ImportError: If FastAPI is not installed.
|
|
626
|
+
"""
|
|
627
|
+
if not FASTAPI_AVAILABLE:
|
|
628
|
+
raise ImportError(
|
|
629
|
+
"FastAPI not installed. Install with: pip install claude-task-master[api]"
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
router = APIRouter(tags=["Control"])
|
|
633
|
+
|
|
634
|
+
@router.post(
|
|
635
|
+
"/control/stop",
|
|
636
|
+
response_model=ControlResponse,
|
|
637
|
+
responses={
|
|
638
|
+
400: {"model": ErrorResponse, "description": "Invalid operation for current state"},
|
|
639
|
+
404: {"model": ErrorResponse, "description": "No active task found"},
|
|
640
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
641
|
+
},
|
|
642
|
+
summary="Stop Task",
|
|
643
|
+
description="Stop a running task with optional cleanup of state files.",
|
|
644
|
+
)
|
|
645
|
+
async def stop_task(
|
|
646
|
+
request: Request, stop_request: StopRequest
|
|
647
|
+
) -> ControlResponse | JSONResponse:
|
|
648
|
+
"""Stop a running task.
|
|
649
|
+
|
|
650
|
+
Stops the current task and optionally cleans up state files.
|
|
651
|
+
The task must be in a stoppable state (planning, working, blocked, or paused).
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
stop_request: Stop request with optional reason and cleanup flag.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
ControlResponse with operation result.
|
|
658
|
+
|
|
659
|
+
Raises:
|
|
660
|
+
404: If no active task exists.
|
|
661
|
+
400: If the task cannot be stopped in its current state.
|
|
662
|
+
500: If an error occurs during the operation.
|
|
663
|
+
"""
|
|
664
|
+
state_manager = _get_state_manager(request)
|
|
665
|
+
|
|
666
|
+
if not state_manager.exists():
|
|
667
|
+
return JSONResponse(
|
|
668
|
+
status_code=404,
|
|
669
|
+
content=ErrorResponse(
|
|
670
|
+
error="not_found",
|
|
671
|
+
message="No active task found",
|
|
672
|
+
suggestion="Start a new task with 'claudetm start <goal>'",
|
|
673
|
+
).model_dump(),
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
# Create control manager and perform stop operation
|
|
678
|
+
control = ControlManager(state_manager=state_manager)
|
|
679
|
+
result = control.stop(reason=stop_request.reason, cleanup=stop_request.cleanup)
|
|
680
|
+
|
|
681
|
+
return ControlResponse(
|
|
682
|
+
success=result.success,
|
|
683
|
+
message=result.message,
|
|
684
|
+
operation=result.operation,
|
|
685
|
+
previous_status=result.previous_status,
|
|
686
|
+
new_status=result.new_status,
|
|
687
|
+
details=result.details,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
except Exception as e:
|
|
691
|
+
logger.exception("Error stopping task")
|
|
692
|
+
|
|
693
|
+
# Check if it's a known control error
|
|
694
|
+
if "Cannot stop task" in str(e):
|
|
695
|
+
return JSONResponse(
|
|
696
|
+
status_code=400,
|
|
697
|
+
content=ErrorResponse(
|
|
698
|
+
error="invalid_operation",
|
|
699
|
+
message=str(e),
|
|
700
|
+
suggestion="Task may be in a terminal state or already stopped",
|
|
701
|
+
).model_dump(),
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
return JSONResponse(
|
|
705
|
+
status_code=500,
|
|
706
|
+
content=ErrorResponse(
|
|
707
|
+
error="internal_error",
|
|
708
|
+
message="Failed to stop task",
|
|
709
|
+
detail=str(e),
|
|
710
|
+
).model_dump(),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
@router.post(
|
|
714
|
+
"/control/resume",
|
|
715
|
+
response_model=ControlResponse,
|
|
716
|
+
responses={
|
|
717
|
+
400: {"model": ErrorResponse, "description": "Invalid operation for current state"},
|
|
718
|
+
404: {"model": ErrorResponse, "description": "No active task found"},
|
|
719
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
720
|
+
},
|
|
721
|
+
summary="Resume Task",
|
|
722
|
+
description="Resume a paused or blocked task.",
|
|
723
|
+
)
|
|
724
|
+
async def resume_task(
|
|
725
|
+
request: Request, resume_request: ResumeRequest
|
|
726
|
+
) -> ControlResponse | JSONResponse:
|
|
727
|
+
"""Resume a paused or blocked task.
|
|
728
|
+
|
|
729
|
+
Resumes the current task from paused or blocked status.
|
|
730
|
+
The task must be in a resumable state (paused, stopped, blocked, or working).
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
ControlResponse with operation result.
|
|
734
|
+
|
|
735
|
+
Raises:
|
|
736
|
+
404: If no active task exists.
|
|
737
|
+
400: If the task cannot be resumed in its current state.
|
|
738
|
+
500: If an error occurs during the operation.
|
|
739
|
+
"""
|
|
740
|
+
state_manager = _get_state_manager(request)
|
|
741
|
+
|
|
742
|
+
if not state_manager.exists():
|
|
743
|
+
return JSONResponse(
|
|
744
|
+
status_code=404,
|
|
745
|
+
content=ErrorResponse(
|
|
746
|
+
error="not_found",
|
|
747
|
+
message="No active task found",
|
|
748
|
+
suggestion="Start a new task with 'claudetm start <goal>'",
|
|
749
|
+
).model_dump(),
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
try:
|
|
753
|
+
# Create control manager and perform resume operation
|
|
754
|
+
control = ControlManager(state_manager=state_manager)
|
|
755
|
+
result = control.resume()
|
|
756
|
+
|
|
757
|
+
return ControlResponse(
|
|
758
|
+
success=result.success,
|
|
759
|
+
message=result.message,
|
|
760
|
+
operation=result.operation,
|
|
761
|
+
previous_status=result.previous_status,
|
|
762
|
+
new_status=result.new_status,
|
|
763
|
+
details=result.details,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
except Exception as e:
|
|
767
|
+
logger.exception("Error resuming task")
|
|
768
|
+
|
|
769
|
+
# Check if it's a known control error
|
|
770
|
+
if "Cannot resume task" in str(e):
|
|
771
|
+
return JSONResponse(
|
|
772
|
+
status_code=400,
|
|
773
|
+
content=ErrorResponse(
|
|
774
|
+
error="invalid_operation",
|
|
775
|
+
message=str(e),
|
|
776
|
+
suggestion="Task may be in a terminal state or already running",
|
|
777
|
+
).model_dump(),
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
return JSONResponse(
|
|
781
|
+
status_code=500,
|
|
782
|
+
content=ErrorResponse(
|
|
783
|
+
error="internal_error",
|
|
784
|
+
message="Failed to resume task",
|
|
785
|
+
detail=str(e),
|
|
786
|
+
).model_dump(),
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
@router.patch(
|
|
790
|
+
"/config",
|
|
791
|
+
response_model=ControlResponse,
|
|
792
|
+
responses={
|
|
793
|
+
400: {
|
|
794
|
+
"model": ErrorResponse,
|
|
795
|
+
"description": "Invalid configuration or no updates provided",
|
|
796
|
+
},
|
|
797
|
+
404: {"model": ErrorResponse, "description": "No active task found"},
|
|
798
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
799
|
+
},
|
|
800
|
+
summary="Update Configuration",
|
|
801
|
+
description="Update runtime task configuration options.",
|
|
802
|
+
)
|
|
803
|
+
async def update_config(
|
|
804
|
+
request: Request, config_update: ConfigUpdateRequest
|
|
805
|
+
) -> ControlResponse | JSONResponse:
|
|
806
|
+
"""Update task configuration at runtime.
|
|
807
|
+
|
|
808
|
+
Updates the specified configuration options for the current task.
|
|
809
|
+
Only the fields specified in the request are updated; all other
|
|
810
|
+
configuration options retain their current values.
|
|
811
|
+
|
|
812
|
+
Supported options:
|
|
813
|
+
- auto_merge: Whether to auto-merge PRs when approved
|
|
814
|
+
- max_sessions: Maximum number of work sessions before pausing
|
|
815
|
+
- pause_on_pr: Whether to pause after creating PR for manual review
|
|
816
|
+
- enable_checkpointing: Whether to enable state checkpointing
|
|
817
|
+
- log_level: Log level (quiet, normal, verbose)
|
|
818
|
+
- log_format: Log format (text, json)
|
|
819
|
+
- pr_per_task: Whether to create PR per task vs per group
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
config_update: Configuration update request with fields to update.
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
ControlResponse with operation result including updated values.
|
|
826
|
+
|
|
827
|
+
Raises:
|
|
828
|
+
404: If no active task exists.
|
|
829
|
+
400: If no configuration updates were provided or invalid values.
|
|
830
|
+
500: If an error occurs during the operation.
|
|
831
|
+
"""
|
|
832
|
+
state_manager = _get_state_manager(request)
|
|
833
|
+
|
|
834
|
+
if not state_manager.exists():
|
|
835
|
+
return JSONResponse(
|
|
836
|
+
status_code=404,
|
|
837
|
+
content=ErrorResponse(
|
|
838
|
+
error="not_found",
|
|
839
|
+
message="No active task found",
|
|
840
|
+
suggestion="Start a new task with 'claudetm start <goal>'",
|
|
841
|
+
).model_dump(),
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
# Validate that at least one field is being updated
|
|
845
|
+
if not config_update.has_updates():
|
|
846
|
+
return JSONResponse(
|
|
847
|
+
status_code=400,
|
|
848
|
+
content=ErrorResponse(
|
|
849
|
+
error="invalid_request",
|
|
850
|
+
message="No configuration updates provided",
|
|
851
|
+
suggestion="Specify at least one configuration field to update",
|
|
852
|
+
).model_dump(),
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
try:
|
|
856
|
+
# Create control manager and perform config update
|
|
857
|
+
control = ControlManager(state_manager=state_manager)
|
|
858
|
+
|
|
859
|
+
# Convert config update to kwargs dictionary
|
|
860
|
+
update_kwargs = config_update.to_update_dict()
|
|
861
|
+
result = control.update_config(**update_kwargs)
|
|
862
|
+
|
|
863
|
+
return ControlResponse(
|
|
864
|
+
success=result.success,
|
|
865
|
+
message=result.message,
|
|
866
|
+
operation=result.operation,
|
|
867
|
+
previous_status=result.previous_status,
|
|
868
|
+
new_status=result.new_status,
|
|
869
|
+
details=result.details,
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
except ValueError as e:
|
|
873
|
+
logger.exception("Invalid configuration update")
|
|
874
|
+
return JSONResponse(
|
|
875
|
+
status_code=400,
|
|
876
|
+
content=ErrorResponse(
|
|
877
|
+
error="invalid_configuration",
|
|
878
|
+
message="Invalid configuration option",
|
|
879
|
+
detail=str(e),
|
|
880
|
+
).model_dump(),
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
except Exception as e:
|
|
884
|
+
logger.exception("Error updating configuration")
|
|
885
|
+
return JSONResponse(
|
|
886
|
+
status_code=500,
|
|
887
|
+
content=ErrorResponse(
|
|
888
|
+
error="internal_error",
|
|
889
|
+
message="Failed to update configuration",
|
|
890
|
+
detail=str(e),
|
|
891
|
+
).model_dump(),
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
return router
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
# =============================================================================
|
|
898
|
+
# Task Management Router (Init, Delete)
|
|
899
|
+
# =============================================================================
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def create_task_router() -> APIRouter:
|
|
903
|
+
"""Create router for task management endpoints.
|
|
904
|
+
|
|
905
|
+
These endpoints allow task lifecycle management including
|
|
906
|
+
initializing new tasks and deleting existing tasks.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
APIRouter configured with task management endpoints.
|
|
910
|
+
|
|
911
|
+
Raises:
|
|
912
|
+
ImportError: If FastAPI is not installed.
|
|
913
|
+
"""
|
|
914
|
+
if not FASTAPI_AVAILABLE:
|
|
915
|
+
raise ImportError(
|
|
916
|
+
"FastAPI not installed. Install with: pip install claude-task-master[api]"
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
router = APIRouter(tags=["Task Management"])
|
|
920
|
+
|
|
921
|
+
@router.post(
|
|
922
|
+
"/task/init",
|
|
923
|
+
response_model=TaskInitResponse,
|
|
924
|
+
responses={
|
|
925
|
+
400: {"model": ErrorResponse, "description": "Invalid request or task already exists"},
|
|
926
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
927
|
+
},
|
|
928
|
+
summary="Initialize Task",
|
|
929
|
+
description="Initialize a new task with the given goal and options.",
|
|
930
|
+
)
|
|
931
|
+
async def init_task(
|
|
932
|
+
request: Request, task_init: TaskInitRequest
|
|
933
|
+
) -> TaskInitResponse | JSONResponse:
|
|
934
|
+
"""Initialize a new task.
|
|
935
|
+
|
|
936
|
+
Creates a new task with the specified goal and configuration options.
|
|
937
|
+
The task will be in 'planning' status after initialization.
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
task_init: Task initialization request with goal and options.
|
|
941
|
+
|
|
942
|
+
Returns:
|
|
943
|
+
TaskInitResponse with initialization result including run_id.
|
|
944
|
+
|
|
945
|
+
Raises:
|
|
946
|
+
400: If a task already exists or request is invalid.
|
|
947
|
+
500: If an error occurs during initialization.
|
|
948
|
+
"""
|
|
949
|
+
state_manager = _get_state_manager(request)
|
|
950
|
+
|
|
951
|
+
# Check if task already exists
|
|
952
|
+
if state_manager.exists():
|
|
953
|
+
return JSONResponse(
|
|
954
|
+
status_code=400,
|
|
955
|
+
content=ErrorResponse(
|
|
956
|
+
error="task_exists",
|
|
957
|
+
message="A task already exists",
|
|
958
|
+
suggestion="Use DELETE /task to remove the existing task first",
|
|
959
|
+
).model_dump(),
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
try:
|
|
963
|
+
# Validate model type
|
|
964
|
+
try:
|
|
965
|
+
ModelType(task_init.model)
|
|
966
|
+
except ValueError:
|
|
967
|
+
return JSONResponse(
|
|
968
|
+
status_code=400,
|
|
969
|
+
content=ErrorResponse(
|
|
970
|
+
error="invalid_model",
|
|
971
|
+
message=f"Invalid model '{task_init.model}'",
|
|
972
|
+
detail="Model must be one of: opus, sonnet, haiku",
|
|
973
|
+
suggestion="Use 'opus', 'sonnet', or 'haiku'",
|
|
974
|
+
).model_dump(),
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
# Load credentials to verify we can authenticate
|
|
978
|
+
try:
|
|
979
|
+
cred_manager = CredentialManager()
|
|
980
|
+
cred_manager.get_valid_token()
|
|
981
|
+
except Exception as e:
|
|
982
|
+
logger.exception("Failed to load credentials")
|
|
983
|
+
return JSONResponse(
|
|
984
|
+
status_code=500,
|
|
985
|
+
content=ErrorResponse(
|
|
986
|
+
error="credentials_error",
|
|
987
|
+
message="Failed to load Claude credentials",
|
|
988
|
+
detail=str(e),
|
|
989
|
+
suggestion="Ensure you have authenticated with 'claude auth'",
|
|
990
|
+
).model_dump(),
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
# Initialize task state
|
|
994
|
+
logger.info(f"Initializing new task: {task_init.goal}")
|
|
995
|
+
options = TaskOptions(
|
|
996
|
+
auto_merge=task_init.auto_merge,
|
|
997
|
+
max_sessions=task_init.max_sessions,
|
|
998
|
+
pause_on_pr=task_init.pause_on_pr,
|
|
999
|
+
enable_checkpointing=False, # Default to False
|
|
1000
|
+
log_level="normal", # Default to normal
|
|
1001
|
+
log_format="text", # Default to text
|
|
1002
|
+
pr_per_task=False, # Default to False
|
|
1003
|
+
)
|
|
1004
|
+
state = state_manager.initialize(
|
|
1005
|
+
goal=task_init.goal, model=task_init.model, options=options
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
logger.info(f"Task initialized with run_id: {state.run_id}")
|
|
1009
|
+
|
|
1010
|
+
return TaskInitResponse(
|
|
1011
|
+
success=True,
|
|
1012
|
+
message="Task initialized successfully",
|
|
1013
|
+
run_id=state.run_id,
|
|
1014
|
+
status=state.status,
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
except Exception as e:
|
|
1018
|
+
logger.exception("Error initializing task")
|
|
1019
|
+
return JSONResponse(
|
|
1020
|
+
status_code=500,
|
|
1021
|
+
content=ErrorResponse(
|
|
1022
|
+
error="internal_error",
|
|
1023
|
+
message="Failed to initialize task",
|
|
1024
|
+
detail=str(e),
|
|
1025
|
+
).model_dump(),
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
@router.delete(
|
|
1029
|
+
"/task",
|
|
1030
|
+
response_model=TaskDeleteResponse,
|
|
1031
|
+
responses={
|
|
1032
|
+
404: {"model": ErrorResponse, "description": "No active task found"},
|
|
1033
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
1034
|
+
},
|
|
1035
|
+
summary="Delete Task",
|
|
1036
|
+
description="Delete the current task and cleanup all state files.",
|
|
1037
|
+
)
|
|
1038
|
+
async def delete_task(request: Request) -> TaskDeleteResponse | JSONResponse:
|
|
1039
|
+
"""Delete the current task.
|
|
1040
|
+
|
|
1041
|
+
Removes all task state files including plan, progress, context,
|
|
1042
|
+
and state. This operation cannot be undone.
|
|
1043
|
+
|
|
1044
|
+
Returns:
|
|
1045
|
+
TaskDeleteResponse with deletion result.
|
|
1046
|
+
|
|
1047
|
+
Raises:
|
|
1048
|
+
404: If no active task exists.
|
|
1049
|
+
500: If an error occurs during deletion.
|
|
1050
|
+
"""
|
|
1051
|
+
state_manager = _get_state_manager(request)
|
|
1052
|
+
|
|
1053
|
+
if not state_manager.exists():
|
|
1054
|
+
return JSONResponse(
|
|
1055
|
+
status_code=404,
|
|
1056
|
+
content=ErrorResponse(
|
|
1057
|
+
error="not_found",
|
|
1058
|
+
message="No active task found",
|
|
1059
|
+
suggestion="No task to delete",
|
|
1060
|
+
).model_dump(),
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
try:
|
|
1064
|
+
# Check if session is active
|
|
1065
|
+
is_active = state_manager.is_session_active()
|
|
1066
|
+
|
|
1067
|
+
if is_active:
|
|
1068
|
+
logger.warning("Deleting task while session is active")
|
|
1069
|
+
# Release session lock before deletion
|
|
1070
|
+
state_manager.release_session_lock()
|
|
1071
|
+
|
|
1072
|
+
# Remove state directory
|
|
1073
|
+
state_dir = state_manager.state_dir
|
|
1074
|
+
if state_dir.exists():
|
|
1075
|
+
shutil.rmtree(state_dir)
|
|
1076
|
+
logger.info(f"Task state deleted: {state_dir}")
|
|
1077
|
+
files_removed = True
|
|
1078
|
+
else:
|
|
1079
|
+
logger.warning(f"State directory not found: {state_dir}")
|
|
1080
|
+
files_removed = False
|
|
1081
|
+
|
|
1082
|
+
return TaskDeleteResponse(
|
|
1083
|
+
success=True,
|
|
1084
|
+
message="Task deleted successfully",
|
|
1085
|
+
files_removed=files_removed,
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
except Exception as e:
|
|
1089
|
+
logger.exception("Error deleting task")
|
|
1090
|
+
return JSONResponse(
|
|
1091
|
+
status_code=500,
|
|
1092
|
+
content=ErrorResponse(
|
|
1093
|
+
error="internal_error",
|
|
1094
|
+
message="Failed to delete task",
|
|
1095
|
+
detail=str(e),
|
|
1096
|
+
).model_dump(),
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
return router
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
# =============================================================================
|
|
1103
|
+
# Router Registration
|
|
1104
|
+
# =============================================================================
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def register_routes(app: FastAPI) -> None:
|
|
1108
|
+
"""Register all API routes with the FastAPI app.
|
|
1109
|
+
|
|
1110
|
+
This function creates and registers all routers with the app.
|
|
1111
|
+
It's the main entry point for route registration.
|
|
1112
|
+
|
|
1113
|
+
Args:
|
|
1114
|
+
app: The FastAPI application to register routes with.
|
|
1115
|
+
"""
|
|
1116
|
+
# Create and register info router
|
|
1117
|
+
info_router = create_info_router()
|
|
1118
|
+
app.include_router(info_router)
|
|
1119
|
+
|
|
1120
|
+
# Create and register control router
|
|
1121
|
+
control_router = create_control_router()
|
|
1122
|
+
app.include_router(control_router)
|
|
1123
|
+
|
|
1124
|
+
# Create and register task management router
|
|
1125
|
+
task_router = create_task_router()
|
|
1126
|
+
app.include_router(task_router)
|
|
1127
|
+
|
|
1128
|
+
# Create and register webhooks router
|
|
1129
|
+
webhooks_router = create_webhooks_router()
|
|
1130
|
+
app.include_router(webhooks_router, prefix="/webhooks")
|
|
1131
|
+
|
|
1132
|
+
logger.debug("Registered info routes: /status, /plan, /logs, /progress, /context, /health")
|
|
1133
|
+
logger.debug("Registered control routes: /control/stop, /control/resume, /config")
|
|
1134
|
+
logger.debug("Registered task routes: /task/init, /task")
|
|
1135
|
+
logger.debug("Registered webhook routes: /webhooks, /webhooks/{id}, /webhooks/test")
|