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
@@ -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": [{"type": "text", "text": message}],
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": [{"type": "text", "text": message}],
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": [{"type": "text", "text": message}],
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
- try:
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
- agents = _BUILTIN_AGENTS.copy()
219
- agents.update(_load_agent_plugins())
220
- _AGENT_CACHE = agents
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
- _AGENT_CACHE = None
240
+ with _AGENT_CACHE_LOCK:
241
+ _AGENT_CACHE = None
232
242
  return get_registered_agents()
233
243
 
234
244
 
@@ -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 ensure_about_car_file_for_repo
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__ = ["ArchiveResult", "archive_worktree_snapshot"]
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
+ ]