codex-autorunner 1.0.0__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 (170) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/constants.py +3 -0
  4. codex_autorunner/agents/opencode/harness.py +6 -1
  5. codex_autorunner/agents/opencode/runtime.py +59 -18
  6. codex_autorunner/agents/registry.py +22 -3
  7. codex_autorunner/bootstrap.py +7 -3
  8. codex_autorunner/cli.py +5 -1174
  9. codex_autorunner/codex_cli.py +20 -84
  10. codex_autorunner/core/__init__.py +4 -0
  11. codex_autorunner/core/about_car.py +6 -1
  12. codex_autorunner/core/app_server_ids.py +59 -0
  13. codex_autorunner/core/app_server_threads.py +11 -2
  14. codex_autorunner/core/app_server_utils.py +165 -0
  15. codex_autorunner/core/archive.py +349 -0
  16. codex_autorunner/core/codex_runner.py +6 -2
  17. codex_autorunner/core/config.py +197 -3
  18. codex_autorunner/core/drafts.py +58 -4
  19. codex_autorunner/core/engine.py +1329 -680
  20. codex_autorunner/core/exceptions.py +4 -0
  21. codex_autorunner/core/flows/controller.py +25 -1
  22. codex_autorunner/core/flows/models.py +13 -0
  23. codex_autorunner/core/flows/reasons.py +52 -0
  24. codex_autorunner/core/flows/reconciler.py +131 -0
  25. codex_autorunner/core/flows/runtime.py +35 -4
  26. codex_autorunner/core/flows/store.py +83 -0
  27. codex_autorunner/core/flows/transition.py +5 -0
  28. codex_autorunner/core/flows/ux_helpers.py +257 -0
  29. codex_autorunner/core/git_utils.py +62 -0
  30. codex_autorunner/core/hub.py +121 -7
  31. codex_autorunner/core/notifications.py +14 -2
  32. codex_autorunner/core/ports/__init__.py +28 -0
  33. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +11 -3
  34. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  35. codex_autorunner/{integrations/agents → core/ports}/run_event.py +22 -2
  36. codex_autorunner/core/state_roots.py +57 -0
  37. codex_autorunner/core/supervisor_protocol.py +15 -0
  38. codex_autorunner/core/text_delta_coalescer.py +54 -0
  39. codex_autorunner/core/ticket_linter_cli.py +201 -0
  40. codex_autorunner/core/ticket_manager_cli.py +432 -0
  41. codex_autorunner/core/update.py +4 -5
  42. codex_autorunner/core/update_paths.py +28 -0
  43. codex_autorunner/core/usage.py +164 -12
  44. codex_autorunner/core/utils.py +91 -9
  45. codex_autorunner/flows/review/__init__.py +17 -0
  46. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  47. codex_autorunner/flows/ticket_flow/definition.py +9 -2
  48. codex_autorunner/integrations/agents/__init__.py +9 -19
  49. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  50. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  51. codex_autorunner/integrations/agents/codex_backend.py +158 -17
  52. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  53. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  54. codex_autorunner/integrations/agents/runner.py +91 -0
  55. codex_autorunner/integrations/agents/wiring.py +271 -0
  56. codex_autorunner/integrations/app_server/client.py +7 -60
  57. codex_autorunner/integrations/app_server/env.py +2 -107
  58. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  59. codex_autorunner/integrations/telegram/adapter.py +65 -0
  60. codex_autorunner/integrations/telegram/config.py +46 -0
  61. codex_autorunner/integrations/telegram/constants.py +1 -1
  62. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1203 -66
  64. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +4 -3
  65. codex_autorunner/integrations/telegram/handlers/commands_spec.py +8 -2
  66. codex_autorunner/integrations/telegram/handlers/messages.py +1 -0
  67. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  68. codex_autorunner/integrations/telegram/helpers.py +24 -1
  69. codex_autorunner/integrations/telegram/service.py +15 -10
  70. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +329 -40
  71. codex_autorunner/integrations/telegram/transport.py +3 -1
  72. codex_autorunner/routes/__init__.py +37 -76
  73. codex_autorunner/routes/agents.py +2 -137
  74. codex_autorunner/routes/analytics.py +2 -238
  75. codex_autorunner/routes/app_server.py +2 -131
  76. codex_autorunner/routes/base.py +2 -596
  77. codex_autorunner/routes/file_chat.py +4 -833
  78. codex_autorunner/routes/flows.py +4 -977
  79. codex_autorunner/routes/messages.py +4 -456
  80. codex_autorunner/routes/repos.py +2 -196
  81. codex_autorunner/routes/review.py +2 -147
  82. codex_autorunner/routes/sessions.py +2 -175
  83. codex_autorunner/routes/settings.py +2 -168
  84. codex_autorunner/routes/shared.py +2 -275
  85. codex_autorunner/routes/system.py +4 -193
  86. codex_autorunner/routes/usage.py +2 -86
  87. codex_autorunner/routes/voice.py +2 -119
  88. codex_autorunner/routes/workspace.py +2 -270
  89. codex_autorunner/server.py +2 -2
  90. codex_autorunner/static/agentControls.js +40 -11
  91. codex_autorunner/static/app.js +11 -3
  92. codex_autorunner/static/archive.js +826 -0
  93. codex_autorunner/static/archiveApi.js +37 -0
  94. codex_autorunner/static/autoRefresh.js +7 -7
  95. codex_autorunner/static/dashboard.js +224 -171
  96. codex_autorunner/static/hub.js +112 -94
  97. codex_autorunner/static/index.html +80 -33
  98. codex_autorunner/static/messages.js +486 -83
  99. codex_autorunner/static/preserve.js +17 -0
  100. codex_autorunner/static/settings.js +125 -6
  101. codex_autorunner/static/smartRefresh.js +52 -0
  102. codex_autorunner/static/styles.css +1373 -101
  103. codex_autorunner/static/tabs.js +152 -11
  104. codex_autorunner/static/terminal.js +18 -0
  105. codex_autorunner/static/ticketEditor.js +99 -5
  106. codex_autorunner/static/tickets.js +760 -87
  107. codex_autorunner/static/utils.js +11 -0
  108. codex_autorunner/static/workspace.js +133 -40
  109. codex_autorunner/static/workspaceFileBrowser.js +9 -9
  110. codex_autorunner/surfaces/__init__.py +5 -0
  111. codex_autorunner/surfaces/cli/__init__.py +6 -0
  112. codex_autorunner/surfaces/cli/cli.py +1224 -0
  113. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  114. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  115. codex_autorunner/surfaces/web/__init__.py +1 -0
  116. codex_autorunner/surfaces/web/app.py +2019 -0
  117. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  118. codex_autorunner/surfaces/web/middleware.py +587 -0
  119. codex_autorunner/surfaces/web/pty_session.py +370 -0
  120. codex_autorunner/surfaces/web/review.py +6 -0
  121. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  122. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  123. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  124. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  125. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  126. codex_autorunner/surfaces/web/routes/base.py +615 -0
  127. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  128. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  129. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  130. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  131. codex_autorunner/surfaces/web/routes/review.py +148 -0
  132. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  133. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  134. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  135. codex_autorunner/surfaces/web/routes/system.py +196 -0
  136. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  137. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  138. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  139. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  140. codex_autorunner/surfaces/web/schemas.py +417 -0
  141. codex_autorunner/surfaces/web/static_assets.py +490 -0
  142. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  143. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  144. codex_autorunner/tickets/__init__.py +8 -1
  145. codex_autorunner/tickets/agent_pool.py +26 -4
  146. codex_autorunner/tickets/files.py +6 -2
  147. codex_autorunner/tickets/models.py +3 -1
  148. codex_autorunner/tickets/outbox.py +12 -0
  149. codex_autorunner/tickets/runner.py +63 -5
  150. codex_autorunner/web/__init__.py +5 -1
  151. codex_autorunner/web/app.py +2 -1949
  152. codex_autorunner/web/hub_jobs.py +2 -191
  153. codex_autorunner/web/middleware.py +2 -586
  154. codex_autorunner/web/pty_session.py +2 -369
  155. codex_autorunner/web/runner_manager.py +2 -24
  156. codex_autorunner/web/schemas.py +2 -376
  157. codex_autorunner/web/static_assets.py +4 -441
  158. codex_autorunner/web/static_refresh.py +2 -85
  159. codex_autorunner/web/terminal_sessions.py +2 -77
  160. codex_autorunner/workspace/paths.py +49 -33
  161. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  162. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  163. codex_autorunner/core/static_assets.py +0 -55
  164. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  165. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  166. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  167. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +0 -0
  168. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  169. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  170. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,349 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Iterable, Literal, Optional
10
+
11
+ from .git_utils import git_branch, git_head_sha
12
+ from .state import now_iso
13
+ from .utils import atomic_write
14
+
15
+ ArchiveStatus = Literal["complete", "partial", "failed"]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ArchiveResult:
20
+ snapshot_id: str
21
+ snapshot_path: Path
22
+ meta_path: Path
23
+ status: ArchiveStatus
24
+ file_count: int
25
+ total_bytes: int
26
+ flow_run_count: int
27
+ latest_flow_run_id: Optional[str]
28
+ missing_paths: tuple[str, ...]
29
+ skipped_symlinks: tuple[str, ...]
30
+
31
+
32
+ def _snapshot_timestamp() -> str:
33
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
34
+
35
+
36
+ _BRANCH_SANITIZE_RE = re.compile(r"[^a-zA-Z0-9._-]+")
37
+
38
+
39
+ def _sanitize_branch(branch: Optional[str]) -> str:
40
+ if not branch:
41
+ return "unknown"
42
+ cleaned = _BRANCH_SANITIZE_RE.sub("-", branch.strip())
43
+ cleaned = cleaned.strip("-")
44
+ return cleaned or "unknown"
45
+
46
+
47
+ def _is_within(root: Path, target: Path) -> bool:
48
+ try:
49
+ return target.resolve().is_relative_to(root.resolve())
50
+ except FileNotFoundError:
51
+ return False
52
+
53
+
54
+ def _copy_file(src: Path, dest: Path, stats: dict[str, int]) -> None:
55
+ dest.parent.mkdir(parents=True, exist_ok=True)
56
+ shutil.copy2(src, dest)
57
+ stats["file_count"] += 1
58
+ stats["total_bytes"] += dest.stat().st_size
59
+
60
+
61
+ def _copy_tree(
62
+ src_dir: Path,
63
+ dest_dir: Path,
64
+ worktree_root: Path,
65
+ stats: dict[str, int],
66
+ *,
67
+ visited: set[Path],
68
+ skipped_symlinks: list[str],
69
+ ) -> None:
70
+ real_dir = src_dir.resolve()
71
+ if real_dir in visited:
72
+ return
73
+ visited.add(real_dir)
74
+ try:
75
+ dest_dir.mkdir(parents=True, exist_ok=True)
76
+ for child in sorted(src_dir.iterdir(), key=lambda p: p.name):
77
+ _copy_entry(
78
+ child,
79
+ dest_dir / child.name,
80
+ worktree_root,
81
+ stats,
82
+ visited=visited,
83
+ skipped_symlinks=skipped_symlinks,
84
+ )
85
+ try:
86
+ shutil.copystat(src_dir, dest_dir, follow_symlinks=False)
87
+ except OSError:
88
+ pass
89
+ finally:
90
+ visited.remove(real_dir)
91
+
92
+
93
+ def _copy_entry(
94
+ src: Path,
95
+ dest: Path,
96
+ worktree_root: Path,
97
+ stats: dict[str, int],
98
+ *,
99
+ visited: set[Path],
100
+ skipped_symlinks: list[str],
101
+ ) -> bool:
102
+ if src.is_symlink():
103
+ try:
104
+ resolved = src.resolve()
105
+ except FileNotFoundError:
106
+ skipped_symlinks.append(str(src))
107
+ return False
108
+ if not _is_within(worktree_root, resolved):
109
+ skipped_symlinks.append(str(src))
110
+ return False
111
+ if resolved.is_dir():
112
+ _copy_tree(
113
+ resolved,
114
+ dest,
115
+ worktree_root,
116
+ stats,
117
+ visited=visited,
118
+ skipped_symlinks=skipped_symlinks,
119
+ )
120
+ return True
121
+ if resolved.is_file():
122
+ _copy_file(resolved, dest, stats)
123
+ return True
124
+ return False
125
+
126
+ if src.is_dir():
127
+ _copy_tree(
128
+ src,
129
+ dest,
130
+ worktree_root,
131
+ stats,
132
+ visited=visited,
133
+ skipped_symlinks=skipped_symlinks,
134
+ )
135
+ return True
136
+
137
+ if src.is_file():
138
+ _copy_file(src, dest, stats)
139
+ return True
140
+
141
+ return False
142
+
143
+
144
+ def _flow_summary(flows_dir: Path) -> tuple[int, Optional[str]]:
145
+ if not flows_dir.exists() or not flows_dir.is_dir():
146
+ return 0, None
147
+ runs: list[Path] = [
148
+ path
149
+ for path in sorted(flows_dir.iterdir(), key=lambda p: p.name)
150
+ if path.is_dir()
151
+ ]
152
+ if not runs:
153
+ return 0, None
154
+ latest = max(
155
+ runs,
156
+ key=lambda p: (p.stat().st_mtime, p.name),
157
+ )
158
+ return len(runs), latest.name
159
+
160
+
161
+ def _build_meta(
162
+ *,
163
+ snapshot_id: str,
164
+ created_at: str,
165
+ status: ArchiveStatus,
166
+ base_repo_id: str,
167
+ worktree_repo_id: str,
168
+ worktree_of: str,
169
+ branch: str,
170
+ head_sha: str,
171
+ source_path: Path,
172
+ copied_paths: Iterable[str],
173
+ missing_paths: Iterable[str],
174
+ skipped_symlinks: Iterable[str],
175
+ summary: dict[str, object],
176
+ note: Optional[str] = None,
177
+ error: Optional[str] = None,
178
+ ) -> dict[str, object]:
179
+ payload: dict[str, object] = {
180
+ "schema_version": 1,
181
+ "snapshot_id": snapshot_id,
182
+ "created_at": created_at,
183
+ "status": status,
184
+ "base_repo_id": base_repo_id,
185
+ "worktree_repo_id": worktree_repo_id,
186
+ "worktree_of": worktree_of,
187
+ "branch": branch,
188
+ "head_sha": head_sha,
189
+ "source": {
190
+ "path": str(source_path),
191
+ "copied_paths": list(copied_paths),
192
+ "missing_paths": list(missing_paths),
193
+ "skipped_symlinks": list(skipped_symlinks),
194
+ },
195
+ "summary": summary,
196
+ }
197
+ if note:
198
+ payload["note"] = note
199
+ if error:
200
+ payload["error"] = error
201
+ return payload
202
+
203
+
204
+ def build_snapshot_id(branch: Optional[str], head_sha: str) -> str:
205
+ head_short = head_sha[:7] if head_sha and head_sha != "unknown" else "unknown"
206
+ return f"{_snapshot_timestamp()}--{_sanitize_branch(branch)}--{head_short}"
207
+
208
+
209
+ def archive_worktree_snapshot(
210
+ *,
211
+ base_repo_root: Path,
212
+ base_repo_id: str,
213
+ worktree_repo_root: Path,
214
+ worktree_repo_id: str,
215
+ branch: Optional[str],
216
+ worktree_of: str,
217
+ note: Optional[str] = None,
218
+ snapshot_id: Optional[str] = None,
219
+ head_sha: Optional[str] = None,
220
+ source_path: Optional[Path | str] = None,
221
+ ) -> ArchiveResult:
222
+ base_repo_root = base_repo_root.resolve()
223
+ worktree_repo_root = worktree_repo_root.resolve()
224
+ branch_name = branch or git_branch(worktree_repo_root) or "unknown"
225
+ resolved_head_sha = head_sha or git_head_sha(worktree_repo_root) or "unknown"
226
+ snapshot_id = snapshot_id or build_snapshot_id(branch_name, resolved_head_sha)
227
+ snapshot_root = (
228
+ base_repo_root
229
+ / ".codex-autorunner"
230
+ / "archive"
231
+ / "worktrees"
232
+ / worktree_repo_id
233
+ / snapshot_id
234
+ )
235
+ snapshot_root.mkdir(parents=True, exist_ok=False)
236
+
237
+ source_root = worktree_repo_root / ".codex-autorunner"
238
+ curated: list[tuple[Path, Path]] = [
239
+ (source_root / "workspace", snapshot_root / "workspace"),
240
+ (source_root / "tickets", snapshot_root / "tickets"),
241
+ (source_root / "runs", snapshot_root / "runs"),
242
+ (source_root / "flows", snapshot_root / "flows"),
243
+ (source_root / "flows.db", snapshot_root / "flows.db"),
244
+ (source_root / "config.yml", snapshot_root / "config" / "config.yml"),
245
+ (source_root / "state.sqlite3", snapshot_root / "state" / "state.sqlite3"),
246
+ (
247
+ source_root / "codex-autorunner.log",
248
+ snapshot_root / "logs" / "codex-autorunner.log",
249
+ ),
250
+ (
251
+ source_root / "codex-server.log",
252
+ snapshot_root / "logs" / "codex-server.log",
253
+ ),
254
+ ]
255
+
256
+ stats = {"file_count": 0, "total_bytes": 0}
257
+ copied_paths: list[str] = []
258
+ missing_paths: list[str] = []
259
+ skipped_symlinks: list[str] = []
260
+ visited: set[Path] = set()
261
+ created_at = now_iso()
262
+ meta_path = snapshot_root / "META.json"
263
+ summary: dict[str, object] = {}
264
+
265
+ try:
266
+ for src, dest in curated:
267
+ rel = src.relative_to(source_root)
268
+ if not src.exists() and not src.is_symlink():
269
+ missing_paths.append(str(rel))
270
+ continue
271
+ copied = _copy_entry(
272
+ src,
273
+ dest,
274
+ worktree_repo_root,
275
+ stats,
276
+ visited=visited,
277
+ skipped_symlinks=skipped_symlinks,
278
+ )
279
+ if copied:
280
+ copied_paths.append(str(rel))
281
+
282
+ flow_run_count, latest_flow_run_id = _flow_summary(snapshot_root / "flows")
283
+ status: ArchiveStatus = "complete" if not missing_paths else "partial"
284
+ summary = {
285
+ "file_count": stats["file_count"],
286
+ "total_bytes": stats["total_bytes"],
287
+ "flow_run_count": flow_run_count,
288
+ "latest_flow_run_id": latest_flow_run_id,
289
+ }
290
+ meta = _build_meta(
291
+ snapshot_id=snapshot_id,
292
+ created_at=created_at,
293
+ status=status,
294
+ base_repo_id=base_repo_id,
295
+ worktree_repo_id=worktree_repo_id,
296
+ worktree_of=worktree_of,
297
+ branch=branch_name,
298
+ head_sha=resolved_head_sha,
299
+ source_path=(
300
+ Path(source_path) if source_path is not None else worktree_repo_root
301
+ ),
302
+ copied_paths=copied_paths,
303
+ missing_paths=missing_paths,
304
+ skipped_symlinks=skipped_symlinks,
305
+ summary=summary,
306
+ note=note,
307
+ )
308
+ atomic_write(meta_path, json.dumps(meta, indent=2) + "\n")
309
+ except Exception as exc:
310
+ summary = {
311
+ "file_count": stats["file_count"],
312
+ "total_bytes": stats["total_bytes"],
313
+ "flow_run_count": 0,
314
+ "latest_flow_run_id": None,
315
+ }
316
+ meta = _build_meta(
317
+ snapshot_id=snapshot_id,
318
+ created_at=created_at,
319
+ status="failed",
320
+ base_repo_id=base_repo_id,
321
+ worktree_repo_id=worktree_repo_id,
322
+ worktree_of=worktree_of,
323
+ branch=branch_name,
324
+ head_sha=resolved_head_sha,
325
+ source_path=(
326
+ Path(source_path) if source_path is not None else worktree_repo_root
327
+ ),
328
+ copied_paths=copied_paths,
329
+ missing_paths=missing_paths,
330
+ skipped_symlinks=skipped_symlinks,
331
+ summary=summary,
332
+ note=note,
333
+ error=str(exc),
334
+ )
335
+ atomic_write(meta_path, json.dumps(meta, indent=2) + "\n")
336
+ raise
337
+
338
+ return ArchiveResult(
339
+ snapshot_id=snapshot_id,
340
+ snapshot_path=snapshot_root,
341
+ meta_path=meta_path,
342
+ status=status,
343
+ file_count=stats["file_count"],
344
+ total_bytes=stats["total_bytes"],
345
+ flow_run_count=flow_run_count,
346
+ latest_flow_run_id=latest_flow_run_id,
347
+ missing_paths=tuple(missing_paths),
348
+ skipped_symlinks=tuple(skipped_symlinks),
349
+ )
@@ -9,9 +9,13 @@ import subprocess
9
9
  from pathlib import Path
10
10
  from typing import Callable, Optional
11
11
 
12
- from ..codex_cli import apply_codex_options, supports_reasoning
13
12
  from .config import Config, ConfigError
14
- from .utils import resolve_executable, subprocess_env
13
+ from .utils import (
14
+ apply_codex_options,
15
+ resolve_executable,
16
+ subprocess_env,
17
+ supports_reasoning,
18
+ )
15
19
 
16
20
 
17
21
  class CodexRunnerError(Exception):