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,7 +5,8 @@ from dataclasses import dataclass
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Callable, Optional, cast
|
|
7
7
|
|
|
8
|
-
from ..agents.opencode.
|
|
8
|
+
from ..agents.opencode.constants import DEFAULT_TICKET_MODEL
|
|
9
|
+
from ..agents.opencode.runtime import collect_opencode_output, split_model_id
|
|
9
10
|
from ..agents.opencode.supervisor import OpenCodeSupervisor
|
|
10
11
|
from ..core.config import RepoConfig
|
|
11
12
|
from ..core.flows.models import FlowEventType
|
|
@@ -29,6 +30,10 @@ class AgentTurnRequest:
|
|
|
29
30
|
options: Optional[dict[str, Any]] = None
|
|
30
31
|
# Optional flow event emitter (for live streaming).
|
|
31
32
|
emit_event: Optional[EmitEventFn] = None
|
|
33
|
+
# Optional list of additional messages to send in the same turn.
|
|
34
|
+
# Each message is a dict with a "text" field. Agents that support
|
|
35
|
+
# multiple messages will receive all of them; others may queue them.
|
|
36
|
+
additional_messages: Optional[list[dict[str, Any]]] = None
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
@dataclass(frozen=True)
|
|
@@ -178,6 +183,7 @@ class AgentPool:
|
|
|
178
183
|
max_handles=app_server_cfg.max_handles,
|
|
179
184
|
idle_ttl_seconds=app_server_cfg.idle_ttl_seconds,
|
|
180
185
|
session_stall_timeout_seconds=self._config.opencode.session_stall_timeout_seconds,
|
|
186
|
+
max_text_chars=self._config.opencode.max_text_chars,
|
|
181
187
|
base_env=None,
|
|
182
188
|
subagent_models=subagent_models,
|
|
183
189
|
)
|
|
@@ -185,8 +191,8 @@ class AgentPool:
|
|
|
185
191
|
raise RuntimeError(
|
|
186
192
|
"OpenCode supervisor unavailable (missing opencode command/binary)."
|
|
187
193
|
)
|
|
188
|
-
self._opencode_supervisor = supervisor
|
|
189
|
-
return
|
|
194
|
+
self._opencode_supervisor = cast(OpenCodeSupervisor, supervisor)
|
|
195
|
+
return self._opencode_supervisor
|
|
190
196
|
|
|
191
197
|
async def close(self) -> None:
|
|
192
198
|
if self._app_server_supervisor is not None:
|
|
@@ -248,6 +254,19 @@ class AgentPool:
|
|
|
248
254
|
turn_kwargs["model"] = req.options["model"]
|
|
249
255
|
if req.options.get("reasoning"):
|
|
250
256
|
turn_kwargs["effort"] = req.options["reasoning"]
|
|
257
|
+
|
|
258
|
+
# Build input items - main prompt plus any additional messages
|
|
259
|
+
input_items: Optional[list[dict[str, Any]]] = None
|
|
260
|
+
if req.additional_messages:
|
|
261
|
+
input_items = [{"type": "text", "text": req.prompt}]
|
|
262
|
+
for msg in req.additional_messages:
|
|
263
|
+
if isinstance(msg, dict):
|
|
264
|
+
text = msg.get("text", "")
|
|
265
|
+
if text and text.strip():
|
|
266
|
+
input_items.append({"type": "text", "text": text})
|
|
267
|
+
if input_items:
|
|
268
|
+
turn_kwargs["input_items"] = input_items
|
|
269
|
+
|
|
251
270
|
turn_handle = await client.turn_start(thread_id, req.prompt, **turn_kwargs)
|
|
252
271
|
if req.emit_event is not None:
|
|
253
272
|
self._active_emitters[turn_handle.turn_id] = req.emit_event
|
|
@@ -274,6 +293,24 @@ class AgentPool:
|
|
|
274
293
|
client = handle
|
|
275
294
|
directory = str(req.workspace_root)
|
|
276
295
|
|
|
296
|
+
options = req.options if isinstance(req.options, dict) else {}
|
|
297
|
+
model_raw = options.get("model")
|
|
298
|
+
model_payload = None
|
|
299
|
+
if isinstance(model_raw, dict):
|
|
300
|
+
provider_id = model_raw.get("providerID") or model_raw.get("providerId")
|
|
301
|
+
model_id = model_raw.get("modelID") or model_raw.get("modelId")
|
|
302
|
+
if provider_id and model_id:
|
|
303
|
+
model_payload = {"providerID": provider_id, "modelID": model_id}
|
|
304
|
+
elif isinstance(model_raw, str) and model_raw.strip():
|
|
305
|
+
model_payload = split_model_id(model_raw.strip())
|
|
306
|
+
if model_payload is None:
|
|
307
|
+
model_payload = split_model_id(DEFAULT_TICKET_MODEL)
|
|
308
|
+
|
|
309
|
+
variant = None
|
|
310
|
+
reasoning_raw = options.get("reasoning")
|
|
311
|
+
if isinstance(reasoning_raw, str) and reasoning_raw.strip():
|
|
312
|
+
variant = reasoning_raw.strip()
|
|
313
|
+
|
|
277
314
|
session_id = req.conversation_id
|
|
278
315
|
if not session_id:
|
|
279
316
|
created = await client.create_session(title="ticket", directory=directory)
|
|
@@ -281,7 +318,18 @@ class AgentPool:
|
|
|
281
318
|
if not session_id:
|
|
282
319
|
raise RuntimeError("OpenCode create_session returned no session id")
|
|
283
320
|
|
|
284
|
-
|
|
321
|
+
# Send main prompt and any additional messages
|
|
322
|
+
# OpenCode processes messages sequentially; agents that queue will handle them
|
|
323
|
+
prompt_response = await client.prompt_async(
|
|
324
|
+
session_id, message=req.prompt, model=model_payload, variant=variant
|
|
325
|
+
)
|
|
326
|
+
if req.additional_messages:
|
|
327
|
+
for msg in req.additional_messages:
|
|
328
|
+
text = msg.get("text", "") if isinstance(msg, dict) else ""
|
|
329
|
+
if text and text.strip():
|
|
330
|
+
await client.prompt_async(
|
|
331
|
+
session_id, message=text, model=model_payload, variant=variant
|
|
332
|
+
)
|
|
285
333
|
|
|
286
334
|
import uuid
|
|
287
335
|
|
|
@@ -340,6 +388,7 @@ class AgentPool:
|
|
|
340
388
|
client,
|
|
341
389
|
session_id=session_id,
|
|
342
390
|
workspace_path=directory,
|
|
391
|
+
model_payload=model_payload,
|
|
343
392
|
part_handler=_part_handler if req.emit_event is not None else None,
|
|
344
393
|
)
|
|
345
394
|
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from pathlib import Path
|
|
3
|
+
from pathlib import Path, PurePosixPath
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from .frontmatter import parse_markdown_frontmatter
|
|
8
|
-
from .lint import lint_ticket_frontmatter
|
|
7
|
+
from .lint import lint_ticket_frontmatter, parse_ticket_index
|
|
9
8
|
from .models import TicketDoc, TicketFrontmatter
|
|
10
9
|
|
|
11
|
-
_TICKET_NAME_RE = re.compile(r"^TICKET-(\d+)\.md$", re.IGNORECASE)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def parse_ticket_index(name: str) -> Optional[int]:
|
|
15
|
-
match = _TICKET_NAME_RE.match(name)
|
|
16
|
-
if not match:
|
|
17
|
-
return None
|
|
18
|
-
try:
|
|
19
|
-
return int(match.group(1))
|
|
20
|
-
except ValueError:
|
|
21
|
-
return None
|
|
22
|
-
|
|
23
10
|
|
|
24
11
|
def list_ticket_paths(ticket_dir: Path) -> list[Path]:
|
|
25
12
|
if not ticket_dir.exists() or not ticket_dir.is_dir():
|
|
@@ -51,11 +38,14 @@ def read_ticket(path: Path) -> tuple[Optional[TicketDoc], list[str]]:
|
|
|
51
38
|
data, body = parse_markdown_frontmatter(raw)
|
|
52
39
|
idx = parse_ticket_index(path.name)
|
|
53
40
|
if idx is None:
|
|
54
|
-
return None, [
|
|
41
|
+
return None, [
|
|
42
|
+
"Invalid ticket filename; expected TICKET-<number>[suffix].md (e.g. TICKET-001-foo.md)"
|
|
43
|
+
]
|
|
55
44
|
|
|
56
45
|
frontmatter, errors = lint_ticket_frontmatter(data)
|
|
57
46
|
if errors:
|
|
58
47
|
return None, errors
|
|
48
|
+
assert frontmatter is not None
|
|
59
49
|
return TicketDoc(path=path, index=idx, frontmatter=frontmatter, body=body), []
|
|
60
50
|
|
|
61
51
|
|
|
@@ -83,3 +73,34 @@ def safe_relpath(path: Path, root: Path) -> str:
|
|
|
83
73
|
return str(path.relative_to(root))
|
|
84
74
|
except ValueError:
|
|
85
75
|
return str(path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def normalize_ticket_dir(repo_root: Path, ticket_dir: Optional[str]) -> Path:
|
|
79
|
+
"""Normalize a user-supplied ticket directory and ensure it stays in-tree."""
|
|
80
|
+
|
|
81
|
+
base = (repo_root / ".codex-autorunner").resolve(strict=False)
|
|
82
|
+
if not ticket_dir:
|
|
83
|
+
return base / "tickets"
|
|
84
|
+
|
|
85
|
+
cleaned = str(ticket_dir).strip()
|
|
86
|
+
if not cleaned:
|
|
87
|
+
return base / "tickets"
|
|
88
|
+
if "\\" in cleaned:
|
|
89
|
+
raise ValueError("Ticket directory may not include backslashes.")
|
|
90
|
+
|
|
91
|
+
raw_path = Path(cleaned)
|
|
92
|
+
if raw_path.is_absolute():
|
|
93
|
+
candidate = raw_path.resolve(strict=False)
|
|
94
|
+
else:
|
|
95
|
+
relative = PurePosixPath(cleaned)
|
|
96
|
+
if relative.is_absolute() or ".." in relative.parts:
|
|
97
|
+
raise ValueError("Ticket directory must be a relative path.")
|
|
98
|
+
candidate = (repo_root / relative).resolve(strict=False)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
candidate.relative_to(base)
|
|
102
|
+
except ValueError:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"Ticket directory must live under .codex-autorunner."
|
|
105
|
+
) from None
|
|
106
|
+
return candidate
|
codex_autorunner/tickets/lint.py
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from pathlib import Path
|
|
3
6
|
from typing import Any, Optional, Tuple
|
|
4
7
|
|
|
5
8
|
from ..agents.registry import validate_agent_id
|
|
6
9
|
from .models import TicketFrontmatter
|
|
7
10
|
|
|
11
|
+
# Accept TICKET-###.md or TICKET-###<suffix>.md (suffix optional), case-insensitive.
|
|
12
|
+
_TICKET_NAME_RE = re.compile(r"^TICKET-(\d{3,})(?:[^/]*)\.md$", re.IGNORECASE)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_ticket_index(name: str) -> Optional[int]:
|
|
16
|
+
match = _TICKET_NAME_RE.match(name)
|
|
17
|
+
if not match:
|
|
18
|
+
return None
|
|
19
|
+
try:
|
|
20
|
+
return int(match.group(1))
|
|
21
|
+
except ValueError:
|
|
22
|
+
return None
|
|
23
|
+
|
|
8
24
|
|
|
9
25
|
def _as_optional_str(value: Any) -> Optional[str]:
|
|
10
26
|
if isinstance(value, str):
|
|
@@ -100,3 +116,37 @@ def lint_dispatch_frontmatter(
|
|
|
100
116
|
normalized = dict(data)
|
|
101
117
|
normalized["mode"] = mode
|
|
102
118
|
return normalized, errors
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def lint_ticket_directory(ticket_dir: Path) -> list[str]:
|
|
122
|
+
"""Validate ticket directory for duplicate indices.
|
|
123
|
+
|
|
124
|
+
Returns a list of error messages (empty if valid).
|
|
125
|
+
|
|
126
|
+
This check ensures that ticket indices are unique across all ticket files.
|
|
127
|
+
Duplicate indices lead to non-deterministic ordering and confusing behavior.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
if not ticket_dir.exists() or not ticket_dir.is_dir():
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
errors: list[str] = []
|
|
134
|
+
index_to_paths: dict[int, list[str]] = defaultdict(list)
|
|
135
|
+
|
|
136
|
+
for path in ticket_dir.iterdir():
|
|
137
|
+
if not path.is_file():
|
|
138
|
+
continue
|
|
139
|
+
idx = parse_ticket_index(path.name)
|
|
140
|
+
if idx is None:
|
|
141
|
+
continue
|
|
142
|
+
index_to_paths[idx].append(path.name)
|
|
143
|
+
|
|
144
|
+
for idx, filenames in index_to_paths.items():
|
|
145
|
+
if len(filenames) > 1:
|
|
146
|
+
filenames_str = ", ".join([f"'{f}'" for f in filenames])
|
|
147
|
+
errors.append(
|
|
148
|
+
f"Duplicate ticket index {idx:03d}: multiple files share the same index ({filenames_str}). "
|
|
149
|
+
"Rename or remove duplicates to ensure deterministic ordering."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return errors
|
|
@@ -4,6 +4,8 @@ from dataclasses import dataclass, field
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any, Optional
|
|
6
6
|
|
|
7
|
+
DEFAULT_MAX_TOTAL_TURNS = 50
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
@dataclass(frozen=True)
|
|
9
11
|
class TicketFrontmatter:
|
|
@@ -70,13 +72,16 @@ class DispatchRecord:
|
|
|
70
72
|
class TicketRunConfig:
|
|
71
73
|
ticket_dir: Path
|
|
72
74
|
runs_dir: Path
|
|
73
|
-
max_total_turns: int =
|
|
75
|
+
max_total_turns: int = DEFAULT_MAX_TOTAL_TURNS
|
|
74
76
|
max_lint_retries: int = 3
|
|
75
77
|
max_commit_retries: int = 2
|
|
78
|
+
max_network_retries: int = 5
|
|
76
79
|
auto_commit: bool = True
|
|
80
|
+
prompt_max_bytes: int = 5 * 1024 * 1024 # 5 MB default budget
|
|
77
81
|
checkpoint_message_template: str = (
|
|
78
82
|
"CAR checkpoint: run={run_id} turn={turn} agent={agent}"
|
|
79
83
|
)
|
|
84
|
+
include_previous_ticket_context: bool = False
|
|
80
85
|
|
|
81
86
|
|
|
82
87
|
@dataclass(frozen=True)
|
|
@@ -3,12 +3,31 @@ from __future__ import annotations
|
|
|
3
3
|
import shutil
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Any, Callable, Dict, Optional
|
|
7
7
|
|
|
8
8
|
from .frontmatter import parse_markdown_frontmatter
|
|
9
9
|
from .lint import lint_dispatch_frontmatter
|
|
10
10
|
from .models import Dispatch, DispatchRecord
|
|
11
11
|
|
|
12
|
+
_lifecycle_emitter: Optional[Callable[[str, str, str, Dict[str, Any]], None]] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_lifecycle_emitter(
|
|
16
|
+
emitter: Optional[Callable[[str, str, str, Dict[str, Any]], None]],
|
|
17
|
+
) -> None:
|
|
18
|
+
global _lifecycle_emitter
|
|
19
|
+
_lifecycle_emitter = emitter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _emit_lifecycle(
|
|
23
|
+
event_type: str, repo_id: str, run_id: str, data: Dict[str, Any]
|
|
24
|
+
) -> None:
|
|
25
|
+
if _lifecycle_emitter:
|
|
26
|
+
try:
|
|
27
|
+
_lifecycle_emitter(event_type, repo_id, run_id, data)
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
12
31
|
|
|
13
32
|
@dataclass(frozen=True)
|
|
14
33
|
class OutboxPaths:
|
|
@@ -103,12 +122,23 @@ def create_turn_summary(
|
|
|
103
122
|
ticket_id: Optional[str] = None,
|
|
104
123
|
agent_id: Optional[str] = None,
|
|
105
124
|
turn_number: Optional[int] = None,
|
|
125
|
+
diff_stats: Optional[dict] = None,
|
|
106
126
|
) -> tuple[Optional[DispatchRecord], list[str]]:
|
|
107
127
|
"""Create a turn summary dispatch record for the agent's final output.
|
|
108
128
|
|
|
109
129
|
This creates a synthetic dispatch with mode="turn_summary" to show
|
|
110
130
|
the agent's final turn output in the dispatch history panel.
|
|
111
131
|
|
|
132
|
+
Args:
|
|
133
|
+
paths: Outbox paths for the run
|
|
134
|
+
next_seq: Sequence number for this dispatch
|
|
135
|
+
agent_output: The agent's output text
|
|
136
|
+
ticket_id: Optional ticket ID for context
|
|
137
|
+
agent_id: Optional agent ID (e.g., "codex", "opencode")
|
|
138
|
+
turn_number: Optional turn number
|
|
139
|
+
diff_stats: Optional dict with insertions/deletions/files_changed.
|
|
140
|
+
Deprecated: diff stats are now stored as FlowStore DIFF_UPDATED events.
|
|
141
|
+
|
|
112
142
|
Returns (DispatchRecord, []) on success.
|
|
113
143
|
Returns (None, errors) on failure.
|
|
114
144
|
"""
|
|
@@ -123,6 +153,8 @@ def create_turn_summary(
|
|
|
123
153
|
extra["agent_id"] = agent_id
|
|
124
154
|
if turn_number is not None:
|
|
125
155
|
extra["turn_number"] = turn_number
|
|
156
|
+
# NOTE: diff_stats is intentionally not persisted into DISPATCH.md frontmatter.
|
|
157
|
+
# It is stored as structured FlowStore DIFF_UPDATED events instead.
|
|
126
158
|
extra["is_turn_summary"] = True
|
|
127
159
|
|
|
128
160
|
dispatch = Dispatch(
|
|
@@ -163,8 +195,10 @@ def archive_dispatch(
|
|
|
163
195
|
*,
|
|
164
196
|
next_seq: int,
|
|
165
197
|
ticket_id: Optional[str] = None,
|
|
198
|
+
repo_id: str = "",
|
|
199
|
+
run_id: str = "",
|
|
166
200
|
) -> tuple[Optional[DispatchRecord], list[str]]:
|
|
167
|
-
"""Archive
|
|
201
|
+
"""Archive current dispatch and attachments to dispatch history.
|
|
168
202
|
|
|
169
203
|
Moves DISPATCH.md + attachments into dispatch_history/<seq>/.
|
|
170
204
|
|
|
@@ -221,6 +255,20 @@ def archive_dispatch(
|
|
|
221
255
|
pass
|
|
222
256
|
_delete_dispatch_items(items)
|
|
223
257
|
|
|
258
|
+
# Emit lifecycle event for dispatch creation
|
|
259
|
+
if run_id:
|
|
260
|
+
_emit_lifecycle(
|
|
261
|
+
"dispatch_created",
|
|
262
|
+
repo_id,
|
|
263
|
+
run_id,
|
|
264
|
+
{
|
|
265
|
+
"seq": next_seq,
|
|
266
|
+
"mode": dispatch.mode,
|
|
267
|
+
"title": dispatch.title,
|
|
268
|
+
"ticket_id": ticket_id,
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
|
|
224
272
|
return (
|
|
225
273
|
DispatchRecord(
|
|
226
274
|
seq=next_seq,
|