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/graveyard.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import PurePosixPath
|
|
8
|
+
from typing import Any, Iterable
|
|
9
|
+
|
|
10
|
+
from .genome import GraveyardConfig
|
|
11
|
+
from .store import SQLiteStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_WORD_RE = re.compile(r"[A-Za-z0-9_]+")
|
|
15
|
+
_SYNONYM_CANONICAL = {
|
|
16
|
+
"redis": "cache",
|
|
17
|
+
"caching": "cache",
|
|
18
|
+
"latency": "timeout",
|
|
19
|
+
"slow": "timeout",
|
|
20
|
+
"slowness": "timeout",
|
|
21
|
+
"crash": "fail",
|
|
22
|
+
"crashed": "fail",
|
|
23
|
+
"failure": "fail",
|
|
24
|
+
"failed": "fail",
|
|
25
|
+
"error": "fail",
|
|
26
|
+
"errors": "fail",
|
|
27
|
+
"exception": "fail",
|
|
28
|
+
"exceptions": "fail",
|
|
29
|
+
"connection": "connect",
|
|
30
|
+
"connections": "connect",
|
|
31
|
+
"verification": "verify",
|
|
32
|
+
"verifications": "verify",
|
|
33
|
+
"verified": "verify",
|
|
34
|
+
"verifying": "verify",
|
|
35
|
+
"defense": "defend",
|
|
36
|
+
"defences": "defend",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True)
|
|
41
|
+
class GraveyardMatch:
|
|
42
|
+
entry_id: int
|
|
43
|
+
score: float
|
|
44
|
+
summary: str
|
|
45
|
+
reason: str
|
|
46
|
+
files: list[str]
|
|
47
|
+
keyword_overlap: list[str]
|
|
48
|
+
file_overlap: list[str]
|
|
49
|
+
semantic_score: float
|
|
50
|
+
created_at: str
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, object]:
|
|
53
|
+
return {
|
|
54
|
+
"entry_id": self.entry_id,
|
|
55
|
+
"score": round(self.score, 3),
|
|
56
|
+
"summary": self.summary,
|
|
57
|
+
"reason": self.reason,
|
|
58
|
+
"files": self.files,
|
|
59
|
+
"keyword_overlap": self.keyword_overlap,
|
|
60
|
+
"file_overlap": self.file_overlap,
|
|
61
|
+
"semantic_score": round(self.semantic_score, 3),
|
|
62
|
+
"created_at": self.created_at,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Graveyard:
|
|
67
|
+
def __init__(self, store: SQLiteStore, config: GraveyardConfig) -> None:
|
|
68
|
+
self.store = store
|
|
69
|
+
self.config = config
|
|
70
|
+
|
|
71
|
+
def record_failure(
|
|
72
|
+
self,
|
|
73
|
+
session_id: str | None,
|
|
74
|
+
summary: str,
|
|
75
|
+
reason: str,
|
|
76
|
+
files: Iterable[str] = (),
|
|
77
|
+
) -> None:
|
|
78
|
+
if not self.config.enabled:
|
|
79
|
+
return
|
|
80
|
+
file_list = [str(path) for path in files]
|
|
81
|
+
keywords = sorted(self._keywords(summary) | self._keywords(reason))
|
|
82
|
+
self.store.insert_graveyard(
|
|
83
|
+
session_id=session_id,
|
|
84
|
+
summary=summary,
|
|
85
|
+
reason=reason,
|
|
86
|
+
files=file_list,
|
|
87
|
+
keywords=keywords,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def find_similar(
|
|
91
|
+
self,
|
|
92
|
+
summary: str,
|
|
93
|
+
files: Iterable[str] = (),
|
|
94
|
+
*,
|
|
95
|
+
max_matches: int | None = None,
|
|
96
|
+
) -> list[GraveyardMatch]:
|
|
97
|
+
if not self.config.enabled:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
query_keywords = self._keywords(summary)
|
|
101
|
+
query_tokens = self._tokenize(summary)
|
|
102
|
+
query_token_set = set(query_tokens)
|
|
103
|
+
query_files = {self._norm_path(p) for p in files if p}
|
|
104
|
+
if not query_tokens and not query_files:
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
corpus_entries = self.store.list_graveyard(limit=200)
|
|
108
|
+
entries = self._load_candidate_entries(query_tokens=query_tokens)
|
|
109
|
+
idf_source = corpus_entries or entries
|
|
110
|
+
df = Counter(token for entry in idf_source for token in {str(v) for v in entry.get("keywords", [])})
|
|
111
|
+
query_idf = {token: math.log((len(idf_source) + 1) / (df.get(token, 0) + 1)) + 1.0 for token in query_keywords}
|
|
112
|
+
query_weight = sum(query_idf.values()) or 1.0
|
|
113
|
+
scored: list[GraveyardMatch] = []
|
|
114
|
+
for entry in entries:
|
|
115
|
+
entry_keywords = {str(k) for k in entry.get("keywords", [])}
|
|
116
|
+
entry_tokens = self._tokenize(f"{entry.get('summary', '')} {entry.get('reason', '')}")
|
|
117
|
+
entry_files = {self._norm_path(p) for p in entry.get("files", [])}
|
|
118
|
+
keyword_overlap = sorted(query_keywords & entry_keywords)
|
|
119
|
+
file_overlap = sorted(query_files & entry_files)
|
|
120
|
+
|
|
121
|
+
keyword_score = sum(query_idf[token] for token in keyword_overlap) / query_weight
|
|
122
|
+
file_score = len(file_overlap) / max(1, len(query_files)) if query_files else 0.0
|
|
123
|
+
semantic_score = self._token_jaccard(query_token_set, set(entry_tokens))
|
|
124
|
+
if (
|
|
125
|
+
len(keyword_overlap) < self.config.min_keyword_overlap
|
|
126
|
+
and not file_overlap
|
|
127
|
+
and semantic_score < self.config.similarity_threshold
|
|
128
|
+
):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
score = (keyword_score * 0.45) + (file_score * 0.25) + (semantic_score * 0.30)
|
|
132
|
+
if score < self.config.similarity_threshold:
|
|
133
|
+
continue
|
|
134
|
+
scored.append(
|
|
135
|
+
GraveyardMatch(
|
|
136
|
+
entry_id=int(entry["id"]),
|
|
137
|
+
score=score,
|
|
138
|
+
summary=str(entry["summary"]),
|
|
139
|
+
reason=str(entry["reason"]),
|
|
140
|
+
files=list(entry["files"]),
|
|
141
|
+
keyword_overlap=keyword_overlap,
|
|
142
|
+
file_overlap=file_overlap,
|
|
143
|
+
semantic_score=semantic_score,
|
|
144
|
+
created_at=str(entry["created_at"]),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
scored.sort(key=lambda m: (m.score, m.entry_id), reverse=True)
|
|
149
|
+
return scored[: (max_matches or self.config.max_matches)]
|
|
150
|
+
|
|
151
|
+
def _load_candidate_entries(self, *, query_tokens: list[str]) -> list[dict[str, object]]:
|
|
152
|
+
if not query_tokens:
|
|
153
|
+
return self.store.list_graveyard(limit=200)
|
|
154
|
+
candidates = self.store.list_graveyard_fts_candidates(tokens=query_tokens, limit=200, candidate_limit=80)
|
|
155
|
+
if not candidates:
|
|
156
|
+
return self.store.list_graveyard(limit=200)
|
|
157
|
+
return candidates
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _keywords(text: str) -> set[str]:
|
|
161
|
+
return set(Graveyard._tokenize(text))
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _tokenize(text: str) -> list[str]:
|
|
165
|
+
tokens: list[str] = []
|
|
166
|
+
for raw in _WORD_RE.findall(text):
|
|
167
|
+
token = Graveyard._normalize_token(raw)
|
|
168
|
+
if token:
|
|
169
|
+
tokens.append(token)
|
|
170
|
+
return tokens
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _normalize_token(token: str) -> str:
|
|
174
|
+
value = token.lower().strip("_")
|
|
175
|
+
if len(value) <= 2:
|
|
176
|
+
return ""
|
|
177
|
+
if value.startswith("verif"):
|
|
178
|
+
value = "verify"
|
|
179
|
+
elif value.startswith("defenc") or value.startswith("defens"):
|
|
180
|
+
value = "defend"
|
|
181
|
+
if value.endswith("ies") and len(value) > 4:
|
|
182
|
+
value = value[:-3] + "y"
|
|
183
|
+
elif value.endswith("ing") and len(value) > 5:
|
|
184
|
+
value = value[:-3]
|
|
185
|
+
elif value.endswith("ed") and len(value) > 4:
|
|
186
|
+
value = value[:-2]
|
|
187
|
+
elif value.endswith("s") and len(value) > 3:
|
|
188
|
+
value = value[:-1]
|
|
189
|
+
return _SYNONYM_CANONICAL.get(value, value)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _token_jaccard(left: set[str], right: set[str]) -> float:
|
|
193
|
+
if not left or not right:
|
|
194
|
+
return 0.0
|
|
195
|
+
union = left | right
|
|
196
|
+
if not union:
|
|
197
|
+
return 0.0
|
|
198
|
+
return len(left & right) / len(union)
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _norm_path(path: str) -> str:
|
|
202
|
+
parts = [p for p in str(PurePosixPath(path)).split("/") if p not in {"", "."}]
|
|
203
|
+
return "/".join(parts)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def explainability_warnings(matches: list[dict[str, Any]]) -> list[str]:
|
|
207
|
+
if not matches:
|
|
208
|
+
return []
|
|
209
|
+
top = matches[0]
|
|
210
|
+
summary = str(top.get("summary") or "").strip()
|
|
211
|
+
score = top.get("score")
|
|
212
|
+
semantic_score = top.get("semantic_score")
|
|
213
|
+
keyword_overlap = [str(v).strip() for v in top.get("keyword_overlap", []) if str(v).strip()]
|
|
214
|
+
file_overlap = [str(v).strip() for v in top.get("file_overlap", []) if str(v).strip()]
|
|
215
|
+
parts = ["Top graveyard match"]
|
|
216
|
+
if summary:
|
|
217
|
+
parts.append(f"summary='{summary[:120]}'")
|
|
218
|
+
if isinstance(score, (int, float)):
|
|
219
|
+
parts.append(f"score={float(score):.3f}")
|
|
220
|
+
if isinstance(semantic_score, (int, float)):
|
|
221
|
+
parts.append(f"semantic={float(semantic_score):.3f}")
|
|
222
|
+
if keyword_overlap:
|
|
223
|
+
parts.append("keyword_overlap=" + ",".join(keyword_overlap[:5]))
|
|
224
|
+
if file_overlap:
|
|
225
|
+
parts.append("file_overlap=" + ",".join(file_overlap[:3]))
|
|
226
|
+
return ["; ".join(parts)]
|
cortex/hooks/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Runtime hook entrypoints for Cortex."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from cortex.core import CortexKernel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def marker(
|
|
11
|
+
label: str,
|
|
12
|
+
*,
|
|
13
|
+
session_id: str,
|
|
14
|
+
root: str | Path = ".",
|
|
15
|
+
config_path: str | Path | None = None,
|
|
16
|
+
) -> dict[str, object]:
|
|
17
|
+
token = str(label).strip()
|
|
18
|
+
if not token:
|
|
19
|
+
raise ValueError("marker() requires a non-empty label.")
|
|
20
|
+
session_token = str(session_id or "").strip()
|
|
21
|
+
if not session_token:
|
|
22
|
+
raise ValueError("marker() requires session_id. Use marker(label, session_id='sess-...').")
|
|
23
|
+
payload: dict[str, object] = {"label": token, "session_id": session_token}
|
|
24
|
+
return CortexKernel(root=root, config_path=config_path).dispatch("session_marker", payload)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ["marker"]
|
cortex/hooks/_shared.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cortex.core import CortexKernel
|
|
9
|
+
|
|
10
|
+
HOOK_SCHEMA_NATIVE = "native_hook_v1"
|
|
11
|
+
HOOK_SCHEMA_LEGACY = "legacy_json_v0"
|
|
12
|
+
_NATIVE_ALIAS = "".join(["cl", "aude_native_v1"])
|
|
13
|
+
_STOP_PRIORITY_PREFIXES = (
|
|
14
|
+
"Structured stop payload is required",
|
|
15
|
+
"Strict mode rejects Stop message-fallback payloads",
|
|
16
|
+
"Truth claims reported gaps:",
|
|
17
|
+
"Requirement audit reported gaps:",
|
|
18
|
+
"Missing challenge coverage for categories:",
|
|
19
|
+
"Challenge coverage '",
|
|
20
|
+
"Stop attempt is highly similar to the previous failed Stop;",
|
|
21
|
+
)
|
|
22
|
+
_STOP_IGNORED_PREFIXES = ("Using ", "Truth claims note:", "Requirement audit note:")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_hook(
|
|
26
|
+
*,
|
|
27
|
+
hook_name: str,
|
|
28
|
+
event_name: str,
|
|
29
|
+
argv: list[str] | None = None,
|
|
30
|
+
) -> int:
|
|
31
|
+
args = _parse_args(argv)
|
|
32
|
+
try:
|
|
33
|
+
if args.adapter is not None:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"--adapter is no longer supported. Configure [runtime].adapter in cortex.toml."
|
|
36
|
+
)
|
|
37
|
+
payload = _read_stdin_json()
|
|
38
|
+
kernel = CortexKernel(root=args.root, config_path=args.config)
|
|
39
|
+
result = kernel.dispatch(event_name, payload)
|
|
40
|
+
print(json.dumps(_format_hook_output(hook_name=hook_name, result=result, schema_version=args.schema_version)))
|
|
41
|
+
return 0
|
|
42
|
+
except Exception as exc: # noqa: BLE001
|
|
43
|
+
print(json.dumps(_format_hook_error(hook_name=hook_name, error=str(exc), schema_version=args.schema_version)))
|
|
44
|
+
return 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _read_stdin_json() -> dict[str, object]:
|
|
48
|
+
raw = sys.stdin.read().strip()
|
|
49
|
+
if not raw:
|
|
50
|
+
return {}
|
|
51
|
+
data = json.loads(raw)
|
|
52
|
+
if not isinstance(data, dict):
|
|
53
|
+
raise ValueError("Hook payload must be a JSON object")
|
|
54
|
+
return data
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
58
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
59
|
+
parser.add_argument("--root", default=".")
|
|
60
|
+
parser.add_argument("--config")
|
|
61
|
+
parser.add_argument("--adapter")
|
|
62
|
+
parser.add_argument("--schema-version", default=HOOK_SCHEMA_LEGACY)
|
|
63
|
+
args = parser.parse_args([] if argv is None else argv)
|
|
64
|
+
args.schema_version = _normalize_schema_version(str(args.schema_version))
|
|
65
|
+
return args
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _normalize_schema_version(raw: str) -> str:
|
|
69
|
+
schema_version = str(raw or "").strip()
|
|
70
|
+
if schema_version in {HOOK_SCHEMA_NATIVE, _NATIVE_ALIAS}:
|
|
71
|
+
return HOOK_SCHEMA_NATIVE
|
|
72
|
+
if schema_version == HOOK_SCHEMA_LEGACY:
|
|
73
|
+
return HOOK_SCHEMA_LEGACY
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"Unsupported --schema-version. Use one of: "
|
|
76
|
+
f"{HOOK_SCHEMA_NATIVE}, {_NATIVE_ALIAS}, {HOOK_SCHEMA_LEGACY}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _format_hook_output(*, hook_name: str, result: dict[str, Any], schema_version: str) -> dict[str, Any]:
|
|
81
|
+
if schema_version == HOOK_SCHEMA_LEGACY:
|
|
82
|
+
return result
|
|
83
|
+
if hook_name == "PreToolUse":
|
|
84
|
+
proceed = bool(result.get("proceed", True))
|
|
85
|
+
decision = "allow" if proceed else "deny"
|
|
86
|
+
output: dict[str, Any] = {
|
|
87
|
+
"permissionDecision": decision,
|
|
88
|
+
"hookSpecificOutput": {
|
|
89
|
+
"hookEventName": "PreToolUse",
|
|
90
|
+
"permissionDecision": decision,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
if not proceed:
|
|
94
|
+
output["hookSpecificOutput"]["permissionDecisionReason"] = _primary_reason(hook_name, result)
|
|
95
|
+
return output
|
|
96
|
+
if hook_name in {"PostToolUse", "Stop"}:
|
|
97
|
+
if hook_name == "Stop" and _is_stuck_result(result):
|
|
98
|
+
return {"continue": False, "stopReason": _primary_reason(hook_name, result)}
|
|
99
|
+
if bool(result.get("proceed", True)):
|
|
100
|
+
return {}
|
|
101
|
+
return {"decision": "block", "reason": _primary_reason(hook_name, result)}
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _format_hook_error(*, hook_name: str, error: str, schema_version: str) -> dict[str, Any]:
|
|
106
|
+
if schema_version == HOOK_SCHEMA_LEGACY:
|
|
107
|
+
return {"ok": False, "hook": hook_name, "error": error}
|
|
108
|
+
if hook_name == "PreToolUse":
|
|
109
|
+
return {
|
|
110
|
+
"permissionDecision": "deny",
|
|
111
|
+
"hookSpecificOutput": {
|
|
112
|
+
"hookEventName": "PreToolUse",
|
|
113
|
+
"permissionDecision": "deny",
|
|
114
|
+
"permissionDecisionReason": error,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if hook_name in {"PostToolUse", "Stop"}:
|
|
118
|
+
return {"decision": "block", "reason": error}
|
|
119
|
+
return {"decision": "block", "reason": error}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _primary_reason(hook_name: str, result: dict[str, Any]) -> str:
|
|
123
|
+
if _is_stuck_result(result):
|
|
124
|
+
return _stuck_reason(result)
|
|
125
|
+
if hook_name == "Stop":
|
|
126
|
+
stop_reason = _stop_reason(result)
|
|
127
|
+
if stop_reason:
|
|
128
|
+
return stop_reason
|
|
129
|
+
warning = next((item for item in _warning_strings(result) if item), "")
|
|
130
|
+
if warning:
|
|
131
|
+
return warning
|
|
132
|
+
if hook_name == "Stop":
|
|
133
|
+
return "Cortex stop path blocked completion."
|
|
134
|
+
if hook_name == "PostToolUse":
|
|
135
|
+
return "Cortex post-tool gate blocked continuation."
|
|
136
|
+
return "Cortex denied tool execution."
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _stop_reason(result: dict[str, Any]) -> str | None:
|
|
140
|
+
warnings = _warning_strings(result)
|
|
141
|
+
warning = next((item for item in warnings if item.startswith(_STOP_PRIORITY_PREFIXES)), None)
|
|
142
|
+
if warning:
|
|
143
|
+
return warning
|
|
144
|
+
return next((item for item in warnings if not item.startswith(_STOP_IGNORED_PREFIXES)), None)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_stuck_result(result: dict[str, Any]) -> bool:
|
|
148
|
+
return bool(result.get("stuck_declared")) or str(result.get("feedback_mode") or "") == "stuck"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _warning_strings(result: dict[str, Any]) -> list[str]:
|
|
152
|
+
warnings = result.get("warnings")
|
|
153
|
+
if not isinstance(warnings, list):
|
|
154
|
+
return []
|
|
155
|
+
return [str(raw_warning or "").strip() for raw_warning in warnings if str(raw_warning or "").strip()]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _stuck_reason(result: dict[str, Any]) -> str:
|
|
159
|
+
stuck = result.get("stuck_declaration")
|
|
160
|
+
if isinstance(stuck, dict):
|
|
161
|
+
check = str(stuck.get("check") or "").strip()
|
|
162
|
+
obstacle = str(stuck.get("obstacle") or "").strip()
|
|
163
|
+
if check and obstacle:
|
|
164
|
+
return f"Cortex reported stuck on {check}: {obstacle}"
|
|
165
|
+
if obstacle:
|
|
166
|
+
return f"Cortex reported stuck: {obstacle}"
|
|
167
|
+
return "Cortex reported stuck and could not complete the requested evidence boundary."
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from ._shared import run_hook
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> int:
|
|
9
|
+
return run_hook(hook_name="PostToolUse", event_name="post_tool_use", argv=argv)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from ._shared import run_hook
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> int:
|
|
9
|
+
return run_hook(hook_name="PreToolUse", event_name="pre_tool_use", argv=argv)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from ._shared import run_hook
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> int:
|
|
9
|
+
return run_hook(hook_name="SessionStart", event_name="session_start", argv=argv)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
raise SystemExit(main(sys.argv[1:]))
|
cortex/hooks/stop.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from ._shared import run_hook
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> int:
|
|
9
|
+
return run_hook(hook_name="Stop", event_name="stop", argv=argv)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
raise SystemExit(main(sys.argv[1:]))
|