codex-autorunner 1.0.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/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- 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/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- 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 +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -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/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- 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 +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- 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 +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -5,12 +5,12 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any, Callable, Optional
|
|
6
6
|
|
|
7
7
|
from ..core.flows.models import FlowEventType
|
|
8
|
-
from ..core.git_utils import run_git
|
|
8
|
+
from ..core.git_utils import git_diff_stats, run_git
|
|
9
9
|
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,10 +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.",
|
|
231
|
+
reason_code="max_turns",
|
|
80
232
|
)
|
|
81
233
|
|
|
82
234
|
ticket_dir = self._workspace_root / self._config.ticket_dir
|
|
@@ -97,6 +249,23 @@ class TicketRunner:
|
|
|
97
249
|
run_id=self._run_id,
|
|
98
250
|
)
|
|
99
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
|
+
)
|
|
100
269
|
|
|
101
270
|
ticket_paths = list_ticket_paths(ticket_dir)
|
|
102
271
|
if not ticket_paths:
|
|
@@ -106,6 +275,17 @@ class TicketRunner:
|
|
|
106
275
|
"No tickets found. Create tickets under "
|
|
107
276
|
f"{safe_relpath(ticket_dir, self._workspace_root)} and resume."
|
|
108
277
|
),
|
|
278
|
+
reason_code="no_tickets",
|
|
279
|
+
)
|
|
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",
|
|
109
289
|
)
|
|
110
290
|
|
|
111
291
|
current_ticket = state.get("current_ticket")
|
|
@@ -134,6 +314,16 @@ class TicketRunner:
|
|
|
134
314
|
)
|
|
135
315
|
current_path = next_path
|
|
136
316
|
state["current_ticket"] = safe_relpath(current_path, self._workspace_root)
|
|
317
|
+
# Inform listeners immediately which ticket is about to run so the UI
|
|
318
|
+
# can show the active indicator before the first turn completes.
|
|
319
|
+
if emit_event is not None:
|
|
320
|
+
emit_event(
|
|
321
|
+
FlowEventType.STEP_PROGRESS,
|
|
322
|
+
{
|
|
323
|
+
"message": "Selected ticket",
|
|
324
|
+
"current_ticket": state["current_ticket"],
|
|
325
|
+
},
|
|
326
|
+
)
|
|
137
327
|
# New ticket resets per-ticket state.
|
|
138
328
|
state["ticket_turns"] = 0
|
|
139
329
|
state.pop("last_agent_output", None)
|
|
@@ -148,6 +338,7 @@ class TicketRunner:
|
|
|
148
338
|
state["status"] = "running"
|
|
149
339
|
state.pop("reason", None)
|
|
150
340
|
state.pop("reason_details", None)
|
|
341
|
+
state.pop("reason_code", None)
|
|
151
342
|
|
|
152
343
|
_lint_raw = state.get("lint")
|
|
153
344
|
lint_state: dict[str, Any] = _lint_raw if isinstance(_lint_raw, dict) else {}
|
|
@@ -176,6 +367,7 @@ class TicketRunner:
|
|
|
176
367
|
f"{safe_relpath(current_path, self._workspace_root)}: {exc}"
|
|
177
368
|
),
|
|
178
369
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
370
|
+
reason_code="infra_error",
|
|
179
371
|
)
|
|
180
372
|
|
|
181
373
|
data, _ = parse_markdown_frontmatter(raw)
|
|
@@ -189,6 +381,7 @@ class TicketRunner:
|
|
|
189
381
|
"Fix the ticket frontmatter manually and resume."
|
|
190
382
|
),
|
|
191
383
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
384
|
+
reason_code="needs_user_fix",
|
|
192
385
|
)
|
|
193
386
|
|
|
194
387
|
# Validate agent id unless it is the special user sentinel.
|
|
@@ -205,6 +398,7 @@ class TicketRunner:
|
|
|
205
398
|
f"{safe_relpath(current_path, self._workspace_root)}: {exc}"
|
|
206
399
|
),
|
|
207
400
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
401
|
+
reason_code="needs_user_fix",
|
|
208
402
|
)
|
|
209
403
|
|
|
210
404
|
ticket_doc = type(
|
|
@@ -225,6 +419,7 @@ class TicketRunner:
|
|
|
225
419
|
reason=f"Ticket frontmatter invalid: {safe_relpath(current_path, self._workspace_root)}",
|
|
226
420
|
reason_details="Errors:\n- " + "\n- ".join(ticket_errors),
|
|
227
421
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
422
|
+
reason_code="needs_user_fix",
|
|
228
423
|
)
|
|
229
424
|
|
|
230
425
|
# Built-in manual user ticket.
|
|
@@ -239,6 +434,7 @@ class TicketRunner:
|
|
|
239
434
|
f"{safe_relpath(current_path, self._workspace_root)}"
|
|
240
435
|
),
|
|
241
436
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
437
|
+
reason_code="user_pause",
|
|
242
438
|
)
|
|
243
439
|
|
|
244
440
|
ticket_turns = int(state.get("ticket_turns") or 0)
|
|
@@ -248,14 +444,18 @@ class TicketRunner:
|
|
|
248
444
|
)
|
|
249
445
|
|
|
250
446
|
previous_ticket_content: Optional[str] = None
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
259
459
|
|
|
260
460
|
prompt = self._build_prompt(
|
|
261
461
|
ticket_path=current_path,
|
|
@@ -315,17 +515,45 @@ class TicketRunner:
|
|
|
315
515
|
state["last_agent_id"] = result.agent_id
|
|
316
516
|
state["last_agent_conversation_id"] = result.conversation_id
|
|
317
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)
|
|
318
543
|
return self._pause(
|
|
319
544
|
state,
|
|
320
545
|
reason="Agent turn failed. Fix the issue and resume.",
|
|
321
546
|
reason_details=f"Error: {result.error}",
|
|
322
547
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
548
|
+
reason_code="infra_error",
|
|
323
549
|
)
|
|
324
550
|
|
|
325
551
|
# Mark replies as consumed only after a successful agent turn.
|
|
326
552
|
if reply_max_seq > reply_seq:
|
|
327
553
|
state["reply_seq"] = reply_max_seq
|
|
328
554
|
state["last_agent_output"] = result.text
|
|
555
|
+
# Clear network retry state on successful turn
|
|
556
|
+
state.pop("network_retry", None)
|
|
329
557
|
state["last_agent_id"] = result.agent_id
|
|
330
558
|
state["last_agent_conversation_id"] = result.conversation_id
|
|
331
559
|
state["last_agent_turn_id"] = result.turn_id
|
|
@@ -358,7 +586,11 @@ class TicketRunner:
|
|
|
358
586
|
dispatch_seq = int(state.get("dispatch_seq") or 0)
|
|
359
587
|
current_ticket_id = safe_relpath(current_path, self._workspace_root)
|
|
360
588
|
dispatch, dispatch_errors = archive_dispatch(
|
|
361
|
-
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,
|
|
362
594
|
)
|
|
363
595
|
if dispatch_errors:
|
|
364
596
|
# Treat as pause: user should fix DISPATCH.md frontmatter. Keep outbox
|
|
@@ -369,6 +601,7 @@ class TicketRunner:
|
|
|
369
601
|
reason="Invalid DISPATCH.md frontmatter.",
|
|
370
602
|
reason_details="Errors:\n- " + "\n- ".join(dispatch_errors),
|
|
371
603
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
604
|
+
reason_code="needs_user_fix",
|
|
372
605
|
)
|
|
373
606
|
|
|
374
607
|
if dispatch is not None:
|
|
@@ -378,6 +611,25 @@ class TicketRunner:
|
|
|
378
611
|
# Create turn summary record for the agent's final output.
|
|
379
612
|
# This appears in dispatch history as a distinct "turn summary" entry.
|
|
380
613
|
turn_summary_seq = int(state.get("dispatch_seq") or 0) + 1
|
|
614
|
+
|
|
615
|
+
# Compute diff stats for this turn (changes since head_before_turn).
|
|
616
|
+
# This captures both committed and uncommitted changes made by the agent.
|
|
617
|
+
turn_diff_stats = None
|
|
618
|
+
try:
|
|
619
|
+
if head_before_turn:
|
|
620
|
+
# Compare current state (HEAD + working tree) against pre-turn commit
|
|
621
|
+
turn_diff_stats = git_diff_stats(
|
|
622
|
+
self._workspace_root, from_ref=head_before_turn
|
|
623
|
+
)
|
|
624
|
+
else:
|
|
625
|
+
# No reference commit; show all uncommitted changes
|
|
626
|
+
turn_diff_stats = git_diff_stats(
|
|
627
|
+
self._workspace_root, from_ref=None, include_staged=True
|
|
628
|
+
)
|
|
629
|
+
except Exception:
|
|
630
|
+
# Best-effort; don't block on stats computation errors
|
|
631
|
+
turn_diff_stats = None
|
|
632
|
+
|
|
381
633
|
turn_summary, turn_summary_errors = create_turn_summary(
|
|
382
634
|
outbox_paths,
|
|
383
635
|
next_seq=turn_summary_seq,
|
|
@@ -385,10 +637,31 @@ class TicketRunner:
|
|
|
385
637
|
ticket_id=current_ticket_id,
|
|
386
638
|
agent_id=result.agent_id,
|
|
387
639
|
turn_number=total_turns,
|
|
640
|
+
diff_stats=turn_diff_stats,
|
|
388
641
|
)
|
|
389
642
|
if turn_summary is not None:
|
|
390
643
|
state["dispatch_seq"] = turn_summary.seq
|
|
391
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
|
+
|
|
392
665
|
# Post-turn: ticket frontmatter must remain valid.
|
|
393
666
|
updated_fm, fm_errors = self._recheck_ticket_frontmatter(current_path)
|
|
394
667
|
if fm_errors:
|
|
@@ -402,6 +675,7 @@ class TicketRunner:
|
|
|
402
675
|
"Errors:\n- " + "\n- ".join(fm_errors)
|
|
403
676
|
),
|
|
404
677
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
678
|
+
reason_code="needs_user_fix",
|
|
405
679
|
)
|
|
406
680
|
|
|
407
681
|
state["lint"] = {
|
|
@@ -439,7 +713,9 @@ class TicketRunner:
|
|
|
439
713
|
reason = dispatch.dispatch.title or "Paused for user input."
|
|
440
714
|
if checkpoint_error:
|
|
441
715
|
reason += f"\n\nNote: checkpoint commit failed: {checkpoint_error}"
|
|
716
|
+
state["status"] = "paused"
|
|
442
717
|
state["reason"] = reason
|
|
718
|
+
state["reason_code"] = "user_pause"
|
|
443
719
|
return TicketResult(
|
|
444
720
|
status="paused",
|
|
445
721
|
state=state,
|
|
@@ -495,6 +771,7 @@ class TicketRunner:
|
|
|
495
771
|
),
|
|
496
772
|
reason_details="".join(details_parts),
|
|
497
773
|
current_ticket=safe_relpath(current_path, self._workspace_root),
|
|
774
|
+
reason_code="needs_user_fix",
|
|
498
775
|
)
|
|
499
776
|
|
|
500
777
|
return TicketResult(
|
|
@@ -583,12 +860,14 @@ class TicketRunner:
|
|
|
583
860
|
state: dict[str, Any],
|
|
584
861
|
*,
|
|
585
862
|
reason: str,
|
|
863
|
+
reason_code: str = "needs_user_fix",
|
|
586
864
|
reason_details: Optional[str] = None,
|
|
587
865
|
current_ticket: Optional[str] = None,
|
|
588
866
|
) -> TicketResult:
|
|
589
867
|
state = dict(state)
|
|
590
868
|
state["status"] = "paused"
|
|
591
869
|
state["reason"] = reason
|
|
870
|
+
state["reason_code"] = reason_code
|
|
592
871
|
if reason_details:
|
|
593
872
|
state["reason_details"] = reason_details
|
|
594
873
|
else:
|
|
@@ -705,56 +984,41 @@ class TicketRunner:
|
|
|
705
984
|
outbox_paths.dispatch_path, self._workspace_root
|
|
706
985
|
)
|
|
707
986
|
|
|
708
|
-
header = (
|
|
709
|
-
"You are running inside Codex AutoRunner (CAR) in a ticket-based workflow.\n"
|
|
710
|
-
"Complete the current ticket by making changes in the repo and updating the ticket file.\n\n"
|
|
711
|
-
"Key rules:\n"
|
|
712
|
-
f"- Current ticket file: {rel_ticket}\n"
|
|
713
|
-
"- Ticket completion is controlled by YAML frontmatter: set 'done: true' when finished.\n"
|
|
714
|
-
"- To message the user, optionally write attachments first to the dispatch directory, then write DISPATCH.md last.\n"
|
|
715
|
-
f" - Dispatch directory: {rel_dispatch_dir}\n"
|
|
716
|
-
f" - DISPATCH.md path: {rel_dispatch_path}\n"
|
|
717
|
-
" DISPATCH.md frontmatter supports: mode: notify|pause (pause will wait for a user response; notify will continue without waiting for user input).\n"
|
|
718
|
-
"- Keep tickets minimal and avoid scope creep. You may create new tickets only if blocking the current SPEC.\n"
|
|
719
|
-
)
|
|
720
|
-
|
|
721
987
|
checkpoint_block = ""
|
|
722
988
|
if last_checkpoint_error:
|
|
723
989
|
checkpoint_block = (
|
|
724
|
-
"
|
|
990
|
+
"<CAR_CHECKPOINT_WARNING>\n"
|
|
725
991
|
"WARNING: The previous checkpoint git commit failed (often due to pre-commit hooks).\n"
|
|
726
992
|
"Resolve this before proceeding, or future turns may fail to checkpoint.\n\n"
|
|
727
993
|
"Checkpoint error:\n"
|
|
728
994
|
f"{last_checkpoint_error}\n"
|
|
995
|
+
"</CAR_CHECKPOINT_WARNING>"
|
|
729
996
|
)
|
|
730
997
|
|
|
731
998
|
commit_block = ""
|
|
732
999
|
if commit_required:
|
|
733
1000
|
attempts_remaining = max(commit_max_attempts - commit_attempt + 1, 0)
|
|
734
1001
|
commit_block = (
|
|
735
|
-
"
|
|
736
|
-
"ACTION REQUIRED:
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
|
|
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>"
|
|
740
1007
|
)
|
|
741
1008
|
|
|
742
1009
|
if lint_errors:
|
|
743
1010
|
lint_block = (
|
|
744
|
-
"
|
|
1011
|
+
"<CAR_TICKET_FRONTMATTER_LINT_REPAIR>\n"
|
|
1012
|
+
"Ticket frontmatter lint failed. Fix ONLY the ticket YAML frontmatter to satisfy:\n- "
|
|
745
1013
|
+ "\n- ".join(lint_errors)
|
|
746
|
-
+ "\n"
|
|
1014
|
+
+ "\n</CAR_TICKET_FRONTMATTER_LINT_REPAIR>"
|
|
747
1015
|
)
|
|
748
1016
|
else:
|
|
749
1017
|
lint_block = ""
|
|
750
1018
|
|
|
751
1019
|
reply_block = ""
|
|
752
1020
|
if reply_context:
|
|
753
|
-
reply_block =
|
|
754
|
-
"\n\n---\n\nHUMAN REPLIES (from reply_history; newest since last turn):\n"
|
|
755
|
-
+ reply_context
|
|
756
|
-
+ "\n"
|
|
757
|
-
)
|
|
1021
|
+
reply_block = reply_context
|
|
758
1022
|
|
|
759
1023
|
workspace_block = ""
|
|
760
1024
|
workspace_docs: list[tuple[str, str, str]] = []
|
|
@@ -786,38 +1050,113 @@ class TicketRunner:
|
|
|
786
1050
|
blocks = ["Workspace docs (truncated; skip if not relevant):"]
|
|
787
1051
|
for label, rel, body in workspace_docs:
|
|
788
1052
|
blocks.append(f"{label} [{rel}]:\n{body}")
|
|
789
|
-
workspace_block = "\n\n
|
|
1053
|
+
workspace_block = "\n\n".join(blocks)
|
|
790
1054
|
|
|
791
1055
|
prev_ticket_block = ""
|
|
792
1056
|
if previous_ticket_content:
|
|
793
1057
|
prev_ticket_block = (
|
|
794
|
-
"
|
|
795
|
-
"
|
|
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"
|
|
796
1061
|
+ previous_ticket_content
|
|
797
|
-
+ "\n"
|
|
798
1062
|
)
|
|
799
1063
|
|
|
1064
|
+
ticket_raw_content = ticket_path.read_text(encoding="utf-8")
|
|
800
1065
|
ticket_block = (
|
|
801
|
-
"
|
|
802
|
-
"TICKET CONTENT (edit this file to track progress; update frontmatter.done when complete):\n"
|
|
1066
|
+
"<CAR_CURRENT_TICKET_FILE>\n"
|
|
803
1067
|
f"PATH: {rel_ticket}\n"
|
|
804
|
-
"
|
|
1068
|
+
"<TICKET_MARKDOWN>\n"
|
|
1069
|
+
f"{ticket_raw_content}\n"
|
|
1070
|
+
"</TICKET_MARKDOWN>\n"
|
|
1071
|
+
"</CAR_CURRENT_TICKET_FILE>"
|
|
805
1072
|
)
|
|
806
1073
|
|
|
807
1074
|
prev_block = ""
|
|
808
1075
|
if last_agent_output:
|
|
809
|
-
prev_block =
|
|
810
|
-
|
|
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>"
|
|
811
1148
|
)
|
|
812
1149
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
+
],
|
|
823
1161
|
)
|
|
1162
|
+
return prompt
|