cortex-loop 0.1.0a1__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 (52) hide show
  1. cortex/__init__.py +7 -0
  2. cortex/adapters.py +339 -0
  3. cortex/blocklist.py +51 -0
  4. cortex/challenges.py +210 -0
  5. cortex/cli.py +7 -0
  6. cortex/core.py +601 -0
  7. cortex/core_helpers.py +190 -0
  8. cortex/data/identity_preamble.md +5 -0
  9. cortex/data/layer1_part_a.md +65 -0
  10. cortex/data/layer1_part_b.md +17 -0
  11. cortex/executive.py +295 -0
  12. cortex/foundation.py +185 -0
  13. cortex/genome.py +348 -0
  14. cortex/graveyard.py +226 -0
  15. cortex/hooks/__init__.py +27 -0
  16. cortex/hooks/_shared.py +167 -0
  17. cortex/hooks/post_tool_use.py +13 -0
  18. cortex/hooks/pre_tool_use.py +13 -0
  19. cortex/hooks/session_start.py +13 -0
  20. cortex/hooks/stop.py +13 -0
  21. cortex/invariants.py +258 -0
  22. cortex/packs.py +118 -0
  23. cortex/repomap.py +6 -0
  24. cortex/requirements.py +497 -0
  25. cortex/retry.py +312 -0
  26. cortex/stop_contract.py +217 -0
  27. cortex/stop_payload.py +122 -0
  28. cortex/stop_policy.py +100 -0
  29. cortex/stop_runtime.py +400 -0
  30. cortex/stop_signals.py +75 -0
  31. cortex/store.py +793 -0
  32. cortex/templates/__init__.py +10 -0
  33. cortex/utils.py +58 -0
  34. cortex_loop-0.1.0a1.dist-info/METADATA +121 -0
  35. cortex_loop-0.1.0a1.dist-info/RECORD +52 -0
  36. cortex_loop-0.1.0a1.dist-info/WHEEL +5 -0
  37. cortex_loop-0.1.0a1.dist-info/entry_points.txt +3 -0
  38. cortex_loop-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  39. cortex_loop-0.1.0a1.dist-info/top_level.txt +3 -0
  40. cortex_ops_cli/__init__.py +3 -0
  41. cortex_ops_cli/_adapter_validation.py +119 -0
  42. cortex_ops_cli/_check_report.py +454 -0
  43. cortex_ops_cli/_check_report_output.py +270 -0
  44. cortex_ops_cli/_openai_bridge_probe.py +241 -0
  45. cortex_ops_cli/_openai_bridge_protocol.py +469 -0
  46. cortex_ops_cli/_runtime_profile_templates.py +341 -0
  47. cortex_ops_cli/_runtime_profiles.py +445 -0
  48. cortex_ops_cli/gemini_hooks.py +301 -0
  49. cortex_ops_cli/main.py +911 -0
  50. cortex_ops_cli/openai_app_server_bridge.py +375 -0
  51. cortex_repomap/__init__.py +1 -0
  52. cortex_repomap/engine.py +1201 -0
@@ -0,0 +1,445 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shlex
6
+ import shutil
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Any, Callable
10
+
11
+ from ._runtime_profile_templates import (
12
+ render_claude_md,
13
+ render_claude_settings_json,
14
+ render_gemini_md,
15
+ render_gemini_settings_json,
16
+ render_openai_bridge_profile_json,
17
+ render_openai_md,
18
+ )
19
+
20
+ CLAUDE_REQUIRED_HOOK_COMMANDS = {
21
+ "PreToolUse": "cortex.hooks.pre_tool_use",
22
+ "PostToolUse": "cortex.hooks.post_tool_use",
23
+ "Stop": "cortex.hooks.stop",
24
+ }
25
+ CLAUDE_HOOK_SCHEMA_NATIVE = "claude_native_v1"
26
+ CLAUDE_HOOK_SCHEMA_LEGACY = "legacy_json_v0"
27
+ CLAUDE_SUPPORTED_HOOK_SCHEMAS = {CLAUDE_HOOK_SCHEMA_NATIVE, CLAUDE_HOOK_SCHEMA_LEGACY}
28
+ CLAUDE_PINNED_HOOK_SCHEMA = CLAUDE_HOOK_SCHEMA_NATIVE
29
+ GEMINI_REQUIRED_HOOK_COMMANDS = {
30
+ "SessionStart": "cortex_ops_cli.gemini_hooks SessionStart",
31
+ "BeforeTool": "cortex_ops_cli.gemini_hooks BeforeTool",
32
+ "AfterTool": "cortex_ops_cli.gemini_hooks AfterTool",
33
+ "AfterAgent": "cortex_ops_cli.gemini_hooks AfterAgent",
34
+ }
35
+ GEMINI_OPTIONAL_HOOK_COMMANDS = {
36
+ "BeforeAgent": "cortex_ops_cli.gemini_hooks BeforeAgent",
37
+ }
38
+ GEMINI_AFTER_AGENT_BRIDGE_PATTERN = "cortex_ops_cli.gemini_hooks AfterAgent"
39
+ CLAUDE_DIRNAME, LEGACY_CLAUDE_DIRNAME = ".claude", "claude"
40
+ GEMINI_DIRNAME = ".gemini"
41
+ OPENAI_DIRNAME = ".codex"
42
+ OPENAI_BRIDGE_PROFILE_FILENAME = "cortex_openai_bridge.json"
43
+ OPENAI_BRIDGE_SCHEMA_VERSION = "openai_app_server_v1"
44
+ OPENAI_BRIDGE_COMMAND_PATTERN = "cortex_ops_cli.openai_app_server_bridge run"
45
+ OPENAI_CODEX_MIN_VERSION = (0, 111, 0)
46
+ OPENAI_CODEX_MIN_VERSION_LABEL = "0.111.0"
47
+ CLAUDE_ADAPTER_PATH = "cortex.adapters.claude:ClaudeAdapter"
48
+ CLAUDE_ADAPTER_ALIASES = {CLAUDE_ADAPTER_PATH, "cortex.adapters:ClaudeAdapter"}
49
+ GEMINI_ADAPTER_PATH = "cortex.adapters.gemini:GeminiAdapter"
50
+ GEMINI_ADAPTER_ALIASES = {GEMINI_ADAPTER_PATH, "cortex.adapters:GeminiAdapter"}
51
+ OPENAI_ADAPTER_PATH = "cortex.adapters.openai:OpenAIAdapter"
52
+ OPENAI_ADAPTER_ALIASES = {OPENAI_ADAPTER_PATH, "cortex.adapters:OpenAIAdapter"}
53
+
54
+
55
+ def runtime_profile_install_spec(
56
+ *,
57
+ root: Path,
58
+ profile: str,
59
+ python_executable: str | None,
60
+ ) -> tuple[Path, dict[Path, str], str]:
61
+ normalized = str(profile).strip().lower()
62
+ if normalized == "claude":
63
+ runtime_dir = root / CLAUDE_DIRNAME
64
+ return (
65
+ runtime_dir,
66
+ {
67
+ runtime_dir / "settings.json": render_claude_settings_json(
68
+ python_executable=python_executable,
69
+ schema_version=CLAUDE_PINNED_HOOK_SCHEMA,
70
+ ),
71
+ runtime_dir / "CLAUDE.md": render_claude_md(),
72
+ },
73
+ CLAUDE_ADAPTER_PATH,
74
+ )
75
+ if normalized == "gemini":
76
+ runtime_dir = root / GEMINI_DIRNAME
77
+ return (
78
+ runtime_dir,
79
+ {
80
+ runtime_dir / "settings.json": render_gemini_settings_json(
81
+ python_executable=python_executable
82
+ ),
83
+ runtime_dir / "GEMINI.md": render_gemini_md(),
84
+ },
85
+ GEMINI_ADAPTER_PATH,
86
+ )
87
+ if normalized == "openai":
88
+ runtime_dir = root / OPENAI_DIRNAME
89
+ return (
90
+ runtime_dir,
91
+ {
92
+ runtime_dir / OPENAI_BRIDGE_PROFILE_FILENAME: render_openai_bridge_profile_json(
93
+ python_executable=python_executable,
94
+ schema_version=OPENAI_BRIDGE_SCHEMA_VERSION,
95
+ ),
96
+ runtime_dir / "OPENAI.md": render_openai_md(),
97
+ },
98
+ OPENAI_ADAPTER_PATH,
99
+ )
100
+ raise ValueError(f"Unsupported runtime profile: {profile}")
101
+
102
+
103
+ def set_runtime_adapter(config_path: Path, adapter_path: str) -> str:
104
+ if not config_path.exists():
105
+ raise OSError(f"Missing config file: {config_path}")
106
+ lines = config_path.read_text(encoding="utf-8").splitlines()
107
+ out: list[str] = []
108
+ in_runtime = False
109
+ runtime_seen = False
110
+ adapter_written = False
111
+ for line in lines:
112
+ stripped = line.strip()
113
+ section = stripped.startswith("[") and stripped.endswith("]")
114
+ if section:
115
+ if in_runtime and not adapter_written:
116
+ out.append(f'adapter = "{adapter_path}"')
117
+ adapter_written = True
118
+ in_runtime = stripped == "[runtime]"
119
+ runtime_seen = runtime_seen or in_runtime
120
+ out.append(line)
121
+ continue
122
+ if in_runtime and stripped.startswith("adapter"):
123
+ if not adapter_written:
124
+ out.append(f'adapter = "{adapter_path}"')
125
+ adapter_written = True
126
+ continue
127
+ out.append(line)
128
+ if in_runtime and not adapter_written:
129
+ out.append(f'adapter = "{adapter_path}"')
130
+ adapter_written = True
131
+ if not runtime_seen:
132
+ if out and out[-1].strip():
133
+ out.append("")
134
+ out.extend(["[runtime]", f'adapter = "{adapter_path}"'])
135
+ rendered = "\n".join(out).rstrip() + "\n"
136
+ before = config_path.read_text(encoding="utf-8")
137
+ if rendered == before:
138
+ return "unchanged"
139
+ config_path.write_text(rendered, encoding="utf-8")
140
+ return "updated"
141
+
142
+
143
+ def resolve_claude_settings_path(root: Path) -> tuple[Path | None, Path | None]:
144
+ preferred = root / CLAUDE_DIRNAME / "settings.json"
145
+ legacy = root / LEGACY_CLAUDE_DIRNAME / "settings.json"
146
+ if preferred.exists():
147
+ return preferred, legacy
148
+ if legacy.exists():
149
+ return legacy, None
150
+ return None, legacy
151
+
152
+
153
+ def validate_claude_settings(settings_path: Path) -> tuple[list[str], list[str]]:
154
+ errors: list[str] = []
155
+ warnings: list[str] = []
156
+ try:
157
+ data = json.loads(settings_path.read_text(encoding="utf-8"))
158
+ except (json.JSONDecodeError, OSError) as exc:
159
+ return [f"Failed to read {settings_path}: {exc}"], []
160
+
161
+ hooks = data.get("hooks")
162
+ if not isinstance(hooks, dict):
163
+ return [f"Invalid {settings_path}: missing top-level hooks object"], []
164
+
165
+ for event_name, command_fragment in CLAUDE_REQUIRED_HOOK_COMMANDS.items():
166
+ event_entries = hooks.get(event_name)
167
+ if not isinstance(event_entries, list):
168
+ errors.append(f"Invalid {settings_path}: missing hooks.{event_name} list")
169
+ continue
170
+ commands = _event_hook_commands(event_entries)
171
+ if not any(command_fragment in command for command in commands):
172
+ errors.append(
173
+ f"Invalid {settings_path}: hooks.{event_name} does not contain "
174
+ f"command fragment '{command_fragment}'"
175
+ )
176
+ continue
177
+ matched = [command for command in commands if command_fragment in command]
178
+ schema_versions = {_command_schema_version(command) for command in matched}
179
+ schema_versions.discard(None)
180
+ if not schema_versions:
181
+ errors.append(
182
+ f"Invalid {settings_path}: hooks.{event_name} command must pin --schema-version "
183
+ f"{CLAUDE_PINNED_HOOK_SCHEMA}"
184
+ )
185
+ continue
186
+ if len(schema_versions) != 1:
187
+ errors.append(
188
+ f"Invalid {settings_path}: hooks.{event_name} has mixed schema versions: "
189
+ + ", ".join(sorted(str(item) for item in schema_versions))
190
+ )
191
+ continue
192
+ schema_version = next(iter(schema_versions))
193
+ if schema_version not in CLAUDE_SUPPORTED_HOOK_SCHEMAS:
194
+ errors.append(
195
+ f"Invalid {settings_path}: hooks.{event_name} schema version '{schema_version}' "
196
+ f"is unsupported (supported: {', '.join(sorted(CLAUDE_SUPPORTED_HOOK_SCHEMAS))})"
197
+ )
198
+ continue
199
+ if schema_version != CLAUDE_PINNED_HOOK_SCHEMA:
200
+ errors.append(
201
+ f"Invalid {settings_path}: hooks.{event_name} schema version '{schema_version}' "
202
+ f"does not match pinned runtime schema '{CLAUDE_PINNED_HOOK_SCHEMA}'"
203
+ )
204
+ return errors, warnings
205
+
206
+
207
+ def resolve_gemini_settings_path(root: Path) -> Path | None:
208
+ path = root / GEMINI_DIRNAME / "settings.json"
209
+ return path if path.exists() else None
210
+
211
+
212
+ def validate_gemini_settings(settings_path: Path) -> tuple[list[str], list[str]]:
213
+ errors: list[str] = []
214
+ warnings: list[str] = []
215
+ try:
216
+ data = json.loads(settings_path.read_text(encoding="utf-8"))
217
+ except (json.JSONDecodeError, OSError) as exc:
218
+ return [f"Failed to read {settings_path}: {exc}"], []
219
+
220
+ hooks_config = data.get("hooksConfig")
221
+ if isinstance(hooks_config, dict) and hooks_config.get("enabled") is False:
222
+ errors.append(
223
+ "Gemini hooks are globally disabled. "
224
+ "Cortex cannot enforce without hooks. Set hooksConfig.enabled to true or remove the field (defaults to true).",
225
+ )
226
+
227
+ hooks = data.get("hooks")
228
+ if not isinstance(hooks, dict):
229
+ errors.append(f"Invalid {settings_path}: missing top-level hooks object")
230
+ return errors, warnings
231
+
232
+ after_agent_entries = hooks.get("AfterAgent")
233
+ if not isinstance(after_agent_entries, list) or not after_agent_entries:
234
+ errors.append("No AfterAgent hook configured. Cortex stop path cannot fire without this hook.")
235
+ after_agent_configs = _flatten_hook_configs(
236
+ after_agent_entries if isinstance(after_agent_entries, list) else []
237
+ )
238
+ if isinstance(after_agent_entries, list) and after_agent_entries and not after_agent_configs:
239
+ errors.append("No AfterAgent hook configured. Cortex stop path cannot fire without this hook.")
240
+ if after_agent_configs and not all(_is_command_hook(config) for config in after_agent_configs):
241
+ errors.append(
242
+ "AfterAgent hook configuration is invalid. Each hook config requires type: 'command' and a command string."
243
+ )
244
+ after_agent_commands = [
245
+ str(config.get("command") or "").strip()
246
+ for config in after_agent_configs
247
+ if str(config.get("command") or "").strip()
248
+ ]
249
+ if after_agent_commands and not any(
250
+ GEMINI_AFTER_AGENT_BRIDGE_PATTERN in command for command in after_agent_commands
251
+ ):
252
+ warnings.append(
253
+ "AfterAgent hook command does not appear to be a Cortex hook bridge. "
254
+ "Stop path enforcement may not work. Expected command containing "
255
+ f"'{GEMINI_AFTER_AGENT_BRIDGE_PATTERN}'."
256
+ )
257
+
258
+ before_tool_entries = hooks.get("BeforeTool")
259
+ if not isinstance(before_tool_entries, list) or not before_tool_entries:
260
+ warnings.append("No BeforeTool hook. Tool blocklist enforcement will not run.")
261
+ elif not _flatten_hook_configs(before_tool_entries):
262
+ warnings.append("No BeforeTool hook. Tool blocklist enforcement will not run.")
263
+
264
+ after_tool_entries = hooks.get("AfterTool")
265
+ if not isinstance(after_tool_entries, list) or not after_tool_entries:
266
+ warnings.append("No AfterTool hook. Post-tool failure detection will not run.")
267
+ elif not _flatten_hook_configs(after_tool_entries):
268
+ warnings.append("No AfterTool hook. Post-tool failure detection will not run.")
269
+
270
+ before_agent_entries = hooks.get("BeforeAgent")
271
+ if not isinstance(before_agent_entries, list) or not before_agent_entries:
272
+ warnings.append("No BeforeAgent hook. Per-turn persistent executive anchor injection will not run.")
273
+ elif not _flatten_hook_configs(before_agent_entries):
274
+ warnings.append("No BeforeAgent hook. Per-turn persistent executive anchor injection will not run.")
275
+
276
+ project_root = settings_path.parent.parent
277
+ for command in _hook_commands(hooks):
278
+ if _command_references_missing_path(command, project_root):
279
+ warnings.append(
280
+ f"Hook command may reference a missing path: {command}. Hooks may fail at runtime."
281
+ )
282
+ return errors, warnings
283
+
284
+
285
+ def resolve_openai_bridge_profile_path(root: Path) -> Path | None:
286
+ path = root / OPENAI_DIRNAME / OPENAI_BRIDGE_PROFILE_FILENAME
287
+ return path if path.exists() else None
288
+
289
+
290
+ def validate_openai_bridge_profile(
291
+ profile_path: Path,
292
+ *,
293
+ which: Callable[[str], str | None] = shutil.which,
294
+ run_exec: Callable[..., Any] = subprocess.run,
295
+ ) -> tuple[list[str], list[str]]:
296
+ errors: list[str] = []
297
+ warnings: list[str] = []
298
+ try:
299
+ data = json.loads(profile_path.read_text(encoding="utf-8"))
300
+ except (json.JSONDecodeError, OSError) as exc:
301
+ return [f"Failed to read {profile_path}: {exc}"], []
302
+
303
+ if not isinstance(data, dict):
304
+ return [f"Invalid {profile_path}: expected JSON object"], []
305
+
306
+ schema_version = str(data.get("schema_version") or "").strip()
307
+ if schema_version != OPENAI_BRIDGE_SCHEMA_VERSION:
308
+ errors.append(
309
+ f"Invalid {profile_path}: schema_version must be '{OPENAI_BRIDGE_SCHEMA_VERSION}'."
310
+ )
311
+
312
+ bridge = data.get("bridge")
313
+ if not isinstance(bridge, dict):
314
+ errors.append(f"Invalid {profile_path}: missing bridge object.")
315
+ return errors, warnings
316
+
317
+ command = str(bridge.get("command") or "").strip()
318
+ if not command:
319
+ errors.append(f"Invalid {profile_path}: bridge.command is required.")
320
+ return errors, warnings
321
+ if OPENAI_BRIDGE_COMMAND_PATTERN not in command:
322
+ errors.append(
323
+ f"Invalid {profile_path}: bridge.command must contain '{OPENAI_BRIDGE_COMMAND_PATTERN}'."
324
+ )
325
+ command_schema = _command_schema_version(command)
326
+ if command_schema != OPENAI_BRIDGE_SCHEMA_VERSION:
327
+ errors.append(
328
+ f"Invalid {profile_path}: bridge.command must pin --schema-version {OPENAI_BRIDGE_SCHEMA_VERSION}."
329
+ )
330
+
331
+ codex_bin = str(bridge.get("codex_bin") or "").strip()
332
+ if not codex_bin:
333
+ warnings.append(
334
+ f"{profile_path}: bridge.codex_bin is empty; runtime will fallback to 'codex' on PATH."
335
+ )
336
+ return errors, warnings
337
+
338
+ resolved = which(codex_bin)
339
+ if resolved is None:
340
+ errors.append(f"{profile_path}: bridge.codex_bin '{codex_bin}' is not on PATH.")
341
+ return errors, warnings
342
+
343
+ try:
344
+ version_run = run_exec(
345
+ [codex_bin, "--version"],
346
+ capture_output=True,
347
+ text=True,
348
+ timeout=5,
349
+ check=False,
350
+ )
351
+ except Exception as exc: # noqa: BLE001
352
+ warnings.append(f"{profile_path}: failed to run '{codex_bin} --version': {exc}")
353
+ return errors, warnings
354
+
355
+ version_text = f"{version_run.stdout}\n{version_run.stderr}"
356
+ version_tuple = _parse_semver_tuple(version_text)
357
+ if version_tuple is None:
358
+ warnings.append(
359
+ f"{profile_path}: unable to parse Codex version from '{codex_bin} --version' output."
360
+ )
361
+ return errors, warnings
362
+ if version_tuple < OPENAI_CODEX_MIN_VERSION:
363
+ warnings.append(
364
+ f"{profile_path}: Codex version {version_tuple[0]}.{version_tuple[1]}.{version_tuple[2]} is below tested baseline {OPENAI_CODEX_MIN_VERSION_LABEL}."
365
+ )
366
+ return errors, warnings
367
+
368
+
369
+ def _parse_semver_tuple(text: str) -> tuple[int, int, int] | None:
370
+ match = re.search(r"(\d+)\.(\d+)\.(\d+)", str(text))
371
+ if not match:
372
+ return None
373
+ return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
374
+
375
+
376
+ def _flatten_hook_configs(event_entries: list[Any]) -> list[dict[str, Any]]:
377
+ configs: list[dict[str, Any]] = []
378
+ for entry in event_entries:
379
+ if not isinstance(entry, dict):
380
+ continue
381
+ hook_items = entry.get("hooks")
382
+ if not isinstance(hook_items, list):
383
+ continue
384
+ for config in hook_items:
385
+ if isinstance(config, dict):
386
+ configs.append(config)
387
+ return configs
388
+
389
+
390
+ def _is_command_hook(config: dict[str, Any]) -> bool:
391
+ return (
392
+ str(config.get("type") or "").strip() == "command"
393
+ and bool(str(config.get("command") or "").strip())
394
+ )
395
+
396
+
397
+ def _hook_commands(hooks: dict[str, Any]) -> list[str]:
398
+ commands: list[str] = []
399
+ for event_name in [*GEMINI_REQUIRED_HOOK_COMMANDS, *GEMINI_OPTIONAL_HOOK_COMMANDS]:
400
+ event_entries = hooks.get(event_name)
401
+ if not isinstance(event_entries, list):
402
+ continue
403
+ for config in _flatten_hook_configs(event_entries):
404
+ command = str(config.get("command") or "").strip()
405
+ if command:
406
+ commands.append(command)
407
+ return commands
408
+
409
+
410
+ def _command_references_missing_path(command: str, root: Path) -> bool:
411
+ tokens = str(command).split()
412
+ for token in tokens:
413
+ candidate = token.strip().strip("'\"")
414
+ if not candidate or "/" not in candidate:
415
+ continue
416
+ if candidate.startswith("-"):
417
+ continue
418
+ path = Path(candidate)
419
+ if path.is_absolute():
420
+ if path.exists():
421
+ continue
422
+ return True
423
+ if (root / path).exists() or path.exists():
424
+ continue
425
+ return True
426
+ return False
427
+
428
+
429
+ def _event_hook_commands(event_entries: list[Any]) -> list[str]:
430
+ commands: list[str] = []
431
+ for config in _flatten_hook_configs(event_entries):
432
+ command = str(config.get("command") or "").strip()
433
+ if command:
434
+ commands.append(command)
435
+ return commands
436
+
437
+
438
+ def _command_schema_version(command: str) -> str | None:
439
+ parts = shlex.split(command)
440
+ for idx, token in enumerate(parts):
441
+ if token == "--schema-version" and idx + 1 < len(parts):
442
+ return str(parts[idx + 1]).strip() or None
443
+ if token.startswith("--schema-version="):
444
+ return token.split("=", 1)[1].strip() or None
445
+ return None