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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from .
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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,
|
|
268
|
-
* Treat
|
|
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
|
|
275
|
-
* High-signal risks and regressions that would violate
|
|
276
|
-
* Trackable output that can be turned into
|
|
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
|
-
*
|
|
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
|
-
##
|
|
319
|
-
-
|
|
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
|
-
|
|
394
|
+
ctx: RuntimeContext,
|
|
392
395
|
*,
|
|
393
396
|
opencode_supervisor: Optional[OpenCodeSupervisor] = None,
|
|
394
|
-
app_server_supervisor: Optional[
|
|
397
|
+
app_server_supervisor: Optional[Any] = None,
|
|
395
398
|
logger: Optional[logging.Logger] = None,
|
|
396
399
|
) -> None:
|
|
397
|
-
self.
|
|
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(
|
|
404
|
+
self._state_path = _workflow_root(self.ctx.repo_root) / "state.json"
|
|
402
405
|
self._lock_path = (
|
|
403
|
-
|
|
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.
|
|
414
|
+
if not isinstance(self.ctx.config, RepoConfig):
|
|
412
415
|
raise ReviewError("Review requires a repo workspace config")
|
|
413
|
-
return self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 ...
|
|
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
|
-
|
|
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
|
-
|
|
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 .
|
|
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 .
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
"
|
|
16
|
-
"AgentEvent",
|
|
17
|
-
"AgentEventType",
|
|
12
|
+
"CodexAdapterOrchestrator",
|
|
18
13
|
"CodexAppServerBackend",
|
|
14
|
+
"OpenCodeAdapterOrchestrator",
|
|
19
15
|
"OpenCodeBackend",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
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
|
+
]
|