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/install.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from codejig import gitgate, paths, tiers
|
|
12
|
+
from codejig.atomicwrite import atomic_write
|
|
13
|
+
from codejig.overlay import Overlay, load
|
|
14
|
+
|
|
15
|
+
MARKER_BODY = "generated by code-jig sha256:"
|
|
16
|
+
MARKER_NOTE = " : edit the core or the overlay, not this file"
|
|
17
|
+
STYLES = {"md": ("<!-- ", " -->"), "hash": ("# ", "")}
|
|
18
|
+
MARKER_PREFIX = STYLES["md"][0] + MARKER_BODY
|
|
19
|
+
BOM = b"\xef\xbb\xbf"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def stamp(body: str, style: str = "md") -> str:
|
|
23
|
+
head, tail = STYLES[style]
|
|
24
|
+
digest = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
|
25
|
+
return f"{head}{MARKER_BODY}{digest}{MARKER_NOTE}{tail}\n{body}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _off_path(directory: Path) -> bool:
|
|
29
|
+
want = os.path.normcase(os.path.abspath(directory))
|
|
30
|
+
return want not in [os.path.normcase(os.path.abspath(p)) for p in os.environ.get("PATH", "").split(os.pathsep) if p]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_agents(core: Path, overlay_root: Path) -> str:
|
|
34
|
+
parts = [(core / "core" / "AGENTS.core.md").read_text(encoding="utf-8-sig")]
|
|
35
|
+
extra = overlay_root / "AGENTS.overlay.md"
|
|
36
|
+
if extra.is_file():
|
|
37
|
+
parts.append(extra.read_text(encoding="utf-8-sig"))
|
|
38
|
+
launcher = os.environ.get("CODE_JIG_LAUNCHER")
|
|
39
|
+
if launcher:
|
|
40
|
+
launcher = Path(launcher).with_name("code-jig" + Path(launcher).suffix)
|
|
41
|
+
if launcher and _off_path(launcher.parent):
|
|
42
|
+
parts.append(f"On this machine `code-jig` is not on PATH; run it as `{launcher}`.\n")
|
|
43
|
+
index = overlay_root / "knowledge" / "INDEX.md"
|
|
44
|
+
if index.is_file():
|
|
45
|
+
parts.append(f"## Knowledge\n\nThis machine keeps one fact per file in {index.parent}. "
|
|
46
|
+
f"Read {index} when a task touches a tool, service or convention it may cover.\n")
|
|
47
|
+
return stamp("\n".join(p.rstrip("\n") + "\n" for p in parts))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_generated(path: Path) -> bool:
|
|
51
|
+
data = path.read_bytes().removeprefix(BOM)
|
|
52
|
+
first, _, body = data.partition(b"\n")
|
|
53
|
+
for head, tail in STYLES.values():
|
|
54
|
+
prefix, suffix = (head + MARKER_BODY).encode(), (MARKER_NOTE + tail).encode()
|
|
55
|
+
if len(first) > len(prefix) + len(suffix) and first.startswith(prefix) and first.endswith(suffix):
|
|
56
|
+
digest = first[len(prefix): len(first) - len(suffix)].decode("ascii", "replace")
|
|
57
|
+
return hashlib.sha256(body).hexdigest() == digest
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _backup_name(path: Path) -> Path:
|
|
62
|
+
backup = path.with_name(f"{path.name}.bak-{time.strftime('%Y%m%d-%H%M%S')}")
|
|
63
|
+
n = 0
|
|
64
|
+
while backup.exists() or backup.is_symlink():
|
|
65
|
+
n += 1
|
|
66
|
+
backup = path.with_name(f"{path.name}.bak-{time.strftime('%Y%m%d-%H%M%S')}-{n}")
|
|
67
|
+
return backup
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write_generated(path: Path, text: str) -> Path | None:
|
|
71
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
backup = None
|
|
73
|
+
if path.exists() and not is_generated(path):
|
|
74
|
+
backup = _backup_name(path)
|
|
75
|
+
shutil.copy2(path, backup)
|
|
76
|
+
atomic_write(path, text.encode("utf-8"))
|
|
77
|
+
return backup
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
HOOK_STATUS = "code-jig collect"
|
|
81
|
+
CLAUDE_CODE_WATERMARK_STATUS = "code-jig watermark"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def interpreter() -> str:
|
|
85
|
+
return getattr(sys, "_base_executable", None) or sys.executable
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def hook_commands(lib: Path, home: Path, python: str) -> tuple[str, str]:
|
|
89
|
+
posix = f"PYTHONPATH={shlex.quote(str(lib))} {shlex.quote(python)} -P -s -m codejig --codex-home {shlex.quote(str(home))} collect"
|
|
90
|
+
windows = f'set "PYTHONPATH={lib}" && "{python}" -P -s -m codejig --codex-home "{home}" collect'
|
|
91
|
+
return posix, windows
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def watermark_commands(lib: Path, python: str, agent: str) -> tuple[str, str]:
|
|
95
|
+
posix = f"PYTHONPATH={shlex.quote(str(lib))} {shlex.quote(python)} -P -s -m codejig hooks --hook-type {agent}"
|
|
96
|
+
windows = f'set "PYTHONPATH={lib}" && "{python}" -P -s -m codejig hooks --hook-type {agent}'
|
|
97
|
+
return posix, windows
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def claude_code_watermark_hook(lib: Path, python: str) -> str:
|
|
101
|
+
return watermark_commands(lib, python, "claude-code")[0]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _without_status(groups: list, status: str) -> list:
|
|
105
|
+
return [g for g in groups if not (isinstance(g, dict) and any(
|
|
106
|
+
isinstance(h, dict) and h.get("statusMessage") == status for h in g.get("hooks") or []))]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def valid_hook_groups(groups) -> bool:
|
|
110
|
+
return isinstance(groups, list) and all(
|
|
111
|
+
isinstance(g, dict) and isinstance(g.get("hooks", []), list)
|
|
112
|
+
and all(isinstance(h, dict) for h in g.get("hooks", [])) for g in groups)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _load_hooks(path: Path, event: str) -> tuple[dict, dict]:
|
|
116
|
+
try:
|
|
117
|
+
data = json.loads(path.read_text(encoding="utf-8-sig")) if path.is_file() else {"hooks": {}}
|
|
118
|
+
except (OSError, ValueError) as e:
|
|
119
|
+
raise ValueError(f"{path}: {e}") from e
|
|
120
|
+
hooks = data.setdefault("hooks", {}) if isinstance(data, dict) else None
|
|
121
|
+
if not isinstance(hooks, dict) or not valid_hook_groups(hooks.setdefault(event, [])):
|
|
122
|
+
raise ValueError(f'{path}: expected {{"hooks": {{"{event}": [...]}}}}')
|
|
123
|
+
return data, hooks
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def merged_hooks(path: Path, posix: str, windows: str, watermark: tuple[str, str] | None = None) -> str:
|
|
127
|
+
data, hooks = _load_hooks(path, "Stop")
|
|
128
|
+
hooks["Stop"] = _without_status(hooks["Stop"], HOOK_STATUS)
|
|
129
|
+
hooks["Stop"].append({"hooks": [{"type": "command", "command": posix, "commandWindows": windows,
|
|
130
|
+
"timeout": 60, "statusMessage": HOOK_STATUS}]})
|
|
131
|
+
if watermark is not None:
|
|
132
|
+
post = hooks.setdefault("PostToolUse", [])
|
|
133
|
+
if not isinstance(post, list):
|
|
134
|
+
raise ValueError(f'{path}: expected {{"hooks": {{"PostToolUse": [...]}}}}')
|
|
135
|
+
hooks["PostToolUse"] = _without_status(post, CLAUDE_CODE_WATERMARK_STATUS)
|
|
136
|
+
hooks["PostToolUse"].append({"hooks": [{"type": "command", "command": watermark[0],
|
|
137
|
+
"commandWindows": watermark[1], "timeout": 10,
|
|
138
|
+
"statusMessage": CLAUDE_CODE_WATERMARK_STATUS}]})
|
|
139
|
+
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def write_merged(path: Path, text: str) -> Path | None:
|
|
143
|
+
if path.is_file() and path.read_bytes() == text.encode("utf-8"):
|
|
144
|
+
return None
|
|
145
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
backup = None
|
|
147
|
+
if path.exists():
|
|
148
|
+
backup = _backup_name(path)
|
|
149
|
+
shutil.copy2(path, backup)
|
|
150
|
+
atomic_write(path, text.encode("utf-8"))
|
|
151
|
+
return backup
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def merged_claude_code_hooks(path: Path, command: str) -> str:
|
|
155
|
+
data, hooks = _load_hooks(path, "PostToolUse")
|
|
156
|
+
hooks["PostToolUse"] = _without_status(hooks["PostToolUse"], CLAUDE_CODE_WATERMARK_STATUS)
|
|
157
|
+
hooks["PostToolUse"].append({
|
|
158
|
+
"matcher": "Write|Edit|MultiEdit|NotebookEdit",
|
|
159
|
+
"hooks": [{"type": "command", "command": command, "timeout": 10,
|
|
160
|
+
"statusMessage": CLAUDE_CODE_WATERMARK_STATUS}],
|
|
161
|
+
})
|
|
162
|
+
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
RULES_PREFIX = "code-jig-"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def rule_files(overlay_root: Path) -> dict[str, str]:
|
|
169
|
+
src = overlay_root / "rules"
|
|
170
|
+
if not src.is_dir():
|
|
171
|
+
return {}
|
|
172
|
+
return {f"{RULES_PREFIX}{p.stem}.rules": stamp(p.read_text(encoding="utf-8-sig"), "hash")
|
|
173
|
+
for p in sorted(src.glob("*.rules"))}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def prune_rules(rules_dir: Path, keep: set[str]) -> list[str]:
|
|
177
|
+
lines = []
|
|
178
|
+
for f in sorted(rules_dir.glob(f"{RULES_PREFIX}*.rules")) if rules_dir.is_dir() else []:
|
|
179
|
+
if f.name in keep:
|
|
180
|
+
continue
|
|
181
|
+
if is_generated(f):
|
|
182
|
+
f.unlink()
|
|
183
|
+
lines.append(f"removed {f}: no longer in the overlay")
|
|
184
|
+
else:
|
|
185
|
+
lines.append(f"warning: left {f}: edited by hand, Codex still loads it")
|
|
186
|
+
return lines
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
SKILL_SIDECAR = ".code-jig"
|
|
190
|
+
NOT_INSTALLED = (SKILL_SIDECAR, ".skill-check", "evals.toml")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _skill_dirs(root: Path) -> dict[str, Path]:
|
|
194
|
+
return {p.parent.name: p.parent for p in sorted(root.glob("*/SKILL.md"))} if root.is_dir() else {}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def skill_sources(core: Path, overlay_root: Path) -> dict[str, Path]:
|
|
198
|
+
core_skills, ov_skills = _skill_dirs(core / "skills"), _skill_dirs(overlay_root / "skills")
|
|
199
|
+
clash = sorted(set(core_skills) & set(ov_skills))
|
|
200
|
+
if clash:
|
|
201
|
+
raise ValueError(f"{overlay_root / 'skills'}: {', '.join(clash)} already in {core / 'skills'}; rename the overlay skill")
|
|
202
|
+
return {**core_skills, **ov_skills}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def installed_file(src: Path, f: Path) -> bool:
|
|
206
|
+
rel = f.relative_to(src)
|
|
207
|
+
return "__pycache__" not in rel.parts and not (len(rel.parts) == 1 and rel.name in NOT_INSTALLED) and rel.parts[0] != "evals"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def tree_digest(src: Path) -> str:
|
|
211
|
+
h = hashlib.sha256()
|
|
212
|
+
for f in sorted((p for p in src.rglob("*") if p.is_file() and installed_file(src, p)), key=lambda p: p.relative_to(src).parts):
|
|
213
|
+
h.update(f.relative_to(src).as_posix().encode() + b"\0" + f.read_bytes() + b"\0")
|
|
214
|
+
return h.hexdigest()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def release_digest(root: Path) -> str:
|
|
218
|
+
h = hashlib.sha256()
|
|
219
|
+
files = []
|
|
220
|
+
for p in root.rglob("*"):
|
|
221
|
+
rel = p.relative_to(root)
|
|
222
|
+
if p.is_file() and "__pycache__" not in rel.parts and rel.as_posix() != "VERSION":
|
|
223
|
+
files.append((rel.as_posix(), p))
|
|
224
|
+
for rel, p in sorted(files):
|
|
225
|
+
h.update(rel.encode() + b"\0" + p.read_bytes() + b"\0")
|
|
226
|
+
return h.hexdigest()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _own_skill(d: Path) -> bool:
|
|
230
|
+
side = d / SKILL_SIDECAR
|
|
231
|
+
return side.is_file() and side.read_text(encoding="utf-8").strip() == tree_digest(d)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _ignore_installed(src: Path):
|
|
235
|
+
return lambda d, names: [n for n in names if n == "__pycache__" or (n in (*NOT_INSTALLED, "evals") and Path(d) == src)]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _install_skill(name: str, src: Path, dest: Path) -> list[str]:
|
|
239
|
+
lines = []
|
|
240
|
+
target = dest / name
|
|
241
|
+
if target.exists() and not _own_skill(target):
|
|
242
|
+
backups = dest.parent / "skills-backup"
|
|
243
|
+
backups.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
backup = _backup_name(backups / name)
|
|
245
|
+
target.replace(backup)
|
|
246
|
+
lines.append(f"moved your previous {target} to {backup}")
|
|
247
|
+
elif target.exists():
|
|
248
|
+
shutil.rmtree(target)
|
|
249
|
+
shutil.copytree(src, target, ignore=_ignore_installed(src))
|
|
250
|
+
(target / SKILL_SIDECAR).write_text(tree_digest(target) + "\n", encoding="utf-8", newline="\n")
|
|
251
|
+
lines.append(f"wrote {target}")
|
|
252
|
+
return lines
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _prune_skills(sources: dict[str, Path], dest: Path) -> list[str]:
|
|
256
|
+
lines = []
|
|
257
|
+
for d in sorted(p for p in dest.iterdir() if p.is_dir() and not p.is_symlink() and p.name not in sources and (p / SKILL_SIDECAR).is_file()):
|
|
258
|
+
if _own_skill(d):
|
|
259
|
+
shutil.rmtree(d)
|
|
260
|
+
lines.append(f"removed {d}: no longer in the core or overlay")
|
|
261
|
+
else:
|
|
262
|
+
lines.append(f"warning: left {d}: edited by hand, Codex still loads it")
|
|
263
|
+
return lines
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def install_skills(sources: dict[str, Path], dest: Path) -> list[str]:
|
|
267
|
+
lines = []
|
|
268
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
for name, src in sources.items():
|
|
270
|
+
lines += _install_skill(name, src, dest)
|
|
271
|
+
return lines + _prune_skills(sources, dest)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def render_profile(tier: tiers.Tier, policy: str, compact: int | None = None) -> str:
|
|
275
|
+
return stamp(tiers.profile_body(tier, policy, compact), "hash")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
ROLE_TIERS = {"explorer": "bulk", "reviewer": "review", "judge": "deep"}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def role_files(core: Path, ts: list[tiers.Tier]) -> dict[str, str]:
|
|
282
|
+
by = {t.name: t for t in ts}
|
|
283
|
+
out = {}
|
|
284
|
+
for role, tier in ROLE_TIERS.items():
|
|
285
|
+
template = core / "core" / "agents" / f"{role}.toml"
|
|
286
|
+
if tier in by and template.is_file():
|
|
287
|
+
t = by[tier]
|
|
288
|
+
head = f"model = {tiers.toml_str(t.model)}\nmodel_reasoning_effort = {tiers.toml_str(t.effort)}\n"
|
|
289
|
+
out[f"{role}.toml"] = stamp(head + template.read_text(encoding="utf-8-sig"), "hash")
|
|
290
|
+
return out
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def plan(core: Path, ov: Overlay, home: Path, claude_code_settings: Path | None = None) -> list[tuple[Path, str]]:
|
|
294
|
+
skill_sources(core, ov.root)
|
|
295
|
+
policy = tiers.extra_policy(ov.root)
|
|
296
|
+
outputs = [(home / "AGENTS.md", render_agents(core, ov.root))]
|
|
297
|
+
ts = tiers.load(core, ov.root)
|
|
298
|
+
outputs += [(home / f"{t.name}.config.toml", render_profile(t, policy, ov.auto_compact_token_limit)) for t in ts]
|
|
299
|
+
outputs += [(home / "agents" / name, text) for name, text in role_files(core, ts).items()]
|
|
300
|
+
outputs += [(home / "rules" / name, text) for name, text in rule_files(ov.root).items()]
|
|
301
|
+
lib = paths.state_dir() / "lib"
|
|
302
|
+
outputs += [(lib / "codejig" / p.name, stamp(p.read_text(encoding="utf-8"), "hash")) for p in sorted(((core / "codejig") if (core / "codejig").is_dir() else paths.CODE).glob("*.py"))]
|
|
303
|
+
outputs.append((home / "hooks.json", merged_hooks(home / "hooks.json", *hook_commands(lib, home, interpreter()),
|
|
304
|
+
watermark=watermark_commands(lib, interpreter(), "codex"))))
|
|
305
|
+
if claude_code_settings and claude_code_settings.is_file():
|
|
306
|
+
outputs.append((claude_code_settings, merged_claude_code_hooks(claude_code_settings, claude_code_watermark_hook(lib, interpreter()))))
|
|
307
|
+
return outputs
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _git_config(*args: str) -> str | None:
|
|
311
|
+
try:
|
|
312
|
+
r = subprocess.run(["git", "config", "--global", *args], capture_output=True, text=True, encoding="utf-8", check=False)
|
|
313
|
+
except OSError:
|
|
314
|
+
return None
|
|
315
|
+
return r.stdout.strip()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _same_path(a: str, b: Path) -> bool:
|
|
319
|
+
return bool(a) and os.path.normcase(os.path.abspath(os.path.expanduser(a))) == os.path.normcase(os.path.abspath(b))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def git_gate(ov: Overlay) -> list[str]:
|
|
323
|
+
target = paths.state_dir() / "git-hooks"
|
|
324
|
+
current = _git_config("--get", "core.hooksPath")
|
|
325
|
+
if current is None:
|
|
326
|
+
return ["warning: git not found; the git gate was not installed"] if ov.git_gate else []
|
|
327
|
+
if not ov.git_gate:
|
|
328
|
+
if _same_path(current, target):
|
|
329
|
+
_git_config("--unset", "core.hooksPath")
|
|
330
|
+
return [f"removed the git gate: core.hooksPath no longer points at {target}"]
|
|
331
|
+
return []
|
|
332
|
+
for name, text in gitgate.shims(paths.state_dir() / "lib", interpreter(), ov.prose_gate, ov.push_cleanup is not False).items():
|
|
333
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
334
|
+
atomic_write(target / name, text.encode("utf-8"), mode=0o755)
|
|
335
|
+
if current and not _same_path(current, target):
|
|
336
|
+
return [(f"warning: git core.hooksPath is {current}; the gate is in {target} but not active. "
|
|
337
|
+
f"Activate it with: git config --global core.hooksPath \"{target}\" (each repo's own .git/hooks still run)")]
|
|
338
|
+
_git_config("core.hooksPath", str(target))
|
|
339
|
+
return [f"git gate active: core.hooksPath = {target}"]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def run(core: Path, ov: Overlay, home: Path) -> list[str]:
|
|
343
|
+
lines = []
|
|
344
|
+
claude_settings = paths.claude_code_settings()
|
|
345
|
+
for target, text in plan(core, ov, home, claude_settings):
|
|
346
|
+
backup = (write_merged if target.name in ("hooks.json", "settings.json") else write_generated)(target, text)
|
|
347
|
+
lines.append(f"wrote {target}")
|
|
348
|
+
if backup:
|
|
349
|
+
lines.append(f"moved your previous file to {backup}")
|
|
350
|
+
lines += prune_rules(home / "rules", set(rule_files(ov.root)))
|
|
351
|
+
lines += install_skills(skill_sources(core, ov.root), paths.agents_skills())
|
|
352
|
+
lines += git_gate(ov)
|
|
353
|
+
override = home / "AGENTS.override.md"
|
|
354
|
+
if override.is_file() and override.stat().st_size > 0:
|
|
355
|
+
lines.append(f"warning: {override} exists and Codex loads it instead of {home / 'AGENTS.md'}")
|
|
356
|
+
return lines
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def missing_requirements() -> list[str]:
|
|
360
|
+
lines = []
|
|
361
|
+
if sys.version_info < (3, 11):
|
|
362
|
+
lines.append("Python 3.11 or newer")
|
|
363
|
+
if not shutil.which("git"):
|
|
364
|
+
lines.append("git (Fedora: sudo dnf install git; Debian/Ubuntu: sudo apt install git; macOS: xcode-select --install; Windows: winget install --id Git.Git -e)")
|
|
365
|
+
return lines
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def cli(args) -> int:
|
|
369
|
+
missing = missing_requirements()
|
|
370
|
+
for line in missing:
|
|
371
|
+
print(f"missing requirement: {line}", file=sys.stderr)
|
|
372
|
+
if missing:
|
|
373
|
+
return 2
|
|
374
|
+
try:
|
|
375
|
+
lines = run(paths.CORE, load(paths.overlay_dir(args.overlay)), paths.codex_home(args.codex_home))
|
|
376
|
+
except ValueError as e:
|
|
377
|
+
print(f"code-jig: {e}", file=sys.stderr)
|
|
378
|
+
return 2
|
|
379
|
+
for line in lines:
|
|
380
|
+
print(line)
|
|
381
|
+
return 0
|
codejig/managed.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import tomllib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from codejig.tiers import Tier
|
|
7
|
+
|
|
8
|
+
Check = tuple[str, str, str]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def requirements_path() -> Path:
|
|
12
|
+
if sys.platform == "win32":
|
|
13
|
+
return Path(os.environ.get("ProgramData", "C:\\ProgramData")) / "OpenAI" / "Codex" / "requirements.toml"
|
|
14
|
+
return Path("/etc/codex/requirements.toml")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read_here(path: Path, name: str) -> tuple[dict, list[Check]]:
|
|
18
|
+
if not path.is_file():
|
|
19
|
+
return {}, [("ok", name, f"none at {path}; cloud-delivered requirements are not visible to doctor")]
|
|
20
|
+
try:
|
|
21
|
+
here = tomllib.loads(path.read_text(encoding="utf-8-sig"))
|
|
22
|
+
except (OSError, ValueError) as e:
|
|
23
|
+
return {}, [("FAIL", name, f"{path}: {e}")]
|
|
24
|
+
return here, [("ok", name, f"read {path}; cloud-delivered requirements are not visible to doctor")]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _policy_checks(here: dict, ts: list[Tier]) -> list[Check]:
|
|
28
|
+
checks: list[Check] = []
|
|
29
|
+
reviewers = here.get("allowed_approvals_reviewers")
|
|
30
|
+
if isinstance(reviewers, list) and "auto_review" not in reviewers:
|
|
31
|
+
checks.append(("FAIL", "guardian allowed", f"allowed_approvals_reviewers = {reviewers}; the tier profiles need auto_review"))
|
|
32
|
+
auto_review = here.get("auto_review") if isinstance(here.get("auto_review"), dict) else {}
|
|
33
|
+
ignored = auto_review.get("ignore_rules") if isinstance(auto_review.get("ignore_rules"), list) else []
|
|
34
|
+
hit = sorted({t.model for t in ts} & {m for m in ignored if isinstance(m, str)})
|
|
35
|
+
if hit:
|
|
36
|
+
checks.append(("warn", "overlay rules", f"auto_review.ignore_rules lists {', '.join(hit)}; allow rules do nothing for those models"))
|
|
37
|
+
if here.get("allow_managed_hooks_only") is True:
|
|
38
|
+
checks.append(("warn", "hooks", "allow_managed_hooks_only = true; the pack's hooks will not run"))
|
|
39
|
+
if here.get("guardian_extra_policy"):
|
|
40
|
+
checks.append(("warn", "guardian policy", "managed guardian_extra_policy replaces the overlay guardian-policy.md"))
|
|
41
|
+
return checks
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _bench_diff(here: dict, bench: Path) -> Check:
|
|
45
|
+
try:
|
|
46
|
+
sim = tomllib.loads(bench.read_text(encoding="utf-8"))
|
|
47
|
+
except (OSError, ValueError) as e:
|
|
48
|
+
return ("warn", "bench diff", f"{bench}: {e}")
|
|
49
|
+
parts = []
|
|
50
|
+
if only_here := sorted(set(here) - set(sim)):
|
|
51
|
+
parts.append(f"locked here, not simulated: {', '.join(only_here)}")
|
|
52
|
+
if only_bench := sorted(set(sim) - set(here)):
|
|
53
|
+
parts.append(f"simulated, not locked here: {', '.join(only_bench)}")
|
|
54
|
+
if changed := sorted(k for k in set(here) & set(sim) if here[k] != sim[k]):
|
|
55
|
+
parts.append(f"different values: {', '.join(changed)}")
|
|
56
|
+
return ("warn", "bench diff", "; ".join(parts)) if parts else ("ok", "bench diff", "matches bench/requirements.toml")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def check_requirements(path: Path, bench: Path, ts: list[Tier]) -> list[Check]:
|
|
60
|
+
name = "managed requirements"
|
|
61
|
+
here, checks = _read_here(path, name)
|
|
62
|
+
if checks[0][0] == "FAIL":
|
|
63
|
+
return checks
|
|
64
|
+
return checks + _policy_checks(here, ts) + [_bench_diff(here, bench)]
|