codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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 (189) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/opencode/client.py +68 -35
  3. codex_autorunner/agents/opencode/logging.py +21 -5
  4. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  5. codex_autorunner/agents/opencode/runtime.py +118 -30
  6. codex_autorunner/agents/opencode/supervisor.py +36 -48
  7. codex_autorunner/agents/registry.py +136 -8
  8. codex_autorunner/api.py +25 -0
  9. codex_autorunner/bootstrap.py +16 -35
  10. codex_autorunner/cli.py +157 -139
  11. codex_autorunner/core/about_car.py +44 -32
  12. codex_autorunner/core/adapter_utils.py +21 -0
  13. codex_autorunner/core/app_server_logging.py +7 -3
  14. codex_autorunner/core/app_server_prompts.py +27 -260
  15. codex_autorunner/core/app_server_threads.py +15 -26
  16. codex_autorunner/core/codex_runner.py +6 -0
  17. codex_autorunner/core/config.py +390 -100
  18. codex_autorunner/core/docs.py +10 -2
  19. codex_autorunner/core/drafts.py +82 -0
  20. codex_autorunner/core/engine.py +278 -262
  21. codex_autorunner/core/flows/__init__.py +25 -0
  22. codex_autorunner/core/flows/controller.py +178 -0
  23. codex_autorunner/core/flows/definition.py +82 -0
  24. codex_autorunner/core/flows/models.py +75 -0
  25. codex_autorunner/core/flows/runtime.py +351 -0
  26. codex_autorunner/core/flows/store.py +485 -0
  27. codex_autorunner/core/flows/transition.py +133 -0
  28. codex_autorunner/core/flows/worker_process.py +242 -0
  29. codex_autorunner/core/hub.py +15 -9
  30. codex_autorunner/core/locks.py +4 -0
  31. codex_autorunner/core/prompt.py +15 -7
  32. codex_autorunner/core/redaction.py +29 -0
  33. codex_autorunner/core/review_context.py +5 -8
  34. codex_autorunner/core/run_index.py +6 -0
  35. codex_autorunner/core/runner_process.py +5 -2
  36. codex_autorunner/core/state.py +0 -88
  37. codex_autorunner/core/static_assets.py +55 -0
  38. codex_autorunner/core/supervisor_utils.py +67 -0
  39. codex_autorunner/core/update.py +20 -11
  40. codex_autorunner/core/update_runner.py +2 -0
  41. codex_autorunner/core/utils.py +29 -2
  42. codex_autorunner/discovery.py +2 -4
  43. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  44. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  45. codex_autorunner/integrations/agents/__init__.py +27 -0
  46. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  47. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  48. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  49. codex_autorunner/integrations/agents/run_event.py +71 -0
  50. codex_autorunner/integrations/app_server/client.py +576 -92
  51. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  52. codex_autorunner/integrations/telegram/adapter.py +141 -167
  53. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  54. codex_autorunner/integrations/telegram/config.py +175 -0
  55. codex_autorunner/integrations/telegram/constants.py +16 -1
  56. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  57. codex_autorunner/integrations/telegram/doctor.py +47 -0
  58. codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
  59. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  64. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  65. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  66. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
  67. codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
  68. codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
  69. codex_autorunner/integrations/telegram/helpers.py +88 -16
  70. codex_autorunner/integrations/telegram/outbox.py +208 -37
  71. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  72. codex_autorunner/integrations/telegram/service.py +214 -40
  73. codex_autorunner/integrations/telegram/state.py +100 -2
  74. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  75. codex_autorunner/integrations/telegram/transport.py +36 -3
  76. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  77. codex_autorunner/manifest.py +2 -0
  78. codex_autorunner/plugin_api.py +22 -0
  79. codex_autorunner/routes/__init__.py +23 -14
  80. codex_autorunner/routes/analytics.py +239 -0
  81. codex_autorunner/routes/base.py +81 -109
  82. codex_autorunner/routes/file_chat.py +836 -0
  83. codex_autorunner/routes/flows.py +980 -0
  84. codex_autorunner/routes/messages.py +459 -0
  85. codex_autorunner/routes/system.py +6 -1
  86. codex_autorunner/routes/usage.py +87 -0
  87. codex_autorunner/routes/workspace.py +271 -0
  88. codex_autorunner/server.py +2 -1
  89. codex_autorunner/static/agentControls.js +1 -0
  90. codex_autorunner/static/agentEvents.js +248 -0
  91. codex_autorunner/static/app.js +25 -22
  92. codex_autorunner/static/autoRefresh.js +29 -1
  93. codex_autorunner/static/bootstrap.js +1 -0
  94. codex_autorunner/static/bus.js +1 -0
  95. codex_autorunner/static/cache.js +1 -0
  96. codex_autorunner/static/constants.js +20 -4
  97. codex_autorunner/static/dashboard.js +162 -196
  98. codex_autorunner/static/diffRenderer.js +37 -0
  99. codex_autorunner/static/docChatCore.js +324 -0
  100. codex_autorunner/static/docChatStorage.js +65 -0
  101. codex_autorunner/static/docChatVoice.js +65 -0
  102. codex_autorunner/static/docEditor.js +133 -0
  103. codex_autorunner/static/env.js +1 -0
  104. codex_autorunner/static/eventSummarizer.js +166 -0
  105. codex_autorunner/static/fileChat.js +182 -0
  106. codex_autorunner/static/health.js +155 -0
  107. codex_autorunner/static/hub.js +41 -118
  108. codex_autorunner/static/index.html +787 -858
  109. codex_autorunner/static/liveUpdates.js +1 -0
  110. codex_autorunner/static/loader.js +1 -0
  111. codex_autorunner/static/messages.js +470 -0
  112. codex_autorunner/static/mobileCompact.js +2 -1
  113. codex_autorunner/static/settings.js +24 -211
  114. codex_autorunner/static/styles.css +7567 -3865
  115. codex_autorunner/static/tabs.js +28 -5
  116. codex_autorunner/static/terminal.js +14 -0
  117. codex_autorunner/static/terminalManager.js +34 -59
  118. codex_autorunner/static/ticketChatActions.js +333 -0
  119. codex_autorunner/static/ticketChatEvents.js +16 -0
  120. codex_autorunner/static/ticketChatStorage.js +16 -0
  121. codex_autorunner/static/ticketChatStream.js +264 -0
  122. codex_autorunner/static/ticketEditor.js +750 -0
  123. codex_autorunner/static/ticketVoice.js +9 -0
  124. codex_autorunner/static/tickets.js +1315 -0
  125. codex_autorunner/static/utils.js +32 -3
  126. codex_autorunner/static/voice.js +1 -0
  127. codex_autorunner/static/workspace.js +672 -0
  128. codex_autorunner/static/workspaceApi.js +53 -0
  129. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  130. codex_autorunner/tickets/__init__.py +20 -0
  131. codex_autorunner/tickets/agent_pool.py +377 -0
  132. codex_autorunner/tickets/files.py +85 -0
  133. codex_autorunner/tickets/frontmatter.py +55 -0
  134. codex_autorunner/tickets/lint.py +102 -0
  135. codex_autorunner/tickets/models.py +95 -0
  136. codex_autorunner/tickets/outbox.py +232 -0
  137. codex_autorunner/tickets/replies.py +179 -0
  138. codex_autorunner/tickets/runner.py +823 -0
  139. codex_autorunner/tickets/spec_ingest.py +77 -0
  140. codex_autorunner/web/app.py +269 -91
  141. codex_autorunner/web/middleware.py +3 -4
  142. codex_autorunner/web/schemas.py +89 -109
  143. codex_autorunner/web/static_assets.py +1 -44
  144. codex_autorunner/workspace/__init__.py +40 -0
  145. codex_autorunner/workspace/paths.py +319 -0
  146. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
  147. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  148. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  149. codex_autorunner/agents/execution/policy.py +0 -292
  150. codex_autorunner/agents/factory.py +0 -52
  151. codex_autorunner/agents/orchestrator.py +0 -358
  152. codex_autorunner/core/doc_chat.py +0 -1446
  153. codex_autorunner/core/snapshot.py +0 -580
  154. codex_autorunner/integrations/github/chatops.py +0 -268
  155. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  156. codex_autorunner/routes/docs.py +0 -381
  157. codex_autorunner/routes/github.py +0 -327
  158. codex_autorunner/routes/runs.py +0 -250
  159. codex_autorunner/spec_ingest.py +0 -812
  160. codex_autorunner/static/docChatActions.js +0 -287
  161. codex_autorunner/static/docChatEvents.js +0 -300
  162. codex_autorunner/static/docChatRender.js +0 -205
  163. codex_autorunner/static/docChatStream.js +0 -361
  164. codex_autorunner/static/docs.js +0 -20
  165. codex_autorunner/static/docsClipboard.js +0 -69
  166. codex_autorunner/static/docsCrud.js +0 -257
  167. codex_autorunner/static/docsDocUpdates.js +0 -62
  168. codex_autorunner/static/docsDrafts.js +0 -16
  169. codex_autorunner/static/docsElements.js +0 -69
  170. codex_autorunner/static/docsInit.js +0 -285
  171. codex_autorunner/static/docsParse.js +0 -160
  172. codex_autorunner/static/docsSnapshot.js +0 -87
  173. codex_autorunner/static/docsSpecIngest.js +0 -263
  174. codex_autorunner/static/docsState.js +0 -127
  175. codex_autorunner/static/docsThreadRegistry.js +0 -44
  176. codex_autorunner/static/docsUi.js +0 -153
  177. codex_autorunner/static/docsVoice.js +0 -56
  178. codex_autorunner/static/github.js +0 -504
  179. codex_autorunner/static/logs.js +0 -678
  180. codex_autorunner/static/review.js +0 -157
  181. codex_autorunner/static/runs.js +0 -418
  182. codex_autorunner/static/snapshot.js +0 -124
  183. codex_autorunner/static/state.js +0 -94
  184. codex_autorunner/static/todoPreview.js +0 -27
  185. codex_autorunner/workspace.py +0 -16
  186. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  187. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  188. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  189. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -21,12 +21,10 @@ 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
23
  from ....core.utils import canonicalize_path
24
- from ....integrations.github.service import GitHubError, GitHubService
25
24
  from ...app_server.client import _normalize_sandbox_policy
26
25
  from ..adapter import (
27
26
  CompactCallback,
28
27
  InlineButton,
29
- PrFlowStartCallback,
30
28
  TelegramCallbackQuery,
31
29
  TelegramCommand,
32
30
  TelegramMessage,
@@ -56,6 +54,7 @@ from ..constants import (
56
54
  TurnKey,
57
55
  )
58
56
  from ..helpers import (
57
+ CodexFeatureRow,
59
58
  _coerce_model_options,
60
59
  _compact_preview,
61
60
  _extract_command_result,
@@ -79,6 +78,9 @@ from ..helpers import (
79
78
  _set_rollout_path,
80
79
  _thread_summary_preview,
81
80
  _with_conversation_id,
81
+ derive_codex_features_command,
82
+ format_codex_features,
83
+ parse_codex_features_list,
82
84
  )
83
85
  from ..state import (
84
86
  parse_topic_key,
@@ -98,6 +100,7 @@ from .commands import (
98
100
  ApprovalsCommands,
99
101
  ExecutionCommands,
100
102
  FilesCommands,
103
+ FlowCommands,
101
104
  FormattingHelpers,
102
105
  GitHubCommands,
103
106
  VoiceCommands,
@@ -113,20 +116,7 @@ OUTBOX_CONTEXT_RE = re.compile(
113
116
  "(?:\\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
117
  re.IGNORECASE,
115
118
  )
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
- )
119
+
130
120
  FILES_HINT_TEMPLATE = """Inbox: {inbox}
131
121
  Outbox (pending): {outbox}
132
122
  Topic key: {topic_key}
@@ -297,7 +287,7 @@ def _iter_exception_chain(exc: BaseException) -> list[BaseException]:
297
287
  def _sanitize_error_detail(detail: str, *, limit: int = 200) -> str:
298
288
  cleaned = " ".join(detail.split())
299
289
  if len(cleaned) > limit:
300
- return f"{cleaned[:limit - 3]}..."
290
+ return f"{cleaned[: limit - 3]}..."
301
291
  return cleaned
302
292
 
303
293
 
@@ -376,13 +366,13 @@ def _format_media_batch_failure(
376
366
  class TelegramCommandHandlers(
377
367
  WorkspaceCommands,
378
368
  GitHubCommands,
369
+ FlowCommands,
379
370
  FilesCommands,
380
371
  VoiceCommands,
381
372
  ExecutionCommands,
382
373
  ApprovalsCommands,
383
374
  FormattingHelpers,
384
375
  ):
385
-
386
376
  async def _handle_help(
387
377
  self, message: TelegramMessage, _args: str, _runtime: Any
388
378
  ) -> None:
@@ -1182,451 +1172,6 @@ class TelegramCommandHandlers(
1182
1172
  reply_to=message.message_id,
1183
1173
  )
1184
1174
 
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
1175
  async def _list_recent_commits(
1631
1176
  self, record: TelegramTopicRecord
1632
1177
  ) -> list[tuple[str, str]]:
@@ -2068,36 +1613,84 @@ class TelegramCommandHandlers(
2068
1613
  )
2069
1614
  return
2070
1615
  argv = self._parse_command_args(args)
2071
- if not argv:
1616
+
1617
+ async def _read_explicit_config_features() -> Optional[str]:
2072
1618
  try:
2073
1619
  result = await client.request("config/read", {"includeLayers": False})
1620
+ except Exception:
1621
+ return None
1622
+ return _format_feature_flags(result)
1623
+
1624
+ async def _fetch_codex_features() -> (
1625
+ tuple[list[CodexFeatureRow], Optional[str]]
1626
+ ):
1627
+ features_command = derive_codex_features_command(
1628
+ self._config.app_server_command
1629
+ )
1630
+ try:
1631
+ result = await client.request(
1632
+ "command/exec",
1633
+ {
1634
+ "cwd": record.workspace_path,
1635
+ "command": features_command,
1636
+ "timeoutMs": 10000,
1637
+ },
1638
+ )
2074
1639
  except Exception as exc:
2075
1640
  log_event(
2076
1641
  self._logger,
2077
1642
  logging.WARNING,
2078
- "telegram.experimental.read_failed",
1643
+ "telegram.experimental.exec_failed",
2079
1644
  chat_id=message.chat_id,
2080
1645
  thread_id=message.thread_id,
2081
1646
  exc=exc,
2082
1647
  )
1648
+ return (
1649
+ [],
1650
+ "Failed to run `codex features list`; check Codex install/PATH.",
1651
+ )
1652
+ stdout, stderr, exit_code = _extract_command_result(result)
1653
+ if exit_code not in (None, 0):
1654
+ detail = stderr.strip() if isinstance(stderr, str) else ""
1655
+ msg = f"`{' '.join(features_command)}` failed (exit {exit_code})."
1656
+ if detail:
1657
+ msg = f"{msg} stderr: {detail}"
1658
+ return [], msg
1659
+ rows = parse_codex_features_list(stdout)
1660
+ if not rows:
1661
+ return (
1662
+ [],
1663
+ f"No feature rows returned by `{' '.join(features_command)}`.",
1664
+ )
1665
+ return rows, None
1666
+
1667
+ list_all = bool(argv and argv[0].lower() == "all")
1668
+ is_list_request = not argv or list_all or argv[0].lower() in ("list", "ls")
1669
+ if is_list_request:
1670
+ stage_filter = None if list_all else "beta"
1671
+ rows, error = await _fetch_codex_features()
1672
+ if error:
1673
+ fallback = await _read_explicit_config_features()
1674
+ message_lines = [error]
1675
+ if fallback and fallback.strip() != "No feature flags found.":
1676
+ message_lines.append("")
1677
+ message_lines.append("Explicit config entries (may be incomplete):")
1678
+ message_lines.append(fallback)
2083
1679
  await self._send_message(
2084
1680
  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
- ),
1681
+ "\n".join(message_lines),
2090
1682
  thread_id=message.thread_id,
2091
1683
  reply_to=message.message_id,
2092
1684
  )
2093
1685
  return
2094
1686
  await self._send_message(
2095
1687
  message.chat_id,
2096
- _format_feature_flags(result),
1688
+ format_codex_features(rows, stage_filter=stage_filter),
2097
1689
  thread_id=message.thread_id,
2098
1690
  reply_to=message.message_id,
2099
1691
  )
2100
1692
  return
1693
+
2101
1694
  if len(argv) < 2:
2102
1695
  await self._send_message(
2103
1696
  message.chat_id,
@@ -2128,9 +1721,35 @@ class TelegramCommandHandlers(
2128
1721
  reply_to=message.message_id,
2129
1722
  )
2130
1723
  return
1724
+
1725
+ rows, error = await _fetch_codex_features()
1726
+ if error:
1727
+ await self._send_message(
1728
+ message.chat_id,
1729
+ error,
1730
+ thread_id=message.thread_id,
1731
+ reply_to=message.message_id,
1732
+ )
1733
+ return
1734
+
1735
+ normalized_feature = feature
1736
+ if feature.startswith("features."):
1737
+ normalized_feature = feature[len("features.") :]
1738
+ target_row = next((row for row in rows if row.key == normalized_feature), None)
1739
+ if target_row is None:
1740
+ available = ", ".join(sorted(row.key for row in rows))
1741
+ await self._send_message(
1742
+ message.chat_id,
1743
+ f"Unknown feature '{feature}'. Known features: {available}\n"
1744
+ "Use /experimental all to list all stages.",
1745
+ thread_id=message.thread_id,
1746
+ reply_to=message.message_id,
1747
+ )
1748
+ return
1749
+
2131
1750
  key_path = feature if feature.startswith("features.") else f"features.{feature}"
2132
1751
  try:
2133
- await client.request(
1752
+ write_result = await client.request(
2134
1753
  "config/value/write",
2135
1754
  {"keyPath": key_path, "value": value, "mergeStrategy": "replace"},
2136
1755
  )
@@ -2154,9 +1773,49 @@ class TelegramCommandHandlers(
2154
1773
  reply_to=message.message_id,
2155
1774
  )
2156
1775
  return
1776
+
1777
+ post_rows, post_error = await _fetch_codex_features()
1778
+ effective_row = None
1779
+ if not post_error:
1780
+ effective_row = next(
1781
+ (row for row in post_rows if row.key == normalized_feature), None
1782
+ )
1783
+
1784
+ lines = [f"Feature {key_path} set to {value}."]
1785
+ if effective_row:
1786
+ lines.append(
1787
+ f"Stage: {effective_row.stage}; effective state: {effective_row.enabled}."
1788
+ )
1789
+ elif post_error:
1790
+ lines.append(f"(Could not verify effective state: {post_error})")
1791
+
1792
+ if isinstance(write_result, dict):
1793
+ status = write_result.get("status")
1794
+ overridden = write_result.get("overriddenMetadata")
1795
+ if status == "okOverridden" and isinstance(overridden, dict):
1796
+ message_txt = overridden.get("message")
1797
+ effective_value = overridden.get("effectiveValue")
1798
+ layer = overridden.get("overridingLayer") or {}
1799
+ layer_name = layer.get("name") if isinstance(layer, dict) else None
1800
+ layer_version = (
1801
+ layer.get("version") if isinstance(layer, dict) else None
1802
+ )
1803
+ lines.append("Write was overridden by another config layer.")
1804
+ if layer_name:
1805
+ layer_desc = (
1806
+ f"{layer_name} (version {layer_version})"
1807
+ if layer_version
1808
+ else layer_name
1809
+ )
1810
+ lines.append(f"- Overriding layer: {layer_desc}")
1811
+ if effective_value is not None:
1812
+ lines.append(f"- Effective value: {effective_value}")
1813
+ if isinstance(message_txt, str) and message_txt:
1814
+ lines.append(f"- Note: {message_txt}")
1815
+
2157
1816
  await self._send_message(
2158
1817
  message.chat_id,
2159
- f"Feature {key_path} set to {value}.",
1818
+ "\n".join(lines),
2160
1819
  thread_id=message.thread_id,
2161
1820
  reply_to=message.message_id,
2162
1821
  )
@@ -2352,7 +2011,6 @@ class TelegramCommandHandlers(
2352
2011
  async def _handle_compact_callback(
2353
2012
  self, key: str, callback: TelegramCallbackQuery, parsed: CompactCallback
2354
2013
  ) -> None:
2355
-
2356
2014
  async def _send_compact_status(text: str) -> bool:
2357
2015
  try:
2358
2016
  await self._send_message(
@@ -2630,6 +2288,7 @@ Summary applied.""",
2630
2288
  update_dir=update_dir,
2631
2289
  logger=self._logger,
2632
2290
  update_target=update_target,
2291
+ skip_checks=bool(getattr(self, "_update_skip_checks", False)),
2633
2292
  notify_chat_id=chat_id,
2634
2293
  notify_thread_id=thread_id,
2635
2294
  notify_reply_to=notify_reply_to,
@@ -2773,7 +2432,6 @@ Summary applied.""",
2773
2432
  timeout_seconds: float = 300.0,
2774
2433
  interval_seconds: float = 2.0,
2775
2434
  ) -> None:
2776
-
2777
2435
  async def _watch() -> None:
2778
2436
  deadline = time.monotonic() + timeout_seconds
2779
2437
  while time.monotonic() < deadline: