codex-autorunner 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -10,15 +10,20 @@ import zipfile
10
10
  from pathlib import Path
11
11
  from typing import Any, Optional
12
12
 
13
- from ..agents.opencode.run_prompt import OpenCodeRunConfig, run_opencode_prompt
14
- from ..agents.opencode.supervisor import OpenCodeSupervisor
15
- from ..agents.registry import has_capability, validate_agent_id
16
- from ..integrations.app_server.supervisor import WorkspaceAppServerSupervisor
17
- from .config import RepoConfig
18
- from .engine import Engine
19
- from .locks import FileLock, FileLockBusy, FileLockError, process_alive, read_lock_info
20
- from .state import now_iso
21
- from .utils import atomic_write, read_json
13
+ from ...agents.opencode.run_prompt import OpenCodeRunConfig, run_opencode_prompt
14
+ from ...agents.opencode.supervisor import OpenCodeSupervisor
15
+ from ...agents.registry import has_capability, validate_agent_id
16
+ from ...core.config import RepoConfig
17
+ from ...core.locks import (
18
+ FileLock,
19
+ FileLockBusy,
20
+ FileLockError,
21
+ process_alive,
22
+ read_lock_info,
23
+ )
24
+ from ...core.runtime import RuntimeContext
25
+ from ...core.state import now_iso
26
+ from ...core.utils import atomic_write, read_json
22
27
 
23
28
  REVIEW_STATE_VERSION = 1
24
29
  REVIEW_TIMEOUT_SECONDS = 3600
@@ -247,7 +252,7 @@ Stop digging deeper when:
247
252
  """
248
253
  REVIEW_PROMPT_SPEC_PROGRESS = """# Autorunner Final Review (Spec + Progress Focus)
249
254
 
250
- You are coordinating a multi-agent review immediately after an autorunner loop completes. The goal is to validate work against SPEC.md and PROGRESS.md, not to suggest generic code style fixes.
255
+ You are coordinating a multi-agent review immediately after an autorunner loop completes. The goal is to validate work against spec.md, not to suggest generic code style fixes.
251
256
 
252
257
  ## Required Scratchpad + Output
253
258
 
@@ -264,16 +269,16 @@ You are coordinating a multi-agent review immediately after an autorunner loop c
264
269
 
265
270
  ## Source of Truth + Focus
266
271
 
267
- * Read AUTORUNNER_CONTEXT.md first. It contains the exit reason, SPEC.md, PROGRESS.md, optional TODO/SUMMARY, and the last-run artifacts (output/diff/plan excerpts).
268
- * Treat SPEC.md + PROGRESS.md as contracts: extract explicit requirements, promised validation steps, and open questions.
272
+ * Read AUTORUNNER_CONTEXT.md first. It contains the exit reason, spec.md, and last-run artifacts (output/diff/plan excerpts).
273
+ * Treat spec.md as the contract: extract explicit requirements, promised validation steps, and open questions.
269
274
  * Derive must-hold invariants directly from SPEC (not generic guesses).
270
275
  * Buckets should be anchored to spec sections mapped to implementation areas, plus any recently changed/critical modules from AUTORUNNER_CONTEXT.md.
271
276
 
272
277
  ## North Star
273
278
 
274
- * Spec compliance and claim verification over style.
275
- * High-signal risks and regressions that would violate SPEC/PROGRESS commitments.
276
- * Trackable output that can be turned into TODO items.
279
+ * Spec compliance over style.
280
+ * High-signal risks and regressions that would violate spec commitments.
281
+ * Trackable output that can be turned into tickets.
277
282
 
278
283
  ## Phase 0: Setup (Coordinator)
279
284
 
@@ -283,8 +288,7 @@ Prepare scratchpad files under {{scratchpad_dir}} (see list above).
283
288
 
284
289
  1) Read AUTORUNNER_CONTEXT.md and summarize:
285
290
  * Project shape and runtime assumptions
286
- * SPEC requirements + invariants
287
- * PROGRESS claims + validation evidence
291
+ * Spec requirements + invariants
288
292
  * Open questions/gaps
289
293
  2) Define buckets in BUCKETS.md:
290
294
  * Buckets by review dimension + code areas (spec sections → code paths)
@@ -294,7 +298,6 @@ Prepare scratchpad files under {{scratchpad_dir}} (see list above).
294
298
 
295
299
  Launch subagents by review dimension:
296
300
  * Spec compliance agent: requirement → evidence mapping.
297
- * Progress verification agent: PROGRESS claims → repo/tests evidence.
298
301
  * Risk & regression agent: likely failure modes introduced by recent changes.
299
302
  * Optional: Test adequacy agent if tests are in-scope.
300
303
 
@@ -315,8 +318,8 @@ Create the final report at {{final_output_path}} with this structure:
315
318
  ## Spec-to-Implementation Matrix
316
319
  | Spec item | Status (met/partial/missing) | Evidence | Notes |
317
320
 
318
- ## Progress Claim Verification
319
- - Claim: ... (cite PROGRESS.md section)
321
+ ## Spec Verification
322
+ - Spec requirement: ... (cite spec section)
320
323
  - Verified by: ...
321
324
  - Not verified because: ...
322
325
 
@@ -388,19 +391,19 @@ def _default_state() -> dict[str, Any]:
388
391
  class ReviewService:
389
392
  def __init__(
390
393
  self,
391
- engine: Engine,
394
+ ctx: RuntimeContext,
392
395
  *,
393
396
  opencode_supervisor: Optional[OpenCodeSupervisor] = None,
394
- app_server_supervisor: Optional[WorkspaceAppServerSupervisor] = None,
397
+ app_server_supervisor: Optional[Any] = None,
395
398
  logger: Optional[logging.Logger] = None,
396
399
  ) -> None:
397
- self.engine = engine
400
+ self.ctx = ctx
398
401
  self._opencode_supervisor = opencode_supervisor
399
402
  self._app_server_supervisor = app_server_supervisor
400
403
  self._logger = logger or logging.getLogger("codex_autorunner.review")
401
- self._state_path = _workflow_root(engine.repo_root) / "state.json"
404
+ self._state_path = _workflow_root(self.ctx.repo_root) / "state.json"
402
405
  self._lock_path = (
403
- engine.repo_root / ".codex-autorunner" / "locks" / "review.lock"
406
+ self.ctx.repo_root / ".codex-autorunner" / "locks" / "review.lock"
404
407
  )
405
408
  self._thread: Optional[threading.Thread] = None
406
409
  self._thread_lock = threading.Lock()
@@ -408,9 +411,9 @@ class ReviewService:
408
411
  self._lock_handle: Optional[FileLock] = None
409
412
 
410
413
  def _repo_config(self) -> RepoConfig:
411
- if not isinstance(self.engine.config, RepoConfig):
414
+ if not isinstance(self.ctx.config, RepoConfig):
412
415
  raise ReviewError("Review requires a repo workspace config")
413
- return self.engine.config
416
+ return self.ctx.config
414
417
 
415
418
  def status(self) -> dict[str, Any]:
416
419
  state = self._load_state()
@@ -433,7 +436,7 @@ class ReviewService:
433
436
  raise ReviewBusyError("Review already running", status_code=409)
434
437
  if self._thread and self._thread.is_alive():
435
438
  raise ReviewBusyError("Review already running", status_code=409)
436
- busy_reason = self.engine.repo_busy_reason()
439
+ busy_reason = self.ctx.repo_busy_reason()
437
440
  if busy_reason:
438
441
  raise ReviewConflictError(
439
442
  f"Cannot start review: {busy_reason}", status_code=409
@@ -470,7 +473,7 @@ class ReviewService:
470
473
  state = self.status()
471
474
  if state.get("status") in ("running", "stopping"):
472
475
  raise ReviewBusyError("Review already running", status_code=409)
473
- busy_reason = self.engine.repo_busy_reason()
476
+ busy_reason = self.ctx.repo_busy_reason()
474
477
  if busy_reason and not ignore_repo_busy:
475
478
  raise ReviewConflictError(
476
479
  f"Cannot start review: {busy_reason}", status_code=409
@@ -675,7 +678,7 @@ class ReviewService:
675
678
  )
676
679
 
677
680
  run_id = state["id"]
678
- runs_dir = _workflow_root(self.engine.repo_root) / "runs"
681
+ runs_dir = _workflow_root(self.ctx.repo_root) / "runs"
679
682
  run_dir = runs_dir / run_id
680
683
  run_dir.mkdir(parents=True, exist_ok=True)
681
684
 
@@ -730,7 +733,7 @@ class ReviewService:
730
733
  if agent_id == "codex":
731
734
  if self._app_server_supervisor is None:
732
735
  raise ReviewError("Codex backend is not configured")
733
- client = await self._app_server_supervisor.get_client(self.engine.repo_root)
736
+ client = await self._app_server_supervisor.get_client(self.ctx.repo_root)
734
737
  thread_id = uuid.uuid4().hex
735
738
  review_kwargs: dict[str, Any] = {}
736
739
  if state.get("model"):
@@ -741,7 +744,7 @@ class ReviewService:
741
744
  thread_id=thread_id,
742
745
  target={"type": "custom", "instructions": prompt},
743
746
  delivery="inline",
744
- cwd=str(self.engine.repo_root),
747
+ cwd=str(self.ctx.repo_root),
745
748
  **review_kwargs,
746
749
  )
747
750
 
@@ -797,7 +800,7 @@ class ReviewService:
797
800
  subagent_model = review_cfg.get("subagent_model")
798
801
  if subagent_agent_id:
799
802
  await self._opencode_supervisor.ensure_subagent_config(
800
- workspace_root=self.engine.repo_root,
803
+ workspace_root=self.ctx.repo_root,
801
804
  agent_id=subagent_agent_id,
802
805
  model=subagent_model,
803
806
  )
@@ -807,7 +810,7 @@ class ReviewService:
807
810
  model=state["model"],
808
811
  reasoning=state.get("reasoning"),
809
812
  prompt=prompt,
810
- workspace_root=str(self.engine.repo_root),
813
+ workspace_root=str(self.ctx.repo_root),
811
814
  timeout_seconds=timeout_seconds,
812
815
  interrupt_grace_seconds=REVIEW_INTERRUPT_GRACE_SECONDS,
813
816
  permission_policy="allow",
@@ -6,7 +6,13 @@ from typing import Any, Dict, Optional
6
6
  from ...core.flows.definition import EmitEventFn, FlowDefinition, StepOutcome
7
7
  from ...core.flows.models import FlowEventType, FlowRunRecord
8
8
  from ...core.utils import find_repo_root
9
- from ...tickets import AgentPool, TicketRunConfig, TicketRunner
9
+ from ...manifest import ManifestError, load_manifest
10
+ from ...tickets import (
11
+ DEFAULT_MAX_TOTAL_TURNS,
12
+ AgentPool,
13
+ TicketRunConfig,
14
+ TicketRunner,
15
+ )
10
16
 
11
17
 
12
18
  def build_ticket_flow_definition(*, agent_pool: AgentPool) -> FlowDefinition:
@@ -30,16 +36,36 @@ def build_ticket_flow_definition(*, agent_pool: AgentPool) -> FlowDefinition:
30
36
  engine_state = dict(engine_state) if isinstance(engine_state, dict) else {}
31
37
 
32
38
  repo_root = find_repo_root()
33
- workspace_root = Path(input_data.get("workspace_root") or repo_root)
39
+ raw_workspace = input_data.get("workspace_root") or repo_root
40
+ workspace_root = Path(raw_workspace)
41
+ if not workspace_root.is_absolute():
42
+ workspace_root = (Path(repo_root) / workspace_root).resolve()
43
+ else:
44
+ workspace_root = workspace_root.resolve()
45
+
34
46
  ticket_dir = Path(input_data.get("ticket_dir") or ".codex-autorunner/tickets")
47
+ if not ticket_dir.is_absolute():
48
+ ticket_dir = (workspace_root / ticket_dir).resolve()
49
+
35
50
  runs_dir = Path(input_data.get("runs_dir") or ".codex-autorunner/runs")
36
- max_total_turns = int(input_data.get("max_total_turns") or 25)
51
+ if not runs_dir.is_absolute():
52
+ runs_dir = (workspace_root / runs_dir).resolve()
53
+ max_total_turns = int(
54
+ input_data.get("max_total_turns") or DEFAULT_MAX_TOTAL_TURNS
55
+ )
37
56
  max_lint_retries = int(input_data.get("max_lint_retries") or 3)
38
57
  max_commit_retries = int(input_data.get("max_commit_retries") or 2)
58
+ max_network_retries = int(input_data.get("max_network_retries") or 5)
39
59
  auto_commit = bool(
40
60
  input_data.get("auto_commit") if "auto_commit" in input_data else True
41
61
  )
62
+ include_previous_ticket_context = bool(
63
+ input_data.get("include_previous_ticket_context")
64
+ if "include_previous_ticket_context" in input_data
65
+ else False
66
+ )
42
67
 
68
+ repo_id = _resolve_ticket_flow_repo_id(workspace_root)
43
69
  runner = TicketRunner(
44
70
  workspace_root=workspace_root,
45
71
  run_id=str(record.id),
@@ -49,9 +75,12 @@ def build_ticket_flow_definition(*, agent_pool: AgentPool) -> FlowDefinition:
49
75
  max_total_turns=max_total_turns,
50
76
  max_lint_retries=max_lint_retries,
51
77
  max_commit_retries=max_commit_retries,
78
+ max_network_retries=max_network_retries,
52
79
  auto_commit=auto_commit,
80
+ include_previous_ticket_context=include_previous_ticket_context,
53
81
  ),
54
82
  agent_pool=agent_pool,
83
+ repo_id=repo_id,
55
84
  )
56
85
 
57
86
  if emit_event is not None:
@@ -84,8 +113,28 @@ def build_ticket_flow_definition(*, agent_pool: AgentPool) -> FlowDefinition:
84
113
  "max_total_turns": {"type": "integer"},
85
114
  "max_lint_retries": {"type": "integer"},
86
115
  "max_commit_retries": {"type": "integer"},
116
+ "max_network_retries": {"type": "integer"},
87
117
  "auto_commit": {"type": "boolean"},
118
+ "include_previous_ticket_context": {"type": "boolean"},
88
119
  },
89
120
  },
90
121
  steps={"ticket_turn": _ticket_turn_step},
91
122
  )
123
+
124
+
125
+ def _resolve_ticket_flow_repo_id(workspace_root: Path) -> str:
126
+ current = workspace_root
127
+ for _ in range(5):
128
+ manifest_path = current / ".codex-autorunner" / "manifest.yml"
129
+ if manifest_path.exists():
130
+ try:
131
+ manifest = load_manifest(manifest_path, current)
132
+ except ManifestError:
133
+ return ""
134
+ entry = manifest.get_by_path(current, workspace_root)
135
+ return entry.id if entry else ""
136
+ parent = current.parent
137
+ if parent == current:
138
+ break
139
+ current = parent
140
+ return ""
@@ -1,27 +1,19 @@
1
- from .agent_backend import AgentBackend, AgentEvent, AgentEventType
1
+ from .backend_orchestrator import build_backend_orchestrator
2
+ from .codex_adapter import CodexAdapterOrchestrator
2
3
  from .codex_backend import CodexAppServerBackend
4
+ from .opencode_adapter import OpenCodeAdapterOrchestrator
3
5
  from .opencode_backend import OpenCodeBackend
4
- from .run_event import (
5
- ApprovalRequested,
6
- Completed,
7
- Failed,
8
- OutputDelta,
9
- RunEvent,
10
- Started,
11
- ToolCall,
6
+ from .wiring import (
7
+ build_agent_backend_factory,
8
+ build_app_server_supervisor_factory,
12
9
  )
13
10
 
14
11
  __all__ = [
15
- "AgentBackend",
16
- "AgentEvent",
17
- "AgentEventType",
12
+ "CodexAdapterOrchestrator",
18
13
  "CodexAppServerBackend",
14
+ "OpenCodeAdapterOrchestrator",
19
15
  "OpenCodeBackend",
20
- "RunEvent",
21
- "Started",
22
- "OutputDelta",
23
- "ToolCall",
24
- "ApprovalRequested",
25
- "Completed",
26
- "Failed",
16
+ "build_agent_backend_factory",
17
+ "build_app_server_supervisor_factory",
18
+ "build_backend_orchestrator",
27
19
  ]
@@ -0,0 +1,302 @@
1
+ """
2
+ Backend orchestrator that manages protocol-agnostic backend lifecycle.
3
+
4
+ The orchestrator sits between the Engine and backend adapters, handling
5
+ backend-specific concerns like supervisor management, event handling,
6
+ and session/thread tracking while exposing a clean, protocol-neutral
7
+ interface to the Engine.
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ import threading
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any, AsyncGenerator, Awaitable, Callable, Optional
16
+
17
+ from ...core.app_server_threads import (
18
+ AppServerThreadRegistry,
19
+ default_app_server_threads_path,
20
+ )
21
+ from ...core.config import RepoConfig
22
+ from ...core.ports.agent_backend import AgentBackend
23
+ from ...core.ports.backend_orchestrator import (
24
+ BackendOrchestrator as BackendOrchestratorProtocol,
25
+ )
26
+ from ...core.ports.run_event import RunEvent
27
+ from ...core.state import RunnerState
28
+ from .codex_backend import CodexAppServerBackend
29
+ from .opencode_backend import OpenCodeBackend
30
+ from .wiring import AgentBackendFactory, BackendFactory
31
+
32
+ NotificationHandler = Callable[[dict[str, Any]], Awaitable[None]]
33
+ SessionIdGetter = Callable[[str], Optional[str]]
34
+ SessionIdSetter = Callable[[str, str], None]
35
+
36
+
37
+ @dataclass
38
+ class BackendContext:
39
+ """Context for a backend run."""
40
+
41
+ agent_id: str
42
+ session_id: Optional[str]
43
+ turn_id: Optional[str]
44
+ thread_info: Optional[dict[str, Any]]
45
+
46
+
47
+ class BackendOrchestrator:
48
+ """
49
+ Orchestrates backend operations, keeping Engine protocol-agnostic.
50
+
51
+ This class manages:
52
+ - Backend factory and lifecycle
53
+ - Backend-specific supervisors (Codex app server, OpenCode)
54
+ - Backend-specific event handling and notification routing
55
+ - Session/thread tracking for backends that support it
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ repo_root: Path,
61
+ config: RepoConfig,
62
+ *,
63
+ notification_handler: Optional[NotificationHandler] = None,
64
+ logger: Optional[logging.Logger] = None,
65
+ ):
66
+ from .wiring import build_agent_backend_factory
67
+
68
+ self._repo_root = repo_root
69
+ self._config = config
70
+ self._logger = logger or logging.getLogger("codex_autorunner.backend")
71
+ self._notification_handler = notification_handler
72
+
73
+ # Backend factory manages creation and caching of backends
74
+ self._backend_factory: BackendFactory = build_agent_backend_factory(
75
+ repo_root, config
76
+ )
77
+
78
+ # Active backend for current run
79
+ self._active_backend: Optional[AgentBackend] = None
80
+
81
+ # Context tracking
82
+ self._context: Optional[BackendContext] = None
83
+
84
+ # Session registry for backend-specific session tracking
85
+ self._app_server_threads = AppServerThreadRegistry(
86
+ default_app_server_threads_path(repo_root)
87
+ )
88
+ self._app_server_threads_lock = threading.Lock()
89
+
90
+ async def get_backend(
91
+ self,
92
+ agent_id: str,
93
+ state: RunnerState,
94
+ ) -> AgentBackend:
95
+ """Get a backend instance for the given agent."""
96
+ backend = self._backend_factory(agent_id, state, self._notification_handler)
97
+ self._active_backend = backend
98
+ return backend
99
+
100
+ async def start_session(
101
+ self,
102
+ agent_id: str,
103
+ state: RunnerState,
104
+ session_id: Optional[str] = None,
105
+ ) -> str:
106
+ """
107
+ Start a backend session.
108
+
109
+ Returns the session/thread ID.
110
+ """
111
+ backend = await self.get_backend(agent_id, state)
112
+
113
+ context: dict[str, Any] = {"workspace": str(self._repo_root)}
114
+ if session_id:
115
+ context["session_id"] = session_id
116
+
117
+ target = {"workspace": str(self._repo_root)}
118
+
119
+ session = await backend.start_session(target, context)
120
+
121
+ # Track context
122
+ self._context = BackendContext(
123
+ agent_id=agent_id,
124
+ session_id=session,
125
+ turn_id=None,
126
+ thread_info=None,
127
+ )
128
+
129
+ return session
130
+
131
+ async def run_turn(
132
+ self,
133
+ agent_id: str,
134
+ state: RunnerState,
135
+ prompt: str,
136
+ *,
137
+ model: Optional[str] = None,
138
+ reasoning: Optional[str] = None,
139
+ session_key: Optional[str] = None,
140
+ ) -> AsyncGenerator[RunEvent, None]:
141
+ """
142
+ Run a turn on the backend.
143
+
144
+ Yields RunEvent objects.
145
+ """
146
+ reuse_session = bool(getattr(self._config, "autorunner_reuse_session", False))
147
+ session_id: Optional[str] = None
148
+ if reuse_session and session_key:
149
+ session_id = self.get_thread_id(session_key)
150
+ if reuse_session and session_id is None and self._context is not None:
151
+ session_id = self._context.session_id
152
+
153
+ session_id = await self.start_session(agent_id, state, session_id=session_id)
154
+ if reuse_session and session_key and session_id:
155
+ self.set_thread_id(session_key, session_id)
156
+
157
+ backend = self._active_backend
158
+ assert backend is not None, "backend should be initialized before run_turn"
159
+
160
+ # Configure backend if supported
161
+ if isinstance(backend, CodexAppServerBackend):
162
+ backend.configure(
163
+ approval_policy=state.autorunner_approval_policy or "never",
164
+ sandbox_policy=state.autorunner_sandbox_mode or "dangerFullAccess",
165
+ model=model,
166
+ reasoning_effort=reasoning,
167
+ turn_timeout_seconds=None,
168
+ notification_handler=self._notification_handler,
169
+ )
170
+ elif isinstance(backend, OpenCodeBackend):
171
+ backend.configure(
172
+ model=model,
173
+ reasoning=reasoning,
174
+ approval_policy=state.autorunner_approval_policy,
175
+ )
176
+
177
+ async for event in backend.run_turn_events(session_id, prompt):
178
+ yield event
179
+
180
+ # Update context from events
181
+ if hasattr(event, "session_id") and event.session_id:
182
+ if self._context:
183
+ self._context.session_id = event.session_id
184
+
185
+ async def interrupt(self, agent_id: str, state: RunnerState) -> None:
186
+ """Interrupt the current backend session."""
187
+ if self._context and self._context.session_id:
188
+ backend = await self.get_backend(agent_id, state)
189
+ await backend.interrupt(self._context.session_id)
190
+
191
+ def get_context(self) -> Optional[BackendContext]:
192
+ """Get the current backend context."""
193
+ return self._context
194
+
195
+ def get_last_turn_id(self) -> Optional[str]:
196
+ """Get the last turn ID from the active backend."""
197
+ if self._active_backend:
198
+ return getattr(self._active_backend, "last_turn_id", None)
199
+ if self._context:
200
+ return self._context.turn_id
201
+ return None
202
+
203
+ def get_last_thread_info(self) -> Optional[dict[str, Any]]:
204
+ """Get the last thread info from the active backend."""
205
+ if self._active_backend:
206
+ return getattr(self._active_backend, "last_thread_info", None)
207
+ if self._context:
208
+ return self._context.thread_info
209
+ return None
210
+
211
+ def get_last_token_total(self) -> Optional[dict[str, Any]]:
212
+ """Get the last token total from the active backend."""
213
+ if self._active_backend:
214
+ return getattr(self._active_backend, "last_token_total", None)
215
+ return None
216
+
217
+ async def close_all(self) -> None:
218
+ """Close all backends and clean up resources."""
219
+ close_all = getattr(self._backend_factory, "close_all", None)
220
+ if close_all:
221
+ result = close_all()
222
+ if asyncio.iscoroutine(result):
223
+ await result
224
+ self._active_backend = None
225
+ self._context = None
226
+
227
+ def update_context(
228
+ self,
229
+ *,
230
+ turn_id: Optional[str] = None,
231
+ thread_info: Optional[dict[str, Any]] = None,
232
+ ) -> None:
233
+ """Update the backend context with new information."""
234
+ if self._context:
235
+ if turn_id:
236
+ self._context.turn_id = turn_id
237
+ if thread_info:
238
+ self._context.thread_info = thread_info
239
+
240
+ def get_thread_id(self, session_key: str) -> Optional[str]:
241
+ """Get the thread ID for a given session key."""
242
+ with self._app_server_threads_lock:
243
+ return self._app_server_threads.get_thread_id(session_key)
244
+
245
+ def set_thread_id(self, session_key: str, thread_id: str) -> None:
246
+ """Set the thread ID for a given session key."""
247
+ with self._app_server_threads_lock:
248
+ self._app_server_threads.set_thread_id(session_key, thread_id)
249
+
250
+ def _agent_backend_factory(self) -> Optional[AgentBackendFactory]:
251
+ if isinstance(self._backend_factory, AgentBackendFactory):
252
+ return self._backend_factory
253
+ return None
254
+
255
+ def ensure_opencode_supervisor(self) -> Optional[Any]:
256
+ """
257
+ Ensure OpenCode supervisor exists.
258
+
259
+ This method delegates to the backend factory for supervisor management,
260
+ keeping Engine protocol-agnostic.
261
+ """
262
+ factory = self._agent_backend_factory()
263
+ if factory is not None:
264
+ return factory._ensure_opencode_supervisor()
265
+ return None
266
+
267
+ def build_app_server_supervisor(
268
+ self, *, event_prefix: str, notification_handler: Optional[NotificationHandler]
269
+ ) -> Optional[Any]:
270
+ """
271
+ Build a Codex app server supervisor factory.
272
+
273
+ This method centralizes backend-specific supervisor creation, keeping
274
+ Engine protocol-agnostic.
275
+ """
276
+ from .wiring import build_app_server_supervisor_factory
277
+
278
+ factory_fn = build_app_server_supervisor_factory(
279
+ self._config, logger=self._logger
280
+ )
281
+ return factory_fn(event_prefix, notification_handler)
282
+
283
+
284
+ def build_backend_orchestrator(
285
+ repo_root: Path, config: RepoConfig
286
+ ) -> BackendOrchestratorProtocol:
287
+ """
288
+ Build a BackendOrchestrator for protocol-agnostic backend management.
289
+ """
290
+ return BackendOrchestrator(
291
+ repo_root=repo_root,
292
+ config=config,
293
+ notification_handler=None,
294
+ logger=logging.getLogger("codex_autorunner.backend"),
295
+ )
296
+
297
+
298
+ __all__ = [
299
+ "BackendOrchestrator",
300
+ "BackendContext",
301
+ "build_backend_orchestrator",
302
+ ]