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
@@ -5,7 +5,7 @@ import re
5
5
  import shutil
6
6
  import time
7
7
  from pathlib import Path
8
- from typing import Dict, List, Optional, Tuple
8
+ from typing import Callable, Dict, List, Optional, Tuple
9
9
 
10
10
  from ..bootstrap import seed_repo_files
11
11
  from ..discovery import DiscoveryRecord, discover_and_init
@@ -16,12 +16,15 @@ from ..manifest import (
16
16
  sanitize_repo_id,
17
17
  save_manifest,
18
18
  )
19
+ from .archive import archive_worktree_snapshot, build_snapshot_id
19
20
  from .config import HubConfig, RepoConfig, derive_repo_config, load_hub_config
20
- from .engine import Engine
21
+ from .engine import AppServerSupervisorFactory, BackendFactory, Engine
21
22
  from .git_utils import (
22
23
  GitError,
23
24
  git_available,
25
+ git_branch,
24
26
  git_default_branch,
27
+ git_head_sha,
25
28
  git_is_clean,
26
29
  git_upstream_status,
27
30
  run_git,
@@ -33,6 +36,9 @@ from .utils import atomic_write
33
36
 
34
37
  logger = logging.getLogger("codex_autorunner.hub")
35
38
 
39
+ BackendFactoryBuilder = Callable[[Path, RepoConfig], BackendFactory]
40
+ AppServerSupervisorFactoryBuilder = Callable[[RepoConfig], AppServerSupervisorFactory]
41
+
36
42
 
37
43
  def _git_failure_detail(proc) -> str:
38
44
  return (proc.stderr or proc.stdout or "").strip() or f"exit {proc.returncode}"
@@ -137,7 +143,8 @@ def load_hub_state(state_path: Path, hub_root: Path) -> HubState:
137
143
  import json
138
144
 
139
145
  payload = json.loads(data)
140
- except Exception:
146
+ except Exception as exc:
147
+ logger.warning("Failed to parse hub state from %s: %s", state_path, exc)
141
148
  return HubState(last_scan_at=None, repos=[])
142
149
  last_scan_at = payload.get("last_scan_at")
143
150
  repos_payload = payload.get("repos") or []
@@ -168,7 +175,13 @@ def load_hub_state(state_path: Path, hub_root: Path) -> HubState:
168
175
  runner_pid=entry.get("runner_pid"),
169
176
  )
170
177
  repos.append(repo)
171
- except Exception:
178
+ except Exception as exc:
179
+ repo_id = entry.get("id", "unknown")
180
+ logger.warning(
181
+ "Failed to load repo snapshot for id=%s from hub state: %s",
182
+ repo_id,
183
+ exc,
184
+ )
172
185
  continue
173
186
  return HubState(last_scan_at=last_scan_at, repos=repos)
174
187
 
@@ -188,9 +201,30 @@ class RepoRunner:
188
201
  *,
189
202
  repo_config: RepoConfig,
190
203
  spawn_fn: Optional[SpawnRunnerFn] = None,
204
+ backend_factory_builder: Optional[BackendFactoryBuilder] = None,
205
+ app_server_supervisor_factory_builder: Optional[
206
+ AppServerSupervisorFactoryBuilder
207
+ ] = None,
208
+ agent_id_validator: Optional[Callable[[str], str]] = None,
191
209
  ):
192
210
  self.repo_id = repo_id
193
- self._engine = Engine(repo_root, config=repo_config)
211
+ backend_factory = (
212
+ backend_factory_builder(repo_root, repo_config)
213
+ if backend_factory_builder is not None
214
+ else None
215
+ )
216
+ app_server_supervisor_factory = (
217
+ app_server_supervisor_factory_builder(repo_config)
218
+ if app_server_supervisor_factory_builder is not None
219
+ else None
220
+ )
221
+ self._engine = Engine(
222
+ repo_root,
223
+ config=repo_config,
224
+ backend_factory=backend_factory,
225
+ app_server_supervisor_factory=app_server_supervisor_factory,
226
+ agent_id_validator=agent_id_validator,
227
+ )
194
228
  self._controller = ProcessRunnerController(self._engine, spawn_fn=spawn_fn)
195
229
 
196
230
  @property
@@ -212,21 +246,46 @@ class RepoRunner:
212
246
 
213
247
  class HubSupervisor:
214
248
  def __init__(
215
- self, hub_config: HubConfig, *, spawn_fn: Optional[SpawnRunnerFn] = None
249
+ self,
250
+ hub_config: HubConfig,
251
+ *,
252
+ spawn_fn: Optional[SpawnRunnerFn] = None,
253
+ backend_factory_builder: Optional[BackendFactoryBuilder] = None,
254
+ app_server_supervisor_factory_builder: Optional[
255
+ AppServerSupervisorFactoryBuilder
256
+ ] = None,
257
+ agent_id_validator: Optional[Callable[[str], str]] = None,
216
258
  ):
217
259
  self.hub_config = hub_config
218
260
  self.state_path = hub_config.root / ".codex-autorunner" / "hub_state.json"
219
261
  self._runners: Dict[str, RepoRunner] = {}
220
262
  self._spawn_fn = spawn_fn
263
+ self._backend_factory_builder = backend_factory_builder
264
+ self._app_server_supervisor_factory_builder = (
265
+ app_server_supervisor_factory_builder
266
+ )
267
+ self._agent_id_validator = agent_id_validator
221
268
  self.state = load_hub_state(self.state_path, self.hub_config.root)
222
269
  self._list_cache_at: Optional[float] = None
223
270
  self._list_cache: Optional[List[RepoSnapshot]] = None
224
271
  self._reconcile_startup()
225
272
 
226
273
  @classmethod
227
- def from_path(cls, path: Path) -> "HubSupervisor":
274
+ def from_path(
275
+ cls,
276
+ path: Path,
277
+ *,
278
+ backend_factory_builder: Optional[BackendFactoryBuilder] = None,
279
+ app_server_supervisor_factory_builder: Optional[
280
+ AppServerSupervisorFactoryBuilder
281
+ ] = None,
282
+ ) -> "HubSupervisor":
228
283
  config = load_hub_config(path)
229
- return cls(config)
284
+ return cls(
285
+ config,
286
+ backend_factory_builder=backend_factory_builder,
287
+ app_server_supervisor_factory_builder=app_server_supervisor_factory_builder,
288
+ )
230
289
 
231
290
  def scan(self) -> List[RepoSnapshot]:
232
291
  self._invalidate_list_cache()
@@ -261,8 +320,24 @@ class HubSupervisor:
261
320
  repo_config = derive_repo_config(
262
321
  self.hub_config, record.absolute_path, load_env=False
263
322
  )
323
+ backend_factory = (
324
+ self._backend_factory_builder(record.absolute_path, repo_config)
325
+ if self._backend_factory_builder is not None
326
+ else None
327
+ )
328
+ app_server_supervisor_factory = (
329
+ self._app_server_supervisor_factory_builder(repo_config)
330
+ if self._app_server_supervisor_factory_builder is not None
331
+ else None
332
+ )
264
333
  controller = ProcessRunnerController(
265
- Engine(record.absolute_path, config=repo_config)
334
+ Engine(
335
+ record.absolute_path,
336
+ config=repo_config,
337
+ backend_factory=backend_factory,
338
+ app_server_supervisor_factory=app_server_supervisor_factory,
339
+ agent_id_validator=self._agent_id_validator,
340
+ )
266
341
  )
267
342
  controller.reconcile()
268
343
  except Exception as exc:
@@ -586,6 +661,9 @@ class HubSupervisor:
586
661
  worktree_repo_id: str,
587
662
  delete_branch: bool = False,
588
663
  delete_remote: bool = False,
664
+ archive: bool = True,
665
+ force_archive: bool = False,
666
+ archive_note: Optional[str] = None,
589
667
  ) -> None:
590
668
  self._invalidate_list_cache()
591
669
  manifest = load_manifest(self.hub_config.manifest_path, self.hub_config.root)
@@ -606,6 +684,44 @@ class HubSupervisor:
606
684
  if runner:
607
685
  runner.stop()
608
686
 
687
+ if archive:
688
+ branch_name = entry.branch or git_branch(worktree_path) or "unknown"
689
+ head_sha = git_head_sha(worktree_path) or "unknown"
690
+ snapshot_id = build_snapshot_id(branch_name, head_sha)
691
+ logger.info(
692
+ "Hub archive worktree start id=%s snapshot_id=%s",
693
+ worktree_repo_id,
694
+ snapshot_id,
695
+ )
696
+ try:
697
+ result = archive_worktree_snapshot(
698
+ base_repo_root=base_path,
699
+ base_repo_id=base.id,
700
+ worktree_repo_root=worktree_path,
701
+ worktree_repo_id=worktree_repo_id,
702
+ branch=branch_name,
703
+ worktree_of=entry.worktree_of,
704
+ note=archive_note,
705
+ snapshot_id=snapshot_id,
706
+ head_sha=head_sha,
707
+ source_path=entry.path,
708
+ )
709
+ except Exception as exc:
710
+ logger.exception(
711
+ "Hub archive worktree failed id=%s snapshot_id=%s",
712
+ worktree_repo_id,
713
+ snapshot_id,
714
+ )
715
+ if not force_archive:
716
+ raise ValueError(f"Worktree archive failed: {exc}") from exc
717
+ else:
718
+ logger.info(
719
+ "Hub archive worktree complete id=%s snapshot_id=%s status=%s",
720
+ worktree_repo_id,
721
+ result.snapshot_id,
722
+ result.status,
723
+ )
724
+
609
725
  # Remove worktree from base repo.
610
726
  try:
611
727
  proc = run_git(
@@ -759,10 +875,10 @@ class HubSupervisor:
759
875
  if not repo:
760
876
  raise ValueError(f"Repo {repo_id} not found in manifest")
761
877
  repo_root = (self.hub_config.root / repo.path).resolve()
762
- state_path = repo_root / ".codex-autorunner" / "state.sqlite3"
763
- if not allow_uninitialized and not state_path.exists():
878
+ tickets_dir = repo_root / ".codex-autorunner" / "tickets"
879
+ if not allow_uninitialized and not tickets_dir.exists():
764
880
  raise ValueError(f"Repo {repo_id} is not initialized")
765
- if not state_path.exists():
881
+ if not tickets_dir.exists():
766
882
  return None
767
883
  repo_config = derive_repo_config(self.hub_config, repo_root, load_env=False)
768
884
  runner = RepoRunner(
@@ -770,6 +886,11 @@ class HubSupervisor:
770
886
  repo_root,
771
887
  repo_config=repo_config,
772
888
  spawn_fn=self._spawn_fn,
889
+ backend_factory_builder=self._backend_factory_builder,
890
+ app_server_supervisor_factory_builder=(
891
+ self._app_server_supervisor_factory_builder
892
+ ),
893
+ agent_id_validator=self._agent_id_validator,
773
894
  )
774
895
  self._runners[repo_id] = runner
775
896
  return runner
@@ -781,7 +902,7 @@ class HubSupervisor:
781
902
  records: List[DiscoveryRecord] = []
782
903
  for entry in manifest.repos:
783
904
  repo_path = (self.hub_config.root / entry.path).resolve()
784
- initialized = (repo_path / ".codex-autorunner" / "state.sqlite3").exists()
905
+ initialized = (repo_path / ".codex-autorunner" / "tickets").exists()
785
906
  records.append(
786
907
  DiscoveryRecord(
787
908
  repo=entry,
@@ -821,9 +942,8 @@ class HubSupervisor:
821
942
  lock_status = read_lock_status(lock_path)
822
943
 
823
944
  runner_state: Optional[RunnerState] = None
824
- state_path = repo_path / ".codex-autorunner" / "state.sqlite3"
825
- if record.initialized and state_path.exists():
826
- runner_state = load_state(state_path)
945
+ if record.initialized:
946
+ runner_state = load_state(repo_path / ".codex-autorunner" / "state.sqlite3")
827
947
 
828
948
  is_clean: Optional[bool] = None
829
949
  if record.exists_on_disk and git_available(repo_path):
@@ -1,5 +1,6 @@
1
1
  import errno
2
2
  import json
3
+ import logging
3
4
  import os
4
5
  import socket
5
6
  import subprocess
@@ -27,6 +28,7 @@ class LockAssessment:
27
28
 
28
29
 
29
30
  DEFAULT_RUNNER_CMD_HINTS = ("codex_autorunner.cli", "codex-autorunner", "car ")
31
+ logger = logging.getLogger(__name__)
30
32
 
31
33
 
32
34
  def process_alive(pid: int) -> bool:
@@ -48,6 +50,7 @@ def process_is_zombie(pid: int) -> bool:
48
50
  check=False,
49
51
  )
50
52
  except Exception:
53
+ logger.debug("Failed to check process status for pid %s", pid, exc_info=True)
51
54
  return False
52
55
  if result.returncode != 0:
53
56
  return False
@@ -65,6 +68,7 @@ def process_command(pid: int) -> Optional[str]:
65
68
  check=False,
66
69
  )
67
70
  except Exception:
71
+ logger.debug("Failed to inspect process command for pid %s", pid, exc_info=True)
68
72
  return None
69
73
  if result.returncode != 0:
70
74
  return None
@@ -23,6 +23,18 @@ class NotificationManager:
23
23
  self._warned_missing: set[str] = set()
24
24
  self._enabled_mode = self._parse_enabled(self._cfg.get("enabled"))
25
25
  self._events = self._normalize_events(self._cfg.get("events"))
26
+ timeout_raw = self._cfg.get("timeout_seconds", DEFAULT_TIMEOUT_SECONDS)
27
+ try:
28
+ timeout_seconds = (
29
+ float(timeout_raw)
30
+ if timeout_raw is not None
31
+ else DEFAULT_TIMEOUT_SECONDS
32
+ )
33
+ except (TypeError, ValueError):
34
+ timeout_seconds = DEFAULT_TIMEOUT_SECONDS
35
+ if timeout_seconds <= 0:
36
+ timeout_seconds = DEFAULT_TIMEOUT_SECONDS
37
+ self._timeout_seconds = timeout_seconds
26
38
  self._warn_unknown_events(self._events)
27
39
  discord_cfg = self._cfg.get("discord")
28
40
  self._discord: Dict[str, Any] = (
@@ -202,7 +214,7 @@ class NotificationManager:
202
214
  if not targets:
203
215
  return
204
216
  try:
205
- with httpx.Client(timeout=DEFAULT_TIMEOUT_SECONDS) as client:
217
+ with httpx.Client(timeout=self._timeout_seconds) as client:
206
218
  self._send_sync(client, targets, message)
207
219
  except Exception as exc:
208
220
  self._log_warning("Notification delivery failed", exc)
@@ -216,7 +228,7 @@ class NotificationManager:
216
228
  if not targets:
217
229
  return
218
230
  try:
219
- async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT_SECONDS) as client:
231
+ async with httpx.AsyncClient(timeout=self._timeout_seconds) as client:
220
232
  await self._send_async(client, targets, message)
221
233
  except Exception as exc:
222
234
  self._log_warning("Notification delivery failed", exc)
@@ -0,0 +1,28 @@
1
+ from .agent_backend import AgentBackend, AgentEvent, AgentEventType, now_iso
2
+ from .run_event import (
3
+ ApprovalRequested,
4
+ Completed,
5
+ Failed,
6
+ OutputDelta,
7
+ RunEvent,
8
+ RunNotice,
9
+ Started,
10
+ TokenUsage,
11
+ ToolCall,
12
+ )
13
+
14
+ __all__ = [
15
+ "AgentBackend",
16
+ "AgentEvent",
17
+ "AgentEventType",
18
+ "now_iso",
19
+ "RunEvent",
20
+ "Started",
21
+ "OutputDelta",
22
+ "ToolCall",
23
+ "ApprovalRequested",
24
+ "TokenUsage",
25
+ "RunNotice",
26
+ "Completed",
27
+ "Failed",
28
+ ]
@@ -0,0 +1,150 @@
1
+ import logging
2
+ from dataclasses import dataclass, field
3
+ from datetime import datetime, timezone
4
+ from enum import Enum
5
+ from typing import Any, AsyncGenerator, Dict, Optional
6
+
7
+ _logger = logging.getLogger(__name__)
8
+
9
+
10
+ def now_iso() -> str:
11
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
12
+
13
+
14
+ class AgentEventType(str, Enum):
15
+ STREAM_DELTA = "stream_delta"
16
+ TOOL_CALL = "tool_call"
17
+ TOOL_RESULT = "tool_result"
18
+ MESSAGE_COMPLETE = "message_complete"
19
+ ERROR = "error"
20
+ APPROVAL_REQUESTED = "approval_requested"
21
+ APPROVAL_GRANTED = "approval_granted"
22
+ APPROVAL_DENIED = "approval_denied"
23
+ SESSION_STARTED = "session_started"
24
+ SESSION_ENDED = "session_ended"
25
+ SESION_STARTED = "session_started" # legacy typo kept for backward tests
26
+
27
+
28
+ @dataclass
29
+ class AgentEvent:
30
+ type: str
31
+ timestamp: str
32
+ data: Dict[str, Any] = field(default_factory=dict)
33
+
34
+ @property
35
+ def event_type(self) -> AgentEventType:
36
+ try:
37
+ return AgentEventType(self.type)
38
+ except ValueError:
39
+ return AgentEventType.ERROR
40
+
41
+ @classmethod
42
+ def stream_delta(cls, content: str, delta_type: str = "text") -> "AgentEvent":
43
+ return cls(
44
+ type=AgentEventType.STREAM_DELTA.value,
45
+ timestamp=now_iso(),
46
+ data={"content": content, "delta_type": delta_type},
47
+ )
48
+
49
+ @classmethod
50
+ def tool_call(cls, tool_name: str, tool_input: Dict[str, Any]) -> "AgentEvent":
51
+ return cls(
52
+ type=AgentEventType.TOOL_CALL.value,
53
+ timestamp=now_iso(),
54
+ data={"tool_name": tool_name, "tool_input": tool_input},
55
+ )
56
+
57
+ @classmethod
58
+ def tool_result(
59
+ cls, tool_name: str, result: Any, error: Optional[str] = None
60
+ ) -> "AgentEvent":
61
+ return cls(
62
+ type=AgentEventType.TOOL_RESULT.value,
63
+ timestamp=now_iso(),
64
+ data={"tool_name": tool_name, "result": result, "error": error},
65
+ )
66
+
67
+ @classmethod
68
+ def message_complete(cls, final_message: str) -> "AgentEvent":
69
+ return cls(
70
+ type=AgentEventType.MESSAGE_COMPLETE.value,
71
+ timestamp=now_iso(),
72
+ data={"final_message": final_message},
73
+ )
74
+
75
+ @classmethod
76
+ def error(cls, error_message: str) -> "AgentEvent":
77
+ return cls(
78
+ type=AgentEventType.ERROR.value,
79
+ timestamp=now_iso(),
80
+ data={"error": error_message},
81
+ )
82
+
83
+ @classmethod
84
+ def approval_requested(
85
+ cls, request_id: str, description: str, context: Optional[Dict[str, Any]] = None
86
+ ) -> "AgentEvent":
87
+ return cls(
88
+ type=AgentEventType.APPROVAL_REQUESTED.value,
89
+ timestamp=now_iso(),
90
+ data={
91
+ "request_id": request_id,
92
+ "description": description,
93
+ "context": context or {},
94
+ },
95
+ )
96
+
97
+ @classmethod
98
+ def approval_granted(cls, request_id: str) -> "AgentEvent":
99
+ return cls(
100
+ type=AgentEventType.APPROVAL_GRANTED.value,
101
+ timestamp=now_iso(),
102
+ data={"request_id": request_id},
103
+ )
104
+
105
+ @classmethod
106
+ def approval_denied(
107
+ cls, request_id: str, reason: Optional[str] = None
108
+ ) -> "AgentEvent":
109
+ return cls(
110
+ type=AgentEventType.APPROVAL_DENIED.value,
111
+ timestamp=now_iso(),
112
+ data={"request_id": request_id, "reason": reason},
113
+ )
114
+
115
+
116
+ class AgentBackend:
117
+ async def start_session(self, target: dict, context: dict) -> str:
118
+ raise NotImplementedError
119
+
120
+ def run_turn(
121
+ self, session_id: str, message: str
122
+ ) -> AsyncGenerator[AgentEvent, None]:
123
+ raise NotImplementedError
124
+
125
+ def stream_events(self, session_id: str) -> AsyncGenerator[AgentEvent, None]:
126
+ raise NotImplementedError
127
+
128
+ def run_turn_events(
129
+ self, session_id: str, message: str
130
+ ) -> AsyncGenerator[Any, None]:
131
+ raise NotImplementedError
132
+
133
+ async def interrupt(self, session_id: str) -> None:
134
+ raise NotImplementedError
135
+
136
+ async def final_messages(self, session_id: str) -> list[str]:
137
+ raise NotImplementedError
138
+
139
+ async def request_approval(
140
+ self, description: str, context: Optional[Dict[str, Any]] = None
141
+ ) -> bool:
142
+ raise NotImplementedError
143
+
144
+
145
+ __all__ = [
146
+ "AgentBackend",
147
+ "AgentEvent",
148
+ "AgentEventType",
149
+ "now_iso",
150
+ ]
@@ -0,0 +1,41 @@
1
+ """Protocol for backend orchestrators used by the Engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, AsyncGenerator, Optional, Protocol
6
+
7
+ from .run_event import RunEvent
8
+
9
+
10
+ class BackendOrchestrator(Protocol):
11
+ def run_turn(
12
+ self,
13
+ *,
14
+ agent_id: str,
15
+ state: Any,
16
+ prompt: str,
17
+ model: Optional[str],
18
+ reasoning: Optional[str],
19
+ session_key: str,
20
+ ) -> AsyncGenerator[RunEvent, None]: ...
21
+
22
+ async def interrupt(self, agent_id: str, state: Any) -> None: ...
23
+
24
+ def get_thread_id(self, session_key: str) -> Optional[str]: ...
25
+
26
+ def set_thread_id(self, session_key: str, thread_id: str) -> None: ...
27
+
28
+ def build_app_server_supervisor(
29
+ self,
30
+ *,
31
+ event_prefix: str,
32
+ notification_handler: Optional[Any] = None,
33
+ ) -> Optional[Any]: ...
34
+
35
+ def ensure_opencode_supervisor(self) -> Optional[Any]: ...
36
+
37
+ def get_last_turn_id(self) -> Optional[str]: ...
38
+
39
+ def get_last_thread_info(self) -> Optional[dict[str, Any]]: ...
40
+
41
+ def get_last_token_total(self) -> Optional[dict[str, Any]]: ...
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Optional, Union
6
+
7
+
8
+ def now_iso() -> str:
9
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Started:
14
+ timestamp: str
15
+ session_id: str
16
+ thread_id: Optional[str] = None
17
+ turn_id: Optional[str] = None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class OutputDelta:
22
+ timestamp: str
23
+ content: str
24
+ delta_type: str = "text"
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ToolCall:
29
+ timestamp: str
30
+ tool_name: str
31
+ tool_input: dict[str, Any]
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ApprovalRequested:
36
+ timestamp: str
37
+ request_id: str
38
+ description: str
39
+ context: dict[str, Any]
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class TokenUsage:
44
+ timestamp: str
45
+ usage: dict[str, Any]
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class RunNotice:
50
+ timestamp: str
51
+ kind: str
52
+ message: str = ""
53
+ data: dict[str, Any] = field(default_factory=dict)
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class Completed:
58
+ timestamp: str
59
+ final_message: str = ""
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Failed:
64
+ timestamp: str
65
+ error_message: str
66
+
67
+
68
+ RunEvent = Union[
69
+ Started,
70
+ OutputDelta,
71
+ ToolCall,
72
+ ApprovalRequested,
73
+ TokenUsage,
74
+ RunNotice,
75
+ Completed,
76
+ Failed,
77
+ ]
78
+
79
+
80
+ __all__ = [
81
+ "RunEvent",
82
+ "Started",
83
+ "OutputDelta",
84
+ "ToolCall",
85
+ "ApprovalRequested",
86
+ "TokenUsage",
87
+ "RunNotice",
88
+ "Completed",
89
+ "Failed",
90
+ "now_iso",
91
+ ]
@@ -15,12 +15,20 @@ def _display_path(root: Path, path: Path) -> str:
15
15
 
16
16
 
17
17
  def build_doc_paths(config: Config) -> Mapping[str, str]:
18
+ def _safe_path(*keys: str) -> str:
19
+ for key in keys:
20
+ try:
21
+ return _display_path(config.root, config.doc_path(key))
22
+ except KeyError:
23
+ continue
24
+ return ""
25
+
18
26
  return {
19
- "todo": _display_path(config.root, config.doc_path("todo")),
20
- "progress": _display_path(config.root, config.doc_path("progress")),
21
- "opinions": _display_path(config.root, config.doc_path("opinions")),
22
- "spec": _display_path(config.root, config.doc_path("spec")),
23
- "summary": _display_path(config.root, config.doc_path("summary")),
27
+ "todo": _safe_path("todo", "active_context"),
28
+ "progress": _safe_path("progress", "decisions"),
29
+ "opinions": _safe_path("opinions"),
30
+ "spec": _safe_path("spec"),
31
+ "summary": _safe_path("summary"),
24
32
  }
25
33
 
26
34
 
@@ -64,8 +72,8 @@ def build_final_summary_prompt(
64
72
 
65
73
  doc_paths = build_doc_paths(config)
66
74
  doc_contents = {
67
- "todo": docs.read_doc("todo"),
68
- "progress": docs.read_doc("progress"),
75
+ "todo": docs.read_doc("todo") or docs.read_doc("active_context"),
76
+ "progress": docs.read_doc("progress") or docs.read_doc("decisions"),
69
77
  "opinions": docs.read_doc("opinions"),
70
78
  "spec": docs.read_doc("spec"),
71
79
  "summary": docs.read_doc("summary"),