shikiru-cli 0.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 (319) hide show
  1. shikiru/__init__.py +1 -0
  2. shikiru/__main__.py +8 -0
  3. shikiru/agents/__init__.py +3 -0
  4. shikiru/agents/contracts.py +590 -0
  5. shikiru/agents/registry.py +87 -0
  6. shikiru/agents/sdk/__init__.py +17 -0
  7. shikiru/agents/sdk/reset_time.py +137 -0
  8. shikiru/agents/sdk/turn.py +335 -0
  9. shikiru/agents/settings.py +151 -0
  10. shikiru/agents/testing/__init__.py +26 -0
  11. shikiru/agents/testing/contract_suite.py +653 -0
  12. shikiru/agents/testing/resume.py +15 -0
  13. shikiru/bootstrap.py +1277 -0
  14. shikiru/cli/__init__.py +1 -0
  15. shikiru/cli/app.py +770 -0
  16. shikiru/cli/context.py +63 -0
  17. shikiru/cli/render.py +1475 -0
  18. shikiru/cli/report.py +156 -0
  19. shikiru/cli/session_prompt.py +63 -0
  20. shikiru/cli/view.py +131 -0
  21. shikiru/clock.py +41 -0
  22. shikiru/config/__init__.py +8 -0
  23. shikiru/config/defaults.toml +131 -0
  24. shikiru/config/loader.py +178 -0
  25. shikiru/config/settings.py +392 -0
  26. shikiru/dashboard/__init__.py +3 -0
  27. shikiru/dashboard/actions_list.py +799 -0
  28. shikiru/dashboard/activity.py +150 -0
  29. shikiru/dashboard/app.py +1765 -0
  30. shikiru/dashboard/blocker_evidence.py +122 -0
  31. shikiru/dashboard/blocker_rows.py +84 -0
  32. shikiru/dashboard/briefing_view.py +871 -0
  33. shikiru/dashboard/contract.py +1086 -0
  34. shikiru/dashboard/covered.py +32 -0
  35. shikiru/dashboard/detail.py +1209 -0
  36. shikiru/dashboard/detail_fold.py +244 -0
  37. shikiru/dashboard/detail_hint.py +68 -0
  38. shikiru/dashboard/details_list_view.py +150 -0
  39. shikiru/dashboard/diagnostics.py +80 -0
  40. shikiru/dashboard/diagnostics_sections.py +186 -0
  41. shikiru/dashboard/evidence_table.py +123 -0
  42. shikiru/dashboard/extra_details.py +214 -0
  43. shikiru/dashboard/fault_guidance.py +216 -0
  44. shikiru/dashboard/fold_bodies.py +276 -0
  45. shikiru/dashboard/help_keys.py +166 -0
  46. shikiru/dashboard/help_screen.py +204 -0
  47. shikiru/dashboard/interaction.py +17 -0
  48. shikiru/dashboard/ladder_editor.py +544 -0
  49. shikiru/dashboard/live_settings.py +62 -0
  50. shikiru/dashboard/motion.py +368 -0
  51. shikiru/dashboard/orchestration_fields.py +201 -0
  52. shikiru/dashboard/palette.py +1606 -0
  53. shikiru/dashboard/palette_app.py +89 -0
  54. shikiru/dashboard/palette_drafts.py +33 -0
  55. shikiru/dashboard/resolution.py +573 -0
  56. shikiru/dashboard/resolution_view.py +302 -0
  57. shikiru/dashboard/row_list.py +256 -0
  58. shikiru/dashboard/section_rows.py +173 -0
  59. shikiru/dashboard/services.py +665 -0
  60. shikiru/dashboard/session_history.py +402 -0
  61. shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
  62. shikiru/dashboard/task_picker.py +358 -0
  63. shikiru/dashboard/tick.py +59 -0
  64. shikiru/dashboard/timeline_filter.py +129 -0
  65. shikiru/dashboard/timeline_section.py +385 -0
  66. shikiru/dashboard/usage_body.py +269 -0
  67. shikiru/dashboard/views.py +2314 -0
  68. shikiru/dashboard/wizard.py +1389 -0
  69. shikiru/domain/__init__.py +1 -0
  70. shikiru/domain/acceptance.py +130 -0
  71. shikiru/domain/activity.py +28 -0
  72. shikiru/domain/attempts.py +333 -0
  73. shikiru/domain/briefing.py +257 -0
  74. shikiru/domain/commands.py +219 -0
  75. shikiru/domain/decisions.py +231 -0
  76. shikiru/domain/docker.py +262 -0
  77. shikiru/domain/engine_uptime.py +87 -0
  78. shikiru/domain/escalation.py +194 -0
  79. shikiru/domain/escalation_evidence.py +186 -0
  80. shikiru/domain/escalation_panel.py +144 -0
  81. shikiru/domain/events.py +71 -0
  82. shikiru/domain/evidence_export.py +89 -0
  83. shikiru/domain/faults.py +86 -0
  84. shikiru/domain/feedback.py +180 -0
  85. shikiru/domain/feedback_rounds.py +172 -0
  86. shikiru/domain/fencing.py +31 -0
  87. shikiru/domain/ladder.py +102 -0
  88. shikiru/domain/ladder_text.py +44 -0
  89. shikiru/domain/levels.py +44 -0
  90. shikiru/domain/motion.py +199 -0
  91. shikiru/domain/packet_evidence.py +377 -0
  92. shikiru/domain/pushes.py +47 -0
  93. shikiru/domain/quota_blocks.py +44 -0
  94. shikiru/domain/quota_waits.py +219 -0
  95. shikiru/domain/recovery.py +194 -0
  96. shikiru/domain/reports.py +498 -0
  97. shikiru/domain/results.py +217 -0
  98. shikiru/domain/runs.py +85 -0
  99. shikiru/domain/scheduling.py +231 -0
  100. shikiru/domain/status_tasks.py +40 -0
  101. shikiru/domain/steps.py +325 -0
  102. shikiru/domain/tasks.py +174 -0
  103. shikiru/domain/timeline.py +208 -0
  104. shikiru/domain/timeline_phrases.py +271 -0
  105. shikiru/domain/versions.py +18 -0
  106. shikiru/domain/workflow.py +372 -0
  107. shikiru/engine/__init__.py +0 -0
  108. shikiru/engine/acceptance.py +90 -0
  109. shikiru/engine/agents.py +61 -0
  110. shikiru/engine/cosmetic_decorator.py +99 -0
  111. shikiru/engine/daemon.py +409 -0
  112. shikiru/engine/dependency.py +37 -0
  113. shikiru/engine/escalation_filing.py +110 -0
  114. shikiru/engine/escalations.py +401 -0
  115. shikiru/engine/events.py +285 -0
  116. shikiru/engine/evidence.py +81 -0
  117. shikiru/engine/fault_reporter.py +144 -0
  118. shikiru/engine/feedback.py +577 -0
  119. shikiru/engine/feedback_audit.py +80 -0
  120. shikiru/engine/feedback_intake.py +95 -0
  121. shikiru/engine/feedback_snapshot.py +148 -0
  122. shikiru/engine/flow.py +87 -0
  123. shikiru/engine/handlers/__init__.py +33 -0
  124. shikiru/engine/handlers/base.py +148 -0
  125. shikiru/engine/handlers/engine.py +24 -0
  126. shikiru/engine/handlers/escalations.py +174 -0
  127. shikiru/engine/handlers/operator.py +81 -0
  128. shikiru/engine/handlers/tasks.py +237 -0
  129. shikiru/engine/history.py +128 -0
  130. shikiru/engine/integration.py +1234 -0
  131. shikiru/engine/integration_turns.py +337 -0
  132. shikiru/engine/layout_guard.py +362 -0
  133. shikiru/engine/lifecycle.py +60 -0
  134. shikiru/engine/monitor.py +317 -0
  135. shikiru/engine/notification_delivery.py +124 -0
  136. shikiru/engine/notification_feed.py +57 -0
  137. shikiru/engine/notifier.py +91 -0
  138. shikiru/engine/pull_request_closing.py +117 -0
  139. shikiru/engine/push_service.py +177 -0
  140. shikiru/engine/quota_probe_threads.py +79 -0
  141. shikiru/engine/quota_reconciler.py +210 -0
  142. shikiru/engine/quota_schedule.py +24 -0
  143. shikiru/engine/quota_waits.py +102 -0
  144. shikiru/engine/reconcile.py +91 -0
  145. shikiru/engine/recovery.py +284 -0
  146. shikiru/engine/retention.py +102 -0
  147. shikiru/engine/root_preparation.py +86 -0
  148. shikiru/engine/scheduler.py +320 -0
  149. shikiru/engine/session_guard.py +46 -0
  150. shikiru/engine/sessions.py +95 -0
  151. shikiru/engine/signalled.py +75 -0
  152. shikiru/engine/step_runner.py +140 -0
  153. shikiru/engine/supervisor.py +101 -0
  154. shikiru/engine/takeover.py +223 -0
  155. shikiru/engine/task_facts.py +46 -0
  156. shikiru/engine/tick_guard.py +95 -0
  157. shikiru/engine/titles.py +297 -0
  158. shikiru/engine/transitions.py +79 -0
  159. shikiru/engine/turn_context.py +206 -0
  160. shikiru/engine/turn_execution.py +30 -0
  161. shikiru/engine/turn_loop.py +230 -0
  162. shikiru/engine/turn_processes.py +79 -0
  163. shikiru/engine/turns.py +457 -0
  164. shikiru/errors.py +144 -0
  165. shikiru/helper/__init__.py +3 -0
  166. shikiru/helper/helper.py +182 -0
  167. shikiru/helper/models.py +11 -0
  168. shikiru/mcp/__init__.py +1 -0
  169. shikiru/mcp/server.py +196 -0
  170. shikiru/operator/__init__.py +33 -0
  171. shikiru/operator/attachment.py +24 -0
  172. shikiru/operator/commands.py +319 -0
  173. shikiru/operator/detectors.py +374 -0
  174. shikiru/operator/doctor.py +351 -0
  175. shikiru/operator/events.py +57 -0
  176. shikiru/operator/evidence_export.py +498 -0
  177. shikiru/operator/evidence_writer.py +78 -0
  178. shikiru/operator/lifecycle.py +294 -0
  179. shikiru/operator/plugin_checks.py +194 -0
  180. shikiru/operator/probe.py +287 -0
  181. shikiru/operator/queries.py +1601 -0
  182. shikiru/operator/services.py +36 -0
  183. shikiru/operator/sessions.py +378 -0
  184. shikiru/operator/strip_views.py +162 -0
  185. shikiru/operator/timeline.py +416 -0
  186. shikiru/operator/toml_text.py +62 -0
  187. shikiru/palette.py +18 -0
  188. shikiru/paths.py +91 -0
  189. shikiru/plugins/__init__.py +0 -0
  190. shikiru/plugins/agents/__init__.py +1 -0
  191. shikiru/plugins/agents/claude/__init__.py +5 -0
  192. shikiru/plugins/agents/claude/agent.py +251 -0
  193. shikiru/plugins/agents/claude/context.py +116 -0
  194. shikiru/plugins/agents/claude/headless.py +92 -0
  195. shikiru/plugins/agents/claude/hook.py +57 -0
  196. shikiru/plugins/agents/claude/model_catalog.py +105 -0
  197. shikiru/plugins/agents/claude/options.py +62 -0
  198. shikiru/plugins/agents/claude/turn.py +1018 -0
  199. shikiru/plugins/agents/codex/__init__.py +5 -0
  200. shikiru/plugins/agents/codex/agent.py +486 -0
  201. shikiru/plugins/agents/codex/headless.py +166 -0
  202. shikiru/plugins/agents/codex/home.py +14 -0
  203. shikiru/plugins/agents/codex/model_catalog.py +128 -0
  204. shikiru/plugins/agents/codex/options.py +94 -0
  205. shikiru/plugins/agents/codex/quota.py +195 -0
  206. shikiru/plugins/agents/codex/sandbox.py +31 -0
  207. shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
  208. shikiru/plugins/agents/codex/turn.py +767 -0
  209. shikiru/plugins/agents/codex/usage.py +106 -0
  210. shikiru/plugins/agents/shared_settings.py +62 -0
  211. shikiru/plugins/widgets/__init__.py +3 -0
  212. shikiru/plugins/widgets/dialogue.py +144 -0
  213. shikiru/plugins/widgets/disk.py +123 -0
  214. shikiru/plugins/widgets/docker.py +225 -0
  215. shikiru/plugins/widgets/escalations.py +402 -0
  216. shikiru/plugins/widgets/faults.py +204 -0
  217. shikiru/plugins/widgets/header.py +321 -0
  218. shikiru/plugins/widgets/queue.py +1140 -0
  219. shikiru/plugins/widgets/queue_hint.py +93 -0
  220. shikiru/plugins/widgets/resources.py +105 -0
  221. shikiru/plugins/widgets/strips.py +82 -0
  222. shikiru/plugins/widgets/usage.py +176 -0
  223. shikiru/private.py +94 -0
  224. shikiru/process.py +535 -0
  225. shikiru/prompts/__init__.py +3 -0
  226. shikiru/prompts/renderer.py +85 -0
  227. shikiru/prompts/templates/_base.md.j2 +17 -0
  228. shikiru/prompts/templates/_macros.md.j2 +61 -0
  229. shikiru/prompts/templates/conflicts.md.j2 +22 -0
  230. shikiru/prompts/templates/consult.md.j2 +5 -0
  231. shikiru/prompts/templates/continue.md.j2 +4 -0
  232. shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
  233. shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
  234. shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
  235. shikiru/prompts/templates/handoff.md.j2 +23 -0
  236. shikiru/prompts/templates/implement.md.j2 +13 -0
  237. shikiru/prompts/templates/plan.md.j2 +30 -0
  238. shikiru/prompts/templates/rework.md.j2 +13 -0
  239. shikiru/prompts/templates/triage.md.j2 +20 -0
  240. shikiru/prompts/templates/verify.md.j2 +29 -0
  241. shikiru/store/__init__.py +1 -0
  242. shikiru/store/commands.py +242 -0
  243. shikiru/store/database.py +297 -0
  244. shikiru/store/escalations.py +240 -0
  245. shikiru/store/events.py +213 -0
  246. shikiru/store/feedback.py +266 -0
  247. shikiru/store/migrations/001_initial.sql +63 -0
  248. shikiru/store/migrations/002_headless_turns.sql +31 -0
  249. shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
  250. shikiru/store/migrations/004_repo_base_ref.sql +3 -0
  251. shikiru/store/migrations/005_complexity_explicit.sql +3 -0
  252. shikiru/store/migrations/006_events_kind_index.sql +1 -0
  253. shikiru/store/migrations/007_notification_cursor.sql +1 -0
  254. shikiru/store/migrations/008_reported_usage.sql +1 -0
  255. shikiru/store/migrations/009_billing_declarations.sql +2 -0
  256. shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
  257. shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
  258. shikiru/store/migrations/013_push_targets.sql +7 -0
  259. shikiru/store/migrations/014_agent_block_time.sql +6 -0
  260. shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
  261. shikiru/store/migrations/016_feedback_rounds.sql +29 -0
  262. shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
  263. shikiru/store/migrations/__init__.py +1 -0
  264. shikiru/store/notification_cursor.py +29 -0
  265. shikiru/store/pushes.py +117 -0
  266. shikiru/store/quotas.py +153 -0
  267. shikiru/store/reader.py +310 -0
  268. shikiru/store/retention.py +92 -0
  269. shikiru/store/runs.py +227 -0
  270. shikiru/store/tasks.py +949 -0
  271. shikiru/telemetry/__init__.py +1 -0
  272. shikiru/telemetry/disk.py +167 -0
  273. shikiru/telemetry/docker.py +164 -0
  274. shikiru/telemetry/resources.py +296 -0
  275. shikiru/tmux/__init__.py +3 -0
  276. shikiru/tmux/client.py +637 -0
  277. shikiru/tmux/decorator.py +225 -0
  278. shikiru/tmux/layout.py +200 -0
  279. shikiru/tmux/session.py +381 -0
  280. shikiru/ui/__init__.py +9 -0
  281. shikiru/ui/age.py +45 -0
  282. shikiru/ui/bubbles.py +41 -0
  283. shikiru/ui/compact.py +1209 -0
  284. shikiru/ui/console.py +49 -0
  285. shikiru/ui/fold.py +50 -0
  286. shikiru/ui/gated_static.py +117 -0
  287. shikiru/ui/gauge.py +40 -0
  288. shikiru/ui/glyphs.py +109 -0
  289. shikiru/ui/keys.py +57 -0
  290. shikiru/ui/live_interval.py +42 -0
  291. shikiru/ui/popup.py +311 -0
  292. shikiru/ui/quota_status.py +26 -0
  293. shikiru/ui/recent_window.py +78 -0
  294. shikiru/ui/screen.py +57 -0
  295. shikiru/ui/section.py +193 -0
  296. shikiru/ui/session.py +65 -0
  297. shikiru/ui/size.py +17 -0
  298. shikiru/ui/splash.py +42 -0
  299. shikiru/ui/status.py +47 -0
  300. shikiru/ui/status_strip.py +149 -0
  301. shikiru/ui/stream.py +152 -0
  302. shikiru/ui/strip.py +332 -0
  303. shikiru/ui/theme.py +83 -0
  304. shikiru/ui/tokens.py +51 -0
  305. shikiru/view_log.py +126 -0
  306. shikiru/workspace/__init__.py +3 -0
  307. shikiru/workspace/base_refs.py +160 -0
  308. shikiru/workspace/caches.py +136 -0
  309. shikiru/workspace/facts.py +118 -0
  310. shikiru/workspace/git.py +601 -0
  311. shikiru/workspace/github.py +744 -0
  312. shikiru/workspace/messages.py +107 -0
  313. shikiru/workspace/reports.py +145 -0
  314. shikiru/workspace/roots.py +180 -0
  315. shikiru_cli-0.2.0.dist-info/METADATA +428 -0
  316. shikiru_cli-0.2.0.dist-info/RECORD +319 -0
  317. shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
  318. shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
  319. shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,5 @@
1
+ """Codex agent plugin."""
2
+
3
+ from shikiru.plugins.agents.codex.agent import CodexAgent, create
4
+
5
+ __all__ = ["CodexAgent", "create"]
@@ -0,0 +1,486 @@
1
+ """Codex headless turn plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tomllib
7
+ from collections.abc import Mapping
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, cast
10
+
11
+ from pydantic import ValidationError
12
+
13
+ from shikiru.agents.contracts import (
14
+ BILLING_SOURCE_OPERATOR_CONFIG,
15
+ DOCTOR_TIMEOUT_SECONDS,
16
+ UNKNOWN_BILLING_DECLARATION,
17
+ BillingDeclaration,
18
+ CheckResult,
19
+ ModelCatalog,
20
+ )
21
+ from shikiru.agents.sdk.reset_time import ResetTimeParser
22
+ from shikiru.agents.sdk.turn import StreamingTurn
23
+ from shikiru.clock import from_local
24
+ from shikiru.errors import CommandFailed, ConfigError, ShikiruError
25
+ from shikiru.plugins.agents.codex.headless import CodexHeadless
26
+ from shikiru.plugins.agents.codex.home import codex_home
27
+ from shikiru.plugins.agents.codex.model_catalog import (
28
+ parse_bundled_context_windows,
29
+ parse_catalog,
30
+ )
31
+ from shikiru.plugins.agents.codex.options import (
32
+ CODEX_BUNDLED_MODELS_ARGV,
33
+ CODEX_COMMAND,
34
+ CODEX_HOME_ENV_VAR,
35
+ CODEX_MODEL_CATALOG_ARGV,
36
+ CODEX_SETTINGS,
37
+ CONFIG_FLAG,
38
+ CONFIG_LONG_FLAG,
39
+ EFFORT_LONG_FLAG,
40
+ GIT_DIRS_ARGV,
41
+ GIT_DIRS_TIMEOUT_SECONDS,
42
+ LOGIN_STATUS_ARGV,
43
+ MODEL_CATALOG_ARGS_DETAIL,
44
+ MODEL_CONFIG_OVERRIDE,
45
+ MODEL_FLAG,
46
+ MODEL_LONG_FLAG,
47
+ MODEL_OVERRIDE,
48
+ NO_DAEMON_FLAG,
49
+ PROFILE_FLAG,
50
+ REASONING_EFFORT_CONFIG,
51
+ RESUME_SUBCOMMAND,
52
+ TESTED_CODEX_VERSION,
53
+ VERSION_ARGV,
54
+ CodexOptions,
55
+ )
56
+ from shikiru.plugins.agents.codex.quota import CodexQuotaProbe
57
+ from shikiru.plugins.agents.codex.sandbox_probe import CodexSandboxProbe
58
+ from shikiru.plugins.agents.codex.turn import CodexTurnSequence, context_window
59
+ from shikiru.plugins.agents.shared_settings import options_error
60
+
61
+ if TYPE_CHECKING:
62
+ from collections.abc import Sequence
63
+ from datetime import datetime
64
+
65
+ from shikiru.agents.contracts import (
66
+ AgentContext,
67
+ HeadlessAgent,
68
+ QuotaReading,
69
+ Rung,
70
+ Turn,
71
+ TurnSpec,
72
+ )
73
+ from shikiru.agents.settings import AgentSetting
74
+ from shikiru.clock import Clock
75
+ from shikiru.process import SubprocessRunner
76
+
77
+ _AUTH_NOT_LOGGED_IN_TEXT = "not logged in"
78
+ _AUTH_LOGGED_IN_TEXTS = ("logged in", "authenticated")
79
+ _CODEX_CONFIG_FILENAME = "config.toml"
80
+ _MODEL_PROVIDER_FIELD = "model_provider"
81
+ _MODEL_PROVIDERS_FIELD = "model_providers"
82
+ _OPENAI_BASE_URL_FIELD = "openai_base_url"
83
+ _PROFILES_FIELD = "profiles"
84
+ _PROFILE_FIELD = "profile"
85
+ _PARTITION_VALUE_INDEX = 2
86
+ _CONTEXT_WINDOW_CHECK = "context_window"
87
+ _UNTESTED_VERSION_DETAIL = "{version} (warning: verified with {tested}; re-check the sandbox probe)"
88
+ _UNKNOWN_WINDOW_DETAIL = (
89
+ "no positive context window for {models} in models_cache.json or Codex CLI's bundled "
90
+ "model catalog; compaction at compact_at_pct cannot be applied"
91
+ )
92
+ _MODELS_CACHE_SOURCE = "models_cache.json"
93
+ _BUNDLED_CATALOG_SOURCE = "Codex CLI bundled model catalog"
94
+ _CURRENT_CATALOG_SOURCE = "Codex CLI current model catalog"
95
+ _CUSTOM_PROVIDER_ACCESS_DETAIL = (
96
+ "custom provider configured; provider credentials are not verified by doctor"
97
+ )
98
+
99
+
100
+ class CodexAgent:
101
+ """Build Codex turn processes and health checks."""
102
+
103
+ name = "codex"
104
+
105
+ def __init__(self, runner: SubprocessRunner, clock: Clock, options: CodexOptions) -> None:
106
+ self._runner = runner
107
+ self._clock = clock
108
+ self._options = options
109
+ self._reset_time = ResetTimeParser(clock, resolve_local=from_local)
110
+ self._billing_declaration = (
111
+ BillingDeclaration(options.billing_mode, BILLING_SOURCE_OPERATOR_CONFIG)
112
+ if options.billing_mode is not None
113
+ else UNKNOWN_BILLING_DECLARATION
114
+ )
115
+ self._codex_home = codex_home()
116
+ self._quota_probe = CodexQuotaProbe(self._codex_home, clock, self._reset_time)
117
+ self._sandbox_probe = CodexSandboxProbe(runner)
118
+ self._catalog_context_windows: dict[str, int] = {}
119
+ self._bundled_context_windows: dict[str, int] = {}
120
+ self._cached_model_catalog: ModelCatalog | None = None
121
+
122
+ @property
123
+ def billing_declaration(self) -> BillingDeclaration:
124
+ """Return the explicit plugin option or the unknown declaration."""
125
+ return self._billing_declaration
126
+
127
+ def read_quota(self, since: datetime) -> QuotaReading | None:
128
+ """Return the newest Codex-bucket reading in this account's rollouts after `since`."""
129
+ return self._quota_probe.latest_reading(since)
130
+
131
+ async def model_catalog(self) -> ModelCatalog:
132
+ """Return choices from Codex CLI's refreshed current model catalog."""
133
+ if self._cached_model_catalog is None:
134
+ self._cached_model_catalog = await self._read_model_catalog()
135
+ return self._cached_model_catalog
136
+
137
+ async def start_turn(self, spec: TurnSpec) -> Turn:
138
+ """Start one Codex process-per-turn exchange."""
139
+ model_context_windows = await self._context_windows_for_turn(spec)
140
+ git_dirs = await self._git_dirs_for_turn(spec)
141
+ sequence = self.process_sequence(
142
+ spec, git_dirs=git_dirs, model_context_windows=model_context_windows
143
+ )
144
+ return await StreamingTurn.start(spec, self._runner, sequence)
145
+
146
+ def process_sequence(
147
+ self,
148
+ spec: TurnSpec,
149
+ *,
150
+ git_dirs: Sequence[Path],
151
+ model_context_windows: Mapping[str, int] | None = None,
152
+ ) -> CodexTurnSequence:
153
+ """Build Codex's prompt and context-failure retry sequence for `spec`."""
154
+ home = Path(spec.env.get(CODEX_HOME_ENV_VAR, str(self._codex_home)))
155
+ return CodexTurnSequence(
156
+ spec,
157
+ args=self._options.args,
158
+ environment=self._turn_environment,
159
+ clock=self._clock,
160
+ reset_time=self._reset_time,
161
+ home=home,
162
+ git_dirs=git_dirs,
163
+ compact_at_pct=self._options.compact_at_pct,
164
+ model_context_windows=(
165
+ self._bundled_context_windows
166
+ if model_context_windows is None
167
+ else model_context_windows
168
+ ),
169
+ )
170
+
171
+ def interactive_command(self, session_id: str, run_dir: Path, rung: Rung) -> Sequence[str]:
172
+ """Return Codex's interactive resume command for a headless session and rung."""
173
+ del run_dir
174
+ argv = [
175
+ CODEX_COMMAND,
176
+ RESUME_SUBCOMMAND,
177
+ NO_DAEMON_FLAG,
178
+ session_id,
179
+ MODEL_FLAG,
180
+ rung.model,
181
+ ]
182
+ if rung.effort is not None:
183
+ argv.extend((CONFIG_FLAG, f'{REASONING_EFFORT_CONFIG}="{rung.effort}"'))
184
+ argv.extend(self._options.args)
185
+ return tuple(argv)
186
+
187
+ async def doctor(self, models: Sequence[str]) -> list[CheckResult]:
188
+ """Check Codex's binary, version, native access and each model's context window."""
189
+ binary = await self._check_binary()
190
+ if not binary.ok:
191
+ unavailable = "binary unavailable"
192
+ return [
193
+ binary,
194
+ CheckResult("version", False, unavailable),
195
+ CheckResult("login", False, unavailable),
196
+ ]
197
+ checks = [
198
+ binary,
199
+ await self._check_version(),
200
+ await self._check_login(),
201
+ await self._sandbox_probe.check(),
202
+ ]
203
+ if models:
204
+ checks.append(await self._check_context_windows(models))
205
+ return checks
206
+
207
+ async def _check_context_windows(self, models: Sequence[str]) -> CheckResult:
208
+ cached_windows = {model: context_window(self._codex_home, model) for model in models}
209
+ missing_from_catalog = [
210
+ model
211
+ for model, window in cached_windows.items()
212
+ if window is None and model not in self._catalog_context_windows
213
+ ]
214
+ if missing_from_catalog:
215
+ catalog_result = await self._read_bundled_model_catalog(missing_from_catalog)
216
+ if isinstance(catalog_result, CheckResult):
217
+ return catalog_result
218
+ self._bundled_context_windows = catalog_result
219
+ self._catalog_context_windows.update(catalog_result)
220
+ windows = {
221
+ model: cached_windows[model]
222
+ if cached_windows[model] is not None
223
+ else self._catalog_context_windows.get(model)
224
+ for model in models
225
+ }
226
+ unknown = [model for model, window in windows.items() if window is None]
227
+ if unknown:
228
+ return CheckResult(
229
+ _CONTEXT_WINDOW_CHECK,
230
+ False,
231
+ _UNKNOWN_WINDOW_DETAIL.format(models=", ".join(unknown)),
232
+ )
233
+ known_parts: list[str] = []
234
+ for model, window in windows.items():
235
+ if cached_windows[model] is not None:
236
+ source = _MODELS_CACHE_SOURCE
237
+ elif model in self._bundled_context_windows:
238
+ source = _BUNDLED_CATALOG_SOURCE
239
+ else:
240
+ source = _CURRENT_CATALOG_SOURCE
241
+ known_parts.append(f"{model}: {window} ({source})")
242
+ known = ", ".join(known_parts)
243
+ return CheckResult(_CONTEXT_WINDOW_CHECK, True, known)
244
+
245
+ async def _read_model_catalog(self) -> ModelCatalog:
246
+ if _model_catalog_args_are_ambiguous(self._options.args):
247
+ return ModelCatalog(error=MODEL_CATALOG_ARGS_DETAIL)
248
+ return await self._query_model_catalog()
249
+
250
+ async def _query_model_catalog(self) -> ModelCatalog:
251
+ try:
252
+ result = await self._runner.run(
253
+ CODEX_MODEL_CATALOG_ARGV,
254
+ timeout=DOCTOR_TIMEOUT_SECONDS,
255
+ env=self._subprocess_environment(),
256
+ check=False,
257
+ )
258
+ except (FileNotFoundError, ShikiruError) as error:
259
+ detail = str(error) or type(error).__name__
260
+ return ModelCatalog(error=f"could not read Codex CLI's current model catalog: {detail}")
261
+ if result.returncode != 0:
262
+ detail = result.stderr.strip() or "no diagnostic output"
263
+ return ModelCatalog(
264
+ error=(
265
+ f"Codex CLI's current model catalog command failed with exit code "
266
+ f"{result.returncode}: {detail}"
267
+ )
268
+ )
269
+ try:
270
+ choices, windows = parse_catalog(result.stdout)
271
+ except TypeError as error:
272
+ return ModelCatalog(error=f"could not parse Codex CLI's current model catalog: {error}")
273
+ self._catalog_context_windows.update(windows)
274
+ return ModelCatalog(models=choices, complete=True)
275
+
276
+ async def _read_bundled_model_catalog(
277
+ self,
278
+ models: Sequence[str],
279
+ *,
280
+ environment: Mapping[str, str] | None = None,
281
+ ) -> dict[str, int] | CheckResult:
282
+ model_names = ", ".join(models)
283
+ try:
284
+ result = await self._runner.run(
285
+ CODEX_BUNDLED_MODELS_ARGV,
286
+ timeout=DOCTOR_TIMEOUT_SECONDS,
287
+ env=(self._subprocess_environment() if environment is None else environment),
288
+ check=False,
289
+ )
290
+ except (FileNotFoundError, ShikiruError) as error:
291
+ return CheckResult(
292
+ _CONTEXT_WINDOW_CHECK,
293
+ False,
294
+ f"could not read Codex CLI's bundled model catalog for {model_names}: {error}",
295
+ )
296
+ if result.returncode != 0:
297
+ detail = result.stderr.strip() or result.stdout.strip()
298
+ return CheckResult(
299
+ _CONTEXT_WINDOW_CHECK,
300
+ False,
301
+ f"Codex CLI's bundled model catalog command failed for {model_names} with exit "
302
+ f"code {result.returncode}: {detail or 'no diagnostic output'}",
303
+ )
304
+ try:
305
+ return parse_bundled_context_windows(result.stdout)
306
+ except TypeError as error:
307
+ return CheckResult(
308
+ _CONTEXT_WINDOW_CHECK,
309
+ False,
310
+ f"could not parse Codex CLI's bundled model catalog for {model_names}: {error}",
311
+ )
312
+
313
+ async def _context_windows_for_turn(self, spec: TurnSpec) -> Mapping[str, int]:
314
+ home = Path(spec.env.get(CODEX_HOME_ENV_VAR, str(self._codex_home)))
315
+ model = spec.rung.model
316
+ if context_window(home, model, spec.session_id) is not None:
317
+ return self._bundled_context_windows
318
+ if model in self._bundled_context_windows:
319
+ return self._bundled_context_windows
320
+ catalog = await self._read_bundled_model_catalog(
321
+ (model,), environment=self._turn_environment(spec)
322
+ )
323
+ if isinstance(catalog, CheckResult):
324
+ raise ConfigError(catalog.detail)
325
+ self._bundled_context_windows = catalog
326
+ if model not in catalog:
327
+ raise ConfigError(_UNKNOWN_WINDOW_DETAIL.format(models=model))
328
+ return catalog
329
+
330
+ def settings(self) -> tuple[AgentSetting, ...]:
331
+ """Return the options worth writing to the project config."""
332
+ return CODEX_SETTINGS
333
+
334
+ def headless(self) -> HeadlessAgent | None:
335
+ """Return Codex's schema-constrained headless client."""
336
+ return CodexHeadless(self._runner, self._subprocess_environment())
337
+
338
+ def _subprocess_environment(self) -> dict[str, str]:
339
+ return dict(os.environ)
340
+
341
+ def _turn_environment(self, spec: TurnSpec) -> dict[str, str]:
342
+ environment = dict(os.environ)
343
+ environment.update(spec.env)
344
+ return environment
345
+
346
+ async def _git_dirs_for_turn(self, spec: TurnSpec) -> tuple[Path, ...]:
347
+ result = await self._runner.run(
348
+ GIT_DIRS_ARGV,
349
+ timeout=GIT_DIRS_TIMEOUT_SECONDS,
350
+ cwd=spec.cwd,
351
+ check=False,
352
+ )
353
+ values = result.stdout.split()
354
+ if result.returncode != 0 or not values:
355
+ message = (
356
+ f"could not resolve the Codex workspace git directories: {result.stderr.strip()}"
357
+ )
358
+ raise ConfigError(message)
359
+ return tuple(Path(value) for value in values)
360
+
361
+ async def _check_binary(self) -> CheckResult:
362
+ try:
363
+ result = await self._runner.run(
364
+ VERSION_ARGV,
365
+ timeout=DOCTOR_TIMEOUT_SECONDS,
366
+ env=self._subprocess_environment(),
367
+ check=False,
368
+ )
369
+ except (FileNotFoundError, CommandFailed, ShikiruError) as error:
370
+ return CheckResult("binary", False, str(error))
371
+ if result.returncode != 0:
372
+ return CheckResult("binary", False, result.stderr.strip())
373
+ return CheckResult("binary", True, CODEX_COMMAND)
374
+
375
+ async def _check_version(self) -> CheckResult:
376
+ try:
377
+ result = await self._runner.run(
378
+ VERSION_ARGV,
379
+ timeout=DOCTOR_TIMEOUT_SECONDS,
380
+ env=self._subprocess_environment(),
381
+ check=False,
382
+ )
383
+ except (FileNotFoundError, ShikiruError) as error:
384
+ return CheckResult("version", False, str(error))
385
+ detail = result.stdout.strip() or result.stderr.strip()
386
+ if result.returncode == 0 and TESTED_CODEX_VERSION not in detail:
387
+ detail = _UNTESTED_VERSION_DETAIL.format(version=detail, tested=TESTED_CODEX_VERSION)
388
+ return CheckResult("version", result.returncode == 0, detail)
389
+
390
+ async def _check_login(self) -> CheckResult:
391
+ configured_provider = _custom_provider_check(self._codex_home)
392
+ if configured_provider is not None:
393
+ return configured_provider
394
+ try:
395
+ result = await self._runner.run(
396
+ LOGIN_STATUS_ARGV,
397
+ timeout=DOCTOR_TIMEOUT_SECONDS,
398
+ env=self._subprocess_environment(),
399
+ check=False,
400
+ )
401
+ except (FileNotFoundError, ShikiruError) as error:
402
+ return CheckResult("login", False, str(error))
403
+ lowered = f"{result.stdout}\n{result.stderr}".lower()
404
+ logged_in = (
405
+ result.returncode == 0
406
+ and _AUTH_NOT_LOGGED_IN_TEXT not in lowered
407
+ and any(text in lowered for text in _AUTH_LOGGED_IN_TEXTS)
408
+ )
409
+ detail = "authenticated" if logged_in else "not authenticated"
410
+ return CheckResult("login", logged_in, detail)
411
+
412
+
413
+ def _custom_provider_check(home: Path) -> CheckResult | None:
414
+ path = home / _CODEX_CONFIG_FILENAME
415
+ if not path.is_file():
416
+ return None
417
+ try:
418
+ config = cast("Mapping[str, object]", tomllib.loads(path.read_text(encoding="utf-8")))
419
+ except (OSError, tomllib.TOMLDecodeError) as error:
420
+ return CheckResult("login", False, f"could not inspect Codex config: {error}")
421
+ profile_name = config.get(_PROFILE_FIELD)
422
+ profiles_value = config.get(_PROFILES_FIELD)
423
+ profiles: Mapping[str, object] = (
424
+ cast("Mapping[str, object]", profiles_value) if isinstance(profiles_value, Mapping) else {}
425
+ )
426
+ profile_value: object | None = (
427
+ profiles.get(profile_name) if isinstance(profile_name, str) else None
428
+ )
429
+ profile: Mapping[str, object] = (
430
+ cast("Mapping[str, object]", profile_value) if isinstance(profile_value, Mapping) else {}
431
+ )
432
+ provider_name: object = profile.get(_MODEL_PROVIDER_FIELD, config.get(_MODEL_PROVIDER_FIELD))
433
+ providers_value = config.get(_MODEL_PROVIDERS_FIELD)
434
+ providers: Mapping[str, object] = (
435
+ cast("Mapping[str, object]", providers_value)
436
+ if isinstance(providers_value, Mapping)
437
+ else {}
438
+ )
439
+ custom_provider = isinstance(provider_name, str) and isinstance(
440
+ providers.get(provider_name), Mapping
441
+ )
442
+ base_url: object = profile.get(_OPENAI_BASE_URL_FIELD, config.get(_OPENAI_BASE_URL_FIELD))
443
+ custom_base_url = isinstance(base_url, str) and bool(base_url.strip())
444
+ if custom_provider or custom_base_url:
445
+ return CheckResult("login", True, _CUSTOM_PROVIDER_ACCESS_DETAIL)
446
+ return None
447
+
448
+
449
+ def _model_catalog_args_are_ambiguous(args: Sequence[str]) -> bool:
450
+ return any(
451
+ _catalog_argument_is_ambiguous(args, index, argument) for index, argument in enumerate(args)
452
+ )
453
+
454
+
455
+ def _catalog_argument_is_ambiguous(args: Sequence[str], index: int, argument: str) -> bool:
456
+ selector_flags = (MODEL_FLAG, MODEL_LONG_FLAG, EFFORT_LONG_FLAG, PROFILE_FLAG)
457
+ selector_prefixes = (f"{MODEL_LONG_FLAG}=", f"{PROFILE_FLAG}=", f"{EFFORT_LONG_FLAG}=")
458
+ if argument in selector_flags or argument.startswith(selector_prefixes):
459
+ return True
460
+ if argument.startswith(MODEL_FLAG) and argument != MODEL_FLAG:
461
+ return True
462
+ if argument in (CONFIG_FLAG, CONFIG_LONG_FLAG):
463
+ return _config_override_is_ambiguous(args, index + 1)
464
+ if argument.startswith(f"{CONFIG_LONG_FLAG}="):
465
+ return _config_value_is_ambiguous(argument.partition("=")[_PARTITION_VALUE_INDEX])
466
+ if argument.startswith(CONFIG_FLAG) and argument != CONFIG_FLAG:
467
+ return _config_value_is_ambiguous(argument[len(CONFIG_FLAG) :])
468
+ return False
469
+
470
+
471
+ def _config_override_is_ambiguous(args: Sequence[str], index: int) -> bool:
472
+ return index >= len(args) or _config_value_is_ambiguous(args[index])
473
+
474
+
475
+ def _config_value_is_ambiguous(value: str) -> bool:
476
+ key = value.partition("=")[0].strip()
477
+ return key in (MODEL_CONFIG_OVERRIDE, MODEL_OVERRIDE, REASONING_EFFORT_CONFIG)
478
+
479
+
480
+ def create(context: AgentContext) -> CodexAgent:
481
+ """Build the Codex plugin from validated `AgentContext.options`."""
482
+ try:
483
+ options = CodexOptions.model_validate(context.options)
484
+ except ValidationError as error:
485
+ raise options_error(CODEX_COMMAND, error) from error
486
+ return CodexAgent(context.runner, context.clock, options)
@@ -0,0 +1,166 @@
1
+ """Codex schema-constrained one-shot calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import tempfile
7
+ from collections.abc import Mapping
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any, cast
10
+
11
+ from shikiru.errors import HeadlessOutputError
12
+ from shikiru.plugins.agents.codex.options import (
13
+ CODEX_COMMAND,
14
+ CONFIG_FLAG,
15
+ EPHEMERAL_FLAG,
16
+ EXEC_SUBCOMMAND,
17
+ JSON_FLAG,
18
+ MODEL_FLAG,
19
+ OUTPUT_SCHEMA_FLAG,
20
+ READ_ONLY_SANDBOX,
21
+ REASONING_EFFORT_CONFIG,
22
+ SANDBOX_FLAG,
23
+ SKIP_GIT_REPO_CHECK_FLAG,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from datetime import timedelta
28
+
29
+ from shikiru.process import SubprocessRunner
30
+
31
+ _AGENT_MESSAGE_TYPE = "agent_message"
32
+ _ITEM_COMPLETED_TYPE = "item.completed"
33
+ _TYPE_FIELD = "type"
34
+ _ITEM_FIELD = "item"
35
+ _TEXT_FIELD = "text"
36
+ _CONTENT_FIELD = "content"
37
+ _SCHEMA_TYPE_FIELD = "type"
38
+ _OBJECT_TYPE = "object"
39
+ _ADDITIONAL_PROPERTIES_FIELD = "additionalProperties"
40
+ _STRICT_SCHEMA_VALUE = False
41
+ _INVALID_JSON_MESSAGE = "Codex headless output was not valid JSON"
42
+ _INVALID_OBJECT_MESSAGE = "Codex headless output was not an object"
43
+ _MISSING_AGENT_MESSAGE = "Codex headless output had no agent_message"
44
+ _STDIN_LINE_ENDING = "\n"
45
+
46
+
47
+ class CodexHeadless:
48
+ """Runs Codex's ephemeral JSON structured-output mode."""
49
+
50
+ def __init__(self, runner: SubprocessRunner, environment: Mapping[str, str]) -> None:
51
+ """Use `runner` and the Operator-provided environment."""
52
+ self._runner = runner
53
+ self._environment = dict(environment)
54
+
55
+ async def ask(
56
+ self,
57
+ prompt: str,
58
+ schema: Mapping[str, Any],
59
+ model: str,
60
+ effort: str | None,
61
+ cwd: Path,
62
+ timeout: timedelta,
63
+ ) -> Mapping[str, Any]:
64
+ """Return Codex's schema-constrained final `agent_message` value."""
65
+ with tempfile.TemporaryDirectory(prefix="shikiru-codex-") as directory:
66
+ schema_path = Path(directory) / "output-schema.json"
67
+ schema_path.write_text(
68
+ json.dumps(_strict_schema(schema), separators=(",", ":")), encoding="utf-8"
69
+ )
70
+ argv = [
71
+ CODEX_COMMAND,
72
+ EXEC_SUBCOMMAND,
73
+ EPHEMERAL_FLAG,
74
+ JSON_FLAG,
75
+ OUTPUT_SCHEMA_FLAG,
76
+ str(schema_path),
77
+ MODEL_FLAG,
78
+ model,
79
+ ]
80
+ if effort is not None:
81
+ argv.extend([CONFIG_FLAG, f'{REASONING_EFFORT_CONFIG}="{effort}"'])
82
+ argv.extend(
83
+ [
84
+ SANDBOX_FLAG,
85
+ READ_ONLY_SANDBOX,
86
+ SKIP_GIT_REPO_CHECK_FLAG,
87
+ ]
88
+ )
89
+ result = await self._runner.run(
90
+ argv,
91
+ timeout=timeout.total_seconds(),
92
+ cwd=cwd,
93
+ env=self._environment,
94
+ input_text=prompt
95
+ if prompt.endswith(_STDIN_LINE_ENDING)
96
+ else prompt + _STDIN_LINE_ENDING,
97
+ check=True,
98
+ )
99
+ return _parse_agent_message(result.stdout)
100
+
101
+
102
+ def _strict_schema(value: object) -> object:
103
+ if isinstance(value, Mapping):
104
+ typed_value = cast("Mapping[str, object]", value)
105
+ schema = {key: _strict_schema(item) for key, item in typed_value.items()}
106
+ if schema.get(_SCHEMA_TYPE_FIELD) == _OBJECT_TYPE:
107
+ schema.setdefault(_ADDITIONAL_PROPERTIES_FIELD, _STRICT_SCHEMA_VALUE)
108
+ return schema
109
+ if isinstance(value, list):
110
+ return [_strict_schema(item) for item in cast("list[object]", value)]
111
+ return value
112
+
113
+
114
+ def _parse_agent_message(stdout: str) -> Mapping[str, Any]:
115
+ final_message: str | None = None
116
+ for line in stdout.splitlines():
117
+ try:
118
+ raw = json.loads(line)
119
+ except json.JSONDecodeError:
120
+ continue
121
+ if not isinstance(raw, Mapping):
122
+ continue
123
+ payload = cast("Mapping[str, Any]", raw)
124
+ message = _message_from_payload(payload)
125
+ if message is None:
126
+ continue
127
+ final_message = message
128
+ if final_message is None:
129
+ raise HeadlessOutputError(_MISSING_AGENT_MESSAGE)
130
+ try:
131
+ decoded = json.loads(final_message)
132
+ except json.JSONDecodeError as error:
133
+ raise HeadlessOutputError(_INVALID_JSON_MESSAGE) from error
134
+ if not isinstance(decoded, Mapping):
135
+ raise HeadlessOutputError(_INVALID_OBJECT_MESSAGE)
136
+ return cast("Mapping[str, Any]", decoded)
137
+
138
+
139
+ def _message_from_payload(payload: Mapping[str, Any]) -> str | None:
140
+ if payload.get(_TYPE_FIELD) != _ITEM_COMPLETED_TYPE:
141
+ return None
142
+ item = payload.get(_ITEM_FIELD)
143
+ if isinstance(item, Mapping):
144
+ typed_item = cast("Mapping[str, Any]", item)
145
+ if typed_item.get(_TYPE_FIELD) == _AGENT_MESSAGE_TYPE:
146
+ return _text(typed_item)
147
+ return None
148
+
149
+
150
+ def _text(payload: Mapping[str, Any]) -> str | None:
151
+ value = payload.get(_TEXT_FIELD)
152
+ if isinstance(value, str):
153
+ return value
154
+ content = payload.get(_CONTENT_FIELD)
155
+ if isinstance(content, str):
156
+ return content
157
+ if isinstance(content, list):
158
+ parts: list[str] = []
159
+ for item in cast("list[object]", content):
160
+ if isinstance(item, Mapping):
161
+ part = cast("Mapping[str, Any]", item).get(_TEXT_FIELD)
162
+ if isinstance(part, str):
163
+ parts.append(part)
164
+ text = "".join(parts)
165
+ return text or None
166
+ return None
@@ -0,0 +1,14 @@
1
+ """Codex's local session data directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from shikiru.plugins.agents.codex.options import CODEX_HOME_ENV_VAR
9
+
10
+
11
+ def codex_home() -> Path:
12
+ """Return the Codex home used for rollout discovery."""
13
+ configured = os.environ.get(CODEX_HOME_ENV_VAR)
14
+ return Path(configured) if configured else Path.home() / ".codex"