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.
Files changed (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {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
@@ -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