code-jig 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_jig-0.12.0.dist-info/METADATA +154 -0
- code_jig-0.12.0.dist-info/RECORD +52 -0
- code_jig-0.12.0.dist-info/WHEEL +4 -0
- code_jig-0.12.0.dist-info/entry_points.txt +2 -0
- code_jig-0.12.0.dist-info/licenses/LICENSE +202 -0
- codejig/__init__.py +1 -0
- codejig/__main__.py +106 -0
- codejig/_core/VERSION +2 -0
- codejig/_core/bench/requirements.toml +4 -0
- codejig/_core/core/AGENTS.core.md +9 -0
- codejig/_core/core/agents/explorer.toml +24 -0
- codejig/_core/core/agents/judge.toml +19 -0
- codejig/_core/core/agents/reviewer.toml +10 -0
- codejig/_core/core/tiers.toml +4 -0
- codejig/_core/skills/bug-hunt/SKILL.md +131 -0
- codejig/_core/skills/code-analysis/SKILL.md +72 -0
- codejig/_core/skills/codegen/SKILL.md +46 -0
- codejig/_core/skills/cross-review/SKILL.md +32 -0
- codejig/_core/skills/grilling/SKILL.md +60 -0
- codejig/_core/skills/hld-trace/SKILL.md +69 -0
- codejig/_core/skills/improve/SKILL.md +38 -0
- codejig/_core/skills/redacao/SKILL.md +114 -0
- codejig/_core/skills/redacao/checar.py +75 -0
- codejig/_core/skills/redacao/reference.md +118 -0
- codejig/_core/skills/regression-tests/SKILL.md +53 -0
- codejig/_core/skills/remove-ai-attribution/SKILL.md +33 -0
- codejig/_core/skills/skill-validate/SKILL.md +58 -0
- codejig/_core/skills/web-consensus/SKILL.md +55 -0
- codejig/analyze.py +499 -0
- codejig/atomicwrite.py +49 -0
- codejig/collect.py +311 -0
- codejig/doctor.py +370 -0
- codejig/evidence.py +418 -0
- codejig/feedback.py +165 -0
- codejig/gitgate.py +278 -0
- codejig/hooks.py +64 -0
- codejig/humanize.py +206 -0
- codejig/install.py +381 -0
- codejig/managed.py +64 -0
- codejig/migrate.py +515 -0
- codejig/overlay.py +43 -0
- codejig/paths.py +40 -0
- codejig/probe.py +136 -0
- codejig/review.py +73 -0
- codejig/scrub.py +255 -0
- codejig/skillcheck.py +400 -0
- codejig/skilllint.py +99 -0
- codejig/sources.py +248 -0
- codejig/tiers.py +63 -0
- codejig/tokens.py +118 -0
- codejig/trace.py +137 -0
- codejig/watermark.py +147 -0
codejig/collect.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import sqlite3
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from codejig import paths
|
|
8
|
+
|
|
9
|
+
SCHEMA = """
|
|
10
|
+
CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, offset INTEGER NOT NULL, size INTEGER NOT NULL, thread_id TEXT, model TEXT, turn_id TEXT, line INTEGER NOT NULL, last_total INTEGER NOT NULL);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS threads (id TEXT PRIMARY KEY, parent_id TEXT, kind TEXT NOT NULL, started TEXT, cwd TEXT);
|
|
12
|
+
CREATE TABLE IF NOT EXISTS usage (thread_id TEXT NOT NULL, key TEXT NOT NULL, turn_id TEXT, ts TEXT, model TEXT, input INTEGER, cached INTEGER, output INTEGER, reasoning INTEGER, PRIMARY KEY (thread_id, key));
|
|
13
|
+
CREATE TABLE IF NOT EXISTS calls (thread_id TEXT NOT NULL, call_id TEXT NOT NULL, turn_id TEXT, ts TEXT, cmd TEXT, escalated INTEGER NOT NULL, PRIMARY KEY (thread_id, call_id));
|
|
14
|
+
CREATE TABLE IF NOT EXISTS compactions (thread_id TEXT NOT NULL, line INTEGER NOT NULL, ts TEXT, PRIMARY KEY (thread_id, line));
|
|
15
|
+
CREATE TABLE IF NOT EXISTS quarantine (path TEXT NOT NULL, line INTEGER NOT NULL, reason TEXT, PRIMARY KEY (path, line));
|
|
16
|
+
CREATE TABLE IF NOT EXISTS call_results (thread_id TEXT NOT NULL, call_id TEXT NOT NULL, exit_code INTEGER, skill TEXT, PRIMARY KEY (thread_id, call_id));
|
|
17
|
+
CREATE TABLE IF NOT EXISTS skill_reads (thread_id TEXT NOT NULL, call_id TEXT NOT NULL, skill TEXT NOT NULL, PRIMARY KEY (thread_id, call_id, skill));
|
|
18
|
+
CREATE TABLE IF NOT EXISTS guardian (thread_id TEXT NOT NULL, line INTEGER NOT NULL, ts TEXT, outcome TEXT NOT NULL, PRIMARY KEY (thread_id, line));
|
|
19
|
+
CREATE VIEW IF NOT EXISTS turn_usage AS
|
|
20
|
+
SELECT u.*, COALESCE(t.kind, 'main') AS kind, t.parent_id AS parent_id
|
|
21
|
+
FROM usage u LEFT JOIN threads t ON t.id = u.thread_id
|
|
22
|
+
WHERE u.key NOT LIKE 'tc:%' OR u.thread_id NOT IN (SELECT thread_id FROM usage WHERE key NOT LIKE 'tc:%');
|
|
23
|
+
DROP VIEW IF EXISTS call_view;
|
|
24
|
+
CREATE VIEW call_view AS
|
|
25
|
+
SELECT c.*, r.exit_code AS exit_code
|
|
26
|
+
FROM calls c LEFT JOIN call_results r ON r.thread_id = c.thread_id AND r.call_id = c.call_id;
|
|
27
|
+
"""
|
|
28
|
+
USAGE_KEYS = ("input_tokens", "cached_input_tokens", "output_tokens", "reasoning_output_tokens")
|
|
29
|
+
CMD = re.compile(r'\bcmd:\s*"((?:[^"\\]|\\.)*)"')
|
|
30
|
+
EXIT = re.compile(r'"exit_code"\s*:\s*(-?\d+)|Exit code:\s*(-?\d+)|exited with code\s+(-?\d+)')
|
|
31
|
+
SKILL = re.compile(r"skills[/\\]+([A-Za-z0-9_.-]+)[/\\]+SKILL\.md")
|
|
32
|
+
OUTPUTS = ("function_call_output", "custom_tool_call_output")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def db_path() -> Path:
|
|
36
|
+
return paths.state_dir() / "usage.sqlite3"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def connect(path: Path) -> sqlite3.Connection:
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
con = sqlite3.connect(path, timeout=30)
|
|
42
|
+
con.executescript(SCHEMA)
|
|
43
|
+
return con
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rollout_files(home: Path) -> list[Path]:
|
|
47
|
+
return sorted([*home.glob("sessions/**/rollout-*.jsonl"), *home.glob("archived_sessions/rollout-*.jsonl")])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def kind_of(meta: dict) -> str:
|
|
51
|
+
src = meta.get("source")
|
|
52
|
+
if meta.get("thread_source") == "guardian_review" or (isinstance(src, dict) and src.get("subagent") == {"other": "guardian"}):
|
|
53
|
+
return "guardian"
|
|
54
|
+
if isinstance(src, dict) and "subagent" in src:
|
|
55
|
+
return "subagent"
|
|
56
|
+
return "main"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _s(v) -> str | None:
|
|
60
|
+
return v if isinstance(v, str) else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _usage(u) -> tuple[int, ...] | None:
|
|
64
|
+
if not isinstance(u, dict) or not all(isinstance(u.get(k), int) and not isinstance(u.get(k), bool) for k in USAGE_KEYS):
|
|
65
|
+
return None
|
|
66
|
+
return tuple(u[k] for k in USAGE_KEYS)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _custom_call(call_id: str, payload: dict) -> tuple[str, str, bool] | None:
|
|
70
|
+
text = payload.get("input")
|
|
71
|
+
m = CMD.search(text) if isinstance(text, str) else None
|
|
72
|
+
if not m:
|
|
73
|
+
return None
|
|
74
|
+
return call_id, _decode(m.group(1)), "require_escalated" in text
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _function_call(call_id: str, payload: dict) -> tuple[str, str, bool] | None:
|
|
78
|
+
try:
|
|
79
|
+
args = json.loads(payload.get("arguments") or "{}")
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
return None
|
|
82
|
+
if not isinstance(args, dict):
|
|
83
|
+
return None
|
|
84
|
+
cmd = args.get("cmd", args.get("command"))
|
|
85
|
+
if isinstance(cmd, list):
|
|
86
|
+
cmd = " ".join(str(c) for c in cmd)
|
|
87
|
+
if not isinstance(cmd, str):
|
|
88
|
+
return None
|
|
89
|
+
return call_id, cmd, args.get("sandbox_permissions") == "require_escalated"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def tool_call(payload: dict) -> tuple[str, str, bool] | None:
|
|
93
|
+
call_id = payload.get("call_id")
|
|
94
|
+
if not isinstance(call_id, str):
|
|
95
|
+
return None
|
|
96
|
+
build = {"custom_tool_call": _custom_call, "function_call": _function_call}.get(payload.get("type"))
|
|
97
|
+
return build(call_id, payload) if build else None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _decode(raw: str) -> str:
|
|
101
|
+
try:
|
|
102
|
+
return json.loads(f'"{raw}"')
|
|
103
|
+
except ValueError:
|
|
104
|
+
return raw
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cell_commands(payload: dict) -> list[str]:
|
|
108
|
+
text = payload.get("input") if payload.get("type") == "custom_tool_call" else None
|
|
109
|
+
return [_decode(m) for m in CMD.findall(text)] if isinstance(text, str) else []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _output_text(payload: dict) -> str:
|
|
113
|
+
out = payload.get("output")
|
|
114
|
+
if isinstance(out, list):
|
|
115
|
+
return "\n".join(o["text"] for o in out if isinstance(o, dict) and isinstance(o.get("text"), str))
|
|
116
|
+
return out if isinstance(out, str) else ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def exit_code(payload: dict) -> int | None:
|
|
120
|
+
text = _output_text(payload)
|
|
121
|
+
try:
|
|
122
|
+
meta = json.loads(text).get("metadata")
|
|
123
|
+
except (ValueError, AttributeError):
|
|
124
|
+
meta = None
|
|
125
|
+
if isinstance(meta, dict) and isinstance(meta.get("exit_code"), int):
|
|
126
|
+
return meta["exit_code"]
|
|
127
|
+
m = EXIT.search(text)
|
|
128
|
+
return int(next(g for g in m.groups() if g is not None)) if m else None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def skills_of(cmd: str) -> list[str]:
|
|
132
|
+
return list(dict.fromkeys(SKILL.findall(cmd)))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def guardian_outcome(payload: dict) -> str | None:
|
|
136
|
+
try:
|
|
137
|
+
msg = json.loads(payload.get("last_agent_message") or "")
|
|
138
|
+
except (TypeError, ValueError):
|
|
139
|
+
return None
|
|
140
|
+
return msg["outcome"] if isinstance(msg, dict) and isinstance(msg.get("outcome"), str) else None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _response_item(con: sqlite3.Connection, thread: str, turn: str | None, ts: str | None, p: dict) -> None:
|
|
144
|
+
call = tool_call(p)
|
|
145
|
+
if call:
|
|
146
|
+
rows = [(call[0], call[1], int(call[2]))]
|
|
147
|
+
rows += [(f"{call[0]}#{i}", cmd, 0) for i, cmd in enumerate(cell_commands(p)[1:], start=2)]
|
|
148
|
+
for call_id, cmd, escalated in rows:
|
|
149
|
+
con.execute("INSERT OR IGNORE INTO calls (thread_id, call_id, turn_id, ts, cmd, escalated) VALUES (?,?,?,?,?,?)",
|
|
150
|
+
(thread, call_id, turn, ts, cmd, escalated))
|
|
151
|
+
for skill in skills_of(cmd):
|
|
152
|
+
con.execute("INSERT OR IGNORE INTO skill_reads VALUES (?,?,?)", (thread, call_id, skill))
|
|
153
|
+
elif p.get("type") in OUTPUTS and isinstance(p.get("call_id"), str):
|
|
154
|
+
code = exit_code(p)
|
|
155
|
+
if code is not None:
|
|
156
|
+
con.execute("INSERT INTO call_results (thread_id, call_id, exit_code) SELECT ?, ?, ? "
|
|
157
|
+
"WHERE EXISTS (SELECT 1 FROM calls WHERE thread_id = ? AND call_id = ?) "
|
|
158
|
+
"AND NOT EXISTS (SELECT 1 FROM calls WHERE thread_id = ? AND call_id = ? || '#2') "
|
|
159
|
+
"ON CONFLICT(thread_id, call_id) DO UPDATE SET exit_code = excluded.exit_code WHERE call_results.exit_code IS NULL",
|
|
160
|
+
(thread, p["call_id"], code, thread, p["call_id"], thread, p["call_id"]))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class _Cursor:
|
|
164
|
+
def __init__(self, thread=None, model=None, turn=None, line=0, last_total=-1):
|
|
165
|
+
self.thread, self.model, self.turn, self.line, self.last_total = thread, model, turn, line, last_total
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _session_meta(con, st, p, ts, quarantine) -> None:
|
|
169
|
+
if st.thread is None and isinstance(p.get("id"), str):
|
|
170
|
+
st.thread = p["id"]
|
|
171
|
+
con.execute("INSERT OR REPLACE INTO threads VALUES (?,?,?,?,?)",
|
|
172
|
+
(st.thread, _s(p.get("parent_thread_id")), kind_of(p), _s(p.get("timestamp")) or ts, _s(p.get("cwd"))))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _turn_context(con, st, p, ts, quarantine) -> None:
|
|
176
|
+
st.model = _s(p.get("model")) or st.model
|
|
177
|
+
st.turn = _s(p.get("turn_id")) or st.turn
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _token_usage_record(con, st, p, ts, quarantine) -> None:
|
|
181
|
+
u = _usage(p.get("usage"))
|
|
182
|
+
if u is None or not isinstance(p.get("response_id"), str):
|
|
183
|
+
quarantine("token_usage_record shape")
|
|
184
|
+
return
|
|
185
|
+
con.execute("INSERT OR IGNORE INTO usage VALUES (?,?,?,?,?,?,?,?,?)",
|
|
186
|
+
(st.thread, p["response_id"], _s(p.get("turn_id")) or st.turn, ts, st.model, *u))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _token_count(con, st, p, ts, quarantine) -> None:
|
|
190
|
+
info = p.get("info")
|
|
191
|
+
if info is None:
|
|
192
|
+
return
|
|
193
|
+
total = (info.get("total_token_usage") or {}).get("total_tokens") if isinstance(info, dict) else None
|
|
194
|
+
u = _usage(info.get("last_token_usage")) if isinstance(info, dict) else None
|
|
195
|
+
if u is None or not isinstance(total, int):
|
|
196
|
+
quarantine("token_count shape")
|
|
197
|
+
return
|
|
198
|
+
if total == st.last_total:
|
|
199
|
+
return
|
|
200
|
+
st.last_total = total
|
|
201
|
+
con.execute("INSERT OR IGNORE INTO usage VALUES (?,?,?,?,?,?,?,?,?)",
|
|
202
|
+
(st.thread, f"tc:{st.line}", st.turn, ts, st.model, *u))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _compacted(con, st, p, ts, quarantine) -> None:
|
|
206
|
+
con.execute("INSERT OR IGNORE INTO compactions VALUES (?,?,?)", (st.thread, st.line, ts))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _response(con, st, p, ts, quarantine) -> None:
|
|
210
|
+
_response_item(con, st.thread, st.turn, ts, p)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _task_complete(con, st, p, ts, quarantine) -> None:
|
|
214
|
+
outcome = guardian_outcome(p)
|
|
215
|
+
if outcome and con.execute("SELECT 1 FROM threads WHERE id=? AND kind='guardian'", (st.thread,)).fetchone():
|
|
216
|
+
con.execute("INSERT OR IGNORE INTO guardian VALUES (?,?,?,?)", (st.thread, st.line, ts, outcome))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
_RECORDS = {"turn_context": _turn_context, "token_usage_record": _token_usage_record,
|
|
220
|
+
"compacted": _compacted, "response_item": _response}
|
|
221
|
+
_EVENTS = {"token_count": _token_count, "task_complete": _task_complete}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _handler(t, p):
|
|
225
|
+
if t == "event_msg":
|
|
226
|
+
kind = p.get("type")
|
|
227
|
+
return _EVENTS.get(kind) if isinstance(kind, str) else None
|
|
228
|
+
return _RECORDS.get(t) if isinstance(t, str) else None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _record(con, st, raw, quarantine) -> None:
|
|
232
|
+
try:
|
|
233
|
+
o = json.loads(raw)
|
|
234
|
+
except ValueError:
|
|
235
|
+
quarantine("not JSON")
|
|
236
|
+
return
|
|
237
|
+
p = o.get("payload") if isinstance(o, dict) else None
|
|
238
|
+
if not isinstance(p, dict):
|
|
239
|
+
quarantine("no payload")
|
|
240
|
+
return
|
|
241
|
+
t, ts = o.get("type"), _s(o.get("timestamp"))
|
|
242
|
+
if t == "session_meta":
|
|
243
|
+
_session_meta(con, st, p, ts, quarantine)
|
|
244
|
+
elif st.thread is None:
|
|
245
|
+
quarantine("before session_meta")
|
|
246
|
+
else:
|
|
247
|
+
handler = _handler(t, p)
|
|
248
|
+
if handler:
|
|
249
|
+
handler(con, st, p, ts, quarantine)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _scan_file(con: sqlite3.Connection, path: Path) -> tuple[int, int]:
|
|
253
|
+
size = path.stat().st_size
|
|
254
|
+
row = con.execute("SELECT offset, thread_id, model, turn_id, line, last_total FROM files WHERE path=?", (str(path),)).fetchone()
|
|
255
|
+
offset, *state = row or (0, None, None, None, 0, -1)
|
|
256
|
+
if size < offset:
|
|
257
|
+
offset, state = 0, [None, None, None, 0, -1]
|
|
258
|
+
if size == offset:
|
|
259
|
+
return 0, 0
|
|
260
|
+
with open(path, "rb") as f:
|
|
261
|
+
f.seek(offset)
|
|
262
|
+
data = f.read()
|
|
263
|
+
end = data.rfind(b"\n") + 1
|
|
264
|
+
st = _Cursor(*state)
|
|
265
|
+
count = bad = 0
|
|
266
|
+
for raw in data[:end].split(b"\n")[:-1]:
|
|
267
|
+
st.line += 1
|
|
268
|
+
count += 1
|
|
269
|
+
|
|
270
|
+
def quarantine(reason: str, line: int = st.line) -> None:
|
|
271
|
+
nonlocal bad
|
|
272
|
+
bad += 1
|
|
273
|
+
con.execute("INSERT OR IGNORE INTO quarantine VALUES (?,?,?)", (str(path), line, reason))
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
_record(con, st, raw, quarantine)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
quarantine(type(e).__name__)
|
|
279
|
+
con.execute("INSERT OR REPLACE INTO files VALUES (?,?,?,?,?,?,?,?)",
|
|
280
|
+
(str(path), offset + end, size, st.thread, st.model, st.turn, st.line, st.last_total))
|
|
281
|
+
return count, bad
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def scan(con: sqlite3.Connection, home: Path, rescan: bool = False) -> dict[str, int]:
|
|
285
|
+
if rescan:
|
|
286
|
+
with con:
|
|
287
|
+
for table in ("files", "threads", "usage", "calls", "compactions", "quarantine", "guardian", "call_results", "skill_reads"):
|
|
288
|
+
con.execute(f"DELETE FROM {table}")
|
|
289
|
+
summary = {"files": 0, "lines": 0, "quarantined": 0}
|
|
290
|
+
for path in rollout_files(home):
|
|
291
|
+
with con:
|
|
292
|
+
lines, bad = _scan_file(con, path)
|
|
293
|
+
summary["files"] += 1
|
|
294
|
+
summary["lines"] += lines
|
|
295
|
+
summary["quarantined"] += bad
|
|
296
|
+
return summary
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cli(args) -> int:
|
|
300
|
+
try:
|
|
301
|
+
home = paths.codex_home(args.codex_home)
|
|
302
|
+
con = connect(db_path())
|
|
303
|
+
try:
|
|
304
|
+
summary = scan(con, home, rescan=args.rescan)
|
|
305
|
+
finally:
|
|
306
|
+
con.close()
|
|
307
|
+
print(f"code-jig collect: {summary['files']} files, {summary['lines']} new lines, {summary['quarantined']} quarantined", file=sys.stderr)
|
|
308
|
+
except Exception as e:
|
|
309
|
+
print(f"code-jig collect: {type(e).__name__}: {e}", file=sys.stderr)
|
|
310
|
+
print("{}")
|
|
311
|
+
return 0
|
codejig/doctor.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import mmap
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tomllib
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from codejig import managed, migrate, paths, probe, tiers
|
|
12
|
+
from codejig.install import (
|
|
13
|
+
HOOK_STATUS,
|
|
14
|
+
ROLE_TIERS,
|
|
15
|
+
_git_config,
|
|
16
|
+
_same_path,
|
|
17
|
+
release_digest,
|
|
18
|
+
render_agents,
|
|
19
|
+
render_profile,
|
|
20
|
+
role_files,
|
|
21
|
+
skill_sources,
|
|
22
|
+
valid_hook_groups,
|
|
23
|
+
)
|
|
24
|
+
from codejig.overlay import Overlay, load
|
|
25
|
+
|
|
26
|
+
Check = tuple[str, str, str]
|
|
27
|
+
|
|
28
|
+
PACK_KEYS = (
|
|
29
|
+
"project_doc_max_bytes",
|
|
30
|
+
"approvals_reviewer",
|
|
31
|
+
"model_auto_compact_token_limit",
|
|
32
|
+
"tool_output_token_limit",
|
|
33
|
+
"model_reasoning_effort",
|
|
34
|
+
"extra_policy",
|
|
35
|
+
"allowed_approvals_reviewers",
|
|
36
|
+
"ignore_rules",
|
|
37
|
+
"allow_managed_hooks_only",
|
|
38
|
+
"guardian_extra_policy",
|
|
39
|
+
"developer_instructions",
|
|
40
|
+
)
|
|
41
|
+
SHIM_MAX_BYTES = 1_000_000
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_codex_bin(arg: str | None) -> Path | None:
|
|
45
|
+
value = arg or os.environ.get("CODEX_BIN") or shutil.which("codex")
|
|
46
|
+
return Path(value) if value else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_binary(bin_path: Path | None, keys=PACK_KEYS) -> list[Check]:
|
|
50
|
+
name = "codex keys"
|
|
51
|
+
if bin_path is None or not bin_path.is_file():
|
|
52
|
+
return [("FAIL", name, "codex binary not found; pass --codex-bin PATH or set CODEX_BIN")]
|
|
53
|
+
try:
|
|
54
|
+
if bin_path.stat().st_size < SHIM_MAX_BYTES:
|
|
55
|
+
return [("FAIL", name, f"{bin_path} is a launcher, not the native binary; pass --codex-bin PATH")]
|
|
56
|
+
with open(bin_path, "rb") as f, mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as m:
|
|
57
|
+
missing = [k for k in keys if m.find(k.encode()) == -1]
|
|
58
|
+
except OSError as e:
|
|
59
|
+
return [("FAIL", name, f"cannot read {bin_path}: {e.strerror or e}")]
|
|
60
|
+
if missing:
|
|
61
|
+
return [("FAIL", name, f"not in {bin_path.name}: {', '.join(missing)}")]
|
|
62
|
+
return [("ok", name, f"{len(keys)} keys present in {bin_path.name}")]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def report(checks: list[Check]) -> int:
|
|
66
|
+
for level, name, detail in checks:
|
|
67
|
+
print(f"{level} {name}: {detail}")
|
|
68
|
+
return 1 if any(c[0] == "FAIL" for c in checks) else 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
DEFAULT_DOC_MAX = 32768
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _pick(directory: Path) -> Path | None:
|
|
75
|
+
for name in ("AGENTS.override.md", "AGENTS.md"):
|
|
76
|
+
f = directory / name
|
|
77
|
+
if f.is_file() and f.stat().st_size > 0:
|
|
78
|
+
return f
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _git_root(cwd: Path) -> Path:
|
|
83
|
+
for d in (cwd, *cwd.parents):
|
|
84
|
+
if (d / ".git").exists():
|
|
85
|
+
return d
|
|
86
|
+
return cwd
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def chain_files(home: Path, cwd: Path) -> list[Path]:
|
|
90
|
+
files = [f for f in [_pick(home)] if f]
|
|
91
|
+
root = _git_root(cwd)
|
|
92
|
+
dirs = [root, *[d for d in reversed(cwd.parents) if root in d.parents], cwd] if cwd != root else [root]
|
|
93
|
+
seen = set()
|
|
94
|
+
for d in dirs:
|
|
95
|
+
if d in seen:
|
|
96
|
+
continue
|
|
97
|
+
seen.add(d)
|
|
98
|
+
f = _pick(d)
|
|
99
|
+
if f:
|
|
100
|
+
files.append(f)
|
|
101
|
+
return files
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def read_config(home: Path) -> dict:
|
|
105
|
+
try:
|
|
106
|
+
return tomllib.loads((home / "config.toml").read_text(encoding="utf-8-sig"))
|
|
107
|
+
except (OSError, ValueError):
|
|
108
|
+
return {}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _doc_max(home: Path) -> int:
|
|
112
|
+
value = read_config(home).get("project_doc_max_bytes", DEFAULT_DOC_MAX)
|
|
113
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
114
|
+
return DEFAULT_DOC_MAX
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def check_chain(home: Path, cwd: Path) -> list[Check]:
|
|
119
|
+
cap = _doc_max(home)
|
|
120
|
+
total = sum(f.stat().st_size for f in chain_files(home, cwd))
|
|
121
|
+
detail = f"{total} of {cap} bytes"
|
|
122
|
+
if total > cap:
|
|
123
|
+
return [("FAIL", "AGENTS.md chain", detail + "; Codex silently stops adding files past the cap")]
|
|
124
|
+
if total > cap * 0.8:
|
|
125
|
+
return [("warn", "AGENTS.md chain", detail)]
|
|
126
|
+
return [("ok", "AGENTS.md chain", detail)]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check_global(home: Path, core: Path, ov: Overlay) -> list[Check]:
|
|
130
|
+
name = "global AGENTS.md"
|
|
131
|
+
override = home / "AGENTS.override.md"
|
|
132
|
+
if override.is_file() and override.stat().st_size > 0:
|
|
133
|
+
return [("FAIL", name, f"{override} is non-empty; Codex loads it instead of the generated AGENTS.md")]
|
|
134
|
+
target = home / "AGENTS.md"
|
|
135
|
+
if not target.is_file():
|
|
136
|
+
return [("warn", name, f"{target} is missing; run code-jig install")]
|
|
137
|
+
if target.read_bytes() != render_agents(core, ov.root).encode("utf-8"):
|
|
138
|
+
return [("warn", name, f"{target} is stale; run code-jig install")]
|
|
139
|
+
return [("ok", name, f"{target} is current")]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def catalog(home: Path) -> dict[str, set[str]] | None:
|
|
143
|
+
try:
|
|
144
|
+
data = json.loads((home / "models_cache.json").read_text(encoding="utf-8"))
|
|
145
|
+
except (OSError, ValueError):
|
|
146
|
+
return None
|
|
147
|
+
models = data.get("models") if isinstance(data, dict) else None
|
|
148
|
+
if not isinstance(models, list):
|
|
149
|
+
return None
|
|
150
|
+
out = {}
|
|
151
|
+
for m in models:
|
|
152
|
+
if isinstance(m, dict) and isinstance(m.get("slug"), str):
|
|
153
|
+
levels = m.get("supported_reasoning_levels")
|
|
154
|
+
out[m["slug"]] = {lv["effort"] for lv in levels if isinstance(lv, dict) and isinstance(lv.get("effort"), str)} if isinstance(levels, list) else set()
|
|
155
|
+
return out
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def family(slug: str) -> str:
|
|
159
|
+
return slug.rsplit("-", 1)[0]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _legacy_check(home: Path, ts, cfg: dict) -> Check:
|
|
163
|
+
profiles = cfg.get("profiles") if isinstance(cfg.get("profiles"), dict) else {}
|
|
164
|
+
legacy = [t.name for t in ts if cfg.get("profile") == t.name or t.name in profiles]
|
|
165
|
+
return ("FAIL", "legacy profiles", f"{home / 'config.toml'} has profile = or [profiles.*] for {', '.join(legacy)}; Codex refuses --profile with them") \
|
|
166
|
+
if legacy else ("ok", "legacy profiles", "none")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _profile_check(home: Path, t, policy, ov: Overlay) -> Check:
|
|
170
|
+
f = home / f"{t.name}.config.toml"
|
|
171
|
+
name = f"tier {t.name} profile"
|
|
172
|
+
if not f.is_file():
|
|
173
|
+
return ("warn", name, f"{f} is missing; Codex silently uses the base config; run code-jig install")
|
|
174
|
+
if f.read_bytes() != render_profile(t, policy, ov.auto_compact_token_limit).encode("utf-8"):
|
|
175
|
+
return ("warn", name, f"{f} is stale or edited; run code-jig install")
|
|
176
|
+
return ("ok", name, f"{t.model} at {t.effort}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _model_check(t, cat: dict) -> Check:
|
|
180
|
+
name = f"tier {t.name} model"
|
|
181
|
+
if t.model not in cat:
|
|
182
|
+
return ("FAIL", name, f"{t.model} is not in this account's catalog; candidates: {', '.join(sorted(cat))}")
|
|
183
|
+
if cat[t.model] and t.effort not in cat[t.model]:
|
|
184
|
+
return ("FAIL", name, f"{t.model} does not support {t.effort}; supported: {', '.join(sorted(cat[t.model]))}")
|
|
185
|
+
return ("ok", name, f"{t.model} supports {t.effort}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _family_checks(ts) -> list[Check]:
|
|
189
|
+
by = {t.name: t for t in ts}
|
|
190
|
+
if "review" not in by or "default" not in by:
|
|
191
|
+
return []
|
|
192
|
+
same = family(by["review"].model) == family(by["default"].model)
|
|
193
|
+
return [("warn", "review family", f"review and default are both {family(by['default'].model)}; cross-family review catches different bugs")
|
|
194
|
+
if same else ("ok", "review family", f"{by['review'].model} vs {by['default'].model}")]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def check_tiers(home: Path, core: Path, ov: Overlay) -> list[Check]:
|
|
198
|
+
try:
|
|
199
|
+
ts = tiers.load(core, ov.root)
|
|
200
|
+
except ValueError as e:
|
|
201
|
+
return [("FAIL", "tiers", str(e))]
|
|
202
|
+
checks: list[Check] = [_legacy_check(home, ts, read_config(home))]
|
|
203
|
+
policy = tiers.extra_policy(ov.root)
|
|
204
|
+
checks += [_profile_check(home, t, policy, ov) for t in ts]
|
|
205
|
+
cat = catalog(home)
|
|
206
|
+
if cat is None:
|
|
207
|
+
checks.append(("warn", "model catalog", f"{home / 'models_cache.json'} missing or unreadable; start codex once"))
|
|
208
|
+
else:
|
|
209
|
+
checks += [_model_check(t, cat) for t in ts]
|
|
210
|
+
return checks + _family_checks(ts)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def check_roles(home: Path, core: Path, ov: Overlay) -> list[Check]:
|
|
214
|
+
try:
|
|
215
|
+
ts = tiers.load(core, ov.root)
|
|
216
|
+
except ValueError:
|
|
217
|
+
return []
|
|
218
|
+
by = {t.name: t for t in ts}
|
|
219
|
+
checks: list[Check] = []
|
|
220
|
+
for name, text in role_files(core, ts).items():
|
|
221
|
+
role = name.removesuffix(".toml")
|
|
222
|
+
t = by[ROLE_TIERS[role]]
|
|
223
|
+
f = home / "agents" / name
|
|
224
|
+
if not f.is_file():
|
|
225
|
+
checks.append(("warn", f"{role} role", f"{f} is missing; subagents of that kind use the parent's model; run code-jig install"))
|
|
226
|
+
elif f.read_bytes() != text.encode("utf-8"):
|
|
227
|
+
checks.append(("warn", f"{role} role", f"{f} is stale or edited; run code-jig install"))
|
|
228
|
+
elif "default" in by and t.model == by["default"].model and t.effort == by["default"].effort:
|
|
229
|
+
checks.append(("warn", f"{role} role", f"{t.name} tier is the same as default ({t.model} at {t.effort}); the role saves nothing"))
|
|
230
|
+
else:
|
|
231
|
+
checks.append(("ok", f"{role} role", f"{role}s run {t.model} at {t.effort}"))
|
|
232
|
+
return checks
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def check_git_gate(ov: Overlay, cwd: Path) -> list[Check]:
|
|
236
|
+
target = paths.state_dir() / "git-hooks"
|
|
237
|
+
current = _git_config("--get", "core.hooksPath") or ""
|
|
238
|
+
if not ov.git_gate:
|
|
239
|
+
return [("warn", "git gate", f"off in overlay.toml but core.hooksPath still points at {target}; run code-jig install")
|
|
240
|
+
if _same_path(current, target) else ("ok", "git gate", "off")]
|
|
241
|
+
if not _same_path(current, target):
|
|
242
|
+
return [("warn", "git gate", f"not active: global core.hooksPath is {current or 'unset'}, expected {target}")]
|
|
243
|
+
local = subprocess.run(["git", "-C", str(cwd), "config", "--local", "--get", "core.hooksPath"],
|
|
244
|
+
capture_output=True, text=True, encoding="utf-8", check=False).stdout.strip()
|
|
245
|
+
if local:
|
|
246
|
+
return [("warn", "git gate", f"this repo sets core.hooksPath = {local}, which bypasses the gate here")]
|
|
247
|
+
return [("ok", "git gate", f"active{' with the prose gate' if ov.prose_gate else ''}")]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def check_hook(home: Path) -> list[Check]:
|
|
251
|
+
path = home / "hooks.json"
|
|
252
|
+
if not path.is_file():
|
|
253
|
+
return [("warn", "stop hook", f"{path} missing; run code-jig install")]
|
|
254
|
+
try:
|
|
255
|
+
groups = json.loads(path.read_text(encoding="utf-8-sig"))["hooks"]["Stop"]
|
|
256
|
+
if not valid_hook_groups(groups):
|
|
257
|
+
raise ValueError('expected {"hooks": {"Stop": [{"hooks": [...]}]}}')
|
|
258
|
+
except (OSError, ValueError, KeyError, TypeError) as e:
|
|
259
|
+
return [("FAIL", "stop hook", f"{path}: {e}")]
|
|
260
|
+
found = isinstance(groups, list) and any(
|
|
261
|
+
isinstance(g, dict) and any(isinstance(h, dict) and h.get("statusMessage") == HOOK_STATUS for h in g.get("hooks") or [])
|
|
262
|
+
for g in groups)
|
|
263
|
+
return [("ok", "stop hook", "registered; Codex asks to trust it once") if found
|
|
264
|
+
else ("warn", "stop hook", "not registered; run code-jig install")]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def check_knowledge(ov: Overlay) -> list[Check]:
|
|
268
|
+
kb = ov.root / "knowledge"
|
|
269
|
+
index = kb / "INDEX.md"
|
|
270
|
+
if not kb.is_dir():
|
|
271
|
+
return [("ok", "knowledge", "no knowledge base")]
|
|
272
|
+
if not index.is_file():
|
|
273
|
+
return [("warn", "knowledge", f"{kb} has no INDEX.md; Codex is not pointed at it")]
|
|
274
|
+
n = len(index.read_text(encoding="utf-8-sig").splitlines())
|
|
275
|
+
return [("ok", "knowledge", f"index has {n} of 150 lines") if n <= 150
|
|
276
|
+
else ("warn", "knowledge", f"{index} has {n} lines; merge or delete facts to stay at 150")]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def check_superpowers(home: Path) -> list[Check]:
|
|
280
|
+
plugins = read_config(home).get("plugins")
|
|
281
|
+
if isinstance(plugins, dict) and any(k.startswith("superpowers@") for k in plugins) \
|
|
282
|
+
or any((home / "plugins" / "cache").glob("*/superpowers")):
|
|
283
|
+
return [("ok", "superpowers", "installed")]
|
|
284
|
+
return [("warn", "superpowers", "not installed; run: codex plugin add superpowers@openai-curated-remote")]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _git(core: Path, *args: str) -> subprocess.CompletedProcess | None:
|
|
288
|
+
try:
|
|
289
|
+
return subprocess.run(["git", "-C", str(core), *args], capture_output=True, text=True, encoding="utf-8", check=False)
|
|
290
|
+
except OSError:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def check_release(core: Path, ov: Overlay) -> list[Check]:
|
|
295
|
+
strict = "FAIL" if not ov.core_writable else "warn"
|
|
296
|
+
try:
|
|
297
|
+
words = (core / "VERSION").read_text(encoding="utf-8", errors="replace").split()
|
|
298
|
+
except OSError as e:
|
|
299
|
+
return [("FAIL", "core pin", f"cannot read VERSION: {e}")]
|
|
300
|
+
tag, digest = (words + ["", ""])[:2]
|
|
301
|
+
pin = ("ok", "core pin", tag) if tag == ov.core_version else (
|
|
302
|
+
strict, "core pin", f"core is release {tag or 'unknown'}, overlay pins {ov.core_version}")
|
|
303
|
+
if ov.core_writable:
|
|
304
|
+
return [pin, ("ok", "core clean", "release core; local edits allowed")]
|
|
305
|
+
if digest and digest == release_digest(core):
|
|
306
|
+
return [pin, ("ok", "core clean", "matches its release digest")]
|
|
307
|
+
return [pin, ("FAIL", "core clean", "core differs from its release digest but core_writable = false")]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _git_failure(core: Path, ov: Overlay, strict: str, status) -> list[Check]:
|
|
311
|
+
reason = f"{core} is not a readable git repo or git is missing"
|
|
312
|
+
first = (status.stderr.strip().splitlines() or [""])[0] if status else ""
|
|
313
|
+
if first:
|
|
314
|
+
reason += f": {first}"
|
|
315
|
+
pin = (strict, "core pin", f"cannot be checked; overlay pins {ov.core_version}")
|
|
316
|
+
return [pin, ("FAIL", "core clean", reason)]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def check_core(core: Path, ov: Overlay) -> list[Check]:
|
|
320
|
+
if (core / "VERSION").is_file() and not (core / ".git").exists():
|
|
321
|
+
return check_release(core, ov)
|
|
322
|
+
strict = "FAIL" if not ov.core_writable else "warn"
|
|
323
|
+
described = _git(core, "describe", "--tags", "--exact-match", "HEAD")
|
|
324
|
+
tag = described.stdout.strip() if described and described.returncode == 0 else ""
|
|
325
|
+
status = _git(core, "status", "--porcelain")
|
|
326
|
+
if status is None or status.returncode != 0:
|
|
327
|
+
return _git_failure(core, ov, strict, status)
|
|
328
|
+
pin = ("ok", "core pin", tag) if tag == ov.core_version else (
|
|
329
|
+
strict, "core pin", f"core is at {tag or 'an untagged commit'}, overlay pins {ov.core_version}")
|
|
330
|
+
dirty = status.stdout.strip()
|
|
331
|
+
clean = ("FAIL", "core clean", "core has local edits but core_writable = false") if dirty and not ov.core_writable \
|
|
332
|
+
else ("ok", "core clean", "local edits allowed" if dirty else "no local edits")
|
|
333
|
+
return [pin, clean]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def cli(args) -> int:
|
|
337
|
+
bin_path = resolve_codex_bin(args.codex_bin)
|
|
338
|
+
home = paths.codex_home(args.codex_home)
|
|
339
|
+
checks = [("FAIL", "requirements", "git not found")] if not shutil.which("git") else []
|
|
340
|
+
checks += check_binary(bin_path)
|
|
341
|
+
if not args.check_binary:
|
|
342
|
+
try:
|
|
343
|
+
ov = load(paths.overlay_dir(args.overlay))
|
|
344
|
+
except ValueError as e:
|
|
345
|
+
checks.append(("FAIL", "overlay", str(e)))
|
|
346
|
+
else:
|
|
347
|
+
checks += check_core(paths.CORE, ov)
|
|
348
|
+
checks += check_global(home, paths.CORE, ov)
|
|
349
|
+
checks += check_tiers(home, paths.CORE, ov)
|
|
350
|
+
checks += check_roles(home, paths.CORE, ov)
|
|
351
|
+
checks += check_hook(home)
|
|
352
|
+
checks += check_git_gate(ov, Path.cwd())
|
|
353
|
+
checks += check_knowledge(ov)
|
|
354
|
+
checks += check_superpowers(home)
|
|
355
|
+
try:
|
|
356
|
+
ts = tiers.load(paths.CORE, ov.root)
|
|
357
|
+
except ValueError:
|
|
358
|
+
ts = []
|
|
359
|
+
checks += managed.check_requirements(managed.requirements_path(), paths.CORE / "bench" / "requirements.toml", ts)
|
|
360
|
+
try:
|
|
361
|
+
skills = sorted(skill_sources(paths.CORE, ov.root))
|
|
362
|
+
except ValueError:
|
|
363
|
+
skills = []
|
|
364
|
+
checks += probe.check_prompt(bin_path, home, Path.cwd(), ts, skills)
|
|
365
|
+
checks += probe.check_rules(bin_path, home, Path.cwd())
|
|
366
|
+
if args.live:
|
|
367
|
+
checks += probe.check_live(bin_path, home, Path.cwd(), ts)
|
|
368
|
+
checks += check_chain(home, Path.cwd())
|
|
369
|
+
checks += [("FAIL", "old state", f"{line}; run code-jig migrate") for line in migrate.pending(home)]
|
|
370
|
+
return report(checks)
|