stravinsky 0.2.52__py3-none-any.whl → 0.4.18__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/token_store.py +113 -11
- mcp_bridge/cli/__init__.py +6 -0
- mcp_bridge/cli/install_hooks.py +1265 -0
- mcp_bridge/cli/session_report.py +585 -0
- mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
- mcp_bridge/config/README.md +276 -0
- mcp_bridge/config/hook_config.py +249 -0
- mcp_bridge/config/hooks_manifest.json +138 -0
- mcp_bridge/config/rate_limits.py +222 -0
- mcp_bridge/config/skills_manifest.json +128 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
- mcp_bridge/hooks/README.md +215 -0
- mcp_bridge/hooks/__init__.py +119 -60
- mcp_bridge/hooks/edit_recovery.py +42 -37
- mcp_bridge/hooks/git_noninteractive.py +89 -0
- mcp_bridge/hooks/keyword_detector.py +30 -0
- mcp_bridge/hooks/manager.py +8 -0
- mcp_bridge/hooks/notification_hook.py +103 -0
- mcp_bridge/hooks/parallel_execution.py +111 -0
- mcp_bridge/hooks/pre_compact.py +82 -183
- mcp_bridge/hooks/rules_injector.py +507 -0
- mcp_bridge/hooks/session_notifier.py +125 -0
- mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
- mcp_bridge/hooks/subagent_stop.py +98 -0
- mcp_bridge/hooks/task_validator.py +73 -0
- mcp_bridge/hooks/tmux_manager.py +141 -0
- mcp_bridge/hooks/todo_continuation.py +90 -0
- mcp_bridge/hooks/todo_delegation.py +88 -0
- mcp_bridge/hooks/tool_messaging.py +267 -0
- mcp_bridge/hooks/truncator.py +21 -17
- mcp_bridge/notifications.py +151 -0
- mcp_bridge/prompts/multimodal.py +24 -3
- mcp_bridge/server.py +214 -49
- mcp_bridge/server_tools.py +445 -0
- mcp_bridge/tools/__init__.py +22 -18
- mcp_bridge/tools/agent_manager.py +220 -32
- mcp_bridge/tools/code_search.py +97 -11
- mcp_bridge/tools/lsp/__init__.py +7 -0
- mcp_bridge/tools/lsp/manager.py +448 -0
- mcp_bridge/tools/lsp/tools.py +637 -150
- mcp_bridge/tools/model_invoke.py +208 -106
- mcp_bridge/tools/query_classifier.py +323 -0
- mcp_bridge/tools/semantic_search.py +3042 -0
- mcp_bridge/tools/templates.py +32 -18
- mcp_bridge/update_manager.py +589 -0
- mcp_bridge/update_manager_pypi.py +299 -0
- stravinsky-0.4.18.dist-info/METADATA +468 -0
- stravinsky-0.4.18.dist-info/RECORD +88 -0
- stravinsky-0.4.18.dist-info/entry_points.txt +5 -0
- mcp_bridge/native_hooks/edit_recovery.py +0 -46
- mcp_bridge/native_hooks/todo_delegation.py +0 -54
- mcp_bridge/native_hooks/truncator.py +0 -23
- stravinsky-0.2.52.dist-info/METADATA +0 -204
- stravinsky-0.2.52.dist-info/RECORD +0 -63
- stravinsky-0.2.52.dist-info/entry_points.txt +0 -3
- /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
- {stravinsky-0.2.52.dist-info → stravinsky-0.4.18.dist-info}/WHEEL +0 -0
mcp_bridge/hooks/__init__.py
CHANGED
|
@@ -1,66 +1,125 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Hooks
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
Stravinsky Hooks - Claude Code Integration
|
|
3
|
+
|
|
4
|
+
This package contains all hook files for deep integration with Claude Code.
|
|
5
|
+
Hooks are Python scripts that intercept Claude Code events to enforce
|
|
6
|
+
parallel execution, stravinsky mode, and other workflow patterns.
|
|
7
|
+
|
|
8
|
+
## Available Hooks
|
|
9
|
+
|
|
10
|
+
### Core Execution Hooks
|
|
11
|
+
- `parallel_execution.py` - UserPromptSubmit: Pre-emptive parallel execution enforcement
|
|
12
|
+
- `stravinsky_mode.py` - PreToolUse: Hard blocking of direct tools (Read, Grep, Bash)
|
|
13
|
+
- `todo_delegation.py` - PostToolUse: Parallel execution enforcer after TodoWrite
|
|
14
|
+
|
|
15
|
+
### Context & State Hooks
|
|
16
|
+
- `context.py` - UserPromptSubmit: Auto-inject project context (CLAUDE.md, README.md)
|
|
17
|
+
- `todo_continuation.py` - UserPromptSubmit: Remind about incomplete todos
|
|
18
|
+
- `pre_compact.py` - PreCompact: Context preservation before compaction
|
|
19
|
+
|
|
20
|
+
### Tool Enhancement Hooks
|
|
21
|
+
- `tool_messaging.py` - PostToolUse: User-friendly tool/agent messaging
|
|
22
|
+
- `edit_recovery.py` - PostToolUse: Recovery guidance for failed Edit operations
|
|
23
|
+
- `truncator.py` - PostToolUse: Truncate long tool responses to prevent token overflow
|
|
24
|
+
|
|
25
|
+
### Agent Lifecycle Hooks
|
|
26
|
+
- `notification_hook.py` - Notification: Agent spawn messages
|
|
27
|
+
- `subagent_stop.py` - SubagentStop: Agent completion handling
|
|
28
|
+
|
|
29
|
+
## Installation for Claude Code
|
|
30
|
+
|
|
31
|
+
Copy the HOOKS_SETTINGS.json configuration to your project's .claude/settings.json:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# From PyPI package location
|
|
35
|
+
cp $(python -c "import mcp_bridge; print(mcp_bridge.__path__[0])")/hooks/HOOKS_SETTINGS.json .claude/settings.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or manually configure in .claude/settings.json (see HOOKS_SETTINGS.json for template).
|
|
39
|
+
|
|
40
|
+
## Hook Types
|
|
41
|
+
|
|
42
|
+
Claude Code supports these hook types:
|
|
43
|
+
- **UserPromptSubmit**: Fires before response generation
|
|
44
|
+
- **PreToolUse**: Fires before tool execution (can block with exit 2)
|
|
45
|
+
- **PostToolUse**: Fires after tool execution
|
|
46
|
+
- **Notification**: Fires on notification events
|
|
47
|
+
- **SubagentStop**: Fires when subagent completes
|
|
48
|
+
- **PreCompact**: Fires before context compaction
|
|
49
|
+
|
|
50
|
+
## Exit Codes
|
|
51
|
+
|
|
52
|
+
- `0` - Success (allow continuation)
|
|
53
|
+
- `1` - Warning (show but continue)
|
|
54
|
+
- `2` - Block (hard failure in stravinsky mode)
|
|
55
|
+
|
|
56
|
+
## Environment Variables
|
|
57
|
+
|
|
58
|
+
Hooks receive these environment variables from Claude Code:
|
|
59
|
+
- `CLAUDE_CWD` - Current working directory
|
|
60
|
+
- `CLAUDE_TOOL_NAME` - Tool being invoked (PreToolUse/PostToolUse)
|
|
61
|
+
- `CLAUDE_SESSION_ID` - Active session ID
|
|
62
|
+
|
|
63
|
+
## State Management
|
|
64
|
+
|
|
65
|
+
Stravinsky mode uses a marker file for state:
|
|
66
|
+
- `~/.stravinsky_mode` - Active when file exists
|
|
67
|
+
- Created by `/stravinsky` skill invocation
|
|
68
|
+
- Enables hard blocking of direct tools
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
These hooks are automatically installed with the Stravinsky MCP package.
|
|
73
|
+
To enable them in a Claude Code project:
|
|
74
|
+
|
|
75
|
+
1. Copy HOOKS_SETTINGS.json to .claude/settings.json
|
|
76
|
+
2. Adjust hook paths if needed (default assumes installed via PyPI)
|
|
77
|
+
3. Restart Claude Code or reload configuration
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
To test hooks locally:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Test parallel_execution hook
|
|
85
|
+
echo '{"prompt": "implement feature X"}' | python parallel_execution.py
|
|
86
|
+
|
|
87
|
+
# Test stravinsky_mode hook (requires marker file)
|
|
88
|
+
touch ~/.stravinsky_mode
|
|
89
|
+
echo '{"toolName": "Read", "params": {}}' | python stravinsky_mode.py
|
|
90
|
+
echo $? # Should be 2 (blocked)
|
|
91
|
+
rm ~/.stravinsky_mode
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Package Contents
|
|
11
95
|
"""
|
|
12
96
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
from .session_idle import session_idle_hook
|
|
31
|
-
from .pre_compact import pre_compact_hook
|
|
32
|
-
from .parallel_enforcer import parallel_enforcer_post_tool_hook
|
|
97
|
+
__all__ = [
|
|
98
|
+
# Core execution
|
|
99
|
+
"parallel_execution",
|
|
100
|
+
"stravinsky_mode",
|
|
101
|
+
"todo_delegation",
|
|
102
|
+
# Context & state
|
|
103
|
+
"context",
|
|
104
|
+
"todo_continuation",
|
|
105
|
+
"pre_compact",
|
|
106
|
+
# Tool enhancement
|
|
107
|
+
"tool_messaging",
|
|
108
|
+
"edit_recovery",
|
|
109
|
+
"truncator",
|
|
110
|
+
# Agent lifecycle
|
|
111
|
+
"notification_hook",
|
|
112
|
+
"subagent_stop",
|
|
113
|
+
]
|
|
33
114
|
|
|
34
115
|
|
|
35
116
|
def initialize_hooks():
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
manager.register_post_tool_call(parallel_enforcer_post_tool_hook) # NEW: Enforce parallel spawning
|
|
46
|
-
|
|
47
|
-
# Tier 2: Pre-model-invoke (context management)
|
|
48
|
-
manager.register_pre_model_invoke(directory_context_hook)
|
|
49
|
-
manager.register_pre_model_invoke(context_compaction_hook)
|
|
50
|
-
manager.register_pre_model_invoke(context_monitor_hook)
|
|
51
|
-
manager.register_pre_model_invoke(preemptive_compaction_hook)
|
|
52
|
-
manager.register_pre_model_invoke(empty_message_sanitizer_hook)
|
|
53
|
-
|
|
54
|
-
# Tier 3: Pre-model-invoke (performance optimization)
|
|
55
|
-
manager.register_pre_model_invoke(budget_optimizer_hook)
|
|
56
|
-
|
|
57
|
-
# Tier 4: Pre-model-invoke (behavior enforcement)
|
|
58
|
-
manager.register_pre_model_invoke(keyword_detector_hook)
|
|
59
|
-
manager.register_pre_model_invoke(todo_continuation_hook)
|
|
60
|
-
manager.register_pre_model_invoke(auto_slash_command_hook)
|
|
61
|
-
|
|
62
|
-
# Tier 5: Session lifecycle hooks (NEW)
|
|
63
|
-
manager.register_session_idle(session_idle_hook) # Stop hook - idle detection
|
|
64
|
-
manager.register_pre_compact(pre_compact_hook) # PreCompact - context preservation
|
|
65
|
-
|
|
66
|
-
return manager
|
|
117
|
+
"""Initialize and register all hooks with the HookManager."""
|
|
118
|
+
# Currently hooks are primarily external scripts or lazy-loaded.
|
|
119
|
+
# This entry point allows for future internal hook registration.
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__version__ = "0.2.63"
|
|
124
|
+
__author__ = "David Andrews"
|
|
125
|
+
__description__ = "Claude Code hooks for Stravinsky MCP parallel execution"
|
|
@@ -1,41 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
6
4
|
import re
|
|
7
|
-
from typing import Any, Dict, Optional
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
def main():
|
|
7
|
+
# Claude Code PostToolUse inputs via Environment Variables
|
|
8
|
+
tool_name = os.environ.get("CLAUDE_TOOL_NAME")
|
|
9
|
+
|
|
10
|
+
# We only care about Edit/MultiEdit
|
|
11
|
+
if tool_name not in ["Edit", "MultiEdit"]:
|
|
12
|
+
return
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
# Read from stdin (Claude Code passes the tool response via stdin for some hook types,
|
|
15
|
+
# but for PostToolUse it's often better to check the environment variable if available.
|
|
16
|
+
# Actually, the summary says input is a JSON payload.
|
|
17
|
+
try:
|
|
18
|
+
data = json.load(sys.stdin)
|
|
19
|
+
tool_response = data.get("tool_response", "")
|
|
20
|
+
except Exception:
|
|
21
|
+
# Fallback to direct string if not JSON
|
|
22
|
+
return
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
# Error patterns
|
|
25
|
+
error_patterns = [
|
|
26
|
+
r"oldString not found",
|
|
27
|
+
r"oldString matched multiple times",
|
|
28
|
+
r"line numbers out of range"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
recovery_needed = any(re.search(p, tool_response, re.IGNORECASE) for p in error_patterns)
|
|
32
|
+
|
|
33
|
+
if recovery_needed:
|
|
34
|
+
correction = (
|
|
35
|
+
"\n\n[SYSTEM RECOVERY] It appears the Edit tool failed to find the target string. "
|
|
36
|
+
"Please call 'Read' on the file again to verify the current content, "
|
|
37
|
+
"then ensure your 'oldString' is an EXACT match including all whitespace."
|
|
38
|
+
)
|
|
39
|
+
# For PostToolUse, stdout is captured and appended/replaces output
|
|
40
|
+
print(tool_response + correction)
|
|
41
|
+
else:
|
|
42
|
+
# No change
|
|
43
|
+
print(tool_response)
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Non-Interactive Environment Hook.
|
|
3
|
+
|
|
4
|
+
Prevents git interactive command hangs by prepending environment variables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Patterns for banned interactive git commands
|
|
15
|
+
BANNED_INTERACTIVE_PATTERNS = [
|
|
16
|
+
r"git\s+add\s+.*-p", # git add -p (patch mode)
|
|
17
|
+
r"git\s+add\s+.*--patch",
|
|
18
|
+
r"git\s+commit\s+.*-v", # git commit -v (verbose with diff)
|
|
19
|
+
r"git\s+rebase\s+.*-i", # git rebase -i (interactive)
|
|
20
|
+
r"git\s+rebase\s+.*--interactive",
|
|
21
|
+
r"git\s+add\s+.*-i", # git add -i (interactive)
|
|
22
|
+
r"git\s+add\s+.*--interactive",
|
|
23
|
+
r"git\s+checkout\s+.*-p", # git checkout -p (patch mode)
|
|
24
|
+
r"git\s+reset\s+.*-p", # git reset -p (patch mode)
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Environment variables to set for non-interactive git
|
|
28
|
+
NON_INTERACTIVE_ENV = {
|
|
29
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
30
|
+
"GIT_EDITOR": "true", # No-op editor
|
|
31
|
+
"GIT_PAGER": "cat", # No paging
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def escape_shell_arg(arg: str) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Escape shell argument for safe injection.
|
|
38
|
+
"""
|
|
39
|
+
# Use shlex.quote for proper escaping
|
|
40
|
+
return shlex.quote(arg)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def git_noninteractive_hook(
|
|
44
|
+
tool_name: str, arguments: Dict[str, Any]
|
|
45
|
+
) -> Optional[Dict[str, Any]]:
|
|
46
|
+
"""
|
|
47
|
+
Pre-tool-call hook that prepends non-interactive env vars to git commands.
|
|
48
|
+
|
|
49
|
+
Detects interactive git commands and either:
|
|
50
|
+
1. Warns and blocks them (if highly interactive like -i)
|
|
51
|
+
2. Prepends env vars to make them non-interactive
|
|
52
|
+
"""
|
|
53
|
+
# Only process Bash tool
|
|
54
|
+
if tool_name != "Bash":
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
command = arguments.get("command", "")
|
|
58
|
+
if not command or "git" not in command.lower():
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Check for banned interactive patterns
|
|
62
|
+
for pattern in BANNED_INTERACTIVE_PATTERNS:
|
|
63
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
64
|
+
logger.warning(
|
|
65
|
+
f"[GitNonInteractive] Detected interactive git command: {pattern}"
|
|
66
|
+
)
|
|
67
|
+
# Add warning to command output
|
|
68
|
+
warning = (
|
|
69
|
+
f"\n[WARNING] Interactive git command detected: {command}\n"
|
|
70
|
+
f"This may hang. Consider using non-interactive alternatives.\n"
|
|
71
|
+
)
|
|
72
|
+
# Don't block, just warn - user might know what they're doing
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
# Prepend environment variables to make git non-interactive
|
|
76
|
+
if "git" in command.lower():
|
|
77
|
+
env_prefix = " ".join(
|
|
78
|
+
[f"{k}={escape_shell_arg(v)}" for k, v in NON_INTERACTIVE_ENV.items()]
|
|
79
|
+
)
|
|
80
|
+
modified_command = f"{env_prefix} {command}"
|
|
81
|
+
|
|
82
|
+
logger.info(f"[GitNonInteractive] Prepending non-interactive env vars to git command")
|
|
83
|
+
|
|
84
|
+
# Return modified arguments
|
|
85
|
+
modified_args = arguments.copy()
|
|
86
|
+
modified_args["command"] = modified_command
|
|
87
|
+
return modified_args
|
|
88
|
+
|
|
89
|
+
return None
|
|
@@ -84,11 +84,41 @@ IF COMPLEX (architecture, multi-system, debugging after 2+ failures):
|
|
|
84
84
|
SYNTHESIZE findings before proceeding.
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
|
+
ULTRATHINK_MODE = """[ultrathink-mode]
|
|
88
|
+
ENGAGE MAXIMUM REASONING CAPACITY.
|
|
89
|
+
|
|
90
|
+
Extended thinking mode activated with 32k token thinking budget.
|
|
91
|
+
This enables exhaustive deep reasoning and multi-dimensional analysis.
|
|
92
|
+
|
|
93
|
+
## REASONING PRINCIPLES
|
|
94
|
+
- **Deep Analysis**: Consider edge cases, security implications, performance impacts
|
|
95
|
+
- **Multi-Perspective**: Analyze from user, developer, system, and security viewpoints
|
|
96
|
+
- **Strategic Planning**: Consult delphi agent for architecture decisions and hard problems
|
|
97
|
+
- **Root Cause**: Don't treat symptoms - identify and address underlying causes
|
|
98
|
+
- **Risk Assessment**: Evaluate trade-offs, failure modes, and mitigation strategies
|
|
99
|
+
|
|
100
|
+
## THINKING WORKFLOW
|
|
101
|
+
1. Problem decomposition into atomic components
|
|
102
|
+
2. Parallel exploration of solution space (spawn agents for research)
|
|
103
|
+
3. Consult delphi for strategic guidance on complex decisions
|
|
104
|
+
4. Multi-dimensional trade-off analysis
|
|
105
|
+
5. Solution synthesis with verification plan
|
|
106
|
+
|
|
107
|
+
## VERIFICATION
|
|
108
|
+
- Test assumptions against reality
|
|
109
|
+
- Challenge your own reasoning
|
|
110
|
+
- Seek disconfirming evidence
|
|
111
|
+
- Consider second-order effects
|
|
112
|
+
|
|
113
|
+
Use delphi agent for strategic consultation on architecture, debugging, and complex trade-offs.
|
|
114
|
+
"""
|
|
115
|
+
|
|
87
116
|
KEYWORD_PATTERNS = {
|
|
88
117
|
r"\bironstar\b": IRONSTAR_MODE,
|
|
89
118
|
r"\birs\b": IRONSTAR_MODE,
|
|
90
119
|
r"\bultrawork\b": IRONSTAR_MODE,
|
|
91
120
|
r"\bulw\b": IRONSTAR_MODE,
|
|
121
|
+
r"\bultrathink\b": ULTRATHINK_MODE,
|
|
92
122
|
r"\bsearch\b": SEARCH_MODE,
|
|
93
123
|
r"\banalyze\b": ANALYZE_MODE,
|
|
94
124
|
r"\banalysis\b": ANALYZE_MODE,
|
mcp_bridge/hooks/manager.py
CHANGED
|
@@ -6,6 +6,14 @@ Provides interception points for tool calls and model invocations.
|
|
|
6
6
|
import logging
|
|
7
7
|
from typing import Any, Callable, Dict, List, Optional, Awaitable
|
|
8
8
|
|
|
9
|
+
try:
|
|
10
|
+
from mcp_bridge.config.hook_config import is_hook_enabled
|
|
11
|
+
except ImportError:
|
|
12
|
+
|
|
13
|
+
def is_hook_enabled(hook_name: str) -> bool:
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
logger = logging.getLogger(__name__)
|
|
10
18
|
|
|
11
19
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Notification hook for agent spawn messages.
|
|
4
|
+
|
|
5
|
+
Fires on Notification events to output user-friendly messages about
|
|
6
|
+
which agent was spawned, what model it uses, and what task it's doing.
|
|
7
|
+
|
|
8
|
+
Format: spawned {agent_type}:{model}('{description}')
|
|
9
|
+
Example: spawned delphi:gpt-5.2-medium('Debug xyz code')
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Optional, Dict, Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Agent display model mappings
|
|
18
|
+
AGENT_DISPLAY_MODELS = {
|
|
19
|
+
"explore": "gemini-3-flash",
|
|
20
|
+
"dewey": "gemini-3-flash",
|
|
21
|
+
"document_writer": "gemini-3-flash",
|
|
22
|
+
"multimodal": "gemini-3-flash",
|
|
23
|
+
"frontend": "gemini-3-pro-high",
|
|
24
|
+
"delphi": "gpt-5.2-medium",
|
|
25
|
+
"planner": "opus-4.5",
|
|
26
|
+
"code-reviewer": "sonnet-4.5",
|
|
27
|
+
"debugger": "sonnet-4.5",
|
|
28
|
+
"_default": "sonnet-4.5",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_agent_info(message: str) -> Optional[Dict[str, str]]:
|
|
33
|
+
"""
|
|
34
|
+
Extract agent spawn information from notification message.
|
|
35
|
+
|
|
36
|
+
Looks for patterns like:
|
|
37
|
+
- "Agent explore spawned for task..."
|
|
38
|
+
- "Spawned delphi agent: description"
|
|
39
|
+
- Task tool delegation messages
|
|
40
|
+
"""
|
|
41
|
+
message_lower = message.lower()
|
|
42
|
+
|
|
43
|
+
# Try to extract agent type from message
|
|
44
|
+
agent_type = None
|
|
45
|
+
description = ""
|
|
46
|
+
|
|
47
|
+
for agent in AGENT_DISPLAY_MODELS.keys():
|
|
48
|
+
if agent == "_default":
|
|
49
|
+
continue
|
|
50
|
+
if agent in message_lower:
|
|
51
|
+
agent_type = agent
|
|
52
|
+
# Extract description after agent name
|
|
53
|
+
idx = message_lower.find(agent)
|
|
54
|
+
description = message[idx + len(agent):].strip()[:60]
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
if not agent_type:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# Clean up description
|
|
61
|
+
description = description.strip(":-() ")
|
|
62
|
+
if not description:
|
|
63
|
+
description = "task delegated"
|
|
64
|
+
|
|
65
|
+
display_model = AGENT_DISPLAY_MODELS.get(agent_type, AGENT_DISPLAY_MODELS["_default"])
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"agent_type": agent_type,
|
|
69
|
+
"model": display_model,
|
|
70
|
+
"description": description,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
"""Main hook entry point."""
|
|
76
|
+
try:
|
|
77
|
+
hook_input = json.load(sys.stdin)
|
|
78
|
+
except (json.JSONDecodeError, EOFError):
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
# Get notification message
|
|
82
|
+
message = hook_input.get("message", "")
|
|
83
|
+
notification_type = hook_input.get("notification_type", "")
|
|
84
|
+
|
|
85
|
+
# Only process agent-related notifications
|
|
86
|
+
agent_keywords = ["agent", "spawn", "delegat", "task"]
|
|
87
|
+
if not any(kw in message.lower() for kw in agent_keywords):
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
# Extract agent info
|
|
91
|
+
agent_info = extract_agent_info(message)
|
|
92
|
+
if not agent_info:
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
# Format and output
|
|
96
|
+
output = f"spawned {agent_info['agent_type']}:{agent_info['model']}('{agent_info['description']}')"
|
|
97
|
+
print(output, file=sys.stderr)
|
|
98
|
+
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
sys.exit(main())
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
UserPromptSubmit hook: Pre-emptive parallel execution enforcement.
|
|
4
|
+
|
|
5
|
+
Fires BEFORE response generation to inject parallel execution instructions
|
|
6
|
+
when implementation tasks are detected. Eliminates timing ambiguity.
|
|
7
|
+
|
|
8
|
+
CRITICAL: Also activates stravinsky mode marker when /stravinsky is invoked,
|
|
9
|
+
enabling hard blocking of direct tools (Read, Grep, Bash) via stravinsky_mode.py.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# Marker file that enables hard blocking of direct tools
|
|
17
|
+
STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_stravinsky_invocation(prompt):
|
|
21
|
+
"""Detect if /stravinsky skill is being invoked."""
|
|
22
|
+
patterns = [
|
|
23
|
+
r'/stravinsky',
|
|
24
|
+
r'<command-name>/stravinsky</command-name>',
|
|
25
|
+
r'stravinsky orchestrator',
|
|
26
|
+
r'ultrawork',
|
|
27
|
+
r'ultrathink',
|
|
28
|
+
]
|
|
29
|
+
prompt_lower = prompt.lower()
|
|
30
|
+
return any(re.search(p, prompt_lower) for p in patterns)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def activate_stravinsky_mode():
|
|
34
|
+
"""Create marker file to enable hard blocking of direct tools."""
|
|
35
|
+
try:
|
|
36
|
+
config = {"active": True, "reason": "invoked via /stravinsky skill"}
|
|
37
|
+
STRAVINSKY_MODE_FILE.write_text(json.dumps(config))
|
|
38
|
+
return True
|
|
39
|
+
except IOError:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detect_implementation_task(prompt):
|
|
44
|
+
"""Detect if prompt is an implementation task requiring parallel execution."""
|
|
45
|
+
keywords = [
|
|
46
|
+
'implement', 'add', 'create', 'build', 'refactor', 'fix',
|
|
47
|
+
'update', 'modify', 'change', 'develop', 'write code',
|
|
48
|
+
'feature', 'bug fix', 'enhancement', 'integrate'
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
prompt_lower = prompt.lower()
|
|
52
|
+
return any(kw in prompt_lower for kw in keywords)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
try:
|
|
57
|
+
hook_input = json.load(sys.stdin)
|
|
58
|
+
except (json.JSONDecodeError, EOFError):
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
prompt = hook_input.get("prompt", "")
|
|
62
|
+
|
|
63
|
+
# CRITICAL: Activate stravinsky mode if /stravinsky is invoked
|
|
64
|
+
# This creates the marker file that enables hard blocking of direct tools
|
|
65
|
+
is_stravinsky = detect_stravinsky_invocation(prompt)
|
|
66
|
+
if is_stravinsky:
|
|
67
|
+
activate_stravinsky_mode()
|
|
68
|
+
|
|
69
|
+
# Only inject for implementation tasks OR stravinsky invocation
|
|
70
|
+
if not detect_implementation_task(prompt) and not is_stravinsky:
|
|
71
|
+
print(prompt)
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
# Inject parallel execution instruction BEFORE prompt
|
|
75
|
+
instruction = """
|
|
76
|
+
[🔄 PARALLEL EXECUTION MODE ACTIVE]
|
|
77
|
+
|
|
78
|
+
When you create a TodoWrite with 2+ pending items:
|
|
79
|
+
|
|
80
|
+
✅ IMMEDIATELY in THIS SAME RESPONSE (do NOT end response after TodoWrite):
|
|
81
|
+
1. Spawn Task() for EACH independent pending TODO
|
|
82
|
+
2. Use: Task(subagent_type="explore"|"Plan"|etc., prompt="...", description="...", run_in_background=true)
|
|
83
|
+
3. Fire ALL Task calls in ONE response block
|
|
84
|
+
4. Do NOT mark any TODO as in_progress until Task results return
|
|
85
|
+
|
|
86
|
+
❌ DO NOT:
|
|
87
|
+
- End your response after TodoWrite
|
|
88
|
+
- Mark TODOs in_progress before spawning Tasks
|
|
89
|
+
- Spawn only ONE Task (spawn ALL independent tasks)
|
|
90
|
+
- Wait for "next response" to spawn Tasks
|
|
91
|
+
|
|
92
|
+
Example pattern (all in SAME response):
|
|
93
|
+
```
|
|
94
|
+
TodoWrite([task1, task2, task3])
|
|
95
|
+
Task(subagent_type="Explore", prompt="Task 1 details", description="Task 1", run_in_background=true)
|
|
96
|
+
Task(subagent_type="Plan", prompt="Task 2 details", description="Task 2", run_in_background=true)
|
|
97
|
+
Task(subagent_type="Explore", prompt="Task 3 details", description="Task 3", run_in_background=true)
|
|
98
|
+
# Continue response - collect results with TaskOutput
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
modified_prompt = instruction + prompt
|
|
106
|
+
print(modified_prompt)
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
sys.exit(main())
|