stravinsky 0.4.18__py3-none-any.whl → 0.4.66__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 -1
- mcp_bridge/auth/__init__.py +16 -6
- mcp_bridge/auth/cli.py +202 -11
- mcp_bridge/auth/oauth.py +1 -2
- mcp_bridge/auth/openai_oauth.py +4 -7
- mcp_bridge/auth/token_store.py +0 -1
- mcp_bridge/cli/__init__.py +1 -1
- mcp_bridge/cli/install_hooks.py +503 -107
- mcp_bridge/cli/session_report.py +0 -3
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +3 -5
- mcp_bridge/config/rate_limits.py +108 -13
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +14 -4
- mcp_bridge/hooks/agent_reminder.py +4 -4
- mcp_bridge/hooks/auto_slash_command.py +5 -5
- mcp_bridge/hooks/budget_optimizer.py +2 -2
- mcp_bridge/hooks/claude_limits_hook.py +114 -0
- mcp_bridge/hooks/comment_checker.py +3 -4
- mcp_bridge/hooks/compaction.py +2 -2
- mcp_bridge/hooks/context.py +2 -1
- mcp_bridge/hooks/context_monitor.py +2 -2
- mcp_bridge/hooks/delegation_policy.py +85 -0
- mcp_bridge/hooks/directory_context.py +3 -3
- mcp_bridge/hooks/edit_recovery.py +3 -2
- mcp_bridge/hooks/edit_recovery_policy.py +49 -0
- mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
- mcp_bridge/hooks/events.py +160 -0
- mcp_bridge/hooks/git_noninteractive.py +4 -4
- mcp_bridge/hooks/keyword_detector.py +8 -10
- mcp_bridge/hooks/manager.py +35 -22
- mcp_bridge/hooks/notification_hook.py +13 -6
- mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
- mcp_bridge/hooks/parallel_enforcer.py +5 -5
- mcp_bridge/hooks/parallel_execution.py +22 -10
- mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
- mcp_bridge/hooks/pre_compact.py +8 -9
- mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
- mcp_bridge/hooks/preemptive_compaction.py +2 -3
- mcp_bridge/hooks/routing_notifications.py +80 -0
- mcp_bridge/hooks/rules_injector.py +11 -19
- mcp_bridge/hooks/session_idle.py +4 -4
- mcp_bridge/hooks/session_notifier.py +4 -4
- mcp_bridge/hooks/session_recovery.py +4 -5
- mcp_bridge/hooks/stravinsky_mode.py +1 -1
- mcp_bridge/hooks/subagent_stop.py +1 -3
- mcp_bridge/hooks/task_validator.py +2 -2
- mcp_bridge/hooks/tmux_manager.py +7 -8
- mcp_bridge/hooks/todo_delegation.py +4 -1
- mcp_bridge/hooks/todo_enforcer.py +180 -10
- mcp_bridge/hooks/truncation_policy.py +37 -0
- mcp_bridge/hooks/truncator.py +1 -2
- mcp_bridge/metrics/cost_tracker.py +115 -0
- mcp_bridge/native_search.py +93 -0
- mcp_bridge/native_watcher.py +118 -0
- mcp_bridge/notifications.py +3 -4
- mcp_bridge/orchestrator/enums.py +11 -0
- mcp_bridge/orchestrator/router.py +165 -0
- mcp_bridge/orchestrator/state.py +32 -0
- mcp_bridge/orchestrator/visualization.py +14 -0
- mcp_bridge/orchestrator/wisdom.py +34 -0
- mcp_bridge/prompts/__init__.py +1 -8
- mcp_bridge/prompts/dewey.py +1 -1
- mcp_bridge/prompts/planner.py +2 -4
- mcp_bridge/prompts/stravinsky.py +53 -31
- mcp_bridge/proxy/__init__.py +0 -0
- mcp_bridge/proxy/client.py +70 -0
- mcp_bridge/proxy/model_server.py +157 -0
- mcp_bridge/routing/__init__.py +43 -0
- mcp_bridge/routing/config.py +250 -0
- mcp_bridge/routing/model_tiers.py +135 -0
- mcp_bridge/routing/provider_state.py +261 -0
- mcp_bridge/routing/task_classifier.py +190 -0
- mcp_bridge/server.py +363 -34
- mcp_bridge/server_tools.py +298 -6
- mcp_bridge/tools/__init__.py +19 -8
- mcp_bridge/tools/agent_manager.py +549 -799
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +54 -51
- mcp_bridge/tools/continuous_loop.py +0 -1
- mcp_bridge/tools/dashboard.py +19 -0
- mcp_bridge/tools/find_code.py +296 -0
- mcp_bridge/tools/init.py +1 -0
- mcp_bridge/tools/list_directory.py +42 -0
- mcp_bridge/tools/lsp/__init__.py +8 -8
- mcp_bridge/tools/lsp/manager.py +51 -28
- mcp_bridge/tools/lsp/tools.py +98 -65
- mcp_bridge/tools/model_invoke.py +1047 -152
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +132 -49
- mcp_bridge/tools/read_file.py +84 -0
- mcp_bridge/tools/replace.py +45 -0
- mcp_bridge/tools/run_shell_command.py +38 -0
- mcp_bridge/tools/search_enhancements.py +347 -0
- mcp_bridge/tools/semantic_search.py +677 -92
- mcp_bridge/tools/session_manager.py +0 -2
- mcp_bridge/tools/skill_loader.py +0 -1
- mcp_bridge/tools/task_runner.py +5 -7
- mcp_bridge/tools/templates.py +3 -3
- mcp_bridge/tools/tool_search.py +331 -0
- mcp_bridge/tools/write_file.py +29 -0
- mcp_bridge/update_manager.py +33 -37
- mcp_bridge/update_manager_pypi.py +6 -8
- mcp_bridge/utils/cache.py +82 -0
- mcp_bridge/utils/process.py +71 -0
- mcp_bridge/utils/session_state.py +51 -0
- mcp_bridge/utils/truncation.py +76 -0
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/METADATA +84 -35
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
- stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
- stravinsky_claude_assets/agents/HOOKS.md +437 -0
- stravinsky_claude_assets/agents/code-reviewer.md +210 -0
- stravinsky_claude_assets/agents/comment_checker.md +580 -0
- stravinsky_claude_assets/agents/debugger.md +254 -0
- stravinsky_claude_assets/agents/delphi.md +495 -0
- stravinsky_claude_assets/agents/dewey.md +248 -0
- stravinsky_claude_assets/agents/explore.md +1198 -0
- stravinsky_claude_assets/agents/frontend.md +472 -0
- stravinsky_claude_assets/agents/implementation-lead.md +164 -0
- stravinsky_claude_assets/agents/momus.md +464 -0
- stravinsky_claude_assets/agents/research-lead.md +141 -0
- stravinsky_claude_assets/agents/stravinsky.md +730 -0
- stravinsky_claude_assets/commands/delphi.md +9 -0
- stravinsky_claude_assets/commands/dewey.md +54 -0
- stravinsky_claude_assets/commands/git-master.md +112 -0
- stravinsky_claude_assets/commands/index.md +49 -0
- stravinsky_claude_assets/commands/publish.md +86 -0
- stravinsky_claude_assets/commands/review.md +73 -0
- stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
- stravinsky_claude_assets/commands/str/agent_list.md +56 -0
- stravinsky_claude_assets/commands/str/agent_output.md +92 -0
- stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
- stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
- stravinsky_claude_assets/commands/str/cancel.md +51 -0
- stravinsky_claude_assets/commands/str/clean.md +97 -0
- stravinsky_claude_assets/commands/str/continue.md +38 -0
- stravinsky_claude_assets/commands/str/index.md +199 -0
- stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
- stravinsky_claude_assets/commands/str/search.md +205 -0
- stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
- stravinsky_claude_assets/commands/str/stats.md +71 -0
- stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
- stravinsky_claude_assets/commands/str/unwatch.md +42 -0
- stravinsky_claude_assets/commands/str/watch.md +45 -0
- stravinsky_claude_assets/commands/strav.md +53 -0
- stravinsky_claude_assets/commands/stravinsky.md +292 -0
- stravinsky_claude_assets/commands/verify.md +60 -0
- stravinsky_claude_assets/commands/version.md +5 -0
- stravinsky_claude_assets/hooks/README.md +248 -0
- stravinsky_claude_assets/hooks/comment_checker.py +193 -0
- stravinsky_claude_assets/hooks/context.py +38 -0
- stravinsky_claude_assets/hooks/context_monitor.py +153 -0
- stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
- stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
- stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
- stravinsky_claude_assets/hooks/notification_hook.py +103 -0
- stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
- stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
- stravinsky_claude_assets/hooks/pre_compact.py +123 -0
- stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
- stravinsky_claude_assets/hooks/session_recovery.py +263 -0
- stravinsky_claude_assets/hooks/stop_hook.py +89 -0
- stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
- stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
- stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
- stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
- stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
- stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
- stravinsky_claude_assets/hooks/truncator.py +23 -0
- stravinsky_claude_assets/rules/deployment_safety.md +51 -0
- stravinsky_claude_assets/rules/integration_wiring.md +89 -0
- stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
- stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
- stravinsky_claude_assets/settings.json +152 -0
- stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
- stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
- stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
- stravinsky_claude_assets/task_dependencies.json +34 -0
- stravinsky-0.4.18.dist-info/RECORD +0 -88
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
mcp_bridge/cli/session_report.py
CHANGED
|
@@ -8,8 +8,6 @@ Optionally uses Gemini for session summarization.
|
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
10
|
import json
|
|
11
|
-
import os
|
|
12
|
-
import re
|
|
13
11
|
import sys
|
|
14
12
|
from collections import Counter
|
|
15
13
|
from datetime import datetime
|
|
@@ -23,7 +21,6 @@ from rich.table import Table
|
|
|
23
21
|
from rich.text import Text
|
|
24
22
|
from rich.tree import Tree
|
|
25
23
|
|
|
26
|
-
|
|
27
24
|
console = Console()
|
|
28
25
|
|
|
29
26
|
|
mcp_bridge/config/__init__.py
CHANGED
mcp_bridge/config/hook_config.py
CHANGED
|
@@ -5,10 +5,8 @@ Provides batteries-included defaults with user-configurable overrides.
|
|
|
5
5
|
Users can disable specific hooks via ~/.stravinsky/disable_hooks.txt
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import os
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Set, Optional
|
|
11
8
|
import logging
|
|
9
|
+
from pathlib import Path
|
|
12
10
|
|
|
13
11
|
logger = logging.getLogger(__name__)
|
|
14
12
|
|
|
@@ -20,7 +18,7 @@ DISABLE_HOOKS_PATHS = [
|
|
|
20
18
|
]
|
|
21
19
|
|
|
22
20
|
|
|
23
|
-
def get_disabled_hooks() ->
|
|
21
|
+
def get_disabled_hooks() -> set[str]:
|
|
24
22
|
"""
|
|
25
23
|
Load disabled hooks from config files.
|
|
26
24
|
|
|
@@ -74,7 +72,7 @@ def get_hook_config_path() -> Path:
|
|
|
74
72
|
return config_dir
|
|
75
73
|
|
|
76
74
|
|
|
77
|
-
def create_sample_disable_hooks() ->
|
|
75
|
+
def create_sample_disable_hooks() -> Path | None:
|
|
78
76
|
"""
|
|
79
77
|
Create a sample disable_hooks.txt file with documentation.
|
|
80
78
|
|
mcp_bridge/config/rate_limits.py
CHANGED
|
@@ -17,14 +17,13 @@ Configuration file: ~/.stravinsky/config.json
|
|
|
17
17
|
}
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
import asyncio
|
|
21
20
|
import json
|
|
21
|
+
import logging
|
|
22
|
+
import sys
|
|
22
23
|
import threading
|
|
24
|
+
import time
|
|
25
|
+
from collections import defaultdict, deque
|
|
23
26
|
from pathlib import Path
|
|
24
|
-
from typing import Dict, Optional
|
|
25
|
-
from collections import defaultdict
|
|
26
|
-
from datetime import datetime
|
|
27
|
-
import logging
|
|
28
27
|
|
|
29
28
|
logger = logging.getLogger(__name__)
|
|
30
29
|
|
|
@@ -56,13 +55,13 @@ class RateLimiter:
|
|
|
56
55
|
"""
|
|
57
56
|
|
|
58
57
|
def __init__(self):
|
|
59
|
-
self._semaphores:
|
|
58
|
+
self._semaphores: dict[str, threading.Semaphore] = {}
|
|
60
59
|
self._lock = threading.Lock()
|
|
61
60
|
self._limits = self._load_limits()
|
|
62
|
-
self._active_counts:
|
|
63
|
-
self._queue_counts:
|
|
61
|
+
self._active_counts: dict[str, int] = defaultdict(int)
|
|
62
|
+
self._queue_counts: dict[str, int] = defaultdict(int)
|
|
64
63
|
|
|
65
|
-
def _load_limits(self) ->
|
|
64
|
+
def _load_limits(self) -> dict[str, int]:
|
|
66
65
|
"""Load rate limits from config file or use defaults."""
|
|
67
66
|
limits = DEFAULT_RATE_LIMITS.copy()
|
|
68
67
|
|
|
@@ -73,7 +72,7 @@ class RateLimiter:
|
|
|
73
72
|
if "rate_limits" in config:
|
|
74
73
|
limits.update(config["rate_limits"])
|
|
75
74
|
logger.info(f"[RateLimiter] Loaded custom limits from {CONFIG_FILE}")
|
|
76
|
-
except (json.JSONDecodeError
|
|
75
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
77
76
|
logger.warning(f"[RateLimiter] Failed to load config: {e}")
|
|
78
77
|
|
|
79
78
|
return limits
|
|
@@ -150,7 +149,7 @@ class RateLimiter:
|
|
|
150
149
|
semaphore.release()
|
|
151
150
|
logger.debug(f"[RateLimiter] Released slot for {normalized}")
|
|
152
151
|
|
|
153
|
-
def get_status(self) ->
|
|
152
|
+
def get_status(self) -> dict[str, dict[str, int]]:
|
|
154
153
|
"""Get current rate limiter status."""
|
|
155
154
|
with self._lock:
|
|
156
155
|
return {
|
|
@@ -162,7 +161,7 @@ class RateLimiter:
|
|
|
162
161
|
for model in set(list(self._active_counts.keys()) + list(self._queue_counts.keys()))
|
|
163
162
|
}
|
|
164
163
|
|
|
165
|
-
def update_limits(self, new_limits:
|
|
164
|
+
def update_limits(self, new_limits: dict[str, int]):
|
|
166
165
|
"""
|
|
167
166
|
Update rate limits dynamically.
|
|
168
167
|
|
|
@@ -196,7 +195,7 @@ class RateLimitContext:
|
|
|
196
195
|
|
|
197
196
|
|
|
198
197
|
# Global rate limiter instance
|
|
199
|
-
_rate_limiter:
|
|
198
|
+
_rate_limiter: RateLimiter | None = None
|
|
200
199
|
_rate_limiter_lock = threading.Lock()
|
|
201
200
|
|
|
202
201
|
|
|
@@ -220,3 +219,99 @@ def rate_limited(model: str, timeout: float = 60.0) -> RateLimitContext:
|
|
|
220
219
|
pass
|
|
221
220
|
"""
|
|
222
221
|
return RateLimitContext(get_rate_limiter(), model, timeout)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TimeWindowRateLimiter:
|
|
225
|
+
"""
|
|
226
|
+
Time-window rate limiter (30 requests/minute) with user-visible feedback.
|
|
227
|
+
|
|
228
|
+
Implements sliding window algorithm for accurate rate limiting.
|
|
229
|
+
Complements the existing semaphore-based concurrency limiter.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(self, calls: int = 30, period: int = 60):
|
|
233
|
+
"""
|
|
234
|
+
Initialize time-window rate limiter.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
calls: Maximum number of calls per period (default: 30)
|
|
238
|
+
period: Time period in seconds (default: 60)
|
|
239
|
+
"""
|
|
240
|
+
self.calls = calls
|
|
241
|
+
self.period = period
|
|
242
|
+
self._timestamps: deque = deque()
|
|
243
|
+
self._lock = threading.Lock()
|
|
244
|
+
|
|
245
|
+
def acquire_visible(self, provider: str, auth_mode: str) -> float:
|
|
246
|
+
"""
|
|
247
|
+
Acquire slot with user-visible feedback.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
provider: Provider name for logging (e.g., "GEMINI", "OPENAI")
|
|
251
|
+
auth_mode: Authentication method (e.g., "OAuth", "API key")
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
wait_time: Time to wait in seconds (0 if no wait needed)
|
|
255
|
+
|
|
256
|
+
Note:
|
|
257
|
+
This method prints status to stderr for user visibility.
|
|
258
|
+
"""
|
|
259
|
+
with self._lock:
|
|
260
|
+
now = time.time()
|
|
261
|
+
|
|
262
|
+
# Clean old timestamps (sliding window)
|
|
263
|
+
while self._timestamps and self._timestamps[0] < now - self.period:
|
|
264
|
+
self._timestamps.popleft()
|
|
265
|
+
|
|
266
|
+
current = len(self._timestamps)
|
|
267
|
+
|
|
268
|
+
if current < self.calls:
|
|
269
|
+
self._timestamps.append(now)
|
|
270
|
+
# Show current count in stderr for visibility
|
|
271
|
+
print(
|
|
272
|
+
f"🔮 {provider} ({auth_mode}): {current + 1}/{self.calls} this minute",
|
|
273
|
+
file=sys.stderr,
|
|
274
|
+
)
|
|
275
|
+
return 0.0
|
|
276
|
+
|
|
277
|
+
# Rate limit hit - calculate wait time
|
|
278
|
+
wait_time = self._timestamps[0] + self.period - now
|
|
279
|
+
print(
|
|
280
|
+
f"⏳ RATE LIMIT ({provider}): {self.calls}/min hit. Waiting {wait_time:.1f}s...",
|
|
281
|
+
file=sys.stderr,
|
|
282
|
+
)
|
|
283
|
+
logger.warning(
|
|
284
|
+
f"[RateLimit] {provider} hit {self.calls}/min limit. Waiting {wait_time:.1f}s ({auth_mode})"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return wait_time
|
|
288
|
+
|
|
289
|
+
def get_stats(self) -> dict[str, int]:
|
|
290
|
+
"""Get current rate limiter statistics."""
|
|
291
|
+
with self._lock:
|
|
292
|
+
now = time.time()
|
|
293
|
+
# Clean old timestamps
|
|
294
|
+
while self._timestamps and self._timestamps[0] < now - self.period:
|
|
295
|
+
self._timestamps.popleft()
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
"current_count": len(self._timestamps),
|
|
299
|
+
"limit": self.calls,
|
|
300
|
+
"period_seconds": self.period,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# Global time-window rate limiter for Gemini (30 req/min)
|
|
305
|
+
_gemini_time_limiter: TimeWindowRateLimiter | None = None
|
|
306
|
+
_gemini_time_limiter_lock = threading.Lock()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def get_gemini_time_limiter() -> TimeWindowRateLimiter:
|
|
310
|
+
"""Get or create the global Gemini time-window rate limiter."""
|
|
311
|
+
global _gemini_time_limiter
|
|
312
|
+
if _gemini_time_limiter is None:
|
|
313
|
+
with _gemini_time_limiter_lock:
|
|
314
|
+
if _gemini_time_limiter is None:
|
|
315
|
+
_gemini_time_limiter = TimeWindowRateLimiter(calls=30, period=60)
|
|
316
|
+
logger.info("[TimeWindowRateLimiter] Created Gemini rate limiter (30 req/min)")
|
|
317
|
+
return _gemini_time_limiter
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"type": "command",
|
|
48
48
|
"command": "python3 ~/.claude/hooks/stravinsky_mode.py",
|
|
49
49
|
"description": "Hard blocking of direct tools in stravinsky mode"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "python3 ~/.claude/hooks/pre_tool/agent_spawn_validator.py",
|
|
54
|
+
"description": "Blocks sequential tool usage when parallel delegation is required"
|
|
50
55
|
}
|
|
51
56
|
]
|
|
52
57
|
}
|
|
@@ -111,6 +116,11 @@
|
|
|
111
116
|
"type": "command",
|
|
112
117
|
"command": "python3 ~/.claude/hooks/todo_delegation.py",
|
|
113
118
|
"description": "Parallel execution enforcer after TodoWrite"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"type": "command",
|
|
122
|
+
"command": "python3 ~/.claude/hooks/post_tool/parallel_validation.py",
|
|
123
|
+
"description": "Tracks pending tasks for hard enforcement"
|
|
114
124
|
}
|
|
115
125
|
]
|
|
116
126
|
}
|
|
@@ -139,7 +149,9 @@
|
|
|
139
149
|
"execution_control": {
|
|
140
150
|
"parallel_execution.py": "Detects implementation tasks and injects parallel execution instructions before response generation. Activates stravinsky mode on /stravinsky invocation.",
|
|
141
151
|
"stravinsky_mode.py": "Blocks native file tools (Read, Grep, Bash, Edit) when stravinsky mode is active, forcing Task delegation.",
|
|
142
|
-
"todo_delegation.py": "Hard blocks response completion after TodoWrite if 2+ pending items exist without spawning Task agents."
|
|
152
|
+
"todo_delegation.py": "Hard blocks response completion after TodoWrite if 2+ pending items exist without spawning Task agents.",
|
|
153
|
+
"parallel_validation.py": "Tracks pending tasks count after TodoWrite to enable hard enforcement state.",
|
|
154
|
+
"agent_spawn_validator.py": "Blocks sequential tool usage (Read, Grep, etc.) if parallel delegation is required and enabled."
|
|
143
155
|
},
|
|
144
156
|
"context_management": {
|
|
145
157
|
"context.py": "Auto-injects CLAUDE.md, README.md, or AGENTS.md content into prompts for project-specific context.",
|
|
@@ -166,10 +178,11 @@
|
|
|
166
178
|
"state_files": {
|
|
167
179
|
"~/.stravinsky_mode": "Marker file indicating stravinsky orchestrator mode is active (enables hard blocking)",
|
|
168
180
|
"~/.claude/state/compaction.jsonl": "Audit log of context compaction events with preserved items",
|
|
169
|
-
".claude/todo_state.json": "Cached todo state for continuation enforcement"
|
|
181
|
+
".claude/todo_state.json": "Cached todo state for continuation enforcement",
|
|
182
|
+
".claude/parallel_state.json": "Transient state tracking pending tasks for hard enforcement"
|
|
170
183
|
},
|
|
171
184
|
|
|
172
|
-
"version": "0.2.
|
|
185
|
+
"version": "0.2.64",
|
|
173
186
|
"package": "stravinsky",
|
|
174
187
|
"documentation": "https://github.com/GratefulDave/stravinsky"
|
|
175
|
-
}
|
|
188
|
+
}
|
mcp_bridge/hooks/__init__.py
CHANGED
|
@@ -115,11 +115,21 @@ __all__ = [
|
|
|
115
115
|
|
|
116
116
|
def initialize_hooks():
|
|
117
117
|
"""Initialize and register all hooks with the HookManager."""
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
from .delegation_policy import DelegationReminderPolicy
|
|
119
|
+
from .edit_recovery_policy import EditRecoveryPolicy
|
|
120
|
+
from .manager import get_hook_manager
|
|
121
|
+
from .parallel_enforcement_policy import ParallelEnforcementPolicy
|
|
122
|
+
from .truncation_policy import TruncationPolicy
|
|
121
123
|
|
|
124
|
+
manager = get_hook_manager()
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
# Register unified policies
|
|
127
|
+
manager.register_policy(TruncationPolicy())
|
|
128
|
+
manager.register_policy(DelegationReminderPolicy())
|
|
129
|
+
manager.register_policy(EditRecoveryPolicy())
|
|
130
|
+
manager.register_policy(ParallelEnforcementPolicy())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__version__ = "0.4.60"
|
|
124
134
|
__author__ = "David Andrews"
|
|
125
135
|
__description__ = "Claude Code hooks for Stravinsky MCP parallel execution"
|
|
@@ -6,7 +6,7 @@ suggests using background agents for more comprehensive results.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
@@ -28,8 +28,8 @@ SEARCH_TOOLS = {"grep", "glob", "rg", "find", "Grep", "Glob", "grep_search", "gl
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
async def agent_reminder_hook(
|
|
31
|
-
tool_name: str, arguments:
|
|
32
|
-
) ->
|
|
31
|
+
tool_name: str, arguments: dict[str, Any], output: str
|
|
32
|
+
) -> str | None:
|
|
33
33
|
"""
|
|
34
34
|
Post-tool call hook that suggests background agents after direct search tool usage.
|
|
35
35
|
"""
|
|
@@ -51,7 +51,7 @@ async def agent_reminder_hook(
|
|
|
51
51
|
return None
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def _extract_search_context(arguments:
|
|
54
|
+
def _extract_search_context(arguments: dict[str, Any]) -> str:
|
|
55
55
|
"""Extract search context from tool arguments."""
|
|
56
56
|
for key in ("pattern", "query", "search", "name", "path"):
|
|
57
57
|
if key in arguments:
|
|
@@ -11,7 +11,7 @@ Detects and auto-processes slash commands in user input:
|
|
|
11
11
|
import logging
|
|
12
12
|
import re
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
SLASH_COMMAND_PATTERN = re.compile(r'(?:^|(?<=\s))\/([a-zA-Z][a-zA-Z0-9_-]*)\b', re.MULTILINE)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def extract_slash_commands(text: str) ->
|
|
22
|
+
def extract_slash_commands(text: str) -> list[str]:
|
|
23
23
|
"""
|
|
24
24
|
Extract all slash command names from text.
|
|
25
25
|
|
|
@@ -40,7 +40,7 @@ def extract_slash_commands(text: str) -> List[str]:
|
|
|
40
40
|
return unique
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def load_skill_content(command_name: str, project_path:
|
|
43
|
+
def load_skill_content(command_name: str, project_path: str | None = None) -> tuple[str, str] | None:
|
|
44
44
|
"""
|
|
45
45
|
Load skill content by command name.
|
|
46
46
|
|
|
@@ -92,7 +92,7 @@ def load_skill_content(command_name: str, project_path: Optional[str] = None) ->
|
|
|
92
92
|
return None
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
def get_project_path_from_prompt(prompt: str) ->
|
|
95
|
+
def get_project_path_from_prompt(prompt: str) -> str | None:
|
|
96
96
|
"""
|
|
97
97
|
Try to extract project path from prompt context.
|
|
98
98
|
Looks for common patterns that indicate the working directory.
|
|
@@ -126,7 +126,7 @@ SKILL_NOT_FOUND_WARNING = """
|
|
|
126
126
|
"""
|
|
127
127
|
|
|
128
128
|
|
|
129
|
-
async def auto_slash_command_hook(params:
|
|
129
|
+
async def auto_slash_command_hook(params: dict[str, Any]) -> dict[str, Any] | None:
|
|
130
130
|
"""
|
|
131
131
|
Pre-model invoke hook that detects slash commands and injects skill content.
|
|
132
132
|
|
|
@@ -3,14 +3,14 @@ Thinking budget optimizer hook.
|
|
|
3
3
|
Analyzes prompt complexity and adjusts thinking_budget for models that support it.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
REASONING_KEYWORDS = [
|
|
9
9
|
"architect", "design", "refactor", "debug", "complex", "optimize",
|
|
10
10
|
"summarize", "analyze", "explain", "why", "review", "strangler"
|
|
11
11
|
]
|
|
12
12
|
|
|
13
|
-
async def budget_optimizer_hook(params:
|
|
13
|
+
async def budget_optimizer_hook(params: dict[str, Any]) -> dict[str, Any] | None:
|
|
14
14
|
"""
|
|
15
15
|
Adjusts the thinking_budget based on presence of reasoning-heavy keywords.
|
|
16
16
|
"""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook for Claude rate limit detection.
|
|
4
|
+
|
|
5
|
+
Monitors model invocation responses for Claude-specific rate limit indicators
|
|
6
|
+
and updates the provider state tracker accordingly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
# Add parent directory to path for imports
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from routing import get_provider_tracker
|
|
19
|
+
except ImportError:
|
|
20
|
+
get_provider_tracker = None # type: ignore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Claude rate limit indicators
|
|
26
|
+
CLAUDE_RATE_LIMIT_PATTERNS = [
|
|
27
|
+
"rate limit",
|
|
28
|
+
"rate_limit_error",
|
|
29
|
+
"too many requests",
|
|
30
|
+
"429",
|
|
31
|
+
"quota exceeded",
|
|
32
|
+
"rate-limited",
|
|
33
|
+
"overloaded_error",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def detect_claude_rate_limit(tool_result: str | dict) -> bool:
|
|
38
|
+
"""
|
|
39
|
+
Detect if a tool result indicates Claude rate limiting.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
tool_result: Tool result string or dict
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if rate limit detected, False otherwise
|
|
46
|
+
"""
|
|
47
|
+
# Convert result to searchable string
|
|
48
|
+
if isinstance(tool_result, dict):
|
|
49
|
+
search_text = json.dumps(tool_result).lower()
|
|
50
|
+
else:
|
|
51
|
+
search_text = str(tool_result).lower()
|
|
52
|
+
|
|
53
|
+
# Check for rate limit patterns
|
|
54
|
+
for pattern in CLAUDE_RATE_LIMIT_PATTERNS:
|
|
55
|
+
if pattern in search_text:
|
|
56
|
+
logger.info(f"[ClaudeLimitsHook] Detected rate limit pattern: {pattern}")
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main() -> None:
|
|
63
|
+
"""Process PostToolUse hook event."""
|
|
64
|
+
try:
|
|
65
|
+
# Read hook input from stdin
|
|
66
|
+
hook_input = json.loads(sys.stdin.read())
|
|
67
|
+
|
|
68
|
+
tool_name = hook_input.get("tool_name", "")
|
|
69
|
+
tool_result = hook_input.get("tool_result", "")
|
|
70
|
+
|
|
71
|
+
# Only monitor invoke tools (Claude uses default Claude models)
|
|
72
|
+
# We're looking for rate limits from the main Claude context
|
|
73
|
+
# This is different from invoke_openai/invoke_gemini which are explicit
|
|
74
|
+
if not any(
|
|
75
|
+
keyword in tool_name.lower() for keyword in ["invoke", "chat", "generate", "complete"]
|
|
76
|
+
):
|
|
77
|
+
# Not a model invocation tool - skip
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
|
|
80
|
+
# Check for rate limit indicators
|
|
81
|
+
if detect_claude_rate_limit(tool_result):
|
|
82
|
+
logger.warning("[ClaudeLimitsHook] Claude rate limit detected")
|
|
83
|
+
|
|
84
|
+
if get_provider_tracker and callable(get_provider_tracker):
|
|
85
|
+
tracker = get_provider_tracker()
|
|
86
|
+
if tracker:
|
|
87
|
+
tracker.mark_rate_limited("claude", duration=300, reason="Claude rate limit")
|
|
88
|
+
logger.info("[ClaudeLimitsHook] Marked Claude as rate-limited (300s cooldown)")
|
|
89
|
+
|
|
90
|
+
print(
|
|
91
|
+
"\n⚠️ Claude Rate Limit Detected\n"
|
|
92
|
+
"→ Routing future requests to OpenAI/Gemini for 5 minutes\n",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
logger.debug(
|
|
97
|
+
"[ClaudeLimitsHook] Provider tracker not available, skipping state update"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
sys.exit(0)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
# Log error but don't block
|
|
104
|
+
logger.error(f"[ClaudeLimitsHook] Error: {e}", exc_info=True)
|
|
105
|
+
print(f"[ClaudeLimitsHook] Warning: {e}", file=sys.stderr)
|
|
106
|
+
sys.exit(0) # Exit 0 to not block execution
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
# Configure logging
|
|
111
|
+
logging.basicConfig(
|
|
112
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
113
|
+
)
|
|
114
|
+
main()
|
|
@@ -6,8 +6,7 @@ Code should be self-documenting; excessive comments indicate AI slop.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
import
|
|
10
|
-
from typing import Any, Dict, Optional
|
|
9
|
+
from typing import Any
|
|
11
10
|
|
|
12
11
|
logger = logging.getLogger(__name__)
|
|
13
12
|
|
|
@@ -53,8 +52,8 @@ CODE_EXTENSIONS = {
|
|
|
53
52
|
|
|
54
53
|
|
|
55
54
|
async def comment_checker_hook(
|
|
56
|
-
tool_name: str, arguments:
|
|
57
|
-
) ->
|
|
55
|
+
tool_name: str, arguments: dict[str, Any], output: str
|
|
56
|
+
) -> str | None:
|
|
58
57
|
"""
|
|
59
58
|
Post-tool call hook that checks for excessive comments in code edits.
|
|
60
59
|
"""
|
mcp_bridge/hooks/compaction.py
CHANGED
|
@@ -3,7 +3,7 @@ Preemptive context compaction hook.
|
|
|
3
3
|
Monitors context size and injects optimization reminders.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
THRESHOLD_CHARS = 100000 # Roughly 25k-30k tokens for typical LLM text
|
|
9
9
|
|
|
@@ -17,7 +17,7 @@ COMPACTION_REMINDER = """
|
|
|
17
17
|
> 4. Keep your next responses concise and focused only on the current sub-task.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
async def context_compaction_hook(params:
|
|
20
|
+
async def context_compaction_hook(params: dict[str, Any]) -> dict[str, Any] | None:
|
|
21
21
|
"""
|
|
22
22
|
Checks prompt length and injects a compaction reminder if it's too large.
|
|
23
23
|
"""
|
mcp_bridge/hooks/context.py
CHANGED
|
@@ -7,7 +7,7 @@ At 85%, suggests compaction before hitting hard limits.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -32,7 +32,7 @@ CONTEXT_THRESHOLD_WARNING = 0.85
|
|
|
32
32
|
ESTIMATED_MAX_TOKENS = 200000
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
async def context_monitor_hook(params:
|
|
35
|
+
async def context_monitor_hook(params: dict[str, Any]) -> dict[str, Any] | None:
|
|
36
36
|
"""
|
|
37
37
|
Pre-model invoke hook that monitors context window usage.
|
|
38
38
|
"""
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..utils.session_state import get_current_session_id, update_session_state
|
|
6
|
+
from .events import EventType, HookPolicy, PolicyResult, ToolCallEvent
|
|
7
|
+
|
|
8
|
+
# Check if stravinsky mode is active (hard blocking enabled)
|
|
9
|
+
STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_stravinsky_mode():
|
|
13
|
+
"""Check if hard blocking mode is active."""
|
|
14
|
+
return STRAVINSKY_MODE_FILE.exists()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DelegationReminderPolicy(HookPolicy):
|
|
18
|
+
"""
|
|
19
|
+
Policy for TodoWrite: CRITICAL parallel execution enforcer.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def event_type(self) -> EventType:
|
|
24
|
+
return EventType.POST_TOOL_CALL
|
|
25
|
+
|
|
26
|
+
async def evaluate(self, event: ToolCallEvent) -> PolicyResult:
|
|
27
|
+
if event.tool_name != "TodoWrite":
|
|
28
|
+
return PolicyResult(modified_data=event.output)
|
|
29
|
+
|
|
30
|
+
todos = event.arguments.get("todos", [])
|
|
31
|
+
pending_count = sum(1 for t in todos if t.get("status") == "pending")
|
|
32
|
+
|
|
33
|
+
# Update session state
|
|
34
|
+
session_id = event.metadata.get("session_id") or get_current_session_id()
|
|
35
|
+
update_session_state(
|
|
36
|
+
{
|
|
37
|
+
"last_todo_write_at": time.time(),
|
|
38
|
+
"pending_todo_count": pending_count,
|
|
39
|
+
},
|
|
40
|
+
session_id=session_id,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if pending_count < 2:
|
|
44
|
+
return PolicyResult(modified_data=event.output)
|
|
45
|
+
|
|
46
|
+
stravinsky_active = is_stravinsky_mode()
|
|
47
|
+
|
|
48
|
+
mode_warning = ""
|
|
49
|
+
if stravinsky_active:
|
|
50
|
+
mode_warning = """
|
|
51
|
+
⚠️ STRAVINSKY MODE ACTIVE - Direct tools (Read, Grep, Bash) are BLOCKED.
|
|
52
|
+
You MUST use Task(subagent_type="explore", ...) for ALL file operations.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
error_message = f"""
|
|
56
|
+
╔══════════════════════════════════════════════════════════════════════════╗
|
|
57
|
+
║ 🚨 PARALLEL DELEGATION REQUIRED 🚨 ║
|
|
58
|
+
╠══════════════════════════════════════════════════════════════════════════╣
|
|
59
|
+
║ ║
|
|
60
|
+
║ TodoWrite created {pending_count} pending items. ║
|
|
61
|
+
║ {mode_warning.strip()} ║
|
|
62
|
+
║ ║
|
|
63
|
+
║ You MUST spawn Task agents for ALL independent TODOs in this response. ║
|
|
64
|
+
║ ║
|
|
65
|
+
╠══════════════════════════════════════════════════════════════════════════╣
|
|
66
|
+
║ REQUIRED PATTERN: ║
|
|
67
|
+
║ Task(subagent_type="explore", prompt="TODO 1...", run_in_background=t) ║
|
|
68
|
+
║ Task(subagent_type="explore", prompt="TODO 2...", run_in_background=t) ║
|
|
69
|
+
║ ... ║
|
|
70
|
+
╚══════════════════════════════════════════════════════════════════════════╝
|
|
71
|
+
"""
|
|
72
|
+
# In PostToolUse, we often want to APPEND the reminder to the output
|
|
73
|
+
new_output = (event.output or "") + "\n" + error_message
|
|
74
|
+
|
|
75
|
+
return PolicyResult(
|
|
76
|
+
modified_data=new_output,
|
|
77
|
+
message=error_message, # For native hooks, we might just print the message
|
|
78
|
+
should_block=stravinsky_active,
|
|
79
|
+
exit_code=2 if stravinsky_active else 1,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
policy = DelegationReminderPolicy()
|
|
85
|
+
policy.run_as_native()
|