codex-autorunner 1.1.0__py3-none-any.whl → 1.2.1__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 (134) hide show
  1. codex_autorunner/agents/opencode/client.py +113 -4
  2. codex_autorunner/agents/opencode/supervisor.py +4 -0
  3. codex_autorunner/agents/registry.py +17 -7
  4. codex_autorunner/bootstrap.py +219 -1
  5. codex_autorunner/core/__init__.py +17 -1
  6. codex_autorunner/core/about_car.py +124 -11
  7. codex_autorunner/core/app_server_threads.py +6 -0
  8. codex_autorunner/core/config.py +238 -3
  9. codex_autorunner/core/context_awareness.py +39 -0
  10. codex_autorunner/core/docs.py +0 -122
  11. codex_autorunner/core/filebox.py +265 -0
  12. codex_autorunner/core/flows/controller.py +71 -1
  13. codex_autorunner/core/flows/reconciler.py +4 -1
  14. codex_autorunner/core/flows/runtime.py +22 -0
  15. codex_autorunner/core/flows/store.py +61 -9
  16. codex_autorunner/core/flows/transition.py +23 -16
  17. codex_autorunner/core/flows/ux_helpers.py +18 -3
  18. codex_autorunner/core/flows/worker_process.py +32 -6
  19. codex_autorunner/core/hub.py +198 -41
  20. codex_autorunner/core/lifecycle_events.py +253 -0
  21. codex_autorunner/core/path_utils.py +2 -1
  22. codex_autorunner/core/pma_audit.py +224 -0
  23. codex_autorunner/core/pma_context.py +683 -0
  24. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  25. codex_autorunner/core/pma_lifecycle.py +527 -0
  26. codex_autorunner/core/pma_queue.py +367 -0
  27. codex_autorunner/core/pma_safety.py +221 -0
  28. codex_autorunner/core/pma_state.py +115 -0
  29. codex_autorunner/core/ports/agent_backend.py +2 -5
  30. codex_autorunner/core/ports/run_event.py +1 -4
  31. codex_autorunner/core/prompt.py +0 -80
  32. codex_autorunner/core/prompts.py +56 -172
  33. codex_autorunner/core/redaction.py +0 -4
  34. codex_autorunner/core/review_context.py +11 -9
  35. codex_autorunner/core/runner_controller.py +35 -33
  36. codex_autorunner/core/runner_state.py +147 -0
  37. codex_autorunner/core/runtime.py +829 -0
  38. codex_autorunner/core/sqlite_utils.py +13 -4
  39. codex_autorunner/core/state.py +7 -10
  40. codex_autorunner/core/state_roots.py +5 -0
  41. codex_autorunner/core/templates/__init__.py +39 -0
  42. codex_autorunner/core/templates/git_mirror.py +234 -0
  43. codex_autorunner/core/templates/provenance.py +56 -0
  44. codex_autorunner/core/templates/scan_cache.py +120 -0
  45. codex_autorunner/core/ticket_linter_cli.py +17 -0
  46. codex_autorunner/core/ticket_manager_cli.py +154 -92
  47. codex_autorunner/core/time_utils.py +11 -0
  48. codex_autorunner/core/types.py +18 -0
  49. codex_autorunner/core/utils.py +34 -6
  50. codex_autorunner/flows/review/service.py +23 -25
  51. codex_autorunner/flows/ticket_flow/definition.py +43 -1
  52. codex_autorunner/integrations/agents/__init__.py +2 -0
  53. codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
  54. codex_autorunner/integrations/agents/codex_backend.py +19 -8
  55. codex_autorunner/integrations/agents/runner.py +3 -8
  56. codex_autorunner/integrations/agents/wiring.py +8 -0
  57. codex_autorunner/integrations/telegram/adapter.py +1 -1
  58. codex_autorunner/integrations/telegram/config.py +1 -1
  59. codex_autorunner/integrations/telegram/doctor.py +228 -6
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
  63. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  64. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
  65. codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
  66. codex_autorunner/integrations/telegram/handlers/messages.py +34 -3
  67. codex_autorunner/integrations/telegram/helpers.py +1 -3
  68. codex_autorunner/integrations/telegram/runtime.py +9 -4
  69. codex_autorunner/integrations/telegram/service.py +30 -0
  70. codex_autorunner/integrations/telegram/state.py +38 -0
  71. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
  72. codex_autorunner/integrations/telegram/transport.py +10 -3
  73. codex_autorunner/integrations/templates/__init__.py +27 -0
  74. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  75. codex_autorunner/server.py +2 -2
  76. codex_autorunner/static/agentControls.js +21 -5
  77. codex_autorunner/static/app.js +115 -11
  78. codex_autorunner/static/archive.js +274 -81
  79. codex_autorunner/static/archiveApi.js +21 -0
  80. codex_autorunner/static/chatUploads.js +137 -0
  81. codex_autorunner/static/constants.js +1 -1
  82. codex_autorunner/static/docChatCore.js +185 -13
  83. codex_autorunner/static/fileChat.js +68 -40
  84. codex_autorunner/static/fileboxUi.js +159 -0
  85. codex_autorunner/static/hub.js +46 -81
  86. codex_autorunner/static/index.html +303 -24
  87. codex_autorunner/static/messages.js +82 -4
  88. codex_autorunner/static/notifications.js +288 -0
  89. codex_autorunner/static/pma.js +1167 -0
  90. codex_autorunner/static/settings.js +3 -0
  91. codex_autorunner/static/streamUtils.js +57 -0
  92. codex_autorunner/static/styles.css +9141 -6742
  93. codex_autorunner/static/templateReposSettings.js +225 -0
  94. codex_autorunner/static/terminalManager.js +22 -3
  95. codex_autorunner/static/ticketChatActions.js +165 -3
  96. codex_autorunner/static/ticketChatStream.js +17 -119
  97. codex_autorunner/static/ticketEditor.js +41 -13
  98. codex_autorunner/static/ticketTemplates.js +798 -0
  99. codex_autorunner/static/tickets.js +69 -19
  100. codex_autorunner/static/turnEvents.js +27 -0
  101. codex_autorunner/static/turnResume.js +33 -0
  102. codex_autorunner/static/utils.js +28 -0
  103. codex_autorunner/static/workspace.js +258 -44
  104. codex_autorunner/static/workspaceFileBrowser.js +6 -4
  105. codex_autorunner/surfaces/cli/cli.py +1465 -155
  106. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  107. codex_autorunner/surfaces/web/app.py +253 -49
  108. codex_autorunner/surfaces/web/routes/__init__.py +4 -0
  109. codex_autorunner/surfaces/web/routes/analytics.py +29 -22
  110. codex_autorunner/surfaces/web/routes/archive.py +197 -0
  111. codex_autorunner/surfaces/web/routes/file_chat.py +297 -36
  112. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  113. codex_autorunner/surfaces/web/routes/flows.py +219 -29
  114. codex_autorunner/surfaces/web/routes/messages.py +70 -39
  115. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  116. codex_autorunner/surfaces/web/routes/repos.py +1 -1
  117. codex_autorunner/surfaces/web/routes/shared.py +0 -3
  118. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  119. codex_autorunner/surfaces/web/runner_manager.py +2 -2
  120. codex_autorunner/surfaces/web/schemas.py +81 -18
  121. codex_autorunner/tickets/agent_pool.py +27 -0
  122. codex_autorunner/tickets/files.py +33 -16
  123. codex_autorunner/tickets/lint.py +50 -0
  124. codex_autorunner/tickets/models.py +3 -0
  125. codex_autorunner/tickets/outbox.py +41 -5
  126. codex_autorunner/tickets/runner.py +350 -69
  127. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +15 -19
  128. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
  129. codex_autorunner/core/adapter_utils.py +0 -21
  130. codex_autorunner/core/engine.py +0 -3302
  131. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
  132. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
  133. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
  134. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Optional
9
+
10
+ from .config import load_repo_config
11
+ from .lifecycle_events import LifecycleEvent, LifecycleEventType
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class DispatchInterceptRule:
18
+ pattern: str
19
+ reply: str
20
+ description: str = ""
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class InterceptionResult:
25
+ action: str
26
+ reason: str
27
+ reply: Optional[str] = None
28
+ escalated: bool = False
29
+
30
+
31
+ def default_dispatch_rules() -> list[DispatchInterceptRule]:
32
+ return [
33
+ DispatchInterceptRule(
34
+ pattern=r"(?i)^(proceed|continue|resume|ok|yes|go ahead)([\s!.]*)?$",
35
+ reply="Continuing with the current task.",
36
+ description="Simple continuation confirmation",
37
+ ),
38
+ DispatchInterceptRule(
39
+ pattern=r"(?i)^skip.*(?:pre.?commit|commit.*hook|hook)([\s!.]*)?$",
40
+ reply="Skipping pre-commit hooks and proceeding.",
41
+ description="Skip pre-commit hooks",
42
+ ),
43
+ ]
44
+
45
+
46
+ def _match_dispatch(
47
+ body: str, rules: list[DispatchInterceptRule]
48
+ ) -> Optional[DispatchInterceptRule]:
49
+ body_stripped = body.strip()
50
+ for rule in rules:
51
+ try:
52
+ if re.match(rule.pattern, body_stripped):
53
+ return rule
54
+ except re.error:
55
+ logger.warning("Invalid dispatch pattern: %s", rule.pattern)
56
+ continue
57
+ return None
58
+
59
+
60
+ async def _can_auto_resume_run(repo_root: Path, run_id: str) -> bool:
61
+ from .flows.models import FlowRunStatus
62
+ from .flows.store import FlowStore
63
+
64
+ db_path = repo_root / ".codex-autorunner" / "flows.db"
65
+ if not db_path.exists():
66
+ return False
67
+ try:
68
+ config = load_repo_config(repo_root)
69
+ with FlowStore(db_path, durable=config.durable_writes) as store:
70
+ run = store.get_flow_run(run_id)
71
+ if not run:
72
+ return False
73
+ return run.status == FlowRunStatus.PAUSED
74
+ except Exception as exc:
75
+ logger.warning("Failed to check run status for %s: %s", run_id, exc)
76
+ return False
77
+
78
+
79
+ async def _write_reply(repo_root: Path, run_id: str, reply_body: str) -> bool:
80
+ from ..tickets.replies import ensure_reply_dirs, resolve_reply_paths
81
+
82
+ try:
83
+ config = load_repo_config(repo_root)
84
+ runs_dir = Path(config.raw.get("runs_dir") or ".codex-autorunner/runs")
85
+ workspace_root = repo_root
86
+
87
+ reply_paths = resolve_reply_paths(
88
+ workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_id
89
+ )
90
+ ensure_reply_dirs(reply_paths)
91
+
92
+ reply_history_dir = reply_paths.reply_history_dir
93
+ if not reply_history_dir:
94
+ logger.warning("Reply history dir not found for run %s", run_id)
95
+ return False
96
+
97
+ next_seq = 1
98
+ for child in reply_history_dir.iterdir():
99
+ if child.is_dir() and child.name.isdigit():
100
+ next_seq = max(next_seq, int(child.name) + 1)
101
+
102
+ seq_dir = reply_history_dir / f"{next_seq:04d}"
103
+ seq_dir.mkdir(parents=True, exist_ok=True)
104
+
105
+ reply_content = f"""---
106
+ title: Auto-resolved by PMA
107
+ ---
108
+
109
+ {reply_body}
110
+ """
111
+ reply_path = seq_dir / "USER_REPLY.md"
112
+ reply_path.write_text(reply_content, encoding="utf-8")
113
+
114
+ return True
115
+
116
+ except Exception:
117
+ logger.exception("Failed to write reply for run %s", run_id)
118
+ return False
119
+
120
+
121
+ class PmaDispatchInterceptor:
122
+ def __init__(
123
+ self,
124
+ hub_root: Path,
125
+ supervisor: Any,
126
+ rules: Optional[list[DispatchInterceptRule]] = None,
127
+ on_intercept: Optional[Callable[[str, InterceptionResult], None]] = None,
128
+ ):
129
+ self._hub_root = hub_root
130
+ self._supervisor = supervisor
131
+ self._rules = rules or default_dispatch_rules()
132
+ self._on_intercept = on_intercept
133
+ self._processing = set[str]()
134
+ self._logger = logging.getLogger(f"{__name__}.PmaDispatchInterceptor")
135
+
136
+ async def process_dispatch_event(
137
+ self, event: LifecycleEvent, repo_root: Path
138
+ ) -> Optional[InterceptionResult]:
139
+ if event.event_type != LifecycleEventType.DISPATCH_CREATED:
140
+ return None
141
+
142
+ event_id = event.event_id
143
+ if event_id in self._processing:
144
+ return None
145
+
146
+ run_id = event.run_id
147
+ repo_id = event.data.get("repo_id") or event.repo_id
148
+
149
+ try:
150
+ self._processing.add(event_id)
151
+
152
+ self._logger.info(
153
+ "Processing dispatch event %s for run %s in repo %s",
154
+ event_id,
155
+ run_id,
156
+ repo_id,
157
+ )
158
+
159
+ from .pma_context import _latest_dispatch
160
+
161
+ latest = _latest_dispatch(repo_root, run_id, {}, max_text_chars=10000)
162
+ if not latest or not latest.get("dispatch"):
163
+ return InterceptionResult(
164
+ action="ignore",
165
+ reason="No dispatch found or dispatch is invalid",
166
+ )
167
+
168
+ dispatch_info = latest["dispatch"]
169
+ mode = dispatch_info.get("mode")
170
+ body = dispatch_info.get("body", "")
171
+
172
+ if mode != "pause":
173
+ return InterceptionResult(
174
+ action="ignore",
175
+ reason=f"Dispatch mode is '{mode}', only 'pause' is intercepted",
176
+ )
177
+
178
+ matched_rule = _match_dispatch(body, self._rules)
179
+ if matched_rule:
180
+ self._logger.info(
181
+ "Dispatch matched auto-resolve rule: %s",
182
+ matched_rule.description,
183
+ )
184
+
185
+ can_resume = await _can_auto_resume_run(repo_root, run_id)
186
+ if not can_resume:
187
+ return InterceptionResult(
188
+ action="escalate",
189
+ reason="Run cannot be auto-resumed",
190
+ escalated=True,
191
+ )
192
+
193
+ success = await _write_reply(repo_root, run_id, matched_rule.reply)
194
+ if success:
195
+ result = InterceptionResult(
196
+ action="auto_resolved",
197
+ reason=matched_rule.description,
198
+ reply=matched_rule.reply,
199
+ )
200
+ if self._on_intercept:
201
+ self._on_intercept(event_id, result)
202
+ return result
203
+ else:
204
+ return InterceptionResult(
205
+ action="escalate",
206
+ reason="Failed to write reply",
207
+ escalated=True,
208
+ )
209
+
210
+ return InterceptionResult(
211
+ action="escalate",
212
+ reason="Dispatch does not match any auto-resolve rule",
213
+ escalated=True,
214
+ )
215
+
216
+ except Exception as exc:
217
+ self._logger.exception(
218
+ "Error processing dispatch event %s for run %s", event_id, run_id
219
+ )
220
+ return InterceptionResult(
221
+ action="error",
222
+ reason=f"Processing error: {exc}",
223
+ )
224
+ finally:
225
+ self._processing.discard(event_id)
226
+
227
+
228
+ async def run_dispatch_interceptor(
229
+ hub_root: Path,
230
+ supervisor: Any,
231
+ interval_seconds: float = 5.0,
232
+ on_intercept: Optional[Callable[[str, InterceptionResult], None]] = None,
233
+ ) -> asyncio.Task[None]:
234
+ interceptor = PmaDispatchInterceptor(
235
+ hub_root=hub_root,
236
+ supervisor=supervisor,
237
+ on_intercept=on_intercept,
238
+ )
239
+
240
+ async def loop() -> None:
241
+ while True:
242
+ try:
243
+ lifecycle_store = supervisor.lifecycle_store
244
+ unprocessed = lifecycle_store.get_unprocessed(limit=50)
245
+
246
+ for event in unprocessed:
247
+ if event.event_type != LifecycleEventType.DISPATCH_CREATED:
248
+ continue
249
+
250
+ repo_id = event.repo_id
251
+ snapshots = supervisor.list_repos()
252
+ repo_snapshot = None
253
+ for snap in snapshots:
254
+ if snap.id == repo_id:
255
+ repo_snapshot = snap
256
+ break
257
+
258
+ if not repo_snapshot or not repo_snapshot.exists_on_disk:
259
+ lifecycle_store.mark_processed(event.event_id)
260
+ continue
261
+
262
+ result = await interceptor.process_dispatch_event(
263
+ event, repo_snapshot.path
264
+ )
265
+
266
+ if result and result.action != "ignore":
267
+ lifecycle_store.mark_processed(event.event_id)
268
+
269
+ await asyncio.sleep(interval_seconds)
270
+ except asyncio.CancelledError:
271
+ break
272
+ except Exception:
273
+ logger.exception("Dispatch interceptor loop error")
274
+
275
+ return asyncio.create_task(loop())
276
+
277
+
278
+ __all__ = [
279
+ "DispatchInterceptRule",
280
+ "InterceptionResult",
281
+ "default_dispatch_rules",
282
+ "PmaDispatchInterceptor",
283
+ "run_dispatch_interceptor",
284
+ ]