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.
- 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 +124 -11
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +238 -3
- codex_autorunner/core/context_awareness.py +39 -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 +683 -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/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- 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 +34 -3
- 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/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/constants.js +1 -1
- 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 +288 -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 +9141 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminalManager.js +22 -3
- 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/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +297 -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 +81 -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.1.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
- 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.1.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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="
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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,
|
|
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
|
-
"
|
|
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
|
-
"
|
|
794
|
-
"ACTION REQUIRED:
|
|
795
|
-
"
|
|
796
|
-
"
|
|
797
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
853
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
105
|
+
### From source (repo checkout)
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|

|
|
151
144
|

|
|
152
145
|
|
|
146
|
+
Don't want to use the product directly? Delegate work to the PMA.
|
|
147
|
+

|
|
148
|
+
|
|
153
149
|
## Star history
|
|
154
150
|
[](https://star-history.com/#Git-on-my-level/codex-autorunner&Date)
|