codex-autorunner 1.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. codex_autorunner/agents/opencode/client.py +113 -4
  2. codex_autorunner/agents/opencode/supervisor.py +4 -0
  3. codex_autorunner/agents/registry.py +17 -7
  4. codex_autorunner/bootstrap.py +219 -1
  5. codex_autorunner/core/__init__.py +17 -1
  6. codex_autorunner/core/about_car.py +124 -11
  7. codex_autorunner/core/app_server_threads.py +6 -0
  8. codex_autorunner/core/config.py +238 -3
  9. codex_autorunner/core/context_awareness.py +39 -0
  10. codex_autorunner/core/docs.py +0 -122
  11. codex_autorunner/core/filebox.py +265 -0
  12. codex_autorunner/core/flows/controller.py +71 -1
  13. codex_autorunner/core/flows/reconciler.py +4 -1
  14. codex_autorunner/core/flows/runtime.py +22 -0
  15. codex_autorunner/core/flows/store.py +61 -9
  16. codex_autorunner/core/flows/transition.py +23 -16
  17. codex_autorunner/core/flows/ux_helpers.py +18 -3
  18. codex_autorunner/core/flows/worker_process.py +32 -6
  19. codex_autorunner/core/hub.py +198 -41
  20. codex_autorunner/core/lifecycle_events.py +253 -0
  21. codex_autorunner/core/path_utils.py +2 -1
  22. codex_autorunner/core/pma_audit.py +224 -0
  23. codex_autorunner/core/pma_context.py +683 -0
  24. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  25. codex_autorunner/core/pma_lifecycle.py +527 -0
  26. codex_autorunner/core/pma_queue.py +367 -0
  27. codex_autorunner/core/pma_safety.py +221 -0
  28. codex_autorunner/core/pma_state.py +115 -0
  29. codex_autorunner/core/ports/agent_backend.py +2 -5
  30. codex_autorunner/core/ports/run_event.py +1 -4
  31. codex_autorunner/core/prompt.py +0 -80
  32. codex_autorunner/core/prompts.py +56 -172
  33. codex_autorunner/core/redaction.py +0 -4
  34. codex_autorunner/core/review_context.py +11 -9
  35. codex_autorunner/core/runner_controller.py +35 -33
  36. codex_autorunner/core/runner_state.py +147 -0
  37. codex_autorunner/core/runtime.py +829 -0
  38. codex_autorunner/core/sqlite_utils.py +13 -4
  39. codex_autorunner/core/state.py +7 -10
  40. codex_autorunner/core/state_roots.py +5 -0
  41. codex_autorunner/core/templates/__init__.py +39 -0
  42. codex_autorunner/core/templates/git_mirror.py +234 -0
  43. codex_autorunner/core/templates/provenance.py +56 -0
  44. codex_autorunner/core/templates/scan_cache.py +120 -0
  45. codex_autorunner/core/ticket_linter_cli.py +17 -0
  46. codex_autorunner/core/ticket_manager_cli.py +154 -92
  47. codex_autorunner/core/time_utils.py +11 -0
  48. codex_autorunner/core/types.py +18 -0
  49. codex_autorunner/core/utils.py +34 -6
  50. codex_autorunner/flows/review/service.py +23 -25
  51. codex_autorunner/flows/ticket_flow/definition.py +43 -1
  52. codex_autorunner/integrations/agents/__init__.py +2 -0
  53. codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
  54. codex_autorunner/integrations/agents/codex_backend.py +19 -8
  55. codex_autorunner/integrations/agents/runner.py +3 -8
  56. codex_autorunner/integrations/agents/wiring.py +8 -0
  57. codex_autorunner/integrations/telegram/adapter.py +1 -1
  58. codex_autorunner/integrations/telegram/config.py +1 -1
  59. codex_autorunner/integrations/telegram/doctor.py +228 -6
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
  63. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  64. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
  65. codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
  66. codex_autorunner/integrations/telegram/handlers/messages.py +34 -3
  67. codex_autorunner/integrations/telegram/helpers.py +1 -3
  68. codex_autorunner/integrations/telegram/runtime.py +9 -4
  69. codex_autorunner/integrations/telegram/service.py +30 -0
  70. codex_autorunner/integrations/telegram/state.py +38 -0
  71. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
  72. codex_autorunner/integrations/telegram/transport.py +10 -3
  73. codex_autorunner/integrations/templates/__init__.py +27 -0
  74. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  75. codex_autorunner/server.py +2 -2
  76. codex_autorunner/static/agentControls.js +21 -5
  77. codex_autorunner/static/app.js +115 -11
  78. codex_autorunner/static/archive.js +274 -81
  79. codex_autorunner/static/archiveApi.js +21 -0
  80. codex_autorunner/static/chatUploads.js +137 -0
  81. codex_autorunner/static/constants.js +1 -1
  82. codex_autorunner/static/docChatCore.js +185 -13
  83. codex_autorunner/static/fileChat.js +68 -40
  84. codex_autorunner/static/fileboxUi.js +159 -0
  85. codex_autorunner/static/hub.js +46 -81
  86. codex_autorunner/static/index.html +303 -24
  87. codex_autorunner/static/messages.js +82 -4
  88. codex_autorunner/static/notifications.js +288 -0
  89. codex_autorunner/static/pma.js +1167 -0
  90. codex_autorunner/static/settings.js +3 -0
  91. codex_autorunner/static/streamUtils.js +57 -0
  92. codex_autorunner/static/styles.css +9141 -6742
  93. codex_autorunner/static/templateReposSettings.js +225 -0
  94. codex_autorunner/static/terminalManager.js +22 -3
  95. codex_autorunner/static/ticketChatActions.js +165 -3
  96. codex_autorunner/static/ticketChatStream.js +17 -119
  97. codex_autorunner/static/ticketEditor.js +41 -13
  98. codex_autorunner/static/ticketTemplates.js +798 -0
  99. codex_autorunner/static/tickets.js +69 -19
  100. codex_autorunner/static/turnEvents.js +27 -0
  101. codex_autorunner/static/turnResume.js +33 -0
  102. codex_autorunner/static/utils.js +28 -0
  103. codex_autorunner/static/workspace.js +258 -44
  104. codex_autorunner/static/workspaceFileBrowser.js +6 -4
  105. codex_autorunner/surfaces/cli/cli.py +1465 -155
  106. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  107. codex_autorunner/surfaces/web/app.py +253 -49
  108. codex_autorunner/surfaces/web/routes/__init__.py +4 -0
  109. codex_autorunner/surfaces/web/routes/analytics.py +29 -22
  110. codex_autorunner/surfaces/web/routes/archive.py +197 -0
  111. codex_autorunner/surfaces/web/routes/file_chat.py +297 -36
  112. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  113. codex_autorunner/surfaces/web/routes/flows.py +219 -29
  114. codex_autorunner/surfaces/web/routes/messages.py +70 -39
  115. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  116. codex_autorunner/surfaces/web/routes/repos.py +1 -1
  117. codex_autorunner/surfaces/web/routes/shared.py +0 -3
  118. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  119. codex_autorunner/surfaces/web/runner_manager.py +2 -2
  120. codex_autorunner/surfaces/web/schemas.py +81 -18
  121. codex_autorunner/tickets/agent_pool.py +27 -0
  122. codex_autorunner/tickets/files.py +33 -16
  123. codex_autorunner/tickets/lint.py +50 -0
  124. codex_autorunner/tickets/models.py +3 -0
  125. codex_autorunner/tickets/outbox.py +41 -5
  126. codex_autorunner/tickets/runner.py +350 -69
  127. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +15 -19
  128. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
  129. codex_autorunner/core/adapter_utils.py +0 -21
  130. codex_autorunner/core/engine.py +0 -3302
  131. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
  132. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
  133. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
  134. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,7 @@ from ..workspace.paths import workspace_doc_path
10
10
  from .agent_pool import AgentPool, AgentTurnRequest
11
11
  from .files import list_ticket_paths, read_ticket, safe_relpath, ticket_is_done
12
12
  from .frontmatter import parse_markdown_frontmatter
13
- from .lint import lint_ticket_frontmatter
13
+ from .lint import lint_ticket_directory, lint_ticket_frontmatter
14
14
  from .models import TicketFrontmatter, TicketResult, TicketRunConfig
15
15
  from .outbox import (
16
16
  archive_dispatch,
@@ -18,11 +18,154 @@ from .outbox import (
18
18
  ensure_outbox_dirs,
19
19
  resolve_outbox_paths,
20
20
  )
21
- from .replies import ensure_reply_dirs, parse_user_reply, resolve_reply_paths
21
+ from .replies import (
22
+ dispatch_reply,
23
+ ensure_reply_dirs,
24
+ next_reply_seq,
25
+ parse_user_reply,
26
+ resolve_reply_paths,
27
+ )
22
28
 
23
29
  _logger = logging.getLogger(__name__)
24
30
 
25
31
  WORKSPACE_DOC_MAX_CHARS = 4000
32
+ TRUNCATION_MARKER = "\n\n[... TRUNCATED ...]\n\n"
33
+
34
+
35
+ def _truncate_text_by_bytes(text: str, max_bytes: int) -> str:
36
+ """Truncate text to fit within max_bytes UTF-8 encoded size."""
37
+ if max_bytes <= 0:
38
+ return ""
39
+ normalized = text or ""
40
+ encoded = normalized.encode("utf-8")
41
+ if len(encoded) <= max_bytes:
42
+ return normalized
43
+ marker_bytes = len(TRUNCATION_MARKER.encode("utf-8"))
44
+ if max_bytes <= marker_bytes:
45
+ return TRUNCATION_MARKER.encode("utf-8")[:max_bytes].decode(
46
+ "utf-8", errors="ignore"
47
+ )
48
+ target_bytes = max_bytes - marker_bytes
49
+ truncated = encoded[:target_bytes].decode("utf-8", errors="ignore")
50
+ return truncated + TRUNCATION_MARKER
51
+
52
+
53
+ def _is_network_error(error_message: str) -> bool:
54
+ """Check if an error message indicates a transient network issue.
55
+
56
+ Returns True if the error appears to be network-related and retryable.
57
+ This includes connection errors, timeouts, and transport failures.
58
+ """
59
+ if not error_message:
60
+ return False
61
+ error_lower = error_message.lower()
62
+ network_indicators = [
63
+ "network error",
64
+ "connection",
65
+ "timeout",
66
+ "transport error",
67
+ "disconnected",
68
+ "unreachable",
69
+ "reconnecting",
70
+ "connection refused",
71
+ "connection reset",
72
+ "connection broken",
73
+ "temporary failure",
74
+ ]
75
+ return any(indicator in error_lower for indicator in network_indicators)
76
+
77
+
78
+ def _preserve_ticket_structure(ticket_block: str, max_bytes: int) -> str:
79
+ """Truncate ticket block while preserving prefix and ticket frontmatter.
80
+
81
+ ticket_block format:
82
+ "\\n\\n<CAR_CURRENT_TICKET_FILE>\\nPATH: ...\\n<TICKET_MARKDOWN>\\n"
83
+ "{ticket_raw_content}\\n</TICKET_MARKDOWN>\\n</CAR_CURRENT_TICKET_FILE>\\n"
84
+ where ticket_raw_content itself contains markdown frontmatter.
85
+ """
86
+ if len(ticket_block.encode("utf-8")) <= max_bytes:
87
+ return ticket_block
88
+
89
+ # ticket_block structure:
90
+ # "<CAR_CURRENT_TICKET_FILE>\n"
91
+ # "PATH: {rel_ticket}\n"
92
+ # "<TICKET_MARKDOWN>\n"
93
+ # "---\n" - ticket frontmatter start
94
+ # "agent: ...\n"
95
+ # "done: ...\n"
96
+ # "title: ...\n"
97
+ # "goal: ...\n"
98
+ # "---\n" - ticket frontmatter end (what we want to preserve)
99
+ # ticket body...
100
+ # "</TICKET_MARKDOWN>\n"
101
+ # "</CAR_CURRENT_TICKET_FILE>\n"
102
+
103
+ # Find the frontmatter markers after <TICKET_MARKDOWN>.
104
+ marker = "\n---\n"
105
+ ticket_md_idx = ticket_block.find("<TICKET_MARKDOWN>")
106
+ if ticket_md_idx == -1:
107
+ return _truncate_text_by_bytes(ticket_block, max_bytes)
108
+
109
+ first_marker_idx = ticket_block.find(marker, ticket_md_idx)
110
+ if first_marker_idx == -1:
111
+ return _truncate_text_by_bytes(ticket_block, max_bytes)
112
+
113
+ second_marker_idx = ticket_block.find(marker, first_marker_idx + 1)
114
+ if second_marker_idx == -1:
115
+ return _truncate_text_by_bytes(ticket_block, max_bytes)
116
+
117
+ # Preserve everything up to and including the second marker
118
+ preserve_end = second_marker_idx + len(marker)
119
+ preserved_part = ticket_block[:preserve_end]
120
+
121
+ # Check if we still have room (account for truncation marker that will be added)
122
+ preserved_bytes = len(preserved_part.encode("utf-8"))
123
+ marker_bytes = len(TRUNCATION_MARKER.encode("utf-8"))
124
+ remaining_bytes = max(max_bytes - preserved_bytes, 0)
125
+
126
+ if remaining_bytes > 0:
127
+ body = ticket_block[preserve_end:]
128
+ # Account for marker in the body budget
129
+ body_budget = max(remaining_bytes - marker_bytes, 0)
130
+ truncated_body = _truncate_text_by_bytes(body, body_budget)
131
+ return preserved_part + truncated_body
132
+
133
+ # Not enough room even for preserved part, fall back to simple truncation
134
+ return _truncate_text_by_bytes(ticket_block, max_bytes)
135
+
136
+
137
+ def _shrink_prompt(
138
+ *,
139
+ max_bytes: int,
140
+ render: Callable[[], str],
141
+ sections: dict[str, str],
142
+ order: list[str],
143
+ ) -> str:
144
+ """Shrink prompt by truncating sections in order of priority."""
145
+ prompt = render()
146
+ if len(prompt.encode("utf-8")) <= max_bytes:
147
+ return prompt
148
+
149
+ for key in order:
150
+ if len(prompt.encode("utf-8")) <= max_bytes:
151
+ break
152
+ value = sections.get(key, "")
153
+ if not value:
154
+ continue
155
+ overflow = len(prompt.encode("utf-8")) - max_bytes
156
+ value_bytes = len(value.encode("utf-8"))
157
+ new_limit = max(value_bytes - overflow, 0)
158
+
159
+ if key == "ticket_block":
160
+ sections[key] = _preserve_ticket_structure(value, new_limit)
161
+ else:
162
+ sections[key] = _truncate_text_by_bytes(value, new_limit)
163
+ prompt = render()
164
+
165
+ if len(prompt.encode("utf-8")) > max_bytes:
166
+ prompt = _truncate_text_by_bytes(prompt, max_bytes)
167
+
168
+ return prompt
26
169
 
27
170
 
28
171
  class TicketRunner:
@@ -41,11 +184,13 @@ class TicketRunner:
41
184
  run_id: str,
42
185
  config: TicketRunConfig,
43
186
  agent_pool: AgentPool,
187
+ repo_id: str = "",
44
188
  ):
45
189
  self._workspace_root = workspace_root
46
190
  self._run_id = run_id
47
191
  self._config = config
48
192
  self._agent_pool = agent_pool
193
+ self._repo_id = repo_id
49
194
 
50
195
  async def step(
51
196
  self,
@@ -73,11 +218,17 @@ class TicketRunner:
73
218
  commit_retries = int(commit_state.get("retries") or 0)
74
219
  # Global counters.
75
220
  total_turns = int(state.get("total_turns") or 0)
221
+
222
+ _network_raw = state.get("network_retry")
223
+ network_retry_state: dict[str, Any] = (
224
+ _network_raw if isinstance(_network_raw, dict) else {}
225
+ )
226
+ network_retries = int(network_retry_state.get("retries") or 0)
76
227
  if total_turns >= self._config.max_total_turns:
77
228
  return self._pause(
78
229
  state,
79
230
  reason=f"Max turns reached ({self._config.max_total_turns}). Review tickets and resume.",
80
- reason_code="needs_user_fix",
231
+ reason_code="max_turns",
81
232
  )
82
233
 
83
234
  ticket_dir = self._workspace_root / self._config.ticket_dir
@@ -98,6 +249,23 @@ class TicketRunner:
98
249
  run_id=self._run_id,
99
250
  )
100
251
  ensure_reply_dirs(reply_paths)
252
+ if reply_paths.user_reply_path.exists():
253
+ next_seq = next_reply_seq(reply_paths.reply_history_dir)
254
+ archived, errors = dispatch_reply(reply_paths, next_seq=next_seq)
255
+ if errors:
256
+ return self._pause(
257
+ state,
258
+ reason="Failed to archive USER_REPLY.md.",
259
+ reason_details="Errors:\n- " + "\n- ".join(errors),
260
+ reason_code="needs_user_fix",
261
+ )
262
+ if archived is None:
263
+ return self._pause(
264
+ state,
265
+ reason="Failed to archive USER_REPLY.md.",
266
+ reason_details="Errors:\n- Failed to archive reply",
267
+ reason_code="needs_user_fix",
268
+ )
101
269
 
102
270
  ticket_paths = list_ticket_paths(ticket_dir)
103
271
  if not ticket_paths:
@@ -110,6 +278,16 @@ class TicketRunner:
110
278
  reason_code="no_tickets",
111
279
  )
112
280
 
281
+ # Check for duplicate ticket indices before proceeding.
282
+ dir_lint_errors = lint_ticket_directory(ticket_dir)
283
+ if dir_lint_errors:
284
+ return self._pause(
285
+ state,
286
+ reason="Duplicate ticket indices detected.",
287
+ reason_details="Errors:\n- " + "\n- ".join(dir_lint_errors),
288
+ reason_code="needs_user_fix",
289
+ )
290
+
113
291
  current_ticket = state.get("current_ticket")
114
292
  current_path: Optional[Path] = (
115
293
  (self._workspace_root / current_ticket)
@@ -266,14 +444,18 @@ class TicketRunner:
266
444
  )
267
445
 
268
446
  previous_ticket_content: Optional[str] = None
269
- try:
270
- if current_path in ticket_paths:
271
- curr_idx = ticket_paths.index(current_path)
272
- if curr_idx > 0:
273
- prev_path = ticket_paths[curr_idx - 1]
274
- previous_ticket_content = prev_path.read_text(encoding="utf-8")
275
- except Exception:
276
- pass
447
+ if self._config.include_previous_ticket_context:
448
+ try:
449
+ if current_path in ticket_paths:
450
+ curr_idx = ticket_paths.index(current_path)
451
+ if curr_idx > 0:
452
+ prev_path = ticket_paths[curr_idx - 1]
453
+ content = prev_path.read_text(encoding="utf-8")
454
+ previous_ticket_content = _truncate_text_by_bytes(
455
+ content, 16384
456
+ )
457
+ except Exception:
458
+ pass
277
459
 
278
460
  prompt = self._build_prompt(
279
461
  ticket_path=current_path,
@@ -333,6 +515,31 @@ class TicketRunner:
333
515
  state["last_agent_id"] = result.agent_id
334
516
  state["last_agent_conversation_id"] = result.conversation_id
335
517
  state["last_agent_turn_id"] = result.turn_id
518
+
519
+ # Check if this is a network error that should be retried
520
+ if _is_network_error(result.error):
521
+ network_retries += 1
522
+ if network_retries <= self._config.max_network_retries:
523
+ state["network_retry"] = {
524
+ "retries": network_retries,
525
+ "last_error": result.error,
526
+ }
527
+ return TicketResult(
528
+ status="continue",
529
+ state=state,
530
+ reason=(
531
+ f"Network error detected (attempt {network_retries}/{self._config.max_network_retries}): {result.error}\n"
532
+ "Retrying automatically..."
533
+ ),
534
+ current_ticket=safe_relpath(current_path, self._workspace_root),
535
+ agent_output=result.text,
536
+ agent_id=result.agent_id,
537
+ agent_conversation_id=result.conversation_id,
538
+ agent_turn_id=result.turn_id,
539
+ )
540
+
541
+ # Not a network error or retries exhausted - pause for user intervention
542
+ state.pop("network_retry", None)
336
543
  return self._pause(
337
544
  state,
338
545
  reason="Agent turn failed. Fix the issue and resume.",
@@ -345,6 +552,8 @@ class TicketRunner:
345
552
  if reply_max_seq > reply_seq:
346
553
  state["reply_seq"] = reply_max_seq
347
554
  state["last_agent_output"] = result.text
555
+ # Clear network retry state on successful turn
556
+ state.pop("network_retry", None)
348
557
  state["last_agent_id"] = result.agent_id
349
558
  state["last_agent_conversation_id"] = result.conversation_id
350
559
  state["last_agent_turn_id"] = result.turn_id
@@ -377,7 +586,11 @@ class TicketRunner:
377
586
  dispatch_seq = int(state.get("dispatch_seq") or 0)
378
587
  current_ticket_id = safe_relpath(current_path, self._workspace_root)
379
588
  dispatch, dispatch_errors = archive_dispatch(
380
- outbox_paths, next_seq=dispatch_seq + 1, ticket_id=current_ticket_id
589
+ outbox_paths,
590
+ next_seq=dispatch_seq + 1,
591
+ ticket_id=current_ticket_id,
592
+ repo_id=self._repo_id,
593
+ run_id=self._run_id,
381
594
  )
382
595
  if dispatch_errors:
383
596
  # Treat as pause: user should fix DISPATCH.md frontmatter. Keep outbox
@@ -429,6 +642,26 @@ class TicketRunner:
429
642
  if turn_summary is not None:
430
643
  state["dispatch_seq"] = turn_summary.seq
431
644
 
645
+ # Persist per-turn diff stats in FlowStore as a structured event
646
+ # instead of embedding them into DISPATCH.md metadata.
647
+ if emit_event is not None and isinstance(turn_diff_stats, dict):
648
+ try:
649
+ emit_event(
650
+ FlowEventType.DIFF_UPDATED,
651
+ {
652
+ "ticket_id": current_ticket_id,
653
+ "dispatch_seq": turn_summary.seq,
654
+ "insertions": int(turn_diff_stats.get("insertions") or 0),
655
+ "deletions": int(turn_diff_stats.get("deletions") or 0),
656
+ "files_changed": int(
657
+ turn_diff_stats.get("files_changed") or 0
658
+ ),
659
+ },
660
+ )
661
+ except Exception:
662
+ # Best-effort; do not block ticket execution on event emission.
663
+ pass
664
+
432
665
  # Post-turn: ticket frontmatter must remain valid.
433
666
  updated_fm, fm_errors = self._recheck_ticket_frontmatter(current_path)
434
667
  if fm_errors:
@@ -751,68 +984,41 @@ class TicketRunner:
751
984
  outbox_paths.dispatch_path, self._workspace_root
752
985
  )
753
986
 
754
- header = (
755
- "You are running inside Codex AutoRunner (CAR) in a ticket-based workflow.\n"
756
- "Complete the current ticket by making changes in the repo.\n\n"
757
- "How to operate within CAR:\n"
758
- f"- Current ticket file: {rel_ticket}\n"
759
- "- Ticket completion is controlled by YAML frontmatter: set 'done: true' when finished.\n"
760
- "- To message the user, optionally write attachments first to the dispatch directory, then write DISPATCH.md last.\n"
761
- f" - Dispatch directory: {rel_dispatch_dir}\n"
762
- f" - DISPATCH.md path: {rel_dispatch_path}\n"
763
- " DISPATCH.md frontmatter supports: mode: notify|pause (pause will wait for a user response; notify will continue without waiting for user input).\n"
764
- " Example: `---\\nmode: pause\\n---\\nNeed clarification on X before proceeding.`\n"
765
- "- No need to dispatch a final notification to the user; your final turn summary is dispatched automatically. Only dispatch if you want something important to stand out to the user, or if you need their input (pause).\n"
766
- "- If you are completely blocked (missing info, unclear requirements, external dependency), dispatch with mode: pause immediately rather than guessing.\n"
767
- "- You may create new tickets only if blocking the current SPEC or if the current ticket is too ambiguous and you want to scope it out further. Keep tickets minimal and avoid scope creep.\n"
768
- "- Avoid stubs, TODOs, or placeholder logic. Either implement fully, create a follow-up ticket, or pause for user input.\n"
769
- "- Only set 'done: true' when the ticket is truly complete. If partially done, update the ticket body with progress so the next agent can continue.\n"
770
- "- Each ticket is handled by a new series of agents in a loop, where each new agent gets the context of the previous agent. No context is shared across tickets EXCEPT via the workspace files.\n"
771
- "- You may update or add new workspace docs and add files under `.codex-autorunner/workspace/` to leave context for future agents.\n"
772
- "- active_context and spec are ALWAYS passed to each agent and should be considered the most precious context.\n"
773
- "- decisions.md: can contain conditional decision context that many only be relevant to some tickets.\n"
774
- "- If you create new documents that future agents should reference, modify their tickets and leave a pointer to your new files.\n"
775
- "- All files and folders under `.codex-autorunner/workspace/` are viewable and editable by the user. If you need the user's input on something, make sure it's in the workspace including copies of any artifacts they should review.\n"
776
- "- Do NOT add any files under `.codex-autorunner/` to git unless they are already tracked and not gitignored."
777
- )
778
-
779
987
  checkpoint_block = ""
780
988
  if last_checkpoint_error:
781
989
  checkpoint_block = (
782
- "\n\n---\n\n"
990
+ "<CAR_CHECKPOINT_WARNING>\n"
783
991
  "WARNING: The previous checkpoint git commit failed (often due to pre-commit hooks).\n"
784
992
  "Resolve this before proceeding, or future turns may fail to checkpoint.\n\n"
785
993
  "Checkpoint error:\n"
786
994
  f"{last_checkpoint_error}\n"
995
+ "</CAR_CHECKPOINT_WARNING>"
787
996
  )
788
997
 
789
998
  commit_block = ""
790
999
  if commit_required:
791
1000
  attempts_remaining = max(commit_max_attempts - commit_attempt + 1, 0)
792
1001
  commit_block = (
793
- "\n\n---\n\n"
794
- "ACTION REQUIRED: Commit your changes, ensuring any pre-commit hooks pass.\n"
795
- "- Use a meaningful commit message that matches what you implemented.\n"
796
- "- If hooks fail, fix the underlying issues and retry the commit.\n"
797
- f"- Attempts remaining before user intervention: {attempts_remaining}\n"
1002
+ "<CAR_COMMIT_REQUIRED>\n"
1003
+ "ACTION REQUIRED: The repo is dirty but the ticket is marked done.\n"
1004
+ "Commit your changes (ensuring any pre-commit hooks pass) so the flow can advance.\n\n"
1005
+ f"Attempts remaining before user intervention: {attempts_remaining}\n"
1006
+ "</CAR_COMMIT_REQUIRED>"
798
1007
  )
799
1008
 
800
1009
  if lint_errors:
801
1010
  lint_block = (
802
- "\n\nTicket frontmatter lint failed. Fix ONLY the ticket frontmatter to satisfy:\n- "
1011
+ "<CAR_TICKET_FRONTMATTER_LINT_REPAIR>\n"
1012
+ "Ticket frontmatter lint failed. Fix ONLY the ticket YAML frontmatter to satisfy:\n- "
803
1013
  + "\n- ".join(lint_errors)
804
- + "\n"
1014
+ + "\n</CAR_TICKET_FRONTMATTER_LINT_REPAIR>"
805
1015
  )
806
1016
  else:
807
1017
  lint_block = ""
808
1018
 
809
1019
  reply_block = ""
810
1020
  if reply_context:
811
- reply_block = (
812
- "\n\n---\n\nHUMAN REPLIES (from reply_history; newest since last turn):\n"
813
- + reply_context
814
- + "\n"
815
- )
1021
+ reply_block = reply_context
816
1022
 
817
1023
  workspace_block = ""
818
1024
  workspace_docs: list[tuple[str, str, str]] = []
@@ -844,38 +1050,113 @@ class TicketRunner:
844
1050
  blocks = ["Workspace docs (truncated; skip if not relevant):"]
845
1051
  for label, rel, body in workspace_docs:
846
1052
  blocks.append(f"{label} [{rel}]:\n{body}")
847
- workspace_block = "\n\n---\n\n" + "\n\n".join(blocks) + "\n"
1053
+ workspace_block = "\n\n".join(blocks)
848
1054
 
849
1055
  prev_ticket_block = ""
850
1056
  if previous_ticket_content:
851
1057
  prev_ticket_block = (
852
- "\n\n---\n\n"
853
- "PREVIOUS TICKET CONTEXT (for reference only; do not edit):\n"
1058
+ "PREVIOUS TICKET CONTEXT (truncated to 16KB; for reference only; do not edit):\n"
1059
+ "Cross-ticket context should flow through workspace docs (active_context.md, decisions.md, spec.md) "
1060
+ "rather than implicit previous ticket content. This is included only for legacy compatibility.\n"
854
1061
  + previous_ticket_content
855
- + "\n"
856
1062
  )
857
1063
 
1064
+ ticket_raw_content = ticket_path.read_text(encoding="utf-8")
858
1065
  ticket_block = (
859
- "\n\n---\n\n"
860
- "TICKET CONTENT (edit this file to track progress; update frontmatter.done when complete):\n"
1066
+ "<CAR_CURRENT_TICKET_FILE>\n"
861
1067
  f"PATH: {rel_ticket}\n"
862
- "\n" + ticket_path.read_text(encoding="utf-8")
1068
+ "<TICKET_MARKDOWN>\n"
1069
+ f"{ticket_raw_content}\n"
1070
+ "</TICKET_MARKDOWN>\n"
1071
+ "</CAR_CURRENT_TICKET_FILE>"
863
1072
  )
864
1073
 
865
1074
  prev_block = ""
866
1075
  if last_agent_output:
867
- prev_block = (
868
- "\n\n---\n\nPREVIOUS AGENT OUTPUT (same ticket):\n" + last_agent_output
1076
+ prev_block = last_agent_output
1077
+
1078
+ sections = {
1079
+ "prev_block": prev_block,
1080
+ "prev_ticket_block": prev_ticket_block,
1081
+ "workspace_block": workspace_block,
1082
+ "reply_block": reply_block,
1083
+ "ticket_block": ticket_block,
1084
+ }
1085
+
1086
+ def render() -> str:
1087
+ return (
1088
+ "<CAR_TICKET_FLOW_PROMPT>\n\n"
1089
+ "<CAR_TICKET_FLOW_INSTRUCTIONS>\n"
1090
+ "You are running inside Codex Autorunner (CAR) in a ticket-based workflow.\n\n"
1091
+ "Your job in this turn:\n"
1092
+ "- Read the current ticket file.\n"
1093
+ "- Make the required repo changes.\n"
1094
+ "- Update the ticket file to reflect progress.\n"
1095
+ "- Set `done: true` in the ticket YAML frontmatter only when the ticket is truly complete.\n\n"
1096
+ "CAR orientation (80/20):\n"
1097
+ "- `.codex-autorunner/tickets/` is the queue that drives the flow (files named `TICKET-###*.md`, processed in numeric order).\n"
1098
+ "- `.codex-autorunner/workspace/` holds durable context shared across ticket turns (especially `active_context.md` and `spec.md`).\n"
1099
+ "- `.codex-autorunner/ABOUT_CAR.md` is the repo-local briefing (what CAR auto-generates + helper scripts) if you need operational details.\n\n"
1100
+ "Communicating with the user (optional):\n"
1101
+ "- To send a message or request input, write to the dispatch directory:\n"
1102
+ " 1) write any attachments to the dispatch directory\n"
1103
+ " 2) write `DISPATCH.md` last\n"
1104
+ "- `DISPATCH.md` YAML supports `mode: notify|pause`.\n"
1105
+ " - `pause` waits for user input; `notify` continues without waiting.\n"
1106
+ " - Example:\n"
1107
+ " ---\n"
1108
+ " mode: pause\n"
1109
+ " ---\n"
1110
+ " Need clarification on X before proceeding.\n"
1111
+ "- You do not need a “final” dispatch when you finish; the runner will archive your turn output automatically. Dispatch only if you want something to stand out or you need user input.\n\n"
1112
+ "If blocked:\n"
1113
+ "- Dispatch with `mode: pause` rather than guessing.\n\n"
1114
+ "Creating follow-up tickets (optional):\n"
1115
+ "- New tickets live under `.codex-autorunner/tickets/` and follow the `TICKET-###*.md` naming pattern.\n"
1116
+ "- If present, `.codex-autorunner/bin/ticket_tool.py` can create/insert/move tickets; `.codex-autorunner/bin/lint_tickets.py` lints ticket frontmatter (see `.codex-autorunner/ABOUT_CAR.md`).\n"
1117
+ "Using ticket templates (optional):\n"
1118
+ "- If you need a standard ticket pattern, prefer: `car templates fetch <repo_id>:<path>[@<ref>]`\n"
1119
+ " - Trusted repos skip scanning; untrusted repos are scanned (cached by blob SHA).\n\n"
1120
+ "Workspace docs:\n"
1121
+ "- You may update or add context under `.codex-autorunner/workspace/` so future ticket turns have durable context.\n"
1122
+ "- Prefer referencing these docs instead of creating duplicate “shadow” docs elsewhere.\n\n"
1123
+ "Repo hygiene:\n"
1124
+ "- Do not add new `.codex-autorunner/` artifacts to git unless they are already tracked.\n"
1125
+ "</CAR_TICKET_FLOW_INSTRUCTIONS>\n\n"
1126
+ "<CAR_RUNTIME_PATHS>\n"
1127
+ f"Current ticket file: {rel_ticket}\n"
1128
+ f"Dispatch directory: {rel_dispatch_dir}\n"
1129
+ f"DISPATCH.md path: {rel_dispatch_path}\n"
1130
+ "</CAR_RUNTIME_PATHS>\n\n"
1131
+ f"{checkpoint_block}\n\n"
1132
+ f"{commit_block}\n\n"
1133
+ f"{lint_block}\n\n"
1134
+ "<CAR_WORKSPACE_DOCS>\n"
1135
+ f"{sections['workspace_block']}\n"
1136
+ "</CAR_WORKSPACE_DOCS>\n\n"
1137
+ "<CAR_HUMAN_REPLIES>\n"
1138
+ f"{sections['reply_block']}\n"
1139
+ "</CAR_HUMAN_REPLIES>\n\n"
1140
+ "<CAR_PREVIOUS_TICKET_REFERENCE>\n"
1141
+ f"{sections['prev_ticket_block']}\n"
1142
+ "</CAR_PREVIOUS_TICKET_REFERENCE>\n\n"
1143
+ f"{sections['ticket_block']}\n\n"
1144
+ "<CAR_PREVIOUS_AGENT_OUTPUT>\n"
1145
+ f"{sections['prev_block']}\n"
1146
+ "</CAR_PREVIOUS_AGENT_OUTPUT>\n\n"
1147
+ "</CAR_TICKET_FLOW_PROMPT>"
869
1148
  )
870
1149
 
871
- return (
872
- header
873
- + checkpoint_block
874
- + commit_block
875
- + lint_block
876
- + workspace_block
877
- + reply_block
878
- + prev_ticket_block
879
- + ticket_block
880
- + prev_block
1150
+ prompt = _shrink_prompt(
1151
+ max_bytes=self._config.prompt_max_bytes,
1152
+ render=render,
1153
+ sections=sections,
1154
+ order=[
1155
+ "prev_block",
1156
+ "prev_ticket_block",
1157
+ "reply_block",
1158
+ "workspace_block",
1159
+ "ticket_block",
1160
+ ],
881
1161
  )
1162
+ return prompt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-autorunner
3
- Version: 1.1.0
3
+ Version: 1.2.1
4
4
  Summary: Codex autorunner CLI per DESIGN-V1
5
5
  Author: Codex
6
6
  License: MIT License
@@ -85,7 +85,7 @@ The philosophy behind CAR is to let the agents do what they do best, and get out
85
85
  CAR treats tickets as the control plane and models as the execution layer. This means that we rely on agents to follow the instructions written in the tickets. If you use a sufficiently weak model, CAR may not work well for you. CAR is an amplifier for agent capabilities. Agents who like to scope creep (create too many new tickets) or reward hack (mark a ticket as done despite it being incomplete) are not a good fit for CAR.
86
86
 
87
87
  ## Interaction patterns
88
- CAR's core is a set of python functions surfaced as a CLI, operating on a file system. There are current 2 UIs built on top of this core.
88
+ CAR's core is a set of python functions surfaced as a CLI, operating on a file system. There are current 3 UIs built on top of this core.
89
89
 
90
90
  ### Web UI
91
91
  The web UI is the main control plane for CAR. From here you can set up new repositories or clone existing ones, chat with agents using their TUI, and run the ticket autorunner. There are many quality-of-life features like Whisper integration, editing documents by chatting with AI (useful for mobile), viewing usage analytics, and much more. The Web UI is the most full featured user-facing interface and a good starting point for trying out CAR.
@@ -95,30 +95,23 @@ I recommend serving the web UI over Tailscale. There is an auth token option but
95
95
  ### Telegram
96
96
  Telegram is the "on-the-go" and notification hub for CAR. From here you can kick off and monitor existing tickets, set up new tickets, and chat with agents. Your primary UX here is asking the agent to do things for you rather than you doing it yourself like you would on the web UI. This is great for on-the-go work, but it doesn't have full feature parity with the web UI.
97
97
 
98
+ ### Project Manager Agent
99
+ The project manager agent (PMA) is a way to use an AI agent to run CAR. For example instead of making/editing tickets in the UI, you can ask the PMA to do it for you. Instead of starting a ticket flow, the PMA can babysit it for you. The PMA can be accessed in the web and in Telegram, and also uses the CAR CLI directly.
100
+
98
101
  ## Quickstart
99
102
 
100
103
  The fastest way to get started is to pass [this setup guide](docs/AGENT_SETUP_GUIDE.md) to your favorite AI agent. The agent will walk you through installation and configuration interactively based on your environment.
101
104
 
102
- **TL;DR for the impatient:**
105
+ ### From source (repo checkout)
103
106
 
104
- # Install
105
- ```
106
- pipx install codex-autorunner
107
- ```
108
- # Initialize in your repo
109
- ```
110
- cd /path/to/your/repo
111
- car init
112
- ```
113
- # Verify setup
114
- ```
115
- car doctor
116
- ```
117
- # Create a ticket and run
118
- ```
119
- car run
107
+ If you're working from a fresh clone of this repo, you can run the repo-local CLI shim:
108
+
109
+ ```bash
110
+ ./car --help
120
111
  ```
121
112
 
113
+ The shim will try `PYTHONPATH=src` first and, if dependencies are missing, will bootstrap a local `.venv` and install CAR.
114
+
122
115
  ## Supported models
123
116
  CAR currently supports:
124
117
  - Codex
@@ -150,5 +143,8 @@ On the go? The web UI is mobile responsive, or if you prefer you can type or voi
150
143
  ![CAR Telegram Media Voice Screenshot](docs/screenshots/telegram-media-voice.PNG)
151
144
  ![CAR Telegram Media Image Screenshot](docs/screenshots/telegram-media-image.PNG)
152
145
 
146
+ Don't want to use the product directly? Delegate work to the PMA.
147
+ ![CAR Telegram PMA Screenshot](docs/screenshots/telegram-pma.png)
148
+
153
149
  ## Star history
154
150
  [![Star History Chart](https://api.star-history.com/svg?repos=Git-on-my-level/codex-autorunner&type=Date)](https://star-history.com/#Git-on-my-level/codex-autorunner&Date)