codex-autorunner 1.0.0__py3-none-any.whl → 1.1.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 (170) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/constants.py +3 -0
  4. codex_autorunner/agents/opencode/harness.py +6 -1
  5. codex_autorunner/agents/opencode/runtime.py +59 -18
  6. codex_autorunner/agents/registry.py +22 -3
  7. codex_autorunner/bootstrap.py +7 -3
  8. codex_autorunner/cli.py +5 -1174
  9. codex_autorunner/codex_cli.py +20 -84
  10. codex_autorunner/core/__init__.py +4 -0
  11. codex_autorunner/core/about_car.py +6 -1
  12. codex_autorunner/core/app_server_ids.py +59 -0
  13. codex_autorunner/core/app_server_threads.py +11 -2
  14. codex_autorunner/core/app_server_utils.py +165 -0
  15. codex_autorunner/core/archive.py +349 -0
  16. codex_autorunner/core/codex_runner.py +6 -2
  17. codex_autorunner/core/config.py +197 -3
  18. codex_autorunner/core/drafts.py +58 -4
  19. codex_autorunner/core/engine.py +1329 -680
  20. codex_autorunner/core/exceptions.py +4 -0
  21. codex_autorunner/core/flows/controller.py +25 -1
  22. codex_autorunner/core/flows/models.py +13 -0
  23. codex_autorunner/core/flows/reasons.py +52 -0
  24. codex_autorunner/core/flows/reconciler.py +131 -0
  25. codex_autorunner/core/flows/runtime.py +35 -4
  26. codex_autorunner/core/flows/store.py +83 -0
  27. codex_autorunner/core/flows/transition.py +5 -0
  28. codex_autorunner/core/flows/ux_helpers.py +257 -0
  29. codex_autorunner/core/git_utils.py +62 -0
  30. codex_autorunner/core/hub.py +121 -7
  31. codex_autorunner/core/notifications.py +14 -2
  32. codex_autorunner/core/ports/__init__.py +28 -0
  33. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +11 -3
  34. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  35. codex_autorunner/{integrations/agents → core/ports}/run_event.py +22 -2
  36. codex_autorunner/core/state_roots.py +57 -0
  37. codex_autorunner/core/supervisor_protocol.py +15 -0
  38. codex_autorunner/core/text_delta_coalescer.py +54 -0
  39. codex_autorunner/core/ticket_linter_cli.py +201 -0
  40. codex_autorunner/core/ticket_manager_cli.py +432 -0
  41. codex_autorunner/core/update.py +4 -5
  42. codex_autorunner/core/update_paths.py +28 -0
  43. codex_autorunner/core/usage.py +164 -12
  44. codex_autorunner/core/utils.py +91 -9
  45. codex_autorunner/flows/review/__init__.py +17 -0
  46. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  47. codex_autorunner/flows/ticket_flow/definition.py +9 -2
  48. codex_autorunner/integrations/agents/__init__.py +9 -19
  49. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  50. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  51. codex_autorunner/integrations/agents/codex_backend.py +158 -17
  52. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  53. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  54. codex_autorunner/integrations/agents/runner.py +91 -0
  55. codex_autorunner/integrations/agents/wiring.py +271 -0
  56. codex_autorunner/integrations/app_server/client.py +7 -60
  57. codex_autorunner/integrations/app_server/env.py +2 -107
  58. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  59. codex_autorunner/integrations/telegram/adapter.py +65 -0
  60. codex_autorunner/integrations/telegram/config.py +46 -0
  61. codex_autorunner/integrations/telegram/constants.py +1 -1
  62. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1203 -66
  64. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +4 -3
  65. codex_autorunner/integrations/telegram/handlers/commands_spec.py +8 -2
  66. codex_autorunner/integrations/telegram/handlers/messages.py +1 -0
  67. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  68. codex_autorunner/integrations/telegram/helpers.py +24 -1
  69. codex_autorunner/integrations/telegram/service.py +15 -10
  70. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +329 -40
  71. codex_autorunner/integrations/telegram/transport.py +3 -1
  72. codex_autorunner/routes/__init__.py +37 -76
  73. codex_autorunner/routes/agents.py +2 -137
  74. codex_autorunner/routes/analytics.py +2 -238
  75. codex_autorunner/routes/app_server.py +2 -131
  76. codex_autorunner/routes/base.py +2 -596
  77. codex_autorunner/routes/file_chat.py +4 -833
  78. codex_autorunner/routes/flows.py +4 -977
  79. codex_autorunner/routes/messages.py +4 -456
  80. codex_autorunner/routes/repos.py +2 -196
  81. codex_autorunner/routes/review.py +2 -147
  82. codex_autorunner/routes/sessions.py +2 -175
  83. codex_autorunner/routes/settings.py +2 -168
  84. codex_autorunner/routes/shared.py +2 -275
  85. codex_autorunner/routes/system.py +4 -193
  86. codex_autorunner/routes/usage.py +2 -86
  87. codex_autorunner/routes/voice.py +2 -119
  88. codex_autorunner/routes/workspace.py +2 -270
  89. codex_autorunner/server.py +2 -2
  90. codex_autorunner/static/agentControls.js +40 -11
  91. codex_autorunner/static/app.js +11 -3
  92. codex_autorunner/static/archive.js +826 -0
  93. codex_autorunner/static/archiveApi.js +37 -0
  94. codex_autorunner/static/autoRefresh.js +7 -7
  95. codex_autorunner/static/dashboard.js +224 -171
  96. codex_autorunner/static/hub.js +112 -94
  97. codex_autorunner/static/index.html +80 -33
  98. codex_autorunner/static/messages.js +486 -83
  99. codex_autorunner/static/preserve.js +17 -0
  100. codex_autorunner/static/settings.js +125 -6
  101. codex_autorunner/static/smartRefresh.js +52 -0
  102. codex_autorunner/static/styles.css +1373 -101
  103. codex_autorunner/static/tabs.js +152 -11
  104. codex_autorunner/static/terminal.js +18 -0
  105. codex_autorunner/static/ticketEditor.js +99 -5
  106. codex_autorunner/static/tickets.js +760 -87
  107. codex_autorunner/static/utils.js +11 -0
  108. codex_autorunner/static/workspace.js +133 -40
  109. codex_autorunner/static/workspaceFileBrowser.js +9 -9
  110. codex_autorunner/surfaces/__init__.py +5 -0
  111. codex_autorunner/surfaces/cli/__init__.py +6 -0
  112. codex_autorunner/surfaces/cli/cli.py +1224 -0
  113. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  114. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  115. codex_autorunner/surfaces/web/__init__.py +1 -0
  116. codex_autorunner/surfaces/web/app.py +2019 -0
  117. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  118. codex_autorunner/surfaces/web/middleware.py +587 -0
  119. codex_autorunner/surfaces/web/pty_session.py +370 -0
  120. codex_autorunner/surfaces/web/review.py +6 -0
  121. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  122. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  123. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  124. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  125. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  126. codex_autorunner/surfaces/web/routes/base.py +615 -0
  127. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  128. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  129. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  130. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  131. codex_autorunner/surfaces/web/routes/review.py +148 -0
  132. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  133. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  134. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  135. codex_autorunner/surfaces/web/routes/system.py +196 -0
  136. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  137. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  138. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  139. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  140. codex_autorunner/surfaces/web/schemas.py +417 -0
  141. codex_autorunner/surfaces/web/static_assets.py +490 -0
  142. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  143. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  144. codex_autorunner/tickets/__init__.py +8 -1
  145. codex_autorunner/tickets/agent_pool.py +26 -4
  146. codex_autorunner/tickets/files.py +6 -2
  147. codex_autorunner/tickets/models.py +3 -1
  148. codex_autorunner/tickets/outbox.py +12 -0
  149. codex_autorunner/tickets/runner.py +63 -5
  150. codex_autorunner/web/__init__.py +5 -1
  151. codex_autorunner/web/app.py +2 -1949
  152. codex_autorunner/web/hub_jobs.py +2 -191
  153. codex_autorunner/web/middleware.py +2 -586
  154. codex_autorunner/web/pty_session.py +2 -369
  155. codex_autorunner/web/runner_manager.py +2 -24
  156. codex_autorunner/web/schemas.py +2 -376
  157. codex_autorunner/web/static_assets.py +4 -441
  158. codex_autorunner/web/static_refresh.py +2 -85
  159. codex_autorunner/web/terminal_sessions.py +2 -77
  160. codex_autorunner/workspace/paths.py +49 -33
  161. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  162. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  163. codex_autorunner/core/static_assets.py +0 -55
  164. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  165. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  166. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  167. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +0 -0
  168. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  169. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  170. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,14 @@
1
1
  """Codex autorunner package."""
2
2
 
3
- __all__ = ["cli", "core", "integrations", "routes", "server", "voice", "web"]
3
+ __all__ = [
4
+ "cli",
5
+ "core",
6
+ "integrations",
7
+ "routes",
8
+ "server",
9
+ "surfaces",
10
+ "surfaces.web.routes",
11
+ "surfaces.web",
12
+ "voice",
13
+ "web",
14
+ ]
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Any, AsyncIterator, Optional
5
5
 
6
- from ...core.app_server_events import AppServerEventBuffer
6
+ from ...integrations.app_server.event_buffer import AppServerEventBuffer
7
7
  from ...integrations.app_server.supervisor import WorkspaceAppServerSupervisor
8
8
  from ..base import AgentHarness
9
9
  from ..types import AgentId, ConversationRef, ModelCatalog, ModelSpec, TurnRef
@@ -0,0 +1,3 @@
1
+ DEFAULT_TICKET_MODEL = "zai-coding-plan/glm-4.7"
2
+
3
+ __all__ = ["DEFAULT_TICKET_MODEL"]
@@ -6,9 +6,10 @@ import logging
6
6
  from pathlib import Path
7
7
  from typing import Any, AsyncIterator, Optional
8
8
 
9
- from ...core.app_server_events import format_sse
9
+ from ...integrations.app_server.event_buffer import format_sse
10
10
  from ..base import AgentHarness
11
11
  from ..types import AgentId, ConversationRef, ModelCatalog, ModelSpec, TurnRef
12
+ from .constants import DEFAULT_TICKET_MODEL
12
13
  from .runtime import (
13
14
  build_turn_id,
14
15
  extract_session_id,
@@ -168,6 +169,8 @@ class OpenCodeHarness(AgentHarness):
168
169
  sandbox_policy: Optional[Any],
169
170
  ) -> TurnRef:
170
171
  client = await self._supervisor.get_client(workspace_root)
172
+ if model is None:
173
+ model = DEFAULT_TICKET_MODEL
171
174
  model_payload = split_model_id(model)
172
175
  await client.prompt_async(
173
176
  conversation_id,
@@ -192,6 +195,8 @@ class OpenCodeHarness(AgentHarness):
192
195
  sandbox_policy: Optional[Any],
193
196
  ) -> TurnRef:
194
197
  client = await self._supervisor.get_client(workspace_root)
198
+ if model is None:
199
+ model = DEFAULT_TICKET_MODEL
195
200
  arguments = prompt if prompt else ""
196
201
 
197
202
  async def _send_review() -> None:
@@ -122,6 +122,12 @@ def extract_session_id(
122
122
  value = payload.get(key)
123
123
  if isinstance(value, str) and value:
124
124
  return value
125
+ info = payload.get("info")
126
+ if isinstance(info, dict):
127
+ for key in ("sessionID", "sessionId", "session_id"):
128
+ value = info.get(key)
129
+ if isinstance(value, str) and value:
130
+ return value
125
131
  if allow_fallback_id:
126
132
  value = payload.get("id")
127
133
  if isinstance(value, str) and value:
@@ -132,6 +138,12 @@ def extract_session_id(
132
138
  value = properties.get(key)
133
139
  if isinstance(value, str) and value:
134
140
  return value
141
+ info = properties.get("info")
142
+ if isinstance(info, dict):
143
+ for key in ("sessionID", "sessionId", "session_id"):
144
+ value = info.get(key)
145
+ if isinstance(value, str) and value:
146
+ return value
135
147
  part = properties.get("part")
136
148
  if isinstance(part, dict):
137
149
  for key in ("sessionID", "sessionId", "session_id"):
@@ -757,8 +769,9 @@ async def collect_opencode_output_from_events(
757
769
  error: Optional[str] = None
758
770
  message_roles: dict[str, str] = {}
759
771
  message_roles_seen = False
760
- last_role_seen: Optional[str] = None
761
772
  pending_text: dict[str, list[str]] = {}
773
+ pending_no_id: list[str] = []
774
+ no_id_role: Optional[str] = None
762
775
  fallback_message: Optional[tuple[Optional[str], Optional[str], str]] = None
763
776
  last_usage_total: Optional[int] = None
764
777
  last_context_window: Optional[int] = None
@@ -793,7 +806,7 @@ async def collect_opencode_output_from_events(
793
806
  return None
794
807
 
795
808
  def _register_message_role(payload: Any) -> tuple[Optional[str], Optional[str]]:
796
- nonlocal last_role_seen, message_roles_seen
809
+ nonlocal message_roles_seen
797
810
  if not isinstance(payload, dict):
798
811
  return None, None
799
812
  info = payload.get("info")
@@ -806,18 +819,27 @@ async def collect_opencode_output_from_events(
806
819
  if isinstance(role, str) and msg_id:
807
820
  message_roles[msg_id] = role
808
821
  message_roles_seen = True
809
- last_role_seen = role
810
822
  return msg_id, role if isinstance(role, str) else None
811
823
 
824
+ def _flush_pending_no_id_as_assistant() -> None:
825
+ nonlocal no_id_role
826
+ if pending_no_id:
827
+ text_parts.extend(pending_no_id)
828
+ pending_no_id.clear()
829
+ no_id_role = "assistant"
830
+
831
+ def _discard_pending_no_id() -> None:
832
+ if pending_no_id:
833
+ pending_no_id.clear()
834
+
812
835
  def _append_text_for_message(message_id: Optional[str], text: str) -> None:
813
836
  if not text:
814
837
  return
815
838
  if message_id is None:
816
- if not message_roles_seen:
817
- text_parts.append(text)
818
- return
819
- if last_role_seen != "user":
839
+ if no_id_role == "assistant":
820
840
  text_parts.append(text)
841
+ else:
842
+ pending_no_id.append(text)
821
843
  return
822
844
  role = message_roles.get(message_id)
823
845
  if role == "user":
@@ -839,12 +861,32 @@ async def collect_opencode_output_from_events(
839
861
  text_parts.extend(pending)
840
862
 
841
863
  def _flush_all_pending_text() -> None:
842
- if not pending_text:
864
+ if pending_text:
865
+ for pending in list(pending_text.values()):
866
+ if pending:
867
+ text_parts.extend(pending)
868
+ pending_text.clear()
869
+ if pending_no_id:
870
+ # If we have not seen a role yet, assume assistant for backwards
871
+ # compatibility with providers that omit roles entirely. Otherwise,
872
+ # only flush when we have already classified no-id text as assistant
873
+ # or when we have no other text (to avoid echoing user prompts).
874
+ if not message_roles_seen or no_id_role == "assistant" or not text_parts:
875
+ text_parts.extend(pending_no_id)
876
+ pending_no_id.clear()
877
+
878
+ def _handle_role_update(message_id: Optional[str], role: Optional[str]) -> None:
879
+ nonlocal no_id_role
880
+ if not role:
881
+ return
882
+ if role == "assistant":
883
+ _flush_pending_text(message_id)
884
+ _flush_pending_no_id_as_assistant()
843
885
  return
844
- for pending in list(pending_text.values()):
845
- if pending:
846
- text_parts.extend(pending)
847
- pending_text.clear()
886
+ if role == "user":
887
+ _flush_pending_text(message_id)
888
+ _discard_pending_no_id()
889
+ no_id_role = None
848
890
 
849
891
  async def _resolve_session_model_ids() -> tuple[Optional[str], Optional[str]]:
850
892
  nonlocal session_model_ids
@@ -1002,7 +1044,7 @@ async def collect_opencode_output_from_events(
1002
1044
  status_type=status_type,
1003
1045
  idle_seconds=idle_seconds,
1004
1046
  )
1005
- if not text_parts and pending_text:
1047
+ if not text_parts and (pending_text or pending_no_id):
1006
1048
  _flush_all_pending_text()
1007
1049
  break
1008
1050
  if last_primary_completion_at is not None:
@@ -1079,7 +1121,7 @@ async def collect_opencode_output_from_events(
1079
1121
  status_type=status_type,
1080
1122
  idle_seconds=idle_seconds,
1081
1123
  )
1082
- if not text_parts and pending_text:
1124
+ if not text_parts and (pending_text or pending_no_id):
1083
1125
  _flush_all_pending_text()
1084
1126
  break
1085
1127
  if last_primary_completion_at is not None:
@@ -1296,8 +1338,7 @@ async def collect_opencode_output_from_events(
1296
1338
  if event.event in ("message.updated", "message.completed"):
1297
1339
  if is_primary_session:
1298
1340
  msg_id, role = _register_message_role(payload)
1299
- if role == "assistant":
1300
- _flush_pending_text(msg_id)
1341
+ _handle_role_update(msg_id, role)
1301
1342
  if event.event == "message.part.updated":
1302
1343
  properties = (
1303
1344
  payload.get("properties") if isinstance(payload, dict) else None
@@ -1470,7 +1511,7 @@ async def collect_opencode_output_from_events(
1470
1511
  ):
1471
1512
  if not is_primary_session:
1472
1513
  continue
1473
- if not text_parts and pending_text:
1514
+ if not text_parts and (pending_text or pending_no_id):
1474
1515
  _flush_all_pending_text()
1475
1516
  break
1476
1517
  if event.event == "message.completed" and is_primary_session:
@@ -1485,7 +1526,7 @@ async def collect_opencode_output_from_events(
1485
1526
  resolved_role = message_roles.get(msg_id)
1486
1527
  if resolved_role == "assistant":
1487
1528
  _append_text_for_message(msg_id, text)
1488
- if pending_text:
1529
+ if pending_text or pending_no_id:
1489
1530
  _flush_all_pending_text()
1490
1531
 
1491
1532
  return OpenCodeTurnOutput(text="".join(text_parts).strip(), error=error)
@@ -163,14 +163,33 @@ def _load_agent_plugins() -> dict[str, AgentDescriptor]:
163
163
  )
164
164
  continue
165
165
 
166
- if descriptor.plugin_api_version != CAR_PLUGIN_API_VERSION:
166
+ api_version_raw = getattr(descriptor, "plugin_api_version", None)
167
+ try:
168
+ api_version = int(api_version_raw)
169
+ except Exception:
170
+ api_version = None
171
+ if api_version is None:
172
+ _logger.warning(
173
+ "Ignoring agent plugin %s: invalid api_version %s",
174
+ agent_id,
175
+ api_version_raw,
176
+ )
177
+ continue
178
+ if api_version > CAR_PLUGIN_API_VERSION:
167
179
  _logger.warning(
168
- "Ignoring agent plugin %s (api_version=%s): expected %s",
180
+ "Ignoring agent plugin %s (api_version=%s) requires newer core (%s)",
169
181
  agent_id,
170
- descriptor.plugin_api_version,
182
+ api_version,
171
183
  CAR_PLUGIN_API_VERSION,
172
184
  )
173
185
  continue
186
+ if api_version < CAR_PLUGIN_API_VERSION:
187
+ _logger.info(
188
+ "Loaded agent plugin %s with older api_version=%s (current=%s)",
189
+ agent_id,
190
+ api_version,
191
+ CAR_PLUGIN_API_VERSION,
192
+ )
174
193
 
175
194
  if agent_id in _BUILTIN_AGENTS:
176
195
  _logger.warning(
@@ -11,6 +11,8 @@ from .core.config import (
11
11
  resolve_hub_config_data,
12
12
  )
13
13
  from .core.state import RunnerState, save_state
14
+ from .core.ticket_linter_cli import ensure_ticket_linter
15
+ from .core.ticket_manager_cli import ensure_ticket_manager
14
16
  from .core.utils import atomic_write
15
17
  from .manifest import load_manifest
16
18
 
@@ -19,11 +21,11 @@ GENERATED_CONFIG_HEADER = "# GENERATED by CAR - DO NOT EDIT\n"
19
21
 
20
22
 
21
23
  def sample_todo() -> str:
22
- return """# TODO\n\n- [ ] Replace this item with your first task\n- [ ] Add another task\n- [x] Example completed item\n"""
24
+ return ""
23
25
 
24
26
 
25
27
  def sample_spec() -> str:
26
- return """# Spec\n\n## Context\n- Add project background and goals here.\n\n## Requirements\n- Requirement 1\n- Requirement 2\n\n## Non-goals\n- Out of scope items\n"""
28
+ return ""
27
29
 
28
30
 
29
31
  def _seed_doc(path: Path, force: bool, content: str) -> None:
@@ -102,7 +104,7 @@ def seed_repo_files(
102
104
  workspace_dir.mkdir(parents=True, exist_ok=True)
103
105
 
104
106
  _seed_doc(workspace_dir / "active_context.md", force, sample_todo())
105
- _seed_doc(workspace_dir / "decisions.md", force, "# Decisions\n\n")
107
+ _seed_doc(workspace_dir / "decisions.md", force, "")
106
108
  _seed_doc(workspace_dir / "spec.md", force, sample_spec())
107
109
 
108
110
  # Seed an always-available briefing doc for interactive Codex sessions.
@@ -115,6 +117,8 @@ def seed_repo_files(
115
117
  },
116
118
  force=force,
117
119
  )
120
+ ensure_ticket_linter(repo_root, force=force)
121
+ ensure_ticket_manager(repo_root, force=force)
118
122
 
119
123
 
120
124
  def seed_hub_files(hub_root: Path, force: bool = False) -> None: