codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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 (189) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/opencode/client.py +68 -35
  3. codex_autorunner/agents/opencode/logging.py +21 -5
  4. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  5. codex_autorunner/agents/opencode/runtime.py +118 -30
  6. codex_autorunner/agents/opencode/supervisor.py +36 -48
  7. codex_autorunner/agents/registry.py +136 -8
  8. codex_autorunner/api.py +25 -0
  9. codex_autorunner/bootstrap.py +16 -35
  10. codex_autorunner/cli.py +157 -139
  11. codex_autorunner/core/about_car.py +44 -32
  12. codex_autorunner/core/adapter_utils.py +21 -0
  13. codex_autorunner/core/app_server_logging.py +7 -3
  14. codex_autorunner/core/app_server_prompts.py +27 -260
  15. codex_autorunner/core/app_server_threads.py +15 -26
  16. codex_autorunner/core/codex_runner.py +6 -0
  17. codex_autorunner/core/config.py +390 -100
  18. codex_autorunner/core/docs.py +10 -2
  19. codex_autorunner/core/drafts.py +82 -0
  20. codex_autorunner/core/engine.py +278 -262
  21. codex_autorunner/core/flows/__init__.py +25 -0
  22. codex_autorunner/core/flows/controller.py +178 -0
  23. codex_autorunner/core/flows/definition.py +82 -0
  24. codex_autorunner/core/flows/models.py +75 -0
  25. codex_autorunner/core/flows/runtime.py +351 -0
  26. codex_autorunner/core/flows/store.py +485 -0
  27. codex_autorunner/core/flows/transition.py +133 -0
  28. codex_autorunner/core/flows/worker_process.py +242 -0
  29. codex_autorunner/core/hub.py +15 -9
  30. codex_autorunner/core/locks.py +4 -0
  31. codex_autorunner/core/prompt.py +15 -7
  32. codex_autorunner/core/redaction.py +29 -0
  33. codex_autorunner/core/review_context.py +5 -8
  34. codex_autorunner/core/run_index.py +6 -0
  35. codex_autorunner/core/runner_process.py +5 -2
  36. codex_autorunner/core/state.py +0 -88
  37. codex_autorunner/core/static_assets.py +55 -0
  38. codex_autorunner/core/supervisor_utils.py +67 -0
  39. codex_autorunner/core/update.py +20 -11
  40. codex_autorunner/core/update_runner.py +2 -0
  41. codex_autorunner/core/utils.py +29 -2
  42. codex_autorunner/discovery.py +2 -4
  43. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  44. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  45. codex_autorunner/integrations/agents/__init__.py +27 -0
  46. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  47. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  48. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  49. codex_autorunner/integrations/agents/run_event.py +71 -0
  50. codex_autorunner/integrations/app_server/client.py +576 -92
  51. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  52. codex_autorunner/integrations/telegram/adapter.py +141 -167
  53. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  54. codex_autorunner/integrations/telegram/config.py +175 -0
  55. codex_autorunner/integrations/telegram/constants.py +16 -1
  56. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  57. codex_autorunner/integrations/telegram/doctor.py +47 -0
  58. codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
  59. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  64. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  65. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  66. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
  67. codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
  68. codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
  69. codex_autorunner/integrations/telegram/helpers.py +88 -16
  70. codex_autorunner/integrations/telegram/outbox.py +208 -37
  71. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  72. codex_autorunner/integrations/telegram/service.py +214 -40
  73. codex_autorunner/integrations/telegram/state.py +100 -2
  74. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  75. codex_autorunner/integrations/telegram/transport.py +36 -3
  76. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  77. codex_autorunner/manifest.py +2 -0
  78. codex_autorunner/plugin_api.py +22 -0
  79. codex_autorunner/routes/__init__.py +23 -14
  80. codex_autorunner/routes/analytics.py +239 -0
  81. codex_autorunner/routes/base.py +81 -109
  82. codex_autorunner/routes/file_chat.py +836 -0
  83. codex_autorunner/routes/flows.py +980 -0
  84. codex_autorunner/routes/messages.py +459 -0
  85. codex_autorunner/routes/system.py +6 -1
  86. codex_autorunner/routes/usage.py +87 -0
  87. codex_autorunner/routes/workspace.py +271 -0
  88. codex_autorunner/server.py +2 -1
  89. codex_autorunner/static/agentControls.js +1 -0
  90. codex_autorunner/static/agentEvents.js +248 -0
  91. codex_autorunner/static/app.js +25 -22
  92. codex_autorunner/static/autoRefresh.js +29 -1
  93. codex_autorunner/static/bootstrap.js +1 -0
  94. codex_autorunner/static/bus.js +1 -0
  95. codex_autorunner/static/cache.js +1 -0
  96. codex_autorunner/static/constants.js +20 -4
  97. codex_autorunner/static/dashboard.js +162 -196
  98. codex_autorunner/static/diffRenderer.js +37 -0
  99. codex_autorunner/static/docChatCore.js +324 -0
  100. codex_autorunner/static/docChatStorage.js +65 -0
  101. codex_autorunner/static/docChatVoice.js +65 -0
  102. codex_autorunner/static/docEditor.js +133 -0
  103. codex_autorunner/static/env.js +1 -0
  104. codex_autorunner/static/eventSummarizer.js +166 -0
  105. codex_autorunner/static/fileChat.js +182 -0
  106. codex_autorunner/static/health.js +155 -0
  107. codex_autorunner/static/hub.js +41 -118
  108. codex_autorunner/static/index.html +787 -858
  109. codex_autorunner/static/liveUpdates.js +1 -0
  110. codex_autorunner/static/loader.js +1 -0
  111. codex_autorunner/static/messages.js +470 -0
  112. codex_autorunner/static/mobileCompact.js +2 -1
  113. codex_autorunner/static/settings.js +24 -211
  114. codex_autorunner/static/styles.css +7567 -3865
  115. codex_autorunner/static/tabs.js +28 -5
  116. codex_autorunner/static/terminal.js +14 -0
  117. codex_autorunner/static/terminalManager.js +34 -59
  118. codex_autorunner/static/ticketChatActions.js +333 -0
  119. codex_autorunner/static/ticketChatEvents.js +16 -0
  120. codex_autorunner/static/ticketChatStorage.js +16 -0
  121. codex_autorunner/static/ticketChatStream.js +264 -0
  122. codex_autorunner/static/ticketEditor.js +750 -0
  123. codex_autorunner/static/ticketVoice.js +9 -0
  124. codex_autorunner/static/tickets.js +1315 -0
  125. codex_autorunner/static/utils.js +32 -3
  126. codex_autorunner/static/voice.js +1 -0
  127. codex_autorunner/static/workspace.js +672 -0
  128. codex_autorunner/static/workspaceApi.js +53 -0
  129. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  130. codex_autorunner/tickets/__init__.py +20 -0
  131. codex_autorunner/tickets/agent_pool.py +377 -0
  132. codex_autorunner/tickets/files.py +85 -0
  133. codex_autorunner/tickets/frontmatter.py +55 -0
  134. codex_autorunner/tickets/lint.py +102 -0
  135. codex_autorunner/tickets/models.py +95 -0
  136. codex_autorunner/tickets/outbox.py +232 -0
  137. codex_autorunner/tickets/replies.py +179 -0
  138. codex_autorunner/tickets/runner.py +823 -0
  139. codex_autorunner/tickets/spec_ingest.py +77 -0
  140. codex_autorunner/web/app.py +269 -91
  141. codex_autorunner/web/middleware.py +3 -4
  142. codex_autorunner/web/schemas.py +89 -109
  143. codex_autorunner/web/static_assets.py +1 -44
  144. codex_autorunner/workspace/__init__.py +40 -0
  145. codex_autorunner/workspace/paths.py +319 -0
  146. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
  147. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  148. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  149. codex_autorunner/agents/execution/policy.py +0 -292
  150. codex_autorunner/agents/factory.py +0 -52
  151. codex_autorunner/agents/orchestrator.py +0 -358
  152. codex_autorunner/core/doc_chat.py +0 -1446
  153. codex_autorunner/core/snapshot.py +0 -580
  154. codex_autorunner/integrations/github/chatops.py +0 -268
  155. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  156. codex_autorunner/routes/docs.py +0 -381
  157. codex_autorunner/routes/github.py +0 -327
  158. codex_autorunner/routes/runs.py +0 -250
  159. codex_autorunner/spec_ingest.py +0 -812
  160. codex_autorunner/static/docChatActions.js +0 -287
  161. codex_autorunner/static/docChatEvents.js +0 -300
  162. codex_autorunner/static/docChatRender.js +0 -205
  163. codex_autorunner/static/docChatStream.js +0 -361
  164. codex_autorunner/static/docs.js +0 -20
  165. codex_autorunner/static/docsClipboard.js +0 -69
  166. codex_autorunner/static/docsCrud.js +0 -257
  167. codex_autorunner/static/docsDocUpdates.js +0 -62
  168. codex_autorunner/static/docsDrafts.js +0 -16
  169. codex_autorunner/static/docsElements.js +0 -69
  170. codex_autorunner/static/docsInit.js +0 -285
  171. codex_autorunner/static/docsParse.js +0 -160
  172. codex_autorunner/static/docsSnapshot.js +0 -87
  173. codex_autorunner/static/docsSpecIngest.js +0 -263
  174. codex_autorunner/static/docsState.js +0 -127
  175. codex_autorunner/static/docsThreadRegistry.js +0 -44
  176. codex_autorunner/static/docsUi.js +0 -153
  177. codex_autorunner/static/docsVoice.js +0 -56
  178. codex_autorunner/static/github.js +0 -504
  179. codex_autorunner/static/logs.js +0 -678
  180. codex_autorunner/static/review.js +0 -157
  181. codex_autorunner/static/runs.js +0 -418
  182. codex_autorunner/static/snapshot.js +0 -124
  183. codex_autorunner/static/state.js +0 -94
  184. codex_autorunner/static/todoPreview.js +0 -27
  185. codex_autorunner/workspace.py +0 -16
  186. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  187. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  188. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  189. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,239 @@
1
+ """Analytics summary routes.
2
+
3
+ This module aggregates run/ticket/message metadata for the analytics dashboard
4
+ without relying on legacy autorunner endpoints. It intentionally reads from the
5
+ filesystem-backed ticket_flow store and ticket files to keep the UI consistent
6
+ with the rest of the app.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ from fastapi import APIRouter
16
+
17
+ from ..core.flows.models import FlowRunRecord, FlowRunStatus
18
+ from ..core.flows.store import FlowStore
19
+ from ..core.utils import find_repo_root
20
+ from ..tickets.files import list_ticket_paths, read_ticket, ticket_is_done
21
+ from ..tickets.outbox import resolve_outbox_paths
22
+ from ..tickets.replies import resolve_reply_paths
23
+
24
+
25
+ def _flows_db_path(repo_root: Path) -> Path:
26
+ return repo_root / ".codex-autorunner" / "flows.db"
27
+
28
+
29
+ def _load_flow_store(repo_root: Path) -> Optional[FlowStore]:
30
+ db_path = _flows_db_path(repo_root)
31
+ if not db_path.exists():
32
+ return None
33
+ store = FlowStore(db_path)
34
+ try:
35
+ store.initialize()
36
+ except Exception:
37
+ return None
38
+ return store
39
+
40
+
41
+ def _select_primary_run(records: list[FlowRunRecord]) -> Optional[FlowRunRecord]:
42
+ """Select the primary run for analytics display.
43
+
44
+ Only considers the newest run (records[0]). If it's active or paused, return it.
45
+ If the newest run is terminal (completed/stopped/failed), return None to show idle.
46
+ This matches the backend's _active_or_paused_run() logic and prevents showing
47
+ stale data from old paused runs when newer runs have completed.
48
+ """
49
+ if not records:
50
+ return None
51
+ newest = records[0]
52
+ if (
53
+ FlowRunStatus(newest.status).is_active()
54
+ or FlowRunStatus(newest.status).is_paused()
55
+ ):
56
+ return newest
57
+ return None
58
+
59
+
60
+ def _parse_timestamp(value: Optional[str]) -> Optional[datetime]:
61
+ if not value:
62
+ return None
63
+ try:
64
+ if value.endswith("Z"):
65
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
66
+ return datetime.fromisoformat(value)
67
+ except ValueError:
68
+ return None
69
+
70
+
71
+ def _duration_seconds(
72
+ started_at: Optional[str], finished_at: Optional[str], status: str
73
+ ) -> Optional[float]:
74
+ start_dt = _parse_timestamp(started_at)
75
+ if not start_dt:
76
+ return None
77
+ end_dt = _parse_timestamp(finished_at)
78
+ if not end_dt and status in {
79
+ FlowRunStatus.RUNNING.value,
80
+ FlowRunStatus.PAUSED.value,
81
+ FlowRunStatus.PENDING.value,
82
+ }:
83
+ end_dt = datetime.now(timezone.utc)
84
+ if not end_dt:
85
+ return None
86
+ return (end_dt - start_dt).total_seconds()
87
+
88
+
89
+ def _ticket_counts(ticket_dir: Path) -> dict[str, int]:
90
+ total = 0
91
+ done = 0
92
+ for path in list_ticket_paths(ticket_dir):
93
+ total += 1
94
+ try:
95
+ if ticket_is_done(path):
96
+ done += 1
97
+ except Exception:
98
+ # Treat unreadable/invalid tickets as not-done but still count them.
99
+ continue
100
+ todo = max(total - done, 0)
101
+ return {"todo": todo, "done": done, "total": total}
102
+
103
+
104
+ def _count_history_dirs(history_dir: Path) -> int:
105
+ if not history_dir.exists() or not history_dir.is_dir():
106
+ return 0
107
+ count = 0
108
+ try:
109
+ for child in history_dir.iterdir():
110
+ try:
111
+ if child.is_dir() and len(child.name) == 4 and child.name.isdigit():
112
+ count += 1
113
+ except OSError:
114
+ continue
115
+ except OSError:
116
+ return count
117
+ return count
118
+
119
+
120
+ def _build_summary(repo_root: Path) -> Dict[str, Any]:
121
+ ticket_dir = repo_root / ".codex-autorunner" / "tickets"
122
+ store = _load_flow_store(repo_root)
123
+ records: list[FlowRunRecord] = []
124
+ if store:
125
+ try:
126
+ records = store.list_flow_runs(flow_type="ticket_flow")
127
+ except Exception:
128
+ records = []
129
+ finally:
130
+ try:
131
+ store.close()
132
+ except Exception:
133
+ pass
134
+
135
+ run_record = _select_primary_run(records)
136
+
137
+ default_run = {
138
+ "id": None,
139
+ "short_id": None,
140
+ "status": "idle",
141
+ "started_at": None,
142
+ "finished_at": None,
143
+ "duration_seconds": None,
144
+ "current_step": None,
145
+ "created_at": None,
146
+ }
147
+
148
+ run_data: Dict[str, Any] = default_run
149
+ turns: Dict[str, Optional[int]] = {
150
+ "total": None,
151
+ "current_ticket": None,
152
+ "dispatches": 0,
153
+ "replies": 0,
154
+ }
155
+ current_ticket: Optional[str] = None
156
+ agent_id: Optional[str] = None
157
+
158
+ if run_record:
159
+ run_data = {
160
+ "id": run_record.id,
161
+ "short_id": run_record.id.split("-")[0] if run_record.id else None,
162
+ "status": run_record.status.value,
163
+ "started_at": run_record.started_at,
164
+ "finished_at": run_record.finished_at,
165
+ "duration_seconds": _duration_seconds(
166
+ run_record.started_at, run_record.finished_at, run_record.status.value
167
+ ),
168
+ "current_step": run_record.current_step,
169
+ "created_at": run_record.created_at,
170
+ }
171
+
172
+ state = run_record.state if isinstance(run_record.state, dict) else {}
173
+ ticket_state = state.get("ticket_engine") if isinstance(state, dict) else {}
174
+ if isinstance(ticket_state, dict):
175
+ turns["total"] = ticket_state.get("total_turns") # type: ignore[index]
176
+ turns["current_ticket"] = ticket_state.get("ticket_turns") # type: ignore[index]
177
+ current_ticket = ticket_state.get("current_ticket") # type: ignore[assignment]
178
+ agent_id = ticket_state.get("last_agent_id") # type: ignore[assignment]
179
+
180
+ workspace_value = run_record.input_data.get("workspace_root")
181
+ workspace_root = Path(workspace_value) if workspace_value else repo_root
182
+ runs_dir = Path(
183
+ run_record.input_data.get("runs_dir") or ".codex-autorunner/runs"
184
+ )
185
+ outbox_paths = resolve_outbox_paths(
186
+ workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_record.id
187
+ )
188
+ reply_paths = resolve_reply_paths(
189
+ workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_record.id
190
+ )
191
+ turns["dispatches"] = _count_history_dirs(outbox_paths.dispatch_history_dir)
192
+ turns["replies"] = _count_history_dirs(reply_paths.reply_history_dir)
193
+
194
+ # If current ticket is known, read its frontmatter to pick agent id when available.
195
+ if current_ticket:
196
+ current_path = (workspace_root / current_ticket).resolve()
197
+ try:
198
+ doc, _errors = read_ticket(current_path)
199
+ if doc and doc.frontmatter and getattr(doc.frontmatter, "agent", None):
200
+ agent_id = doc.frontmatter.agent
201
+ except Exception:
202
+ pass
203
+
204
+ ticket_counts = _ticket_counts(ticket_dir)
205
+
206
+ return {
207
+ "run": run_data,
208
+ "tickets": {
209
+ "todo_count": ticket_counts["todo"],
210
+ "done_count": ticket_counts["done"],
211
+ "total_count": ticket_counts["total"],
212
+ "current_ticket": current_ticket,
213
+ },
214
+ "turns": {
215
+ "total": turns.get("total"),
216
+ "current_ticket": turns.get("current_ticket"),
217
+ "dispatches": turns.get("dispatches"),
218
+ "replies": turns.get("replies"),
219
+ },
220
+ "agent": {
221
+ "id": agent_id,
222
+ "model": None,
223
+ },
224
+ }
225
+
226
+
227
+ def build_analytics_routes() -> APIRouter:
228
+ router = APIRouter(prefix="/api/analytics", tags=["analytics"])
229
+
230
+ @router.get("/summary")
231
+ def get_analytics_summary():
232
+ repo_root = find_repo_root()
233
+ data = _build_summary(repo_root)
234
+ return data
235
+
236
+ return router
237
+
238
+
239
+ __all__ = ["build_analytics_routes"]
@@ -1,5 +1,5 @@
1
1
  """
2
- Base routes: Index, state streaming, WebSocket terminal, and logs.
2
+ Base routes: Index, WebSocket terminal.
3
3
  """
4
4
 
5
5
  import asyncio
@@ -14,142 +14,94 @@ from fastapi import APIRouter, HTTPException, Request, WebSocket, WebSocketDisco
14
14
  from fastapi.responses import (
15
15
  HTMLResponse,
16
16
  JSONResponse,
17
- PlainTextResponse,
18
- StreamingResponse,
19
17
  )
20
18
 
21
- from ..codex_cli import extract_flag_value
19
+ from ..core.config import HubConfig
22
20
  from ..core.logging_utils import safe_log
23
- from ..core.state import SessionRecord, load_state, now_iso, persist_session_registry
21
+ from ..core.state import SessionRecord, now_iso, persist_session_registry
24
22
  from ..web.pty_session import REPLAY_END, ActiveSession, PTYSession
25
- from ..web.schemas import StateResponse, VersionResponse
23
+ from ..web.schemas import VersionResponse
26
24
  from ..web.static_assets import index_response_headers, render_index_html
27
25
  from ..web.static_refresh import refresh_static_assets
28
26
  from .shared import (
29
- SSE_HEADERS,
30
27
  build_codex_terminal_cmd,
31
28
  build_opencode_terminal_cmd,
32
- log_stream,
33
- resolve_lock_payload,
34
- resolve_runner_status,
35
- state_stream,
36
29
  )
37
30
 
38
31
  ALT_SCREEN_ENTER = b"\x1b[?1049h"
39
32
 
40
33
 
34
+ def _serve_index(request: Request, static_dir: Path):
35
+ active_static = getattr(request.app.state, "static_dir", static_dir)
36
+ index_path = active_static / "index.html"
37
+ if not index_path.exists():
38
+ if refresh_static_assets(request.app):
39
+ active_static = request.app.state.static_dir
40
+ index_path = active_static / "index.html"
41
+ if not index_path.exists():
42
+ raise HTTPException(
43
+ status_code=500, detail="Static UI assets missing; reinstall package"
44
+ )
45
+ html = render_index_html(active_static, request.app.state.asset_version)
46
+ return HTMLResponse(html, headers=index_response_headers())
47
+
48
+
41
49
  def build_base_routes(static_dir: Path) -> APIRouter:
42
50
  """Build routes for index, state, logs, and terminal WebSocket."""
43
51
  router = APIRouter()
44
52
 
45
53
  @router.get("/", include_in_schema=False)
46
54
  def index(request: Request):
47
- active_static = getattr(request.app.state, "static_dir", static_dir)
48
- index_path = active_static / "index.html"
49
- if not index_path.exists():
50
- if refresh_static_assets(request.app):
51
- active_static = request.app.state.static_dir
52
- index_path = active_static / "index.html"
53
- if not index_path.exists():
54
- raise HTTPException(
55
- status_code=500, detail="Static UI assets missing; reinstall package"
56
- )
57
- html = render_index_html(active_static, request.app.state.asset_version)
58
- return HTMLResponse(html, headers=index_response_headers())
59
-
60
- @router.get("/api/state", response_model=StateResponse)
61
- def get_state(request: Request):
62
- engine = request.app.state.engine
63
- config = request.app.state.config
64
- state = load_state(engine.state_path)
65
- outstanding, done = engine.docs.todos()
66
- status, runner_pid, running = resolve_runner_status(engine, state)
67
- lock_payload = resolve_lock_payload(engine)
68
- codex_model = config.codex_model or extract_flag_value(
69
- config.codex_args, "--model"
70
- )
71
- return {
72
- "last_run_id": state.last_run_id,
73
- "status": status,
74
- "last_exit_code": state.last_exit_code,
75
- "last_run_started_at": state.last_run_started_at,
76
- "last_run_finished_at": state.last_run_finished_at,
77
- "outstanding_count": len(outstanding),
78
- "done_count": len(done),
79
- "running": running,
80
- "runner_pid": runner_pid,
81
- **lock_payload,
82
- "terminal_idle_timeout_seconds": config.terminal_idle_timeout_seconds,
83
- "codex_model": codex_model or "auto",
84
- }
55
+ return _serve_index(request, static_dir)
85
56
 
86
57
  @router.get("/api/version", response_model=VersionResponse)
87
58
  def get_version(request: Request):
88
59
  return {"asset_version": request.app.state.asset_version}
89
60
 
90
- @router.get("/api/state/stream")
91
- async def stream_state_endpoint(request: Request):
92
- engine = request.app.state.engine
93
- manager = request.app.state.manager
94
- shutdown_event = getattr(request.app.state, "shutdown_event", None)
95
- return StreamingResponse(
96
- state_stream(
97
- engine,
98
- manager,
99
- logger=request.app.state.logger,
100
- shutdown_event=shutdown_event,
101
- max_seconds=60.0,
102
- ),
103
- media_type="text/event-stream",
104
- headers=SSE_HEADERS,
105
- )
61
+ @router.get("/api/repo/health")
62
+ def repo_health(request: Request):
63
+ config = getattr(request.app.state, "config", None)
64
+ if isinstance(config, HubConfig):
65
+ raise HTTPException(
66
+ status_code=404, detail="Repo health not available in hub mode"
67
+ )
68
+
69
+ engine = getattr(request.app.state, "engine", None)
70
+ repo_root = getattr(engine, "repo_root", None)
71
+ if repo_root is None:
72
+ return JSONResponse(
73
+ {"status": "error", "detail": "Repo context unavailable"},
74
+ status_code=503,
75
+ )
106
76
 
107
- @router.get("/api/logs")
108
- def get_logs(
109
- request: Request,
110
- run_id: Optional[int] = None,
111
- tail: Optional[int] = None,
112
- raw: bool = False,
113
- ):
114
- engine = request.app.state.engine
115
-
116
- def _build_response(
117
- text: str, *, run_id: Optional[int] = None, tail: Optional[int] = None
118
- ):
119
- if raw:
120
- return PlainTextResponse(text or "")
121
- payload = {"log": text or ""}
122
- if run_id is not None:
123
- payload["run_id"] = run_id
124
- if tail is not None:
125
- payload["tail"] = tail
126
- return JSONResponse(payload)
127
-
128
- if run_id is not None:
129
- block = engine.read_run_block(run_id)
130
- if not block:
131
- raise HTTPException(status_code=404, detail="run not found")
132
- return _build_response(block, run_id=run_id)
133
- if tail is not None:
134
- return _build_response(engine.tail_log(tail), tail=tail)
135
- state = load_state(engine.state_path)
136
- if state.last_run_id is None:
137
- return _build_response("")
138
- block = engine.read_run_block(state.last_run_id) or ""
139
- return _build_response(block, run_id=state.last_run_id)
140
-
141
- @router.get("/api/logs/stream")
142
- async def stream_logs_endpoint(request: Request):
143
- engine = request.app.state.engine
144
- shutdown_event = getattr(request.app.state, "shutdown_event", None)
145
- return StreamingResponse(
146
- log_stream(
147
- engine.log_path, shutdown_event=shutdown_event, max_seconds=60.0
148
- ),
149
- media_type="text/event-stream",
150
- headers=SSE_HEADERS,
77
+ flows_db = repo_root / ".codex-autorunner" / "flows.db"
78
+
79
+ docs_dir = repo_root / ".codex-autorunner"
80
+ docs_status = "ok" if docs_dir.exists() else "missing"
81
+
82
+ tickets_dir = repo_root / ".codex-autorunner" / "tickets"
83
+ tickets_status = "ok" if tickets_dir.exists() else "missing"
84
+
85
+ flows_status = "ok" if tickets_dir.exists() else "missing"
86
+ flows_detail = None
87
+
88
+ overall_status = (
89
+ "ok" if docs_status == "ok" and tickets_status == "ok" else "degraded"
151
90
  )
152
91
 
92
+ return {
93
+ "status": overall_status,
94
+ "mode": "repo",
95
+ "repo_root": str(repo_root),
96
+ "flows": {
97
+ "status": flows_status,
98
+ "path": str(flows_db),
99
+ "detail": flows_detail,
100
+ },
101
+ "docs": {"status": docs_status, "path": str(docs_dir)},
102
+ "tickets": {"status": tickets_status, "path": str(tickets_dir)},
103
+ }
104
+
153
105
  @router.websocket("/api/terminal")
154
106
  async def terminal(ws: WebSocket):
155
107
  selected_protocol = None
@@ -623,3 +575,23 @@ def build_base_routes(static_dir: Path) -> APIRouter:
623
575
  active_websockets.discard(ws)
624
576
 
625
577
  return router
578
+
579
+
580
+ def build_frontend_routes(static_dir: Path) -> APIRouter:
581
+ """Build catch-all routes for frontend tabs."""
582
+ router = APIRouter()
583
+
584
+ @router.get("/{tab}", include_in_schema=False)
585
+ def tab_route(tab: str, request: Request):
586
+ if tab in {
587
+ "workspace",
588
+ "tickets",
589
+ "messages",
590
+ "analytics",
591
+ "terminal",
592
+ "settings",
593
+ }:
594
+ return _serve_index(request, static_dir)
595
+ raise HTTPException(status_code=404, detail="Not Found")
596
+
597
+ return router