codex-autorunner 1.2.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.
- codex_autorunner/core/about_car.py +12 -12
- codex_autorunner/core/config.py +2 -2
- codex_autorunner/core/context_awareness.py +1 -0
- codex_autorunner/core/pma_context.py +188 -1
- codex_autorunner/integrations/telegram/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- codex_autorunner/integrations/telegram/handlers/messages.py +8 -2
- codex_autorunner/static/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/constants.js +1 -1
- codex_autorunner/static/notifications.js +33 -0
- codex_autorunner/static/styles.css +16 -0
- codex_autorunner/static/terminalManager.js +22 -3
- codex_autorunner/surfaces/web/routes/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +6 -26
- codex_autorunner/surfaces/web/schemas.py +11 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +1 -1
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +22 -22
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,6 @@ from .config import (
|
|
|
7
7
|
REPO_OVERRIDE_FILENAME,
|
|
8
8
|
ROOT_CONFIG_FILENAME,
|
|
9
9
|
ROOT_OVERRIDE_FILENAME,
|
|
10
|
-
Config,
|
|
11
10
|
find_nearest_hub_config_path,
|
|
12
11
|
)
|
|
13
12
|
|
|
@@ -98,6 +97,18 @@ def build_about_car_markdown(
|
|
|
98
97
|
f"`{decisions_disp}`\n"
|
|
99
98
|
"- **Spec**: "
|
|
100
99
|
f"`{spec_disp}`\n\n"
|
|
100
|
+
"## Web UI quick map (repo page)\n"
|
|
101
|
+
"- Repo view: `/repos/<repo_id>/`\n"
|
|
102
|
+
"- Tabs: **Tickets** = `.codex-autorunner/tickets/` queue.\n"
|
|
103
|
+
"- Tabs: **Inbox** = paused run dispatches/handoffs.\n"
|
|
104
|
+
"- Tabs: **Workspace** = edit `active_context.md`, `spec.md`, `decisions.md`.\n"
|
|
105
|
+
"- Tabs: **Terminal** = launches the configured `codex` binary in a PTY.\n"
|
|
106
|
+
"- Tabs: **Archive** = browse worktree snapshots.\n\n"
|
|
107
|
+
"## FileBox (attachments)\n"
|
|
108
|
+
"- Repo FileBox root: `.codex-autorunner/filebox/`.\n"
|
|
109
|
+
"- User uploads: `.codex-autorunner/filebox/inbox/`.\n"
|
|
110
|
+
"- Files to send back: `.codex-autorunner/filebox/outbox/`.\n"
|
|
111
|
+
"- Note: ticket_flow uses per-run dispatch directories; do not confuse dispatch with FileBox.\n\n"
|
|
101
112
|
"## Critical rules\n"
|
|
102
113
|
"- Do **not** create new copies of workspace docs elsewhere in the repo.\n"
|
|
103
114
|
"- Treat `.codex-autorunner/` as intentional project structure even though it is hidden/gitignored.\n\n"
|
|
@@ -211,17 +222,6 @@ def build_tickets_agents_markdown(*, repo_root: Path) -> str:
|
|
|
211
222
|
)
|
|
212
223
|
|
|
213
224
|
|
|
214
|
-
def ensure_about_car_file(config: Config, *, force: bool = False) -> Path:
|
|
215
|
-
"""Config-aware wrapper that uses configured doc paths."""
|
|
216
|
-
repo_root = config.root
|
|
217
|
-
docs = {
|
|
218
|
-
"active_context": config.doc_path("active_context"),
|
|
219
|
-
"decisions": config.doc_path("decisions"),
|
|
220
|
-
"spec": config.doc_path("spec"),
|
|
221
|
-
}
|
|
222
|
-
return ensure_about_car_file_for_repo(repo_root, doc_paths=docs, force=force)
|
|
223
|
-
|
|
224
|
-
|
|
225
225
|
def ensure_ticket_flow_quickstart_file_for_repo(
|
|
226
226
|
repo_root: Path, *, force: bool = False
|
|
227
227
|
) -> Path:
|
codex_autorunner/core/config.py
CHANGED
|
@@ -258,7 +258,7 @@ DEFAULT_REPO_CONFIG: Dict[str, Any] = {
|
|
|
258
258
|
"files": True,
|
|
259
259
|
"max_image_bytes": 10_000_000,
|
|
260
260
|
"max_voice_bytes": 10_000_000,
|
|
261
|
-
"max_file_bytes":
|
|
261
|
+
"max_file_bytes": 100_000_000,
|
|
262
262
|
"image_prompt": (
|
|
263
263
|
"The user sent an image with no caption. Use it to continue the "
|
|
264
264
|
"conversation; if no clear task, describe the image and ask what "
|
|
@@ -542,7 +542,7 @@ DEFAULT_HUB_CONFIG: Dict[str, Any] = {
|
|
|
542
542
|
"files": True,
|
|
543
543
|
"max_image_bytes": 10_000_000,
|
|
544
544
|
"max_voice_bytes": 10_000_000,
|
|
545
|
-
"max_file_bytes":
|
|
545
|
+
"max_file_bytes": 100_000_000,
|
|
546
546
|
"image_prompt": (
|
|
547
547
|
"The user sent an image with no caption. Use it to continue the "
|
|
548
548
|
"conversation; if no clear task, describe the image and ask what "
|
|
@@ -12,6 +12,7 @@ CAR’s durable control-plane lives under `.codex-autorunner/`:
|
|
|
12
12
|
- `active_context.md` — current “north star” context; kept fresh for ongoing work.
|
|
13
13
|
- `spec.md` — longer spec / acceptance criteria when needed.
|
|
14
14
|
- `decisions.md` — prior decisions / tradeoffs when relevant.
|
|
15
|
+
- `.codex-autorunner/filebox/` — attachments inbox/outbox used by CAR surfaces (if present).
|
|
15
16
|
|
|
16
17
|
Intent signals: if the user mentions tickets, “dispatch”, “resume”, workspace docs, or `.codex-autorunner/`, they are likely referring to CAR artifacts/workflow rather than generic repo files.
|
|
17
18
|
|
|
@@ -19,6 +19,30 @@ PMA_MAX_MESSAGES = 10
|
|
|
19
19
|
PMA_MAX_TEXT = 800
|
|
20
20
|
PMA_MAX_TEMPLATE_REPOS = 25
|
|
21
21
|
PMA_MAX_TEMPLATE_FIELD_CHARS = 120
|
|
22
|
+
PMA_MAX_PMA_FILES = 50
|
|
23
|
+
PMA_MAX_LIFECYCLE_EVENTS = 20
|
|
24
|
+
|
|
25
|
+
# Keep this short and stable; see ticket TICKET-001 for rationale.
|
|
26
|
+
PMA_FASTPATH = """<pma_fastpath>
|
|
27
|
+
You are PMA inside Codex Autorunner (CAR). Treat the filesystem as truth; prefer creating/updating CAR artifacts over "chat-only" plans.
|
|
28
|
+
|
|
29
|
+
First-turn routine:
|
|
30
|
+
1) Read <user_message> and <hub_snapshot>.
|
|
31
|
+
2) If hub_snapshot.inbox has entries, handle them first (these are paused runs needing input):
|
|
32
|
+
- Summarize the dispatch question.
|
|
33
|
+
- Answer it or propose the next minimal action.
|
|
34
|
+
- Include the item.open_url so the user can jump straight to the repo Inbox tab.
|
|
35
|
+
3) If the request is new work:
|
|
36
|
+
- Identify the target repo(s).
|
|
37
|
+
- Prefer hub-owned worktrees for changes.
|
|
38
|
+
- Create/adjust repo tickets under each repo's `.codex-autorunner/tickets/`.
|
|
39
|
+
|
|
40
|
+
Web UI map (user perspective):
|
|
41
|
+
- Hub root: `/` (repos list + global notifications).
|
|
42
|
+
- Repo view: `/repos/<repo_id>/` tabs: Tickets | Inbox | Workspace | Terminal | Analytics | Archive.
|
|
43
|
+
- Tickets: edit queue; Inbox: paused run dispatches; Workspace: active_context/spec/decisions.
|
|
44
|
+
</pma_fastpath>
|
|
45
|
+
"""
|
|
22
46
|
|
|
23
47
|
# Defaults used when hub config is not available (should be rare).
|
|
24
48
|
PMA_DOCS_MAX_CHARS = 12_000
|
|
@@ -192,13 +216,170 @@ def load_pma_prompt(hub_root: Path) -> str:
|
|
|
192
216
|
return ""
|
|
193
217
|
|
|
194
218
|
|
|
219
|
+
def _render_ticket_flow_summary(summary: Optional[dict[str, Any]]) -> str:
|
|
220
|
+
if not summary:
|
|
221
|
+
return "null"
|
|
222
|
+
status = summary.get("status")
|
|
223
|
+
done_count = summary.get("done_count")
|
|
224
|
+
total_count = summary.get("total_count")
|
|
225
|
+
current_step = summary.get("current_step")
|
|
226
|
+
parts: list[str] = []
|
|
227
|
+
if status is not None:
|
|
228
|
+
parts.append(f"status={status}")
|
|
229
|
+
if done_count is not None and total_count is not None:
|
|
230
|
+
parts.append(f"done={done_count}/{total_count}")
|
|
231
|
+
if current_step is not None:
|
|
232
|
+
parts.append(f"step={current_step}")
|
|
233
|
+
if not parts:
|
|
234
|
+
return "null"
|
|
235
|
+
return " ".join(parts)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _render_hub_snapshot(
|
|
239
|
+
snapshot: dict[str, Any],
|
|
240
|
+
*,
|
|
241
|
+
max_repos: int = PMA_MAX_REPOS,
|
|
242
|
+
max_messages: int = PMA_MAX_MESSAGES,
|
|
243
|
+
max_text_chars: int = PMA_MAX_TEXT,
|
|
244
|
+
max_template_repos: int = PMA_MAX_TEMPLATE_REPOS,
|
|
245
|
+
max_field_chars: int = PMA_MAX_TEMPLATE_FIELD_CHARS,
|
|
246
|
+
max_pma_files: int = PMA_MAX_PMA_FILES,
|
|
247
|
+
max_lifecycle_events: int = PMA_MAX_LIFECYCLE_EVENTS,
|
|
248
|
+
) -> str:
|
|
249
|
+
lines: list[str] = []
|
|
250
|
+
|
|
251
|
+
inbox = snapshot.get("inbox") or []
|
|
252
|
+
if inbox:
|
|
253
|
+
lines.append("Inbox (paused runs needing attention):")
|
|
254
|
+
for item in list(inbox)[: max(0, max_messages)]:
|
|
255
|
+
repo_id = _truncate(str(item.get("repo_id", "")), max_field_chars)
|
|
256
|
+
run_id = _truncate(str(item.get("run_id", "")), max_field_chars)
|
|
257
|
+
seq = _truncate(str(item.get("seq", "")), max_field_chars)
|
|
258
|
+
dispatch = item.get("dispatch") or {}
|
|
259
|
+
mode = _truncate(str(dispatch.get("mode", "")), max_field_chars)
|
|
260
|
+
handoff = bool(dispatch.get("is_handoff"))
|
|
261
|
+
lines.append(
|
|
262
|
+
f"- repo_id={repo_id} run_id={run_id} seq={seq} mode={mode} "
|
|
263
|
+
f"handoff={str(handoff).lower()}"
|
|
264
|
+
)
|
|
265
|
+
title = dispatch.get("title")
|
|
266
|
+
if title:
|
|
267
|
+
lines.append(f" title: {_truncate(str(title), max_text_chars)}")
|
|
268
|
+
body = dispatch.get("body")
|
|
269
|
+
if body:
|
|
270
|
+
lines.append(f" body: {_truncate(str(body), max_text_chars)}")
|
|
271
|
+
files = item.get("files") or []
|
|
272
|
+
if files:
|
|
273
|
+
display = [
|
|
274
|
+
_truncate(str(name), max_field_chars)
|
|
275
|
+
for name in list(files)[: max(0, max_pma_files)]
|
|
276
|
+
]
|
|
277
|
+
lines.append(f" attachments: [{', '.join(display)}]")
|
|
278
|
+
open_url = item.get("open_url")
|
|
279
|
+
if open_url:
|
|
280
|
+
lines.append(f" open_url: {_truncate(str(open_url), max_field_chars)}")
|
|
281
|
+
lines.append("")
|
|
282
|
+
|
|
283
|
+
repos = snapshot.get("repos") or []
|
|
284
|
+
if repos:
|
|
285
|
+
lines.append("Repos:")
|
|
286
|
+
for repo in list(repos)[: max(0, max_repos)]:
|
|
287
|
+
repo_id = _truncate(str(repo.get("id", "")), max_field_chars)
|
|
288
|
+
display_name = _truncate(str(repo.get("display_name", "")), max_field_chars)
|
|
289
|
+
status = _truncate(str(repo.get("status", "")), max_field_chars)
|
|
290
|
+
last_run_id = _truncate(str(repo.get("last_run_id", "")), max_field_chars)
|
|
291
|
+
last_exit = _truncate(str(repo.get("last_exit_code", "")), max_field_chars)
|
|
292
|
+
ticket_flow = _render_ticket_flow_summary(repo.get("ticket_flow"))
|
|
293
|
+
lines.append(
|
|
294
|
+
f"- {repo_id} ({display_name}): status={status} "
|
|
295
|
+
f"last_run_id={last_run_id} last_exit_code={last_exit} "
|
|
296
|
+
f"ticket_flow={ticket_flow}"
|
|
297
|
+
)
|
|
298
|
+
lines.append("")
|
|
299
|
+
|
|
300
|
+
templates = snapshot.get("templates") or {}
|
|
301
|
+
template_repos = templates.get("repos") or []
|
|
302
|
+
template_scan = templates.get("last_scan")
|
|
303
|
+
if templates.get("enabled") or template_repos or template_scan:
|
|
304
|
+
enabled = bool(templates.get("enabled"))
|
|
305
|
+
lines.append("Templates:")
|
|
306
|
+
lines.append(f"- enabled={str(enabled).lower()}")
|
|
307
|
+
if template_repos:
|
|
308
|
+
items: list[str] = []
|
|
309
|
+
for repo in list(template_repos)[: max(0, max_template_repos)]:
|
|
310
|
+
repo_id = _truncate(str(repo.get("id", "")), max_field_chars)
|
|
311
|
+
default_ref = _truncate(
|
|
312
|
+
str(repo.get("default_ref", "")), max_field_chars
|
|
313
|
+
)
|
|
314
|
+
trusted = bool(repo.get("trusted"))
|
|
315
|
+
items.append(f"{repo_id}@{default_ref} trusted={str(trusted).lower()}")
|
|
316
|
+
lines.append(f"- repos: [{', '.join(items)}]")
|
|
317
|
+
if template_scan:
|
|
318
|
+
repo_id = _truncate(str(template_scan.get("repo_id", "")), max_field_chars)
|
|
319
|
+
decision = _truncate(
|
|
320
|
+
str(template_scan.get("decision", "")), max_field_chars
|
|
321
|
+
)
|
|
322
|
+
severity = _truncate(
|
|
323
|
+
str(template_scan.get("severity", "")), max_field_chars
|
|
324
|
+
)
|
|
325
|
+
scanned_at = _truncate(
|
|
326
|
+
str(template_scan.get("scanned_at", "")), max_field_chars
|
|
327
|
+
)
|
|
328
|
+
lines.append(
|
|
329
|
+
f"- last_scan: {repo_id} {decision} {severity} {scanned_at}".strip()
|
|
330
|
+
)
|
|
331
|
+
lines.append("")
|
|
332
|
+
|
|
333
|
+
pma_files = snapshot.get("pma_files") or {}
|
|
334
|
+
inbox_files = pma_files.get("inbox") or []
|
|
335
|
+
outbox_files = pma_files.get("outbox") or []
|
|
336
|
+
if inbox_files or outbox_files:
|
|
337
|
+
lines.append("PMA files:")
|
|
338
|
+
if inbox_files:
|
|
339
|
+
files = [
|
|
340
|
+
_truncate(str(name), max_field_chars)
|
|
341
|
+
for name in list(inbox_files)[: max(0, max_pma_files)]
|
|
342
|
+
]
|
|
343
|
+
lines.append(f"- inbox: [{', '.join(files)}]")
|
|
344
|
+
if outbox_files:
|
|
345
|
+
files = [
|
|
346
|
+
_truncate(str(name), max_field_chars)
|
|
347
|
+
for name in list(outbox_files)[: max(0, max_pma_files)]
|
|
348
|
+
]
|
|
349
|
+
lines.append(f"- outbox: [{', '.join(files)}]")
|
|
350
|
+
lines.append("")
|
|
351
|
+
|
|
352
|
+
lifecycle_events = snapshot.get("lifecycle_events") or []
|
|
353
|
+
if lifecycle_events:
|
|
354
|
+
lines.append("Lifecycle events (recent):")
|
|
355
|
+
for event in list(lifecycle_events)[: max(0, max_lifecycle_events)]:
|
|
356
|
+
timestamp = _truncate(str(event.get("timestamp", "")), max_field_chars)
|
|
357
|
+
event_type = _truncate(str(event.get("event_type", "")), max_field_chars)
|
|
358
|
+
repo_id = _truncate(str(event.get("repo_id", "")), max_field_chars)
|
|
359
|
+
run_id = _truncate(str(event.get("run_id", "")), max_field_chars)
|
|
360
|
+
lines.append(
|
|
361
|
+
f"- {timestamp} {event_type} repo_id={repo_id} run_id={run_id}"
|
|
362
|
+
)
|
|
363
|
+
lines.append("")
|
|
364
|
+
|
|
365
|
+
if lines and lines[-1] == "":
|
|
366
|
+
lines.pop()
|
|
367
|
+
return "\n".join(lines)
|
|
368
|
+
|
|
369
|
+
|
|
195
370
|
def format_pma_prompt(
|
|
196
371
|
base_prompt: str,
|
|
197
372
|
snapshot: dict[str, Any],
|
|
198
373
|
message: str,
|
|
199
374
|
hub_root: Optional[Path] = None,
|
|
200
375
|
) -> str:
|
|
201
|
-
|
|
376
|
+
limits = snapshot.get("limits") or {}
|
|
377
|
+
snapshot_text = _render_hub_snapshot(
|
|
378
|
+
snapshot,
|
|
379
|
+
max_repos=limits.get("max_repos", PMA_MAX_REPOS),
|
|
380
|
+
max_messages=limits.get("max_messages", PMA_MAX_MESSAGES),
|
|
381
|
+
max_text_chars=limits.get("max_text_chars", PMA_MAX_TEXT),
|
|
382
|
+
)
|
|
202
383
|
|
|
203
384
|
pma_docs: Optional[dict[str, Any]] = None
|
|
204
385
|
if hub_root is not None:
|
|
@@ -235,6 +416,7 @@ def format_pma_prompt(
|
|
|
235
416
|
"</pma_workspace_docs>\n\n"
|
|
236
417
|
)
|
|
237
418
|
|
|
419
|
+
prompt += f"{PMA_FASTPATH}\n\n"
|
|
238
420
|
prompt += (
|
|
239
421
|
"<hub_snapshot>\n"
|
|
240
422
|
f"{snapshot_text}\n"
|
|
@@ -493,4 +675,9 @@ async def build_hub_snapshot(
|
|
|
493
675
|
"templates": templates,
|
|
494
676
|
"pma_files": pma_files,
|
|
495
677
|
"lifecycle_events": lifecycle_events,
|
|
678
|
+
"limits": {
|
|
679
|
+
"max_repos": max_repos,
|
|
680
|
+
"max_messages": max_messages,
|
|
681
|
+
"max_text_chars": max_text_chars,
|
|
682
|
+
},
|
|
496
683
|
}
|
|
@@ -1370,7 +1370,7 @@ class TelegramBotClient:
|
|
|
1370
1370
|
return bool(result) if isinstance(result, bool) else False
|
|
1371
1371
|
|
|
1372
1372
|
async def download_file(
|
|
1373
|
-
self, file_path: str, max_size_bytes: int =
|
|
1373
|
+
self, file_path: str, max_size_bytes: int = 100 * 1024 * 1024
|
|
1374
1374
|
) -> bytes:
|
|
1375
1375
|
safe_path = file_path.lstrip("/")
|
|
1376
1376
|
url = f"{self._file_base_url}/{safe_path}"
|
|
@@ -44,7 +44,7 @@ DEFAULT_APP_SERVER_TURN_TIMEOUT_SECONDS = 28800
|
|
|
44
44
|
DEFAULT_APPROVAL_TIMEOUT_SECONDS = 300.0
|
|
45
45
|
DEFAULT_MEDIA_MAX_IMAGE_BYTES = 10 * 1024 * 1024
|
|
46
46
|
DEFAULT_MEDIA_MAX_VOICE_BYTES = 10 * 1024 * 1024
|
|
47
|
-
DEFAULT_MEDIA_MAX_FILE_BYTES =
|
|
47
|
+
DEFAULT_MEDIA_MAX_FILE_BYTES = 100 * 1024 * 1024
|
|
48
48
|
DEFAULT_MEDIA_IMAGE_PROMPT = (
|
|
49
49
|
"The user sent an image with no caption. Use it to continue the "
|
|
50
50
|
"conversation; if no clear task, describe the image and ask what they want."
|
|
@@ -859,7 +859,10 @@ async def handle_media_message(
|
|
|
859
859
|
best = photos[0]
|
|
860
860
|
try:
|
|
861
861
|
file_info = await handlers._bot.get_file(best.file_id)
|
|
862
|
-
data = await handlers._bot.download_file(
|
|
862
|
+
data = await handlers._bot.download_file(
|
|
863
|
+
file_info.file_path,
|
|
864
|
+
max_size_bytes=handlers._config.media.max_image_bytes,
|
|
865
|
+
)
|
|
863
866
|
filename = f"photo_{best.file_id}.jpg"
|
|
864
867
|
files.append((filename, data))
|
|
865
868
|
except Exception as exc:
|
|
@@ -868,7 +871,10 @@ async def handle_media_message(
|
|
|
868
871
|
elif message.document:
|
|
869
872
|
try:
|
|
870
873
|
file_info = await handlers._bot.get_file(message.document.file_id)
|
|
871
|
-
data = await handlers._bot.download_file(
|
|
874
|
+
data = await handlers._bot.download_file(
|
|
875
|
+
file_info.file_path,
|
|
876
|
+
max_size_bytes=handlers._config.media.max_file_bytes,
|
|
877
|
+
)
|
|
872
878
|
filename = (
|
|
873
879
|
message.document.file_name or f"document_{message.document.file_id}"
|
|
874
880
|
)
|