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
@@ -20,13 +20,12 @@ from ....agents.opencode.supervisor import OpenCodeSupervisorError
20
20
  from ....core.logging_utils import log_event
21
21
  from ....core.state import now_iso
22
22
  from ....core.update import _normalize_update_target, _spawn_update_process
23
+ from ....core.update_paths import resolve_update_paths
23
24
  from ....core.utils import canonicalize_path
24
- from ....integrations.github.service import GitHubError, GitHubService
25
25
  from ...app_server.client import _normalize_sandbox_policy
26
26
  from ..adapter import (
27
27
  CompactCallback,
28
28
  InlineButton,
29
- PrFlowStartCallback,
30
29
  TelegramCallbackQuery,
31
30
  TelegramCommand,
32
31
  TelegramMessage,
@@ -56,6 +55,7 @@ from ..constants import (
56
55
  TurnKey,
57
56
  )
58
57
  from ..helpers import (
58
+ CodexFeatureRow,
59
59
  _coerce_model_options,
60
60
  _compact_preview,
61
61
  _extract_command_result,
@@ -79,6 +79,9 @@ from ..helpers import (
79
79
  _set_rollout_path,
80
80
  _thread_summary_preview,
81
81
  _with_conversation_id,
82
+ derive_codex_features_command,
83
+ format_codex_features,
84
+ parse_codex_features_list,
82
85
  )
83
86
  from ..state import (
84
87
  parse_topic_key,
@@ -98,6 +101,7 @@ from .commands import (
98
101
  ApprovalsCommands,
99
102
  ExecutionCommands,
100
103
  FilesCommands,
104
+ FlowCommands,
101
105
  FormattingHelpers,
102
106
  GitHubCommands,
103
107
  VoiceCommands,
@@ -113,20 +117,7 @@ OUTBOX_CONTEXT_RE = re.compile(
113
117
  "(?:\\b(?:pdf|png|jpg|jpeg|gif|webp|svg|csv|tsv|json|yaml|yml|zip|tar|gz|tgz|xlsx|xls|docx|pptx|md|txt|log|html|xml)\\b|\\.(?:pdf|png|jpg|jpeg|gif|webp|svg|csv|tsv|json|yaml|yml|zip|tar|gz|tgz|xlsx|xls|docx|pptx|md|txt|log|html|xml)\\b|\\b(?:outbox)\\b)",
114
118
  re.IGNORECASE,
115
119
  )
116
- CAR_CONTEXT_KEYWORDS = (
117
- "car",
118
- "codex",
119
- "todo",
120
- "progress",
121
- "opinions",
122
- "spec",
123
- "summary",
124
- "autorunner",
125
- "work docs",
126
- )
127
- CAR_CONTEXT_HINT = (
128
- "Context: read .codex-autorunner/ABOUT_CAR.md for repo-specific rules."
129
- )
120
+
130
121
  FILES_HINT_TEMPLATE = """Inbox: {inbox}
131
122
  Outbox (pending): {outbox}
132
123
  Topic key: {topic_key}
@@ -297,7 +288,7 @@ def _iter_exception_chain(exc: BaseException) -> list[BaseException]:
297
288
  def _sanitize_error_detail(detail: str, *, limit: int = 200) -> str:
298
289
  cleaned = " ".join(detail.split())
299
290
  if len(cleaned) > limit:
300
- return f"{cleaned[:limit - 3]}..."
291
+ return f"{cleaned[: limit - 3]}..."
301
292
  return cleaned
302
293
 
303
294
 
@@ -376,13 +367,13 @@ def _format_media_batch_failure(
376
367
  class TelegramCommandHandlers(
377
368
  WorkspaceCommands,
378
369
  GitHubCommands,
370
+ FlowCommands,
379
371
  FilesCommands,
380
372
  VoiceCommands,
381
373
  ExecutionCommands,
382
374
  ApprovalsCommands,
383
375
  FormattingHelpers,
384
376
  ):
385
-
386
377
  async def _handle_help(
387
378
  self, message: TelegramMessage, _args: str, _runtime: Any
388
379
  ) -> None:
@@ -1182,451 +1173,6 @@ class TelegramCommandHandlers(
1182
1173
  reply_to=message.message_id,
1183
1174
  )
1184
1175
 
1185
- async def _pr_flow_request(
1186
- self,
1187
- record: "TelegramTopicRecord",
1188
- *,
1189
- method: str,
1190
- path: str,
1191
- payload: Optional[dict[str, Any]] = None,
1192
- ) -> dict[str, Any]:
1193
- base, headers = self._pr_flow_api_base(record)
1194
- if not base:
1195
- raise RuntimeError(
1196
- "PR flow cannot start: repo server base URL could not be resolved for this chat/topic."
1197
- )
1198
- url = f"{base}{path}"
1199
- async with httpx.AsyncClient(timeout=30.0) as client:
1200
- res = await client.request(method, url, json=payload, headers=headers)
1201
- res.raise_for_status()
1202
- data = res.json()
1203
- if isinstance(data, dict):
1204
- return data
1205
- return {"status": "ok", "flow": data}
1206
-
1207
- def _parse_pr_flags(self, argv: list[str]) -> tuple[Optional[str], dict[str, Any]]:
1208
- ref: Optional[str] = None
1209
- flags: dict[str, Any] = {}
1210
- idx = 0
1211
- while idx < len(argv):
1212
- token = argv[idx]
1213
- if token.startswith("--"):
1214
- if token == "--draft":
1215
- flags["draft"] = True
1216
- idx += 1
1217
- continue
1218
- if token == "--ready":
1219
- flags["draft"] = False
1220
- idx += 1
1221
- continue
1222
- if token == "--base" and idx + 1 < len(argv):
1223
- flags["base_branch"] = argv[idx + 1]
1224
- idx += 2
1225
- continue
1226
- if token == "--until" and idx + 1 < len(argv):
1227
- until = argv[idx + 1].strip().lower()
1228
- if until in ("minor", "minor_only"):
1229
- flags["stop_condition"] = "minor_only"
1230
- elif until in ("clean", "no_issues"):
1231
- flags["stop_condition"] = "no_issues"
1232
- idx += 2
1233
- continue
1234
- if token in ("--max-cycles", "--max_cycles") and idx + 1 < len(argv):
1235
- try:
1236
- flags["max_cycles"] = int(argv[idx + 1])
1237
- except ValueError:
1238
- pass
1239
- idx += 2
1240
- continue
1241
- if token in ("--max-runs", "--max_runs") and idx + 1 < len(argv):
1242
- try:
1243
- flags["max_implementation_runs"] = int(argv[idx + 1])
1244
- except ValueError:
1245
- pass
1246
- idx += 2
1247
- continue
1248
- if token in ("--timeout", "--timeout-seconds") and idx + 1 < len(argv):
1249
- try:
1250
- flags["max_wallclock_seconds"] = int(argv[idx + 1])
1251
- except ValueError:
1252
- pass
1253
- idx += 2
1254
- continue
1255
- idx += 1
1256
- continue
1257
- if ref is None:
1258
- ref = token
1259
- idx += 1
1260
- return ref, flags
1261
-
1262
- def _format_pr_flow_status(self, flow: dict[str, Any]) -> str:
1263
- status = flow.get("status") or "unknown"
1264
- step = flow.get("step") or "unknown"
1265
- cycle = flow.get("cycle") or 0
1266
- pr_url = flow.get("pr_url") or ""
1267
- lines = [f"PR flow: {status} (step: {step}, cycle: {cycle})"]
1268
- if pr_url:
1269
- lines.append(f"PR: {pr_url}")
1270
- return "\n".join(lines)
1271
-
1272
- async def _handle_github_issue_url(
1273
- self, message: TelegramMessage, key: str, slug: str, number: int
1274
- ) -> None:
1275
- if key is None:
1276
- return
1277
-
1278
- record = await self._router.get_topic(key)
1279
- if record is None or not record.workspace_path:
1280
- await self._send_message(
1281
- message.chat_id,
1282
- self._with_conversation_id(
1283
- "Topic not bound. Use /bind <repo_id> or /bind <path>.",
1284
- chat_id=message.chat_id,
1285
- thread_id=message.thread_id,
1286
- ),
1287
- thread_id=message.thread_id,
1288
- reply_to=message.message_id,
1289
- )
1290
- return
1291
-
1292
- try:
1293
- from pathlib import Path
1294
-
1295
- service = GitHubService(Path(record.workspace_path), self._raw_config)
1296
- issue_ref = f"{slug}#{number}"
1297
- service.validate_issue_same_repo(issue_ref)
1298
- except GitHubError as exc:
1299
- await self._send_message(
1300
- message.chat_id,
1301
- str(exc),
1302
- thread_id=message.thread_id,
1303
- reply_to=message.message_id,
1304
- )
1305
- return
1306
-
1307
- await self._offer_pr_flow_start(message, record, slug, number)
1308
-
1309
- async def _offer_pr_flow_start(
1310
- self,
1311
- message: TelegramMessage,
1312
- record: "TelegramTopicRecord",
1313
- slug: str,
1314
- number: int,
1315
- ) -> None:
1316
- from ..adapter import (
1317
- InlineButton,
1318
- build_inline_keyboard,
1319
- encode_cancel_callback,
1320
- encode_pr_flow_start_callback,
1321
- )
1322
-
1323
- keyboard = build_inline_keyboard(
1324
- [
1325
- [
1326
- InlineButton(
1327
- f"Create PR for #{number}",
1328
- encode_pr_flow_start_callback(slug, number),
1329
- ),
1330
- InlineButton(
1331
- "Cancel",
1332
- encode_cancel_callback("pr_flow_offer"),
1333
- ),
1334
- ]
1335
- ]
1336
- )
1337
- await self._send_message(
1338
- message.chat_id,
1339
- f"Detected GitHub issue: {slug}#{number}\nStart PR flow to create a PR?",
1340
- thread_id=message.thread_id,
1341
- reply_to=message.message_id,
1342
- reply_markup=keyboard,
1343
- )
1344
-
1345
- async def _handle_pr_flow_start_callback(
1346
- self,
1347
- key: str,
1348
- callback: TelegramCallbackQuery,
1349
- parsed: PrFlowStartCallback,
1350
- ) -> None:
1351
- from ..adapter import TelegramMessage
1352
-
1353
- await self._answer_callback(callback)
1354
- record = await self._router.get_topic(key)
1355
- if record is None or not record.workspace_path:
1356
- return
1357
-
1358
- issue_ref = f"{parsed.slug}#{parsed.number}"
1359
- payload = {"mode": "issue", "issue": issue_ref}
1360
- payload["source"] = "telegram"
1361
- source_meta: dict[str, Any] = {}
1362
- if callback.chat_id is not None:
1363
- source_meta["chat_id"] = callback.chat_id
1364
- if callback.thread_id is not None:
1365
- source_meta["thread_id"] = callback.thread_id
1366
- if source_meta:
1367
- payload["source_meta"] = source_meta
1368
-
1369
- message = TelegramMessage(
1370
- update_id=callback.update_id,
1371
- message_id=callback.message_id or 0,
1372
- chat_id=callback.chat_id or 0,
1373
- thread_id=callback.thread_id,
1374
- from_user_id=callback.from_user_id,
1375
- text="",
1376
- date=None,
1377
- is_topic_message=False,
1378
- )
1379
-
1380
- try:
1381
- data = await self._pr_flow_request(
1382
- record,
1383
- method="POST",
1384
- path="/api/github/pr_flow/start",
1385
- payload=payload,
1386
- )
1387
- flow = data.get("flow") if isinstance(data, dict) else data
1388
- except Exception as exc:
1389
- detail = _format_httpx_exception(exc) or str(exc)
1390
- await self._send_message(
1391
- message.chat_id,
1392
- f"PR flow error: {detail}",
1393
- thread_id=message.thread_id,
1394
- reply_to=callback.message_id,
1395
- )
1396
- return
1397
- await self._send_message(
1398
- message.chat_id,
1399
- self._format_pr_flow_status(flow),
1400
- thread_id=message.thread_id,
1401
- reply_to=callback.message_id,
1402
- )
1403
-
1404
- async def _handle_pr(
1405
- self, message: TelegramMessage, args: str, runtime: Any
1406
- ) -> None:
1407
- record = await self._require_bound_record(message)
1408
- if not record:
1409
- return
1410
- argv = self._parse_command_args(args)
1411
- if not argv:
1412
- await self._send_message(
1413
- message.chat_id,
1414
- "Usage: /pr start <issueRef> | /pr fix <prRef> | /pr status | /pr stop | /pr resume | /pr collect",
1415
- thread_id=message.thread_id,
1416
- reply_to=message.message_id,
1417
- )
1418
- return
1419
- command = argv[0].lower()
1420
- if command == "status":
1421
- try:
1422
- data = await self._pr_flow_request(
1423
- record, method="GET", path="/api/github/pr_flow/status"
1424
- )
1425
- flow = data.get("flow") if isinstance(data, dict) else data
1426
- except Exception as exc:
1427
- detail = _format_httpx_exception(exc) or str(exc)
1428
- await self._send_message(
1429
- message.chat_id,
1430
- f"PR flow error: {detail}",
1431
- thread_id=message.thread_id,
1432
- reply_to=message.message_id,
1433
- )
1434
- return
1435
- await self._send_message(
1436
- message.chat_id,
1437
- self._format_pr_flow_status(flow),
1438
- thread_id=message.thread_id,
1439
- reply_to=message.message_id,
1440
- )
1441
- return
1442
- if command == "stop":
1443
- try:
1444
- data = await self._pr_flow_request(
1445
- record, method="POST", path="/api/github/pr_flow/stop", payload={}
1446
- )
1447
- flow = data.get("flow") if isinstance(data, dict) else data
1448
- except Exception as exc:
1449
- detail = _format_httpx_exception(exc) or str(exc)
1450
- await self._send_message(
1451
- message.chat_id,
1452
- f"PR flow error: {detail}",
1453
- thread_id=message.thread_id,
1454
- reply_to=message.message_id,
1455
- )
1456
- return
1457
- await self._send_message(
1458
- message.chat_id,
1459
- self._format_pr_flow_status(flow),
1460
- thread_id=message.thread_id,
1461
- reply_to=message.message_id,
1462
- )
1463
- return
1464
- if command == "resume":
1465
- try:
1466
- data = await self._pr_flow_request(
1467
- record, method="POST", path="/api/github/pr_flow/resume", payload={}
1468
- )
1469
- flow = data.get("flow") if isinstance(data, dict) else data
1470
- except Exception as exc:
1471
- detail = _format_httpx_exception(exc) or str(exc)
1472
- await self._send_message(
1473
- message.chat_id,
1474
- f"PR flow error: {detail}",
1475
- thread_id=message.thread_id,
1476
- reply_to=message.message_id,
1477
- )
1478
- return
1479
- await self._send_message(
1480
- message.chat_id,
1481
- self._format_pr_flow_status(flow),
1482
- thread_id=message.thread_id,
1483
- reply_to=message.message_id,
1484
- )
1485
- return
1486
- if command == "collect":
1487
- try:
1488
- data = await self._pr_flow_request(
1489
- record,
1490
- method="POST",
1491
- path="/api/github/pr_flow/collect",
1492
- payload={},
1493
- )
1494
- flow = data.get("flow") if isinstance(data, dict) else data
1495
- except Exception as exc:
1496
- detail = _format_httpx_exception(exc) or str(exc)
1497
- await self._send_message(
1498
- message.chat_id,
1499
- f"PR flow error: {detail}",
1500
- thread_id=message.thread_id,
1501
- reply_to=message.message_id,
1502
- )
1503
- return
1504
- await self._send_message(
1505
- message.chat_id,
1506
- self._format_pr_flow_status(flow),
1507
- thread_id=message.thread_id,
1508
- reply_to=message.message_id,
1509
- )
1510
- return
1511
- if command in ("start", "implement"):
1512
- ref, flags = self._parse_pr_flags(argv[1:])
1513
- if not ref:
1514
- gh = GitHubService(Path(record.workspace_path))
1515
- issues = await asyncio.to_thread(gh.list_open_issues, limit=5)
1516
- if issues:
1517
- lines = ["Open issues:"]
1518
- for issue in issues:
1519
- num = issue.get("number")
1520
- title = issue.get("title") or ""
1521
- lines.append(f"- #{num} {title}".strip())
1522
- lines.append("Use /pr start <issueRef> to begin.")
1523
- await self._send_message(
1524
- message.chat_id,
1525
- "\n".join(lines),
1526
- thread_id=message.thread_id,
1527
- reply_to=message.message_id,
1528
- )
1529
- return
1530
- await self._send_message(
1531
- message.chat_id,
1532
- "Usage: /pr start <issueRef>",
1533
- thread_id=message.thread_id,
1534
- reply_to=message.message_id,
1535
- )
1536
- return
1537
- payload = {"mode": "issue", "issue": ref, **flags}
1538
- payload["source"] = "telegram"
1539
- payload["source_meta"] = {
1540
- "chat_id": message.chat_id,
1541
- "thread_id": message.thread_id,
1542
- }
1543
- try:
1544
- data = await self._pr_flow_request(
1545
- record,
1546
- method="POST",
1547
- path="/api/github/pr_flow/start",
1548
- payload=payload,
1549
- )
1550
- flow = data.get("flow") if isinstance(data, dict) else data
1551
- except Exception as exc:
1552
- detail = _format_httpx_exception(exc) or str(exc)
1553
- await self._send_message(
1554
- message.chat_id,
1555
- f"PR flow error: {detail}",
1556
- thread_id=message.thread_id,
1557
- reply_to=message.message_id,
1558
- )
1559
- return
1560
- await self._send_message(
1561
- message.chat_id,
1562
- self._format_pr_flow_status(flow),
1563
- thread_id=message.thread_id,
1564
- reply_to=message.message_id,
1565
- )
1566
- return
1567
- if command in ("fix", "pr"):
1568
- ref, flags = self._parse_pr_flags(argv[1:])
1569
- if not ref:
1570
- gh = GitHubService(Path(record.workspace_path))
1571
- prs = await asyncio.to_thread(gh.list_open_prs, limit=5)
1572
- if prs:
1573
- lines = ["Open PRs:"]
1574
- for pr in prs:
1575
- num = pr.get("number")
1576
- title = pr.get("title") or ""
1577
- lines.append(f"- #{num} {title}".strip())
1578
- lines.append("Use /pr fix <prRef> to begin.")
1579
- await self._send_message(
1580
- message.chat_id,
1581
- "\n".join(lines),
1582
- thread_id=message.thread_id,
1583
- reply_to=message.message_id,
1584
- )
1585
- return
1586
- await self._send_message(
1587
- message.chat_id,
1588
- "Usage: /pr fix <prRef>",
1589
- thread_id=message.thread_id,
1590
- reply_to=message.message_id,
1591
- )
1592
- return
1593
- payload = {"mode": "pr", "pr": ref, **flags}
1594
- payload["source"] = "telegram"
1595
- payload["source_meta"] = {
1596
- "chat_id": message.chat_id,
1597
- "thread_id": message.thread_id,
1598
- }
1599
- try:
1600
- data = await self._pr_flow_request(
1601
- record,
1602
- method="POST",
1603
- path="/api/github/pr_flow/start",
1604
- payload=payload,
1605
- )
1606
- flow = data.get("flow") if isinstance(data, dict) else data
1607
- except Exception as exc:
1608
- detail = _format_httpx_exception(exc) or str(exc)
1609
- await self._send_message(
1610
- message.chat_id,
1611
- f"PR flow error: {detail}",
1612
- thread_id=message.thread_id,
1613
- reply_to=message.message_id,
1614
- )
1615
- return
1616
- await self._send_message(
1617
- message.chat_id,
1618
- self._format_pr_flow_status(flow),
1619
- thread_id=message.thread_id,
1620
- reply_to=message.message_id,
1621
- )
1622
- return
1623
- await self._send_message(
1624
- message.chat_id,
1625
- "Unknown /pr command. Use /pr start|fix|status|stop|resume|collect.",
1626
- thread_id=message.thread_id,
1627
- reply_to=message.message_id,
1628
- )
1629
-
1630
1176
  async def _list_recent_commits(
1631
1177
  self, record: TelegramTopicRecord
1632
1178
  ) -> list[tuple[str, str]]:
@@ -2068,36 +1614,84 @@ class TelegramCommandHandlers(
2068
1614
  )
2069
1615
  return
2070
1616
  argv = self._parse_command_args(args)
2071
- if not argv:
1617
+
1618
+ async def _read_explicit_config_features() -> Optional[str]:
2072
1619
  try:
2073
1620
  result = await client.request("config/read", {"includeLayers": False})
1621
+ except Exception:
1622
+ return None
1623
+ return _format_feature_flags(result)
1624
+
1625
+ async def _fetch_codex_features() -> (
1626
+ tuple[list[CodexFeatureRow], Optional[str]]
1627
+ ):
1628
+ features_command = derive_codex_features_command(
1629
+ self._config.app_server_command
1630
+ )
1631
+ try:
1632
+ result = await client.request(
1633
+ "command/exec",
1634
+ {
1635
+ "cwd": record.workspace_path,
1636
+ "command": features_command,
1637
+ "timeoutMs": 10000,
1638
+ },
1639
+ )
2074
1640
  except Exception as exc:
2075
1641
  log_event(
2076
1642
  self._logger,
2077
1643
  logging.WARNING,
2078
- "telegram.experimental.read_failed",
1644
+ "telegram.experimental.exec_failed",
2079
1645
  chat_id=message.chat_id,
2080
1646
  thread_id=message.thread_id,
2081
1647
  exc=exc,
2082
1648
  )
1649
+ return (
1650
+ [],
1651
+ "Failed to run `codex features list`; check Codex install/PATH.",
1652
+ )
1653
+ stdout, stderr, exit_code = _extract_command_result(result)
1654
+ if exit_code not in (None, 0):
1655
+ detail = stderr.strip() if isinstance(stderr, str) else ""
1656
+ msg = f"`{' '.join(features_command)}` failed (exit {exit_code})."
1657
+ if detail:
1658
+ msg = f"{msg} stderr: {detail}"
1659
+ return [], msg
1660
+ rows = parse_codex_features_list(stdout)
1661
+ if not rows:
1662
+ return (
1663
+ [],
1664
+ f"No feature rows returned by `{' '.join(features_command)}`.",
1665
+ )
1666
+ return rows, None
1667
+
1668
+ list_all = bool(argv and argv[0].lower() == "all")
1669
+ is_list_request = not argv or list_all or argv[0].lower() in ("list", "ls")
1670
+ if is_list_request:
1671
+ stage_filter = None if list_all else "beta"
1672
+ rows, error = await _fetch_codex_features()
1673
+ if error:
1674
+ fallback = await _read_explicit_config_features()
1675
+ message_lines = [error]
1676
+ if fallback and fallback.strip() != "No feature flags found.":
1677
+ message_lines.append("")
1678
+ message_lines.append("Explicit config entries (may be incomplete):")
1679
+ message_lines.append(fallback)
2083
1680
  await self._send_message(
2084
1681
  message.chat_id,
2085
- _with_conversation_id(
2086
- "Failed to read config; check logs for details.",
2087
- chat_id=message.chat_id,
2088
- thread_id=message.thread_id,
2089
- ),
1682
+ "\n".join(message_lines),
2090
1683
  thread_id=message.thread_id,
2091
1684
  reply_to=message.message_id,
2092
1685
  )
2093
1686
  return
2094
1687
  await self._send_message(
2095
1688
  message.chat_id,
2096
- _format_feature_flags(result),
1689
+ format_codex_features(rows, stage_filter=stage_filter),
2097
1690
  thread_id=message.thread_id,
2098
1691
  reply_to=message.message_id,
2099
1692
  )
2100
1693
  return
1694
+
2101
1695
  if len(argv) < 2:
2102
1696
  await self._send_message(
2103
1697
  message.chat_id,
@@ -2128,9 +1722,35 @@ class TelegramCommandHandlers(
2128
1722
  reply_to=message.message_id,
2129
1723
  )
2130
1724
  return
1725
+
1726
+ rows, error = await _fetch_codex_features()
1727
+ if error:
1728
+ await self._send_message(
1729
+ message.chat_id,
1730
+ error,
1731
+ thread_id=message.thread_id,
1732
+ reply_to=message.message_id,
1733
+ )
1734
+ return
1735
+
1736
+ normalized_feature = feature
1737
+ if feature.startswith("features."):
1738
+ normalized_feature = feature[len("features.") :]
1739
+ target_row = next((row for row in rows if row.key == normalized_feature), None)
1740
+ if target_row is None:
1741
+ available = ", ".join(sorted(row.key for row in rows))
1742
+ await self._send_message(
1743
+ message.chat_id,
1744
+ f"Unknown feature '{feature}'. Known features: {available}\n"
1745
+ "Use /experimental all to list all stages.",
1746
+ thread_id=message.thread_id,
1747
+ reply_to=message.message_id,
1748
+ )
1749
+ return
1750
+
2131
1751
  key_path = feature if feature.startswith("features.") else f"features.{feature}"
2132
1752
  try:
2133
- await client.request(
1753
+ write_result = await client.request(
2134
1754
  "config/value/write",
2135
1755
  {"keyPath": key_path, "value": value, "mergeStrategy": "replace"},
2136
1756
  )
@@ -2154,9 +1774,49 @@ class TelegramCommandHandlers(
2154
1774
  reply_to=message.message_id,
2155
1775
  )
2156
1776
  return
1777
+
1778
+ post_rows, post_error = await _fetch_codex_features()
1779
+ effective_row = None
1780
+ if not post_error:
1781
+ effective_row = next(
1782
+ (row for row in post_rows if row.key == normalized_feature), None
1783
+ )
1784
+
1785
+ lines = [f"Feature {key_path} set to {value}."]
1786
+ if effective_row:
1787
+ lines.append(
1788
+ f"Stage: {effective_row.stage}; effective state: {effective_row.enabled}."
1789
+ )
1790
+ elif post_error:
1791
+ lines.append(f"(Could not verify effective state: {post_error})")
1792
+
1793
+ if isinstance(write_result, dict):
1794
+ status = write_result.get("status")
1795
+ overridden = write_result.get("overriddenMetadata")
1796
+ if status == "okOverridden" and isinstance(overridden, dict):
1797
+ message_txt = overridden.get("message")
1798
+ effective_value = overridden.get("effectiveValue")
1799
+ layer = overridden.get("overridingLayer") or {}
1800
+ layer_name = layer.get("name") if isinstance(layer, dict) else None
1801
+ layer_version = (
1802
+ layer.get("version") if isinstance(layer, dict) else None
1803
+ )
1804
+ lines.append("Write was overridden by another config layer.")
1805
+ if layer_name:
1806
+ layer_desc = (
1807
+ f"{layer_name} (version {layer_version})"
1808
+ if layer_version
1809
+ else layer_name
1810
+ )
1811
+ lines.append(f"- Overriding layer: {layer_desc}")
1812
+ if effective_value is not None:
1813
+ lines.append(f"- Effective value: {effective_value}")
1814
+ if isinstance(message_txt, str) and message_txt:
1815
+ lines.append(f"- Note: {message_txt}")
1816
+
2157
1817
  await self._send_message(
2158
1818
  message.chat_id,
2159
- f"Feature {key_path} set to {value}.",
1819
+ "\n".join(lines),
2160
1820
  thread_id=message.thread_id,
2161
1821
  reply_to=message.message_id,
2162
1822
  )
@@ -2352,7 +2012,6 @@ class TelegramCommandHandlers(
2352
2012
  async def _handle_compact_callback(
2353
2013
  self, key: str, callback: TelegramCallbackQuery, parsed: CompactCallback
2354
2014
  ) -> None:
2355
-
2356
2015
  async def _send_compact_status(text: str) -> bool:
2357
2016
  try:
2358
2017
  await self._send_message(
@@ -2619,7 +2278,7 @@ Summary applied.""",
2619
2278
  repo_ref = (self._update_repo_ref or DEFAULT_UPDATE_REPO_REF).strip()
2620
2279
  if not repo_ref:
2621
2280
  repo_ref = DEFAULT_UPDATE_REPO_REF
2622
- update_dir = Path.home() / ".codex-autorunner" / "update_cache"
2281
+ update_dir = resolve_update_paths().cache_dir
2623
2282
  notify_reply_to = reply_to
2624
2283
  if notify_reply_to is None and callback is not None:
2625
2284
  notify_reply_to = callback.message_id
@@ -2630,6 +2289,7 @@ Summary applied.""",
2630
2289
  update_dir=update_dir,
2631
2290
  logger=self._logger,
2632
2291
  update_target=update_target,
2292
+ skip_checks=bool(getattr(self, "_update_skip_checks", False)),
2633
2293
  notify_chat_id=chat_id,
2634
2294
  notify_thread_id=thread_id,
2635
2295
  notify_reply_to=notify_reply_to,
@@ -2724,7 +2384,7 @@ Summary applied.""",
2724
2384
  )
2725
2385
 
2726
2386
  def _update_status_path(self) -> Path:
2727
- return Path.home() / ".codex-autorunner" / "update_status.json"
2387
+ return resolve_update_paths().status_path
2728
2388
 
2729
2389
  def _read_update_status(self) -> Optional[dict[str, Any]]:
2730
2390
  path = self._update_status_path()
@@ -2773,7 +2433,6 @@ Summary applied.""",
2773
2433
  timeout_seconds: float = 300.0,
2774
2434
  interval_seconds: float = 2.0,
2775
2435
  ) -> None:
2776
-
2777
2436
  async def _watch() -> None:
2778
2437
  deadline = time.monotonic() + timeout_seconds
2779
2438
  while time.monotonic() < deadline:
@@ -2809,7 +2468,7 @@ Summary applied.""",
2809
2468
  )
2810
2469
 
2811
2470
  def _compact_status_path(self) -> Path:
2812
- return Path.home() / ".codex-autorunner" / "compact_status.json"
2471
+ return resolve_update_paths().compact_status_path
2813
2472
 
2814
2473
  def _read_compact_status(self) -> Optional[dict[str, Any]]:
2815
2474
  path = self._compact_status_path()