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.
- cortex/__init__.py +7 -0
- cortex/adapters.py +339 -0
- cortex/blocklist.py +51 -0
- cortex/challenges.py +210 -0
- cortex/cli.py +7 -0
- cortex/core.py +601 -0
- cortex/core_helpers.py +190 -0
- cortex/data/identity_preamble.md +5 -0
- cortex/data/layer1_part_a.md +65 -0
- cortex/data/layer1_part_b.md +17 -0
- cortex/executive.py +295 -0
- cortex/foundation.py +185 -0
- cortex/genome.py +348 -0
- cortex/graveyard.py +226 -0
- cortex/hooks/__init__.py +27 -0
- cortex/hooks/_shared.py +167 -0
- cortex/hooks/post_tool_use.py +13 -0
- cortex/hooks/pre_tool_use.py +13 -0
- cortex/hooks/session_start.py +13 -0
- cortex/hooks/stop.py +13 -0
- cortex/invariants.py +258 -0
- cortex/packs.py +118 -0
- cortex/repomap.py +6 -0
- cortex/requirements.py +497 -0
- cortex/retry.py +312 -0
- cortex/stop_contract.py +217 -0
- cortex/stop_payload.py +122 -0
- cortex/stop_policy.py +100 -0
- cortex/stop_runtime.py +400 -0
- cortex/stop_signals.py +75 -0
- cortex/store.py +793 -0
- cortex/templates/__init__.py +10 -0
- cortex/utils.py +58 -0
- cortex_loop-0.1.0a1.dist-info/METADATA +121 -0
- cortex_loop-0.1.0a1.dist-info/RECORD +52 -0
- cortex_loop-0.1.0a1.dist-info/WHEEL +5 -0
- cortex_loop-0.1.0a1.dist-info/entry_points.txt +3 -0
- cortex_loop-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- cortex_loop-0.1.0a1.dist-info/top_level.txt +3 -0
- cortex_ops_cli/__init__.py +3 -0
- cortex_ops_cli/_adapter_validation.py +119 -0
- cortex_ops_cli/_check_report.py +454 -0
- cortex_ops_cli/_check_report_output.py +270 -0
- cortex_ops_cli/_openai_bridge_probe.py +241 -0
- cortex_ops_cli/_openai_bridge_protocol.py +469 -0
- cortex_ops_cli/_runtime_profile_templates.py +341 -0
- cortex_ops_cli/_runtime_profiles.py +445 -0
- cortex_ops_cli/gemini_hooks.py +301 -0
- cortex_ops_cli/main.py +911 -0
- cortex_ops_cli/openai_app_server_bridge.py +375 -0
- cortex_repomap/__init__.py +1 -0
- 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 []
|