codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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 (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,11 @@
1
+ from datetime import datetime, timezone
2
+
3
+
4
+ def now_iso_utc_z() -> str:
5
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
6
+
7
+
8
+ now_iso = now_iso_utc_z
9
+
10
+
11
+ __all__ = ["now_iso_utc_z", "now_iso"]
@@ -0,0 +1,18 @@
1
+ """Core type definitions.
2
+
3
+ This module provides type definitions that were previously in Engine.
4
+ """
5
+
6
+ from typing import Any, Callable, Optional
7
+
8
+ # Type aliases for factory functions
9
+ BackendFactory = Callable[[str, Any, Optional[Callable[[dict[str, Any]], Any]]], Any]
10
+ AppServerSupervisorFactory = Callable[
11
+ [str, Optional[Callable[[dict[str, Any]], Any]]], Any
12
+ ]
13
+
14
+
15
+ __all__ = [
16
+ "BackendFactory",
17
+ "AppServerSupervisorFactory",
18
+ ]
@@ -11,6 +11,7 @@ from typing import Optional
11
11
  from urllib.parse import unquote, urlparse
12
12
 
13
13
  from .git_utils import GitError, run_git
14
+ from .update_paths import resolve_update_paths
14
15
 
15
16
 
16
17
  class UpdateInProgressError(RuntimeError):
@@ -55,7 +56,7 @@ def _normalize_update_ref(raw: Optional[str]) -> str:
55
56
 
56
57
 
57
58
  def _update_status_path() -> Path:
58
- return Path.home() / ".codex-autorunner" / "update_status.json"
59
+ return resolve_update_paths().status_path
59
60
 
60
61
 
61
62
  def _write_update_status(status: str, message: str, **extra) -> None:
@@ -134,7 +135,7 @@ def _read_update_status() -> Optional[dict[str, object]]:
134
135
 
135
136
 
136
137
  def _update_lock_path() -> Path:
137
- return Path.home() / ".codex-autorunner" / "update.lock"
138
+ return resolve_update_paths().lock_path
138
139
 
139
140
 
140
141
  def _read_update_lock() -> Optional[dict[str, object]]:
@@ -281,9 +282,7 @@ def _system_update_check(
281
282
  update_cache_dir: Optional[Path] = None,
282
283
  ) -> dict:
283
284
  module_dir = module_dir or Path(__file__).resolve().parent
284
- update_cache_dir = update_cache_dir or (
285
- Path.home() / ".codex-autorunner" / "update_cache"
286
- )
285
+ update_cache_dir = update_cache_dir or resolve_update_paths().cache_dir
287
286
  repo_ref = _normalize_update_ref(repo_ref)
288
287
 
289
288
  repo_root = _resolve_local_repo_root(
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
+ from .state_roots import resolve_global_state_root
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class UpdatePaths:
12
+ status_path: Path
13
+ lock_path: Path
14
+ cache_dir: Path
15
+ compact_status_path: Path
16
+
17
+
18
+ def resolve_update_paths(
19
+ *, config: Optional[Any] = None, repo_root: Optional[Path] = None
20
+ ) -> UpdatePaths:
21
+ """Resolve update status, lock, cache, and compact status paths."""
22
+ root = resolve_global_state_root(config=config, repo_root=repo_root)
23
+ return UpdatePaths(
24
+ status_path=root / "update_status.json",
25
+ lock_path=root / "update.lock",
26
+ cache_dir=root / "update_cache",
27
+ compact_status_path=root / "compact_status.json",
28
+ )
@@ -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,13 +1,17 @@
1
1
  import contextvars
2
+ import importlib
2
3
  import json
3
4
  import logging
4
5
  import os
5
6
  import shlex
6
7
  import shutil
8
+ import subprocess
9
+ from functools import lru_cache
7
10
  from pathlib import Path
8
11
  from typing import (
9
- TYPE_CHECKING,
12
+ Any,
10
13
  Dict,
14
+ Iterable,
11
15
  Mapping,
12
16
  MutableMapping,
13
17
  Optional,
@@ -16,8 +20,83 @@ from typing import (
16
20
  cast,
17
21
  )
18
22
 
19
- if TYPE_CHECKING:
20
- 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")
21
100
 
22
101
 
23
102
  class RepoNotFoundError(Exception):
@@ -68,12 +147,21 @@ def is_within(root: Path, target: Path) -> bool:
68
147
  return False
69
148
 
70
149
 
71
- def atomic_write(path: Path, content: str) -> None:
150
+ def atomic_write(path: Path, content: str, durable: bool = False) -> None:
72
151
  path.parent.mkdir(parents=True, exist_ok=True)
73
152
  tmp_path = path.with_suffix(path.suffix + ".tmp")
74
153
  with tmp_path.open("w", encoding="utf-8") as f:
75
154
  f.write(content)
155
+ if durable:
156
+ f.flush()
157
+ os.fsync(f.fileno())
76
158
  tmp_path.replace(path)
159
+ if durable:
160
+ dir_fd = os.open(path.parent, os.O_RDONLY)
161
+ try:
162
+ os.fsync(dir_fd)
163
+ finally:
164
+ os.close(dir_fd)
77
165
 
78
166
 
79
167
  def read_json(path: Path) -> Optional[dict]:
@@ -96,7 +184,28 @@ def _default_path_prefixes() -> list[str]:
96
184
  str(home / ".opencode" / "bin"), # OpenCode default install
97
185
  str(home / ".local" / "bin"), # Common user-local installs
98
186
  ]
99
- return [p for p in candidates if os.path.isdir(p)]
187
+ repo_candidates: list[str] = []
188
+ repo_root = get_repo_root_context()
189
+ if repo_root is None:
190
+ try:
191
+ repo_root = find_repo_root()
192
+ except RepoNotFoundError:
193
+ repo_root = None
194
+ if repo_root is not None:
195
+ bin_dir = "Scripts" if os.name == "nt" else "bin"
196
+ repo_candidates = [
197
+ str(repo_root / ".venv" / bin_dir),
198
+ str(repo_root / ".codex-autorunner" / "bin"),
199
+ str(repo_root / "bin"),
200
+ ]
201
+ car_shim = repo_root / "car"
202
+ if car_shim.exists():
203
+ repo_candidates.append(str(repo_root))
204
+ return [
205
+ p
206
+ for p in (repo_candidates + candidates)
207
+ if (os.path.isdir(p) or os.path.isfile(p))
208
+ ]
100
209
 
101
210
 
102
211
  def augmented_path(path: Optional[str] = None) -> str:
@@ -148,12 +257,8 @@ def resolve_executable(
148
257
  return resolved
149
258
 
150
259
 
151
- def ensure_executable(binary: str) -> bool:
152
- return resolve_executable(binary) is not None
153
-
154
-
155
- def default_editor() -> str:
156
- return os.environ.get("EDITOR") or "vi"
260
+ def default_editor(*, fallback: str = "vi") -> str:
261
+ return os.environ.get("EDITOR") or fallback
157
262
 
158
263
 
159
264
  def resolve_opencode_binary(raw_command: Optional[str] = None) -> Optional[str]:
@@ -216,9 +321,10 @@ def build_opencode_supervisor(
216
321
  max_handles: Optional[int] = None,
217
322
  idle_ttl_seconds: Optional[float] = None,
218
323
  session_stall_timeout_seconds: Optional[float] = None,
324
+ max_text_chars: Optional[int] = None,
219
325
  base_env: Optional[MutableMapping[str, str]] = None,
220
326
  subagent_models: Optional[Mapping[str, str]] = None,
221
- ) -> Optional["OpenCodeSupervisor"]:
327
+ ) -> Optional[Any]:
222
328
  """
223
329
  Unified factory for building OpenCodeSupervisor instances.
224
330
 
@@ -262,20 +368,24 @@ def build_opencode_supervisor(
262
368
  if password and not username:
263
369
  username = "opencode"
264
370
 
265
- from ..agents.opencode.supervisor import OpenCodeSupervisor
266
-
267
- return OpenCodeSupervisor(
371
+ supervisor_module = importlib.import_module(
372
+ "codex_autorunner.agents.opencode.supervisor"
373
+ )
374
+ supervisor_cls = supervisor_module.OpenCodeSupervisor
375
+ supervisor = supervisor_cls(
268
376
  command,
269
377
  logger=logger,
270
378
  request_timeout=request_timeout,
271
379
  max_handles=max_handles,
272
380
  idle_ttl_seconds=idle_ttl_seconds,
273
381
  session_stall_timeout_seconds=session_stall_timeout_seconds,
382
+ max_text_chars=max_text_chars,
274
383
  username=username if password else None,
275
384
  password=password if password else None,
276
385
  base_env=base_env,
277
386
  subagent_models=subagent_models,
278
387
  )
388
+ return cast(Any, supervisor)
279
389
 
280
390
 
281
391
  def _command_available(
@@ -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
+ ]