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
cortex/foundation.py ADDED
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from collections import Counter
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path, PurePosixPath
8
+ from typing import Iterable
9
+
10
+ from .genome import FoundationConfig
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class FoundationFinding:
15
+ path: str
16
+ churn_count: int
17
+ level: str
18
+
19
+ def to_dict(self) -> dict[str, object]:
20
+ return {"path": self.path, "churn_count": self.churn_count, "level": self.level}
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class FoundationReport:
25
+ generated_at: str
26
+ enabled: bool
27
+ git_available: bool
28
+ watch_paths: list[str]
29
+ warnings: list[str] = field(default_factory=list)
30
+ findings: list[FoundationFinding] = field(default_factory=list)
31
+
32
+ def to_dict(self) -> dict[str, object]:
33
+ return {
34
+ "generated_at": self.generated_at,
35
+ "enabled": self.enabled,
36
+ "git_available": self.git_available,
37
+ "watch_paths": self.watch_paths,
38
+ "warnings": self.warnings,
39
+ "findings": [f.to_dict() for f in self.findings],
40
+ }
41
+
42
+ def by_path(self) -> dict[str, FoundationFinding]:
43
+ return {finding.path: finding for finding in self.findings}
44
+
45
+
46
+ class FoundationAnalyzer:
47
+ def __init__(self, repo_root: Path, config: FoundationConfig) -> None:
48
+ self.repo_root = repo_root
49
+ self.config = config
50
+
51
+ def analyze(self) -> FoundationReport:
52
+ now = datetime.now(timezone.utc).isoformat()
53
+ if not self.config.enabled:
54
+ return FoundationReport(
55
+ generated_at=now,
56
+ enabled=False,
57
+ git_available=False,
58
+ watch_paths=list(self.config.watch_paths),
59
+ warnings=["Foundation analysis disabled in cortex.toml."],
60
+ )
61
+
62
+ git_repo, repo_warning = self._is_git_repo()
63
+ if not git_repo:
64
+ return FoundationReport(
65
+ generated_at=now,
66
+ enabled=True,
67
+ git_available=False,
68
+ watch_paths=list(self.config.watch_paths),
69
+ warnings=[repo_warning or "Git repository not detected; skipping churn analysis."],
70
+ )
71
+
72
+ counts, git_available, churn_warning = self._collect_churn_counts()
73
+ findings: list[FoundationFinding] = []
74
+ warnings: list[str] = [churn_warning] if churn_warning else []
75
+ for path, count in counts.most_common():
76
+ level = ""
77
+ if count >= self.config.stability_thresholds.high_churn_count:
78
+ level = "high"
79
+ elif count >= self.config.stability_thresholds.warn_churn_count:
80
+ level = "warn"
81
+ else:
82
+ continue
83
+ findings.append(FoundationFinding(path=path, churn_count=count, level=level))
84
+
85
+ if findings:
86
+ warnings.append(
87
+ f"Foundation analysis found {len(findings)} churn-heavy files in watched paths."
88
+ )
89
+
90
+ return FoundationReport(
91
+ generated_at=now,
92
+ enabled=True,
93
+ git_available=git_available,
94
+ watch_paths=list(self.config.watch_paths),
95
+ warnings=warnings,
96
+ findings=findings,
97
+ )
98
+
99
+ def warnings_for_target_files(self, target_files: Iterable[str]) -> list[str]:
100
+ report = self.analyze()
101
+ if not report.findings:
102
+ return report.warnings
103
+
104
+ target_set = {self._norm_path(path) for path in target_files if path}
105
+ if not target_set:
106
+ return report.warnings
107
+
108
+ findings = report.by_path()
109
+ matched: list[str] = []
110
+ for path in sorted(target_set):
111
+ finding = findings.get(path)
112
+ if finding is None:
113
+ continue
114
+ matched.append(
115
+ f"Target file {path} is {finding.level}-churn ({finding.churn_count} touches in recent window)."
116
+ )
117
+ return report.warnings + matched
118
+
119
+ def _is_git_repo(self) -> tuple[bool, str | None]:
120
+ timeout_ms = self._git_timeout_millis()
121
+ try:
122
+ result = subprocess.run(
123
+ ["git", "rev-parse", "--is-inside-work-tree"],
124
+ cwd=self.repo_root,
125
+ capture_output=True,
126
+ text=True,
127
+ check=False,
128
+ timeout=timeout_ms / 1000.0,
129
+ )
130
+ except FileNotFoundError:
131
+ return False, "Git binary not found; skipping churn analysis."
132
+ except subprocess.TimeoutExpired:
133
+ return (
134
+ False,
135
+ f"Git repository detection timed out after {timeout_ms}ms; skipping churn analysis.",
136
+ )
137
+ if result.returncode == 0 and result.stdout.strip() == "true":
138
+ return True, None
139
+ return False, "Git repository not detected; skipping churn analysis."
140
+
141
+ def _collect_churn_counts(self) -> tuple[Counter[str], bool, str | None]:
142
+ timeout_ms = self._git_timeout_millis()
143
+ cmd = ["git", "log", "--name-only", "--pretty=format:", f"-n{self.config.churn_window_commits}"]
144
+ cmd.extend(["--", *self.config.watch_paths])
145
+ try:
146
+ result = subprocess.run(
147
+ cmd,
148
+ cwd=self.repo_root,
149
+ capture_output=True,
150
+ text=True,
151
+ check=False,
152
+ timeout=timeout_ms / 1000.0,
153
+ )
154
+ except FileNotFoundError:
155
+ return Counter(), False, "Git binary not found; skipping churn analysis."
156
+ except subprocess.TimeoutExpired:
157
+ return (
158
+ Counter(),
159
+ False,
160
+ f"Git churn scan timed out after {timeout_ms}ms; skipping churn analysis.",
161
+ )
162
+ if result.returncode != 0:
163
+ return Counter(), True, "Git churn scan failed; skipping churn analysis."
164
+
165
+ counts: Counter[str] = Counter()
166
+ for raw in result.stdout.splitlines():
167
+ path = raw.strip()
168
+ if not path:
169
+ continue
170
+ norm = self._norm_path(path)
171
+ if self._ignored(norm):
172
+ continue
173
+ counts[norm] += 1
174
+ return counts, True, None
175
+
176
+ def _ignored(self, path: str) -> bool:
177
+ parts = set(PurePosixPath(path).parts)
178
+ return any(ignored in parts for ignored in self.config.ignored_dirs)
179
+
180
+ @staticmethod
181
+ def _norm_path(path: str) -> str:
182
+ return str(PurePosixPath(path))
183
+
184
+ def _git_timeout_millis(self) -> int:
185
+ return max(1, int(self.config.git_timeout_ms))
cortex/genome.py ADDED
@@ -0,0 +1,348 @@
1
+ from __future__ import annotations
2
+
3
+ import tomllib
4
+ from dataclasses import asdict, dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any, Iterable
7
+
8
+ from .packs import load_pack_overlay
9
+ from .utils import _as_bool
10
+
11
+ GENOME_SCHEMA_VERSION = "cortex_toml_v1"
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class ProjectConfig:
16
+ name: str = "unknown-project"
17
+ type: str = "generic"
18
+ root: str = "."
19
+ trust_profile: str = "trusted"
20
+ @dataclass(slots=True)
21
+ class InvariantGraduationConfig:
22
+ enabled: bool = True
23
+ target_dir: str = "tests/invariants/graduated"
24
+ @dataclass(slots=True)
25
+ class InvariantsConfig:
26
+ suite_paths: list[str] = field(default_factory=list)
27
+ pytest_bin: str = "pytest"
28
+ run_on_stop: bool = True
29
+ execution_mode: str = "host"
30
+ container_engine: str = "docker"
31
+ container_image: str = "python:3.11-slim"
32
+ container_workdir: str = "/workspace"
33
+ graduation: InvariantGraduationConfig = field(default_factory=InvariantGraduationConfig)
34
+ @dataclass(slots=True)
35
+ class ChallengesConfig:
36
+ active_categories: list[str] = field(default_factory=lambda: ["null_inputs", "boundary_values", "error_handling", "graveyard_regression"])
37
+ custom_paths: list[str] = field(default_factory=list)
38
+ require_coverage: bool = True
39
+ @dataclass(slots=True)
40
+ class GraveyardConfig:
41
+ enabled: bool = True
42
+ max_matches: int = 5
43
+ similarity_threshold: float = 0.35
44
+ min_keyword_overlap: int = 1
45
+ context_max_matches: int = 2
46
+ context_summary_chars: int = 140
47
+ context_reason_chars: int = 200
48
+ context_max_tokens: int = 300
49
+ @dataclass(slots=True)
50
+ class StabilityThresholds:
51
+ warn_churn_count: int = 8
52
+ high_churn_count: int = 15
53
+ @dataclass(slots=True)
54
+ class FoundationConfig:
55
+ enabled: bool = True
56
+ watch_paths: list[str] = field(default_factory=lambda: ["src"])
57
+ ignored_dirs: list[str] = field(default_factory=lambda: ["node_modules", "dist", "build", ".git", "__pycache__"])
58
+ stability_thresholds: StabilityThresholds = field(default_factory=StabilityThresholds)
59
+ churn_window_commits: int = 200
60
+ git_timeout_ms: int = 1500
61
+ @dataclass(slots=True)
62
+ class RepomapConfig:
63
+ enabled: bool = False
64
+ run_on_session_start: bool = False
65
+ prefer_ast_graph: bool = True
66
+ parity_profile: bool = False
67
+ extended_language_profile: bool = False
68
+ watch_paths: list[str] = field(default_factory=lambda: ["src"])
69
+ ignored_dirs: list[str] = field(default_factory=lambda: ["node_modules", "dist", "build", ".git", ".cortex", "__pycache__"])
70
+ max_ranked_files: int = 20
71
+ max_text_bytes: int = 8192
72
+ artifact_path: str = ".cortex/artifacts/repomap/latest.json"
73
+ non_blocking: bool = True
74
+ session_start_timeout_ms: int = 2500
75
+ @dataclass(slots=True)
76
+ class RuntimeConfig:
77
+ adapter: str = ""
78
+ @dataclass(slots=True)
79
+ class ExecutiveConfig:
80
+ enabled: bool = True
81
+ inject_identity_preamble: bool = True
82
+ part_a_mode: str = "once_per_project"
83
+ halflife_sessions: int = 30
84
+ decay_threshold: float = 0.3
85
+ inject_threshold: float = 0.45
86
+ max_entries: int = 20
87
+ max_tokens: int = 1200
88
+ min_hold_sessions: int = 3
89
+ @dataclass(slots=True)
90
+ class HooksConfig:
91
+ mode: str = "advisory"
92
+ fail_on_missing_challenge_coverage: bool = False
93
+ recommend_revert_on_invariant_failure: bool = True
94
+ require_requirement_audit: bool = False
95
+ fail_on_requirement_audit_gap: bool = False
96
+ require_evidence_for_passed_requirement: bool = True
97
+ require_structured_stop_payload: bool = False
98
+ allow_message_stop_fallback: bool = False
99
+ minimal_response: bool = False
100
+ @dataclass(slots=True)
101
+ class RetryConfig:
102
+ enabled: bool = True
103
+ max_retries: int = 3
104
+ @dataclass(slots=True)
105
+ class BlocklistConfig:
106
+ enabled: bool = True
107
+ blocked_tools: list[str] = field(default_factory=list)
108
+ allowed_tools: list[str] = field(default_factory=list)
109
+ fail_closed: bool = False
110
+ @dataclass(slots=True)
111
+ class PacksConfig:
112
+ paths: list[str] = field(default_factory=list)
113
+ @dataclass(slots=True)
114
+ class MetricsConfig:
115
+ enabled: bool = True
116
+ track: list[str] = field(default_factory=lambda: ["human_oversight_minutes", "interrupt_count", "escaped_defects", "completion_minutes", "foundation_quality"])
117
+ @dataclass(slots=True)
118
+ class CortexGenome:
119
+ project: ProjectConfig = field(default_factory=ProjectConfig)
120
+ invariants: InvariantsConfig = field(default_factory=InvariantsConfig)
121
+ challenges: ChallengesConfig = field(default_factory=ChallengesConfig)
122
+ graveyard: GraveyardConfig = field(default_factory=GraveyardConfig)
123
+ foundation: FoundationConfig = field(default_factory=FoundationConfig)
124
+ repomap: RepomapConfig = field(default_factory=RepomapConfig)
125
+ runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
126
+ executive: ExecutiveConfig = field(default_factory=ExecutiveConfig)
127
+ hooks: HooksConfig = field(default_factory=HooksConfig)
128
+ retry: RetryConfig = field(default_factory=RetryConfig)
129
+ blocklist: BlocklistConfig = field(default_factory=BlocklistConfig)
130
+ packs: PacksConfig = field(default_factory=PacksConfig)
131
+ metrics: MetricsConfig = field(default_factory=MetricsConfig)
132
+ source_path: str | None = None
133
+ parse_error: str | None = None
134
+ load_warnings: list[str] = field(default_factory=list)
135
+ def to_dict(self) -> dict[str, Any]: return asdict(self)
136
+
137
+
138
+ def load_genome(path: str | Path | None = None) -> CortexGenome:
139
+ config_path = Path(path) if path is not None else Path("cortex.toml")
140
+ if not config_path.exists():
141
+ return CortexGenome(source_path=str(config_path))
142
+ try:
143
+ with config_path.open("rb") as fh:
144
+ raw = tomllib.load(fh)
145
+ if not isinstance(raw, dict):
146
+ raise ValueError("Top-level TOML must be a table")
147
+ except Exception as exc: # noqa: BLE001
148
+ return CortexGenome(source_path=str(config_path), parse_error=str(exc))
149
+ invariants = _load_invariants(raw.get("invariants", {}))
150
+ challenges = _load_challenges(raw.get("challenges", {}))
151
+ blocklist = _load_blocklist(raw.get("blocklist", {}))
152
+ packs = _load_packs(raw.get("packs", {}))
153
+ overlay = load_pack_overlay(root=config_path.resolve().parent, pack_paths=packs.paths)
154
+ if overlay.challenge_categories:
155
+ challenges.active_categories = _merge_unique(challenges.active_categories, overlay.challenge_categories)
156
+ if overlay.invariant_paths:
157
+ invariants.suite_paths = _merge_unique(invariants.suite_paths, overlay.invariant_paths)
158
+ if overlay.blocked_tools:
159
+ blocklist.blocked_tools = _merge_unique(blocklist.blocked_tools, overlay.blocked_tools)
160
+ if overlay.allowed_tools:
161
+ blocklist.allowed_tools = _merge_unique(blocklist.allowed_tools, overlay.allowed_tools)
162
+ return CortexGenome(
163
+ project=_load_project(raw.get("project", {})),
164
+ invariants=invariants,
165
+ challenges=challenges,
166
+ graveyard=_load_graveyard(raw.get("graveyard", {})),
167
+ foundation=_load_foundation(raw.get("foundation", {})),
168
+ repomap=_load_repomap(raw.get("repomap", {})),
169
+ runtime=_load_runtime(raw.get("runtime", {})),
170
+ executive=_load_executive(raw.get("executive", {})),
171
+ hooks=_load_hooks(raw.get("hooks", {})),
172
+ retry=_load_retry(raw.get("retry", {})),
173
+ blocklist=blocklist,
174
+ packs=packs,
175
+ metrics=_load_metrics(raw.get("metrics", {})),
176
+ source_path=str(config_path),
177
+ load_warnings=overlay.warnings,
178
+ )
179
+
180
+
181
+ def _load_project(data: Any) -> ProjectConfig:
182
+ d, x = _as_dict(data), ProjectConfig()
183
+ trust_profile = str(d.get("trust_profile", x.trust_profile)).strip().lower()
184
+ if not trust_profile:
185
+ trust_profile = x.trust_profile
186
+ return ProjectConfig(
187
+ name=str(d.get("name", x.name)),
188
+ type=str(d.get("type", x.type)),
189
+ root=str(d.get("root", x.root)),
190
+ trust_profile=trust_profile,
191
+ )
192
+ def _load_invariants(data: Any) -> InvariantsConfig:
193
+ d, x, g = _as_dict(data), InvariantsConfig(), _as_dict(_as_dict(data).get("graduation", {}))
194
+ gd = InvariantGraduationConfig()
195
+ execution_mode = str(d.get("execution_mode", x.execution_mode)).strip().lower()
196
+ if execution_mode not in {"host", "container"}:
197
+ execution_mode = x.execution_mode
198
+ return InvariantsConfig(
199
+ suite_paths=[str(v) for v in _as_list(d.get("suite_paths"), x.suite_paths)],
200
+ pytest_bin=str(d.get("pytest_bin", x.pytest_bin)),
201
+ run_on_stop=_as_bool(d.get("run_on_stop"), x.run_on_stop),
202
+ execution_mode=execution_mode,
203
+ container_engine=str(d.get("container_engine", x.container_engine)),
204
+ container_image=str(d.get("container_image", x.container_image)),
205
+ container_workdir=str(d.get("container_workdir", x.container_workdir)),
206
+ graduation=InvariantGraduationConfig(
207
+ enabled=_as_bool(g.get("enabled"), gd.enabled),
208
+ target_dir=str(g.get("target_dir", gd.target_dir)),
209
+ ),
210
+ )
211
+ def _load_challenges(data: Any) -> ChallengesConfig:
212
+ d, x = _as_dict(data), ChallengesConfig()
213
+ return ChallengesConfig(active_categories=[str(v) for v in _as_list(d.get("active_categories"), x.active_categories)] or list(x.active_categories), custom_paths=[str(v) for v in _as_list(d.get("custom_paths"), x.custom_paths)], require_coverage=_as_bool(d.get("require_coverage"), x.require_coverage))
214
+ def _load_graveyard(data: Any) -> GraveyardConfig:
215
+ d, x = _as_dict(data), GraveyardConfig()
216
+ return GraveyardConfig(
217
+ enabled=_as_bool(d.get("enabled"), x.enabled),
218
+ max_matches=_as_int(d.get("max_matches"), x.max_matches),
219
+ similarity_threshold=_as_float(d.get("similarity_threshold"), x.similarity_threshold),
220
+ min_keyword_overlap=_as_int(d.get("min_keyword_overlap"), x.min_keyword_overlap),
221
+ context_max_matches=max(1, _as_int(d.get("context_max_matches"), x.context_max_matches)),
222
+ context_summary_chars=max(40, _as_int(d.get("context_summary_chars"), x.context_summary_chars)),
223
+ context_reason_chars=max(40, _as_int(d.get("context_reason_chars"), x.context_reason_chars)),
224
+ context_max_tokens=max(50, _as_int(d.get("context_max_tokens"), x.context_max_tokens)),
225
+ )
226
+ def _load_foundation(data: Any) -> FoundationConfig:
227
+ d, x, td = _as_dict(data), FoundationConfig(), StabilityThresholds()
228
+ t = _as_dict(d.get("stability_thresholds", {}))
229
+ return FoundationConfig(
230
+ enabled=_as_bool(d.get("enabled"), x.enabled),
231
+ watch_paths=[str(v) for v in _as_list(d.get("watch_paths"), x.watch_paths)],
232
+ ignored_dirs=[str(v) for v in _as_list(d.get("ignored_dirs"), x.ignored_dirs)],
233
+ stability_thresholds=StabilityThresholds(
234
+ warn_churn_count=_as_int(t.get("warn_churn_count"), td.warn_churn_count),
235
+ high_churn_count=_as_int(t.get("high_churn_count"), td.high_churn_count),
236
+ ),
237
+ churn_window_commits=_as_int(d.get("churn_window_commits"), x.churn_window_commits),
238
+ git_timeout_ms=max(1, _as_int(d.get("git_timeout_ms"), x.git_timeout_ms)),
239
+ )
240
+ def _load_repomap(data: Any) -> RepomapConfig:
241
+ d, x = _as_dict(data), RepomapConfig()
242
+ return RepomapConfig(
243
+ enabled=_as_bool(d.get("enabled"), x.enabled),
244
+ run_on_session_start=_as_bool(d.get("run_on_session_start"), x.run_on_session_start),
245
+ prefer_ast_graph=_as_bool(d.get("prefer_ast_graph"), x.prefer_ast_graph),
246
+ parity_profile=_as_bool(d.get("parity_profile"), x.parity_profile),
247
+ extended_language_profile=_as_bool(d.get("extended_language_profile"), x.extended_language_profile),
248
+ watch_paths=[str(v) for v in _as_list(d.get("watch_paths"), x.watch_paths)],
249
+ ignored_dirs=[str(v) for v in _as_list(d.get("ignored_dirs"), x.ignored_dirs)],
250
+ max_ranked_files=_as_int(d.get("max_ranked_files"), x.max_ranked_files),
251
+ max_text_bytes=_as_int(d.get("max_text_bytes"), x.max_text_bytes),
252
+ artifact_path=str(d.get("artifact_path", x.artifact_path)),
253
+ non_blocking=_as_bool(d.get("non_blocking"), x.non_blocking),
254
+ session_start_timeout_ms=_as_int(d.get("session_start_timeout_ms"), x.session_start_timeout_ms),
255
+ )
256
+ def _load_runtime(data: Any) -> RuntimeConfig:
257
+ d, x = _as_dict(data), RuntimeConfig()
258
+ return RuntimeConfig(adapter=str(d.get("adapter", x.adapter)).strip())
259
+ def _load_executive(data: Any) -> ExecutiveConfig:
260
+ d, x = _as_dict(data), ExecutiveConfig()
261
+ part_a_mode = str(d.get("part_a_mode", x.part_a_mode)).strip().lower()
262
+ if part_a_mode not in {"once_per_project", "always"}:
263
+ part_a_mode = x.part_a_mode
264
+ return ExecutiveConfig(
265
+ enabled=_as_bool(d.get("enabled"), x.enabled),
266
+ inject_identity_preamble=_as_bool(d.get("inject_identity_preamble"), x.inject_identity_preamble),
267
+ part_a_mode=part_a_mode,
268
+ halflife_sessions=max(1, _as_int(d.get("halflife_sessions"), x.halflife_sessions)),
269
+ decay_threshold=max(0.0, _as_float(d.get("decay_threshold"), x.decay_threshold)),
270
+ inject_threshold=max(0.0, _as_float(d.get("inject_threshold"), x.inject_threshold)),
271
+ max_entries=max(1, _as_int(d.get("max_entries"), x.max_entries)),
272
+ max_tokens=max(200, _as_int(d.get("max_tokens"), x.max_tokens)),
273
+ min_hold_sessions=max(0, _as_int(d.get("min_hold_sessions"), x.min_hold_sessions)),
274
+ )
275
+ def _load_hooks(data: Any) -> HooksConfig:
276
+ d, x = _as_dict(data), HooksConfig()
277
+ return HooksConfig(
278
+ mode=str(d.get("mode", x.mode)),
279
+ fail_on_missing_challenge_coverage=_as_bool(
280
+ d.get("fail_on_missing_challenge_coverage"), x.fail_on_missing_challenge_coverage
281
+ ),
282
+ recommend_revert_on_invariant_failure=_as_bool(
283
+ d.get("recommend_revert_on_invariant_failure"), x.recommend_revert_on_invariant_failure
284
+ ),
285
+ require_requirement_audit=_as_bool(
286
+ d.get("require_requirement_audit"), x.require_requirement_audit
287
+ ),
288
+ fail_on_requirement_audit_gap=_as_bool(
289
+ d.get("fail_on_requirement_audit_gap"), x.fail_on_requirement_audit_gap
290
+ ),
291
+ require_evidence_for_passed_requirement=_as_bool(
292
+ d.get("require_evidence_for_passed_requirement"), x.require_evidence_for_passed_requirement
293
+ ),
294
+ require_structured_stop_payload=_as_bool(
295
+ d.get("require_structured_stop_payload"), x.require_structured_stop_payload
296
+ ),
297
+ allow_message_stop_fallback=_as_bool(
298
+ d.get("allow_message_stop_fallback"), x.allow_message_stop_fallback
299
+ ),
300
+ minimal_response=_as_bool(
301
+ d.get("minimal_response"), x.minimal_response
302
+ ),
303
+ )
304
+ def _load_retry(data: Any) -> RetryConfig:
305
+ d, x = _as_dict(data), RetryConfig()
306
+ return RetryConfig(enabled=_as_bool(d.get("enabled"), x.enabled), max_retries=_as_int(d.get("max_retries"), x.max_retries))
307
+ def _load_blocklist(data: Any) -> BlocklistConfig:
308
+ d, x = _as_dict(data), BlocklistConfig()
309
+ return BlocklistConfig(
310
+ enabled=_as_bool(d.get("enabled"), x.enabled),
311
+ blocked_tools=[str(v) for v in _as_list(d.get("blocked_tools"), x.blocked_tools)],
312
+ allowed_tools=[str(v) for v in _as_list(d.get("allowed_tools"), x.allowed_tools)],
313
+ fail_closed=_as_bool(d.get("fail_closed"), x.fail_closed),
314
+ )
315
+ def _load_packs(data: Any) -> PacksConfig:
316
+ d = _as_dict(data)
317
+ return PacksConfig(paths=[str(v) for v in _as_list(d.get("paths"), [])])
318
+ def _load_metrics(data: Any) -> MetricsConfig:
319
+ d, x = _as_dict(data), MetricsConfig()
320
+ return MetricsConfig(enabled=_as_bool(d.get("enabled"), x.enabled), track=[str(v) for v in _as_list(d.get("track"), x.track)])
321
+
322
+
323
+ def _as_dict(value: Any) -> dict[str, Any]:
324
+ return value if isinstance(value, dict) else {}
325
+ def _as_list(value: Any, default: list[Any]) -> list[Any]:
326
+ return value if isinstance(value, list) else list(default)
327
+ def _as_int(value: Any, default: int) -> int:
328
+ try:
329
+ return int(value)
330
+ except (TypeError, ValueError):
331
+ return default
332
+ def _as_float(value: Any, default: float) -> float:
333
+ try:
334
+ return float(value)
335
+ except (TypeError, ValueError):
336
+ return default
337
+ def _merge_unique(base: list[str], extra: list[str]) -> list[str]:
338
+ merged: list[str] = []
339
+ seen: set[str] = set()
340
+ for item in [*base, *extra]:
341
+ token = str(item).strip()
342
+ if not token or token in seen:
343
+ continue
344
+ seen.add(token)
345
+ merged.append(token)
346
+ return merged
347
+ def collect_active_metric_names(genome: CortexGenome) -> Iterable[str]:
348
+ return genome.metrics.track if genome.metrics.enabled else []