codex-autorunner 1.1.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/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +114 -1
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +236 -1
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- 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/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- 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 +5 -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/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- 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 +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +26 -1
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/chatUploads.js +137 -0
- 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 +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9125 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/file_chat.py +317 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +70 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from ..bootstrap import ensure_pma_docs
|
|
9
|
+
from ..tickets.files import list_ticket_paths, safe_relpath, ticket_is_done
|
|
10
|
+
from ..tickets.outbox import parse_dispatch, resolve_outbox_paths
|
|
11
|
+
from .config import load_hub_config, load_repo_config
|
|
12
|
+
from .flows.models import FlowRunStatus
|
|
13
|
+
from .flows.store import FlowStore
|
|
14
|
+
from .hub import HubSupervisor
|
|
15
|
+
from .state_roots import resolve_hub_templates_root
|
|
16
|
+
|
|
17
|
+
PMA_MAX_REPOS = 25
|
|
18
|
+
PMA_MAX_MESSAGES = 10
|
|
19
|
+
PMA_MAX_TEXT = 800
|
|
20
|
+
PMA_MAX_TEMPLATE_REPOS = 25
|
|
21
|
+
PMA_MAX_TEMPLATE_FIELD_CHARS = 120
|
|
22
|
+
|
|
23
|
+
# Defaults used when hub config is not available (should be rare).
|
|
24
|
+
PMA_DOCS_MAX_CHARS = 12_000
|
|
25
|
+
PMA_ACTIVE_CONTEXT_MAX_LINES = 200
|
|
26
|
+
PMA_CONTEXT_LOG_TAIL_LINES = 120
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _tail_lines(text: str, max_lines: int) -> str:
|
|
30
|
+
if max_lines <= 0:
|
|
31
|
+
return ""
|
|
32
|
+
lines = (text or "").splitlines()
|
|
33
|
+
if len(lines) <= max_lines:
|
|
34
|
+
return "\n".join(lines)
|
|
35
|
+
return "\n".join(lines[-max_lines:])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_pma_workspace_docs(hub_root: Path) -> dict[str, Any]:
|
|
39
|
+
"""Load hub-level PMA workspace docs for prompt injection.
|
|
40
|
+
|
|
41
|
+
These docs act as durable memory and working context for PMA.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
ensure_pma_docs(hub_root)
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
docs_max_chars = PMA_DOCS_MAX_CHARS
|
|
49
|
+
active_context_max_lines = PMA_ACTIVE_CONTEXT_MAX_LINES
|
|
50
|
+
context_log_tail_lines = PMA_CONTEXT_LOG_TAIL_LINES
|
|
51
|
+
try:
|
|
52
|
+
hub_config = load_hub_config(hub_root)
|
|
53
|
+
pma_cfg = getattr(hub_config, "pma", None)
|
|
54
|
+
if pma_cfg is not None:
|
|
55
|
+
docs_max_chars = int(getattr(pma_cfg, "docs_max_chars", docs_max_chars))
|
|
56
|
+
active_context_max_lines = int(
|
|
57
|
+
getattr(pma_cfg, "active_context_max_lines", active_context_max_lines)
|
|
58
|
+
)
|
|
59
|
+
context_log_tail_lines = int(
|
|
60
|
+
getattr(pma_cfg, "context_log_tail_lines", context_log_tail_lines)
|
|
61
|
+
)
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
pma_dir = hub_root / ".codex-autorunner" / "pma"
|
|
66
|
+
agents_path = pma_dir / "AGENTS.md"
|
|
67
|
+
active_context_path = pma_dir / "active_context.md"
|
|
68
|
+
context_log_path = pma_dir / "context_log.md"
|
|
69
|
+
|
|
70
|
+
def _read(path: Path) -> str:
|
|
71
|
+
try:
|
|
72
|
+
return path.read_text(encoding="utf-8")
|
|
73
|
+
except Exception:
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
agents = _truncate(_read(agents_path), docs_max_chars)
|
|
77
|
+
active_context = _read(active_context_path)
|
|
78
|
+
active_context_lines = len((active_context or "").splitlines())
|
|
79
|
+
active_context = _truncate(active_context, docs_max_chars)
|
|
80
|
+
context_log_tail = _tail_lines(_read(context_log_path), context_log_tail_lines)
|
|
81
|
+
context_log_tail = _truncate(context_log_tail, docs_max_chars)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"agents": agents,
|
|
85
|
+
"active_context": active_context,
|
|
86
|
+
"active_context_line_count": active_context_lines,
|
|
87
|
+
"active_context_max_lines": active_context_max_lines,
|
|
88
|
+
"context_log_tail": context_log_tail,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _truncate(text: Optional[str], limit: int) -> str:
|
|
93
|
+
raw = text or ""
|
|
94
|
+
if len(raw) <= limit:
|
|
95
|
+
return raw
|
|
96
|
+
return raw[: max(0, limit - 3)] + "..."
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _trim_extra(extra: Any, limit: int) -> Any:
|
|
100
|
+
if extra is None:
|
|
101
|
+
return None
|
|
102
|
+
if isinstance(extra, str):
|
|
103
|
+
return _truncate(extra, limit)
|
|
104
|
+
try:
|
|
105
|
+
raw = json.dumps(extra, ensure_ascii=True, sort_keys=True, default=str)
|
|
106
|
+
except Exception:
|
|
107
|
+
raw = str(extra)
|
|
108
|
+
if len(raw) <= limit:
|
|
109
|
+
return extra
|
|
110
|
+
return {
|
|
111
|
+
"_omitted": True,
|
|
112
|
+
"note": "extra omitted due to size",
|
|
113
|
+
"preview": _truncate(raw, limit),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _load_template_scan_summary(
|
|
118
|
+
hub_root: Optional[Path],
|
|
119
|
+
*,
|
|
120
|
+
max_field_chars: int = PMA_MAX_TEMPLATE_FIELD_CHARS,
|
|
121
|
+
) -> Optional[dict[str, Any]]:
|
|
122
|
+
if hub_root is None:
|
|
123
|
+
return None
|
|
124
|
+
try:
|
|
125
|
+
scans_root = resolve_hub_templates_root(hub_root) / "scans"
|
|
126
|
+
if not scans_root.exists():
|
|
127
|
+
return None
|
|
128
|
+
candidates = [
|
|
129
|
+
entry
|
|
130
|
+
for entry in scans_root.iterdir()
|
|
131
|
+
if entry.is_file() and entry.suffix == ".json"
|
|
132
|
+
]
|
|
133
|
+
if not candidates:
|
|
134
|
+
return None
|
|
135
|
+
newest = max(candidates, key=lambda entry: entry.stat().st_mtime)
|
|
136
|
+
payload = json.loads(newest.read_text(encoding="utf-8"))
|
|
137
|
+
if not isinstance(payload, dict):
|
|
138
|
+
return None
|
|
139
|
+
return {
|
|
140
|
+
"repo_id": _truncate(str(payload.get("repo_id", "")), max_field_chars),
|
|
141
|
+
"decision": _truncate(str(payload.get("decision", "")), max_field_chars),
|
|
142
|
+
"severity": _truncate(str(payload.get("severity", "")), max_field_chars),
|
|
143
|
+
"scanned_at": _truncate(
|
|
144
|
+
str(payload.get("scanned_at", "")), max_field_chars
|
|
145
|
+
),
|
|
146
|
+
}
|
|
147
|
+
except Exception:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _build_templates_snapshot(
|
|
152
|
+
supervisor: HubSupervisor,
|
|
153
|
+
*,
|
|
154
|
+
hub_root: Optional[Path] = None,
|
|
155
|
+
max_repos: int = PMA_MAX_TEMPLATE_REPOS,
|
|
156
|
+
max_field_chars: int = PMA_MAX_TEMPLATE_FIELD_CHARS,
|
|
157
|
+
) -> dict[str, Any]:
|
|
158
|
+
hub_config = getattr(supervisor, "hub_config", None)
|
|
159
|
+
templates_cfg = getattr(hub_config, "templates", None)
|
|
160
|
+
if templates_cfg is None:
|
|
161
|
+
return {"enabled": False, "repos": []}
|
|
162
|
+
repos = []
|
|
163
|
+
for repo in templates_cfg.repos[: max(0, max_repos)]:
|
|
164
|
+
repos.append(
|
|
165
|
+
{
|
|
166
|
+
"id": _truncate(repo.id, max_field_chars),
|
|
167
|
+
"default_ref": _truncate(repo.default_ref, max_field_chars),
|
|
168
|
+
"trusted": bool(repo.trusted),
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
payload: dict[str, Any] = {
|
|
172
|
+
"enabled": bool(templates_cfg.enabled),
|
|
173
|
+
"repos": repos,
|
|
174
|
+
}
|
|
175
|
+
scan_summary = _load_template_scan_summary(
|
|
176
|
+
hub_root, max_field_chars=max_field_chars
|
|
177
|
+
)
|
|
178
|
+
if scan_summary:
|
|
179
|
+
payload["last_scan"] = scan_summary
|
|
180
|
+
return payload
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def load_pma_prompt(hub_root: Path) -> str:
|
|
184
|
+
path = hub_root / ".codex-autorunner" / "pma" / "prompt.md"
|
|
185
|
+
try:
|
|
186
|
+
ensure_pma_docs(hub_root)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
try:
|
|
190
|
+
return path.read_text(encoding="utf-8")
|
|
191
|
+
except Exception:
|
|
192
|
+
return ""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def format_pma_prompt(
|
|
196
|
+
base_prompt: str,
|
|
197
|
+
snapshot: dict[str, Any],
|
|
198
|
+
message: str,
|
|
199
|
+
hub_root: Optional[Path] = None,
|
|
200
|
+
) -> str:
|
|
201
|
+
snapshot_text = json.dumps(snapshot, sort_keys=True)
|
|
202
|
+
|
|
203
|
+
pma_docs: Optional[dict[str, Any]] = None
|
|
204
|
+
if hub_root is not None:
|
|
205
|
+
try:
|
|
206
|
+
pma_docs = load_pma_workspace_docs(hub_root)
|
|
207
|
+
except Exception:
|
|
208
|
+
pma_docs = None
|
|
209
|
+
|
|
210
|
+
prompt = f"{base_prompt}\n\n"
|
|
211
|
+
prompt += (
|
|
212
|
+
"Ops guide: `.codex-autorunner/pma/ABOUT_CAR.md`.\n"
|
|
213
|
+
"Durable guidance: `.codex-autorunner/pma/AGENTS.md`.\n"
|
|
214
|
+
"Working context: `.codex-autorunner/pma/active_context.md`.\n"
|
|
215
|
+
"History: `.codex-autorunner/pma/context_log.md`.\n"
|
|
216
|
+
"To send a file to the user, write it to `.codex-autorunner/pma/outbox/`.\n"
|
|
217
|
+
"User uploaded files are in `.codex-autorunner/pma/inbox/`.\n\n"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if pma_docs:
|
|
221
|
+
max_lines = pma_docs.get("active_context_max_lines")
|
|
222
|
+
line_count = pma_docs.get("active_context_line_count")
|
|
223
|
+
prompt += (
|
|
224
|
+
"<pma_workspace_docs>\n"
|
|
225
|
+
"<AGENTS_MD>\n"
|
|
226
|
+
f"{pma_docs.get('agents', '')}\n"
|
|
227
|
+
"</AGENTS_MD>\n"
|
|
228
|
+
"<ACTIVE_CONTEXT_MD>\n"
|
|
229
|
+
f"{pma_docs.get('active_context', '')}\n"
|
|
230
|
+
"</ACTIVE_CONTEXT_MD>\n"
|
|
231
|
+
f"<ACTIVE_CONTEXT_BUDGET lines='{max_lines}' current_lines='{line_count}' />\n"
|
|
232
|
+
"<CONTEXT_LOG_TAIL_MD>\n"
|
|
233
|
+
f"{pma_docs.get('context_log_tail', '')}\n"
|
|
234
|
+
"</CONTEXT_LOG_TAIL_MD>\n"
|
|
235
|
+
"</pma_workspace_docs>\n\n"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
prompt += (
|
|
239
|
+
"<hub_snapshot>\n"
|
|
240
|
+
f"{snapshot_text}\n"
|
|
241
|
+
"</hub_snapshot>\n\n"
|
|
242
|
+
"<user_message>\n"
|
|
243
|
+
f"{message}\n"
|
|
244
|
+
"</user_message>\n"
|
|
245
|
+
)
|
|
246
|
+
return prompt
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _get_ticket_flow_summary(repo_path: Path) -> Optional[dict[str, Any]]:
|
|
250
|
+
db_path = repo_path / ".codex-autorunner" / "flows.db"
|
|
251
|
+
if not db_path.exists():
|
|
252
|
+
return None
|
|
253
|
+
try:
|
|
254
|
+
config = load_repo_config(repo_path)
|
|
255
|
+
with FlowStore(db_path, durable=config.durable_writes) as store:
|
|
256
|
+
runs = store.list_flow_runs(flow_type="ticket_flow")
|
|
257
|
+
if not runs:
|
|
258
|
+
return None
|
|
259
|
+
latest = runs[0]
|
|
260
|
+
|
|
261
|
+
ticket_dir = repo_path / ".codex-autorunner" / "tickets"
|
|
262
|
+
total = 0
|
|
263
|
+
done = 0
|
|
264
|
+
for path in list_ticket_paths(ticket_dir):
|
|
265
|
+
total += 1
|
|
266
|
+
try:
|
|
267
|
+
if ticket_is_done(path):
|
|
268
|
+
done += 1
|
|
269
|
+
except Exception:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
if total == 0:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
state = latest.state if isinstance(latest.state, dict) else {}
|
|
276
|
+
engine = state.get("ticket_engine") if isinstance(state, dict) else {}
|
|
277
|
+
engine = engine if isinstance(engine, dict) else {}
|
|
278
|
+
current_step = engine.get("total_turns")
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"status": latest.status.value,
|
|
282
|
+
"done_count": done,
|
|
283
|
+
"total_count": total,
|
|
284
|
+
"current_step": current_step,
|
|
285
|
+
}
|
|
286
|
+
except Exception:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _latest_dispatch(
|
|
291
|
+
repo_root: Path, run_id: str, input_data: dict, *, max_text_chars: int
|
|
292
|
+
) -> Optional[dict[str, Any]]:
|
|
293
|
+
try:
|
|
294
|
+
workspace_root = Path(input_data.get("workspace_root") or repo_root)
|
|
295
|
+
runs_dir = Path(input_data.get("runs_dir") or ".codex-autorunner/runs")
|
|
296
|
+
outbox_paths = resolve_outbox_paths(
|
|
297
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_id
|
|
298
|
+
)
|
|
299
|
+
history_dir = outbox_paths.dispatch_history_dir
|
|
300
|
+
if not history_dir.exists() or not history_dir.is_dir():
|
|
301
|
+
return None
|
|
302
|
+
seq_dirs: list[Path] = []
|
|
303
|
+
for child in history_dir.iterdir():
|
|
304
|
+
if not child.is_dir():
|
|
305
|
+
continue
|
|
306
|
+
name = child.name
|
|
307
|
+
if len(name) == 4 and name.isdigit():
|
|
308
|
+
seq_dirs.append(child)
|
|
309
|
+
if not seq_dirs:
|
|
310
|
+
return None
|
|
311
|
+
latest_dir = sorted(seq_dirs, key=lambda p: p.name)[-1]
|
|
312
|
+
seq = int(latest_dir.name)
|
|
313
|
+
dispatch_path = latest_dir / "DISPATCH.md"
|
|
314
|
+
dispatch, errors = parse_dispatch(dispatch_path)
|
|
315
|
+
if errors or dispatch is None:
|
|
316
|
+
return {
|
|
317
|
+
"seq": seq,
|
|
318
|
+
"dir": safe_relpath(latest_dir, repo_root),
|
|
319
|
+
"dispatch": None,
|
|
320
|
+
"errors": errors,
|
|
321
|
+
"files": [],
|
|
322
|
+
}
|
|
323
|
+
files: list[str] = []
|
|
324
|
+
for child in sorted(latest_dir.iterdir(), key=lambda p: p.name):
|
|
325
|
+
if child.name.startswith("."):
|
|
326
|
+
continue
|
|
327
|
+
if child.name == "DISPATCH.md":
|
|
328
|
+
continue
|
|
329
|
+
if child.is_file():
|
|
330
|
+
files.append(child.name)
|
|
331
|
+
dispatch_dict = {
|
|
332
|
+
"mode": dispatch.mode,
|
|
333
|
+
"title": _truncate(dispatch.title, max_text_chars),
|
|
334
|
+
"body": _truncate(dispatch.body, max_text_chars),
|
|
335
|
+
"extra": _trim_extra(dispatch.extra, max_text_chars),
|
|
336
|
+
"is_handoff": dispatch.is_handoff,
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
"seq": seq,
|
|
340
|
+
"dir": safe_relpath(latest_dir, repo_root),
|
|
341
|
+
"dispatch": dispatch_dict,
|
|
342
|
+
"errors": [],
|
|
343
|
+
"files": files,
|
|
344
|
+
}
|
|
345
|
+
except Exception:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _gather_inbox(
|
|
350
|
+
supervisor: HubSupervisor, *, max_text_chars: int
|
|
351
|
+
) -> list[dict[str, Any]]:
|
|
352
|
+
messages: list[dict[str, Any]] = []
|
|
353
|
+
try:
|
|
354
|
+
snapshots = supervisor.list_repos()
|
|
355
|
+
except Exception:
|
|
356
|
+
return []
|
|
357
|
+
for snap in snapshots:
|
|
358
|
+
if not (snap.initialized and snap.exists_on_disk):
|
|
359
|
+
continue
|
|
360
|
+
repo_root = snap.path
|
|
361
|
+
db_path = repo_root / ".codex-autorunner" / "flows.db"
|
|
362
|
+
if not db_path.exists():
|
|
363
|
+
continue
|
|
364
|
+
try:
|
|
365
|
+
config = load_repo_config(repo_root)
|
|
366
|
+
with FlowStore(db_path, durable=config.durable_writes) as store:
|
|
367
|
+
paused = store.list_flow_runs(
|
|
368
|
+
flow_type="ticket_flow", status=FlowRunStatus.PAUSED
|
|
369
|
+
)
|
|
370
|
+
except Exception:
|
|
371
|
+
continue
|
|
372
|
+
if not paused:
|
|
373
|
+
continue
|
|
374
|
+
for record in paused:
|
|
375
|
+
latest = _latest_dispatch(
|
|
376
|
+
repo_root,
|
|
377
|
+
str(record.id),
|
|
378
|
+
dict(record.input_data or {}),
|
|
379
|
+
max_text_chars=max_text_chars,
|
|
380
|
+
)
|
|
381
|
+
if not latest or not latest.get("dispatch"):
|
|
382
|
+
continue
|
|
383
|
+
messages.append(
|
|
384
|
+
{
|
|
385
|
+
"repo_id": snap.id,
|
|
386
|
+
"repo_display_name": snap.display_name,
|
|
387
|
+
"run_id": record.id,
|
|
388
|
+
"run_created_at": record.created_at,
|
|
389
|
+
"seq": latest["seq"],
|
|
390
|
+
"dispatch": latest["dispatch"],
|
|
391
|
+
"files": latest.get("files") or [],
|
|
392
|
+
"open_url": f"/repos/{snap.id}/?tab=inbox&run_id={record.id}",
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
messages.sort(key=lambda m: (m.get("run_created_at") or ""), reverse=True)
|
|
396
|
+
return messages
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _gather_lifecycle_events(
|
|
400
|
+
supervisor: HubSupervisor, limit: int = 20
|
|
401
|
+
) -> list[dict[str, Any]]:
|
|
402
|
+
events = supervisor.lifecycle_store.get_unprocessed(limit=limit)
|
|
403
|
+
result: list[dict[str, Any]] = []
|
|
404
|
+
for event in events[:limit]:
|
|
405
|
+
result.append(
|
|
406
|
+
{
|
|
407
|
+
"event_type": event.event_type.value,
|
|
408
|
+
"repo_id": event.repo_id,
|
|
409
|
+
"run_id": event.run_id,
|
|
410
|
+
"timestamp": event.timestamp,
|
|
411
|
+
"data": event.data,
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
return result
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
async def build_hub_snapshot(
|
|
418
|
+
supervisor: Optional[HubSupervisor],
|
|
419
|
+
hub_root: Optional[Path] = None,
|
|
420
|
+
) -> dict[str, Any]:
|
|
421
|
+
if supervisor is None:
|
|
422
|
+
return {
|
|
423
|
+
"repos": [],
|
|
424
|
+
"inbox": [],
|
|
425
|
+
"templates": {"enabled": False, "repos": []},
|
|
426
|
+
"lifecycle_events": [],
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
snapshots = await asyncio.to_thread(supervisor.list_repos)
|
|
430
|
+
snapshots = sorted(snapshots, key=lambda snap: snap.id)
|
|
431
|
+
pma_config = supervisor.hub_config.pma if supervisor else None
|
|
432
|
+
max_repos = (
|
|
433
|
+
pma_config.max_repos
|
|
434
|
+
if pma_config and pma_config.max_repos > 0
|
|
435
|
+
else PMA_MAX_REPOS
|
|
436
|
+
)
|
|
437
|
+
max_messages = (
|
|
438
|
+
pma_config.max_messages
|
|
439
|
+
if pma_config and pma_config.max_messages > 0
|
|
440
|
+
else PMA_MAX_MESSAGES
|
|
441
|
+
)
|
|
442
|
+
max_text_chars = (
|
|
443
|
+
pma_config.max_text_chars
|
|
444
|
+
if pma_config and pma_config.max_text_chars > 0
|
|
445
|
+
else PMA_MAX_TEXT
|
|
446
|
+
)
|
|
447
|
+
repos: list[dict[str, Any]] = []
|
|
448
|
+
for snap in snapshots[:max_repos]:
|
|
449
|
+
summary: dict[str, Any] = {
|
|
450
|
+
"id": snap.id,
|
|
451
|
+
"display_name": snap.display_name,
|
|
452
|
+
"status": snap.status.value,
|
|
453
|
+
"last_run_id": snap.last_run_id,
|
|
454
|
+
"last_run_started_at": snap.last_run_started_at,
|
|
455
|
+
"last_run_finished_at": snap.last_run_finished_at,
|
|
456
|
+
"last_exit_code": snap.last_exit_code,
|
|
457
|
+
"ticket_flow": None,
|
|
458
|
+
}
|
|
459
|
+
if snap.initialized and snap.exists_on_disk:
|
|
460
|
+
summary["ticket_flow"] = _get_ticket_flow_summary(snap.path)
|
|
461
|
+
repos.append(summary)
|
|
462
|
+
|
|
463
|
+
inbox = await asyncio.to_thread(
|
|
464
|
+
_gather_inbox, supervisor, max_text_chars=max_text_chars
|
|
465
|
+
)
|
|
466
|
+
inbox = inbox[:max_messages]
|
|
467
|
+
|
|
468
|
+
lifecycle_events = await asyncio.to_thread(
|
|
469
|
+
_gather_lifecycle_events, supervisor, limit=20
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
templates = _build_templates_snapshot(supervisor, hub_root=hub_root)
|
|
473
|
+
|
|
474
|
+
pma_files: dict[str, list[str]] = {"inbox": [], "outbox": []}
|
|
475
|
+
if hub_root:
|
|
476
|
+
try:
|
|
477
|
+
pma_dir = hub_root / ".codex-autorunner" / "pma"
|
|
478
|
+
for box in ["inbox", "outbox"]:
|
|
479
|
+
box_dir = pma_dir / box
|
|
480
|
+
if box_dir.exists():
|
|
481
|
+
files = [
|
|
482
|
+
f.name
|
|
483
|
+
for f in box_dir.iterdir()
|
|
484
|
+
if f.is_file() and not f.name.startswith(".")
|
|
485
|
+
]
|
|
486
|
+
pma_files[box] = sorted(files)
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
"repos": repos,
|
|
492
|
+
"inbox": inbox,
|
|
493
|
+
"templates": templates,
|
|
494
|
+
"pma_files": pma_files,
|
|
495
|
+
"lifecycle_events": lifecycle_events,
|
|
496
|
+
}
|