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
@@ -0,0 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path, PurePosixPath
7
+ from typing import Literal, cast
8
+
9
+ from ..core import drafts as draft_utils
10
+ from ..core.logging_utils import log_event
11
+
12
+ WorkspaceDocKind = Literal["active_context", "decisions", "spec"]
13
+ WORKSPACE_DOC_KINDS: tuple[WorkspaceDocKind, ...] = (
14
+ "active_context",
15
+ "decisions",
16
+ "spec",
17
+ )
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class WorkspaceFile:
24
+ name: str
25
+ path: str # path relative to the workspace directory (POSIX)
26
+ is_pinned: bool = False
27
+ modified_at: str | None = None
28
+
29
+
30
+ def _normalize_kind(kind: str) -> WorkspaceDocKind:
31
+ key = (kind or "").strip().lower()
32
+ if key not in WORKSPACE_DOC_KINDS:
33
+ raise ValueError("invalid workspace doc kind")
34
+ return cast(WorkspaceDocKind, key)
35
+
36
+
37
+ def workspace_dir(repo_root: Path) -> Path:
38
+ return repo_root / ".codex-autorunner" / "workspace"
39
+
40
+
41
+ PINNED_DOC_FILENAMES = {f"{kind}.md" for kind in WORKSPACE_DOC_KINDS}
42
+
43
+
44
+ @dataclass
45
+ class WorkspaceNode:
46
+ name: str
47
+ path: str # relative to workspace dir
48
+ type: Literal["file", "folder"]
49
+ is_pinned: bool = False
50
+ modified_at: str | None = None
51
+ size: int | None = None # files only
52
+ children: list["WorkspaceNode"] | None = None # folders only
53
+
54
+
55
+ def normalize_workspace_rel_path(repo_root: Path, rel_path: str) -> tuple[Path, str]:
56
+ """Normalize a user-supplied workspace path and ensure it stays in-tree.
57
+
58
+ We accept POSIX-style relative paths only, then resolve the full path and
59
+ verify the result is still under the workspace directory. This guards
60
+ against ".." traversal and symlink escapes that CodeQL flagged.
61
+ """
62
+
63
+ base = workspace_dir(repo_root).resolve(strict=False)
64
+ base_real = base.resolve(strict=False)
65
+ cleaned = (rel_path or "").strip()
66
+ if not cleaned:
67
+ raise ValueError("invalid workspace file path")
68
+
69
+ relative = PurePosixPath(cleaned)
70
+ if relative.is_absolute() or ".." in relative.parts:
71
+ raise ValueError("invalid workspace file path")
72
+
73
+ candidate = (base / relative).resolve(strict=False)
74
+ try:
75
+ rel_posix = candidate.relative_to(base_real).as_posix()
76
+ except ValueError:
77
+ raise ValueError("invalid workspace file path") from None
78
+
79
+ return candidate, rel_posix
80
+
81
+
82
+ def sanitize_workspace_filename(filename: str) -> str:
83
+ """Return a safe filename for workspace uploads.
84
+
85
+ We strip any directory components, collapse whitespace, and guard against
86
+ empty names. Caller is responsible for applying any per-workspace policy
87
+ (e.g., overwrite vs. reject).
88
+ """
89
+
90
+ cleaned = (filename or "").strip()
91
+ # Drop any path fragments that may be embedded in the upload
92
+ base = PurePosixPath(cleaned).name
93
+ # Remove remaining separators/backslashes that PurePosixPath.name could keep
94
+ base = base.replace("/", "").replace("\\", "")
95
+ if base in {".", ".."}:
96
+ base = ""
97
+ # Collapse whitespace to single spaces to keep names readable
98
+ base = " ".join(base.split())
99
+ if not base:
100
+ return "upload"
101
+ return base
102
+
103
+
104
+ def workspace_doc_path(repo_root: Path, kind: str) -> Path:
105
+ key = _normalize_kind(kind)
106
+ return workspace_dir(repo_root) / f"{key}.md"
107
+
108
+
109
+ def read_workspace_file(
110
+ repo_root: Path, rel_path: str
111
+ ) -> str: # codeql[py/path-injection]
112
+ path, _ = normalize_workspace_rel_path(repo_root, rel_path)
113
+ if (
114
+ path.is_dir()
115
+ ): # codeql[py/path-injection] validated by normalize_workspace_rel_path
116
+ raise ValueError("path points to a directory")
117
+ if (
118
+ not path.exists()
119
+ ): # codeql[py/path-injection] validated by normalize_workspace_rel_path
120
+ return ""
121
+ return path.read_text(
122
+ encoding="utf-8"
123
+ ) # codeql[py/path-injection] validated by normalize_workspace_rel_path
124
+
125
+
126
+ def write_workspace_file( # codeql[py/path-injection]
127
+ repo_root: Path, rel_path: str, content: str
128
+ ) -> str:
129
+ path, rel_posix = normalize_workspace_rel_path(repo_root, rel_path)
130
+ if (
131
+ path.exists() and path.is_dir()
132
+ ): # codeql[py/path-injection] validated by normalize_workspace_rel_path
133
+ raise ValueError("path points to a directory")
134
+ path.parent.mkdir(
135
+ parents=True, exist_ok=True
136
+ ) # codeql[py/path-injection] validated by normalize_workspace_rel_path
137
+ path.write_text(
138
+ content or "", encoding="utf-8"
139
+ ) # codeql[py/path-injection] validated by normalize_workspace_rel_path
140
+ rel = path.relative_to(repo_root).as_posix()
141
+ state_key = f"workspace_{rel_posix.replace('/', '_')}"
142
+ try:
143
+ draft_utils.invalidate_drafts_for_path(repo_root, rel)
144
+ draft_utils.remove_draft(repo_root, state_key)
145
+ except Exception as exc:
146
+ log_event(
147
+ logger,
148
+ logging.WARNING,
149
+ "workspace.draft_invalidation_failed",
150
+ repo_root=str(repo_root),
151
+ rel_path=rel_posix,
152
+ state_key=state_key,
153
+ exc=exc,
154
+ )
155
+ logger.debug(
156
+ "workspace draft invalidation failed for %s (repo_root=%s)",
157
+ rel_posix,
158
+ repo_root,
159
+ exc_info=True,
160
+ )
161
+ return path.read_text(encoding="utf-8")
162
+
163
+
164
+ def read_workspace_doc(repo_root: Path, kind: str) -> str:
165
+ path = workspace_doc_path(repo_root, kind)
166
+ if not path.exists():
167
+ return ""
168
+ return path.read_text(encoding="utf-8")
169
+
170
+
171
+ def write_workspace_doc( # codeql[py/path-injection]
172
+ repo_root: Path, kind: str, content: str
173
+ ) -> str:
174
+ path = workspace_doc_path(repo_root, kind)
175
+ path.parent.mkdir(parents=True, exist_ok=True)
176
+ path.write_text(
177
+ content or "", encoding="utf-8"
178
+ ) # codeql[py/path-injection] workspace_doc_path is deterministic
179
+ rel = path.relative_to(repo_root).as_posix()
180
+ state_key = f"workspace_{rel.replace('/', '_')}"
181
+ try:
182
+ draft_utils.invalidate_drafts_for_path(repo_root, rel)
183
+ draft_utils.remove_draft(repo_root, state_key)
184
+ except Exception as exc:
185
+ log_event(
186
+ logger,
187
+ logging.WARNING,
188
+ "workspace.draft_invalidation_failed",
189
+ repo_root=str(repo_root),
190
+ rel_path=rel,
191
+ state_key=state_key,
192
+ kind=kind,
193
+ exc=exc,
194
+ )
195
+ logger.debug(
196
+ "workspace draft invalidation failed for %s (repo_root=%s kind=%s)",
197
+ rel,
198
+ repo_root,
199
+ kind,
200
+ exc_info=True,
201
+ )
202
+ return path.read_text(encoding="utf-8")
203
+
204
+
205
+ def _format_mtime(path: Path) -> str | None:
206
+ if not path.exists():
207
+ return None
208
+ ts = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
209
+ return ts.isoformat()
210
+
211
+
212
+ def list_workspace_files(
213
+ repo_root: Path,
214
+ ) -> list[WorkspaceFile]: # codeql[py/path-injection]
215
+ base = workspace_dir(repo_root)
216
+ base.mkdir(parents=True, exist_ok=True)
217
+
218
+ pinned: list[WorkspaceFile] = []
219
+ for kind in WORKSPACE_DOC_KINDS:
220
+ path = workspace_doc_path(repo_root, kind)
221
+ rel = path.relative_to(base).as_posix()
222
+ pinned.append(
223
+ WorkspaceFile(
224
+ name=path.name,
225
+ path=rel,
226
+ is_pinned=True,
227
+ modified_at=_format_mtime(path),
228
+ )
229
+ )
230
+
231
+ others: list[WorkspaceFile] = []
232
+ if base.exists():
233
+ for file_path in base.rglob("*"):
234
+ if file_path.is_dir():
235
+ continue
236
+ try:
237
+ rel = file_path.relative_to(base).as_posix()
238
+ except ValueError:
239
+ continue
240
+ if any(rel == pinned_file.path for pinned_file in pinned):
241
+ continue
242
+ others.append(
243
+ WorkspaceFile(
244
+ name=file_path.name,
245
+ path=rel,
246
+ is_pinned=False,
247
+ modified_at=_format_mtime(file_path),
248
+ )
249
+ )
250
+
251
+ others.sort(key=lambda f: f.path)
252
+ return [*pinned, *others]
253
+
254
+
255
+ def _sort_workspace_children(path: Path) -> tuple[int, str]:
256
+ # Folders first, then files, both alphabetized (case-insensitive)
257
+ return (0 if path.is_dir() else 1, path.name.lower())
258
+
259
+
260
+ def _is_within_workspace(base_real: Path, candidate: Path) -> bool:
261
+ try:
262
+ candidate.resolve().relative_to(base_real)
263
+ return True
264
+ except Exception:
265
+ return False
266
+
267
+
268
+ def _file_node(base: Path, path: Path, is_pinned: bool = False) -> WorkspaceNode:
269
+ rel = path.relative_to(base).as_posix()
270
+ size: int | None = None
271
+ if path.exists() and path.is_file():
272
+ try:
273
+ size = path.stat().st_size
274
+ except OSError:
275
+ size = None
276
+ return WorkspaceNode(
277
+ name=path.name,
278
+ path=rel,
279
+ type="file",
280
+ is_pinned=is_pinned,
281
+ modified_at=_format_mtime(path),
282
+ size=size,
283
+ )
284
+
285
+
286
+ def _build_workspace_tree(base: Path, path: Path) -> WorkspaceNode:
287
+ is_symlink = path.is_symlink()
288
+ is_folder = path.is_dir() and not is_symlink
289
+ is_pinned = path.name in PINNED_DOC_FILENAMES and path.parent == base
290
+
291
+ if not is_folder:
292
+ return _file_node(base, path, is_pinned=is_pinned)
293
+
294
+ children: list[WorkspaceNode] = []
295
+ for child in sorted(path.iterdir(), key=_sort_workspace_children):
296
+ # Avoid duplicating pinned docs surfaced at the root list
297
+ if child.parent == base and child.name in PINNED_DOC_FILENAMES:
298
+ continue
299
+ # Skip symlink escapes that resolve outside the workspace
300
+ if child.is_symlink() and not _is_within_workspace(base.resolve(), child):
301
+ continue
302
+ children.append(_build_workspace_tree(base, child))
303
+
304
+ return WorkspaceNode(
305
+ name=path.name,
306
+ path=path.relative_to(base).as_posix(),
307
+ type="folder",
308
+ is_pinned=False,
309
+ modified_at=_format_mtime(path),
310
+ children=children,
311
+ )
312
+
313
+
314
+ def list_workspace_tree(repo_root: Path) -> list[WorkspaceNode]:
315
+ """Return hierarchical workspace structure (folders + files)."""
316
+
317
+ base = workspace_dir(repo_root)
318
+ base.mkdir(parents=True, exist_ok=True)
319
+ base_real = base.resolve()
320
+
321
+ nodes: list[WorkspaceNode] = []
322
+
323
+ # Pinned docs first (even if missing)
324
+ for name in sorted(PINNED_DOC_FILENAMES):
325
+ pinned_path = base / name
326
+ nodes.append(_file_node(base, pinned_path, is_pinned=True))
327
+
328
+ for child in sorted(base.iterdir(), key=_sort_workspace_children):
329
+ if child.parent == base and child.name in PINNED_DOC_FILENAMES:
330
+ continue
331
+ if child.is_symlink() and not _is_within_workspace(base_real, child):
332
+ continue
333
+ nodes.append(_build_workspace_tree(base, child))
334
+
335
+ return nodes
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-autorunner
3
+ Version: 1.1.0
4
+ Summary: Codex autorunner CLI per DESIGN-V1
5
+ Author: Codex
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 David Zhang
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/Git-on-my-level/codex-autorunner
29
+ Project-URL: Repository, https://github.com/Git-on-my-level/codex-autorunner
30
+ Project-URL: Issues, https://github.com/Git-on-my-level/codex-autorunner/issues
31
+ Keywords: codex,automation,agent,cli,fastapi
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3 :: Only
35
+ Classifier: Operating System :: OS Independent
36
+ Requires-Python: >=3.9
37
+ Description-Content-Type: text/markdown
38
+ License-File: LICENSE
39
+ Requires-Dist: typer>=0.9
40
+ Requires-Dist: click<8.2
41
+ Requires-Dist: pyyaml>=6.0
42
+ Requires-Dist: fastapi>=0.111
43
+ Requires-Dist: uvicorn[standard]>=0.30
44
+ Requires-Dist: ptyprocess>=0.7
45
+ Requires-Dist: python-multipart>=0.0.9
46
+ Requires-Dist: python-dotenv>=1.0
47
+ Requires-Dist: httpx>=0.27
48
+ Requires-Dist: tenacity>=8.0
49
+ Provides-Extra: dev
50
+ Requires-Dist: black==25.11.0; extra == "dev"
51
+ Requires-Dist: mypy>=1.10; extra == "dev"
52
+ Requires-Dist: pytest>=7.0; extra == "dev"
53
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
54
+ Requires-Dist: pytest-timeout>=2.0; extra == "dev"
55
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
56
+ Requires-Dist: types-PyYAML; extra == "dev"
57
+ Provides-Extra: telegram
58
+ Requires-Dist: httpx>=0.27; extra == "telegram"
59
+ Provides-Extra: voice
60
+ Requires-Dist: httpx>=0.27; extra == "voice"
61
+ Requires-Dist: python-multipart>=0.0.9; extra == "voice"
62
+ Provides-Extra: github
63
+ Dynamic: license-file
64
+
65
+ # CAR (codex-autorunner)
66
+ [![PyPI](https://img.shields.io/pypi/v/codex-autorunner.svg)](https://pypi.org/project/codex-autorunner/)
67
+
68
+ CAR provides a set of low-opinion agent coordination tools for you to run long complex implementations using the agents you already love.
69
+
70
+ What this looks like in practice:
71
+ - You write a plan, or generate a plan by chatting with your favorite AI
72
+ - You convert the plan (or ask an AI to convert it for you) into CAR compatible tickets (markdown with some frontmatter)
73
+ - Go off and do something else, no need to babysit the agents, they will notify you if they need your input
74
+
75
+ ![CAR Web Hub Screenshot](docs/screenshots/hub.png)
76
+
77
+ ## How it works
78
+ CAR is very simple. At it's core, CAR is a state machine which checks to see if there are any incomplete tickets. If yes, pick the next one and run it against an agent. Tickets can be pre-populated by the user, but agents can also write tickets. _Tickets are the control plane for CAR_.
79
+
80
+ When each agent wakes up, it gets knowledge about CAR and how to operate within CAR, a pre-defined set of context (workspace files), the current ticket, and optionally the final output of the previous agent. This simple loop ensures that agents know enough to use CAR while also focusing them on the task at hand.
81
+
82
+ ## Philosophy
83
+ The philosophy behind CAR is to let the agents do what they do best, and get out of their way. CAR is _very bitter-lesson-pilled_. As models and agents get more powerful, CAR should serve as a form of leverage, and avoid constraining models and their harnesses. This is why we treat the filesystem as the first class data plane and utilize tools and languages the models are already very familiar with (git, python).
84
+
85
+ CAR treats tickets as the control plane and models as the execution layer. This means that we rely on agents to follow the instructions written in the tickets. If you use a sufficiently weak model, CAR may not work well for you. CAR is an amplifier for agent capabilities. Agents who like to scope creep (create too many new tickets) or reward hack (mark a ticket as done despite it being incomplete) are not a good fit for CAR.
86
+
87
+ ## Interaction patterns
88
+ CAR's core is a set of python functions surfaced as a CLI, operating on a file system. There are current 2 UIs built on top of this core.
89
+
90
+ ### Web UI
91
+ The web UI is the main control plane for CAR. From here you can set up new repositories or clone existing ones, chat with agents using their TUI, and run the ticket autorunner. There are many quality-of-life features like Whisper integration, editing documents by chatting with AI (useful for mobile), viewing usage analytics, and much more. The Web UI is the most full featured user-facing interface and a good starting point for trying out CAR.
92
+
93
+ I recommend serving the web UI over Tailscale. There is an auth token option but the system is not very battle tested.
94
+
95
+ ### Telegram
96
+ Telegram is the "on-the-go" and notification hub for CAR. From here you can kick off and monitor existing tickets, set up new tickets, and chat with agents. Your primary UX here is asking the agent to do things for you rather than you doing it yourself like you would on the web UI. This is great for on-the-go work, but it doesn't have full feature parity with the web UI.
97
+
98
+ ## Quickstart
99
+
100
+ The fastest way to get started is to pass [this setup guide](docs/AGENT_SETUP_GUIDE.md) to your favorite AI agent. The agent will walk you through installation and configuration interactively based on your environment.
101
+
102
+ **TL;DR for the impatient:**
103
+
104
+ # Install
105
+ ```
106
+ pipx install codex-autorunner
107
+ ```
108
+ # Initialize in your repo
109
+ ```
110
+ cd /path/to/your/repo
111
+ car init
112
+ ```
113
+ # Verify setup
114
+ ```
115
+ car doctor
116
+ ```
117
+ # Create a ticket and run
118
+ ```
119
+ car run
120
+ ```
121
+
122
+ ## Supported models
123
+ CAR currently supports:
124
+ - Codex
125
+ - Opencode
126
+
127
+ CAR is built to easily integrate any reasonable agent built for Agent Client Protocol (ACP). If you would like to see your agent supported, please reach out or open a PR.
128
+
129
+ ## Examples
130
+ Build out complex features and products by providing a series of tickets assigned to various agents.
131
+ ![CAR Tickets in Progress Screenshot](docs/screenshots/tickets-in-progress.png)
132
+
133
+ Tickets are just markdown files that both you and the agent can edit.
134
+ ![CAR Ticket Markdown Screenshot](docs/screenshots/ticket-markdown.png)
135
+
136
+ You don't have to babysit the agents, they inbox you or ping you on Telegram.
137
+ ![CAR Inbox Screenshot](docs/screenshots/inbox.png)
138
+
139
+ You can collaborate with the agents in a shared workspace, independent of the codebase. Drop context there, extract artifacts, it's like a shared scratchpad.
140
+ ![CAR Workspace Screenshot](docs/screenshots/workspace.png)
141
+
142
+ All core workspace documents are also just markdown files, so you and the agent can easily edit them.
143
+ ![CAR Workspace New MD Screenshot](docs/screenshots/workspace-new-md.png)
144
+
145
+ If you need to do something more custom or granular, you can use your favorite agent TUIs in the built-in terminal.
146
+ ![CAR Terminal Codex Screenshot](docs/screenshots/terminal-codex.png)
147
+ ![CAR Terminal Opencode Screenshot](docs/screenshots/terminal-opencode.png)
148
+
149
+ On the go? The web UI is mobile responsive, or if you prefer you can type or voice chat with your agents on Telegram.
150
+ ![CAR Telegram Media Voice Screenshot](docs/screenshots/telegram-media-voice.PNG)
151
+ ![CAR Telegram Media Image Screenshot](docs/screenshots/telegram-media-image.PNG)
152
+
153
+ ## Star history
154
+ [![Star History Chart](https://api.star-history.com/svg?repos=Git-on-my-level/codex-autorunner&type=Date)](https://star-history.com/#Git-on-my-level/codex-autorunner&Date)