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.

Files changed (58) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/token_store.py +113 -11
  3. mcp_bridge/cli/__init__.py +6 -0
  4. mcp_bridge/cli/install_hooks.py +1265 -0
  5. mcp_bridge/cli/session_report.py +585 -0
  6. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  7. mcp_bridge/config/README.md +276 -0
  8. mcp_bridge/config/hook_config.py +249 -0
  9. mcp_bridge/config/hooks_manifest.json +138 -0
  10. mcp_bridge/config/rate_limits.py +222 -0
  11. mcp_bridge/config/skills_manifest.json +128 -0
  12. mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
  13. mcp_bridge/hooks/README.md +215 -0
  14. mcp_bridge/hooks/__init__.py +119 -60
  15. mcp_bridge/hooks/edit_recovery.py +42 -37
  16. mcp_bridge/hooks/git_noninteractive.py +89 -0
  17. mcp_bridge/hooks/keyword_detector.py +30 -0
  18. mcp_bridge/hooks/manager.py +8 -0
  19. mcp_bridge/hooks/notification_hook.py +103 -0
  20. mcp_bridge/hooks/parallel_execution.py +111 -0
  21. mcp_bridge/hooks/pre_compact.py +82 -183
  22. mcp_bridge/hooks/rules_injector.py +507 -0
  23. mcp_bridge/hooks/session_notifier.py +125 -0
  24. mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
  25. mcp_bridge/hooks/subagent_stop.py +98 -0
  26. mcp_bridge/hooks/task_validator.py +73 -0
  27. mcp_bridge/hooks/tmux_manager.py +141 -0
  28. mcp_bridge/hooks/todo_continuation.py +90 -0
  29. mcp_bridge/hooks/todo_delegation.py +88 -0
  30. mcp_bridge/hooks/tool_messaging.py +267 -0
  31. mcp_bridge/hooks/truncator.py +21 -17
  32. mcp_bridge/notifications.py +151 -0
  33. mcp_bridge/prompts/multimodal.py +24 -3
  34. mcp_bridge/server.py +214 -49
  35. mcp_bridge/server_tools.py +445 -0
  36. mcp_bridge/tools/__init__.py +22 -18
  37. mcp_bridge/tools/agent_manager.py +220 -32
  38. mcp_bridge/tools/code_search.py +97 -11
  39. mcp_bridge/tools/lsp/__init__.py +7 -0
  40. mcp_bridge/tools/lsp/manager.py +448 -0
  41. mcp_bridge/tools/lsp/tools.py +637 -150
  42. mcp_bridge/tools/model_invoke.py +208 -106
  43. mcp_bridge/tools/query_classifier.py +323 -0
  44. mcp_bridge/tools/semantic_search.py +3042 -0
  45. mcp_bridge/tools/templates.py +32 -18
  46. mcp_bridge/update_manager.py +589 -0
  47. mcp_bridge/update_manager_pypi.py +299 -0
  48. stravinsky-0.4.18.dist-info/METADATA +468 -0
  49. stravinsky-0.4.18.dist-info/RECORD +88 -0
  50. stravinsky-0.4.18.dist-info/entry_points.txt +5 -0
  51. mcp_bridge/native_hooks/edit_recovery.py +0 -46
  52. mcp_bridge/native_hooks/todo_delegation.py +0 -54
  53. mcp_bridge/native_hooks/truncator.py +0 -23
  54. stravinsky-0.2.52.dist-info/METADATA +0 -204
  55. stravinsky-0.2.52.dist-info/RECORD +0 -63
  56. stravinsky-0.2.52.dist-info/entry_points.txt +0 -3
  57. /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
  58. {stravinsky-0.2.52.dist-info → stravinsky-0.4.18.dist-info}/WHEEL +0 -0
@@ -1,66 +1,125 @@
1
1
  """
2
- Hooks initialization.
3
- Registers all Tier 1-5 hooks into the HookManager.
4
-
5
- Hook Tiers:
6
- - Tier 1: Post-tool-call (immediate response modification)
7
- - Tier 2: Pre-model-invoke (context management)
8
- - Tier 3: Pre-model-invoke (performance optimization)
9
- - Tier 4: Pre-model-invoke (behavior enforcement)
10
- - Tier 5: Session lifecycle (idle detection, compaction)
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
- from .manager import get_hook_manager
14
- from .truncator import output_truncator_hook
15
- from .edit_recovery import edit_error_recovery_hook
16
- from .directory_context import directory_context_hook
17
- from .compaction import context_compaction_hook
18
- from .budget_optimizer import budget_optimizer_hook
19
- from .todo_enforcer import todo_continuation_hook
20
- from .keyword_detector import keyword_detector_hook
21
- from .comment_checker import comment_checker_hook
22
- from .context_monitor import context_monitor_hook
23
- from .agent_reminder import agent_reminder_hook
24
- from .preemptive_compaction import preemptive_compaction_hook
25
- from .auto_slash_command import auto_slash_command_hook
26
- from .session_recovery import session_recovery_hook
27
- from .empty_message_sanitizer import empty_message_sanitizer_hook
28
-
29
- # New hooks based on oh-my-opencode patterns
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
- """Register all available hooks."""
37
- manager = get_hook_manager()
38
-
39
- # Tier 1: Post-tool-call (immediate response modification)
40
- manager.register_post_tool_call(output_truncator_hook)
41
- manager.register_post_tool_call(edit_error_recovery_hook)
42
- manager.register_post_tool_call(comment_checker_hook)
43
- manager.register_post_tool_call(agent_reminder_hook)
44
- manager.register_post_tool_call(session_recovery_hook)
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
- Edit error recovery hook.
3
- Detects common mistakes in file editing and injects high-priority corrective directives.
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
- 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
- ]
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
- 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
- """
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
- 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
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,
@@ -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())