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,271 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import zipfile
5
+ from dataclasses import asdict
6
+
7
+ from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
8
+ from fastapi.responses import FileResponse, PlainTextResponse, StreamingResponse
9
+
10
+ from ..core import drafts as draft_utils
11
+ from ..tickets.spec_ingest import (
12
+ SpecIngestTicketsError,
13
+ ingest_workspace_spec_to_tickets,
14
+ )
15
+ from ..web.schemas import (
16
+ SpecIngestTicketsResponse,
17
+ WorkspaceFileListResponse,
18
+ WorkspaceResponse,
19
+ WorkspaceTreeResponse,
20
+ WorkspaceUploadResponse,
21
+ WorkspaceWriteRequest,
22
+ )
23
+ from ..workspace.paths import (
24
+ PINNED_DOC_FILENAMES,
25
+ WORKSPACE_DOC_KINDS,
26
+ list_workspace_files,
27
+ list_workspace_tree,
28
+ normalize_workspace_rel_path,
29
+ read_workspace_doc,
30
+ read_workspace_file,
31
+ sanitize_workspace_filename,
32
+ workspace_dir,
33
+ workspace_doc_path,
34
+ write_workspace_doc,
35
+ write_workspace_file,
36
+ )
37
+
38
+
39
+ def build_workspace_routes() -> APIRouter:
40
+ router = APIRouter(prefix="/api", tags=["workspace"])
41
+
42
+ @router.get("/workspace", response_model=WorkspaceResponse)
43
+ def get_workspace(request: Request):
44
+ repo_root = request.app.state.engine.repo_root
45
+ return {
46
+ "active_context": read_workspace_doc(repo_root, "active_context"),
47
+ "decisions": read_workspace_doc(repo_root, "decisions"),
48
+ "spec": read_workspace_doc(repo_root, "spec"),
49
+ }
50
+
51
+ @router.get("/workspace/file", response_class=PlainTextResponse)
52
+ def read_workspace(request: Request, path: str):
53
+ repo_root = request.app.state.engine.repo_root
54
+ try:
55
+ content = read_workspace_file(repo_root, path)
56
+ except ValueError as exc: # invalid path
57
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
58
+ return PlainTextResponse(content)
59
+
60
+ @router.put("/workspace/file", response_class=PlainTextResponse)
61
+ def write_workspace(request: Request, payload: WorkspaceWriteRequest, path: str):
62
+ repo_root = request.app.state.engine.repo_root
63
+ try:
64
+ # Normalize path the same way workspace helpers do to avoid traversal
65
+ safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
66
+ content = write_workspace_file(repo_root, path, payload.content)
67
+ try:
68
+ rel_repo_path = safe_path.relative_to(repo_root).as_posix()
69
+ draft_utils.invalidate_drafts_for_path(repo_root, rel_repo_path)
70
+ state_key = f"workspace_{rel_posix.replace('/', '_')}"
71
+ draft_utils.remove_draft(repo_root, state_key)
72
+ except Exception:
73
+ pass
74
+ except ValueError as exc:
75
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
76
+ return PlainTextResponse(content)
77
+
78
+ @router.put("/workspace/{kind}", response_model=WorkspaceResponse)
79
+ def put_workspace(kind: str, payload: WorkspaceWriteRequest, request: Request):
80
+ key = (kind or "").strip().lower()
81
+ if key not in WORKSPACE_DOC_KINDS:
82
+ raise HTTPException(status_code=400, detail="invalid workspace doc kind")
83
+ repo_root = request.app.state.engine.repo_root
84
+ write_workspace_doc(repo_root, key, payload.content)
85
+ try:
86
+ rel_path = workspace_doc_path(repo_root, key).relative_to(repo_root)
87
+ draft_utils.invalidate_drafts_for_path(repo_root, rel_path.as_posix())
88
+ state_key = f"workspace_{rel_path.name}"
89
+ draft_utils.remove_draft(repo_root, state_key)
90
+ except Exception:
91
+ # best-effort invalidation; avoid blocking writes
92
+ pass
93
+ return {
94
+ "active_context": read_workspace_doc(repo_root, "active_context"),
95
+ "decisions": read_workspace_doc(repo_root, "decisions"),
96
+ "spec": read_workspace_doc(repo_root, "spec"),
97
+ }
98
+
99
+ @router.get("/workspace/files", response_model=WorkspaceFileListResponse)
100
+ def list_files(request: Request):
101
+ repo_root = request.app.state.engine.repo_root
102
+ files = [asdict(item) for item in list_workspace_files(repo_root)]
103
+ return {"files": files}
104
+
105
+ @router.get("/workspace/tree", response_model=WorkspaceTreeResponse)
106
+ def get_workspace_tree(request: Request):
107
+ repo_root = request.app.state.engine.repo_root
108
+ tree = [asdict(item) for item in list_workspace_tree(repo_root)]
109
+ return {"tree": tree}
110
+
111
+ @router.post("/workspace/upload", response_model=WorkspaceUploadResponse)
112
+ async def upload_workspace_files(
113
+ request: Request,
114
+ files: list[UploadFile] = File(...), # noqa: B008
115
+ subdir: str = Form(""),
116
+ ):
117
+ if not files:
118
+ raise HTTPException(status_code=400, detail="no files provided")
119
+
120
+ repo_root = request.app.state.engine.repo_root
121
+ base = workspace_dir(repo_root)
122
+ target_dir = base
123
+ if subdir:
124
+ try:
125
+ target_dir, _ = normalize_workspace_rel_path(repo_root, subdir)
126
+ except ValueError as exc:
127
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
128
+
129
+ target_dir.mkdir(parents=True, exist_ok=True)
130
+
131
+ uploaded: list[dict[str, str | int]] = []
132
+ for upload in files:
133
+ filename = sanitize_workspace_filename(upload.filename or "")
134
+ try:
135
+ data = await upload.read()
136
+ except (
137
+ Exception
138
+ ) as exc: # pragma: no cover - handled by FastAPI for most cases
139
+ raise HTTPException(
140
+ status_code=400, detail="failed to read upload"
141
+ ) from exc
142
+
143
+ dest = target_dir / filename
144
+ dest.write_bytes(
145
+ data
146
+ ) # codeql[py/path-injection] dest sits under normalized workspace dir
147
+ rel_path = dest.relative_to(base).as_posix()
148
+ uploaded.append({"filename": filename, "path": rel_path, "size": len(data)})
149
+
150
+ return {"status": "ok", "uploaded": uploaded}
151
+
152
+ @router.get("/workspace/download")
153
+ async def download_workspace_file(request: Request, path: str):
154
+ repo_root = request.app.state.engine.repo_root
155
+ try:
156
+ safe_path, _ = normalize_workspace_rel_path(repo_root, path)
157
+ except ValueError as exc:
158
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
159
+
160
+ if not safe_path.exists() or safe_path.is_dir():
161
+ raise HTTPException(status_code=404, detail="file not found")
162
+
163
+ return FileResponse(
164
+ path=safe_path, filename=safe_path.name
165
+ ) # codeql[py/path-injection] safe_path validated by normalize_workspace_rel_path
166
+
167
+ @router.get("/workspace/download-zip")
168
+ async def download_workspace_zip(request: Request, path: str = ""):
169
+ repo_root = request.app.state.engine.repo_root
170
+ base = workspace_dir(repo_root)
171
+ base.mkdir(parents=True, exist_ok=True)
172
+
173
+ target_dir = base
174
+ zip_name = "workspace.zip"
175
+ if path:
176
+ try:
177
+ target_dir, _ = normalize_workspace_rel_path(repo_root, path)
178
+ except ValueError as exc:
179
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
180
+ if not target_dir.exists() or not target_dir.is_dir():
181
+ raise HTTPException(status_code=404, detail="folder not found")
182
+ zip_name = f"{target_dir.name}.zip"
183
+
184
+ buffer = io.BytesIO()
185
+ base_real = base.resolve()
186
+ with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
187
+ for file_path in target_dir.rglob("*"):
188
+ if file_path.is_dir():
189
+ continue
190
+ if file_path.is_symlink():
191
+ try:
192
+ file_path.resolve().relative_to(base_real)
193
+ except Exception:
194
+ continue
195
+ arc_name = file_path.relative_to(target_dir).as_posix()
196
+ zf.write(
197
+ file_path, arc_name
198
+ ) # codeql[py/path-injection] file_path constrained to workspace dir
199
+
200
+ buffer.seek(0)
201
+ return StreamingResponse(
202
+ buffer,
203
+ media_type="application/zip",
204
+ headers={"Content-Disposition": f'attachment; filename="{zip_name}"'},
205
+ )
206
+
207
+ @router.post("/workspace/folder")
208
+ async def create_workspace_folder(request: Request, path: str):
209
+ repo_root = request.app.state.engine.repo_root
210
+ try:
211
+ safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
212
+ except ValueError as exc:
213
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
214
+
215
+ if safe_path.exists():
216
+ raise HTTPException(status_code=400, detail="path already exists")
217
+
218
+ safe_path.mkdir(parents=True, exist_ok=True)
219
+ return {"status": "created", "path": rel_posix}
220
+
221
+ @router.delete("/workspace/folder")
222
+ async def delete_workspace_folder(request: Request, path: str):
223
+ repo_root = request.app.state.engine.repo_root
224
+ try:
225
+ safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
226
+ except ValueError as exc:
227
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
228
+
229
+ if not safe_path.exists():
230
+ raise HTTPException(status_code=404, detail="folder not found")
231
+ if not safe_path.is_dir():
232
+ raise HTTPException(status_code=400, detail="not a folder")
233
+ if any(safe_path.iterdir()):
234
+ raise HTTPException(status_code=400, detail="folder not empty")
235
+
236
+ safe_path.rmdir()
237
+ return {"status": "deleted", "path": rel_posix}
238
+
239
+ @router.delete("/workspace/file")
240
+ async def delete_workspace_file(request: Request, path: str):
241
+ repo_root = request.app.state.engine.repo_root
242
+ base = workspace_dir(repo_root)
243
+ try:
244
+ safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
245
+ except ValueError as exc:
246
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
247
+
248
+ if safe_path.parent == base and safe_path.name in PINNED_DOC_FILENAMES:
249
+ raise HTTPException(status_code=400, detail="cannot delete pinned docs")
250
+ if not safe_path.exists():
251
+ raise HTTPException(status_code=404, detail="file not found")
252
+ if safe_path.is_dir():
253
+ raise HTTPException(status_code=400, detail="use folder delete endpoint")
254
+
255
+ safe_path.unlink() # codeql[py/path-injection] safe_path validated by normalize_workspace_rel_path
256
+ return {"status": "deleted", "path": rel_posix}
257
+
258
+ @router.post("/workspace/spec/ingest", response_model=SpecIngestTicketsResponse)
259
+ def ingest_workspace_spec(request: Request):
260
+ repo_root = request.app.state.engine.repo_root
261
+ try:
262
+ result = ingest_workspace_spec_to_tickets(repo_root)
263
+ except SpecIngestTicketsError as exc:
264
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
265
+ return {
266
+ "status": "ok",
267
+ "created": result.created,
268
+ "first_ticket_path": result.first_ticket_path,
269
+ }
270
+
271
+ return router
@@ -1,7 +1,7 @@
1
1
  from importlib import resources
2
2
 
3
3
  from .core.engine import Engine, LockError, clear_stale_lock, doctor
4
- from .web.app import create_app, create_hub_app
4
+ from .web.app import create_app, create_hub_app, create_repo_app
5
5
  from .web.middleware import BasePathRouterMiddleware
6
6
 
7
7
  __all__ = [
@@ -11,6 +11,7 @@ __all__ = [
11
11
  "clear_stale_lock",
12
12
  "create_app",
13
13
  "create_hub_app",
14
+ "create_repo_app",
14
15
  "doctor",
15
16
  "resources",
16
17
  ]
@@ -1,3 +1,4 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
1
2
  import { api, flash } from "./utils.js";
2
3
  const STORAGE_KEYS = {
3
4
  selected: "car.agent.selected",
@@ -0,0 +1,248 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ /**
3
+ * Shared parsing helpers for agent (app-server) events.
4
+ * Used by ticket chat and live agent output to render rich activity.
5
+ */
6
+ function extractCommand(item, params) {
7
+ const command = item?.command ?? params?.command;
8
+ if (Array.isArray(command)) {
9
+ return command
10
+ .map((part) => String(part))
11
+ .join(" ")
12
+ .trim();
13
+ }
14
+ if (typeof command === "string")
15
+ return command.trim();
16
+ return "";
17
+ }
18
+ function extractFiles(payload) {
19
+ const files = [];
20
+ const addEntry = (entry) => {
21
+ if (typeof entry === "string" && entry.trim()) {
22
+ files.push(entry.trim());
23
+ return;
24
+ }
25
+ if (entry && typeof entry === "object") {
26
+ const entryObj = entry;
27
+ const path = entryObj.path || entryObj.file || entryObj.name;
28
+ if (typeof path === "string" && path.trim()) {
29
+ files.push(path.trim());
30
+ }
31
+ }
32
+ };
33
+ if (!payload || typeof payload !== "object")
34
+ return files;
35
+ for (const key of ["files", "fileChanges", "paths"]) {
36
+ const value = payload[key];
37
+ if (Array.isArray(value)) {
38
+ value.forEach(addEntry);
39
+ }
40
+ }
41
+ for (const key of ["path", "file", "name"]) {
42
+ addEntry(payload[key]);
43
+ }
44
+ return files;
45
+ }
46
+ function extractErrorMessage(params) {
47
+ if (!params || typeof params !== "object")
48
+ return "";
49
+ const err = params.error;
50
+ if (err && typeof err === "object") {
51
+ const errObj = err;
52
+ const message = typeof errObj.message === "string" ? errObj.message : "";
53
+ const details = typeof errObj.additionalDetails === "string"
54
+ ? errObj.additionalDetails
55
+ : typeof errObj.details === "string"
56
+ ? errObj.details
57
+ : "";
58
+ if (message && details && message !== details) {
59
+ return `${message} (${details})`;
60
+ }
61
+ return message || details;
62
+ }
63
+ if (typeof err === "string")
64
+ return err;
65
+ if (typeof params.message === "string")
66
+ return params.message;
67
+ return "";
68
+ }
69
+ function hasMeaningfulText(summary, detail) {
70
+ return Boolean(summary.trim() || detail.trim());
71
+ }
72
+ function inferSignificance(kind, method) {
73
+ if (kind === "thinking")
74
+ return true;
75
+ if (kind === "error")
76
+ return true;
77
+ if (["tool", "command", "file", "output"].includes(kind))
78
+ return true;
79
+ if (method.includes("requestApproval"))
80
+ return true;
81
+ return false;
82
+ }
83
+ /**
84
+ * Extract output delta text from an event payload.
85
+ */
86
+ export function extractOutputDelta(payload) {
87
+ const message = payload && typeof payload === "object" ? payload.message || payload : payload;
88
+ if (!message || typeof message !== "object")
89
+ return "";
90
+ const method = String(message.method || "").toLowerCase();
91
+ if (!method.includes("outputdelta"))
92
+ return "";
93
+ const params = message.params || {};
94
+ if (typeof params.delta === "string")
95
+ return params.delta;
96
+ if (typeof params.text === "string")
97
+ return params.text;
98
+ if (typeof params.output === "string")
99
+ return params.output;
100
+ return "";
101
+ }
102
+ /**
103
+ * Parse an app-server event payload into a normalized AgentEvent plus merge hints.
104
+ */
105
+ export function parseAppServerEvent(payload) {
106
+ const message = payload && typeof payload === "object" ? payload.message || payload : payload;
107
+ if (!message || typeof message !== "object")
108
+ return null;
109
+ const messageObj = message;
110
+ const method = messageObj.method || "app-server";
111
+ const params = messageObj.params || {};
112
+ const item = params.item || {};
113
+ const itemId = params.itemId || item.id || item.itemId || null;
114
+ const receivedAt = payload && typeof payload === "object"
115
+ ? payload.received_at || payload.receivedAt || Date.now()
116
+ : Date.now();
117
+ // Handle reasoning/thinking deltas - accumulate into existing event
118
+ if (method === "item/reasoning/summaryTextDelta") {
119
+ const delta = params.delta || "";
120
+ if (!delta)
121
+ return null;
122
+ const event = {
123
+ id: payload?.id || `${Date.now()}`,
124
+ title: "Thinking",
125
+ summary: delta,
126
+ detail: "",
127
+ kind: "thinking",
128
+ isSignificant: true,
129
+ time: receivedAt,
130
+ itemId,
131
+ method,
132
+ };
133
+ return { event, mergeStrategy: "append" };
134
+ }
135
+ // Handle reasoning part added (paragraph break)
136
+ if (method === "item/reasoning/summaryPartAdded") {
137
+ const event = {
138
+ id: payload?.id || `${Date.now()}`,
139
+ title: "Thinking",
140
+ summary: "",
141
+ detail: "",
142
+ kind: "thinking",
143
+ isSignificant: true,
144
+ time: receivedAt,
145
+ itemId,
146
+ method,
147
+ };
148
+ return { event, mergeStrategy: "newline" };
149
+ }
150
+ let title = method;
151
+ let summary = "";
152
+ let detail = "";
153
+ let kind = "event";
154
+ // Handle generic status updates
155
+ if (method === "status" || params.status) {
156
+ title = "Status";
157
+ summary = params.status || "Processing";
158
+ kind = "status";
159
+ }
160
+ else if (method === "item/completed") {
161
+ const itemType = item.type;
162
+ if (itemType === "commandExecution") {
163
+ title = "Command";
164
+ summary = extractCommand(item, params);
165
+ kind = "command";
166
+ if (item.exitCode !== undefined && item.exitCode !== null) {
167
+ detail = `exit ${item.exitCode}`;
168
+ }
169
+ }
170
+ else if (itemType === "fileChange") {
171
+ title = "File change";
172
+ const files = extractFiles(item);
173
+ summary = files.join(", ") || "Updated files";
174
+ kind = "file";
175
+ }
176
+ else if (itemType === "tool") {
177
+ title = "Tool";
178
+ summary =
179
+ item.name ||
180
+ item.tool ||
181
+ item.id ||
182
+ "Tool call";
183
+ kind = "tool";
184
+ }
185
+ else if (itemType === "agentMessage") {
186
+ title = "Agent";
187
+ summary = item.text || "Agent message";
188
+ kind = "output";
189
+ }
190
+ else {
191
+ title = itemType ? `Item ${itemType}` : "Item completed";
192
+ summary = item.text || item.message || "";
193
+ }
194
+ }
195
+ else if (method === "item/commandExecution/requestApproval") {
196
+ title = "Command approval";
197
+ summary = extractCommand(item, params) || "Approval requested";
198
+ kind = "command";
199
+ }
200
+ else if (method === "item/fileChange/requestApproval") {
201
+ title = "File approval";
202
+ const files = extractFiles(params);
203
+ summary = files.join(", ") || "Approval requested";
204
+ kind = "file";
205
+ }
206
+ else if (method === "turn/completed") {
207
+ title = "Turn completed";
208
+ summary = params.status || "completed";
209
+ kind = "status";
210
+ }
211
+ else if (method === "error") {
212
+ title = "Error";
213
+ summary = extractErrorMessage(params) || "App-server error";
214
+ kind = "error";
215
+ }
216
+ else if (method.includes("outputDelta")) {
217
+ title = "Output";
218
+ summary = params.delta || params.text || "";
219
+ kind = "output";
220
+ }
221
+ else if (params.delta) {
222
+ title = "Delta";
223
+ summary = params.delta;
224
+ }
225
+ const summaryText = typeof summary === "string" ? summary : String(summary ?? "");
226
+ const detailText = typeof detail === "string" ? detail : String(detail ?? "");
227
+ const meaningful = hasMeaningfulText(summaryText, detailText);
228
+ const isStarted = method.includes("item/started");
229
+ if (!meaningful && isStarted) {
230
+ return null;
231
+ }
232
+ if (!meaningful) {
233
+ return null;
234
+ }
235
+ const isSignificant = inferSignificance(kind, method);
236
+ const event = {
237
+ id: payload?.id || `${Date.now()}`,
238
+ title,
239
+ summary: summaryText,
240
+ detail: detailText,
241
+ kind,
242
+ isSignificant,
243
+ time: receivedAt,
244
+ itemId,
245
+ method,
246
+ };
247
+ return { event };
248
+ }
@@ -1,19 +1,20 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
1
2
  import { REPO_ID, HUB_BASE } from "./env.js";
2
3
  import { initHub } from "./hub.js";
3
4
  import { initTabs, registerTab } from "./tabs.js";
4
- import { initDashboard } from "./dashboard.js";
5
- import { initDocs } from "./docs.js";
6
- import { initLogs } from "./logs.js";
7
5
  import { initTerminal } from "./terminal.js";
8
- import { initRuns } from "./runs.js";
9
- import { loadState } from "./state.js";
10
- import { initGitHub } from "./github.js";
6
+ import { initTicketFlow } from "./tickets.js";
7
+ import { initMessages, initMessageBell } from "./messages.js";
11
8
  import { initMobileCompact } from "./mobileCompact.js";
12
9
  import { subscribe } from "./bus.js";
13
10
  import { initRepoSettingsPanel } from "./settings.js";
14
11
  import { flash } from "./utils.js";
15
12
  import { initLiveUpdates } from "./liveUpdates.js";
16
- function initRepoShell() {
13
+ import { initHealthGate } from "./health.js";
14
+ import { initWorkspace } from "./workspace.js";
15
+ import { initDashboard } from "./dashboard.js";
16
+ async function initRepoShell() {
17
+ await initHealthGate();
17
18
  if (REPO_ID) {
18
19
  const navBar = document.querySelector(".nav-bar");
19
20
  if (navBar) {
@@ -32,23 +33,27 @@ function initRepoShell() {
32
33
  brand.insertAdjacentElement("afterend", repoName);
33
34
  }
34
35
  }
35
- registerTab("dashboard", "Dashboard");
36
- registerTab("docs", "Docs");
37
- registerTab("runs", "Runs");
38
- registerTab("logs", "Logs");
36
+ const defaultTab = REPO_ID ? "tickets" : "analytics";
37
+ registerTab("tickets", "Tickets");
38
+ registerTab("inbox", "Inbox");
39
+ registerTab("analytics", "Analytics");
40
+ registerTab("workspace", "Workspace");
39
41
  registerTab("terminal", "Terminal");
40
42
  const initializedTabs = new Set();
41
43
  const lazyInit = (tabId) => {
42
44
  if (initializedTabs.has(tabId))
43
45
  return;
44
- if (tabId === "docs") {
45
- initDocs();
46
+ if (tabId === "workspace") {
47
+ initWorkspace();
48
+ }
49
+ else if (tabId === "inbox" || tabId === "messages") {
50
+ initMessages();
46
51
  }
47
- else if (tabId === "logs") {
48
- initLogs();
52
+ else if (tabId === "analytics") {
53
+ initDashboard();
49
54
  }
50
- else if (tabId === "runs") {
51
- initRuns();
55
+ else if (tabId === "tickets") {
56
+ initTicketFlow();
52
57
  }
53
58
  initializedTabs.add(tabId);
54
59
  };
@@ -58,7 +63,7 @@ function initRepoShell() {
58
63
  }
59
64
  lazyInit(tabId);
60
65
  });
61
- initTabs();
66
+ initTabs(defaultTab);
62
67
  const activePanel = document.querySelector(".panel.active");
63
68
  if (activePanel?.id) {
64
69
  lazyInit(activePanel.id);
@@ -67,12 +72,10 @@ function initRepoShell() {
67
72
  terminalPanel?.addEventListener("pointerdown", () => {
68
73
  lazyInit("terminal");
69
74
  }, { once: true });
70
- initDashboard();
75
+ initMessageBell();
71
76
  initLiveUpdates();
72
77
  initRepoSettingsPanel();
73
- initGitHub();
74
78
  initMobileCompact();
75
- loadState();
76
79
  const repoShell = document.getElementById("repo-shell");
77
80
  if (repoShell?.hasAttribute("inert")) {
78
81
  const openModals = document.querySelectorAll(".modal-overlay:not([hidden])");
@@ -97,6 +100,6 @@ function bootstrap() {
97
100
  repoShell.classList.remove("hidden");
98
101
  if (hubShell)
99
102
  hubShell.classList.add("hidden");
100
- initRepoShell();
103
+ void initRepoShell();
101
104
  }
102
105
  bootstrap();