cortexshift 0.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.
- cortexshift/__init__.py +10 -0
- cortexshift/__main__.py +6 -0
- cortexshift/adapters/__init__.py +22 -0
- cortexshift/adapters/command_runner.py +116 -0
- cortexshift/adapters/discovery.py +55 -0
- cortexshift/adapters/git/__init__.py +10 -0
- cortexshift/adapters/git/inspector.py +321 -0
- cortexshift/adapters/git/parser.py +140 -0
- cortexshift/adapters/headless_runner.py +92 -0
- cortexshift/adapters/process_runner.py +56 -0
- cortexshift/adapters/providers/__init__.py +4 -0
- cortexshift/adapters/providers/antigravity.py +530 -0
- cortexshift/adapters/providers/claude.py +375 -0
- cortexshift/adapters/providers/codex.py +434 -0
- cortexshift/adapters/sqlite/__init__.py +10 -0
- cortexshift/adapters/sqlite/migrations.py +268 -0
- cortexshift/adapters/sqlite/store.py +914 -0
- cortexshift/adapters/workspace_lease.py +123 -0
- cortexshift/application/__init__.py +42 -0
- cortexshift/application/checkpoint_builder.py +218 -0
- cortexshift/application/checkpoint_service.py +273 -0
- cortexshift/application/doctor.py +80 -0
- cortexshift/application/handoff_builder.py +281 -0
- cortexshift/application/handoff_renderer.py +430 -0
- cortexshift/application/handoff_service.py +66 -0
- cortexshift/application/init_service.py +86 -0
- cortexshift/application/locator.py +48 -0
- cortexshift/application/native_session.py +65 -0
- cortexshift/application/recovery_service.py +235 -0
- cortexshift/application/repository_service.py +146 -0
- cortexshift/application/resume_service.py +124 -0
- cortexshift/application/run_service.py +270 -0
- cortexshift/application/session_launcher.py +183 -0
- cortexshift/application/session_service.py +63 -0
- cortexshift/application/source_session.py +62 -0
- cortexshift/application/status_service.py +73 -0
- cortexshift/application/switch_service.py +671 -0
- cortexshift/application/task_service.py +201 -0
- cortexshift/application/task_workspace.py +152 -0
- cortexshift/cli/__init__.py +5 -0
- cortexshift/cli/app.py +2477 -0
- cortexshift/domain/__init__.py +153 -0
- cortexshift/domain/checkpoint.py +174 -0
- cortexshift/domain/doctor.py +68 -0
- cortexshift/domain/errors.py +277 -0
- cortexshift/domain/git.py +102 -0
- cortexshift/domain/handoff.py +241 -0
- cortexshift/domain/identifiers.py +27 -0
- cortexshift/domain/launch.py +58 -0
- cortexshift/domain/mcp_binding.py +81 -0
- cortexshift/domain/native_session.py +19 -0
- cortexshift/domain/project.py +37 -0
- cortexshift/domain/provider.py +67 -0
- cortexshift/domain/session.py +92 -0
- cortexshift/domain/status.py +40 -0
- cortexshift/domain/task.py +191 -0
- cortexshift/mcp/__init__.py +38 -0
- cortexshift/mcp/context.py +165 -0
- cortexshift/mcp/facade.py +513 -0
- cortexshift/mcp/models.py +178 -0
- cortexshift/mcp/resources.py +45 -0
- cortexshift/mcp/server.py +52 -0
- cortexshift/mcp/tools.py +176 -0
- cortexshift/ports/__init__.py +39 -0
- cortexshift/ports/checkpoint_store.py +45 -0
- cortexshift/ports/command_runner.py +56 -0
- cortexshift/ports/discovery.py +41 -0
- cortexshift/ports/handoff_delivery.py +91 -0
- cortexshift/ports/handoff_store.py +43 -0
- cortexshift/ports/headless_runner.py +58 -0
- cortexshift/ports/native_session.py +20 -0
- cortexshift/ports/process_runner.py +31 -0
- cortexshift/ports/provider.py +152 -0
- cortexshift/ports/repository.py +44 -0
- cortexshift/ports/session_store.py +27 -0
- cortexshift/ports/state_store.py +55 -0
- cortexshift/ports/workspace_lease.py +39 -0
- cortexshift/tui/__init__.py +24 -0
- cortexshift/tui/actions.py +58 -0
- cortexshift/tui/app.py +1051 -0
- cortexshift/tui/coordinator.py +173 -0
- cortexshift/tui/cortexshift.tcss +258 -0
- cortexshift/tui/facade.py +614 -0
- cortexshift/tui/modals.py +594 -0
- cortexshift/tui/models.py +503 -0
- cortexshift/tui/screens/__init__.py +81 -0
- cortexshift/tui/screens/checkpoints.py +188 -0
- cortexshift/tui/screens/handoffs.py +180 -0
- cortexshift/tui/screens/help.py +117 -0
- cortexshift/tui/screens/overview.py +200 -0
- cortexshift/tui/screens/providers.py +169 -0
- cortexshift/tui/screens/repository.py +143 -0
- cortexshift/tui/screens/sessions.py +146 -0
- cortexshift/tui/screens/task.py +174 -0
- cortexshift/tui/widgets.py +209 -0
- cortexshift-0.1.0.dist-info/METADATA +202 -0
- cortexshift-0.1.0.dist-info/RECORD +100 -0
- cortexshift-0.1.0.dist-info/WHEEL +4 -0
- cortexshift-0.1.0.dist-info/entry_points.txt +2 -0
- cortexshift-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""OpenAI Codex native CLI probe and discovery adapter."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from cortexshift.adapters.headless_runner import SubprocessHeadlessProviderRunner
|
|
11
|
+
from cortexshift.domain.doctor import AuthenticationStatus, ProviderDiagnostic
|
|
12
|
+
from cortexshift.domain.errors import HandoffDeliveryError, NativeResumeError
|
|
13
|
+
from cortexshift.domain.launch import LaunchSpecification
|
|
14
|
+
from cortexshift.domain.mcp_binding import McpSessionBinding
|
|
15
|
+
from cortexshift.domain.native_session import NativeSessionCapabilities, valid_native_id
|
|
16
|
+
from cortexshift.domain.provider import PROVIDER_CODEX, ProviderCapabilities, ProviderId
|
|
17
|
+
from cortexshift.ports.command_runner import CommandRunner
|
|
18
|
+
from cortexshift.ports.discovery import ProviderProbe
|
|
19
|
+
from cortexshift.ports.handoff_delivery import (
|
|
20
|
+
HandoffDeliveryPreparation,
|
|
21
|
+
HandoffDeliveryStrategy,
|
|
22
|
+
ProviderHandoffAdapter,
|
|
23
|
+
)
|
|
24
|
+
from cortexshift.ports.headless_runner import (
|
|
25
|
+
DEFAULT_HEADLESS_TIMEOUT_SECONDS,
|
|
26
|
+
HeadlessProviderRunner,
|
|
27
|
+
)
|
|
28
|
+
from cortexshift.ports.provider import ProviderRuntimeAdapter
|
|
29
|
+
|
|
30
|
+
_VERSION_RE = re.compile(r"(\d+\.\d+(?:\.\d+)?(?:[-.][a-zA-Z0-9]+)?)")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_version(text: str) -> str | None:
|
|
34
|
+
"""Extract a clean semver-like version string from CLI output."""
|
|
35
|
+
match = _VERSION_RE.search(text)
|
|
36
|
+
if match:
|
|
37
|
+
return match.group(1)
|
|
38
|
+
first_line = text.splitlines()[0].strip() if text else ""
|
|
39
|
+
return first_line[:32] if first_line and len(first_line) <= 32 else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _classify_codex_auth(stdout: str, stderr: str, exit_code: int) -> AuthenticationStatus:
|
|
43
|
+
"""Classify Codex auth status conservatively without leaking sensitive output."""
|
|
44
|
+
combined = f"{stdout}\n{stderr}".lower()
|
|
45
|
+
|
|
46
|
+
# Try parsing JSON output
|
|
47
|
+
if stdout.strip().startswith("{"):
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(stdout)
|
|
50
|
+
if isinstance(data, dict):
|
|
51
|
+
if data.get("authenticated") is True or data.get("status") == "logged_in":
|
|
52
|
+
return AuthenticationStatus.AUTHENTICATED
|
|
53
|
+
if data.get("authenticated") is False or data.get("status") == "logged_out":
|
|
54
|
+
return AuthenticationStatus.NOT_AUTHENTICATED
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
# Check for unauthenticated markers
|
|
59
|
+
if (
|
|
60
|
+
"not logged in" in combined
|
|
61
|
+
or "logged out" in combined
|
|
62
|
+
or "no active session" in combined
|
|
63
|
+
or "login required" in combined
|
|
64
|
+
or "run codex login" in combined
|
|
65
|
+
):
|
|
66
|
+
return AuthenticationStatus.NOT_AUTHENTICATED
|
|
67
|
+
|
|
68
|
+
# Check for authenticated markers
|
|
69
|
+
if exit_code == 0 and ("logged in" in combined or "authenticated" in combined):
|
|
70
|
+
return AuthenticationStatus.AUTHENTICATED
|
|
71
|
+
|
|
72
|
+
if exit_code == 0 and not combined.strip():
|
|
73
|
+
return AuthenticationStatus.AUTHENTICATED
|
|
74
|
+
|
|
75
|
+
if exit_code != 0 and ("unauthorized" in combined or "not authenticated" in combined):
|
|
76
|
+
return AuthenticationStatus.NOT_AUTHENTICATED
|
|
77
|
+
|
|
78
|
+
return AuthenticationStatus.UNKNOWN
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CodexProviderProbe(ProviderProbe):
|
|
82
|
+
"""Probe for detecting and diagnosing the Codex native CLI."""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
command_runner: CommandRunner,
|
|
87
|
+
which_fn: Callable[[str], str | None] = shutil.which,
|
|
88
|
+
) -> None:
|
|
89
|
+
self._runner = command_runner
|
|
90
|
+
self._which = which_fn
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def provider_id(self) -> ProviderId:
|
|
94
|
+
return PROVIDER_CODEX
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def display_name(self) -> str:
|
|
98
|
+
return "Codex"
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def executable(self) -> str:
|
|
102
|
+
return "codex"
|
|
103
|
+
|
|
104
|
+
def get_capabilities(self) -> ProviderCapabilities:
|
|
105
|
+
return ProviderCapabilities(
|
|
106
|
+
provider_id=self.provider_id,
|
|
107
|
+
display_name=self.display_name,
|
|
108
|
+
supports_interactive=True,
|
|
109
|
+
supports_headless=True,
|
|
110
|
+
supports_native_resume=True,
|
|
111
|
+
supports_structured_output=True,
|
|
112
|
+
supports_mcp=True,
|
|
113
|
+
supports_usage_metrics=True,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def probe(self) -> ProviderDiagnostic:
|
|
117
|
+
capabilities = self.get_capabilities()
|
|
118
|
+
resolved_path = self._which(self.executable)
|
|
119
|
+
|
|
120
|
+
if not resolved_path:
|
|
121
|
+
return ProviderDiagnostic(
|
|
122
|
+
provider_id=self.provider_id,
|
|
123
|
+
display_name=self.display_name,
|
|
124
|
+
executable=self.executable,
|
|
125
|
+
installed=False,
|
|
126
|
+
resolved_path=None,
|
|
127
|
+
version=None,
|
|
128
|
+
authentication_status=AuthenticationStatus.UNKNOWN,
|
|
129
|
+
capabilities=capabilities,
|
|
130
|
+
diagnostics=["Not found in PATH"],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 1. Version probe
|
|
134
|
+
version_result = self._runner.run([resolved_path, "--version"], timeout=5.0)
|
|
135
|
+
version: str | None = None
|
|
136
|
+
diagnostics: list[str] = []
|
|
137
|
+
|
|
138
|
+
if version_result.success:
|
|
139
|
+
version = _extract_version(version_result.stdout)
|
|
140
|
+
if not version:
|
|
141
|
+
diagnostics.append("Version output could not be parsed")
|
|
142
|
+
else:
|
|
143
|
+
if version_result.timed_out:
|
|
144
|
+
diagnostics.append("Version probe timed out")
|
|
145
|
+
else:
|
|
146
|
+
diagnostics.append("Version probe failed")
|
|
147
|
+
|
|
148
|
+
# 2. Authentication probe: passive `codex login status`
|
|
149
|
+
auth_result = self._runner.run([resolved_path, "login", "status"], timeout=5.0)
|
|
150
|
+
auth_status = _classify_codex_auth(
|
|
151
|
+
stdout=auth_result.stdout,
|
|
152
|
+
stderr=auth_result.stderr,
|
|
153
|
+
exit_code=auth_result.exit_code,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if auth_result.timed_out:
|
|
157
|
+
diagnostics.append("Authentication probe timed out")
|
|
158
|
+
elif auth_status == AuthenticationStatus.UNKNOWN and not auth_result.success:
|
|
159
|
+
diagnostics.append("Authentication probe failed")
|
|
160
|
+
|
|
161
|
+
return ProviderDiagnostic(
|
|
162
|
+
provider_id=self.provider_id,
|
|
163
|
+
display_name=self.display_name,
|
|
164
|
+
executable=self.executable,
|
|
165
|
+
installed=True,
|
|
166
|
+
resolved_path=resolved_path,
|
|
167
|
+
version=version,
|
|
168
|
+
authentication_status=auth_status,
|
|
169
|
+
capabilities=capabilities,
|
|
170
|
+
diagnostics=diagnostics,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
MCP_SERVER_KEY = "mcp_servers.cortexshift"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def build_codex_mcp_args(
|
|
178
|
+
python_executable: str | None = None,
|
|
179
|
+
binding: McpSessionBinding | None = None,
|
|
180
|
+
) -> list[str]:
|
|
181
|
+
"""Build the argument list of -c overrides for Codex MCP configuration.
|
|
182
|
+
|
|
183
|
+
Codex starts MCP servers with a sanitized environment plus the server's own declared
|
|
184
|
+
`env` table, so a managed binding must be stated here to reach the server at all.
|
|
185
|
+
Values are emitted as JSON, which Codex parses as TOML basic strings; this keeps
|
|
186
|
+
Windows paths, quotes, and non-ASCII characters intact.
|
|
187
|
+
"""
|
|
188
|
+
exe = python_executable or sys.executable
|
|
189
|
+
args = [
|
|
190
|
+
"-c",
|
|
191
|
+
f"{MCP_SERVER_KEY}.command={json.dumps(exe, ensure_ascii=False)}",
|
|
192
|
+
"-c",
|
|
193
|
+
f'{MCP_SERVER_KEY}.args=["-m", "cortexshift", "mcp", "serve"]',
|
|
194
|
+
"-c",
|
|
195
|
+
f"{MCP_SERVER_KEY}.required=true",
|
|
196
|
+
]
|
|
197
|
+
if binding is not None:
|
|
198
|
+
for name, value in binding.to_env().items():
|
|
199
|
+
args.extend(
|
|
200
|
+
["-c", f"{MCP_SERVER_KEY}.env.{name}={json.dumps(value, ensure_ascii=False)}"]
|
|
201
|
+
)
|
|
202
|
+
return args
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class CodexRuntimeAdapter(ProviderRuntimeAdapter):
|
|
206
|
+
"""Runtime adapter for launching OpenAI Codex interactive sessions."""
|
|
207
|
+
|
|
208
|
+
def __init__(self, python_executable: str | None = None) -> None:
|
|
209
|
+
self._python_executable = python_executable
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def provider_id(self) -> ProviderId:
|
|
213
|
+
return PROVIDER_CODEX
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def display_name(self) -> str:
|
|
217
|
+
return "Codex"
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def executable(self) -> str:
|
|
221
|
+
return "codex"
|
|
222
|
+
|
|
223
|
+
def get_capabilities(self) -> ProviderCapabilities:
|
|
224
|
+
return ProviderCapabilities(
|
|
225
|
+
provider_id=self.provider_id,
|
|
226
|
+
display_name=self.display_name,
|
|
227
|
+
supports_interactive=True,
|
|
228
|
+
supports_headless=True,
|
|
229
|
+
supports_native_resume=True,
|
|
230
|
+
supports_structured_output=True,
|
|
231
|
+
supports_mcp=True,
|
|
232
|
+
supports_usage_metrics=True,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def get_native_capabilities(self) -> NativeSessionCapabilities:
|
|
236
|
+
return NativeSessionCapabilities(
|
|
237
|
+
supports_exact_resume=True,
|
|
238
|
+
can_capture_native_id_during_bootstrap=True,
|
|
239
|
+
can_resume_with_followup_context=True,
|
|
240
|
+
requires_model_turn_for_handoff_resume=True,
|
|
241
|
+
supports_managed_new_session=True,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def build_exact_resume(
|
|
245
|
+
self, project_root: Path, executable_path: str, native_session_id: str
|
|
246
|
+
) -> LaunchSpecification:
|
|
247
|
+
if not valid_native_id(native_session_id):
|
|
248
|
+
raise NativeResumeError("Invalid native session identifier.")
|
|
249
|
+
mcp_args = build_codex_mcp_args(self._python_executable)
|
|
250
|
+
return LaunchSpecification(
|
|
251
|
+
provider_id=self.provider_id,
|
|
252
|
+
executable=executable_path,
|
|
253
|
+
cwd=project_root,
|
|
254
|
+
argv=[executable_path, *mcp_args, "resume", native_session_id],
|
|
255
|
+
native_session_id=native_session_id,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def build_launch_spec(
|
|
259
|
+
self,
|
|
260
|
+
project_root: Path,
|
|
261
|
+
executable_path: str,
|
|
262
|
+
prompt: str | None = None,
|
|
263
|
+
) -> LaunchSpecification:
|
|
264
|
+
"""Build argument vector for native Codex launch."""
|
|
265
|
+
mcp_args = build_codex_mcp_args(self._python_executable)
|
|
266
|
+
argv = [executable_path, *mcp_args]
|
|
267
|
+
prompt_supplied = False
|
|
268
|
+
if prompt is not None and prompt.strip():
|
|
269
|
+
argv.append(prompt)
|
|
270
|
+
prompt_supplied = True
|
|
271
|
+
|
|
272
|
+
return LaunchSpecification(
|
|
273
|
+
provider_id=self.provider_id,
|
|
274
|
+
executable=executable_path,
|
|
275
|
+
cwd=project_root,
|
|
276
|
+
argv=argv,
|
|
277
|
+
interactive=True,
|
|
278
|
+
initial_prompt_supported=True,
|
|
279
|
+
prompt_supplied=prompt_supplied,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def bind_managed_mcp(
|
|
283
|
+
self,
|
|
284
|
+
launch_spec: LaunchSpecification,
|
|
285
|
+
binding: McpSessionBinding,
|
|
286
|
+
) -> LaunchSpecification:
|
|
287
|
+
"""Restate the managed binding in the -c overrides that configure the MCP server."""
|
|
288
|
+
if launch_spec.provider_id != self.provider_id:
|
|
289
|
+
return launch_spec
|
|
290
|
+
|
|
291
|
+
argv = list(launch_spec.argv)
|
|
292
|
+
# A supplied prompt is arbitrary text in the same argv, so it is never scanned:
|
|
293
|
+
# a prompt that happens to read like an override must not be mistaken for one.
|
|
294
|
+
scan_end = len(argv) - 1 if launch_spec.prompt_supplied else len(argv)
|
|
295
|
+
|
|
296
|
+
remaining: list[str] = []
|
|
297
|
+
insert_at: int | None = None
|
|
298
|
+
index = 0
|
|
299
|
+
while index < len(argv):
|
|
300
|
+
is_override = (
|
|
301
|
+
argv[index] == "-c"
|
|
302
|
+
and index + 1 < scan_end
|
|
303
|
+
and argv[index + 1].startswith(f"{MCP_SERVER_KEY}.")
|
|
304
|
+
)
|
|
305
|
+
if is_override:
|
|
306
|
+
if insert_at is None:
|
|
307
|
+
insert_at = len(remaining)
|
|
308
|
+
index += 2
|
|
309
|
+
continue
|
|
310
|
+
remaining.append(argv[index])
|
|
311
|
+
index += 1
|
|
312
|
+
|
|
313
|
+
if insert_at is None:
|
|
314
|
+
return launch_spec
|
|
315
|
+
|
|
316
|
+
bound = build_codex_mcp_args(self._python_executable, binding=binding)
|
|
317
|
+
remaining[insert_at:insert_at] = bound
|
|
318
|
+
return launch_spec.model_copy(update={"argv": remaining})
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
CODEX_BOOTSTRAP_PREFIX = """This is a CortexShift handoff bootstrap.
|
|
322
|
+
Analyze and ingest the supplied context. Remain read-only.
|
|
323
|
+
Do not modify repository files. Do not run mutating commands.
|
|
324
|
+
The same native Codex session will be resumed immediately in the interactive TUI.
|
|
325
|
+
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class CodexHandoffAdapter(ProviderHandoffAdapter):
|
|
330
|
+
"""Capture native identity through one read-only JSONL turn, then open the TUI."""
|
|
331
|
+
|
|
332
|
+
def __init__(
|
|
333
|
+
self,
|
|
334
|
+
runtime_adapter: CodexRuntimeAdapter | None = None,
|
|
335
|
+
headless_runner: HeadlessProviderRunner | None = None,
|
|
336
|
+
timeout: float = DEFAULT_HEADLESS_TIMEOUT_SECONDS,
|
|
337
|
+
) -> None:
|
|
338
|
+
self._runtime = runtime_adapter or CodexRuntimeAdapter()
|
|
339
|
+
self._headless = headless_runner or SubprocessHeadlessProviderRunner()
|
|
340
|
+
self._timeout = timeout
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def provider_id(self) -> ProviderId:
|
|
344
|
+
return PROVIDER_CODEX
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def display_name(self) -> str:
|
|
348
|
+
return self._runtime.display_name
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def executable(self) -> str:
|
|
352
|
+
return self._runtime.executable
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def delivery_strategy(self) -> HandoffDeliveryStrategy:
|
|
356
|
+
return HandoffDeliveryStrategy.READ_ONLY_BOOTSTRAP_THEN_RESUME
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def bootstrap_model_turn_required(self) -> bool:
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
def bind_managed_mcp(
|
|
363
|
+
self,
|
|
364
|
+
launch_spec: LaunchSpecification,
|
|
365
|
+
binding: McpSessionBinding,
|
|
366
|
+
) -> LaunchSpecification:
|
|
367
|
+
"""Delegate managed MCP binding to the runtime adapter that built the argv."""
|
|
368
|
+
return self._runtime.bind_managed_mcp(launch_spec, binding)
|
|
369
|
+
|
|
370
|
+
def prepare_delivery(
|
|
371
|
+
self,
|
|
372
|
+
executable_path: str,
|
|
373
|
+
project_root: Path,
|
|
374
|
+
rendered_context: str,
|
|
375
|
+
native_session_id: str | None = None,
|
|
376
|
+
) -> HandoffDeliveryPreparation:
|
|
377
|
+
if native_session_id is not None and not valid_native_id(native_session_id):
|
|
378
|
+
raise NativeResumeError("Invalid native session identifier.")
|
|
379
|
+
# Sandbox is an exec parent option, JSON is also supported on exec resume.
|
|
380
|
+
argv = [executable_path, "exec", "--sandbox", "read-only"]
|
|
381
|
+
if native_session_id is not None:
|
|
382
|
+
argv.extend(["resume", "--json", native_session_id])
|
|
383
|
+
else:
|
|
384
|
+
argv.append("--json")
|
|
385
|
+
argv.append(CODEX_BOOTSTRAP_PREFIX + rendered_context)
|
|
386
|
+
result = self._headless.run_headless(
|
|
387
|
+
argv, project_root, timeout=self._timeout, env={"CORTEXSHIFT_MCP_READ_ONLY": "1"}
|
|
388
|
+
)
|
|
389
|
+
if result.timed_out:
|
|
390
|
+
raise HandoffDeliveryError("bootstrap_timeout", "Codex handoff bootstrap timed out.")
|
|
391
|
+
if result.not_found:
|
|
392
|
+
raise HandoffDeliveryError("spawn_failed", "Codex handoff bootstrap could not start.")
|
|
393
|
+
if result.exit_code != 0:
|
|
394
|
+
raise HandoffDeliveryError("bootstrap_failed", "Codex handoff bootstrap failed.")
|
|
395
|
+
|
|
396
|
+
captured: str | None = None
|
|
397
|
+
completed = False
|
|
398
|
+
try:
|
|
399
|
+
for line in result.stdout.splitlines():
|
|
400
|
+
if not line.strip():
|
|
401
|
+
continue
|
|
402
|
+
event = json.loads(line)
|
|
403
|
+
if not isinstance(event, dict):
|
|
404
|
+
raise ValueError
|
|
405
|
+
kind = event.get("type")
|
|
406
|
+
if kind in ("error", "turn.failed"):
|
|
407
|
+
raise HandoffDeliveryError("bootstrap_failed", "Codex handoff turn failed.")
|
|
408
|
+
if kind == "thread.started":
|
|
409
|
+
candidate = event.get("thread_id")
|
|
410
|
+
if not isinstance(candidate, str) or not valid_native_id(candidate):
|
|
411
|
+
raise ValueError
|
|
412
|
+
if captured is not None and captured != candidate:
|
|
413
|
+
raise ValueError
|
|
414
|
+
captured = candidate
|
|
415
|
+
if kind == "turn.completed":
|
|
416
|
+
completed = True
|
|
417
|
+
except (ValueError, TypeError):
|
|
418
|
+
raise HandoffDeliveryError(
|
|
419
|
+
"bootstrap_invalid_output", "Codex returned invalid bootstrap metadata."
|
|
420
|
+
) from None
|
|
421
|
+
if captured is None or not completed:
|
|
422
|
+
raise HandoffDeliveryError(
|
|
423
|
+
"bootstrap_invalid_output",
|
|
424
|
+
"Codex did not confirm a native thread and completed turn.",
|
|
425
|
+
)
|
|
426
|
+
if native_session_id is not None and captured != native_session_id:
|
|
427
|
+
raise HandoffDeliveryError(
|
|
428
|
+
"bootstrap_invalid_output", "Codex returned a different thread; refusing fallback."
|
|
429
|
+
)
|
|
430
|
+
return HandoffDeliveryPreparation(
|
|
431
|
+
launch_spec=self._runtime.build_exact_resume(project_root, executable_path, captured),
|
|
432
|
+
native_session_id=captured,
|
|
433
|
+
bootstrap_performed=True,
|
|
434
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""SQLite persistence adapter package."""
|
|
2
|
+
|
|
3
|
+
from cortexshift.adapters.sqlite.migrations import CURRENT_SCHEMA_VERSION, run_migrations
|
|
4
|
+
from cortexshift.adapters.sqlite.store import SQLiteStateStore
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"CURRENT_SCHEMA_VERSION",
|
|
8
|
+
"SQLiteStateStore",
|
|
9
|
+
"run_migrations",
|
|
10
|
+
]
|