codex-autorunner 1.1.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.
Files changed (127) hide show
  1. codex_autorunner/agents/opencode/client.py +113 -4
  2. codex_autorunner/agents/opencode/supervisor.py +4 -0
  3. codex_autorunner/agents/registry.py +17 -7
  4. codex_autorunner/bootstrap.py +219 -1
  5. codex_autorunner/core/__init__.py +17 -1
  6. codex_autorunner/core/about_car.py +114 -1
  7. codex_autorunner/core/app_server_threads.py +6 -0
  8. codex_autorunner/core/config.py +236 -1
  9. codex_autorunner/core/context_awareness.py +38 -0
  10. codex_autorunner/core/docs.py +0 -122
  11. codex_autorunner/core/filebox.py +265 -0
  12. codex_autorunner/core/flows/controller.py +71 -1
  13. codex_autorunner/core/flows/reconciler.py +4 -1
  14. codex_autorunner/core/flows/runtime.py +22 -0
  15. codex_autorunner/core/flows/store.py +61 -9
  16. codex_autorunner/core/flows/transition.py +23 -16
  17. codex_autorunner/core/flows/ux_helpers.py +18 -3
  18. codex_autorunner/core/flows/worker_process.py +32 -6
  19. codex_autorunner/core/hub.py +198 -41
  20. codex_autorunner/core/lifecycle_events.py +253 -0
  21. codex_autorunner/core/path_utils.py +2 -1
  22. codex_autorunner/core/pma_audit.py +224 -0
  23. codex_autorunner/core/pma_context.py +496 -0
  24. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  25. codex_autorunner/core/pma_lifecycle.py +527 -0
  26. codex_autorunner/core/pma_queue.py +367 -0
  27. codex_autorunner/core/pma_safety.py +221 -0
  28. codex_autorunner/core/pma_state.py +115 -0
  29. codex_autorunner/core/ports/agent_backend.py +2 -5
  30. codex_autorunner/core/ports/run_event.py +1 -4
  31. codex_autorunner/core/prompt.py +0 -80
  32. codex_autorunner/core/prompts.py +56 -172
  33. codex_autorunner/core/redaction.py +0 -4
  34. codex_autorunner/core/review_context.py +11 -9
  35. codex_autorunner/core/runner_controller.py +35 -33
  36. codex_autorunner/core/runner_state.py +147 -0
  37. codex_autorunner/core/runtime.py +829 -0
  38. codex_autorunner/core/sqlite_utils.py +13 -4
  39. codex_autorunner/core/state.py +7 -10
  40. codex_autorunner/core/state_roots.py +5 -0
  41. codex_autorunner/core/templates/__init__.py +39 -0
  42. codex_autorunner/core/templates/git_mirror.py +234 -0
  43. codex_autorunner/core/templates/provenance.py +56 -0
  44. codex_autorunner/core/templates/scan_cache.py +120 -0
  45. codex_autorunner/core/ticket_linter_cli.py +17 -0
  46. codex_autorunner/core/ticket_manager_cli.py +154 -92
  47. codex_autorunner/core/time_utils.py +11 -0
  48. codex_autorunner/core/types.py +18 -0
  49. codex_autorunner/core/utils.py +34 -6
  50. codex_autorunner/flows/review/service.py +23 -25
  51. codex_autorunner/flows/ticket_flow/definition.py +43 -1
  52. codex_autorunner/integrations/agents/__init__.py +2 -0
  53. codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
  54. codex_autorunner/integrations/agents/codex_backend.py +19 -8
  55. codex_autorunner/integrations/agents/runner.py +3 -8
  56. codex_autorunner/integrations/agents/wiring.py +8 -0
  57. codex_autorunner/integrations/telegram/doctor.py +228 -6
  58. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  59. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  60. codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
  61. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  62. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
  63. codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
  64. codex_autorunner/integrations/telegram/handlers/messages.py +26 -1
  65. codex_autorunner/integrations/telegram/helpers.py +1 -3
  66. codex_autorunner/integrations/telegram/runtime.py +9 -4
  67. codex_autorunner/integrations/telegram/service.py +30 -0
  68. codex_autorunner/integrations/telegram/state.py +38 -0
  69. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
  70. codex_autorunner/integrations/telegram/transport.py +10 -3
  71. codex_autorunner/integrations/templates/__init__.py +27 -0
  72. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  73. codex_autorunner/server.py +2 -2
  74. codex_autorunner/static/agentControls.js +21 -5
  75. codex_autorunner/static/app.js +115 -11
  76. codex_autorunner/static/chatUploads.js +137 -0
  77. codex_autorunner/static/docChatCore.js +185 -13
  78. codex_autorunner/static/fileChat.js +68 -40
  79. codex_autorunner/static/fileboxUi.js +159 -0
  80. codex_autorunner/static/hub.js +46 -81
  81. codex_autorunner/static/index.html +303 -24
  82. codex_autorunner/static/messages.js +82 -4
  83. codex_autorunner/static/notifications.js +255 -0
  84. codex_autorunner/static/pma.js +1167 -0
  85. codex_autorunner/static/settings.js +3 -0
  86. codex_autorunner/static/streamUtils.js +57 -0
  87. codex_autorunner/static/styles.css +9125 -6742
  88. codex_autorunner/static/templateReposSettings.js +225 -0
  89. codex_autorunner/static/ticketChatActions.js +165 -3
  90. codex_autorunner/static/ticketChatStream.js +17 -119
  91. codex_autorunner/static/ticketEditor.js +41 -13
  92. codex_autorunner/static/ticketTemplates.js +798 -0
  93. codex_autorunner/static/tickets.js +69 -19
  94. codex_autorunner/static/turnEvents.js +27 -0
  95. codex_autorunner/static/turnResume.js +33 -0
  96. codex_autorunner/static/utils.js +28 -0
  97. codex_autorunner/static/workspace.js +258 -44
  98. codex_autorunner/static/workspaceFileBrowser.js +6 -4
  99. codex_autorunner/surfaces/cli/cli.py +1465 -155
  100. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  101. codex_autorunner/surfaces/web/app.py +253 -49
  102. codex_autorunner/surfaces/web/routes/__init__.py +4 -0
  103. codex_autorunner/surfaces/web/routes/analytics.py +29 -22
  104. codex_autorunner/surfaces/web/routes/file_chat.py +317 -36
  105. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  106. codex_autorunner/surfaces/web/routes/flows.py +219 -29
  107. codex_autorunner/surfaces/web/routes/messages.py +70 -39
  108. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  109. codex_autorunner/surfaces/web/routes/repos.py +1 -1
  110. codex_autorunner/surfaces/web/routes/shared.py +0 -3
  111. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  112. codex_autorunner/surfaces/web/runner_manager.py +2 -2
  113. codex_autorunner/surfaces/web/schemas.py +70 -18
  114. codex_autorunner/tickets/agent_pool.py +27 -0
  115. codex_autorunner/tickets/files.py +33 -16
  116. codex_autorunner/tickets/lint.py +50 -0
  117. codex_autorunner/tickets/models.py +3 -0
  118. codex_autorunner/tickets/outbox.py +41 -5
  119. codex_autorunner/tickets/runner.py +350 -69
  120. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/METADATA +15 -19
  121. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
  122. codex_autorunner/core/adapter_utils.py +0 -21
  123. codex_autorunner/core/engine.py +0 -3302
  124. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  125. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  126. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  127. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -198,6 +198,76 @@ class SessionStopRequest(Payload):
198
198
  repo_path: Optional[str] = None
199
199
 
200
200
 
201
+ class TemplateRepoSummary(ResponseModel):
202
+ id: str
203
+ url: str
204
+ trusted: bool
205
+ default_ref: str
206
+
207
+
208
+ class TemplateReposResponse(ResponseModel):
209
+ enabled: bool
210
+ repos: List[TemplateRepoSummary]
211
+
212
+
213
+ class TemplateRepoCreateRequest(Payload):
214
+ id: str
215
+ url: str
216
+ trusted: bool = False
217
+ default_ref: str = Field(
218
+ default="main", validation_alias=AliasChoices("default_ref", "defaultRef")
219
+ )
220
+
221
+
222
+ class TemplateRepoUpdateRequest(Payload):
223
+ url: Optional[str] = None
224
+ trusted: Optional[bool] = None
225
+ default_ref: Optional[str] = Field(
226
+ default=None, validation_alias=AliasChoices("default_ref", "defaultRef")
227
+ )
228
+
229
+
230
+ class TemplateFetchRequest(Payload):
231
+ template: str
232
+
233
+
234
+ class TemplateFetchResponse(ResponseModel):
235
+ content: str
236
+ repo_id: str
237
+ path: str
238
+ ref: str
239
+ commit_sha: str
240
+ blob_sha: str
241
+ trusted: bool
242
+ scan_decision: Optional[Dict[str, Any]] = None
243
+
244
+
245
+ class TemplateApplyRequest(Payload):
246
+ template: str
247
+ ticket_dir: Optional[str] = Field(
248
+ default=None, validation_alias=AliasChoices("ticket_dir", "ticketDir")
249
+ )
250
+ at: Optional[int] = None
251
+ next_index: bool = Field(
252
+ default=True, validation_alias=AliasChoices("next_index", "nextIndex")
253
+ )
254
+ suffix: Optional[str] = None
255
+ set_agent: Optional[str] = Field(
256
+ default=None, validation_alias=AliasChoices("set_agent", "setAgent")
257
+ )
258
+ include_provenance: bool = Field(
259
+ default=False,
260
+ validation_alias=AliasChoices("include_provenance", "includeProvenance"),
261
+ )
262
+
263
+
264
+ class TemplateApplyResponse(ResponseModel):
265
+ created_path: str
266
+ index: int
267
+ filename: str
268
+ metadata: Dict[str, Any]
269
+
270
+
201
271
  class SystemUpdateRequest(Payload):
202
272
  target: Optional[str] = None
203
273
 
@@ -213,24 +283,6 @@ class HubJobResponse(ResponseModel):
213
283
  error: Optional[str]
214
284
 
215
285
 
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
286
  class SessionSettingsResponse(ResponseModel):
235
287
  autorunner_model_override: Optional[str]
236
288
  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 re
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
@@ -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
- if diff_stats:
137
- extra["diff_stats"] = diff_stats
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 the current dispatch and attachments to the dispatch history.
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,