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
|
@@ -86,6 +86,17 @@ class ArchiveSnapshotDetailResponse(ResponseModel):
|
|
|
86
86
|
meta: Optional[Dict[str, Any]] = None
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
class LocalRunArchiveSummary(ResponseModel):
|
|
90
|
+
run_id: str
|
|
91
|
+
archived_at: Optional[str] = None
|
|
92
|
+
has_tickets: bool = False
|
|
93
|
+
has_runs: bool = False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class LocalRunArchivesResponse(ResponseModel):
|
|
97
|
+
archives: List[LocalRunArchiveSummary]
|
|
98
|
+
|
|
99
|
+
|
|
89
100
|
class ArchiveTreeNode(ResponseModel):
|
|
90
101
|
path: str
|
|
91
102
|
name: str
|
|
@@ -198,6 +209,76 @@ class SessionStopRequest(Payload):
|
|
|
198
209
|
repo_path: Optional[str] = None
|
|
199
210
|
|
|
200
211
|
|
|
212
|
+
class TemplateRepoSummary(ResponseModel):
|
|
213
|
+
id: str
|
|
214
|
+
url: str
|
|
215
|
+
trusted: bool
|
|
216
|
+
default_ref: str
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TemplateReposResponse(ResponseModel):
|
|
220
|
+
enabled: bool
|
|
221
|
+
repos: List[TemplateRepoSummary]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TemplateRepoCreateRequest(Payload):
|
|
225
|
+
id: str
|
|
226
|
+
url: str
|
|
227
|
+
trusted: bool = False
|
|
228
|
+
default_ref: str = Field(
|
|
229
|
+
default="main", validation_alias=AliasChoices("default_ref", "defaultRef")
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class TemplateRepoUpdateRequest(Payload):
|
|
234
|
+
url: Optional[str] = None
|
|
235
|
+
trusted: Optional[bool] = None
|
|
236
|
+
default_ref: Optional[str] = Field(
|
|
237
|
+
default=None, validation_alias=AliasChoices("default_ref", "defaultRef")
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class TemplateFetchRequest(Payload):
|
|
242
|
+
template: str
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class TemplateFetchResponse(ResponseModel):
|
|
246
|
+
content: str
|
|
247
|
+
repo_id: str
|
|
248
|
+
path: str
|
|
249
|
+
ref: str
|
|
250
|
+
commit_sha: str
|
|
251
|
+
blob_sha: str
|
|
252
|
+
trusted: bool
|
|
253
|
+
scan_decision: Optional[Dict[str, Any]] = None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TemplateApplyRequest(Payload):
|
|
257
|
+
template: str
|
|
258
|
+
ticket_dir: Optional[str] = Field(
|
|
259
|
+
default=None, validation_alias=AliasChoices("ticket_dir", "ticketDir")
|
|
260
|
+
)
|
|
261
|
+
at: Optional[int] = None
|
|
262
|
+
next_index: bool = Field(
|
|
263
|
+
default=True, validation_alias=AliasChoices("next_index", "nextIndex")
|
|
264
|
+
)
|
|
265
|
+
suffix: Optional[str] = None
|
|
266
|
+
set_agent: Optional[str] = Field(
|
|
267
|
+
default=None, validation_alias=AliasChoices("set_agent", "setAgent")
|
|
268
|
+
)
|
|
269
|
+
include_provenance: bool = Field(
|
|
270
|
+
default=False,
|
|
271
|
+
validation_alias=AliasChoices("include_provenance", "includeProvenance"),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class TemplateApplyResponse(ResponseModel):
|
|
276
|
+
created_path: str
|
|
277
|
+
index: int
|
|
278
|
+
filename: str
|
|
279
|
+
metadata: Dict[str, Any]
|
|
280
|
+
|
|
281
|
+
|
|
201
282
|
class SystemUpdateRequest(Payload):
|
|
202
283
|
target: Optional[str] = None
|
|
203
284
|
|
|
@@ -213,24 +294,6 @@ class HubJobResponse(ResponseModel):
|
|
|
213
294
|
error: Optional[str]
|
|
214
295
|
|
|
215
296
|
|
|
216
|
-
class StateResponse(ResponseModel):
|
|
217
|
-
last_run_id: Optional[int]
|
|
218
|
-
status: str
|
|
219
|
-
last_exit_code: Optional[int]
|
|
220
|
-
last_run_started_at: Optional[str]
|
|
221
|
-
last_run_finished_at: Optional[str]
|
|
222
|
-
outstanding_count: int
|
|
223
|
-
done_count: int
|
|
224
|
-
running: bool
|
|
225
|
-
runner_pid: Optional[int]
|
|
226
|
-
lock_present: bool
|
|
227
|
-
lock_pid: Optional[int]
|
|
228
|
-
lock_freeable: bool
|
|
229
|
-
lock_freeable_reason: Optional[str]
|
|
230
|
-
terminal_idle_timeout_seconds: Optional[int]
|
|
231
|
-
codex_model: str
|
|
232
|
-
|
|
233
|
-
|
|
234
297
|
class SessionSettingsResponse(ResponseModel):
|
|
235
298
|
autorunner_model_override: Optional[str]
|
|
236
299
|
autorunner_effort_override: Optional[str]
|
|
@@ -30,6 +30,10 @@ class AgentTurnRequest:
|
|
|
30
30
|
options: Optional[dict[str, Any]] = None
|
|
31
31
|
# Optional flow event emitter (for live streaming).
|
|
32
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
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
@dataclass(frozen=True)
|
|
@@ -179,6 +183,7 @@ class AgentPool:
|
|
|
179
183
|
max_handles=app_server_cfg.max_handles,
|
|
180
184
|
idle_ttl_seconds=app_server_cfg.idle_ttl_seconds,
|
|
181
185
|
session_stall_timeout_seconds=self._config.opencode.session_stall_timeout_seconds,
|
|
186
|
+
max_text_chars=self._config.opencode.max_text_chars,
|
|
182
187
|
base_env=None,
|
|
183
188
|
subagent_models=subagent_models,
|
|
184
189
|
)
|
|
@@ -249,6 +254,19 @@ class AgentPool:
|
|
|
249
254
|
turn_kwargs["model"] = req.options["model"]
|
|
250
255
|
if req.options.get("reasoning"):
|
|
251
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
|
+
|
|
252
270
|
turn_handle = await client.turn_start(thread_id, req.prompt, **turn_kwargs)
|
|
253
271
|
if req.emit_event is not None:
|
|
254
272
|
self._active_emitters[turn_handle.turn_id] = req.emit_event
|
|
@@ -300,9 +318,18 @@ class AgentPool:
|
|
|
300
318
|
if not session_id:
|
|
301
319
|
raise RuntimeError("OpenCode create_session returned no session id")
|
|
302
320
|
|
|
321
|
+
# Send main prompt and any additional messages
|
|
322
|
+
# OpenCode processes messages sequentially; agents that queue will handle them
|
|
303
323
|
prompt_response = await client.prompt_async(
|
|
304
324
|
session_id, message=req.prompt, model=model_payload, variant=variant
|
|
305
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
|
+
)
|
|
306
333
|
|
|
307
334
|
import uuid
|
|
308
335
|
|
|
@@ -1,26 +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
|
-
# 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
|
-
|
|
24
10
|
|
|
25
11
|
def list_ticket_paths(ticket_dir: Path) -> list[Path]:
|
|
26
12
|
if not ticket_dir.exists() or not ticket_dir.is_dir():
|
|
@@ -87,3 +73,34 @@ def safe_relpath(path: Path, root: Path) -> str:
|
|
|
87
73
|
return str(path.relative_to(root))
|
|
88
74
|
except ValueError:
|
|
89
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
|
|
@@ -75,10 +75,13 @@ class TicketRunConfig:
|
|
|
75
75
|
max_total_turns: int = DEFAULT_MAX_TOTAL_TURNS
|
|
76
76
|
max_lint_retries: int = 3
|
|
77
77
|
max_commit_retries: int = 2
|
|
78
|
+
max_network_retries: int = 5
|
|
78
79
|
auto_commit: bool = True
|
|
80
|
+
prompt_max_bytes: int = 5 * 1024 * 1024 # 5 MB default budget
|
|
79
81
|
checkpoint_message_template: str = (
|
|
80
82
|
"CAR checkpoint: run={run_id} turn={turn} agent={agent}"
|
|
81
83
|
)
|
|
84
|
+
include_previous_ticket_context: bool = False
|
|
82
85
|
|
|
83
86
|
|
|
84
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:
|
|
@@ -117,7 +136,8 @@ def create_turn_summary(
|
|
|
117
136
|
ticket_id: Optional ticket ID for context
|
|
118
137
|
agent_id: Optional agent ID (e.g., "codex", "opencode")
|
|
119
138
|
turn_number: Optional turn number
|
|
120
|
-
diff_stats: Optional dict with insertions/deletions/files_changed
|
|
139
|
+
diff_stats: Optional dict with insertions/deletions/files_changed.
|
|
140
|
+
Deprecated: diff stats are now stored as FlowStore DIFF_UPDATED events.
|
|
121
141
|
|
|
122
142
|
Returns (DispatchRecord, []) on success.
|
|
123
143
|
Returns (None, errors) on failure.
|
|
@@ -133,8 +153,8 @@ def create_turn_summary(
|
|
|
133
153
|
extra["agent_id"] = agent_id
|
|
134
154
|
if turn_number is not None:
|
|
135
155
|
extra["turn_number"] = turn_number
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
# NOTE: diff_stats is intentionally not persisted into DISPATCH.md frontmatter.
|
|
157
|
+
# It is stored as structured FlowStore DIFF_UPDATED events instead.
|
|
138
158
|
extra["is_turn_summary"] = True
|
|
139
159
|
|
|
140
160
|
dispatch = Dispatch(
|
|
@@ -175,8 +195,10 @@ def archive_dispatch(
|
|
|
175
195
|
*,
|
|
176
196
|
next_seq: int,
|
|
177
197
|
ticket_id: Optional[str] = None,
|
|
198
|
+
repo_id: str = "",
|
|
199
|
+
run_id: str = "",
|
|
178
200
|
) -> tuple[Optional[DispatchRecord], list[str]]:
|
|
179
|
-
"""Archive
|
|
201
|
+
"""Archive current dispatch and attachments to dispatch history.
|
|
180
202
|
|
|
181
203
|
Moves DISPATCH.md + attachments into dispatch_history/<seq>/.
|
|
182
204
|
|
|
@@ -233,6 +255,20 @@ def archive_dispatch(
|
|
|
233
255
|
pass
|
|
234
256
|
_delete_dispatch_items(items)
|
|
235
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
|
+
|
|
236
272
|
return (
|
|
237
273
|
DispatchRecord(
|
|
238
274
|
seq=next_seq,
|