Skip to content

Commit a68b98a

Browse files
cdeustclaude
andcommitted
fix(ci): ruff format 4 files + add pre-commit-ruff hook for CI parity
The prior push (3bf4416) landed with ruff format drift in 4 files, which CI Lint caught. This commit: 1. Applies `ruff format` to the 4 drift files: - .claude/hooks/pre-tool-secret-shield.py - mcp_server/hooks/agent_briefing.py - mcp_server/shared/domain_mapping.py - scripts/test-agent-briefing.py 2. Adds .claude/hooks/pre-commit-ruff.sh — a local pre-commit gate that runs the SAME two checks CI runs (`ruff format --check .` and `ruff check .`) and blocks the commit with exit 2 + a plain-English reason on stderr if either fails. Mirrors zetetic's pre-commit-zetetic.sh pattern so drift can't reach origin without a loud local signal. 3. Wires the ruff hook into .claude/hooks/hooks.json as the FIRST PreToolUse entry on `command contains 'git commit'`, executing before the existing pre-commit-zetetic.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3bf4416 commit a68b98a

6 files changed

Lines changed: 103 additions & 18 deletions

File tree

.claude/hooks/hooks.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
"matcher": "Bash",
1515
"when": "command contains 'git commit'",
1616
"hooks": [
17+
{
18+
"type": "command",
19+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-ruff.sh"
20+
},
1721
{
1822
"type": "command",
1923
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-zetetic.sh"

.claude/hooks/pre-commit-ruff.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
# pre-commit-ruff.sh — block commits with ruff format or check failures.
3+
#
4+
# Runs the SAME two checks CI runs (ruff format --check . and ruff check .),
5+
# so failures surface locally before push instead of in GitHub Actions.
6+
#
7+
# Install: referenced from .claude/hooks/hooks.json as a PreToolUse entry on
8+
# Bash commits (matcher: Bash, when: command contains 'git commit'). The
9+
# Claude Code harness runs this script before executing the commit.
10+
#
11+
# Exit codes:
12+
# 0 — all ruff checks pass; commit proceeds
13+
# 2 — ruff format drift OR ruff check violations; commit blocked with a
14+
# plain-English reason on stderr that Claude sees
15+
#
16+
# Source: mirrors Cortex's .github/workflows/ci.yml Lint job (ruff format
17+
# --check . + ruff check .) so local and CI results agree.
18+
19+
set -uo pipefail
20+
21+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
22+
cd "$REPO_ROOT"
23+
24+
if ! command -v ruff >/dev/null 2>&1; then
25+
echo "[pre-commit-ruff] ruff not installed — skipping (install: pip install ruff)" >&2
26+
exit 0
27+
fi
28+
29+
fmt_out=$(ruff format --check . 2>&1)
30+
fmt_rc=$?
31+
chk_out=$(ruff check . 2>&1)
32+
chk_rc=$?
33+
34+
if (( fmt_rc != 0 )) || (( chk_rc != 0 )); then
35+
echo "[pre-commit-ruff] BLOCKED — ruff CI-parity checks failed" >&2
36+
if (( fmt_rc != 0 )); then
37+
echo "" >&2
38+
echo "[pre-commit-ruff] ruff format --check . said:" >&2
39+
echo "$fmt_out" | tail -20 >&2
40+
echo "" >&2
41+
echo "[pre-commit-ruff] Fix with: ruff format ." >&2
42+
fi
43+
if (( chk_rc != 0 )); then
44+
echo "" >&2
45+
echo "[pre-commit-ruff] ruff check . said:" >&2
46+
echo "$chk_out" | tail -30 >&2
47+
echo "" >&2
48+
echo "[pre-commit-ruff] Fix with: ruff check --fix ." >&2
49+
fi
50+
exit 2
51+
fi
52+
53+
exit 0

.claude/hooks/pre-tool-secret-shield.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,7 @@ def bash_blocked(cmd: str) -> tuple[bool, str | None]:
131131
is_b, pattern = is_blocked_path(candidate)
132132
if is_b:
133133
return True, (
134-
f"read of credential-bearing path '{candidate}' "
135-
f"(pattern '{pattern}')"
134+
f"read of credential-bearing path '{candidate}' (pattern '{pattern}')"
136135
)
137136
m = BLOCKED_PATTERNS_EMBEDDED.search(cmd)
138137
if m:
@@ -156,21 +155,24 @@ def main() -> int:
156155
path = tin.get("file_path", "")
157156
is_blocked, pattern = is_blocked_path(path)
158157
if is_blocked:
159-
blocked_reason = f"Read blocked: {path} matches credential pattern '{pattern}'"
158+
blocked_reason = (
159+
f"Read blocked: {path} matches credential pattern '{pattern}'"
160+
)
160161
elif tool == "Bash":
161162
cmd = tin.get("command", "")
162163
is_blocked, reason = bash_blocked(cmd)
163164
if is_blocked:
164-
blocked_reason = (
165-
f"Bash blocked: {reason}. Command: {cmd[:160]}"
166-
+ ("…" if len(cmd) > 160 else "")
165+
blocked_reason = f"Bash blocked: {reason}. Command: {cmd[:160]}" + (
166+
"…" if len(cmd) > 160 else ""
167167
)
168168
elif tool == "Grep":
169169
for key in ("path", "include"):
170170
v = tin.get(key, "")
171171
is_blocked, pattern = is_blocked_path(v) if v else (False, None)
172172
if is_blocked:
173-
blocked_reason = f"Grep blocked: {key}={v} matches credential pattern '{pattern}'"
173+
blocked_reason = (
174+
f"Grep blocked: {key}={v} matches credential pattern '{pattern}'"
175+
)
174176
break
175177
elif tool in ("Edit", "Write", "NotebookEdit"):
176178
path = tin.get("file_path", "")

mcp_server/hooks/agent_briefing.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,20 @@
8989
_MIN_HEAT = 0.2
9090

9191
# Fallback set used when ~/.claude/agents/ is missing (e.g., CI without install).
92-
_FALLBACK_AGENTS: frozenset[str] = frozenset({
93-
"engineer", "tester", "reviewer", "architect", "dba", "devops",
94-
"frontend", "security", "researcher", "ux",
95-
})
92+
_FALLBACK_AGENTS: frozenset[str] = frozenset(
93+
{
94+
"engineer",
95+
"tester",
96+
"reviewer",
97+
"architect",
98+
"dba",
99+
"devops",
100+
"frontend",
101+
"security",
102+
"researcher",
103+
"ux",
104+
}
105+
)
96106

97107
# Matches `name: <slug>` or `name: "<slug>"` in agent-file YAML frontmatter.
98108
_YAML_NAME_RE = re.compile(r"^name:\s*['\"]?([A-Za-z0-9_.-]+)['\"]?\s*$", re.MULTILINE)

mcp_server/shared/domain_mapping.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def resolve_domain(input_str: str) -> str:
336336
"-users-cdeust-",
337337
):
338338
if stripped.startswith(prefix):
339-
stripped = stripped[len(prefix):]
339+
stripped = stripped[len(prefix) :]
340340
break
341341
# Strip worktree suffixes that survived (no slug match found above).
342342
if "-worktrees-" in stripped:

scripts/test-agent-briefing.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
# Stub psycopg so agent_briefing imports cleanly without a live PG connection.
3232
# ---------------------------------------------------------------------------
3333

34+
3435
def _make_psycopg_stub(stub_rows: list[dict]) -> types.ModuleType:
3536
"""Build a minimal psycopg stub returning stub_rows on any execute().
3637
@@ -57,6 +58,7 @@ def _make_psycopg_stub(stub_rows: list[dict]) -> types.ModuleType:
5758
# Helpers
5859
# ---------------------------------------------------------------------------
5960

61+
6062
def _run_process_event(event: dict, stub_rows: list[dict]) -> tuple[str, str, int]:
6163
"""Run process_event() in isolation, capturing stdout/stderr and exit code.
6264
@@ -71,7 +73,9 @@ def _run_process_event(event: dict, stub_rows: list[dict]) -> tuple[str, str, in
7173

7274
exit_code = 0
7375

74-
with patch.dict(sys.modules, {"psycopg": psycopg_stub, "psycopg.rows": psycopg_stub.rows}):
76+
with patch.dict(
77+
sys.modules, {"psycopg": psycopg_stub, "psycopg.rows": psycopg_stub.rows}
78+
):
7579
# Force re-import so patched psycopg is visible inside _fetch_agent_context.
7680
if "mcp_server.hooks.agent_briefing" in sys.modules:
7781
del sys.modules["mcp_server.hooks.agent_briefing"]
@@ -90,16 +94,20 @@ def _run_process_event(event: dict, stub_rows: list[dict]) -> tuple[str, str, in
9094
# Test cases
9195
# ---------------------------------------------------------------------------
9296

93-
class TestAgentBriefing(unittest.TestCase):
9497

98+
class TestAgentBriefing(unittest.TestCase):
9599
def test_feynman_with_matching_memory_produces_briefing(self) -> None:
96100
"""Genius agent feynman + matching memory → stdout contains Cortex Briefing.
97101
98102
Pre-condition: feynman exists in ~/.claude/agents/genius/ (dynamic load).
99103
Post-condition: exit 0, stdout contains '## Cortex Briefing'.
100104
"""
101105
stub_rows = [
102-
{"content": "feynman past lesson: always verify sources", "heat": 0.8, "agent_context": "feynman"},
106+
{
107+
"content": "feynman past lesson: always verify sources",
108+
"heat": 0.8,
109+
"agent_context": "feynman",
110+
},
103111
]
104112
event = {
105113
"session_id": "test-session-001",
@@ -110,7 +118,11 @@ def test_feynman_with_matching_memory_produces_briefing(self) -> None:
110118
}
111119
stdout, stderr, code = _run_process_event(event, stub_rows)
112120
self.assertEqual(code, 0, f"Expected exit 0, got {code}. stderr: {stderr}")
113-
self.assertIn("Cortex Briefing", stdout, f"Expected 'Cortex Briefing' in stdout.\nstdout: {stdout!r}\nstderr: {stderr!r}")
121+
self.assertIn(
122+
"Cortex Briefing",
123+
stdout,
124+
f"Expected 'Cortex Briefing' in stdout.\nstdout: {stdout!r}\nstderr: {stderr!r}",
125+
)
114126

115127
def test_nonexistent_agent_skips_gracefully(self) -> None:
116128
"""Unknown agent name → exit 0 with skip log, no briefing emitted.
@@ -128,7 +140,9 @@ def test_nonexistent_agent_skips_gracefully(self) -> None:
128140
stdout, stderr, code = _run_process_event(event, [])
129141
self.assertEqual(code, 0, f"Expected exit 0, got {code}. stderr: {stderr}")
130142
self.assertEqual(stdout.strip(), "", f"Expected empty stdout, got: {stdout!r}")
131-
self.assertIn("skip", stderr.lower(), f"Expected 'skip' in stderr. stderr: {stderr!r}")
143+
self.assertIn(
144+
"skip", stderr.lower(), f"Expected 'skip' in stderr. stderr: {stderr!r}"
145+
)
132146

133147

134148
# ---------------------------------------------------------------------------
@@ -144,5 +158,7 @@ def test_nonexistent_agent_skips_gracefully(self) -> None:
144158
print("\nPASS: all agent-briefing tests passed.")
145159
sys.exit(0)
146160
else:
147-
print(f"\nFAIL: {len(result.failures)} failure(s), {len(result.errors)} error(s).")
161+
print(
162+
f"\nFAIL: {len(result.failures)} failure(s), {len(result.errors)} error(s)."
163+
)
148164
sys.exit(1)

0 commit comments

Comments
 (0)