@theglitchking/babel-fish 2.4.2 → 2.5.0
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.
- package/.claude/install.sh +42 -9
- package/.claude/project-map/context-check.py +156 -0
- package/.claude/project-map/test_context_check.py +221 -0
- package/.claude/project-map/test_generate.py +11 -2
- package/.claude/project-map/test_mine_sessions.py +4 -1
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.githooks/install.sh +0 -0
- package/.githooks/pre-commit +17 -0
- package/CHANGELOG.md +70 -0
- package/README.md +19 -2
- package/checksums.json +1 -1
- package/package.json +4 -2
package/.claude/install.sh
CHANGED
|
@@ -98,6 +98,10 @@ POST-INSTALL COMMANDS
|
|
|
98
98
|
python .claude/project-map/mine-sessions.py --verbose
|
|
99
99
|
python .claude/project-map/mine-sessions.py --dry-run
|
|
100
100
|
|
|
101
|
+
Check auto-loaded context (CLAUDE.md + .claude/rules/) — budget, pointers:
|
|
102
|
+
python .claude/project-map/context-check.py
|
|
103
|
+
python .claude/project-map/context-check.py --budget 80000 --map-style
|
|
104
|
+
|
|
101
105
|
Re-install git hooks (if you cloned a fresh copy):
|
|
102
106
|
bash .githooks/install.sh
|
|
103
107
|
|
|
@@ -121,7 +125,7 @@ KEY FILES AFTER INSTALL
|
|
|
121
125
|
.claude/rules/project-vocabulary.md — Auto-loaded every session
|
|
122
126
|
.claude/rules/operational-runbook.md — Edit manually to grow over time
|
|
123
127
|
.claude/skills/<slug>-developer-skill/ — Your developer skill
|
|
124
|
-
.githooks/pre-commit —
|
|
128
|
+
.githooks/pre-commit — Regenerates map; checks auto-loaded context
|
|
125
129
|
|
|
126
130
|
GRADING CATEGORIES (90% to pass)
|
|
127
131
|
Section completeness 25% — All 19 sections generated
|
|
@@ -378,17 +382,45 @@ if echo "$STAGED_FILES" | grep -qE "$EXTENSIONS_PATTERN" 2>/dev/null; then
|
|
|
378
382
|
fi
|
|
379
383
|
# ── End Codebase Mapper ──────────────────────────────────────────────────────'
|
|
380
384
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
# Own marker, checked separately: hooks installed before #20 already carry
|
|
386
|
+
# the Codebase Mapper block, and re-running the installer must still add this.
|
|
387
|
+
local check_snippet
|
|
388
|
+
check_snippet=$(cat <<'SNIPPET'
|
|
389
|
+
# ── Context Check: auto-loaded instructions stay in budget, pointers resolve ──
|
|
390
|
+
if git diff --cached --name-only 2>/dev/null | grep -qE '^(\.claude/)?CLAUDE(\.local)?\.md$|^\.claude/rules/.*\.md$'; then
|
|
391
|
+
CHECK_SCRIPT=".claude/project-map/context-check.py"
|
|
392
|
+
if [ -f "$CHECK_SCRIPT" ]; then
|
|
393
|
+
PYTHON=""
|
|
394
|
+
if [ -f ".venv/bin/python3" ]; then PYTHON=".venv/bin/python3"
|
|
395
|
+
elif command -v python3 &>/dev/null; then PYTHON="python3"
|
|
396
|
+
elif command -v python &>/dev/null; then PYTHON="python"
|
|
387
397
|
fi
|
|
388
|
-
|
|
389
|
-
|
|
398
|
+
if [ -n "$PYTHON" ] && ! $PYTHON "$CHECK_SCRIPT"; then
|
|
399
|
+
echo "[context-check] Commit blocked. Fix the FAIL lines above, or add flags (--budget N, --map-style) to this call in .githooks/pre-commit." >&2
|
|
400
|
+
exit 1
|
|
401
|
+
fi
|
|
402
|
+
fi
|
|
403
|
+
fi
|
|
404
|
+
# ── End Context Check ────────────────────────────────────────────────────────
|
|
405
|
+
SNIPPET
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
if [ ! -f "$pre_commit" ]; then
|
|
409
|
+
printf '#!/bin/bash\n' > "$pre_commit"
|
|
390
410
|
ok "Created pre-commit hook"
|
|
391
411
|
fi
|
|
412
|
+
if ! grep -q 'Codebase Mapper' "$pre_commit" 2>/dev/null; then
|
|
413
|
+
{ echo ""; echo "$hook_snippet"; } >> "$pre_commit"
|
|
414
|
+
ok "Added Codebase Mapper to pre-commit hook"
|
|
415
|
+
else
|
|
416
|
+
ok "pre-commit hook already contains Codebase Mapper snippet"
|
|
417
|
+
fi
|
|
418
|
+
if ! grep -q 'Context Check' "$pre_commit" 2>/dev/null; then
|
|
419
|
+
{ echo ""; echo "$check_snippet"; } >> "$pre_commit"
|
|
420
|
+
ok "Added Context Check to pre-commit hook"
|
|
421
|
+
else
|
|
422
|
+
ok "pre-commit hook already contains Context Check snippet"
|
|
423
|
+
fi
|
|
392
424
|
|
|
393
425
|
chmod +x "$pre_commit"
|
|
394
426
|
|
|
@@ -512,6 +544,7 @@ if [ "$PLUGIN_SOURCE_DIR" != "$CLAUDE_DIR" ]; then
|
|
|
512
544
|
cp -r "$PLUGIN_SOURCE_DIR/project-map/generate.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
|
|
513
545
|
cp -r "$PLUGIN_SOURCE_DIR/project-map/grader.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
|
|
514
546
|
cp -r "$PLUGIN_SOURCE_DIR/project-map/mine-sessions.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
|
|
547
|
+
cp -r "$PLUGIN_SOURCE_DIR/project-map/context-check.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
|
|
515
548
|
fi
|
|
516
549
|
|
|
517
550
|
# ── Step 1: Ensure Python ─────────────────────────────────────────────────────
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
context-check.py — Babel Fish
|
|
4
|
+
Keeps the files Claude Code auto-loads at session start small and pointing at
|
|
5
|
+
things that exist (#20).
|
|
6
|
+
|
|
7
|
+
`CLAUDE.md` and every `.claude/rules/*.md` load into every session. Past the
|
|
8
|
+
harness limit (150k chars) rules stop binding and nothing reports it; before
|
|
9
|
+
that, growth is one paragraph at a time and nobody sees the total. Three checks:
|
|
10
|
+
|
|
11
|
+
1. BUDGET — total chars under --budget (default 60k, well under the limit).
|
|
12
|
+
2. POINTERS — every `→ `path``, backticked `.documentation/…md` path and
|
|
13
|
+
relative markdown link names a file that exists. hewtd link-checks
|
|
14
|
+
only INSIDE `.documentation/`, so archiving or renaming a doc a
|
|
15
|
+
rule points at breaks the pointer silently.
|
|
16
|
+
3. RULE LINES (--map-style only) — in a rules file every bullet is
|
|
17
|
+
`- ALWAYS|NEVER <rule> — <why> → `doc``; in CLAUDE.md every
|
|
18
|
+
bullet with a pointer is. Opt-in: it is one repo's convention, and
|
|
19
|
+
this runs from the pre-commit hook of every babel-fish install.
|
|
20
|
+
|
|
21
|
+
Deliberately NOT part of generate.py: the hook runs generate.py only when code
|
|
22
|
+
is staged, `.claude/` is outside the watch set, and the hook discards its
|
|
23
|
+
errors — a failure there could never block a commit.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
python context-check.py [--budget N] [--map-style] [--descriptive a.md,b.md]
|
|
27
|
+
[--warn-only] [--project-root PATH]
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import re
|
|
33
|
+
import sys
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
|
37
|
+
|
|
38
|
+
HARNESS_LIMIT = 150_000
|
|
39
|
+
DEFAULT_BUDGET = 60_000
|
|
40
|
+
# Maps whose lines describe (tools, traps, vocabulary) rather than rule — exempt
|
|
41
|
+
# from the ALWAYS/NEVER form. project-vocabulary.md is babel-fish's own output.
|
|
42
|
+
DEFAULT_DESCRIPTIVE = {"operational-runbook.md", "project-vocabulary.md", "tool-registry.md"}
|
|
43
|
+
|
|
44
|
+
ARROW_RE = re.compile(r"→ `([^`\s*]+?\.md)(?:#[^`]*)?`")
|
|
45
|
+
DOC_PATH_RE = re.compile(r"`(\.documentation/[^`\s*]+?\.md)(?:#[^`]*)?`")
|
|
46
|
+
MD_LINK_RE = re.compile(r"\]\(([^)\s]+)\)")
|
|
47
|
+
FENCE_RE = re.compile(r"^```.*?^```", re.MULTILINE | re.DOTALL)
|
|
48
|
+
RULE_RE = re.compile(r"- (ALWAYS|NEVER)\b")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def configure_paths(project_root: Path) -> None:
|
|
52
|
+
"""Read root. Nothing here writes, but match the sibling scripts (#18)."""
|
|
53
|
+
global PROJECT_ROOT
|
|
54
|
+
PROJECT_ROOT = project_root
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def loaded() -> dict[str, str]:
|
|
58
|
+
"""-> {relpath: text} for every file Claude Code loads at session start."""
|
|
59
|
+
paths = [PROJECT_ROOT / n for n in ("CLAUDE.md", "CLAUDE.local.md", ".claude/CLAUDE.md")]
|
|
60
|
+
# ponytail: rules with `paths:` frontmatter load lazily but are counted anyway —
|
|
61
|
+
# over-counting errs toward the budget, not past the limit.
|
|
62
|
+
paths += sorted((PROJECT_ROOT / ".claude" / "rules").rglob("*.md"))
|
|
63
|
+
return {p.relative_to(PROJECT_ROOT).as_posix(): p.read_text(encoding="utf-8", errors="replace")
|
|
64
|
+
for p in paths if p.is_file()}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def bullets(text: str) -> list[str]:
|
|
68
|
+
return [ln for ln in text.split("\n") if ln.startswith("- ")]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def bad_rule_lines(text: str) -> list[str]:
|
|
72
|
+
"""-> bullets that are not `- ALWAYS|NEVER … → `path``."""
|
|
73
|
+
return [ln for ln in bullets(text) if not RULE_RE.match(ln) or not ARROW_RE.search(ln)]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def unresolved(text: str, file_dir: Path) -> list[str]:
|
|
77
|
+
"""-> every pointer in `text` naming a path that does not exist.
|
|
78
|
+
|
|
79
|
+
`→` pointers and backticked `.documentation/` paths are root-relative (that is
|
|
80
|
+
how the map convention writes them); markdown links resolve relative to the file or
|
|
81
|
+
to the root. Code fences are skipped — examples are not pointers.
|
|
82
|
+
"""
|
|
83
|
+
text = FENCE_RE.sub("", text)
|
|
84
|
+
missing = [p for p in set(ARROW_RE.findall(text)) | set(DOC_PATH_RE.findall(text))
|
|
85
|
+
if not (PROJECT_ROOT / p).exists()]
|
|
86
|
+
for link in MD_LINK_RE.findall(text):
|
|
87
|
+
target = link.split("#", 1)[0]
|
|
88
|
+
if not target or re.match(r"[a-z][a-z0-9+.-]*:", target, re.I): # anchor-only, http:, mailto:
|
|
89
|
+
continue
|
|
90
|
+
target = target.lstrip("/")
|
|
91
|
+
# File-relative is markdown; root-relative is how Claude reads a path, and
|
|
92
|
+
# what the installer's own .claude/CLAUDE.md pointer uses. Either counts.
|
|
93
|
+
if not ((file_dir / target).exists() or (PROJECT_ROOT / target).exists()):
|
|
94
|
+
missing.append(link)
|
|
95
|
+
return sorted(set(missing))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run(budget: int, map_style: bool, descriptive: set[str]) -> int:
|
|
99
|
+
"""Print the report; return the number of failed checks."""
|
|
100
|
+
fails = 0
|
|
101
|
+
|
|
102
|
+
def check(name: str, ok: bool, detail: str = "") -> None:
|
|
103
|
+
nonlocal fails
|
|
104
|
+
print(f" {'PASS' if ok else 'FAIL'} {name}" + (f" -- {detail}" if detail and not ok else ""))
|
|
105
|
+
fails += not ok
|
|
106
|
+
|
|
107
|
+
files = loaded()
|
|
108
|
+
total = sum(len(t) for t in files.values())
|
|
109
|
+
|
|
110
|
+
print("\nthe budget")
|
|
111
|
+
check(f"auto-loaded context is under {budget:,} chars", total <= budget,
|
|
112
|
+
f"{total:,} -- move detail into .documentation/ and leave one line pointing at it")
|
|
113
|
+
for k, t in sorted(files.items(), key=lambda kv: -len(kv[1])):
|
|
114
|
+
print(f" {len(t):>7,} {k}")
|
|
115
|
+
print(f" {total:>7,} total ({total / HARNESS_LIMIT:.0%} of the {HARNESS_LIMIT:,} harness limit)")
|
|
116
|
+
|
|
117
|
+
print("\nevery pointer resolves")
|
|
118
|
+
for k, t in files.items():
|
|
119
|
+
miss = unresolved(t, (PROJECT_ROOT / k).parent)
|
|
120
|
+
check(f"{k}: all pointers exist", not miss, ", ".join(miss[:3]))
|
|
121
|
+
|
|
122
|
+
if map_style:
|
|
123
|
+
print("\nrule lines are ALWAYS/NEVER, with a pointer")
|
|
124
|
+
for k, t in files.items():
|
|
125
|
+
if Path(k).name in descriptive:
|
|
126
|
+
continue
|
|
127
|
+
if k.startswith(".claude/rules/"):
|
|
128
|
+
bad = bad_rule_lines(t)
|
|
129
|
+
check(f"{k}: every bullet is ALWAYS|NEVER → doc", not bad, bad[0][:80] if bad else "")
|
|
130
|
+
else: # CLAUDE.md: tool lines may go unpointed; a pointed bullet is a rule
|
|
131
|
+
bad = [ln for ln in bad_rule_lines(t) if "→" in ln]
|
|
132
|
+
check(f"{k}: every pointed bullet is ALWAYS|NEVER", not bad, bad[0][:80] if bad else "")
|
|
133
|
+
|
|
134
|
+
print(f"\n{'OK' if not fails else f'{fails} FAILED'}")
|
|
135
|
+
return fails
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main() -> None:
|
|
139
|
+
ap = argparse.ArgumentParser(description="Check the context Claude Code auto-loads (#20).")
|
|
140
|
+
ap.add_argument("--budget", type=int, default=DEFAULT_BUDGET,
|
|
141
|
+
help=f"max total chars (default {DEFAULT_BUDGET:,}; harness limit {HARNESS_LIMIT:,})")
|
|
142
|
+
ap.add_argument("--map-style", action="store_true",
|
|
143
|
+
help="also require rule bullets to be `- ALWAYS|NEVER … → `doc``")
|
|
144
|
+
ap.add_argument("--descriptive", default=",".join(sorted(DEFAULT_DESCRIPTIVE)),
|
|
145
|
+
help="comma-separated filenames exempt from --map-style (default: %(default)s)")
|
|
146
|
+
ap.add_argument("--warn-only", action="store_true", help="report, but always exit 0")
|
|
147
|
+
ap.add_argument("--project-root", type=Path, default=None)
|
|
148
|
+
args = ap.parse_args()
|
|
149
|
+
if args.project_root:
|
|
150
|
+
configure_paths(args.project_root.resolve())
|
|
151
|
+
fails = run(args.budget, args.map_style, {n.strip() for n in args.descriptive.split(",") if n.strip()})
|
|
152
|
+
sys.exit(1 if fails and not args.warn_only else 0)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
main()
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for context-check.py — run: python3 .claude/project-map/test_context_check.py
|
|
3
|
+
|
|
4
|
+
Stdlib assert + __main__, same shape as the sibling suites. Every check is proven
|
|
5
|
+
to DETECT something (a seeded bad input) as well as to pass a good one: a check
|
|
6
|
+
that only ever prints PASS is indistinguishable from one that checks nothing.
|
|
7
|
+
|
|
8
|
+
Every fixture is a temp project passed via --project-root, so nothing here reads
|
|
9
|
+
or writes the real repo (#18).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import inspect
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
HERE = Path(__file__).parent
|
|
21
|
+
SCRIPT = HERE / "context-check.py"
|
|
22
|
+
REPO = HERE.parent.parent
|
|
23
|
+
|
|
24
|
+
GOOD_RULES = "# Rules\n\n- NEVER do the bad thing — it breaks. → `.documentation/a.md`\n"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_project(root: Path, claude_md: str = "# Project\n", rules: dict[str, str] | None = None) -> Path:
|
|
28
|
+
(root / ".documentation").mkdir(parents=True, exist_ok=True)
|
|
29
|
+
(root / ".documentation/a.md").write_text("# A\n")
|
|
30
|
+
(root / "CLAUDE.md").write_text(claude_md)
|
|
31
|
+
(root / ".claude/rules").mkdir(parents=True, exist_ok=True)
|
|
32
|
+
for name, text in (rules or {}).items():
|
|
33
|
+
(root / ".claude/rules" / name).write_text(text)
|
|
34
|
+
return root
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check(root: Path, *flags: str) -> subprocess.CompletedProcess:
|
|
38
|
+
return subprocess.run([sys.executable, str(SCRIPT), "--project-root", str(root), *flags],
|
|
39
|
+
capture_output=True, text=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── budget ───────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
def test_under_budget_passes(tmp):
|
|
45
|
+
r = check(make_project(tmp, rules={"r.md": GOOD_RULES}))
|
|
46
|
+
assert r.returncode == 0, r.stdout
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_over_budget_fails_and_names_the_file(tmp):
|
|
50
|
+
make_project(tmp, rules={"huge.md": "x" * 2_000})
|
|
51
|
+
r = check(tmp, "--budget", "1000")
|
|
52
|
+
assert r.returncode == 1, r.stdout
|
|
53
|
+
assert "FAIL auto-loaded context is under 1,000" in r.stdout
|
|
54
|
+
assert ".claude/rules/huge.md" in r.stdout
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_budget_counts_nested_rules_and_dot_claude_claude_md(tmp):
|
|
58
|
+
make_project(tmp)
|
|
59
|
+
(tmp / ".claude/rules/sub").mkdir()
|
|
60
|
+
(tmp / ".claude/rules/sub/deep.md").write_text("y" * 600)
|
|
61
|
+
(tmp / ".claude/CLAUDE.md").write_text("z" * 600)
|
|
62
|
+
r = check(tmp, "--budget", "1000")
|
|
63
|
+
assert r.returncode == 1, r.stdout
|
|
64
|
+
assert ".claude/rules/sub/deep.md" in r.stdout and ".claude/CLAUDE.md" in r.stdout
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_warn_only_exits_zero(tmp):
|
|
68
|
+
r = check(make_project(tmp, rules={"huge.md": "x" * 2_000}), "--budget", "1000", "--warn-only")
|
|
69
|
+
assert r.returncode == 0 and "FAIL" in r.stdout, r.stdout
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── pointers ─────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def test_dangling_arrow_pointer_fails(tmp):
|
|
75
|
+
r = check(make_project(tmp, rules={"r.md": "- NEVER x — y. → `.documentation/gone.md`\n"}))
|
|
76
|
+
assert r.returncode == 1 and ".documentation/gone.md" in r.stdout, r.stdout
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_dangling_backticked_doc_path_fails(tmp):
|
|
80
|
+
r = check(make_project(tmp, claude_md="See `.documentation/missing/doc.md` for more.\n"))
|
|
81
|
+
assert r.returncode == 1 and "missing/doc.md" in r.stdout, r.stdout
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_dangling_markdown_link_fails_relative_to_the_file(tmp):
|
|
85
|
+
# This repo's runbook links docs this way — the fork's regexes never saw them.
|
|
86
|
+
make_project(tmp, rules={"r.md": "[doc](../../.documentation/a.md) and [bad](../../.documentation/nope.md)\n"})
|
|
87
|
+
r = check(tmp)
|
|
88
|
+
assert r.returncode == 1, r.stdout
|
|
89
|
+
assert "nope.md" in r.stdout and "a.md" not in r.stdout.split("--")[-1], r.stdout
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_root_relative_link_in_dot_claude_is_accepted(tmp):
|
|
93
|
+
# The installer writes exactly this into .claude/CLAUDE.md. Strictly file-relative,
|
|
94
|
+
# it blocked the first commit of every fresh install.
|
|
95
|
+
make_project(tmp)
|
|
96
|
+
(tmp / ".claude/project-map").mkdir()
|
|
97
|
+
(tmp / ".claude/project-map/PROJECT_MAP.md").write_text("# Map\n")
|
|
98
|
+
(tmp / ".claude/CLAUDE.md").write_text("[map](.claude/project-map/PROJECT_MAP.md)\n")
|
|
99
|
+
r = check(tmp)
|
|
100
|
+
assert r.returncode == 0, r.stdout
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_anchor_url_and_code_fence_are_not_false_alarms(tmp):
|
|
104
|
+
make_project(tmp, claude_md=(
|
|
105
|
+
"→ `.documentation/a.md#section`\n"
|
|
106
|
+
"[site](https://example.com/x.md) [mail](mailto:a@b.c) [here](#top)\n"
|
|
107
|
+
"```md\n→ `.documentation/example-only.md`\n[x](nowhere.md)\n```\n"
|
|
108
|
+
))
|
|
109
|
+
r = check(tmp)
|
|
110
|
+
assert r.returncode == 0, r.stdout
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── rule lines (--map-style) ─────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def test_rule_lines_ignored_without_map_style(tmp):
|
|
116
|
+
r = check(make_project(tmp, rules={"r.md": "- just some context\n"}))
|
|
117
|
+
assert r.returncode == 0 and "ALWAYS/NEVER" not in r.stdout, r.stdout
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_context_line_in_rules_file_fails(tmp):
|
|
121
|
+
r = check(make_project(tmp, rules={"r.md": "- run the gates first → `.documentation/a.md`\n"}), "--map-style")
|
|
122
|
+
assert r.returncode == 1 and "run the gates first" in r.stdout, r.stdout
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_rule_without_pointer_fails(tmp):
|
|
126
|
+
r = check(make_project(tmp, rules={"r.md": "- NEVER do it — because.\n"}), "--map-style")
|
|
127
|
+
assert r.returncode == 1, r.stdout
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_good_rule_line_passes(tmp):
|
|
131
|
+
r = check(make_project(tmp, rules={"r.md": GOOD_RULES}), "--map-style")
|
|
132
|
+
assert r.returncode == 0, r.stdout
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_descriptive_files_are_exempt(tmp):
|
|
136
|
+
make_project(tmp, rules={"operational-runbook.md": "- a trap, described\n"})
|
|
137
|
+
assert check(tmp, "--map-style").returncode == 0
|
|
138
|
+
r = check(tmp, "--map-style", "--descriptive", "other.md")
|
|
139
|
+
assert r.returncode == 1, r.stdout
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_claude_md_only_pointed_bullets_must_be_rules(tmp):
|
|
143
|
+
make_project(tmp, claude_md="- `run it` with docker\n")
|
|
144
|
+
assert check(tmp, "--map-style").returncode == 0
|
|
145
|
+
(tmp / "CLAUDE.md").write_text("- a tool line → `.documentation/a.md`\n")
|
|
146
|
+
assert check(tmp, "--map-style").returncode == 1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── wiring ───────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
def test_this_repo_passes():
|
|
152
|
+
r = subprocess.run([sys.executable, str(SCRIPT)], capture_output=True, text=True, cwd=REPO)
|
|
153
|
+
assert r.returncode == 0, r.stdout
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _block(text: str) -> str:
|
|
157
|
+
start = text.index("# ── Context Check")
|
|
158
|
+
end = text.index("# ── End Context Check")
|
|
159
|
+
return text[start:end]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_installer_writes_the_same_hook_block():
|
|
163
|
+
assert _block((REPO / ".githooks/pre-commit").read_text()) == _block((REPO / ".claude/install.sh").read_text())
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_hooks_are_committed_executable():
|
|
167
|
+
# Git silently skips a non-executable hook. Until #20 both were committed 100644,
|
|
168
|
+
# so no clone of this repo ever ran its pre-commit hook.
|
|
169
|
+
out = subprocess.run(["git", "ls-files", "-s", ".githooks/"], capture_output=True, text=True,
|
|
170
|
+
cwd=REPO, check=True).stdout
|
|
171
|
+
modes = {line.split("\t")[1]: line.split()[0] for line in out.splitlines()}
|
|
172
|
+
assert modes and all(m == "100755" for m in modes.values()), modes
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_hook_blocks_a_bad_rules_commit(tmp):
|
|
176
|
+
git = ["git", "-c", "user.name=t", "-c", "user.email=t@t", "-c", "core.hooksPath=.githooks"]
|
|
177
|
+
subprocess.run(["git", "init", "-q", str(tmp)], check=True)
|
|
178
|
+
make_project(tmp, rules={"r.md": GOOD_RULES})
|
|
179
|
+
(tmp / ".githooks").mkdir()
|
|
180
|
+
shutil.copy(REPO / ".githooks/pre-commit", tmp / ".githooks/pre-commit")
|
|
181
|
+
(tmp / ".githooks/pre-commit").chmod(0o755) # the mode itself is test_hooks_are_committed_executable's job
|
|
182
|
+
(tmp / ".claude/project-map").mkdir(parents=True)
|
|
183
|
+
shutil.copy(SCRIPT, tmp / ".claude/project-map/context-check.py")
|
|
184
|
+
commit = lambda msg: subprocess.run(git + ["commit", "-qm", msg], cwd=tmp, capture_output=True, text=True)
|
|
185
|
+
|
|
186
|
+
subprocess.run(["git", "add", "-A"], cwd=tmp, check=True)
|
|
187
|
+
r = commit("good")
|
|
188
|
+
assert r.returncode == 0, r.stdout + r.stderr
|
|
189
|
+
|
|
190
|
+
(tmp / ".claude/rules/r.md").write_text("- NEVER x — y. → `.documentation/gone.md`\n")
|
|
191
|
+
subprocess.run(["git", "add", "-A"], cwd=tmp, check=True)
|
|
192
|
+
r = commit("bad")
|
|
193
|
+
assert r.returncode != 0, "hook let a dangling pointer through"
|
|
194
|
+
assert "Commit blocked" in r.stderr, r.stderr
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def main() -> int:
|
|
198
|
+
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
|
199
|
+
failed = []
|
|
200
|
+
base = Path(tempfile.mkdtemp(prefix="context-check-test-"))
|
|
201
|
+
try:
|
|
202
|
+
for fn in tests:
|
|
203
|
+
d = base / fn.__name__
|
|
204
|
+
d.mkdir(parents=True)
|
|
205
|
+
try:
|
|
206
|
+
if "tmp" in inspect.signature(fn).parameters:
|
|
207
|
+
fn(d)
|
|
208
|
+
else:
|
|
209
|
+
fn()
|
|
210
|
+
print(f" ok {fn.__name__}")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
failed.append(fn.__name__)
|
|
213
|
+
print(f" FAIL {fn.__name__}: {type(e).__name__}: {e}")
|
|
214
|
+
finally:
|
|
215
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
216
|
+
print(f"\n{len(tests) - len(failed)}/{len(tests)} passed")
|
|
217
|
+
return 1 if failed else 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
sys.exit(main())
|
|
@@ -30,7 +30,12 @@ def load_generate(project_root: Path):
|
|
|
30
30
|
spec.loader.exec_module(mod)
|
|
31
31
|
finally:
|
|
32
32
|
sys.argv = argv
|
|
33
|
-
mod.PROJECT_ROOT =
|
|
33
|
+
# configure_paths(), not `mod.PROJECT_ROOT = ...`: generate.py binds MAP_DIR,
|
|
34
|
+
# SECTIONS_DIR, CHECKSUMS, LEARNED_VOC and GLOSSARY at import time from
|
|
35
|
+
# __file__, and rebinds them only here. Setting PROJECT_ROOT alone left every
|
|
36
|
+
# output path aimed at the real repo, so the suite overwrote its own
|
|
37
|
+
# glossary.json with fixture data and still passed (#18).
|
|
38
|
+
mod.configure_paths(project_root)
|
|
34
39
|
return mod
|
|
35
40
|
|
|
36
41
|
|
|
@@ -232,7 +237,11 @@ def test_glossary_survives_foreign_project_root(g):
|
|
|
232
237
|
PROJECT_ROOT when --project-root points elsewhere. That combination raised
|
|
233
238
|
ValueError from relative_to()."""
|
|
234
239
|
import json
|
|
235
|
-
|
|
240
|
+
# Recreate the mismatch explicitly. It used to arrive for free because the
|
|
241
|
+
# loader left SECTIONS_DIR pointing at the script dir — the same leak that
|
|
242
|
+
# let this test write into the real repo (#18).
|
|
243
|
+
g.SECTIONS_DIR = HERE / "sections"
|
|
244
|
+
g.write_glossary([], g.load_stack())
|
|
236
245
|
data = json.loads(g.GLOSSARY.read_text())
|
|
237
246
|
assert data["source"].endswith("01-vocabulary.md"), data["source"]
|
|
238
247
|
|
|
@@ -28,7 +28,10 @@ def load_miner(project_root: Path, cursor_dir: Path):
|
|
|
28
28
|
spec.loader.exec_module(mod)
|
|
29
29
|
finally:
|
|
30
30
|
sys.argv = argv
|
|
31
|
-
mod.PROJECT_ROOT =
|
|
31
|
+
# configure_paths(), not `mod.PROJECT_ROOT = ...` — see the same note in
|
|
32
|
+
# test_generate.py (#18). LEARNED_VOC is bound at import time from __file__,
|
|
33
|
+
# so assigning PROJECT_ROOT alone leaves it aimed at the real repo.
|
|
34
|
+
mod.configure_paths(project_root)
|
|
32
35
|
mod.MINE_CURSOR = cursor_dir / ".mine-cursor.json"
|
|
33
36
|
return mod
|
|
34
37
|
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Official marketplace for babel-fish - Codebase introspection and vocabulary translation for AI coding assistants",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.5.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "babel-fish",
|
|
14
14
|
"description": "Auto-generates a project map, vocabulary translation layer, and developer skill for any codebase. Introspects routes, models, services, features, infrastructure, and session history to give Claude instant full-stack context. Self-updates via pre-commit hook.",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.5.0",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TheGlitchKing"
|
|
18
18
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babel-fish",
|
|
3
3
|
"description": "Auto-generates a project map, vocabulary translation layer, and developer skill for any codebase. Introspects routes, models, services, features, infrastructure, and session history to give Claude instant full-stack context. Self-updates via pre-commit hook.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TheGlitchKing",
|
|
7
7
|
"email": "theglitchking@users.noreply.github.com"
|
package/.githooks/install.sh
CHANGED
|
File without changes
|
package/.githooks/pre-commit
CHANGED
|
@@ -21,3 +21,20 @@ if echo "$STAGED_FILES" | grep -qE "$EXTENSIONS_PATTERN" 2>/dev/null; then
|
|
|
21
21
|
fi
|
|
22
22
|
fi
|
|
23
23
|
# ── End Codebase Mapper ──────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
# ── Context Check: auto-loaded instructions stay in budget, pointers resolve ──
|
|
26
|
+
if git diff --cached --name-only 2>/dev/null | grep -qE '^(\.claude/)?CLAUDE(\.local)?\.md$|^\.claude/rules/.*\.md$'; then
|
|
27
|
+
CHECK_SCRIPT=".claude/project-map/context-check.py"
|
|
28
|
+
if [ -f "$CHECK_SCRIPT" ]; then
|
|
29
|
+
PYTHON=""
|
|
30
|
+
if [ -f ".venv/bin/python3" ]; then PYTHON=".venv/bin/python3"
|
|
31
|
+
elif command -v python3 &>/dev/null; then PYTHON="python3"
|
|
32
|
+
elif command -v python &>/dev/null; then PYTHON="python"
|
|
33
|
+
fi
|
|
34
|
+
if [ -n "$PYTHON" ] && ! $PYTHON "$CHECK_SCRIPT"; then
|
|
35
|
+
echo "[context-check] Commit blocked. Fix the FAIL lines above, or add flags (--budget N, --map-style) to this call in .githooks/pre-commit." >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
# ── End Context Check ────────────────────────────────────────────────────────
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.5.0] - 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`context-check.py` — keeps auto-loaded context small and its pointers live**
|
|
10
|
+
([#20](https://github.com/TheGlitchKing/babel-fish/issues/20)). `CLAUDE.md` and
|
|
11
|
+
every `.claude/rules/*.md` load into every session; past Claude Code's 150k-char
|
|
12
|
+
limit rules stop binding, and nothing reports it. glitch-stock-trading-rig had
|
|
13
|
+
reached 225.6k before forking `generate.py` to enforce a budget.
|
|
14
|
+
|
|
15
|
+
The new script checks three things. **Budget:** total chars under `--budget`
|
|
16
|
+
(default 60,000). **Pointers:** every `` → `path` ``, backticked `.documentation/`
|
|
17
|
+
path and relative markdown link resolves. hewtd link-checks only inside
|
|
18
|
+
`.documentation/`, so a renamed doc used to break a rule's pointer silently.
|
|
19
|
+
**Rule lines** (opt-in, `--map-style`): each rule bullet reads
|
|
20
|
+
`` - ALWAYS|NEVER <rule> — <why> → `doc` ``. That form is one repo's convention,
|
|
21
|
+
and the hook ships to every install, so it is off by default.
|
|
22
|
+
|
|
23
|
+
The pre-commit hook runs it when `CLAUDE.md` or `.claude/rules/` is staged, and a
|
|
24
|
+
failure blocks the commit. It is a separate script rather than part of
|
|
25
|
+
`generate.py`: the hook runs `generate.py` only for code changes, `.claude/` is
|
|
26
|
+
outside the watch set, and the hook discards `generate.py`'s errors. The block
|
|
27
|
+
has its own marker, so re-running `bash .claude/install.sh` adds it to hooks
|
|
28
|
+
installed by older versions.
|
|
29
|
+
|
|
30
|
+
Registry drift (item 4 of #20) stays in the downstream fork; babel-fish has no
|
|
31
|
+
tool registry.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **This repo's pre-commit hook never ran.** Both `.githooks/` files were
|
|
36
|
+
committed `100644`, and git skips a non-executable hook without a word, so no
|
|
37
|
+
clone ever regenerated its map on commit. Both are `100755` now, and
|
|
38
|
+
`test_hooks_are_committed_executable` holds them there.
|
|
39
|
+
|
|
40
|
+
57 tests pass across four suites. Each context check was mutation-tested: breaking
|
|
41
|
+
it fails at least one test. A fresh install and an upgrade over a pre-2.5.0 hook
|
|
42
|
+
were both run end to end.
|
|
43
|
+
|
|
44
|
+
## [2.4.3] - 2026-09-05
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **`npm test` overwrote the map it was testing, and still reported every test
|
|
49
|
+
passing** ([#18](https://github.com/TheGlitchKing/babel-fish/issues/18)). A run
|
|
50
|
+
rewrote the working tree's `.claude/project-map/glossary.json` with fixture
|
|
51
|
+
data — 10 entries down to 1 — and printed a clean pass. Caught only by
|
|
52
|
+
diffing before a commit during the 2.4.2 release.
|
|
53
|
+
|
|
54
|
+
`generate.py` and `mine-sessions.py` bind `MAP_DIR`, `SECTIONS_DIR`,
|
|
55
|
+
`CHECKSUMS`, `LEARNED_VOC` and `GLOSSARY` at import time from `__file__`, and
|
|
56
|
+
rebind them only inside `configure_paths()`. Both test loaders assigned
|
|
57
|
+
`mod.PROJECT_ROOT` and stopped there, so every output path stayed aimed at the
|
|
58
|
+
real repo while the tests believed they were in a temp fixture; whichever test
|
|
59
|
+
called `write_glossary()` last won. This is the [#15](https://github.com/TheGlitchKing/babel-fish/issues/15)
|
|
60
|
+
defect class — writes escaping to the script's own directory — surviving in
|
|
61
|
+
the harness after being fixed in the scripts themselves.
|
|
62
|
+
|
|
63
|
+
Both loaders now call `configure_paths(fixture_root)`.
|
|
64
|
+
`test_glossary_survives_foreign_project_root` depended on the leak (it needs
|
|
65
|
+
`SECTIONS_DIR` outside `PROJECT_ROOT`, which used to arrive for free) and now
|
|
66
|
+
recreates that mismatch explicitly, so it still covers the `ValueError` from
|
|
67
|
+
`relative_to()` without writing outside its fixture — verified by mutation:
|
|
68
|
+
deleting the `try`/`except` in `_relative_source()` fails that test and only
|
|
69
|
+
that one. `test_grader.py` is deliberately untouched; its loader assigns a map
|
|
70
|
+
dir rather than a project root.
|
|
71
|
+
|
|
72
|
+
38 tests pass across the three suites, and a full run now leaves the tree
|
|
73
|
+
clean.
|
|
74
|
+
|
|
5
75
|
## [2.4.2] - 2026-09-05
|
|
6
76
|
|
|
7
77
|
### Fixed
|
package/README.md
CHANGED
|
@@ -146,7 +146,7 @@ See [CHANGELOG.md](./CHANGELOG.md) for the full 2.0.0 release notes, breaking-ch
|
|
|
146
146
|
2. Detects your stack (language, framework, database, ORM, auth, infra)
|
|
147
147
|
3. Runs `generate.py` → grades with `grader.py` (iterates up to 3× until 90%+ quality)
|
|
148
148
|
4. Renders your developer skill and rules files
|
|
149
|
-
5. Installs the pre-commit hook (auto-regenerates map on source file changes)
|
|
149
|
+
5. Installs the pre-commit hook (auto-regenerates map on source file changes; checks auto-loaded context when `CLAUDE.md` or `.claude/rules/` changes)
|
|
150
150
|
6. Updates `CLAUDE.md` with a project map pointer
|
|
151
151
|
7. Prints a full quality report
|
|
152
152
|
|
|
@@ -227,6 +227,21 @@ Entries follow a simple format: symptom → cause → fix. This is the anti-drif
|
|
|
227
227
|
|
|
228
228
|
---
|
|
229
229
|
|
|
230
|
+
## Auto-loaded Context Check *(2.5.0+)*
|
|
231
|
+
|
|
232
|
+
Everything in `CLAUDE.md` and `.claude/rules/` loads into every session, and past Claude Code's 150k-char limit rules stop binding silently. When those files are staged, the pre-commit hook runs `context-check.py` and blocks the commit if:
|
|
233
|
+
|
|
234
|
+
- the total exceeds the budget (default 60,000 chars), or
|
|
235
|
+
- a pointer names a file that doesn't exist. hewtd only link-checks inside `.documentation/`, so a renamed doc breaks a rule's pointer unnoticed.
|
|
236
|
+
|
|
237
|
+
Opt in to `--map-style` to also require every rule bullet to read `` - ALWAYS|NEVER <rule> — <why> → `doc` ``. See [auto-loaded context check](./.documentation/standards/auto-loaded-context-check.md).
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
python .claude/project-map/context-check.py --budget 80000 --map-style
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
230
245
|
## File Structure
|
|
231
246
|
|
|
232
247
|
```
|
|
@@ -235,6 +250,7 @@ Entries follow a simple format: symptom → cause → fix. This is the anti-drif
|
|
|
235
250
|
│ ├── generate.py # Introspection script
|
|
236
251
|
│ ├── grader.py # Quality grader
|
|
237
252
|
│ ├── mine-sessions.py # Session vocabulary miner
|
|
253
|
+
│ ├── context-check.py # Budget + pointer check for auto-loaded files
|
|
238
254
|
│ ├── PROJECT_MAP.md # TOC + quick routing guide
|
|
239
255
|
│ ├── sections/ # 19 focused section files
|
|
240
256
|
│ ├── reports/ # Install and iteration reports
|
|
@@ -247,7 +263,7 @@ Entries follow a simple format: symptom → cause → fix. This is the anti-drif
|
|
|
247
263
|
└── <project>-developer-skill/
|
|
248
264
|
└── SKILL.md
|
|
249
265
|
.githooks/
|
|
250
|
-
├── pre-commit #
|
|
266
|
+
├── pre-commit # Regenerates map; runs the context check
|
|
251
267
|
└── install.sh # Register hooks: bash .githooks/install.sh
|
|
252
268
|
```
|
|
253
269
|
|
|
@@ -281,6 +297,7 @@ Entries follow a simple format: symptom → cause → fix. This is the anti-drif
|
|
|
281
297
|
| `python .claude/project-map/generate.py --force` | Force-regenerate project map |
|
|
282
298
|
| `python .claude/project-map/grader.py` | Grade map quality (0–100%) |
|
|
283
299
|
| `python .claude/project-map/mine-sessions.py` | Mine session vocabulary |
|
|
300
|
+
| `python .claude/project-map/context-check.py` | Check auto-loaded context: budget, pointers |
|
|
284
301
|
| `bash .githooks/install.sh` | (Re)install git hooks |
|
|
285
302
|
| `bash .claude/install.sh` | Re-run full plugin installer |
|
|
286
303
|
|
package/checksums.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theglitchking/babel-fish",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Gives your AI coding assistant instant, accurate knowledge of every route, model, service, feature, and infrastructure element in your codebase.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"postinstall": "node scripts/link-skills.js",
|
|
11
|
-
"test": "python3 .claude/project-map/test_generate.py && python3 .claude/project-map/test_mine_sessions.py && python3 .claude/project-map/test_grader.py"
|
|
11
|
+
"test": "python3 .claude/project-map/test_generate.py && python3 .claude/project-map/test_mine_sessions.py && python3 .claude/project-map/test_grader.py && python3 .claude/project-map/test_context_check.py"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
".claude/project-map/generate.py",
|
|
23
23
|
".claude/project-map/grader.py",
|
|
24
24
|
".claude/project-map/mine-sessions.py",
|
|
25
|
+
".claude/project-map/context-check.py",
|
|
25
26
|
".claude/project-map/test_generate.py",
|
|
26
27
|
".claude/project-map/test_mine_sessions.py",
|
|
27
28
|
".claude/project-map/test_grader.py",
|
|
29
|
+
".claude/project-map/test_context_check.py",
|
|
28
30
|
".githooks/",
|
|
29
31
|
"skills/",
|
|
30
32
|
"install.sh",
|