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.
- 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 +114 -1
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +236 -1
- codex_autorunner/core/context_awareness.py +38 -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 +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/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/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 +26 -1
- 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/chatUploads.js +137 -0
- 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 +255 -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 +9125 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- 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/file_chat.py +317 -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 +70 -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.0.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
- 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.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -21,6 +21,7 @@ class OpenCodeApiProfile:
|
|
|
21
21
|
|
|
22
22
|
supports_prompt_async: bool = True
|
|
23
23
|
supports_global_endpoints: bool = True
|
|
24
|
+
max_text_chars: Optional[int] = None
|
|
24
25
|
spec_fetched: bool = False
|
|
25
26
|
|
|
26
27
|
|
|
@@ -83,6 +84,7 @@ class OpenCodeClient:
|
|
|
83
84
|
*,
|
|
84
85
|
auth: Optional[tuple[str, str]] = None,
|
|
85
86
|
timeout: Optional[float] = None,
|
|
87
|
+
max_text_chars: Optional[int] = None,
|
|
86
88
|
logger: Optional[logging.Logger] = None,
|
|
87
89
|
) -> None:
|
|
88
90
|
self._client = httpx.AsyncClient(
|
|
@@ -93,6 +95,10 @@ class OpenCodeClient:
|
|
|
93
95
|
self._logger = logger or logging.getLogger(__name__)
|
|
94
96
|
self._api_profile: Optional[OpenCodeApiProfile] = None
|
|
95
97
|
self._api_profile_lock = asyncio.Lock()
|
|
98
|
+
self._max_text_chars_override = (
|
|
99
|
+
int(max_text_chars) if isinstance(max_text_chars, int) else None
|
|
100
|
+
)
|
|
101
|
+
self._max_text_chars_cache: Optional[int] = None
|
|
96
102
|
|
|
97
103
|
async def close(self) -> None:
|
|
98
104
|
await self._client.aclose()
|
|
@@ -121,6 +127,7 @@ class OpenCodeClient:
|
|
|
121
127
|
profile.supports_global_endpoints = self.has_endpoint(
|
|
122
128
|
spec, "get", "/global/health"
|
|
123
129
|
) or self.has_endpoint(spec, "get", "/global/event")
|
|
130
|
+
profile.max_text_chars = self._extract_max_text_chars(spec)
|
|
124
131
|
|
|
125
132
|
log_event(
|
|
126
133
|
self._logger,
|
|
@@ -147,6 +154,103 @@ class OpenCodeClient:
|
|
|
147
154
|
return OpenCodeApiProfile()
|
|
148
155
|
return self._api_profile
|
|
149
156
|
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _extract_max_text_chars(spec: dict[str, Any]) -> Optional[int]:
|
|
159
|
+
if not isinstance(spec, dict):
|
|
160
|
+
return None
|
|
161
|
+
components = spec.get("components")
|
|
162
|
+
if not isinstance(components, dict):
|
|
163
|
+
return None
|
|
164
|
+
schemas = components.get("schemas")
|
|
165
|
+
if not isinstance(schemas, dict):
|
|
166
|
+
return None
|
|
167
|
+
candidates: list[int] = []
|
|
168
|
+
for schema in schemas.values():
|
|
169
|
+
max_len = OpenCodeClient._find_text_max_length(schema)
|
|
170
|
+
if isinstance(max_len, int) and max_len > 0:
|
|
171
|
+
candidates.append(max_len)
|
|
172
|
+
return min(candidates) if candidates else None
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _find_text_max_length(schema: Any) -> Optional[int]:
|
|
176
|
+
if not isinstance(schema, dict):
|
|
177
|
+
return None
|
|
178
|
+
candidates: list[int] = []
|
|
179
|
+
properties = schema.get("properties")
|
|
180
|
+
if isinstance(properties, dict) and "text" in properties:
|
|
181
|
+
text_schema = properties.get("text")
|
|
182
|
+
if isinstance(text_schema, dict):
|
|
183
|
+
max_len = text_schema.get("maxLength")
|
|
184
|
+
if isinstance(max_len, int) and max_len > 0:
|
|
185
|
+
candidates.append(max_len)
|
|
186
|
+
for key in ("allOf", "anyOf", "oneOf"):
|
|
187
|
+
seq = schema.get(key)
|
|
188
|
+
if isinstance(seq, list):
|
|
189
|
+
for item in seq:
|
|
190
|
+
item_len = OpenCodeClient._find_text_max_length(item)
|
|
191
|
+
if isinstance(item_len, int) and item_len > 0:
|
|
192
|
+
candidates.append(item_len)
|
|
193
|
+
return min(candidates) if candidates else None
|
|
194
|
+
|
|
195
|
+
def set_max_text_chars(self, value: Optional[int]) -> None:
|
|
196
|
+
self._max_text_chars_override = int(value) if isinstance(value, int) else None
|
|
197
|
+
self._max_text_chars_cache = None
|
|
198
|
+
|
|
199
|
+
async def _resolve_max_text_chars(
|
|
200
|
+
self, profile: Optional[OpenCodeApiProfile] = None
|
|
201
|
+
) -> Optional[int]:
|
|
202
|
+
if self._max_text_chars_cache is not None:
|
|
203
|
+
return self._max_text_chars_cache
|
|
204
|
+
if profile is None:
|
|
205
|
+
profile = await self.detect_api_shape()
|
|
206
|
+
detected = (
|
|
207
|
+
profile.max_text_chars
|
|
208
|
+
if isinstance(profile.max_text_chars, int) and profile.max_text_chars > 0
|
|
209
|
+
else None
|
|
210
|
+
)
|
|
211
|
+
override = (
|
|
212
|
+
self._max_text_chars_override
|
|
213
|
+
if isinstance(self._max_text_chars_override, int)
|
|
214
|
+
and self._max_text_chars_override > 0
|
|
215
|
+
else None
|
|
216
|
+
)
|
|
217
|
+
if override is None:
|
|
218
|
+
resolved = detected
|
|
219
|
+
elif detected is None:
|
|
220
|
+
resolved = override
|
|
221
|
+
else:
|
|
222
|
+
resolved = min(override, detected)
|
|
223
|
+
self._max_text_chars_cache = resolved
|
|
224
|
+
return resolved
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _split_text(text: str, max_chars: int) -> list[str]:
|
|
228
|
+
if max_chars <= 0 or len(text) <= max_chars:
|
|
229
|
+
return [text]
|
|
230
|
+
parts: list[str] = []
|
|
231
|
+
start = 0
|
|
232
|
+
length = len(text)
|
|
233
|
+
while start < length:
|
|
234
|
+
end = min(start + max_chars, length)
|
|
235
|
+
if end < length:
|
|
236
|
+
split = text.rfind("\n", start, end)
|
|
237
|
+
if split <= start:
|
|
238
|
+
split = text.rfind(" ", start, end)
|
|
239
|
+
if split > start:
|
|
240
|
+
end = split + 1
|
|
241
|
+
parts.append(text[start:end])
|
|
242
|
+
start = end
|
|
243
|
+
return [part for part in parts if part]
|
|
244
|
+
|
|
245
|
+
async def _build_text_parts(
|
|
246
|
+
self, message: str, profile: Optional[OpenCodeApiProfile] = None
|
|
247
|
+
) -> list[dict[str, str]]:
|
|
248
|
+
limit = await self._resolve_max_text_chars(profile)
|
|
249
|
+
if limit is None:
|
|
250
|
+
return [{"type": "text", "text": message}]
|
|
251
|
+
chunks = self._split_text(message, limit)
|
|
252
|
+
return [{"type": "text", "text": chunk} for chunk in chunks]
|
|
253
|
+
|
|
150
254
|
def _dir_params(self, directory: Optional[str]) -> dict[str, str]:
|
|
151
255
|
return {"directory": directory} if directory else {}
|
|
152
256
|
|
|
@@ -275,8 +379,10 @@ class OpenCodeClient:
|
|
|
275
379
|
model: Optional[dict[str, str]] = None,
|
|
276
380
|
variant: Optional[str] = None,
|
|
277
381
|
) -> Any:
|
|
382
|
+
profile = await self.detect_api_shape()
|
|
383
|
+
parts = await self._build_text_parts(message, profile)
|
|
278
384
|
payload: dict[str, Any] = {
|
|
279
|
-
"parts":
|
|
385
|
+
"parts": parts,
|
|
280
386
|
}
|
|
281
387
|
if agent:
|
|
282
388
|
payload["agent"] = agent
|
|
@@ -300,8 +406,10 @@ class OpenCodeClient:
|
|
|
300
406
|
model: Optional[dict[str, str]] = None,
|
|
301
407
|
variant: Optional[str] = None,
|
|
302
408
|
) -> Any:
|
|
409
|
+
profile = await self.detect_api_shape()
|
|
410
|
+
parts = await self._build_text_parts(message, profile)
|
|
303
411
|
payload: dict[str, Any] = {
|
|
304
|
-
"parts":
|
|
412
|
+
"parts": parts,
|
|
305
413
|
}
|
|
306
414
|
if agent:
|
|
307
415
|
payload["agent"] = agent
|
|
@@ -310,7 +418,6 @@ class OpenCodeClient:
|
|
|
310
418
|
if variant:
|
|
311
419
|
payload["variant"] = variant
|
|
312
420
|
|
|
313
|
-
profile = await self.detect_api_shape()
|
|
314
421
|
if profile.supports_prompt_async:
|
|
315
422
|
return await self._request(
|
|
316
423
|
"POST",
|
|
@@ -335,8 +442,10 @@ class OpenCodeClient:
|
|
|
335
442
|
model: Optional[dict[str, str]] = None,
|
|
336
443
|
variant: Optional[str] = None,
|
|
337
444
|
) -> Any:
|
|
445
|
+
profile = await self.detect_api_shape()
|
|
446
|
+
parts = await self._build_text_parts(message, profile)
|
|
338
447
|
payload: dict[str, Any] = {
|
|
339
|
-
"parts":
|
|
448
|
+
"parts": parts,
|
|
340
449
|
}
|
|
341
450
|
if agent:
|
|
342
451
|
payload["agent"] = agent
|
|
@@ -55,6 +55,7 @@ class OpenCodeSupervisor:
|
|
|
55
55
|
base_url: Optional[str] = None,
|
|
56
56
|
subagent_models: Optional[Mapping[str, str]] = None,
|
|
57
57
|
session_stall_timeout_seconds: Optional[float] = None,
|
|
58
|
+
max_text_chars: Optional[int] = None,
|
|
58
59
|
) -> None:
|
|
59
60
|
self._command = [str(arg) for arg in command]
|
|
60
61
|
self._logger = logger or logging.getLogger(__name__)
|
|
@@ -70,6 +71,7 @@ class OpenCodeSupervisor:
|
|
|
70
71
|
self._base_env = base_env
|
|
71
72
|
self._base_url = base_url
|
|
72
73
|
self._subagent_models = subagent_models or {}
|
|
74
|
+
self._max_text_chars = max_text_chars
|
|
73
75
|
self._handles: dict[str, OpenCodeHandle] = {}
|
|
74
76
|
self._lock: Optional[asyncio.Lock] = None
|
|
75
77
|
|
|
@@ -275,6 +277,7 @@ class OpenCodeSupervisor:
|
|
|
275
277
|
base_url,
|
|
276
278
|
auth=self._auth,
|
|
277
279
|
timeout=self._request_timeout,
|
|
280
|
+
max_text_chars=self._max_text_chars,
|
|
278
281
|
logger=self._logger,
|
|
279
282
|
)
|
|
280
283
|
try:
|
|
@@ -344,6 +347,7 @@ class OpenCodeSupervisor:
|
|
|
344
347
|
base_url,
|
|
345
348
|
auth=self._auth,
|
|
346
349
|
timeout=self._request_timeout,
|
|
350
|
+
max_text_chars=self._max_text_chars,
|
|
347
351
|
logger=self._logger,
|
|
348
352
|
)
|
|
349
353
|
try:
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
import logging
|
|
5
|
+
import threading
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from typing import Any, Callable, Iterable, Literal, Optional
|
|
7
8
|
|
|
@@ -102,6 +103,9 @@ _BUILTIN_AGENTS: dict[str, AgentDescriptor] = {
|
|
|
102
103
|
# Lazy-loaded cache of built-in + plugin agents.
|
|
103
104
|
_AGENT_CACHE: Optional[dict[str, AgentDescriptor]] = None
|
|
104
105
|
|
|
106
|
+
# Lock to protect cache initialization and reload from concurrent access.
|
|
107
|
+
_AGENT_CACHE_LOCK = threading.Lock()
|
|
108
|
+
|
|
105
109
|
|
|
106
110
|
def _select_entry_points(group: str) -> Iterable[importlib.metadata.EntryPoint]:
|
|
107
111
|
"""Compatibility wrapper for `importlib.metadata.entry_points()` across py versions."""
|
|
@@ -164,10 +168,13 @@ def _load_agent_plugins() -> dict[str, AgentDescriptor]:
|
|
|
164
168
|
continue
|
|
165
169
|
|
|
166
170
|
api_version_raw = getattr(descriptor, "plugin_api_version", None)
|
|
167
|
-
|
|
168
|
-
api_version = int(api_version_raw)
|
|
169
|
-
except Exception:
|
|
171
|
+
if api_version_raw is None:
|
|
170
172
|
api_version = None
|
|
173
|
+
else:
|
|
174
|
+
try:
|
|
175
|
+
api_version = int(api_version_raw)
|
|
176
|
+
except Exception:
|
|
177
|
+
api_version = None
|
|
171
178
|
if api_version is None:
|
|
172
179
|
_logger.warning(
|
|
173
180
|
"Ignoring agent plugin %s: invalid api_version %s",
|
|
@@ -215,9 +222,11 @@ def _load_agent_plugins() -> dict[str, AgentDescriptor]:
|
|
|
215
222
|
def _all_agents() -> dict[str, AgentDescriptor]:
|
|
216
223
|
global _AGENT_CACHE
|
|
217
224
|
if _AGENT_CACHE is None:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
with _AGENT_CACHE_LOCK:
|
|
226
|
+
if _AGENT_CACHE is None:
|
|
227
|
+
agents = _BUILTIN_AGENTS.copy()
|
|
228
|
+
agents.update(_load_agent_plugins())
|
|
229
|
+
_AGENT_CACHE = agents
|
|
221
230
|
return _AGENT_CACHE
|
|
222
231
|
|
|
223
232
|
|
|
@@ -228,7 +237,8 @@ def reload_agents() -> dict[str, AgentDescriptor]:
|
|
|
228
237
|
"""
|
|
229
238
|
|
|
230
239
|
global _AGENT_CACHE
|
|
231
|
-
|
|
240
|
+
with _AGENT_CACHE_LOCK:
|
|
241
|
+
_AGENT_CACHE = None
|
|
232
242
|
return get_registered_agents()
|
|
233
243
|
|
|
234
244
|
|
codex_autorunner/bootstrap.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
2
4
|
|
|
3
5
|
import yaml
|
|
4
6
|
|
|
5
|
-
from .core.about_car import
|
|
7
|
+
from .core.about_car import (
|
|
8
|
+
ensure_about_car_file_for_repo,
|
|
9
|
+
ensure_ticket_flow_quickstart_file_for_repo,
|
|
10
|
+
ensure_tickets_agents_file_for_repo,
|
|
11
|
+
)
|
|
6
12
|
from .core.config import (
|
|
7
13
|
CONFIG_FILENAME,
|
|
8
14
|
DEFAULT_HUB_CONFIG,
|
|
@@ -18,6 +24,9 @@ from .manifest import load_manifest
|
|
|
18
24
|
|
|
19
25
|
GITIGNORE_CONTENT = "*"
|
|
20
26
|
GENERATED_CONFIG_HEADER = "# GENERATED by CAR - DO NOT EDIT\n"
|
|
27
|
+
HUB_CAR_SHIM_MARKER = "# CAR:HUB_CLI_SHIM"
|
|
28
|
+
HUB_CAR_SHIM_REL_PATH = Path(".codex-autorunner/bin/car")
|
|
29
|
+
HUB_CAR_SHIM_ROOT_BASENAME = "car"
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
def sample_todo() -> str:
|
|
@@ -69,6 +78,58 @@ def write_hub_config(hub_root: Path, force: bool = False) -> Path:
|
|
|
69
78
|
return config_path
|
|
70
79
|
|
|
71
80
|
|
|
81
|
+
def _build_hub_car_shim(python_executable: str) -> str:
|
|
82
|
+
content = (
|
|
83
|
+
"#!/bin/sh\n"
|
|
84
|
+
f"{HUB_CAR_SHIM_MARKER}\n"
|
|
85
|
+
"# Generated by CAR. Safe to overwrite.\n"
|
|
86
|
+
f'exec "{python_executable}" -m codex_autorunner.cli "$@"\n'
|
|
87
|
+
)
|
|
88
|
+
return content
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _should_refresh_shim(path: Path, content: str, *, force: bool) -> bool:
|
|
92
|
+
if force or not path.exists():
|
|
93
|
+
return True
|
|
94
|
+
try:
|
|
95
|
+
existing = path.read_text(encoding="utf-8")
|
|
96
|
+
except OSError:
|
|
97
|
+
return True
|
|
98
|
+
if HUB_CAR_SHIM_MARKER not in existing:
|
|
99
|
+
return False
|
|
100
|
+
return existing != content
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ensure_hub_car_shim(
|
|
104
|
+
hub_root: Path,
|
|
105
|
+
*,
|
|
106
|
+
python_executable: Optional[str] = None,
|
|
107
|
+
force: bool = False,
|
|
108
|
+
) -> list[Path]:
|
|
109
|
+
"""Ensure a hub-local car shim points at the current runtime."""
|
|
110
|
+
python_executable = python_executable or sys.executable
|
|
111
|
+
content = _build_hub_car_shim(python_executable)
|
|
112
|
+
if content and not content.endswith("\n"):
|
|
113
|
+
content += "\n"
|
|
114
|
+
targets = [
|
|
115
|
+
hub_root / HUB_CAR_SHIM_ROOT_BASENAME,
|
|
116
|
+
hub_root / HUB_CAR_SHIM_REL_PATH,
|
|
117
|
+
]
|
|
118
|
+
written: list[Path] = []
|
|
119
|
+
for path in targets:
|
|
120
|
+
if not _should_refresh_shim(path, content, force=force):
|
|
121
|
+
continue
|
|
122
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
path.write_text(content, encoding="utf-8")
|
|
124
|
+
try:
|
|
125
|
+
mode = path.stat().st_mode
|
|
126
|
+
path.chmod(mode | 0o111)
|
|
127
|
+
except OSError:
|
|
128
|
+
pass
|
|
129
|
+
written.append(path)
|
|
130
|
+
return written
|
|
131
|
+
|
|
132
|
+
|
|
72
133
|
def seed_repo_files(
|
|
73
134
|
repo_root: Path, force: bool = False, git_required: bool = True
|
|
74
135
|
) -> None:
|
|
@@ -117,10 +178,23 @@ def seed_repo_files(
|
|
|
117
178
|
},
|
|
118
179
|
force=force,
|
|
119
180
|
)
|
|
181
|
+
ensure_ticket_flow_quickstart_file_for_repo(repo_root, force=force)
|
|
182
|
+
ensure_tickets_agents_file_for_repo(repo_root, force=force)
|
|
120
183
|
ensure_ticket_linter(repo_root, force=force)
|
|
121
184
|
ensure_ticket_manager(repo_root, force=force)
|
|
122
185
|
|
|
123
186
|
|
|
187
|
+
def ensure_pma_docs(hub_root: Path, force: bool = False) -> None:
|
|
188
|
+
ca_dir = hub_root / ".codex-autorunner"
|
|
189
|
+
pma_dir = ca_dir / "pma"
|
|
190
|
+
pma_dir.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
_seed_doc(pma_dir / "prompt.md", force, pma_prompt_content())
|
|
192
|
+
_seed_doc(pma_dir / "ABOUT_CAR.md", force, pma_about_content())
|
|
193
|
+
_seed_doc(pma_dir / "AGENTS.md", force, pma_agents_content())
|
|
194
|
+
_seed_doc(pma_dir / "active_context.md", force, pma_active_context_content())
|
|
195
|
+
_seed_doc(pma_dir / "context_log.md", force, pma_context_log_content())
|
|
196
|
+
|
|
197
|
+
|
|
124
198
|
def seed_hub_files(hub_root: Path, force: bool = False) -> None:
|
|
125
199
|
"""
|
|
126
200
|
Initialize a hub workspace with defaults and a manifest.
|
|
@@ -134,6 +208,9 @@ def seed_hub_files(hub_root: Path, force: bool = False) -> None:
|
|
|
134
208
|
|
|
135
209
|
write_hub_config(hub_root, force=force)
|
|
136
210
|
|
|
211
|
+
ensure_pma_docs(hub_root, force=force)
|
|
212
|
+
ensure_hub_car_shim(hub_root, force=force)
|
|
213
|
+
|
|
137
214
|
manifest_path = hub_root / DEFAULT_HUB_CONFIG["hub"]["manifest"]
|
|
138
215
|
load_manifest(manifest_path, hub_root)
|
|
139
216
|
|
|
@@ -143,3 +220,144 @@ def seed_hub_files(hub_root: Path, force: bool = False) -> None:
|
|
|
143
220
|
hub_state_path,
|
|
144
221
|
'{\n "last_scan_at": null,\n "repos": []\n}\n',
|
|
145
222
|
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def pma_prompt_content() -> str:
|
|
226
|
+
return """# Project Management Agent (PMA)
|
|
227
|
+
|
|
228
|
+
You are the hub-level Project Management Agent (PMA), the user's primary interface for coordinating work across repos.
|
|
229
|
+
|
|
230
|
+
## Role
|
|
231
|
+
|
|
232
|
+
You are an **abstraction layer, not an executor**. Coordinate tickets and flows across multiple repos by delegating to repo agents.
|
|
233
|
+
|
|
234
|
+
## Guidance
|
|
235
|
+
|
|
236
|
+
- Use CAR-native artifacts (tickets, ticket_flow, dispatch, PMA inbox/outbox).
|
|
237
|
+
- Ask questions when requirements are ambiguous; keep updates concise.
|
|
238
|
+
- Treat this prompt as code: keep it short and stable.
|
|
239
|
+
- See `.codex-autorunner/pma/ABOUT_CAR.md` for operational how-to.
|
|
240
|
+
|
|
241
|
+
## Worktrees (hub-managed)
|
|
242
|
+
|
|
243
|
+
- Prefer hub-owned worktrees:
|
|
244
|
+
- Hub UI: “New Worktree”
|
|
245
|
+
- CLI: `car hub worktree create <base_repo_id> <branch> [--start-point <ref>]`
|
|
246
|
+
- If a worktree was created manually (e.g. `git worktree add`), it MUST be registered:
|
|
247
|
+
- `car hub scan --path <hub_root>`
|
|
248
|
+
- Never copy `.codex-autorunner/` between worktrees. Each worktree has its own CAR state/docs.
|
|
249
|
+
|
|
250
|
+
## PMA durable workspace
|
|
251
|
+
|
|
252
|
+
Prefer writing durable guidance and recurring best-practices to `.codex-autorunner/pma/AGENTS.md`.
|
|
253
|
+
Keep short-lived working context in `.codex-autorunner/pma/active_context.md` and prune it when it grows.
|
|
254
|
+
When pruning, append the prior context to `.codex-autorunner/pma/context_log.md` with a timestamp.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def pma_notes_content() -> str:
|
|
259
|
+
return """# PMA Operations Guide
|
|
260
|
+
|
|
261
|
+
## Tickets (create/modify)
|
|
262
|
+
|
|
263
|
+
- Tickets live per repo at `<repo>/.codex-autorunner/tickets/`.
|
|
264
|
+
- Create or edit `TICKET-###*.md` files directly; keep diffs small and single-purpose.
|
|
265
|
+
- Set `done: true` in the ticket frontmatter only when the ticket is complete.
|
|
266
|
+
|
|
267
|
+
## Ticket flow (start/resume)
|
|
268
|
+
|
|
269
|
+
- Bootstrap (creates TICKET-001 if missing):
|
|
270
|
+
`car flow ticket_flow bootstrap --repo <path>`
|
|
271
|
+
- Start/resume:
|
|
272
|
+
`car flow ticket_flow start --repo <path>`
|
|
273
|
+
`car flow ticket_flow resume --repo <path> [--run-id <uuid>]`
|
|
274
|
+
- Status/stop:
|
|
275
|
+
`car flow ticket_flow status --repo <path> [--run-id <uuid>]`
|
|
276
|
+
`car flow ticket_flow stop --repo <path> [--run-id <uuid>]`
|
|
277
|
+
- See `<repo>/.codex-autorunner/TICKET_FLOW_QUICKSTART.md` for CLI entrypoints + gotchas.
|
|
278
|
+
|
|
279
|
+
## Worktrees 101 (Hub-managed)
|
|
280
|
+
|
|
281
|
+
Canonical worktree creation:
|
|
282
|
+
- Hub UI: “New Worktree”
|
|
283
|
+
- CLI (from hub root):
|
|
284
|
+
`car hub worktree create <base_repo_id> <branch> [--start-point <ref>]`
|
|
285
|
+
|
|
286
|
+
Registering a manually-created worktree:
|
|
287
|
+
- If you used `git worktree add`, run:
|
|
288
|
+
`car hub scan --path <hub_root>`
|
|
289
|
+
- Worktrees should live under the hub’s configured worktrees root and be shallow (depth=1) discoverable.
|
|
290
|
+
|
|
291
|
+
Naming / grouping:
|
|
292
|
+
- Prefer worktree directory names like:
|
|
293
|
+
`<base_repo_id>--<branch>`
|
|
294
|
+
- This enables `worktree_of` inference during scan and grouping in the hub UI.
|
|
295
|
+
|
|
296
|
+
Do NOT copy `.codex-autorunner/` between worktrees:
|
|
297
|
+
- Each worktree is a full repo with its own `.codex-autorunner/` state/docs.
|
|
298
|
+
- Copying can introduce stale locks and confusing run metadata.
|
|
299
|
+
|
|
300
|
+
## Dispatch pauses (handle)
|
|
301
|
+
|
|
302
|
+
- Paused runs appear in the hub inbox snapshot with `repo_id`, `run_id`, and `open_url`.
|
|
303
|
+
- Read the latest dispatch, respond to the user, then resume the run.
|
|
304
|
+
- Dispatch history lives under:
|
|
305
|
+
`<repo>/.codex-autorunner/runs/<run_id>/dispatch_history/####/DISPATCH.md`
|
|
306
|
+
|
|
307
|
+
## PMA file handoff
|
|
308
|
+
|
|
309
|
+
- User uploads arrive in `.codex-autorunner/pma/inbox/`.
|
|
310
|
+
- Send user-facing files by writing to `.codex-autorunner/pma/outbox/`.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def pma_about_content() -> str:
|
|
315
|
+
return pma_notes_content()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def pma_agents_content() -> str:
|
|
319
|
+
return """# PMA AGENTS (durable guidance + defaults)
|
|
320
|
+
|
|
321
|
+
This document is jointly maintained by the user and PMA.
|
|
322
|
+
|
|
323
|
+
## What belongs here
|
|
324
|
+
|
|
325
|
+
- Durable best-practices you want PMA to apply repeatedly ("defaults").
|
|
326
|
+
- Stable preferences (how to structure tickets, review habits, PR conventions).
|
|
327
|
+
- Template shortcuts / references that PMA should re-use.
|
|
328
|
+
|
|
329
|
+
## What does NOT belong here
|
|
330
|
+
|
|
331
|
+
- Temporary work-in-progress details (put those in `active_context.md`).
|
|
332
|
+
- Long transcripts.
|
|
333
|
+
|
|
334
|
+
## Template shortcuts (optional)
|
|
335
|
+
|
|
336
|
+
- Add references to frequently used ticket templates here (repo/path/ref) and when to apply them.
|
|
337
|
+
|
|
338
|
+
## Defaults (examples)
|
|
339
|
+
|
|
340
|
+
- After implementation work, add a final review ticket, then a ticket to open a PR.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def pma_active_context_content() -> str:
|
|
345
|
+
return """# PMA active context (short-lived)
|
|
346
|
+
|
|
347
|
+
Use this file for the current working set: active projects, open questions, links, and immediate next steps.
|
|
348
|
+
|
|
349
|
+
Pruning guidance:
|
|
350
|
+
- Keep this file compact (prefer bullet points).
|
|
351
|
+
- When it grows too large, summarize older items and move durable guidance to `AGENTS.md`.
|
|
352
|
+
- Before a major prune, append a timestamped snapshot to `context_log.md`.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def pma_context_log_content() -> str:
|
|
357
|
+
return """# PMA context log (append-only)
|
|
358
|
+
|
|
359
|
+
This file is an append-only history of past `active_context.md` snapshots.
|
|
360
|
+
|
|
361
|
+
- Add a new section with an ISO timestamp when you perform a major prune.
|
|
362
|
+
- Keep entries concise; the goal is searchability and historical recall.
|
|
363
|
+
"""
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
"""Core runtime primitives."""
|
|
2
2
|
|
|
3
3
|
from .archive import ArchiveResult, archive_worktree_snapshot
|
|
4
|
+
from .context_awareness import CAR_AWARENESS_BLOCK, format_file_role_addendum
|
|
5
|
+
from .lifecycle_events import (
|
|
6
|
+
LifecycleEvent,
|
|
7
|
+
LifecycleEventEmitter,
|
|
8
|
+
LifecycleEventStore,
|
|
9
|
+
LifecycleEventType,
|
|
10
|
+
)
|
|
4
11
|
|
|
5
|
-
__all__ = [
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ArchiveResult",
|
|
14
|
+
"archive_worktree_snapshot",
|
|
15
|
+
"CAR_AWARENESS_BLOCK",
|
|
16
|
+
"format_file_role_addendum",
|
|
17
|
+
"LifecycleEvent",
|
|
18
|
+
"LifecycleEventEmitter",
|
|
19
|
+
"LifecycleEventStore",
|
|
20
|
+
"LifecycleEventType",
|
|
21
|
+
]
|