stravinsky 0.1.2__py3-none-any.whl → 0.2.38__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.
Potentially problematic release.
This version of stravinsky might be problematic. Click here for more details.
- mcp_bridge/__init__.py +1 -5
- mcp_bridge/auth/cli.py +89 -44
- mcp_bridge/auth/oauth.py +88 -63
- mcp_bridge/hooks/__init__.py +49 -0
- mcp_bridge/hooks/agent_reminder.py +61 -0
- mcp_bridge/hooks/auto_slash_command.py +186 -0
- mcp_bridge/hooks/budget_optimizer.py +38 -0
- mcp_bridge/hooks/comment_checker.py +136 -0
- mcp_bridge/hooks/compaction.py +32 -0
- mcp_bridge/hooks/context_monitor.py +58 -0
- mcp_bridge/hooks/directory_context.py +40 -0
- mcp_bridge/hooks/edit_recovery.py +41 -0
- mcp_bridge/hooks/empty_message_sanitizer.py +240 -0
- mcp_bridge/hooks/keyword_detector.py +122 -0
- mcp_bridge/hooks/manager.py +96 -0
- mcp_bridge/hooks/preemptive_compaction.py +157 -0
- mcp_bridge/hooks/session_recovery.py +186 -0
- mcp_bridge/hooks/todo_enforcer.py +75 -0
- mcp_bridge/hooks/truncator.py +19 -0
- mcp_bridge/native_hooks/context.py +38 -0
- mcp_bridge/native_hooks/edit_recovery.py +46 -0
- mcp_bridge/native_hooks/stravinsky_mode.py +109 -0
- mcp_bridge/native_hooks/truncator.py +23 -0
- mcp_bridge/prompts/delphi.py +3 -2
- mcp_bridge/prompts/dewey.py +105 -21
- mcp_bridge/prompts/stravinsky.py +452 -118
- mcp_bridge/server.py +491 -668
- mcp_bridge/server_tools.py +547 -0
- mcp_bridge/tools/__init__.py +13 -3
- mcp_bridge/tools/agent_manager.py +359 -190
- mcp_bridge/tools/continuous_loop.py +67 -0
- mcp_bridge/tools/init.py +50 -0
- mcp_bridge/tools/lsp/tools.py +15 -15
- mcp_bridge/tools/model_invoke.py +594 -48
- mcp_bridge/tools/skill_loader.py +51 -47
- mcp_bridge/tools/task_runner.py +141 -0
- mcp_bridge/tools/templates.py +175 -0
- {stravinsky-0.1.2.dist-info → stravinsky-0.2.38.dist-info}/METADATA +55 -10
- stravinsky-0.2.38.dist-info/RECORD +57 -0
- stravinsky-0.1.2.dist-info/RECORD +0 -32
- {stravinsky-0.1.2.dist-info → stravinsky-0.2.38.dist-info}/WHEEL +0 -0
- {stravinsky-0.1.2.dist-info → stravinsky-0.2.38.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hooks initialization.
|
|
3
|
+
Registers all Tier 1-4 hooks into the HookManager.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .manager import get_hook_manager
|
|
7
|
+
from .truncator import output_truncator_hook
|
|
8
|
+
from .edit_recovery import edit_error_recovery_hook
|
|
9
|
+
from .directory_context import directory_context_hook
|
|
10
|
+
from .compaction import context_compaction_hook
|
|
11
|
+
from .budget_optimizer import budget_optimizer_hook
|
|
12
|
+
from .todo_enforcer import todo_continuation_hook
|
|
13
|
+
from .keyword_detector import keyword_detector_hook
|
|
14
|
+
from .comment_checker import comment_checker_hook
|
|
15
|
+
from .context_monitor import context_monitor_hook
|
|
16
|
+
from .agent_reminder import agent_reminder_hook
|
|
17
|
+
from .preemptive_compaction import preemptive_compaction_hook
|
|
18
|
+
from .auto_slash_command import auto_slash_command_hook
|
|
19
|
+
from .session_recovery import session_recovery_hook
|
|
20
|
+
from .empty_message_sanitizer import empty_message_sanitizer_hook
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def initialize_hooks():
|
|
24
|
+
"""Register all available hooks."""
|
|
25
|
+
manager = get_hook_manager()
|
|
26
|
+
|
|
27
|
+
# Tier 1: Post-tool-call (immediate response modification)
|
|
28
|
+
manager.register_post_tool_call(output_truncator_hook)
|
|
29
|
+
manager.register_post_tool_call(edit_error_recovery_hook)
|
|
30
|
+
manager.register_post_tool_call(comment_checker_hook)
|
|
31
|
+
manager.register_post_tool_call(agent_reminder_hook)
|
|
32
|
+
manager.register_post_tool_call(session_recovery_hook)
|
|
33
|
+
|
|
34
|
+
# Tier 2: Pre-model-invoke (context management)
|
|
35
|
+
manager.register_pre_model_invoke(directory_context_hook)
|
|
36
|
+
manager.register_pre_model_invoke(context_compaction_hook)
|
|
37
|
+
manager.register_pre_model_invoke(context_monitor_hook)
|
|
38
|
+
manager.register_pre_model_invoke(preemptive_compaction_hook)
|
|
39
|
+
manager.register_pre_model_invoke(empty_message_sanitizer_hook)
|
|
40
|
+
|
|
41
|
+
# Tier 3: Pre-model-invoke (performance optimization)
|
|
42
|
+
manager.register_pre_model_invoke(budget_optimizer_hook)
|
|
43
|
+
|
|
44
|
+
# Tier 4: Pre-model-invoke (behavior enforcement)
|
|
45
|
+
manager.register_pre_model_invoke(keyword_detector_hook)
|
|
46
|
+
manager.register_pre_model_invoke(todo_continuation_hook)
|
|
47
|
+
manager.register_pre_model_invoke(auto_slash_command_hook)
|
|
48
|
+
|
|
49
|
+
return manager
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Usage Reminder Hook.
|
|
3
|
+
|
|
4
|
+
When direct search tools (grep, glob, find) are used,
|
|
5
|
+
suggests using background agents for more comprehensive results.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
AGENT_SUGGESTION = """
|
|
14
|
+
[AGENT SUGGESTION]
|
|
15
|
+
You used `{tool_name}` directly. For more comprehensive results, consider:
|
|
16
|
+
|
|
17
|
+
**Background Agents (parallel, more thorough):**
|
|
18
|
+
```
|
|
19
|
+
background_task(agent="explore", prompt="Search for {search_context}...")
|
|
20
|
+
background_task(agent="dewey", prompt="Find documentation for {search_context}...")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Background agents can search multiple patterns simultaneously and provide richer context.
|
|
24
|
+
Use direct tools for quick, targeted lookups. Use agents for exploratory research.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
SEARCH_TOOLS = {"grep", "glob", "rg", "find", "Grep", "Glob", "grep_search", "glob_files"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def agent_reminder_hook(
|
|
31
|
+
tool_name: str, arguments: Dict[str, Any], output: str
|
|
32
|
+
) -> Optional[str]:
|
|
33
|
+
"""
|
|
34
|
+
Post-tool call hook that suggests background agents after direct search tool usage.
|
|
35
|
+
"""
|
|
36
|
+
if tool_name not in SEARCH_TOOLS:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
search_context = _extract_search_context(arguments)
|
|
40
|
+
|
|
41
|
+
if not search_context:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if len(output) < 100:
|
|
45
|
+
logger.info(
|
|
46
|
+
f"[AgentReminder] Direct search '{tool_name}' returned limited results, suggesting agents"
|
|
47
|
+
)
|
|
48
|
+
suggestion = AGENT_SUGGESTION.format(tool_name=tool_name, search_context=search_context)
|
|
49
|
+
return output + "\n" + suggestion
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_search_context(arguments: Dict[str, Any]) -> str:
|
|
55
|
+
"""Extract search context from tool arguments."""
|
|
56
|
+
for key in ("pattern", "query", "search", "name", "path"):
|
|
57
|
+
if key in arguments:
|
|
58
|
+
value = arguments[key]
|
|
59
|
+
if isinstance(value, str) and len(value) < 100:
|
|
60
|
+
return value
|
|
61
|
+
return "related patterns"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto Slash Command Hook.
|
|
3
|
+
|
|
4
|
+
Detects and auto-processes slash commands in user input:
|
|
5
|
+
- Parses `/command` patterns in user input
|
|
6
|
+
- Looks up matching skill via skill_loader
|
|
7
|
+
- Injects skill content into prompt
|
|
8
|
+
- Registered as pre_model_invoke hook
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Pattern to match slash commands at the beginning of a line or after whitespace
|
|
19
|
+
SLASH_COMMAND_PATTERN = re.compile(r'(?:^|(?<=\s))\/([a-zA-Z][a-zA-Z0-9_-]*)\b', re.MULTILINE)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_slash_commands(text: str) -> List[str]:
|
|
23
|
+
"""
|
|
24
|
+
Extract all slash command names from text.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
text: Input text to scan for slash commands
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of command names (without the slash)
|
|
31
|
+
"""
|
|
32
|
+
matches = SLASH_COMMAND_PATTERN.findall(text)
|
|
33
|
+
# Deduplicate while preserving order
|
|
34
|
+
seen = set()
|
|
35
|
+
unique = []
|
|
36
|
+
for match in matches:
|
|
37
|
+
if match.lower() not in seen:
|
|
38
|
+
seen.add(match.lower())
|
|
39
|
+
unique.append(match)
|
|
40
|
+
return unique
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_skill_content(command_name: str, project_path: Optional[str] = None) -> Optional[Tuple[str, str]]:
|
|
44
|
+
"""
|
|
45
|
+
Load skill content by command name.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
command_name: The command name to look up
|
|
49
|
+
project_path: Optional project path for local skills
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tuple of (skill_name, skill_content) or None if not found
|
|
53
|
+
"""
|
|
54
|
+
from ..tools.skill_loader import discover_skills, parse_frontmatter
|
|
55
|
+
|
|
56
|
+
skills = discover_skills(project_path)
|
|
57
|
+
|
|
58
|
+
# Find matching skill (case-insensitive)
|
|
59
|
+
skill = next(
|
|
60
|
+
(s for s in skills if s["name"].lower() == command_name.lower()),
|
|
61
|
+
None
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not skill:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
content = Path(skill["path"]).read_text()
|
|
69
|
+
metadata, body = parse_frontmatter(content)
|
|
70
|
+
|
|
71
|
+
# Build the skill injection block
|
|
72
|
+
skill_block = f"""
|
|
73
|
+
---
|
|
74
|
+
## Skill: /{skill['name']}
|
|
75
|
+
**Source**: {skill['path']}
|
|
76
|
+
"""
|
|
77
|
+
if metadata.get("description"):
|
|
78
|
+
skill_block += f"**Description**: {metadata['description']}\n"
|
|
79
|
+
|
|
80
|
+
if metadata.get("allowed-tools"):
|
|
81
|
+
skill_block += f"**Allowed Tools**: {metadata['allowed-tools']}\n"
|
|
82
|
+
|
|
83
|
+
skill_block += f"""
|
|
84
|
+
### Instructions:
|
|
85
|
+
{body}
|
|
86
|
+
---
|
|
87
|
+
"""
|
|
88
|
+
return skill["name"], skill_block
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"[AutoSlashCommand] Error loading skill '{command_name}': {e}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_project_path_from_prompt(prompt: str) -> Optional[str]:
|
|
96
|
+
"""
|
|
97
|
+
Try to extract project path from prompt context.
|
|
98
|
+
Looks for common patterns that indicate the working directory.
|
|
99
|
+
"""
|
|
100
|
+
# Look for CWD markers
|
|
101
|
+
cwd_patterns = [
|
|
102
|
+
r'CWD:\s*([^\n]+)',
|
|
103
|
+
r'Working directory:\s*([^\n]+)',
|
|
104
|
+
r'project_path["\']?\s*[:=]\s*["\']?([^"\'}\n]+)',
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for pattern in cwd_patterns:
|
|
108
|
+
match = re.search(pattern, prompt)
|
|
109
|
+
if match:
|
|
110
|
+
path = match.group(1).strip()
|
|
111
|
+
if Path(path).exists():
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
SKILL_INJECTION_HEADER = """
|
|
118
|
+
> **[AUTO-SKILL INJECTION]**
|
|
119
|
+
> The following skill(s) have been automatically loaded based on slash commands detected in your input:
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
SKILL_NOT_FOUND_WARNING = """
|
|
123
|
+
> **[SKILL NOT FOUND]**
|
|
124
|
+
> The slash command `/{command}` was detected but no matching skill was found.
|
|
125
|
+
> Available skills can be listed with the `skill_list` tool.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def auto_slash_command_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
130
|
+
"""
|
|
131
|
+
Pre-model invoke hook that detects slash commands and injects skill content.
|
|
132
|
+
|
|
133
|
+
Scans the prompt for /command patterns and loads corresponding skill files
|
|
134
|
+
from .claude/commands/ directories.
|
|
135
|
+
"""
|
|
136
|
+
prompt = params.get("prompt", "")
|
|
137
|
+
|
|
138
|
+
# Skip if already processed
|
|
139
|
+
if "[AUTO-SKILL INJECTION]" in prompt:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# Extract slash commands
|
|
143
|
+
commands = extract_slash_commands(prompt)
|
|
144
|
+
|
|
145
|
+
if not commands:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
logger.info(f"[AutoSlashCommand] Detected slash commands: {commands}")
|
|
149
|
+
|
|
150
|
+
# Try to get project path from prompt context
|
|
151
|
+
project_path = get_project_path_from_prompt(prompt)
|
|
152
|
+
|
|
153
|
+
# Load skills for each command
|
|
154
|
+
injections = []
|
|
155
|
+
warnings = []
|
|
156
|
+
loaded_skills = []
|
|
157
|
+
|
|
158
|
+
for command in commands:
|
|
159
|
+
result = load_skill_content(command, project_path)
|
|
160
|
+
if result:
|
|
161
|
+
skill_name, skill_content = result
|
|
162
|
+
injections.append(skill_content)
|
|
163
|
+
loaded_skills.append(skill_name)
|
|
164
|
+
logger.info(f"[AutoSlashCommand] Loaded skill: {skill_name}")
|
|
165
|
+
else:
|
|
166
|
+
warnings.append(SKILL_NOT_FOUND_WARNING.format(command=command))
|
|
167
|
+
logger.warning(f"[AutoSlashCommand] Skill not found: {command}")
|
|
168
|
+
|
|
169
|
+
if not injections and not warnings:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Build the injection block
|
|
173
|
+
injection_block = ""
|
|
174
|
+
|
|
175
|
+
if injections:
|
|
176
|
+
injection_block = SKILL_INJECTION_HEADER
|
|
177
|
+
injection_block += f"> Skills loaded: {', '.join(loaded_skills)}\n"
|
|
178
|
+
injection_block += "\n".join(injections)
|
|
179
|
+
|
|
180
|
+
if warnings:
|
|
181
|
+
injection_block += "\n".join(warnings)
|
|
182
|
+
|
|
183
|
+
# Prepend the injection to the prompt
|
|
184
|
+
params["prompt"] = injection_block + "\n\n" + prompt
|
|
185
|
+
|
|
186
|
+
return params
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thinking budget optimizer hook.
|
|
3
|
+
Analyzes prompt complexity and adjusts thinking_budget for models that support it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
REASONING_KEYWORDS = [
|
|
9
|
+
"architect", "design", "refactor", "debug", "complex", "optimize",
|
|
10
|
+
"summarize", "analyze", "explain", "why", "review", "strangler"
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
async def budget_optimizer_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
14
|
+
"""
|
|
15
|
+
Adjusts the thinking_budget based on presence of reasoning-heavy keywords.
|
|
16
|
+
"""
|
|
17
|
+
model = params.get("model", "")
|
|
18
|
+
# Only applies to models that typically support reasoning budgets (Gemini 2.0 Thinking, GPT-o1, etc.)
|
|
19
|
+
if not any(m in model for m in ["thinking", "flash-thinking", "o1", "o3"]):
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
prompt = params.get("prompt", "").lower()
|
|
23
|
+
|
|
24
|
+
# Simple heuristic
|
|
25
|
+
is_complex = any(keyword in prompt for keyword in REASONING_KEYWORDS)
|
|
26
|
+
|
|
27
|
+
current_budget = params.get("thinking_budget", 0)
|
|
28
|
+
|
|
29
|
+
if is_complex and current_budget < 4000:
|
|
30
|
+
# Increase budget for complex tasks
|
|
31
|
+
params["thinking_budget"] = 16000
|
|
32
|
+
return params
|
|
33
|
+
elif not is_complex and current_budget > 2000:
|
|
34
|
+
# Lower budget for simple tasks to save time/cost
|
|
35
|
+
params["thinking_budget"] = 2000
|
|
36
|
+
return params
|
|
37
|
+
|
|
38
|
+
return None
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comment Checker Hook.
|
|
3
|
+
|
|
4
|
+
Warns when generated code contains excessive comments.
|
|
5
|
+
Code should be self-documenting; excessive comments indicate AI slop.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
COMMENT_WARNING = """
|
|
15
|
+
[WARNING - EXCESSIVE COMMENTS DETECTED]
|
|
16
|
+
|
|
17
|
+
Your recent code edit contains a high ratio of comments ({ratio:.0%}).
|
|
18
|
+
Code generated by stravinsky should be indistinguishable from human-written code.
|
|
19
|
+
|
|
20
|
+
**Guidelines:**
|
|
21
|
+
- Comments should explain WHY, not WHAT
|
|
22
|
+
- Self-documenting code > commented code
|
|
23
|
+
- Remove obvious comments like "// Initialize the variable"
|
|
24
|
+
- Keep only: complex algorithms, security notes, performance optimizations, regex explanations
|
|
25
|
+
|
|
26
|
+
**Action Required:**
|
|
27
|
+
Review and remove unnecessary comments from your recent edit.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
COMMENT_PATTERNS = {
|
|
31
|
+
"python": [
|
|
32
|
+
r"^\s*#(?!\!)",
|
|
33
|
+
r'^\s*"""[\s\S]*?"""',
|
|
34
|
+
r"^\s*'''[\s\S]*?'''",
|
|
35
|
+
],
|
|
36
|
+
"javascript": [
|
|
37
|
+
r"^\s*//",
|
|
38
|
+
r"/\*[\s\S]*?\*/",
|
|
39
|
+
],
|
|
40
|
+
"typescript": [
|
|
41
|
+
r"^\s*//",
|
|
42
|
+
r"/\*[\s\S]*?\*/",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
CODE_EXTENSIONS = {
|
|
47
|
+
".py": "python",
|
|
48
|
+
".js": "javascript",
|
|
49
|
+
".ts": "typescript",
|
|
50
|
+
".tsx": "typescript",
|
|
51
|
+
".jsx": "javascript",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def comment_checker_hook(
|
|
56
|
+
tool_name: str, arguments: Dict[str, Any], output: str
|
|
57
|
+
) -> Optional[str]:
|
|
58
|
+
"""
|
|
59
|
+
Post-tool call hook that checks for excessive comments in code edits.
|
|
60
|
+
"""
|
|
61
|
+
if tool_name not in ("Write", "Edit", "write", "edit"):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
file_path = arguments.get("filePath", arguments.get("file_path", ""))
|
|
65
|
+
if not file_path:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
ext = None
|
|
69
|
+
for extension in CODE_EXTENSIONS:
|
|
70
|
+
if file_path.endswith(extension):
|
|
71
|
+
ext = extension
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if not ext:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
lang = CODE_EXTENSIONS[ext]
|
|
78
|
+
content = arguments.get("content", arguments.get("newString", ""))
|
|
79
|
+
|
|
80
|
+
if not content or len(content) < 50:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
comment_ratio = _calculate_comment_ratio(content, lang)
|
|
84
|
+
|
|
85
|
+
if comment_ratio > 0.30:
|
|
86
|
+
logger.warning(
|
|
87
|
+
f"[CommentChecker] High comment ratio ({comment_ratio:.0%}) detected in {file_path}"
|
|
88
|
+
)
|
|
89
|
+
warning = COMMENT_WARNING.format(ratio=comment_ratio)
|
|
90
|
+
return output + "\n" + warning
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _calculate_comment_ratio(content: str, lang: str) -> float:
|
|
96
|
+
"""Calculate the ratio of comment lines to total lines."""
|
|
97
|
+
lines = content.split("\n")
|
|
98
|
+
total_lines = len([l for l in lines if l.strip()])
|
|
99
|
+
|
|
100
|
+
if total_lines == 0:
|
|
101
|
+
return 0.0
|
|
102
|
+
|
|
103
|
+
comment_lines = 0
|
|
104
|
+
patterns = COMMENT_PATTERNS.get(lang, [])
|
|
105
|
+
|
|
106
|
+
in_multiline = False
|
|
107
|
+
for line in lines:
|
|
108
|
+
stripped = line.strip()
|
|
109
|
+
if not stripped:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if lang == "python":
|
|
113
|
+
if stripped.startswith("#") and not stripped.startswith("#!"):
|
|
114
|
+
comment_lines += 1
|
|
115
|
+
elif stripped.startswith('"""') or stripped.startswith("'''"):
|
|
116
|
+
if stripped.count('"""') >= 2 or stripped.count("'''") >= 2:
|
|
117
|
+
comment_lines += 1
|
|
118
|
+
else:
|
|
119
|
+
in_multiline = True
|
|
120
|
+
comment_lines += 1
|
|
121
|
+
elif in_multiline:
|
|
122
|
+
comment_lines += 1
|
|
123
|
+
if '"""' in stripped or "'''" in stripped:
|
|
124
|
+
in_multiline = False
|
|
125
|
+
else:
|
|
126
|
+
if stripped.startswith("//"):
|
|
127
|
+
comment_lines += 1
|
|
128
|
+
elif stripped.startswith("/*"):
|
|
129
|
+
in_multiline = True
|
|
130
|
+
comment_lines += 1
|
|
131
|
+
elif in_multiline:
|
|
132
|
+
comment_lines += 1
|
|
133
|
+
if "*/" in stripped:
|
|
134
|
+
in_multiline = False
|
|
135
|
+
|
|
136
|
+
return comment_lines / total_lines
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Preemptive context compaction hook.
|
|
3
|
+
Monitors context size and injects optimization reminders.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
THRESHOLD_CHARS = 100000 # Roughly 25k-30k tokens for typical LLM text
|
|
9
|
+
|
|
10
|
+
COMPACTION_REMINDER = """
|
|
11
|
+
> **[SYSTEM ALERT - CONTEXT WINDOW NEAR LIMIT]**
|
|
12
|
+
> The current conversation history is reaching its limits. Performance may degrade.
|
|
13
|
+
> Please **STOP** and perform a **Session Compaction**:
|
|
14
|
+
> 1. Summarize all work completed so far in a `TASK_STATE.md` (if not already done).
|
|
15
|
+
> 2. List all pending todos.
|
|
16
|
+
> 3. Clear unnecessary tool outputs from your reasoning.
|
|
17
|
+
> 4. Keep your next responses concise and focused only on the current sub-task.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
async def context_compaction_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
21
|
+
"""
|
|
22
|
+
Checks prompt length and injects a compaction reminder if it's too large.
|
|
23
|
+
"""
|
|
24
|
+
prompt = params.get("prompt", "")
|
|
25
|
+
|
|
26
|
+
if len(prompt) > THRESHOLD_CHARS:
|
|
27
|
+
# Check if we haven't already injected the reminder recently
|
|
28
|
+
if "CONTEXT WINDOW NEAR LIMIT" not in prompt:
|
|
29
|
+
params["prompt"] = COMPACTION_REMINDER + prompt
|
|
30
|
+
return params
|
|
31
|
+
|
|
32
|
+
return None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context Window Monitor Hook.
|
|
3
|
+
|
|
4
|
+
Monitors context window usage and provides headroom reminders.
|
|
5
|
+
At 70% usage, reminds the agent there's still capacity.
|
|
6
|
+
At 85%, suggests compaction before hitting hard limits.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
HEADROOM_REMINDER = """
|
|
15
|
+
[CONTEXT AWARENESS]
|
|
16
|
+
Current context usage: ~{usage:.0%}
|
|
17
|
+
You have approximately {remaining:.0%} headroom remaining.
|
|
18
|
+
Continue working - no compaction needed yet.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
COMPACTION_WARNING = """
|
|
22
|
+
[CONTEXT WARNING - PREEMPTIVE COMPACTION RECOMMENDED]
|
|
23
|
+
Current context usage: ~{usage:.0%}
|
|
24
|
+
Approaching context limit. Consider:
|
|
25
|
+
1. Completing current task quickly
|
|
26
|
+
2. Using `background_cancel(all=true)` to clean up agents
|
|
27
|
+
3. Summarizing findings before context overflow
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
CONTEXT_THRESHOLD_REMINDER = 0.70
|
|
31
|
+
CONTEXT_THRESHOLD_WARNING = 0.85
|
|
32
|
+
ESTIMATED_MAX_TOKENS = 200000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def context_monitor_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
36
|
+
"""
|
|
37
|
+
Pre-model invoke hook that monitors context window usage.
|
|
38
|
+
"""
|
|
39
|
+
prompt = params.get("prompt", "")
|
|
40
|
+
estimated_tokens = len(prompt) / 4
|
|
41
|
+
|
|
42
|
+
usage_ratio = estimated_tokens / ESTIMATED_MAX_TOKENS
|
|
43
|
+
|
|
44
|
+
if usage_ratio >= CONTEXT_THRESHOLD_WARNING:
|
|
45
|
+
remaining = 1.0 - usage_ratio
|
|
46
|
+
logger.warning(f"[ContextMonitor] High context usage: {usage_ratio:.0%}")
|
|
47
|
+
warning = COMPACTION_WARNING.format(usage=usage_ratio, remaining=remaining)
|
|
48
|
+
params["prompt"] = prompt + "\n\n" + warning
|
|
49
|
+
return params
|
|
50
|
+
|
|
51
|
+
elif usage_ratio >= CONTEXT_THRESHOLD_REMINDER:
|
|
52
|
+
remaining = 1.0 - usage_ratio
|
|
53
|
+
logger.info(f"[ContextMonitor] Context usage: {usage_ratio:.0%}, reminding of headroom")
|
|
54
|
+
reminder = HEADROOM_REMINDER.format(usage=usage_ratio, remaining=remaining)
|
|
55
|
+
params["prompt"] = prompt + "\n\n" + reminder
|
|
56
|
+
return params
|
|
57
|
+
|
|
58
|
+
return None
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Directory context injector hook.
|
|
3
|
+
Automatically finds and injects local AGENTS.md or README.md content based on the current context.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
async def directory_context_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
11
|
+
"""
|
|
12
|
+
Search for AGENTS.md or README.md in the current working directory and inject them.
|
|
13
|
+
"""
|
|
14
|
+
cwd = Path.cwd()
|
|
15
|
+
|
|
16
|
+
# Check for AGENTS.md or README.md
|
|
17
|
+
target_files = ["AGENTS.md", "README.md"]
|
|
18
|
+
found_file = None
|
|
19
|
+
for filename in target_files:
|
|
20
|
+
if (cwd / filename).exists():
|
|
21
|
+
found_file = cwd / filename
|
|
22
|
+
break
|
|
23
|
+
|
|
24
|
+
if not found_file:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
content = found_file.read_text()
|
|
29
|
+
# Injects as a special system reminder
|
|
30
|
+
injection = f"\n\n### Local Directory Context ({found_file.name}):\n{content}\n"
|
|
31
|
+
|
|
32
|
+
# Modify the prompt if it exists in params
|
|
33
|
+
if "prompt" in params:
|
|
34
|
+
# Add to the beginning of the prompt as a context block
|
|
35
|
+
params["prompt"] = injection + params["prompt"]
|
|
36
|
+
return params
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
return None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Edit error recovery hook.
|
|
3
|
+
Detects common mistakes in file editing and injects high-priority corrective directives.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
EDIT_ERROR_PATTERNS = [
|
|
10
|
+
r"oldString and newString must be different",
|
|
11
|
+
r"oldString not found",
|
|
12
|
+
r"oldString found multiple times",
|
|
13
|
+
r"Target content not found",
|
|
14
|
+
r"Multiple occurrences of target content found",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
EDIT_RECOVERY_PROMPT = """
|
|
18
|
+
> **[EDIT ERROR - IMMEDIATE ACTION REQUIRED]**
|
|
19
|
+
> You made an Edit mistake. STOP and do this NOW:
|
|
20
|
+
> 1. **READ** the file immediately to see its ACTUAL current state.
|
|
21
|
+
> 2. **VERIFY** what the content really looks like (your assumption was wrong).
|
|
22
|
+
> 3. **APOLOGIZE** briefly to the user for the error.
|
|
23
|
+
> 4. **CONTINUE** with corrected action based on the real file content.
|
|
24
|
+
> **DO NOT** attempt another edit until you've read and verified the file state.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
async def edit_error_recovery_hook(tool_name: str, arguments: Dict[str, Any], output: str) -> Optional[str]:
|
|
28
|
+
"""
|
|
29
|
+
Analyzes tool output for edit errors and appends corrective directives.
|
|
30
|
+
"""
|
|
31
|
+
# Check if this is an edit-related tool (handling both built-in and common MCP tools)
|
|
32
|
+
edit_tools = ["replace_file_content", "multi_replace_file_content", "write_to_file", "edit_file", "Edit"]
|
|
33
|
+
|
|
34
|
+
# We also check the output content for common patterns even if the tool name doesn't match perfectly
|
|
35
|
+
is_edit_error = any(re.search(pattern, output, re.IGNORECASE) for pattern in EDIT_ERROR_PATTERNS)
|
|
36
|
+
|
|
37
|
+
if is_edit_error or any(tool in tool_name for tool in edit_tools):
|
|
38
|
+
if any(re.search(pattern, output, re.IGNORECASE) for pattern in EDIT_ERROR_PATTERNS):
|
|
39
|
+
return output + EDIT_RECOVERY_PROMPT
|
|
40
|
+
|
|
41
|
+
return None
|