codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +344 -325
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +417 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
GitHub integration routes.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import time
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any, Dict, Optional, Tuple
|
|
9
|
-
|
|
10
|
-
from fastapi import APIRouter, HTTPException, Query, Request
|
|
11
|
-
from fastapi.responses import FileResponse, StreamingResponse
|
|
12
|
-
|
|
13
|
-
from ..integrations.github.pr_flow import PrFlowError, PrFlowManager
|
|
14
|
-
from ..integrations.github.service import GitHubError, GitHubService
|
|
15
|
-
from ..web.schemas import (
|
|
16
|
-
GithubContextRequest,
|
|
17
|
-
GithubIssueRequest,
|
|
18
|
-
GithubPrFlowActionRequest,
|
|
19
|
-
GithubPrFlowStartRequest,
|
|
20
|
-
GithubPrSyncRequest,
|
|
21
|
-
)
|
|
22
|
-
from .shared import SSE_HEADERS, jsonl_event_stream
|
|
23
|
-
|
|
24
|
-
_GITHUB_CACHE: Dict[Tuple[str, str], Dict[str, Any]] = {}
|
|
25
|
-
_GITHUB_CACHE_LOCK = asyncio.Lock()
|
|
26
|
-
_GITHUB_STATUS_TTL_SECONDS = 20.0
|
|
27
|
-
_GITHUB_PR_TTL_SECONDS = 60.0
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
async def _get_cached_status_payload(
|
|
31
|
-
request: Request,
|
|
32
|
-
*,
|
|
33
|
-
kind: str,
|
|
34
|
-
ttl_seconds: float,
|
|
35
|
-
) -> dict:
|
|
36
|
-
repo_root = request.app.state.engine.repo_root.resolve()
|
|
37
|
-
key = (str(repo_root), kind)
|
|
38
|
-
now = time.monotonic()
|
|
39
|
-
task: Optional[asyncio.Task] = None
|
|
40
|
-
|
|
41
|
-
async with _GITHUB_CACHE_LOCK:
|
|
42
|
-
entry = _GITHUB_CACHE.get(key) or {}
|
|
43
|
-
value = entry.get("value")
|
|
44
|
-
expires_at = float(entry.get("expires_at", 0) or 0)
|
|
45
|
-
task = entry.get("task")
|
|
46
|
-
|
|
47
|
-
if value is not None and expires_at > now:
|
|
48
|
-
return value
|
|
49
|
-
if task is None:
|
|
50
|
-
task = asyncio.create_task(
|
|
51
|
-
asyncio.to_thread(_github(request).status_payload)
|
|
52
|
-
)
|
|
53
|
-
_GITHUB_CACHE[key] = {
|
|
54
|
-
"value": value,
|
|
55
|
-
"expires_at": expires_at,
|
|
56
|
-
"task": task,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if task is None:
|
|
60
|
-
task = asyncio.create_task(asyncio.to_thread(_github(request).status_payload))
|
|
61
|
-
async with _GITHUB_CACHE_LOCK:
|
|
62
|
-
_GITHUB_CACHE[key] = {"task": task}
|
|
63
|
-
|
|
64
|
-
try:
|
|
65
|
-
value = await task
|
|
66
|
-
except Exception:
|
|
67
|
-
async with _GITHUB_CACHE_LOCK:
|
|
68
|
-
current = _GITHUB_CACHE.get(key) or {}
|
|
69
|
-
if current.get("task") is task:
|
|
70
|
-
_GITHUB_CACHE.pop(key, None)
|
|
71
|
-
raise
|
|
72
|
-
|
|
73
|
-
async with _GITHUB_CACHE_LOCK:
|
|
74
|
-
_GITHUB_CACHE[key] = {
|
|
75
|
-
"value": value,
|
|
76
|
-
"expires_at": now + ttl_seconds,
|
|
77
|
-
}
|
|
78
|
-
return value
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _github(request) -> GitHubService:
|
|
82
|
-
"""Get a GitHubService instance from the request."""
|
|
83
|
-
engine = request.app.state.engine
|
|
84
|
-
return GitHubService(engine.repo_root, raw_config=engine.config.raw)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _pr_flow(request: Request) -> PrFlowManager:
|
|
88
|
-
manager = getattr(request.app.state, "pr_flow_manager", None)
|
|
89
|
-
if manager is None:
|
|
90
|
-
engine = request.app.state.engine
|
|
91
|
-
manager = PrFlowManager(
|
|
92
|
-
engine.repo_root,
|
|
93
|
-
app_server_supervisor=getattr(
|
|
94
|
-
request.app.state, "app_server_supervisor", None
|
|
95
|
-
),
|
|
96
|
-
opencode_supervisor=getattr(request.app.state, "opencode_supervisor", None),
|
|
97
|
-
logger=getattr(request.app.state, "logger", None),
|
|
98
|
-
)
|
|
99
|
-
request.app.state.pr_flow_manager = manager
|
|
100
|
-
return manager
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def build_github_routes() -> APIRouter:
|
|
104
|
-
"""Build routes for GitHub integration."""
|
|
105
|
-
router = APIRouter()
|
|
106
|
-
|
|
107
|
-
@router.get("/api/github/status")
|
|
108
|
-
async def github_status(request: Request):
|
|
109
|
-
try:
|
|
110
|
-
return await _get_cached_status_payload(
|
|
111
|
-
request,
|
|
112
|
-
kind="status",
|
|
113
|
-
ttl_seconds=_GITHUB_STATUS_TTL_SECONDS,
|
|
114
|
-
)
|
|
115
|
-
except GitHubError as exc:
|
|
116
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
117
|
-
except Exception as exc:
|
|
118
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
119
|
-
|
|
120
|
-
@router.get("/api/github/pr")
|
|
121
|
-
async def github_pr(request: Request):
|
|
122
|
-
try:
|
|
123
|
-
status = await _get_cached_status_payload(
|
|
124
|
-
request,
|
|
125
|
-
kind="pr",
|
|
126
|
-
ttl_seconds=_GITHUB_PR_TTL_SECONDS,
|
|
127
|
-
)
|
|
128
|
-
return {
|
|
129
|
-
"status": "ok",
|
|
130
|
-
"git": status.get("git"),
|
|
131
|
-
"pr": status.get("pr"),
|
|
132
|
-
"links": status.get("pr_links"),
|
|
133
|
-
"link": status.get("link") or {},
|
|
134
|
-
}
|
|
135
|
-
except GitHubError as exc:
|
|
136
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
137
|
-
except Exception as exc:
|
|
138
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
139
|
-
|
|
140
|
-
@router.post("/api/github/link-issue")
|
|
141
|
-
async def github_link_issue(request: Request, payload: GithubIssueRequest):
|
|
142
|
-
issue = payload.issue
|
|
143
|
-
try:
|
|
144
|
-
state = await asyncio.to_thread(_github(request).link_issue, str(issue))
|
|
145
|
-
return {"status": "ok", "link": state}
|
|
146
|
-
except GitHubError as exc:
|
|
147
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
148
|
-
except Exception as exc:
|
|
149
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
150
|
-
|
|
151
|
-
@router.post("/api/github/spec/from-issue")
|
|
152
|
-
async def github_spec_from_issue(request: Request, payload: GithubIssueRequest):
|
|
153
|
-
issue = payload.issue
|
|
154
|
-
|
|
155
|
-
doc_chat = request.app.state.doc_chat
|
|
156
|
-
repo_blocked = doc_chat.repo_blocked_reason()
|
|
157
|
-
if repo_blocked:
|
|
158
|
-
raise HTTPException(status_code=409, detail=repo_blocked)
|
|
159
|
-
if doc_chat.doc_busy():
|
|
160
|
-
raise HTTPException(
|
|
161
|
-
status_code=409, detail="Doc chat already running for spec"
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
svc = _github(request)
|
|
165
|
-
try:
|
|
166
|
-
prompt, link_state = await asyncio.to_thread(
|
|
167
|
-
svc.build_spec_prompt_from_issue, str(issue)
|
|
168
|
-
)
|
|
169
|
-
doc_req = doc_chat.parse_request(
|
|
170
|
-
{"message": prompt, "stream": False}, kind="spec"
|
|
171
|
-
)
|
|
172
|
-
async with doc_chat.doc_lock():
|
|
173
|
-
result = await doc_chat.execute(doc_req)
|
|
174
|
-
if result.get("status") != "ok":
|
|
175
|
-
detail = result.get("detail") or "SPEC generation failed"
|
|
176
|
-
raise HTTPException(status_code=500, detail=detail)
|
|
177
|
-
result["github"] = {"issue": link_state.get("issue")}
|
|
178
|
-
return result
|
|
179
|
-
except GitHubError as exc:
|
|
180
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
181
|
-
except HTTPException:
|
|
182
|
-
raise
|
|
183
|
-
except Exception as exc:
|
|
184
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
185
|
-
|
|
186
|
-
@router.post("/api/github/pr/sync")
|
|
187
|
-
async def github_pr_sync(request: Request, payload: GithubPrSyncRequest):
|
|
188
|
-
if payload.mode is not None:
|
|
189
|
-
raise HTTPException(
|
|
190
|
-
status_code=400,
|
|
191
|
-
detail="Hub-only install: create a hub worktree repo instead of passing mode.",
|
|
192
|
-
)
|
|
193
|
-
draft = payload.draft
|
|
194
|
-
title = payload.title
|
|
195
|
-
body = payload.body
|
|
196
|
-
try:
|
|
197
|
-
return await asyncio.to_thread(
|
|
198
|
-
_github(request).sync_pr,
|
|
199
|
-
draft=draft,
|
|
200
|
-
title=str(title) if title else None,
|
|
201
|
-
body=str(body) if body else None,
|
|
202
|
-
)
|
|
203
|
-
except GitHubError as exc:
|
|
204
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
205
|
-
except Exception as exc:
|
|
206
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
207
|
-
|
|
208
|
-
@router.post("/api/github/context")
|
|
209
|
-
async def github_context(request: Request, payload: GithubContextRequest):
|
|
210
|
-
url = payload.url
|
|
211
|
-
try:
|
|
212
|
-
result = await asyncio.to_thread(
|
|
213
|
-
_github(request).build_context_file_from_url, str(url)
|
|
214
|
-
)
|
|
215
|
-
if not result:
|
|
216
|
-
return {"status": "ok", "injected": False}
|
|
217
|
-
return {"status": "ok", "injected": True, **result}
|
|
218
|
-
except GitHubError as exc:
|
|
219
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
220
|
-
except Exception as exc:
|
|
221
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
222
|
-
|
|
223
|
-
@router.get("/api/github/pr_flow/status")
|
|
224
|
-
async def github_pr_flow_status(request: Request):
|
|
225
|
-
try:
|
|
226
|
-
return {"status": "ok", "flow": _pr_flow(request).status()}
|
|
227
|
-
except PrFlowError as exc:
|
|
228
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
229
|
-
except Exception as exc:
|
|
230
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
231
|
-
|
|
232
|
-
@router.post("/api/github/pr_flow/start")
|
|
233
|
-
async def github_pr_flow_start(request: Request, payload: GithubPrFlowStartRequest):
|
|
234
|
-
try:
|
|
235
|
-
state = await asyncio.to_thread(
|
|
236
|
-
_pr_flow(request).start, payload=payload.model_dump()
|
|
237
|
-
)
|
|
238
|
-
return {"status": "ok", "flow": state}
|
|
239
|
-
except PrFlowError as exc:
|
|
240
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
241
|
-
except Exception as exc:
|
|
242
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
243
|
-
|
|
244
|
-
@router.post("/api/github/pr_flow/stop")
|
|
245
|
-
async def github_pr_flow_stop(
|
|
246
|
-
request: Request, _payload: GithubPrFlowActionRequest
|
|
247
|
-
):
|
|
248
|
-
try:
|
|
249
|
-
state = await asyncio.to_thread(_pr_flow(request).stop)
|
|
250
|
-
return {"status": "ok", "flow": state}
|
|
251
|
-
except PrFlowError as exc:
|
|
252
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
253
|
-
except Exception as exc:
|
|
254
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
255
|
-
|
|
256
|
-
@router.post("/api/github/pr_flow/resume")
|
|
257
|
-
async def github_pr_flow_resume(
|
|
258
|
-
request: Request, _payload: GithubPrFlowActionRequest
|
|
259
|
-
):
|
|
260
|
-
try:
|
|
261
|
-
state = await asyncio.to_thread(_pr_flow(request).resume)
|
|
262
|
-
return {"status": "ok", "flow": state}
|
|
263
|
-
except PrFlowError as exc:
|
|
264
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
265
|
-
except Exception as exc:
|
|
266
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
267
|
-
|
|
268
|
-
@router.post("/api/github/pr_flow/collect")
|
|
269
|
-
async def github_pr_flow_collect(
|
|
270
|
-
request: Request, _payload: GithubPrFlowActionRequest
|
|
271
|
-
):
|
|
272
|
-
try:
|
|
273
|
-
state = await asyncio.to_thread(_pr_flow(request).collect_reviews)
|
|
274
|
-
return {"status": "ok", "flow": state}
|
|
275
|
-
except PrFlowError as exc:
|
|
276
|
-
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
|
|
277
|
-
except Exception as exc:
|
|
278
|
-
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
279
|
-
|
|
280
|
-
@router.get("/api/github/pr_flow/events")
|
|
281
|
-
async def github_pr_flow_events(request: Request):
|
|
282
|
-
shutdown_event = getattr(request.app.state, "shutdown_event", None)
|
|
283
|
-
events_path = _pr_flow(request).events_path()
|
|
284
|
-
return StreamingResponse(
|
|
285
|
-
jsonl_event_stream(
|
|
286
|
-
events_path, event_name="pr-flow", shutdown_event=shutdown_event
|
|
287
|
-
),
|
|
288
|
-
headers=SSE_HEADERS,
|
|
289
|
-
media_type="text/event-stream",
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
@router.get("/api/github/pr_flow/artifact")
|
|
293
|
-
async def github_pr_flow_artifact(
|
|
294
|
-
request: Request,
|
|
295
|
-
kind: str = Query(..., description="review_bundle|workflow_log|final_report"),
|
|
296
|
-
):
|
|
297
|
-
flow = _pr_flow(request).status()
|
|
298
|
-
mapping = {
|
|
299
|
-
"review_bundle": flow.get("review_bundle_path"),
|
|
300
|
-
"workflow_log": flow.get("workflow_log_path"),
|
|
301
|
-
"final_report": flow.get("final_report_path"),
|
|
302
|
-
}
|
|
303
|
-
raw_path = mapping.get(kind)
|
|
304
|
-
if not raw_path:
|
|
305
|
-
raise HTTPException(status_code=404, detail="Artifact not found")
|
|
306
|
-
target = Path(raw_path).expanduser().resolve()
|
|
307
|
-
allowed_roots = [request.app.state.engine.repo_root.resolve()]
|
|
308
|
-
worktree_path = flow.get("worktree_path")
|
|
309
|
-
if isinstance(worktree_path, str) and worktree_path:
|
|
310
|
-
allowed_roots.append(Path(worktree_path).expanduser().resolve())
|
|
311
|
-
allowed = False
|
|
312
|
-
for root in allowed_roots:
|
|
313
|
-
try:
|
|
314
|
-
target.relative_to(root)
|
|
315
|
-
if ".codex-autorunner" in target.parts:
|
|
316
|
-
allowed = True
|
|
317
|
-
break
|
|
318
|
-
except ValueError:
|
|
319
|
-
continue
|
|
320
|
-
if not allowed or not target.exists():
|
|
321
|
-
raise HTTPException(status_code=404, detail="Artifact not found")
|
|
322
|
-
media_type = "text/plain"
|
|
323
|
-
if target.suffix == ".md":
|
|
324
|
-
media_type = "text/markdown"
|
|
325
|
-
return FileResponse(target, media_type=media_type, filename=target.name)
|
|
326
|
-
|
|
327
|
-
return router
|
codex_autorunner/routes/runs.py
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Run telemetry routes.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import time
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any, Optional
|
|
9
|
-
|
|
10
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
11
|
-
from fastapi.responses import FileResponse, StreamingResponse
|
|
12
|
-
|
|
13
|
-
from ..core.utils import is_within
|
|
14
|
-
from .shared import SSE_HEADERS, jsonl_event_stream
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _parse_iso(ts: Optional[str]) -> Optional[datetime]:
|
|
18
|
-
if not isinstance(ts, str):
|
|
19
|
-
return None
|
|
20
|
-
try:
|
|
21
|
-
return datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
|
22
|
-
except ValueError:
|
|
23
|
-
return None
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _token_total(entry: dict[str, Any]) -> Optional[float]:
|
|
27
|
-
token_usage = entry.get("token_usage")
|
|
28
|
-
if not isinstance(token_usage, dict):
|
|
29
|
-
return None
|
|
30
|
-
delta = token_usage.get("delta")
|
|
31
|
-
if isinstance(delta, dict):
|
|
32
|
-
for key in ("total_tokens", "totalTokens", "total"):
|
|
33
|
-
value = delta.get(key)
|
|
34
|
-
if isinstance(value, (int, float)):
|
|
35
|
-
return float(value)
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _extract_total_from_dict(token_dict: Optional[dict[str, Any]]) -> Optional[float]:
|
|
40
|
-
if token_dict is None:
|
|
41
|
-
return None
|
|
42
|
-
if not isinstance(token_dict, dict):
|
|
43
|
-
return None
|
|
44
|
-
for key in ("total_tokens", "totalTokens", "total"):
|
|
45
|
-
value = token_dict.get(key)
|
|
46
|
-
if isinstance(value, (int, float)):
|
|
47
|
-
return float(value)
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _completed_todo_count(entry: dict[str, Any]) -> int:
|
|
52
|
-
todo = entry.get("todo")
|
|
53
|
-
if not isinstance(todo, dict):
|
|
54
|
-
return 0
|
|
55
|
-
counts = todo.get("counts")
|
|
56
|
-
if isinstance(counts, dict):
|
|
57
|
-
value = counts.get("completed")
|
|
58
|
-
if isinstance(value, int):
|
|
59
|
-
return value
|
|
60
|
-
completed = todo.get("completed")
|
|
61
|
-
return len(completed) if isinstance(completed, list) else 0
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def build_runs_routes() -> APIRouter:
|
|
65
|
-
router = APIRouter()
|
|
66
|
-
|
|
67
|
-
@router.get("/api/runs")
|
|
68
|
-
def list_runs(request: Request, limit: int = 200):
|
|
69
|
-
engine = request.app.state.engine
|
|
70
|
-
engine.reconcile_run_index()
|
|
71
|
-
index = engine._load_run_index()
|
|
72
|
-
entries: list[dict[str, Any]] = []
|
|
73
|
-
for key, entry in index.items():
|
|
74
|
-
try:
|
|
75
|
-
run_id = int(key)
|
|
76
|
-
except (TypeError, ValueError):
|
|
77
|
-
continue
|
|
78
|
-
if not isinstance(entry, dict):
|
|
79
|
-
continue
|
|
80
|
-
started = _parse_iso(entry.get("started_at"))
|
|
81
|
-
finished = _parse_iso(entry.get("finished_at"))
|
|
82
|
-
duration = None
|
|
83
|
-
if started and finished:
|
|
84
|
-
duration = (finished - started).total_seconds()
|
|
85
|
-
enriched = dict(entry)
|
|
86
|
-
enriched["run_id"] = run_id
|
|
87
|
-
enriched["duration_seconds"] = duration
|
|
88
|
-
enriched["token_total"] = _token_total(entry)
|
|
89
|
-
enriched["completed_todo_count"] = _completed_todo_count(entry)
|
|
90
|
-
entries.append(enriched)
|
|
91
|
-
entries.sort(key=lambda item: item.get("run_id", 0), reverse=True)
|
|
92
|
-
capped = entries[: max(1, min(int(limit), 1000))]
|
|
93
|
-
return {"runs": capped}
|
|
94
|
-
|
|
95
|
-
@router.get("/api/runs/{run_id}/plan")
|
|
96
|
-
def fetch_run_plan(request: Request, run_id: int):
|
|
97
|
-
engine = request.app.state.engine
|
|
98
|
-
entry = engine._load_run_index().get(str(run_id))
|
|
99
|
-
if not isinstance(entry, dict):
|
|
100
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
101
|
-
artifacts = entry.get("artifacts")
|
|
102
|
-
if not isinstance(artifacts, dict):
|
|
103
|
-
raise HTTPException(status_code=404, detail="Plan not found")
|
|
104
|
-
plan_path = artifacts.get("plan_path")
|
|
105
|
-
if not isinstance(plan_path, str) or not plan_path:
|
|
106
|
-
raise HTTPException(status_code=404, detail="Plan not found")
|
|
107
|
-
path = Path(plan_path)
|
|
108
|
-
if not is_within(engine.repo_root, path):
|
|
109
|
-
raise HTTPException(status_code=400, detail="Invalid plan path")
|
|
110
|
-
if not path.exists():
|
|
111
|
-
raise HTTPException(status_code=404, detail="Plan not found")
|
|
112
|
-
return FileResponse(path, media_type="application/json")
|
|
113
|
-
|
|
114
|
-
@router.get("/api/runs/{run_id}/diff")
|
|
115
|
-
def fetch_run_diff(request: Request, run_id: int):
|
|
116
|
-
engine = request.app.state.engine
|
|
117
|
-
entry = engine._load_run_index().get(str(run_id))
|
|
118
|
-
if not isinstance(entry, dict):
|
|
119
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
120
|
-
artifacts = entry.get("artifacts")
|
|
121
|
-
if not isinstance(artifacts, dict):
|
|
122
|
-
raise HTTPException(status_code=404, detail="Diff not found")
|
|
123
|
-
diff_path = artifacts.get("diff_path")
|
|
124
|
-
if not isinstance(diff_path, str) or not diff_path:
|
|
125
|
-
raise HTTPException(status_code=404, detail="Diff not found")
|
|
126
|
-
path = Path(diff_path)
|
|
127
|
-
if not is_within(engine.repo_root, path):
|
|
128
|
-
raise HTTPException(status_code=400, detail="Invalid diff path")
|
|
129
|
-
if not path.exists():
|
|
130
|
-
raise HTTPException(status_code=404, detail="Diff not found")
|
|
131
|
-
return FileResponse(path, media_type="text/plain")
|
|
132
|
-
|
|
133
|
-
@router.get("/api/runs/{run_id}/output")
|
|
134
|
-
def fetch_run_output(request: Request, run_id: int):
|
|
135
|
-
engine = request.app.state.engine
|
|
136
|
-
entry = engine._load_run_index().get(str(run_id))
|
|
137
|
-
if not isinstance(entry, dict):
|
|
138
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
139
|
-
artifacts = entry.get("artifacts")
|
|
140
|
-
if not isinstance(artifacts, dict):
|
|
141
|
-
raise HTTPException(status_code=404, detail="Output not found")
|
|
142
|
-
output_path = artifacts.get("output_path")
|
|
143
|
-
if not isinstance(output_path, str) or not output_path:
|
|
144
|
-
raise HTTPException(status_code=404, detail="Output not found")
|
|
145
|
-
path = Path(output_path)
|
|
146
|
-
if not is_within(engine.repo_root, path):
|
|
147
|
-
raise HTTPException(status_code=400, detail="Invalid output path")
|
|
148
|
-
if not path.exists():
|
|
149
|
-
raise HTTPException(status_code=404, detail="Output not found")
|
|
150
|
-
return FileResponse(path, media_type="text/plain")
|
|
151
|
-
|
|
152
|
-
@router.get("/api/runs/{run_id}/telemetry")
|
|
153
|
-
def fetch_run_telemetry(request: Request, run_id: int):
|
|
154
|
-
engine = request.app.state.engine
|
|
155
|
-
telemetry = engine._snapshot_run_telemetry(run_id)
|
|
156
|
-
if telemetry is None:
|
|
157
|
-
entry = engine._load_run_index().get(str(run_id))
|
|
158
|
-
if not isinstance(entry, dict):
|
|
159
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
160
|
-
token_usage = entry.get("token_usage")
|
|
161
|
-
if isinstance(token_usage, dict):
|
|
162
|
-
delta = token_usage.get("delta")
|
|
163
|
-
thread_total = token_usage.get("thread_total_after")
|
|
164
|
-
else:
|
|
165
|
-
delta = None
|
|
166
|
-
thread_total = None
|
|
167
|
-
return {
|
|
168
|
-
"run_id": run_id,
|
|
169
|
-
"status": "completed",
|
|
170
|
-
"thread_id": None,
|
|
171
|
-
"turn_id": None,
|
|
172
|
-
"token_delta": delta,
|
|
173
|
-
"token_total": thread_total,
|
|
174
|
-
"total_tokens": _extract_total_from_dict(delta),
|
|
175
|
-
"updated_at": None,
|
|
176
|
-
}
|
|
177
|
-
token_total = telemetry.token_total
|
|
178
|
-
total_tokens = _extract_total_from_dict(token_total)
|
|
179
|
-
return {
|
|
180
|
-
"run_id": run_id,
|
|
181
|
-
"status": "active",
|
|
182
|
-
"thread_id": telemetry.thread_id,
|
|
183
|
-
"turn_id": telemetry.turn_id,
|
|
184
|
-
"token_delta": None,
|
|
185
|
-
"token_total": token_total,
|
|
186
|
-
"total_tokens": total_tokens,
|
|
187
|
-
"updated_at": time.time(),
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
@router.get("/api/runs/{run_id}/events/stream")
|
|
191
|
-
async def stream_run_events(request: Request, run_id: int):
|
|
192
|
-
engine = request.app.state.engine
|
|
193
|
-
entry = engine._load_run_index().get(str(run_id))
|
|
194
|
-
if not isinstance(entry, dict):
|
|
195
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
196
|
-
events_path = engine._events_log_path(run_id)
|
|
197
|
-
shutdown_event = getattr(request.app.state, "shutdown_event", None)
|
|
198
|
-
return StreamingResponse(
|
|
199
|
-
jsonl_event_stream(
|
|
200
|
-
events_path,
|
|
201
|
-
event_name="event",
|
|
202
|
-
shutdown_event=shutdown_event,
|
|
203
|
-
),
|
|
204
|
-
media_type="text/event-stream",
|
|
205
|
-
headers=SSE_HEADERS,
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
@router.get("/api/runs/{run_id}/final_review")
|
|
209
|
-
def fetch_final_review(request: Request, run_id: int):
|
|
210
|
-
engine = request.app.state.engine
|
|
211
|
-
entry = engine._load_run_index().get(str(run_id))
|
|
212
|
-
if not isinstance(entry, dict):
|
|
213
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
214
|
-
artifacts = entry.get("artifacts")
|
|
215
|
-
if not isinstance(artifacts, dict):
|
|
216
|
-
raise HTTPException(status_code=404, detail="Review not found")
|
|
217
|
-
report_path = artifacts.get("final_review_report_path")
|
|
218
|
-
if not isinstance(report_path, str) or not report_path:
|
|
219
|
-
raise HTTPException(status_code=404, detail="Review not found")
|
|
220
|
-
path = Path(report_path)
|
|
221
|
-
if not is_within(engine.repo_root, path):
|
|
222
|
-
raise HTTPException(status_code=400, detail="Invalid review path")
|
|
223
|
-
if not path.exists():
|
|
224
|
-
raise HTTPException(status_code=404, detail="Review not found")
|
|
225
|
-
media_type = "text/markdown" if path.suffix == ".md" else "text/plain"
|
|
226
|
-
return FileResponse(path, media_type=media_type)
|
|
227
|
-
|
|
228
|
-
@router.get("/api/runs/{run_id}/final_review_scratchpad")
|
|
229
|
-
def fetch_final_review_scratchpad(request: Request, run_id: int):
|
|
230
|
-
engine = request.app.state.engine
|
|
231
|
-
entry = engine._load_run_index().get(str(run_id))
|
|
232
|
-
if not isinstance(entry, dict):
|
|
233
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
234
|
-
artifacts = entry.get("artifacts")
|
|
235
|
-
if not isinstance(artifacts, dict):
|
|
236
|
-
raise HTTPException(status_code=404, detail="Review scratchpad not found")
|
|
237
|
-
bundle_path = artifacts.get("final_review_scratchpad_bundle_path")
|
|
238
|
-
if not isinstance(bundle_path, str) or not bundle_path:
|
|
239
|
-
raise HTTPException(status_code=404, detail="Review scratchpad not found")
|
|
240
|
-
path = Path(bundle_path)
|
|
241
|
-
if not is_within(engine.repo_root, path):
|
|
242
|
-
raise HTTPException(status_code=400, detail="Invalid scratchpad path")
|
|
243
|
-
if not path.exists():
|
|
244
|
-
raise HTTPException(status_code=404, detail="Review scratchpad not found")
|
|
245
|
-
media_type = (
|
|
246
|
-
"application/zip" if path.suffix == ".zip" else "application/octet-stream"
|
|
247
|
-
)
|
|
248
|
-
return FileResponse(path, media_type=media_type)
|
|
249
|
-
|
|
250
|
-
return router
|