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
@@ -9,7 +9,6 @@ import re
9
9
  import time
10
10
  from contextlib import suppress
11
11
  from dataclasses import dataclass
12
- from os import getenv
13
12
  from pathlib import Path
14
13
  from typing import TYPE_CHECKING, Any, Optional
15
14
 
@@ -26,20 +25,14 @@ from .....agents.opencode.runtime import (
26
25
  opencode_missing_env,
27
26
  split_model_id,
28
27
  )
29
- from .....core.config import load_hub_config, load_repo_config
30
28
  from .....core.logging_utils import log_event
31
29
  from .....core.state import now_iso
32
30
  from .....core.text_delta_coalescer import TextDeltaCoalescer
33
- from .....core.utils import canonicalize_path
34
- from .....integrations.github.service import GitHubError, GitHubService
35
- from .....manifest import load_manifest
36
31
  from ....app_server.client import (
37
32
  CodexAppServerDisconnected,
38
33
  )
39
34
  from ...adapter import (
40
35
  InlineButton,
41
- PrFlowStartCallback,
42
- TelegramCallbackQuery,
43
36
  TelegramMessage,
44
37
  build_inline_keyboard,
45
38
  encode_cancel_callback,
@@ -47,7 +40,6 @@ from ...adapter import (
47
40
  from ...config import AppServerUnavailableError
48
41
  from ...constants import (
49
42
  MAX_TOPIC_THREAD_HISTORY,
50
- OPENCODE_TURN_TIMEOUT_SECONDS,
51
43
  PLACEHOLDER_TEXT,
52
44
  QUEUED_PLACEHOLDER_TEXT,
53
45
  RESUME_PREVIEW_ASSISTANT_LIMIT,
@@ -411,7 +403,7 @@ class GitHubCommands(SharedHelpers):
411
403
  result = await self._wait_for_turn_result(
412
404
  setup.client,
413
405
  turn_context.turn_handle,
414
- timeout_seconds=self._config.app_server_turn_timeout_seconds,
406
+ timeout_seconds=self._config.agent_turn_timeout_seconds.get("codex"),
415
407
  topic_key=topic_key,
416
408
  chat_id=message.chat_id,
417
409
  thread_id=message.thread_id,
@@ -1212,6 +1204,7 @@ class GitHubCommands(SharedHelpers):
1212
1204
  setup.client,
1213
1205
  session_id=setup.review_session_id,
1214
1206
  workspace_path=str(setup.workspace_root),
1207
+ model_payload=model_payload,
1215
1208
  progress_session_ids=watched_session_ids,
1216
1209
  permission_policy=setup.permission_policy,
1217
1210
  permission_handler=(
@@ -1226,9 +1219,12 @@ class GitHubCommands(SharedHelpers):
1226
1219
  )
1227
1220
  with suppress(asyncio.TimeoutError):
1228
1221
  await asyncio.wait_for(ready_event.wait(), timeout=2.0)
1229
- timeout_task = asyncio.create_task(
1230
- asyncio.sleep(OPENCODE_TURN_TIMEOUT_SECONDS)
1222
+ timeout_seconds = self._config.agent_turn_timeout_seconds.get(
1223
+ "opencode"
1231
1224
  )
1225
+ timeout_task: Optional[asyncio.Task] = None
1226
+ if timeout_seconds is not None and timeout_seconds > 0:
1227
+ timeout_task = asyncio.create_task(asyncio.sleep(timeout_seconds))
1232
1228
  command_task = asyncio.create_task(
1233
1229
  setup.client.send_command(
1234
1230
  setup.review_session_id,
@@ -1240,45 +1236,47 @@ class GitHubCommands(SharedHelpers):
1240
1236
  try:
1241
1237
  await command_task
1242
1238
  except Exception as exc:
1243
- timeout_task.cancel()
1244
- with suppress(asyncio.CancelledError):
1245
- await timeout_task
1239
+ if timeout_task is not None:
1240
+ timeout_task.cancel()
1241
+ with suppress(asyncio.CancelledError):
1242
+ await timeout_task
1246
1243
  output_task.cancel()
1247
1244
  with suppress(asyncio.CancelledError):
1248
1245
  await output_task
1249
1246
  raise exc
1250
- done, _pending = await asyncio.wait(
1251
- {output_task, timeout_task},
1252
- return_when=asyncio.FIRST_COMPLETED,
1253
- )
1254
- if timeout_task in done:
1255
- runtime.interrupt_requested = True
1256
- await _abort_opencode()
1257
- output_task.cancel()
1258
- with suppress(asyncio.CancelledError):
1259
- await output_task
1247
+ if timeout_task is not None:
1248
+ done, _pending = await asyncio.wait(
1249
+ {output_task, timeout_task},
1250
+ return_when=asyncio.FIRST_COMPLETED,
1251
+ )
1252
+ if timeout_task in done:
1253
+ runtime.interrupt_requested = True
1254
+ await _abort_opencode()
1255
+ output_task.cancel()
1256
+ with suppress(asyncio.CancelledError):
1257
+ await output_task
1258
+ timeout_task.cancel()
1259
+ with suppress(asyncio.CancelledError):
1260
+ await timeout_task
1261
+ turn_context.turn_elapsed_seconds = (
1262
+ time.monotonic() - turn_started_at
1263
+ if turn_started_at is not None
1264
+ else None
1265
+ )
1266
+ failure_message = "OpenCode review timed out."
1267
+ response_sent = await self._deliver_turn_response(
1268
+ chat_id=message.chat_id,
1269
+ thread_id=message.thread_id,
1270
+ reply_to=message.message_id,
1271
+ placeholder_id=placeholder_id,
1272
+ response=failure_message,
1273
+ )
1274
+ if response_sent:
1275
+ await self._delete_message(message.chat_id, placeholder_id)
1276
+ return turn_context, None
1260
1277
  timeout_task.cancel()
1261
1278
  with suppress(asyncio.CancelledError):
1262
1279
  await timeout_task
1263
- turn_context.turn_elapsed_seconds = (
1264
- time.monotonic() - turn_started_at
1265
- if turn_started_at is not None
1266
- else None
1267
- )
1268
- failure_message = "OpenCode review timed out."
1269
- response_sent = await self._deliver_turn_response(
1270
- chat_id=message.chat_id,
1271
- thread_id=message.thread_id,
1272
- reply_to=message.message_id,
1273
- placeholder_id=placeholder_id,
1274
- response=failure_message,
1275
- )
1276
- if response_sent:
1277
- await self._delete_message(message.chat_id, placeholder_id)
1278
- return turn_context, None
1279
- timeout_task.cancel()
1280
- with suppress(asyncio.CancelledError):
1281
- await timeout_task
1282
1280
  output_result = await output_task
1283
1281
  elapsed = (
1284
1282
  time.monotonic() - turn_started_at
@@ -1563,520 +1561,6 @@ class GitHubCommands(SharedHelpers):
1563
1561
  delivery=delivery,
1564
1562
  )
1565
1563
 
1566
- def _resolve_pr_flow_repo_id(self, record: "TelegramTopicRecord") -> Optional[str]:
1567
- if record.repo_id:
1568
- return record.repo_id
1569
- if not self._hub_root or not self._manifest_path or not record.workspace_path:
1570
- return None
1571
- try:
1572
- manifest = load_manifest(self._manifest_path, self._hub_root)
1573
- except Exception:
1574
- return None
1575
- try:
1576
- workspace_path = canonicalize_path(Path(record.workspace_path))
1577
- except Exception:
1578
- return None
1579
- for repo in manifest.repos:
1580
- repo_path = canonicalize_path(self._hub_root / repo.path)
1581
- if repo_path == workspace_path:
1582
- return repo.id
1583
- return None
1584
-
1585
- def _pr_flow_api_base(
1586
- self, record: "TelegramTopicRecord"
1587
- ) -> tuple[Optional[str], dict[str, str]]:
1588
- headers: dict[str, str] = {}
1589
- if self._hub_root is not None:
1590
- try:
1591
- hub_config = load_hub_config(self._hub_root)
1592
- except Exception:
1593
- return None, headers
1594
- host = hub_config.server_host
1595
- port = hub_config.server_port
1596
- base_path = hub_config.server_base_path
1597
- auth_env = hub_config.server_auth_token_env
1598
- repo_id = self._resolve_pr_flow_repo_id(record)
1599
- if not repo_id:
1600
- return None, headers
1601
- repo_prefix = f"/repos/{repo_id}"
1602
- else:
1603
- if not record.workspace_path:
1604
- return None, headers
1605
- try:
1606
- repo_config = load_repo_config(
1607
- Path(record.workspace_path), hub_path=None
1608
- )
1609
- except Exception:
1610
- return None, headers
1611
- host = repo_config.server_host
1612
- port = repo_config.server_port
1613
- base_path = repo_config.server_base_path
1614
- auth_env = repo_config.server_auth_token_env
1615
- repo_prefix = ""
1616
- if isinstance(auth_env, str) and auth_env:
1617
- token = getenv(auth_env)
1618
- if token:
1619
- headers["Authorization"] = f"Bearer {token}"
1620
- if not host:
1621
- return None, headers
1622
- if host.startswith("http://") or host.startswith("https://"):
1623
- base = host.rstrip("/")
1624
- else:
1625
- base = f"http://{host}:{int(port)}"
1626
- base_path = (base_path or "").strip("/")
1627
- if base_path:
1628
- base = f"{base}/{base_path}"
1629
- return f"{base}{repo_prefix}", headers
1630
-
1631
- async def _pr_flow_request(
1632
- self,
1633
- record: "TelegramTopicRecord",
1634
- *,
1635
- method: str,
1636
- path: str,
1637
- payload: Optional[dict[str, Any]] = None,
1638
- ) -> dict[str, Any]:
1639
- base, headers = self._pr_flow_api_base(record)
1640
- if not base:
1641
- raise RuntimeError(
1642
- "PR flow cannot start: repo server base URL could not be resolved for this chat/topic."
1643
- )
1644
- url = f"{base}{path}"
1645
- async with httpx.AsyncClient(timeout=30.0) as client:
1646
- res = await client.request(method, url, json=payload, headers=headers)
1647
- res.raise_for_status()
1648
- data = res.json()
1649
- if isinstance(data, dict):
1650
- return data
1651
- return {"status": "ok", "flow": data}
1652
-
1653
- def _parse_pr_flags(self, argv: list[str]) -> tuple[Optional[str], dict[str, Any]]:
1654
- ref: Optional[str] = None
1655
- flags: dict[str, Any] = {}
1656
- idx = 0
1657
- while idx < len(argv):
1658
- token = argv[idx]
1659
- if token.startswith("--"):
1660
- if token == "--draft":
1661
- flags["draft"] = True
1662
- idx += 1
1663
- continue
1664
- if token == "--ready":
1665
- flags["draft"] = False
1666
- idx += 1
1667
- continue
1668
- if token == "--base" and idx + 1 < len(argv):
1669
- flags["base_branch"] = argv[idx + 1]
1670
- idx += 2
1671
- continue
1672
- if token == "--until" and idx + 1 < len(argv):
1673
- until = argv[idx + 1].strip().lower()
1674
- if until in ("minor", "minor_only"):
1675
- flags["stop_condition"] = "minor_only"
1676
- elif until in ("clean", "no_issues"):
1677
- flags["stop_condition"] = "no_issues"
1678
- idx += 2
1679
- continue
1680
- if token in ("--max-cycles", "--max_cycles") and idx + 1 < len(argv):
1681
- try:
1682
- flags["max_cycles"] = int(argv[idx + 1])
1683
- except ValueError:
1684
- pass
1685
- idx += 2
1686
- continue
1687
- if token in ("--max-runs", "--max_runs") and idx + 1 < len(argv):
1688
- try:
1689
- flags["max_implementation_runs"] = int(argv[idx + 1])
1690
- except ValueError:
1691
- pass
1692
- idx += 2
1693
- continue
1694
- if token in ("--timeout", "--timeout-seconds") and idx + 1 < len(argv):
1695
- try:
1696
- flags["max_wallclock_seconds"] = int(argv[idx + 1])
1697
- except ValueError:
1698
- pass
1699
- idx += 2
1700
- continue
1701
- idx += 1
1702
- continue
1703
- if ref is None:
1704
- ref = token
1705
- idx += 1
1706
- return ref, flags
1707
-
1708
- def _format_pr_flow_status(self, flow: dict[str, Any]) -> str:
1709
- status = flow.get("status") or "unknown"
1710
- step = flow.get("step") or "unknown"
1711
- cycle = flow.get("cycle") or 0
1712
- pr_url = flow.get("pr_url") or ""
1713
- lines = [f"PR flow: {status} (step: {step}, cycle: {cycle})"]
1714
- if pr_url:
1715
- lines.append(f"PR: {pr_url}")
1716
- return "\n".join(lines)
1717
-
1718
- async def _handle_github_issue_url(
1719
- self,
1720
- message: TelegramMessage,
1721
- key: str,
1722
- slug: str,
1723
- number: int,
1724
- ) -> None:
1725
- if key is None:
1726
- return
1727
-
1728
- record = await self._router.get_topic(key)
1729
- if record is None or not record.workspace_path:
1730
- await self._send_message(
1731
- message.chat_id,
1732
- self._with_conversation_id(
1733
- "Topic not bound. Use /bind <repo_id> or /bind <path>.",
1734
- chat_id=message.chat_id,
1735
- thread_id=message.thread_id,
1736
- ),
1737
- thread_id=message.thread_id,
1738
- reply_to=message.message_id,
1739
- )
1740
- return
1741
-
1742
- try:
1743
- from pathlib import Path
1744
-
1745
- service = GitHubService(Path(record.workspace_path), self._raw_config)
1746
- issue_ref = f"{slug}#{number}"
1747
- service.validate_issue_same_repo(issue_ref)
1748
- except GitHubError as exc:
1749
- await self._send_message(
1750
- message.chat_id,
1751
- str(exc),
1752
- thread_id=message.thread_id,
1753
- reply_to=message.message_id,
1754
- )
1755
- return
1756
-
1757
- await self._offer_pr_flow_start(message, record, slug, number)
1758
-
1759
- async def _offer_pr_flow_start(
1760
- self,
1761
- message: TelegramMessage,
1762
- record: "TelegramTopicRecord",
1763
- slug: str,
1764
- number: int,
1765
- ) -> None:
1766
- from ...adapter import (
1767
- InlineButton,
1768
- build_inline_keyboard,
1769
- encode_cancel_callback,
1770
- encode_pr_flow_start_callback,
1771
- )
1772
-
1773
- keyboard = build_inline_keyboard(
1774
- [
1775
- [
1776
- InlineButton(
1777
- f"Create PR for #{number}",
1778
- encode_pr_flow_start_callback(slug, number),
1779
- ),
1780
- InlineButton(
1781
- "Cancel",
1782
- encode_cancel_callback("pr_flow_offer"),
1783
- ),
1784
- ]
1785
- ]
1786
- )
1787
- await self._send_message(
1788
- message.chat_id,
1789
- f"Detected GitHub issue: {slug}#{number}\nStart PR flow to create a PR?",
1790
- thread_id=message.thread_id,
1791
- reply_to=message.message_id,
1792
- reply_markup=keyboard,
1793
- )
1794
-
1795
- async def _handle_pr_flow_start_callback(
1796
- self,
1797
- key: str,
1798
- callback: TelegramCallbackQuery,
1799
- parsed: PrFlowStartCallback,
1800
- ) -> None:
1801
- from ...adapter import TelegramMessage
1802
-
1803
- await self._answer_callback(callback)
1804
- record = await self._router.get_topic(key)
1805
- if record is None or not record.workspace_path:
1806
- return
1807
-
1808
- issue_ref = f"{parsed.slug}#{parsed.number}"
1809
- payload = {"mode": "issue", "issue": issue_ref}
1810
- payload["source"] = "telegram"
1811
- source_meta: dict[str, Any] = {}
1812
- if callback.chat_id is not None:
1813
- source_meta["chat_id"] = callback.chat_id
1814
- if callback.thread_id is not None:
1815
- source_meta["thread_id"] = callback.thread_id
1816
- if source_meta:
1817
- payload["source_meta"] = source_meta
1818
-
1819
- message = TelegramMessage(
1820
- update_id=callback.update_id,
1821
- message_id=callback.message_id or 0,
1822
- chat_id=callback.chat_id or 0,
1823
- thread_id=callback.thread_id,
1824
- from_user_id=callback.from_user_id,
1825
- text="",
1826
- date=None,
1827
- is_topic_message=False,
1828
- )
1829
-
1830
- try:
1831
- data = await self._pr_flow_request(
1832
- record,
1833
- method="POST",
1834
- path="/api/github/pr_flow/start",
1835
- payload=payload,
1836
- )
1837
- flow = data.get("flow") if isinstance(data, dict) else data
1838
- except Exception as exc:
1839
- detail = _format_httpx_exception(exc) or str(exc)
1840
- await self._send_message(
1841
- message.chat_id,
1842
- f"PR flow error: {detail}",
1843
- thread_id=message.thread_id,
1844
- reply_to=callback.message_id,
1845
- )
1846
- return
1847
- await self._send_message(
1848
- message.chat_id,
1849
- self._format_pr_flow_status(flow),
1850
- thread_id=message.thread_id,
1851
- reply_to=callback.message_id,
1852
- )
1853
-
1854
- async def _handle_pr(
1855
- self, message: TelegramMessage, args: str, runtime: Any
1856
- ) -> None:
1857
- record = await self._require_bound_record(message)
1858
- if not record:
1859
- return
1860
- argv = self._parse_command_args(args)
1861
- if not argv:
1862
- await self._send_message(
1863
- message.chat_id,
1864
- "Usage: /pr start <issueRef> | /pr fix <prRef> | /pr status | /pr stop | /pr resume | /pr collect",
1865
- thread_id=message.thread_id,
1866
- reply_to=message.message_id,
1867
- )
1868
- return
1869
- command = argv[0].lower()
1870
- if command == "status":
1871
- try:
1872
- data = await self._pr_flow_request(
1873
- record, method="GET", path="/api/github/pr_flow/status"
1874
- )
1875
- flow = data.get("flow") if isinstance(data, dict) else data
1876
- except Exception as exc:
1877
- detail = _format_httpx_exception(exc) or str(exc)
1878
- await self._send_message(
1879
- message.chat_id,
1880
- f"PR flow error: {detail}",
1881
- thread_id=message.thread_id,
1882
- reply_to=message.message_id,
1883
- )
1884
- return
1885
- await self._send_message(
1886
- message.chat_id,
1887
- self._format_pr_flow_status(flow),
1888
- thread_id=message.thread_id,
1889
- reply_to=message.message_id,
1890
- )
1891
- return
1892
- if command == "stop":
1893
- try:
1894
- data = await self._pr_flow_request(
1895
- record, method="POST", path="/api/github/pr_flow/stop", payload={}
1896
- )
1897
- flow = data.get("flow") if isinstance(data, dict) else data
1898
- except Exception as exc:
1899
- detail = _format_httpx_exception(exc) or str(exc)
1900
- await self._send_message(
1901
- message.chat_id,
1902
- f"PR flow error: {detail}",
1903
- thread_id=message.thread_id,
1904
- reply_to=message.message_id,
1905
- )
1906
- return
1907
- await self._send_message(
1908
- message.chat_id,
1909
- self._format_pr_flow_status(flow),
1910
- thread_id=message.thread_id,
1911
- reply_to=message.message_id,
1912
- )
1913
- return
1914
- if command == "resume":
1915
- try:
1916
- data = await self._pr_flow_request(
1917
- record, method="POST", path="/api/github/pr_flow/resume", payload={}
1918
- )
1919
- flow = data.get("flow") if isinstance(data, dict) else data
1920
- except Exception as exc:
1921
- detail = _format_httpx_exception(exc) or str(exc)
1922
- await self._send_message(
1923
- message.chat_id,
1924
- f"PR flow error: {detail}",
1925
- thread_id=message.thread_id,
1926
- reply_to=message.message_id,
1927
- )
1928
- return
1929
- await self._send_message(
1930
- message.chat_id,
1931
- self._format_pr_flow_status(flow),
1932
- thread_id=message.thread_id,
1933
- reply_to=message.message_id,
1934
- )
1935
- return
1936
- if command == "collect":
1937
- try:
1938
- data = await self._pr_flow_request(
1939
- record,
1940
- method="POST",
1941
- path="/api/github/pr_flow/collect",
1942
- payload={},
1943
- )
1944
- flow = data.get("flow") if isinstance(data, dict) else data
1945
- except Exception as exc:
1946
- detail = _format_httpx_exception(exc) or str(exc)
1947
- await self._send_message(
1948
- message.chat_id,
1949
- f"PR flow error: {detail}",
1950
- thread_id=message.thread_id,
1951
- reply_to=message.message_id,
1952
- )
1953
- return
1954
- await self._send_message(
1955
- message.chat_id,
1956
- self._format_pr_flow_status(flow),
1957
- thread_id=message.thread_id,
1958
- reply_to=message.message_id,
1959
- )
1960
- return
1961
- if command in ("start", "implement"):
1962
- ref, flags = self._parse_pr_flags(argv[1:])
1963
- if not ref:
1964
- gh = GitHubService(Path(record.workspace_path))
1965
- issues = await asyncio.to_thread(gh.list_open_issues, limit=5)
1966
- if issues:
1967
- lines = ["Open issues:"]
1968
- for issue in issues:
1969
- num = issue.get("number")
1970
- title = issue.get("title") or ""
1971
- lines.append(f"- #{num} {title}".strip())
1972
- lines.append("Use /pr start <issueRef> to begin.")
1973
- await self._send_message(
1974
- message.chat_id,
1975
- "\n".join(lines),
1976
- thread_id=message.thread_id,
1977
- reply_to=message.message_id,
1978
- )
1979
- return
1980
- await self._send_message(
1981
- message.chat_id,
1982
- "Usage: /pr start <issueRef>",
1983
- thread_id=message.thread_id,
1984
- reply_to=message.message_id,
1985
- )
1986
- return
1987
- payload = {"mode": "issue", "issue": ref, **flags}
1988
- payload["source"] = "telegram"
1989
- payload["source_meta"] = {
1990
- "chat_id": message.chat_id,
1991
- "thread_id": message.thread_id,
1992
- }
1993
- try:
1994
- data = await self._pr_flow_request(
1995
- record,
1996
- method="POST",
1997
- path="/api/github/pr_flow/start",
1998
- payload=payload,
1999
- )
2000
- flow = data.get("flow") if isinstance(data, dict) else data
2001
- except Exception as exc:
2002
- detail = _format_httpx_exception(exc) or str(exc)
2003
- await self._send_message(
2004
- message.chat_id,
2005
- f"PR flow error: {detail}",
2006
- thread_id=message.thread_id,
2007
- reply_to=message.message_id,
2008
- )
2009
- return
2010
- await self._send_message(
2011
- message.chat_id,
2012
- self._format_pr_flow_status(flow),
2013
- thread_id=message.thread_id,
2014
- reply_to=message.message_id,
2015
- )
2016
- return
2017
- if command in ("fix", "pr"):
2018
- ref, flags = self._parse_pr_flags(argv[1:])
2019
- if not ref:
2020
- gh = GitHubService(Path(record.workspace_path))
2021
- prs = await asyncio.to_thread(gh.list_open_prs, limit=5)
2022
- if prs:
2023
- lines = ["Open PRs:"]
2024
- for pr in prs:
2025
- num = pr.get("number")
2026
- title = pr.get("title") or ""
2027
- lines.append(f"- #{num} {title}".strip())
2028
- lines.append("Use /pr fix <prRef> to begin.")
2029
- await self._send_message(
2030
- message.chat_id,
2031
- "\n".join(lines),
2032
- thread_id=message.thread_id,
2033
- reply_to=message.message_id,
2034
- )
2035
- return
2036
- await self._send_message(
2037
- message.chat_id,
2038
- "Usage: /pr fix <prRef>",
2039
- thread_id=message.thread_id,
2040
- reply_to=message.message_id,
2041
- )
2042
- return
2043
- payload = {"mode": "pr", "pr": ref, **flags}
2044
- payload["source"] = "telegram"
2045
- payload["source_meta"] = {
2046
- "chat_id": message.chat_id,
2047
- "thread_id": message.thread_id,
2048
- }
2049
- try:
2050
- data = await self._pr_flow_request(
2051
- record,
2052
- method="POST",
2053
- path="/api/github/pr_flow/start",
2054
- payload=payload,
2055
- )
2056
- flow = data.get("flow") if isinstance(data, dict) else data
2057
- except Exception as exc:
2058
- detail = _format_httpx_exception(exc) or str(exc)
2059
- await self._send_message(
2060
- message.chat_id,
2061
- f"PR flow error: {detail}",
2062
- thread_id=message.thread_id,
2063
- reply_to=message.message_id,
2064
- )
2065
- return
2066
- await self._send_message(
2067
- message.chat_id,
2068
- self._format_pr_flow_status(flow),
2069
- thread_id=message.thread_id,
2070
- reply_to=message.message_id,
2071
- )
2072
- return
2073
- await self._send_message(
2074
- message.chat_id,
2075
- "Unknown /pr command. Use /pr start|fix|status|stop|resume|collect.",
2076
- thread_id=message.thread_id,
2077
- reply_to=message.message_id,
2078
- )
2079
-
2080
1564
  async def _prompt_review_commit_picker(
2081
1565
  self,
2082
1566
  message: TelegramMessage,
@@ -2202,28 +1686,3 @@ def _extract_opencode_error_detail(payload: Any) -> Optional[str]:
2202
1686
  if isinstance(value, str) and value:
2203
1687
  return value
2204
1688
  return None
2205
-
2206
-
2207
- def _format_httpx_exception(exc: Exception) -> Optional[str]:
2208
- """Format httpx exceptions for user-friendly error messages."""
2209
- if isinstance(exc, httpx.HTTPStatusError):
2210
- try:
2211
- payload = exc.response.json()
2212
- except Exception:
2213
- payload = None
2214
- if isinstance(payload, dict):
2215
- detail = (
2216
- payload.get("detail") or payload.get("message") or payload.get("error")
2217
- )
2218
- if isinstance(detail, str) and detail:
2219
- return detail
2220
- response_text = exc.response.text.strip()
2221
- if response_text:
2222
- return response_text
2223
- return f"Request failed (HTTP {exc.response.status_code})."
2224
- if isinstance(exc, httpx.RequestError):
2225
- detail = str(exc).strip()
2226
- if detail:
2227
- return detail
2228
- return "Request failed."
2229
- return None