xtrm-tools 2.1.16 → 2.1.17
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/cli/package.json +1 -1
- package/config/hooks.json +8 -20
- package/hooks/beads-compact-restore.mjs +59 -0
- package/hooks/beads-compact-save.mjs +46 -0
- package/hooks/main-guard-post-push.mjs +7 -9
- package/hooks/main-guard.mjs +1 -0
- package/package.json +1 -1
- package/project-skills/quality-gates/.claude/hooks/quality-check.py +11 -1
- package/hooks/skill-discovery.py +0 -90
- package/hooks/skill-suggestion.py +0 -112
- package/hooks/type-safety-enforcement.py +0 -107
package/cli/package.json
CHANGED
package/config/hooks.json
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
-
"
|
|
3
|
+
"SessionStart": [
|
|
4
4
|
{
|
|
5
|
-
"script": "
|
|
5
|
+
"script": "beads-compact-restore.mjs",
|
|
6
6
|
"timeout": 5000
|
|
7
7
|
},
|
|
8
|
-
{
|
|
9
|
-
"script": "gitnexus-impact-reminder.py",
|
|
10
|
-
"timeout": 5000
|
|
11
|
-
}
|
|
12
|
-
],
|
|
13
|
-
"SessionStart": [
|
|
14
8
|
{
|
|
15
9
|
"script": "serena-workflow-reminder.py"
|
|
16
10
|
}
|
|
@@ -30,15 +24,6 @@
|
|
|
30
24
|
"matcher": "Read|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
31
25
|
"script": "serena-workflow-reminder.py"
|
|
32
26
|
},
|
|
33
|
-
{
|
|
34
|
-
"matcher": "Bash",
|
|
35
|
-
"script": "type-safety-enforcement.py",
|
|
36
|
-
"timeout": 30000
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"matcher": "Edit|Write|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
40
|
-
"script": "type-safety-enforcement.py"
|
|
41
|
-
},
|
|
42
27
|
{
|
|
43
28
|
"matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
44
29
|
"script": "beads-edit-gate.mjs",
|
|
@@ -71,11 +56,14 @@
|
|
|
71
56
|
"script": "beads-memory-gate.mjs",
|
|
72
57
|
"timeout": 8000
|
|
73
58
|
}
|
|
59
|
+
],
|
|
60
|
+
"PreCompact": [
|
|
61
|
+
{
|
|
62
|
+
"script": "beads-compact-save.mjs",
|
|
63
|
+
"timeout": 5000
|
|
64
|
+
}
|
|
74
65
|
]
|
|
75
66
|
},
|
|
76
|
-
"skillSuggestions": {
|
|
77
|
-
"enabled": true
|
|
78
|
-
},
|
|
79
67
|
"statusLine": {
|
|
80
68
|
"script": "statusline-starship.sh"
|
|
81
69
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code SessionStart hook — restore in_progress beads issues after context compaction.
|
|
3
|
+
// Reads .beads/.last_active (written by beads-compact-save.mjs), reinstates statuses,
|
|
4
|
+
// deletes the marker, and injects a brief agent context message.
|
|
5
|
+
// Exit 0 in all paths (informational only).
|
|
6
|
+
//
|
|
7
|
+
// Installed by: xtrm install
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
import { readFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
let input;
|
|
14
|
+
try {
|
|
15
|
+
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
16
|
+
} catch {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
21
|
+
const lastActivePath = path.join(cwd, '.beads', '.last_active');
|
|
22
|
+
|
|
23
|
+
if (!existsSync(lastActivePath)) process.exit(0);
|
|
24
|
+
|
|
25
|
+
const ids = readFileSync(lastActivePath, 'utf8').trim().split('\n').filter(Boolean);
|
|
26
|
+
|
|
27
|
+
// Clean up regardless of whether restore succeeds
|
|
28
|
+
unlinkSync(lastActivePath);
|
|
29
|
+
|
|
30
|
+
if (ids.length === 0) process.exit(0);
|
|
31
|
+
|
|
32
|
+
let restored = 0;
|
|
33
|
+
for (const id of ids) {
|
|
34
|
+
try {
|
|
35
|
+
execSync(`bd update ${id} --status in_progress`, {
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
cwd,
|
|
38
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
timeout: 5000,
|
|
40
|
+
});
|
|
41
|
+
restored++;
|
|
42
|
+
} catch {
|
|
43
|
+
// ignore — issue may no longer exist
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (restored > 0) {
|
|
48
|
+
process.stdout.write(
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
hookSpecificOutput: {
|
|
51
|
+
hookEventName: 'SessionStart',
|
|
52
|
+
additionalSystemPrompt:
|
|
53
|
+
`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction. Check \`bd list\` for details.`,
|
|
54
|
+
},
|
|
55
|
+
}) + '\n',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.exit(0);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code PreCompact hook — save in_progress beads issues before context is compacted.
|
|
3
|
+
// Writes issue IDs to .beads/.last_active so beads-compact-restore.mjs can reinstate them.
|
|
4
|
+
// Exit 0 in all paths (informational only).
|
|
5
|
+
//
|
|
6
|
+
// Installed by: xtrm install
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
let input;
|
|
13
|
+
try {
|
|
14
|
+
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
20
|
+
const beadsDir = path.join(cwd, '.beads');
|
|
21
|
+
|
|
22
|
+
if (!existsSync(beadsDir)) process.exit(0);
|
|
23
|
+
|
|
24
|
+
let output = '';
|
|
25
|
+
try {
|
|
26
|
+
output = execSync('bd list --status=in_progress', {
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
cwd,
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
timeout: 8000,
|
|
31
|
+
}).trim();
|
|
32
|
+
} catch {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse issue IDs — lines like "◐ proj-abc123 ● P1 Title"
|
|
37
|
+
const ids = [];
|
|
38
|
+
for (const line of output.split('\n')) {
|
|
39
|
+
const match = line.trim().match(/^[○◐●✓❄]\s+([\w-]+)\s/u);
|
|
40
|
+
if (match) ids.push(match[1]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (ids.length === 0) process.exit(0);
|
|
44
|
+
|
|
45
|
+
writeFileSync(path.join(beadsDir, '.last_active'), ids.join('\n') + '\n', 'utf8');
|
|
46
|
+
process.exit(0);
|
|
@@ -59,13 +59,11 @@ const explicitlyProtectedTarget = protectedBranches
|
|
|
59
59
|
.some((b) => lastToken === b || lastToken.endsWith(`:${b}`));
|
|
60
60
|
if (explicitlyProtectedTarget) process.exit(0);
|
|
61
61
|
|
|
62
|
-
process.stdout.write(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}));
|
|
70
|
-
process.stdout.write('\n');
|
|
62
|
+
process.stdout.write(
|
|
63
|
+
`✅ Pushed '${branch}'. Next workflow steps:\n\n` +
|
|
64
|
+
' 1. gh pr create --fill\n' +
|
|
65
|
+
' 2. gh pr merge --squash\n' +
|
|
66
|
+
' 3. git checkout main && git reset --hard origin/main\n\n' +
|
|
67
|
+
'Before/after merge, ensure beads state is updated (e.g. bd close <id>).\n',
|
|
68
|
+
);
|
|
71
69
|
process.exit(0);
|
package/hooks/main-guard.mjs
CHANGED
package/package.json
CHANGED
|
@@ -140,7 +140,17 @@ def check_mypy(file_path: str, project_root: str) -> tuple[bool, list[str]]:
|
|
|
140
140
|
|
|
141
141
|
log_info('Running Mypy type checking...')
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
# Build mypy command with strictness flags
|
|
144
|
+
# Default: --disallow-untyped-defs catches untyped function parameters
|
|
145
|
+
# Opt-in: CLAUDE_HOOKS_MYPY_STRICT=true enables full --strict mode
|
|
146
|
+
mypy_strict = os.environ.get('CLAUDE_HOOKS_MYPY_STRICT', 'false').lower() == 'true'
|
|
147
|
+
|
|
148
|
+
if mypy_strict:
|
|
149
|
+
cmd = ['mypy', '--strict', '--pretty', file_path]
|
|
150
|
+
log_debug('Running mypy with --strict (full strictness)')
|
|
151
|
+
else:
|
|
152
|
+
cmd = ['mypy', '--disallow-untyped-defs', '--pretty', file_path]
|
|
153
|
+
log_debug('Running mypy with --disallow-untyped-defs (baseline strictness)')
|
|
144
154
|
try:
|
|
145
155
|
result = subprocess.run(cmd, capture_output=True, text=True, cwd=project_root)
|
|
146
156
|
|
package/hooks/skill-discovery.py
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import json
|
|
3
|
-
import sys
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
|
|
7
|
-
# Add script directory to path to allow importing shared modules
|
|
8
|
-
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
9
|
-
from agent_context import AgentContext
|
|
10
|
-
|
|
11
|
-
def get_first_sentence(text):
|
|
12
|
-
if not text:
|
|
13
|
-
return ""
|
|
14
|
-
# Remove newlines and extra spaces
|
|
15
|
-
text = re.sub(r'\s+', ' ', text).strip()
|
|
16
|
-
# Find the first period followed by a space or end of string
|
|
17
|
-
match = re.search(r'^(.*?)[.!?](\s|$)', text)
|
|
18
|
-
if match:
|
|
19
|
-
return match.group(0).strip()
|
|
20
|
-
return text
|
|
21
|
-
|
|
22
|
-
def parse_skill_md(file_path):
|
|
23
|
-
try:
|
|
24
|
-
with open(file_path, 'r') as f:
|
|
25
|
-
content = f.read()
|
|
26
|
-
|
|
27
|
-
# Extract YAML frontmatter
|
|
28
|
-
match = re.search(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
|
|
29
|
-
if match:
|
|
30
|
-
frontmatter_raw = match.group(1)
|
|
31
|
-
# Basic YAML-like parsing using regex to avoid external dependency (PyYAML)
|
|
32
|
-
name_match = re.search(r'^name:\s*(.*)$', frontmatter_raw, re.MULTILINE)
|
|
33
|
-
desc_match = re.search(r'^description:\s*(?:>-\s*)?\n?\s*(.*)$', frontmatter_raw, re.MULTILINE | re.DOTALL)
|
|
34
|
-
|
|
35
|
-
name = name_match.group(1).strip() if name_match else os.path.basename(os.path.dirname(file_path))
|
|
36
|
-
description = ""
|
|
37
|
-
|
|
38
|
-
if desc_match:
|
|
39
|
-
desc_raw = desc_match.group(1).strip()
|
|
40
|
-
# If it was a folded scalar, it might have multiple lines
|
|
41
|
-
if '\n' in desc_raw:
|
|
42
|
-
# Capture until next YAML key or end
|
|
43
|
-
description = desc_raw.split('\n')[0].strip()
|
|
44
|
-
else:
|
|
45
|
-
description = desc_raw
|
|
46
|
-
|
|
47
|
-
return name, get_first_sentence(description)
|
|
48
|
-
except Exception as e:
|
|
49
|
-
print(f"Error parsing {file_path}: {e}", file=sys.stderr)
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
def main():
|
|
53
|
-
try:
|
|
54
|
-
ctx = AgentContext()
|
|
55
|
-
project_dir = os.environ.get('GEMINI_PROJECT_DIR', os.getcwd())
|
|
56
|
-
skills_root = os.path.join(project_dir, 'skills')
|
|
57
|
-
|
|
58
|
-
if not os.path.exists(skills_root):
|
|
59
|
-
ctx.fail_open()
|
|
60
|
-
|
|
61
|
-
available_skills = []
|
|
62
|
-
for skill_dir in os.listdir(skills_root):
|
|
63
|
-
skill_path = os.path.join(skills_root, skill_dir)
|
|
64
|
-
if os.path.isdir(skill_path):
|
|
65
|
-
skill_md = os.path.join(skill_path, 'SKILL.md')
|
|
66
|
-
if os.path.exists(skill_md):
|
|
67
|
-
result = parse_skill_md(skill_md)
|
|
68
|
-
if result:
|
|
69
|
-
name, desc = result
|
|
70
|
-
available_skills.append(f"- {name}: {desc}")
|
|
71
|
-
|
|
72
|
-
if not available_skills:
|
|
73
|
-
ctx.fail_open()
|
|
74
|
-
|
|
75
|
-
context_msg = "## Available Local Agent Skills\n"
|
|
76
|
-
context_msg += "The following specialized skills are available in this repository. Use them when appropriate:\n"
|
|
77
|
-
context_msg += "\n".join(sorted(available_skills))
|
|
78
|
-
context_msg += "\n\nYou can activate a skill using `activate_skill(name='skill-name')`."
|
|
79
|
-
|
|
80
|
-
ctx.allow(
|
|
81
|
-
system_message="🚀 Loaded available local skills into context.",
|
|
82
|
-
additional_context=context_msg
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
except Exception as e:
|
|
86
|
-
print(f"Hook failed: {e}", file=sys.stderr)
|
|
87
|
-
sys.exit(0)
|
|
88
|
-
|
|
89
|
-
if __name__ == "__main__":
|
|
90
|
-
main()
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import sys
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
|
|
6
|
-
# Add script directory to path to allow importing shared modules
|
|
7
|
-
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
8
|
-
from agent_context import AgentContext
|
|
9
|
-
|
|
10
|
-
# Configuration
|
|
11
|
-
ORCHESTRATION_PATTERNS = [
|
|
12
|
-
r"review.*(code|security|quality)|code.*(review|audit)",
|
|
13
|
-
r"security.*(audit|review|scan)",
|
|
14
|
-
r"implement.*(feature|endpoint|api)|build.*feature",
|
|
15
|
-
r"(debug|investigate|root.*cause|crash|fix.*unknown)",
|
|
16
|
-
r"(refactor.*sprint|major.*refactor|migration|technical.*debt)",
|
|
17
|
-
r"validate.*(commit|staged)|pre.*commit",
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
CCS_PATTERNS = [
|
|
21
|
-
r"(fix|correggi|risolvi).*typo",
|
|
22
|
-
r"(fix|correggi).*spelling",
|
|
23
|
-
r"(add|aggiungi|crea|create).*test",
|
|
24
|
-
r"(genera|generate).*(test|unit|case)",
|
|
25
|
-
r"(estrai|extract).*(function|method|funzione|metodo)",
|
|
26
|
-
r"rename.*variable|rinomina.*variabile",
|
|
27
|
-
r"(add|aggiungi).*(doc|docstring|comment)",
|
|
28
|
-
r"(aggiorna|update).*comment",
|
|
29
|
-
r"(format|formatta|lint|indenta|indent)",
|
|
30
|
-
r"(add|aggiungi).*(type|typing|hint)",
|
|
31
|
-
r"(rimuovi|remove|elimina|delete).*(import|unused)",
|
|
32
|
-
r"(modifica|modify|cambia|change).*(name|nome)"
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
P_PATTERNS = [
|
|
36
|
-
r"analiz|analyz|esamina|studia|review|rivedi",
|
|
37
|
-
r"implementa|implement|create|crea",
|
|
38
|
-
r"spiega|explain|descri|describe",
|
|
39
|
-
r"^(come|how|what|cosa|perch|why)"
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
EXCLUDE_PATTERNS = [
|
|
43
|
-
r"archit|design|progett",
|
|
44
|
-
r"(add|implement|fix|patch).*(security|auth|oauth)|security.*(vuln|fix|patch)",
|
|
45
|
-
r"bug|debug|investig|indaga",
|
|
46
|
-
r"performance|ottimizz|optim",
|
|
47
|
-
r"migra|breaking.*change",
|
|
48
|
-
r"complex|compless"
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
CONVERSATIONAL_PATTERNS = [
|
|
52
|
-
r"^(ciao|hi|hello|hey|buongiorno|buonasera|salve)([!.]|$)",
|
|
53
|
-
r"^(good morning|good afternoon|good evening)([!.]|$)",
|
|
54
|
-
r"^(grazie|thanks|thank you|merci|thx)([!.]|$)",
|
|
55
|
-
r"^(grazie mille|thanks a lot|many thanks)([!.]|$)",
|
|
56
|
-
r"^(ok|okay|va bene|perfetto|perfect|fine|d'accordo|agreed?)([!.]|$)",
|
|
57
|
-
r"^(si|sì|yes|no|nope|yeah|yep)([!.]|$)",
|
|
58
|
-
r"^(arrivederci|addio|ciao|bye|goodbye|see you|ci vediamo)([!.]|$)",
|
|
59
|
-
r"^come stai\?$|^how are you\?$|^come va\?$",
|
|
60
|
-
r"^tutto bene\?$|^all good\?$|^everything ok\?$"
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
def matches(text, patterns):
|
|
64
|
-
for pattern in patterns:
|
|
65
|
-
if re.search(pattern, text, re.IGNORECASE):
|
|
66
|
-
return True
|
|
67
|
-
return False
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
ctx = AgentContext()
|
|
71
|
-
prompt = ctx.prompt
|
|
72
|
-
|
|
73
|
-
if not prompt:
|
|
74
|
-
ctx.fail_open()
|
|
75
|
-
|
|
76
|
-
ccs_available = not bool(os.environ.get('CLAUDECODE'))
|
|
77
|
-
ccs_hint = "CCS backend" if ccs_available else "Gemini or Qwen directly (CCS unavailable inside Claude Code)"
|
|
78
|
-
|
|
79
|
-
# 1. Check Exclusions
|
|
80
|
-
if matches(prompt, EXCLUDE_PATTERNS) or matches(prompt, CONVERSATIONAL_PATTERNS):
|
|
81
|
-
ctx.fail_open()
|
|
82
|
-
|
|
83
|
-
agent_name = ctx.agent_type.capitalize()
|
|
84
|
-
|
|
85
|
-
# 2. Check Explicit Delegation
|
|
86
|
-
if re.search(r'delegate', prompt, re.IGNORECASE):
|
|
87
|
-
ctx.allow(system_message=f"💡 {agent_name} Internal Reminder: User mentioned 'delegate'. Consider using the /delegating skill to offload this task.")
|
|
88
|
-
|
|
89
|
-
# 3. Check CCS Delegation (Simple Tasks)
|
|
90
|
-
if matches(prompt, CCS_PATTERNS):
|
|
91
|
-
ctx.allow(system_message=f"💡 {agent_name} Internal Reminder: This appears to be a simple, deterministic task (typo/test/format/doc). Consider using the /delegating skill ({ccs_hint}) for cost-optimized execution.")
|
|
92
|
-
|
|
93
|
-
# 4. Check Orchestration (Complex Tasks)
|
|
94
|
-
elif matches(prompt, ORCHESTRATION_PATTERNS) and not matches(prompt, EXCLUDE_PATTERNS):
|
|
95
|
-
ctx.allow(system_message=f"💡 {agent_name} Internal Reminder: This looks like a multi-agent task (review/implement/debug). Consider using the /delegating skill (Gemini+Qwen orchestration) instead of handling in main session.")
|
|
96
|
-
|
|
97
|
-
# 5. Check Prompt Improving (/p)
|
|
98
|
-
word_count = len(prompt.split())
|
|
99
|
-
is_vague = matches(prompt, P_PATTERNS)
|
|
100
|
-
|
|
101
|
-
# Heuristic for very short command-like prompts
|
|
102
|
-
if word_count < 6 and not is_vague:
|
|
103
|
-
if matches(prompt, [r"(creare|create|fare|do|aggiungere|add|modificare|modify|controllare|check|verificare|verify|testare|test)"]):
|
|
104
|
-
is_vague = True
|
|
105
|
-
|
|
106
|
-
if is_vague:
|
|
107
|
-
ctx.allow(system_message=f"💡 {agent_name} Internal Reminder: This prompt appears vague or could benefit from structure. Consider using the /prompt-improving skill to add XML structure, examples, and thinking space before proceeding.")
|
|
108
|
-
|
|
109
|
-
ctx.fail_open()
|
|
110
|
-
|
|
111
|
-
except Exception:
|
|
112
|
-
sys.exit(0)
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import sys
|
|
3
|
-
import os
|
|
4
|
-
import subprocess
|
|
5
|
-
|
|
6
|
-
# Add script directory to path to allow importing shared modules
|
|
7
|
-
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
8
|
-
from agent_context import AgentContext
|
|
9
|
-
|
|
10
|
-
# Configuration
|
|
11
|
-
STRICT_DIRS = ["mcp_server"]
|
|
12
|
-
WARN_DIRS = ["scripts"]
|
|
13
|
-
PROJECT_ROOT = os.environ.get('GEMINI_PROJECT_DIR', os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd()))
|
|
14
|
-
VENV_PATH = os.path.join(PROJECT_ROOT, ".venv")
|
|
15
|
-
|
|
16
|
-
# Colors
|
|
17
|
-
RED = '\033[0;31m'
|
|
18
|
-
YELLOW = '\033[1;33m'
|
|
19
|
-
GREEN = '\033[0;32m'
|
|
20
|
-
CYAN = '\033[0;36m'
|
|
21
|
-
NC = '\033[0m'
|
|
22
|
-
|
|
23
|
-
def is_strict_path(file_path):
|
|
24
|
-
rel_path = os.path.relpath(file_path, PROJECT_ROOT)
|
|
25
|
-
for d in STRICT_DIRS:
|
|
26
|
-
if rel_path.startswith(d):
|
|
27
|
-
return True
|
|
28
|
-
return False
|
|
29
|
-
|
|
30
|
-
def run_mypy(target, is_strict):
|
|
31
|
-
if not os.path.exists(os.path.join(VENV_PATH, "bin", "activate")):
|
|
32
|
-
print(f"{YELLOW}⚠️ Venv not found at {VENV_PATH}, skipping check{NC}")
|
|
33
|
-
return True
|
|
34
|
-
|
|
35
|
-
cmd = f"source {VENV_PATH}/bin/activate && python -m mypy {target} --explicit-package-bases"
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
result = subprocess.run(cmd, shell=True, executable="/bin/bash", capture_output=True, text=True)
|
|
39
|
-
|
|
40
|
-
if result.returncode != 0:
|
|
41
|
-
if is_strict:
|
|
42
|
-
print(f"{RED}❌ MYPY FAILED (STRICT MODE){NC}", file=sys.stderr)
|
|
43
|
-
print(result.stdout, file=sys.stderr)
|
|
44
|
-
print(f"\n{RED}🚫 COMMIT BLOCKED: Fix type errors in {target}{NC}", file=sys.stderr)
|
|
45
|
-
print(f"{CYAN}💡 Run: source .venv/bin/activate && python -m mypy {target}{NC}", file=sys.stderr)
|
|
46
|
-
return False
|
|
47
|
-
else:
|
|
48
|
-
print(f"{YELLOW}⚠️ MYPY WARNING (LENIENT MODE){NC}", file=sys.stderr)
|
|
49
|
-
print("\n".join(result.stdout.splitlines()[:20]), file=sys.stderr)
|
|
50
|
-
print(f"\n{YELLOW}⚡ Type errors exist in {target} (commit allowed){NC}", file=sys.stderr)
|
|
51
|
-
return True
|
|
52
|
-
else:
|
|
53
|
-
print(f"{GREEN}✅ MYPY PASSED: {target}{NC}", file=sys.stderr)
|
|
54
|
-
return True
|
|
55
|
-
|
|
56
|
-
except Exception as e:
|
|
57
|
-
print(f"Error running mypy: {e}", file=sys.stderr)
|
|
58
|
-
return True # Fail open
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
ctx = AgentContext()
|
|
62
|
-
|
|
63
|
-
# 1. Check Git Commits (Shell tools)
|
|
64
|
-
if ctx.is_shell_tool():
|
|
65
|
-
command = ctx.get_command()
|
|
66
|
-
if 'git commit' in command:
|
|
67
|
-
print(f"{CYAN}🔍 TYPE SAFETY CHECK: Validating staged Python files...{NC}", file=sys.stderr)
|
|
68
|
-
|
|
69
|
-
# Get staged files
|
|
70
|
-
try:
|
|
71
|
-
staged = subprocess.check_output(
|
|
72
|
-
"git diff --cached --name-only --diff-filter=ACM | grep '\.py$'",
|
|
73
|
-
shell=True, cwd=PROJECT_ROOT
|
|
74
|
-
).decode().strip().splitlines()
|
|
75
|
-
except subprocess.CalledProcessError:
|
|
76
|
-
staged = []
|
|
77
|
-
|
|
78
|
-
if not staged:
|
|
79
|
-
print(f"{GREEN}✅ No Python files staged{NC}", file=sys.stderr)
|
|
80
|
-
ctx.allow()
|
|
81
|
-
|
|
82
|
-
failed = False
|
|
83
|
-
|
|
84
|
-
# Check individual files
|
|
85
|
-
for f in staged:
|
|
86
|
-
full_path = os.path.join(PROJECT_ROOT, f)
|
|
87
|
-
if is_strict_path(full_path):
|
|
88
|
-
if not run_mypy(full_path, True):
|
|
89
|
-
failed = True
|
|
90
|
-
|
|
91
|
-
# If failed, block the tool
|
|
92
|
-
if failed:
|
|
93
|
-
ctx.block(reason="Type safety violations in strict directory.")
|
|
94
|
-
|
|
95
|
-
ctx.allow()
|
|
96
|
-
|
|
97
|
-
# 2. Check Edits (Write/Edit tools)
|
|
98
|
-
elif ctx.is_write_tool() or ctx.is_edit_tool():
|
|
99
|
-
file_path = ctx.get_file_path()
|
|
100
|
-
if file_path.endswith('.py') and is_strict_path(file_path):
|
|
101
|
-
ctx.allow(system_message=f"""{YELLOW}⚠️ EDITING STRICT TYPE-SAFE FILE{NC}
|
|
102
|
-
This file is in a STRICT zone ({', '.join(STRICT_DIRS)}).
|
|
103
|
-
Any type errors will BLOCK commits.
|
|
104
|
-
""")
|
|
105
|
-
|
|
106
|
-
except Exception:
|
|
107
|
-
sys.exit(0)
|