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
@@ -522,8 +522,35 @@ def summarize_hub_usage(
522
522
  return repo_id
523
523
  return None
524
524
 
525
+ base_repo_ids = sorted(
526
+ {repo_id for repo_id, _ in repo_map}, key=lambda rid: (-len(rid), rid)
527
+ )
528
+
529
+ def _heuristic_match_base(cwd: Optional[Path]) -> Optional[str]:
530
+ if not cwd:
531
+ return None
532
+ for repo_id in base_repo_ids:
533
+ prefix = f"{repo_id}--"
534
+ if cwd.name.startswith(prefix):
535
+ logger.debug(
536
+ "Heuristic matched cwd %s to base %s via name", cwd, repo_id
537
+ )
538
+ return repo_id
539
+ for part in cwd.parts:
540
+ if part.startswith(prefix):
541
+ logger.debug(
542
+ "Heuristic matched cwd %s to base %s via path part %s",
543
+ cwd,
544
+ repo_id,
545
+ part,
546
+ )
547
+ return repo_id
548
+ return None
549
+
525
550
  for event in iter_token_events(codex_home, since=since, until=until):
526
551
  repo_id = _match_repo(event.cwd)
552
+ if repo_id is None:
553
+ repo_id = _heuristic_match_base(event.cwd)
527
554
  if repo_id is None:
528
555
  unmatched.totals.add(event.delta)
529
556
  unmatched.events += 1
@@ -540,6 +567,8 @@ def summarize_hub_usage(
540
567
  [path for _, path in repo_map], since=since, until=until
541
568
  ):
542
569
  repo_id = _match_repo(event.cwd)
570
+ if repo_id is None:
571
+ repo_id = _heuristic_match_base(event.cwd)
543
572
  if repo_id is None:
544
573
  continue
545
574
  summary = per_repo[repo_id]
@@ -1323,6 +1352,31 @@ class UsageSeriesCache:
1323
1352
  return repo_id
1324
1353
  return None
1325
1354
 
1355
+ base_repo_ids = sorted(
1356
+ {repo_id for repo_id, _ in repo_map}, key=lambda rid: (-len(rid), rid)
1357
+ )
1358
+
1359
+ def _heuristic_match_base(cwd: Optional[Path]) -> Optional[str]:
1360
+ if not cwd:
1361
+ return None
1362
+ for repo_id in base_repo_ids:
1363
+ prefix = f"{repo_id}--"
1364
+ if cwd.name.startswith(prefix):
1365
+ logger.debug(
1366
+ "Heuristic matched cwd %s to base %s via name", cwd, repo_id
1367
+ )
1368
+ return repo_id
1369
+ for part in cwd.parts:
1370
+ if part.startswith(prefix):
1371
+ logger.debug(
1372
+ "Heuristic matched cwd %s to base %s via path part %s",
1373
+ cwd,
1374
+ repo_id,
1375
+ part,
1376
+ )
1377
+ return repo_id
1378
+ return None
1379
+
1326
1380
  rollups = cast(Dict[str, Any], payload.get("summary", {}).get("by_cwd", {}))
1327
1381
  per_repo: Dict[str, _SummaryAccumulator] = {
1328
1382
  repo_id: _SummaryAccumulator() for repo_id, _ in repo_map
@@ -1336,6 +1390,8 @@ class UsageSeriesCache:
1336
1390
  logger.debug("Failed to create Path from cwd %r: %s", cwd, exc)
1337
1391
  cwd_path = None
1338
1392
  repo_id = _match_repo(cwd_path)
1393
+ if repo_id is None:
1394
+ repo_id = _heuristic_match_base(cwd_path)
1339
1395
  if repo_id is None:
1340
1396
  unmatched.add_entry(entry)
1341
1397
  else:
@@ -1513,7 +1569,8 @@ class UsageSeriesCache:
1513
1569
  }
1514
1570
 
1515
1571
 
1516
- _USAGE_SERIES_CACHES: Dict[str, UsageSeriesCache] = {}
1572
+ _USAGE_SERIES_CACHES: Dict[Tuple[str, str], UsageSeriesCache] = {}
1573
+ _REPO_USAGE_CACHE_MIGRATED: set[str] = set()
1517
1574
 
1518
1575
 
1519
1576
  def _build_series_entries(
@@ -1781,9 +1838,80 @@ def _build_hub_opencode_series(
1781
1838
  }
1782
1839
 
1783
1840
 
1784
- def get_usage_series_cache(codex_home: Path) -> UsageSeriesCache:
1785
- cache_path = _default_usage_series_cache_path(codex_home)
1786
- key = str(cache_path)
1841
+ def _resolve_usage_cache_paths(
1842
+ *,
1843
+ config: Optional[Any] = None,
1844
+ repo_root: Optional[Path] = None,
1845
+ codex_home: Optional[Path] = None,
1846
+ ) -> Tuple[Path, Path, str, Path]:
1847
+ codex_root = (codex_home or default_codex_home()).expanduser()
1848
+ cache_scope = "global"
1849
+ cache_path = _default_usage_series_cache_path(codex_root)
1850
+ global_cache_root = codex_root
1851
+ usage_cfg: Optional[Any] = None
1852
+ if config is not None:
1853
+ usage_cfg = getattr(config, "usage", None)
1854
+ if usage_cfg is None:
1855
+ raw = getattr(config, "raw", None)
1856
+ if isinstance(raw, dict):
1857
+ usage_cfg = raw.get("usage")
1858
+ if usage_cfg:
1859
+ cache_scope = str(getattr(usage_cfg, "cache_scope", "global") or "global")
1860
+ cache_scope = cache_scope.lower().strip() or "global"
1861
+ global_root = getattr(usage_cfg, "global_cache_root", None)
1862
+ repo_cache_path = getattr(usage_cfg, "repo_cache_path", None)
1863
+ if global_root:
1864
+ global_cache_root = Path(global_root)
1865
+ if cache_scope == "repo":
1866
+ if repo_cache_path:
1867
+ cache_path = Path(repo_cache_path)
1868
+ elif repo_root:
1869
+ cache_path = (
1870
+ repo_root
1871
+ / ".codex-autorunner"
1872
+ / "usage"
1873
+ / "usage_series_cache.json"
1874
+ )
1875
+ else:
1876
+ if global_root:
1877
+ cache_path = _default_usage_series_cache_path(global_cache_root)
1878
+ else:
1879
+ cache_path = _default_usage_series_cache_path(codex_root)
1880
+ return codex_root, cache_path, cache_scope, Path(global_cache_root)
1881
+
1882
+
1883
+ def _maybe_migrate_usage_cache(cache_path: Path, global_cache_path: Path) -> None:
1884
+ cache_key = str(cache_path)
1885
+ if cache_key in _REPO_USAGE_CACHE_MIGRATED:
1886
+ return
1887
+ _REPO_USAGE_CACHE_MIGRATED.add(cache_key)
1888
+ if cache_path.exists() or not global_cache_path.exists():
1889
+ return
1890
+ try:
1891
+ payload = global_cache_path.read_text(encoding="utf-8")
1892
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
1893
+ tmp_path = cache_path.with_suffix(".tmp")
1894
+ tmp_path.write_text(payload, encoding="utf-8")
1895
+ tmp_path.replace(cache_path)
1896
+ logger.warning(
1897
+ "Imported global usage cache into repo cache at %s from %s",
1898
+ cache_path,
1899
+ global_cache_path,
1900
+ )
1901
+ except OSError as exc:
1902
+ logger.warning(
1903
+ "Failed to import global usage cache from %s to %s: %s",
1904
+ global_cache_path,
1905
+ cache_path,
1906
+ exc,
1907
+ )
1908
+
1909
+
1910
+ def get_usage_series_cache(
1911
+ codex_home: Path, *, cache_path: Optional[Path] = None
1912
+ ) -> UsageSeriesCache:
1913
+ cache_path = cache_path or _default_usage_series_cache_path(codex_home)
1914
+ key = (str(cache_path), str(codex_home))
1787
1915
  cache = _USAGE_SERIES_CACHES.get(key)
1788
1916
  if cache is None:
1789
1917
  cache = UsageSeriesCache(codex_home, cache_path)
@@ -1795,13 +1923,19 @@ def get_repo_usage_series_cached(
1795
1923
  repo_root: Path,
1796
1924
  codex_home: Optional[Path] = None,
1797
1925
  *,
1926
+ config: Optional[Any] = None,
1798
1927
  since: Optional[datetime] = None,
1799
1928
  until: Optional[datetime] = None,
1800
1929
  bucket: str = "day",
1801
1930
  segment: str = "none",
1802
1931
  ) -> Tuple[Dict[str, object], str]:
1803
- codex_root = (codex_home or default_codex_home()).expanduser()
1804
- cache = get_usage_series_cache(codex_root)
1932
+ codex_root, cache_path, cache_scope, global_cache_root = _resolve_usage_cache_paths(
1933
+ config=config, repo_root=repo_root, codex_home=codex_home
1934
+ )
1935
+ if cache_scope == "repo":
1936
+ global_cache_path = _default_usage_series_cache_path(global_cache_root)
1937
+ _maybe_migrate_usage_cache(cache_path, global_cache_path)
1938
+ cache = get_usage_series_cache(codex_root, cache_path=cache_path)
1805
1939
  if segment == "agent":
1806
1940
  codex_series, status = cache.get_repo_series(
1807
1941
  repo_root, since=since, until=until, bucket=bucket, segment="none"
@@ -1829,11 +1963,17 @@ def get_repo_usage_summary_cached(
1829
1963
  repo_root: Path,
1830
1964
  codex_home: Optional[Path] = None,
1831
1965
  *,
1966
+ config: Optional[Any] = None,
1832
1967
  since: Optional[datetime] = None,
1833
1968
  until: Optional[datetime] = None,
1834
1969
  ) -> Tuple[UsageSummary, str]:
1835
- codex_root = (codex_home or default_codex_home()).expanduser()
1836
- cache = get_usage_series_cache(codex_root)
1970
+ codex_root, cache_path, cache_scope, global_cache_root = _resolve_usage_cache_paths(
1971
+ config=config, repo_root=repo_root, codex_home=codex_home
1972
+ )
1973
+ if cache_scope == "repo":
1974
+ global_cache_path = _default_usage_series_cache_path(global_cache_root)
1975
+ _maybe_migrate_usage_cache(cache_path, global_cache_path)
1976
+ cache = get_usage_series_cache(codex_root, cache_path=cache_path)
1837
1977
  summary, status = cache.get_repo_summary(repo_root, since=since, until=until)
1838
1978
  opencode_summary = summarize_opencode_repo_usage(
1839
1979
  repo_root, since=since, until=until
@@ -1852,13 +1992,19 @@ def get_hub_usage_series_cached(
1852
1992
  repo_map: List[Tuple[str, Path]],
1853
1993
  codex_home: Optional[Path] = None,
1854
1994
  *,
1995
+ config: Optional[Any] = None,
1855
1996
  since: Optional[datetime] = None,
1856
1997
  until: Optional[datetime] = None,
1857
1998
  bucket: str = "day",
1858
1999
  segment: str = "none",
1859
2000
  ) -> Tuple[Dict[str, object], str]:
1860
- codex_root = (codex_home or default_codex_home()).expanduser()
1861
- cache = get_usage_series_cache(codex_root)
2001
+ codex_root, cache_path, cache_scope, global_cache_root = _resolve_usage_cache_paths(
2002
+ config=config, repo_root=None, codex_home=codex_home
2003
+ )
2004
+ if cache_scope == "repo":
2005
+ global_cache_path = _default_usage_series_cache_path(global_cache_root)
2006
+ _maybe_migrate_usage_cache(cache_path, global_cache_path)
2007
+ cache = get_usage_series_cache(codex_root, cache_path=cache_path)
1862
2008
  if segment == "agent":
1863
2009
  codex_series, status = cache.get_hub_series(
1864
2010
  repo_map, since=since, until=until, bucket=bucket, segment="none"
@@ -1886,11 +2032,17 @@ def get_hub_usage_summary_cached(
1886
2032
  repo_map: List[Tuple[str, Path]],
1887
2033
  codex_home: Optional[Path] = None,
1888
2034
  *,
2035
+ config: Optional[Any] = None,
1889
2036
  since: Optional[datetime] = None,
1890
2037
  until: Optional[datetime] = None,
1891
2038
  ) -> Tuple[Dict[str, UsageSummary], UsageSummary, str]:
1892
- codex_root = (codex_home or default_codex_home()).expanduser()
1893
- cache = get_usage_series_cache(codex_root)
2039
+ codex_root, cache_path, cache_scope, global_cache_root = _resolve_usage_cache_paths(
2040
+ config=config, repo_root=None, codex_home=codex_home
2041
+ )
2042
+ if cache_scope == "repo":
2043
+ global_cache_path = _default_usage_series_cache_path(global_cache_root)
2044
+ _maybe_migrate_usage_cache(cache_path, global_cache_path)
2045
+ cache = get_usage_series_cache(codex_root, cache_path=cache_path)
1894
2046
  per_repo, unmatched, status = cache.get_hub_summary(
1895
2047
  repo_map, since=since, until=until
1896
2048
  )
@@ -1,12 +1,17 @@
1
+ import contextvars
2
+ import importlib
1
3
  import json
2
4
  import logging
3
5
  import os
4
6
  import shlex
5
7
  import shutil
8
+ import subprocess
9
+ from functools import lru_cache
6
10
  from pathlib import Path
7
11
  from typing import (
8
- TYPE_CHECKING,
12
+ Any,
9
13
  Dict,
14
+ Iterable,
10
15
  Mapping,
11
16
  MutableMapping,
12
17
  Optional,
@@ -15,16 +20,115 @@ from typing import (
15
20
  cast,
16
21
  )
17
22
 
18
- if TYPE_CHECKING:
19
- from ..agents.opencode.supervisor import OpenCodeSupervisor
23
+ SUBCOMMAND_HINTS = ("exec", "resume")
24
+
25
+
26
+ def extract_flag_value(args: Iterable[str], flag: str) -> Optional[str]:
27
+ if not args:
28
+ return None
29
+ for arg in args:
30
+ if not isinstance(arg, str):
31
+ continue
32
+ if arg.startswith(f"{flag}="):
33
+ return arg.split("=", 1)[1] or None
34
+ args_list = [str(a) for a in args]
35
+ for idx, arg in enumerate(args_list):
36
+ if arg == flag and idx + 1 < len(args_list):
37
+ return args_list[idx + 1]
38
+ return None
39
+
40
+
41
+ def inject_flag(
42
+ args: Iterable[str],
43
+ flag: str,
44
+ value: Optional[str],
45
+ *,
46
+ subcommands: Iterable[str] = SUBCOMMAND_HINTS,
47
+ ) -> list[str]:
48
+ if not value:
49
+ return [str(a) for a in args]
50
+ args_list = [str(a) for a in args]
51
+ if extract_flag_value(args_list, flag):
52
+ return args_list
53
+ insert_at = None
54
+ for cmd in subcommands:
55
+ try:
56
+ insert_at = args_list.index(cmd)
57
+ break
58
+ except ValueError:
59
+ continue
60
+ if insert_at is None:
61
+ if args_list and not args_list[0].startswith("-"):
62
+ return [args_list[0], flag, value] + args_list[1:]
63
+ return [flag, value] + args_list
64
+ return args_list[:insert_at] + [flag, value] + args_list[insert_at:]
65
+
66
+
67
+ def apply_codex_options(
68
+ args: Iterable[str],
69
+ *,
70
+ model: Optional[str] = None,
71
+ reasoning: Optional[str] = None,
72
+ supports_reasoning: Optional[bool] = None,
73
+ ) -> list[str]:
74
+ with_model = inject_flag(args, "--model", model)
75
+ if reasoning and supports_reasoning is False:
76
+ return with_model
77
+ return inject_flag(with_model, "--reasoning", reasoning)
78
+
79
+
80
+ def _read_help_text(binary: str) -> str:
81
+ try:
82
+ result = subprocess.run(
83
+ [binary, "--help"],
84
+ capture_output=True,
85
+ text=True,
86
+ check=False,
87
+ )
88
+ except FileNotFoundError:
89
+ return ""
90
+ return "\n".join(filter(None, [result.stdout, result.stderr]))
91
+
92
+
93
+ @lru_cache(maxsize=8)
94
+ def supports_flag(binary: str, flag: str) -> bool:
95
+ return flag in _read_help_text(binary)
96
+
97
+
98
+ def supports_reasoning(binary: str) -> bool:
99
+ return supports_flag(binary, "--reasoning")
20
100
 
21
101
 
22
102
  class RepoNotFoundError(Exception):
23
103
  pass
24
104
 
25
105
 
26
- def find_repo_root(start: Path) -> Path:
27
- current = start.resolve()
106
+ _repo_root_ctx: contextvars.ContextVar[Optional[Path]] = contextvars.ContextVar(
107
+ "codex_autorunner_repo_root", default=None
108
+ )
109
+
110
+
111
+ def set_repo_root_context(
112
+ repo_root: Optional[Path],
113
+ ) -> contextvars.Token[Optional[Path]]:
114
+ """Set the current repo root for the active context."""
115
+ return _repo_root_ctx.set(repo_root.resolve() if repo_root else None)
116
+
117
+
118
+ def reset_repo_root_context(token: contextvars.Token[Optional[Path]]) -> None:
119
+ _repo_root_ctx.reset(token)
120
+
121
+
122
+ def get_repo_root_context() -> Optional[Path]:
123
+ return _repo_root_ctx.get()
124
+
125
+
126
+ def find_repo_root(start: Optional[Path] = None) -> Path:
127
+ ctx_root = get_repo_root_context()
128
+ if ctx_root is not None and (ctx_root / ".git").exists():
129
+ return ctx_root
130
+
131
+ current = (start or Path.cwd()).resolve()
28
132
  for parent in [current] + list(current.parents):
29
133
  if (parent / ".git").exists():
30
134
  return parent
@@ -127,8 +231,8 @@ def ensure_executable(binary: str) -> bool:
127
231
  return resolve_executable(binary) is not None
128
232
 
129
233
 
130
- def default_editor() -> str:
131
- return os.environ.get("EDITOR") or "vi"
234
+ def default_editor(*, fallback: str = "vi") -> str:
235
+ return os.environ.get("EDITOR") or fallback
132
236
 
133
237
 
134
238
  def resolve_opencode_binary(raw_command: Optional[str] = None) -> Optional[str]:
@@ -190,9 +294,10 @@ def build_opencode_supervisor(
190
294
  request_timeout: Optional[float] = None,
191
295
  max_handles: Optional[int] = None,
192
296
  idle_ttl_seconds: Optional[float] = None,
297
+ session_stall_timeout_seconds: Optional[float] = None,
193
298
  base_env: Optional[MutableMapping[str, str]] = None,
194
299
  subagent_models: Optional[Mapping[str, str]] = None,
195
- ) -> Optional["OpenCodeSupervisor"]:
300
+ ) -> Optional[Any]:
196
301
  """
197
302
  Unified factory for building OpenCodeSupervisor instances.
198
303
 
@@ -236,19 +341,23 @@ def build_opencode_supervisor(
236
341
  if password and not username:
237
342
  username = "opencode"
238
343
 
239
- from ..agents.opencode.supervisor import OpenCodeSupervisor
240
-
241
- return OpenCodeSupervisor(
344
+ supervisor_module = importlib.import_module(
345
+ "codex_autorunner.agents.opencode.supervisor"
346
+ )
347
+ supervisor_cls = supervisor_module.OpenCodeSupervisor
348
+ supervisor = supervisor_cls(
242
349
  command,
243
350
  logger=logger,
244
351
  request_timeout=request_timeout,
245
352
  max_handles=max_handles,
246
353
  idle_ttl_seconds=idle_ttl_seconds,
354
+ session_stall_timeout_seconds=session_stall_timeout_seconds,
247
355
  username=username if password else None,
248
356
  password=password if password else None,
249
357
  base_env=base_env,
250
358
  subagent_models=subagent_models,
251
359
  )
360
+ return cast(Any, supervisor)
252
361
 
253
362
 
254
363
  def _command_available(
@@ -57,7 +57,7 @@ def discover_and_init(hub_config: HubConfig) -> Tuple[Manifest, List[DiscoveryRe
57
57
 
58
58
  def _record_repo(repo_entry: ManifestRepo, *, added: bool) -> None:
59
59
  repo_path = (hub_config.root / repo_entry.path).resolve()
60
- initialized = (repo_path / ".codex-autorunner" / "state.sqlite3").exists()
60
+ initialized = (repo_path / ".codex-autorunner" / "tickets").exists()
61
61
  init_error: Optional[str] = None
62
62
  if hub_config.auto_init_missing and repo_path.exists() and not initialized:
63
63
  try:
@@ -176,9 +176,7 @@ def discover_and_init(hub_config: HubConfig) -> Tuple[Manifest, List[DiscoveryRe
176
176
  absolute_path=repo_path,
177
177
  added_to_manifest=False,
178
178
  exists_on_disk=repo_path.exists(),
179
- initialized=(
180
- repo_path / ".codex-autorunner" / "state.sqlite3"
181
- ).exists(),
179
+ initialized=(repo_path / ".codex-autorunner" / "tickets").exists(),
182
180
  init_error=None,
183
181
  )
184
182
  )
@@ -0,0 +1,17 @@
1
+ from .service import (
2
+ REVIEW_PROMPT,
3
+ REVIEW_PROMPT_SPEC_PROGRESS,
4
+ ReviewBusyError,
5
+ ReviewConflictError,
6
+ ReviewError,
7
+ ReviewService,
8
+ )
9
+
10
+ __all__ = [
11
+ "REVIEW_PROMPT",
12
+ "REVIEW_PROMPT_SPEC_PROGRESS",
13
+ "ReviewBusyError",
14
+ "ReviewConflictError",
15
+ "ReviewError",
16
+ "ReviewService",
17
+ ]
@@ -10,15 +10,20 @@ import zipfile
10
10
  from pathlib import Path
11
11
  from typing import Any, Optional
12
12
 
13
- from ..agents.opencode.run_prompt import OpenCodeRunConfig, run_opencode_prompt
14
- from ..agents.opencode.supervisor import OpenCodeSupervisor
15
- from ..agents.registry import has_capability, validate_agent_id
16
- from ..integrations.app_server.supervisor import WorkspaceAppServerSupervisor
17
- from .config import RepoConfig
18
- from .engine import Engine
19
- from .locks import FileLock, FileLockBusy, FileLockError, process_alive, read_lock_info
20
- from .state import now_iso
21
- from .utils import atomic_write, read_json
13
+ from ...agents.opencode.run_prompt import OpenCodeRunConfig, run_opencode_prompt
14
+ from ...agents.opencode.supervisor import OpenCodeSupervisor
15
+ from ...agents.registry import has_capability, validate_agent_id
16
+ from ...core.config import RepoConfig
17
+ from ...core.engine import Engine
18
+ from ...core.locks import (
19
+ FileLock,
20
+ FileLockBusy,
21
+ FileLockError,
22
+ process_alive,
23
+ read_lock_info,
24
+ )
25
+ from ...core.state import now_iso
26
+ from ...core.utils import atomic_write, read_json
22
27
 
23
28
  REVIEW_STATE_VERSION = 1
24
29
  REVIEW_TIMEOUT_SECONDS = 3600
@@ -391,7 +396,7 @@ class ReviewService:
391
396
  engine: Engine,
392
397
  *,
393
398
  opencode_supervisor: Optional[OpenCodeSupervisor] = None,
394
- app_server_supervisor: Optional[WorkspaceAppServerSupervisor] = None,
399
+ app_server_supervisor: Optional[Any] = None,
395
400
  logger: Optional[logging.Logger] = None,
396
401
  ) -> None:
397
402
  self.engine = engine
@@ -0,0 +1,3 @@
1
+ from .definition import build_ticket_flow_definition
2
+
3
+ __all__ = ["build_ticket_flow_definition"]
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional
5
+
6
+ from ...core.flows.definition import EmitEventFn, FlowDefinition, StepOutcome
7
+ from ...core.flows.models import FlowEventType, FlowRunRecord
8
+ from ...core.utils import find_repo_root
9
+ from ...tickets import (
10
+ DEFAULT_MAX_TOTAL_TURNS,
11
+ AgentPool,
12
+ TicketRunConfig,
13
+ TicketRunner,
14
+ )
15
+
16
+
17
+ def build_ticket_flow_definition(*, agent_pool: AgentPool) -> FlowDefinition:
18
+ """Build the single-step ticket runner flow.
19
+
20
+ The flow is intentionally simple: each step executes at most one agent turn
21
+ against the current ticket, and re-schedules itself until paused or complete.
22
+ """
23
+
24
+ async def _ticket_turn_step(
25
+ record: FlowRunRecord,
26
+ input_data: Dict[str, Any],
27
+ emit_event: Optional[EmitEventFn],
28
+ ) -> StepOutcome:
29
+ # Namespace all state under `ticket_engine` to avoid collisions with other flows.
30
+ engine_state = (
31
+ record.state.get("ticket_engine")
32
+ if isinstance(record.state, dict)
33
+ else None
34
+ )
35
+ engine_state = dict(engine_state) if isinstance(engine_state, dict) else {}
36
+
37
+ repo_root = find_repo_root()
38
+ workspace_root = Path(input_data.get("workspace_root") or repo_root)
39
+ ticket_dir = Path(input_data.get("ticket_dir") or ".codex-autorunner/tickets")
40
+ runs_dir = Path(input_data.get("runs_dir") or ".codex-autorunner/runs")
41
+ max_total_turns = int(
42
+ input_data.get("max_total_turns") or DEFAULT_MAX_TOTAL_TURNS
43
+ )
44
+ max_lint_retries = int(input_data.get("max_lint_retries") or 3)
45
+ max_commit_retries = int(input_data.get("max_commit_retries") or 2)
46
+ auto_commit = bool(
47
+ input_data.get("auto_commit") if "auto_commit" in input_data else True
48
+ )
49
+
50
+ runner = TicketRunner(
51
+ workspace_root=workspace_root,
52
+ run_id=str(record.id),
53
+ config=TicketRunConfig(
54
+ ticket_dir=ticket_dir,
55
+ runs_dir=runs_dir,
56
+ max_total_turns=max_total_turns,
57
+ max_lint_retries=max_lint_retries,
58
+ max_commit_retries=max_commit_retries,
59
+ auto_commit=auto_commit,
60
+ ),
61
+ agent_pool=agent_pool,
62
+ )
63
+
64
+ if emit_event is not None:
65
+ emit_event(FlowEventType.STEP_PROGRESS, {"message": "Running ticket turn"})
66
+ result = await runner.step(engine_state, emit_event=emit_event)
67
+ out_state = dict(record.state or {})
68
+ out_state["ticket_engine"] = result.state
69
+
70
+ if result.status == "completed":
71
+ return StepOutcome.complete(output=out_state)
72
+ if result.status == "paused":
73
+ return StepOutcome.pause(output=out_state)
74
+ if result.status == "failed":
75
+ return StepOutcome.fail(
76
+ error=result.reason or "Ticket engine failed", output=out_state
77
+ )
78
+ return StepOutcome.continue_to(next_steps={"ticket_turn"}, output=out_state)
79
+
80
+ return FlowDefinition(
81
+ flow_type="ticket_flow",
82
+ name="Ticket Flow",
83
+ description="Ticket-based agent workflow runner",
84
+ initial_step="ticket_turn",
85
+ input_schema={
86
+ "type": "object",
87
+ "properties": {
88
+ "workspace_root": {"type": "string"},
89
+ "ticket_dir": {"type": "string"},
90
+ "runs_dir": {"type": "string"},
91
+ "max_total_turns": {"type": "integer"},
92
+ "max_lint_retries": {"type": "integer"},
93
+ "max_commit_retries": {"type": "integer"},
94
+ "auto_commit": {"type": "boolean"},
95
+ },
96
+ },
97
+ steps={"ticket_turn": _ticket_turn_step},
98
+ )
@@ -0,0 +1,17 @@
1
+ from .codex_adapter import CodexAdapterOrchestrator
2
+ from .codex_backend import CodexAppServerBackend
3
+ from .opencode_adapter import OpenCodeAdapterOrchestrator
4
+ from .opencode_backend import OpenCodeBackend
5
+ from .wiring import (
6
+ build_agent_backend_factory,
7
+ build_app_server_supervisor_factory,
8
+ )
9
+
10
+ __all__ = [
11
+ "CodexAdapterOrchestrator",
12
+ "CodexAppServerBackend",
13
+ "OpenCodeAdapterOrchestrator",
14
+ "OpenCodeBackend",
15
+ "build_agent_backend_factory",
16
+ "build_app_server_supervisor_factory",
17
+ ]