stravinsky 0.2.67__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.

Files changed (190) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/__init__.py +16 -6
  3. mcp_bridge/auth/cli.py +202 -11
  4. mcp_bridge/auth/oauth.py +1 -2
  5. mcp_bridge/auth/openai_oauth.py +4 -7
  6. mcp_bridge/auth/token_store.py +112 -11
  7. mcp_bridge/cli/__init__.py +1 -1
  8. mcp_bridge/cli/install_hooks.py +503 -107
  9. mcp_bridge/cli/session_report.py +0 -3
  10. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  11. mcp_bridge/config/README.md +276 -0
  12. mcp_bridge/config/__init__.py +2 -2
  13. mcp_bridge/config/hook_config.py +247 -0
  14. mcp_bridge/config/hooks_manifest.json +138 -0
  15. mcp_bridge/config/rate_limits.py +317 -0
  16. mcp_bridge/config/skills_manifest.json +128 -0
  17. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  18. mcp_bridge/hooks/__init__.py +19 -4
  19. mcp_bridge/hooks/agent_reminder.py +4 -4
  20. mcp_bridge/hooks/auto_slash_command.py +5 -5
  21. mcp_bridge/hooks/budget_optimizer.py +2 -2
  22. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  23. mcp_bridge/hooks/comment_checker.py +3 -4
  24. mcp_bridge/hooks/compaction.py +2 -2
  25. mcp_bridge/hooks/context.py +2 -1
  26. mcp_bridge/hooks/context_monitor.py +2 -2
  27. mcp_bridge/hooks/delegation_policy.py +85 -0
  28. mcp_bridge/hooks/directory_context.py +3 -3
  29. mcp_bridge/hooks/edit_recovery.py +3 -2
  30. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  31. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  32. mcp_bridge/hooks/events.py +160 -0
  33. mcp_bridge/hooks/git_noninteractive.py +4 -4
  34. mcp_bridge/hooks/keyword_detector.py +8 -10
  35. mcp_bridge/hooks/manager.py +43 -22
  36. mcp_bridge/hooks/notification_hook.py +13 -6
  37. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  38. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  39. mcp_bridge/hooks/parallel_execution.py +22 -10
  40. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  41. mcp_bridge/hooks/pre_compact.py +8 -9
  42. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  43. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  44. mcp_bridge/hooks/routing_notifications.py +80 -0
  45. mcp_bridge/hooks/rules_injector.py +11 -19
  46. mcp_bridge/hooks/session_idle.py +4 -4
  47. mcp_bridge/hooks/session_notifier.py +4 -4
  48. mcp_bridge/hooks/session_recovery.py +4 -5
  49. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  50. mcp_bridge/hooks/subagent_stop.py +1 -3
  51. mcp_bridge/hooks/task_validator.py +2 -2
  52. mcp_bridge/hooks/tmux_manager.py +7 -8
  53. mcp_bridge/hooks/todo_delegation.py +4 -1
  54. mcp_bridge/hooks/todo_enforcer.py +180 -10
  55. mcp_bridge/hooks/tool_messaging.py +113 -10
  56. mcp_bridge/hooks/truncation_policy.py +37 -0
  57. mcp_bridge/hooks/truncator.py +1 -2
  58. mcp_bridge/metrics/cost_tracker.py +115 -0
  59. mcp_bridge/native_search.py +93 -0
  60. mcp_bridge/native_watcher.py +118 -0
  61. mcp_bridge/notifications.py +150 -0
  62. mcp_bridge/orchestrator/enums.py +11 -0
  63. mcp_bridge/orchestrator/router.py +165 -0
  64. mcp_bridge/orchestrator/state.py +32 -0
  65. mcp_bridge/orchestrator/visualization.py +14 -0
  66. mcp_bridge/orchestrator/wisdom.py +34 -0
  67. mcp_bridge/prompts/__init__.py +1 -8
  68. mcp_bridge/prompts/dewey.py +1 -1
  69. mcp_bridge/prompts/planner.py +2 -4
  70. mcp_bridge/prompts/stravinsky.py +53 -31
  71. mcp_bridge/proxy/__init__.py +0 -0
  72. mcp_bridge/proxy/client.py +70 -0
  73. mcp_bridge/proxy/model_server.py +157 -0
  74. mcp_bridge/routing/__init__.py +43 -0
  75. mcp_bridge/routing/config.py +250 -0
  76. mcp_bridge/routing/model_tiers.py +135 -0
  77. mcp_bridge/routing/provider_state.py +261 -0
  78. mcp_bridge/routing/task_classifier.py +190 -0
  79. mcp_bridge/server.py +542 -59
  80. mcp_bridge/server_tools.py +738 -6
  81. mcp_bridge/tools/__init__.py +40 -25
  82. mcp_bridge/tools/agent_manager.py +616 -697
  83. mcp_bridge/tools/background_tasks.py +13 -17
  84. mcp_bridge/tools/code_search.py +70 -53
  85. mcp_bridge/tools/continuous_loop.py +0 -1
  86. mcp_bridge/tools/dashboard.py +19 -0
  87. mcp_bridge/tools/find_code.py +296 -0
  88. mcp_bridge/tools/init.py +1 -0
  89. mcp_bridge/tools/list_directory.py +42 -0
  90. mcp_bridge/tools/lsp/__init__.py +12 -5
  91. mcp_bridge/tools/lsp/manager.py +471 -0
  92. mcp_bridge/tools/lsp/tools.py +723 -207
  93. mcp_bridge/tools/model_invoke.py +1195 -273
  94. mcp_bridge/tools/mux_client.py +75 -0
  95. mcp_bridge/tools/project_context.py +1 -2
  96. mcp_bridge/tools/query_classifier.py +406 -0
  97. mcp_bridge/tools/read_file.py +84 -0
  98. mcp_bridge/tools/replace.py +45 -0
  99. mcp_bridge/tools/run_shell_command.py +38 -0
  100. mcp_bridge/tools/search_enhancements.py +347 -0
  101. mcp_bridge/tools/semantic_search.py +3627 -0
  102. mcp_bridge/tools/session_manager.py +0 -2
  103. mcp_bridge/tools/skill_loader.py +0 -1
  104. mcp_bridge/tools/task_runner.py +5 -7
  105. mcp_bridge/tools/templates.py +3 -3
  106. mcp_bridge/tools/tool_search.py +331 -0
  107. mcp_bridge/tools/write_file.py +29 -0
  108. mcp_bridge/update_manager.py +585 -0
  109. mcp_bridge/update_manager_pypi.py +297 -0
  110. mcp_bridge/utils/cache.py +82 -0
  111. mcp_bridge/utils/process.py +71 -0
  112. mcp_bridge/utils/session_state.py +51 -0
  113. mcp_bridge/utils/truncation.py +76 -0
  114. stravinsky-0.4.66.dist-info/METADATA +517 -0
  115. stravinsky-0.4.66.dist-info/RECORD +198 -0
  116. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  117. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  118. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  119. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  120. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  121. stravinsky_claude_assets/agents/debugger.md +254 -0
  122. stravinsky_claude_assets/agents/delphi.md +495 -0
  123. stravinsky_claude_assets/agents/dewey.md +248 -0
  124. stravinsky_claude_assets/agents/explore.md +1198 -0
  125. stravinsky_claude_assets/agents/frontend.md +472 -0
  126. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  127. stravinsky_claude_assets/agents/momus.md +464 -0
  128. stravinsky_claude_assets/agents/research-lead.md +141 -0
  129. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  130. stravinsky_claude_assets/commands/delphi.md +9 -0
  131. stravinsky_claude_assets/commands/dewey.md +54 -0
  132. stravinsky_claude_assets/commands/git-master.md +112 -0
  133. stravinsky_claude_assets/commands/index.md +49 -0
  134. stravinsky_claude_assets/commands/publish.md +86 -0
  135. stravinsky_claude_assets/commands/review.md +73 -0
  136. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  137. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  138. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  139. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  140. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  141. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  142. stravinsky_claude_assets/commands/str/clean.md +97 -0
  143. stravinsky_claude_assets/commands/str/continue.md +38 -0
  144. stravinsky_claude_assets/commands/str/index.md +199 -0
  145. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  146. stravinsky_claude_assets/commands/str/search.md +205 -0
  147. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  148. stravinsky_claude_assets/commands/str/stats.md +71 -0
  149. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  150. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  151. stravinsky_claude_assets/commands/str/watch.md +45 -0
  152. stravinsky_claude_assets/commands/strav.md +53 -0
  153. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  154. stravinsky_claude_assets/commands/verify.md +60 -0
  155. stravinsky_claude_assets/commands/version.md +5 -0
  156. stravinsky_claude_assets/hooks/README.md +248 -0
  157. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  158. stravinsky_claude_assets/hooks/context.py +38 -0
  159. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  160. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  161. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  162. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  163. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  164. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  165. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  166. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  167. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  168. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  169. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  170. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  171. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  172. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  173. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  174. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  175. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  176. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  177. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  178. stravinsky_claude_assets/hooks/truncator.py +23 -0
  179. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  180. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  181. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  182. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  183. stravinsky_claude_assets/settings.json +152 -0
  184. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  185. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  186. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  187. stravinsky_claude_assets/task_dependencies.json +34 -0
  188. stravinsky-0.2.67.dist-info/METADATA +0 -284
  189. stravinsky-0.2.67.dist-info/RECORD +0 -76
  190. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
@@ -0,0 +1,160 @@
1
+ """
2
+ Unified Event Model and Policy Base for Stravinsky Hooks.
3
+ Enables code sharing between native Claude Code hooks and MCP bridge hooks.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import sys
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import Any, Optional
13
+
14
+
15
+ class EventType(Enum):
16
+ PRE_TOOL_CALL = "pre_tool_call"
17
+ POST_TOOL_CALL = "post_tool_call"
18
+ PRE_MODEL_INVOKE = "pre_model_invoke"
19
+ SESSION_IDLE = "session_idle"
20
+ PRE_COMPACT = "pre_compact"
21
+ NOTIFICATION = "notification"
22
+ SUBAGENT_STOP = "subagent_stop"
23
+
24
+
25
+ @dataclass
26
+ class ToolCallEvent:
27
+ tool_name: str
28
+ arguments: dict[str, Any]
29
+ output: str | None = None
30
+ event_type: EventType = EventType.PRE_TOOL_CALL
31
+ metadata: dict[str, Any] = field(default_factory=dict)
32
+
33
+ @classmethod
34
+ def from_mcp(
35
+ cls, tool_name: str, arguments: dict[str, Any], output: str | None = None
36
+ ) -> "ToolCallEvent":
37
+ event_type = EventType.POST_TOOL_CALL if output is not None else EventType.PRE_TOOL_CALL
38
+ return cls(tool_name=tool_name, arguments=arguments, output=output, event_type=event_type)
39
+
40
+ @classmethod
41
+ def from_native(cls) -> Optional["ToolCallEvent"]:
42
+ """Parses ToolCallEvent from stdin (Native Claude Code hook pattern)."""
43
+ try:
44
+ # Native hooks often get input via stdin
45
+ if sys.stdin.isatty():
46
+ return None
47
+
48
+ raw_input = sys.stdin.read()
49
+ if not raw_input:
50
+ return None
51
+
52
+ data = json.loads(raw_input)
53
+ # print(f"DEBUG: raw_data={data}", file=sys.stderr)
54
+
55
+ # Claude Code native hook input varies by type
56
+ tool_name = data.get("tool_name") or data.get("toolName", "")
57
+ arguments = data.get("tool_input") or data.get("params", {})
58
+ output = data.get("output") or data.get("tool_response")
59
+
60
+ # Infer event type from env or data
61
+ import os
62
+ native_type = os.environ.get("CLAUDE_HOOK_TYPE", "")
63
+
64
+ # print(f"DEBUG: native_type={native_type}, output={output}", file=sys.stderr)
65
+
66
+ event_type = EventType.PRE_TOOL_CALL
67
+ if "PostToolUse" in native_type or output is not None:
68
+ event_type = EventType.POST_TOOL_CALL
69
+ elif "PreCompact" in native_type:
70
+ event_type = EventType.PRE_COMPACT
71
+ elif "Notification" in native_type:
72
+ event_type = EventType.NOTIFICATION
73
+ elif "SubagentStop" in native_type:
74
+ event_type = EventType.SUBAGENT_STOP
75
+
76
+ return cls(
77
+ tool_name=tool_name,
78
+ arguments=arguments,
79
+ output=output,
80
+ event_type=event_type,
81
+ metadata=data,
82
+ )
83
+ except Exception:
84
+ return None
85
+
86
+
87
+ @dataclass
88
+ class PolicyResult:
89
+ """
90
+ The result of a policy evaluation.
91
+ """
92
+
93
+ modified_data: Any | None = None
94
+ should_block: bool = False
95
+ message: str | None = None
96
+ exit_code: int = 0
97
+
98
+
99
+ class HookPolicy(ABC):
100
+ """
101
+ Abstract Base Class for unified hook policies.
102
+ """
103
+
104
+ @property
105
+ @abstractmethod
106
+ def event_type(self) -> EventType:
107
+ """The event type this policy responds to."""
108
+ pass
109
+
110
+ @abstractmethod
111
+ async def evaluate(self, event: ToolCallEvent) -> PolicyResult:
112
+ """
113
+ Evaluate the policy and return a PolicyResult.
114
+ """
115
+ pass
116
+
117
+ def as_mcp_pre_hook(self):
118
+ """Wraps the policy for HookManager.register_pre_tool_call."""
119
+
120
+ async def pre_hook(tool_name: str, arguments: dict[str, Any]) -> dict[str, Any] | None:
121
+ event = ToolCallEvent.from_mcp(tool_name, arguments)
122
+ result = await self.evaluate(event)
123
+ return result.modified_data
124
+
125
+ return pre_hook
126
+
127
+ def as_mcp_post_hook(self):
128
+ """Wraps the policy for HookManager.register_post_tool_call."""
129
+
130
+ async def post_hook(tool_name: str, arguments: dict[str, Any], output: str) -> str | None:
131
+ event = ToolCallEvent.from_mcp(tool_name, arguments, output)
132
+ result = await self.evaluate(event)
133
+ return result.modified_data
134
+
135
+ return post_hook
136
+
137
+ def run_as_native(self):
138
+ """Entry point for running the policy as a standalone script."""
139
+ event = ToolCallEvent.from_native()
140
+ if not event:
141
+ sys.exit(0)
142
+
143
+ # Allow policies to respond to multiple event types if they want,
144
+ # but default to strict matching.
145
+ if hasattr(self, "supported_event_types"):
146
+ if event.event_type not in self.supported_event_types:
147
+ sys.exit(0)
148
+ elif event.event_type != self.event_type:
149
+ sys.exit(0)
150
+
151
+ result = asyncio.run(self.evaluate(event))
152
+
153
+ if result.message:
154
+ # Print message to stdout for Claude to see
155
+ print(result.message)
156
+
157
+ if result.should_block:
158
+ sys.exit(result.exit_code or 2)
159
+
160
+ sys.exit(0)
@@ -7,7 +7,7 @@ Prevents git interactive command hangs by prepending environment variables.
7
7
  import logging
8
8
  import re
9
9
  import shlex
10
- from typing import Any, Dict, Optional
10
+ from typing import Any
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -41,8 +41,8 @@ def escape_shell_arg(arg: str) -> str:
41
41
 
42
42
 
43
43
  async def git_noninteractive_hook(
44
- tool_name: str, arguments: Dict[str, Any]
45
- ) -> Optional[Dict[str, Any]]:
44
+ tool_name: str, arguments: dict[str, Any]
45
+ ) -> dict[str, Any] | None:
46
46
  """
47
47
  Pre-tool-call hook that prepends non-interactive env vars to git commands.
48
48
 
@@ -79,7 +79,7 @@ async def git_noninteractive_hook(
79
79
  )
80
80
  modified_command = f"{env_prefix} {command}"
81
81
 
82
- logger.info(f"[GitNonInteractive] Prepending non-interactive env vars to git command")
82
+ logger.info("[GitNonInteractive] Prepending non-interactive env vars to git command")
83
83
 
84
84
  # Return modified arguments
85
85
  modified_args = arguments.copy()
@@ -1,17 +1,17 @@
1
1
  """
2
2
  Keyword Detector Hook.
3
3
 
4
- Detects trigger keywords (ironstar, search, analyze) in user prompts
4
+ Detects trigger keywords (ultrawork, search, analyze) in user prompts
5
5
  and injects corresponding mode activation tags.
6
6
  """
7
7
 
8
8
  import logging
9
9
  import re
10
- from typing import Any, Dict, Optional
10
+ from typing import Any
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
- IRONSTAR_MODE = """<ironstar-mode>
14
+ ULTRAWORK_MODE = """<ultrawork-mode>
15
15
  [CODE RED] Maximum precision required. Ultrathink before acting.
16
16
 
17
17
  YOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.
@@ -27,7 +27,7 @@ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
27
27
  ## EXECUTION RULES
28
28
  - **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each.
29
29
  - **PARALLEL**: Fire independent agent calls simultaneously via background_task - NEVER wait sequentially.
30
- - **BACKGROUND FIRST**: Use background_task for exploration/research agents (10+ concurrent if needed).
30
+ - **BACKGROUND FUWT**: Use background_task for exploration/research agents (10+ concurrent if needed).
31
31
  - **VERIFY**: Re-read request after completion. Check ALL requirements met before reporting done.
32
32
  - **DELEGATE**: Don't do everything yourself - orchestrate specialized agents for their strengths.
33
33
 
@@ -57,7 +57,7 @@ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
57
57
 
58
58
  THE USER ASKED FOR X. DELIVER EXACTLY X. NOT A SUBSET. NOT A DEMO. NOT A STARTING POINT.
59
59
 
60
- </ironstar-mode>
60
+ </ultrawork-mode>
61
61
 
62
62
  ---
63
63
  """
@@ -114,10 +114,8 @@ Use delphi agent for strategic consultation on architecture, debugging, and comp
114
114
  """
115
115
 
116
116
  KEYWORD_PATTERNS = {
117
- r"\bironstar\b": IRONSTAR_MODE,
118
- r"\birs\b": IRONSTAR_MODE,
119
- r"\bultrawork\b": IRONSTAR_MODE,
120
- r"\bulw\b": IRONSTAR_MODE,
117
+ r"\bultrawork\b": ULTRAWORK_MODE,
118
+ r"\buw\b": ULTRAWORK_MODE,
121
119
  r"\bultrathink\b": ULTRATHINK_MODE,
122
120
  r"\bsearch\b": SEARCH_MODE,
123
121
  r"\banalyze\b": ANALYZE_MODE,
@@ -125,7 +123,7 @@ KEYWORD_PATTERNS = {
125
123
  }
126
124
 
127
125
 
128
- async def keyword_detector_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
126
+ async def keyword_detector_hook(params: dict[str, Any]) -> dict[str, Any] | None:
129
127
  """
130
128
  Pre-model invoke hook that detects keywords and injects mode tags.
131
129
  """
@@ -4,7 +4,16 @@ Provides interception points for tool calls and model invocations.
4
4
  """
5
5
 
6
6
  import logging
7
- from typing import Any, Callable, Dict, List, Optional, Awaitable
7
+ from collections.abc import Awaitable, Callable
8
+ from typing import Any, Optional
9
+
10
+ try:
11
+ from mcp_bridge.config.hook_config import is_hook_enabled
12
+ except ImportError:
13
+
14
+ def is_hook_enabled(hook_name: str) -> bool:
15
+ return True
16
+
8
17
 
9
18
  logger = logging.getLogger(__name__)
10
19
 
@@ -24,21 +33,21 @@ class HookManager:
24
33
  _instance = None
25
34
 
26
35
  def __init__(self):
27
- self.pre_tool_call_hooks: List[
28
- Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
36
+ self.pre_tool_call_hooks: list[
37
+ Callable[[str, dict[str, Any]], Awaitable[dict[str, Any] | None]]
29
38
  ] = []
30
- self.post_tool_call_hooks: List[
31
- Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]
39
+ self.post_tool_call_hooks: list[
40
+ Callable[[str, dict[str, Any], str], Awaitable[str | None]]
32
41
  ] = []
33
- self.pre_model_invoke_hooks: List[
34
- Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
42
+ self.pre_model_invoke_hooks: list[
43
+ Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
35
44
  ] = []
36
45
  # New hook types based on oh-my-opencode patterns
37
- self.session_idle_hooks: List[
38
- Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
46
+ self.session_idle_hooks: list[
47
+ Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
39
48
  ] = []
40
- self.pre_compact_hooks: List[
41
- Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
49
+ self.pre_compact_hooks: list[
50
+ Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
42
51
  ] = []
43
52
 
44
53
  @classmethod
@@ -48,38 +57,50 @@ class HookManager:
48
57
  return cls._instance
49
58
 
50
59
  def register_pre_tool_call(
51
- self, hook: Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
60
+ self, hook: Callable[[str, dict[str, Any]], Awaitable[dict[str, Any] | None]]
52
61
  ):
53
62
  """Run before a tool is called. Can modify arguments or return early result."""
54
63
  self.pre_tool_call_hooks.append(hook)
55
64
 
56
65
  def register_post_tool_call(
57
- self, hook: Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]
66
+ self, hook: Callable[[str, dict[str, Any], str], Awaitable[str | None]]
58
67
  ):
59
68
  """Run after a tool call. Can modify or recover from tool output/error."""
60
69
  self.post_tool_call_hooks.append(hook)
61
70
 
62
71
  def register_pre_model_invoke(
63
- self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
72
+ self, hook: Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
64
73
  ):
65
74
  """Run before model invocation. Can modify prompt or parameters."""
66
75
  self.pre_model_invoke_hooks.append(hook)
67
76
 
68
77
  def register_session_idle(
69
- self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
78
+ self, hook: Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
70
79
  ):
71
80
  """Run when session becomes idle. Can inject continuation prompts."""
72
81
  self.session_idle_hooks.append(hook)
73
82
 
74
83
  def register_pre_compact(
75
- self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
84
+ self, hook: Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
76
85
  ):
77
86
  """Run before context compaction. Can preserve critical context."""
78
87
  self.pre_compact_hooks.append(hook)
79
88
 
89
+ def register_policy(self, policy: Any):
90
+ """
91
+ Registers a unified HookPolicy.
92
+ Uses duck-typing to avoid circular imports.
93
+ """
94
+ from .events import EventType
95
+
96
+ if policy.event_type == EventType.PRE_TOOL_CALL:
97
+ self.register_pre_tool_call(policy.as_mcp_pre_hook())
98
+ elif policy.event_type == EventType.POST_TOOL_CALL:
99
+ self.register_post_tool_call(policy.as_mcp_post_hook())
100
+
80
101
  async def execute_pre_tool_call(
81
- self, tool_name: str, arguments: Dict[str, Any]
82
- ) -> Dict[str, Any]:
102
+ self, tool_name: str, arguments: dict[str, Any]
103
+ ) -> dict[str, Any]:
83
104
  """Executes all pre-tool call hooks."""
84
105
  current_args = arguments
85
106
  for hook in self.pre_tool_call_hooks:
@@ -92,7 +113,7 @@ class HookManager:
92
113
  return current_args
93
114
 
94
115
  async def execute_post_tool_call(
95
- self, tool_name: str, arguments: Dict[str, Any], output: str
116
+ self, tool_name: str, arguments: dict[str, Any], output: str
96
117
  ) -> str:
97
118
  """Executes all post-tool call hooks."""
98
119
  current_output = output
@@ -105,7 +126,7 @@ class HookManager:
105
126
  logger.error(f"[HookManager] Error in post_tool_call hook {hook.__name__}: {e}")
106
127
  return current_output
107
128
 
108
- async def execute_pre_model_invoke(self, params: Dict[str, Any]) -> Dict[str, Any]:
129
+ async def execute_pre_model_invoke(self, params: dict[str, Any]) -> dict[str, Any]:
109
130
  """Executes all pre-model invoke hooks."""
110
131
  current_params = params
111
132
  for hook in self.pre_model_invoke_hooks:
@@ -117,7 +138,7 @@ class HookManager:
117
138
  logger.error(f"[HookManager] Error in pre_model_invoke hook {hook.__name__}: {e}")
118
139
  return current_params
119
140
 
120
- async def execute_session_idle(self, params: Dict[str, Any]) -> Dict[str, Any]:
141
+ async def execute_session_idle(self, params: dict[str, Any]) -> dict[str, Any]:
121
142
  """Executes all session idle hooks (Stop hook pattern)."""
122
143
  current_params = params
123
144
  for hook in self.session_idle_hooks:
@@ -129,7 +150,7 @@ class HookManager:
129
150
  logger.error(f"[HookManager] Error in session_idle hook {hook.__name__}: {e}")
130
151
  return current_params
131
152
 
132
- async def execute_pre_compact(self, params: Dict[str, Any]) -> Dict[str, Any]:
153
+ async def execute_pre_compact(self, params: dict[str, Any]) -> dict[str, Any]:
133
154
  """Executes all pre-compact hooks (context preservation)."""
134
155
  current_params = params
135
156
  for hook in self.pre_compact_hooks:
@@ -10,9 +10,8 @@ Example: spawned delphi:gpt-5.2-medium('Debug xyz code')
10
10
  """
11
11
 
12
12
  import json
13
+ import os
13
14
  import sys
14
- from typing import Optional, Dict, Any
15
-
16
15
 
17
16
  # Agent display model mappings
18
17
  AGENT_DISPLAY_MODELS = {
@@ -29,7 +28,7 @@ AGENT_DISPLAY_MODELS = {
29
28
  }
30
29
 
31
30
 
32
- def extract_agent_info(message: str) -> Optional[Dict[str, str]]:
31
+ def extract_agent_info(message: str) -> dict[str, str] | None:
33
32
  """
34
33
  Extract agent spawn information from notification message.
35
34
 
@@ -44,14 +43,14 @@ def extract_agent_info(message: str) -> Optional[Dict[str, str]]:
44
43
  agent_type = None
45
44
  description = ""
46
45
 
47
- for agent in AGENT_DISPLAY_MODELS.keys():
46
+ for agent in AGENT_DISPLAY_MODELS:
48
47
  if agent == "_default":
49
48
  continue
50
49
  if agent in message_lower:
51
50
  agent_type = agent
52
51
  # Extract description after agent name
53
52
  idx = message_lower.find(agent)
54
- description = message[idx + len(agent):].strip()[:60]
53
+ description = message[idx + len(agent) :].strip()[:60]
55
54
  break
56
55
 
57
56
  if not agent_type:
@@ -92,8 +91,16 @@ def main():
92
91
  if not agent_info:
93
92
  return 0
94
93
 
94
+ # Get repo name for context
95
+ cwd = os.environ.get("CLAUDE_CWD", "")
96
+ repo_name = os.path.basename(cwd) if cwd else ""
97
+
95
98
  # Format and output
96
- output = f"spawned {agent_info['agent_type']}:{agent_info['model']}('{agent_info['description']}')"
99
+ if repo_name:
100
+ output = f"spawned [{repo_name}] {agent_info['agent_type']}:{agent_info['model']}('{agent_info['description']}')"
101
+ else:
102
+ output = f"spawned {agent_info['agent_type']}:{agent_info['model']}('{agent_info['description']}')"
103
+
97
104
  print(output, file=sys.stderr)
98
105
 
99
106
  return 0
@@ -0,0 +1,67 @@
1
+ import os
2
+ import time
3
+
4
+ from ..utils.session_state import get_current_session_id, get_session_state, update_session_state
5
+ from .events import EventType, HookPolicy, PolicyResult, ToolCallEvent
6
+
7
+
8
+ class ParallelEnforcementPolicy(HookPolicy):
9
+ """
10
+ Policy to enforce parallel delegation after TodoWrite.
11
+ Warns if the agent tries to use direct tools instead of Task agents
12
+ when multiple TODOs are pending.
13
+ """
14
+
15
+ @property
16
+ def event_type(self) -> EventType:
17
+ return EventType.PRE_TOOL_CALL
18
+
19
+ async def evaluate(self, event: ToolCallEvent) -> PolicyResult:
20
+ # We don't block agent_spawn or Task themselves
21
+ if event.tool_name in ["agent_spawn", "task_spawn", "Task"]:
22
+ return PolicyResult(modified_data=event.arguments)
23
+
24
+ session_id = event.metadata.get("session_id") or get_current_session_id()
25
+ state = get_session_state(session_id)
26
+
27
+ last_write = state.get("last_todo_write_at", 0)
28
+ pending_count = state.get("pending_todo_count", 0)
29
+
30
+ # If TodoWrite was recent (last 60 seconds) and multiple tasks are pending
31
+ if time.time() - last_write < 60 and pending_count >= 2:
32
+ # Check if this tool is allowed
33
+ allowed_tools = ["Read", "read_file", "ls", "list_directory"]
34
+
35
+ # If it's a direct action tool (Bash, Edit, etc.), warn
36
+ if event.tool_name not in allowed_tools:
37
+ message = f"""
38
+ ┌──────────────────────────────────────────────────────────────────────────┐
39
+ │ ⚠️ SEQUENTIAL EXECUTION DETECTED ⚠️ │
40
+ ├──────────────────────────────────────────────────────────────────────────┤
41
+ │ │
42
+ │ You have {pending_count} pending tasks from your last TodoWrite. │
43
+ │ You are currently attempting to use '{event.tool_name}' directly. │
44
+ │ │
45
+ │ To maintain high performance and parallel workflow: │
46
+ │ 1. You SHOULD have spawned Task agents for all independent tasks. │
47
+ │ 2. Direct tool use is discouraged when multiple tasks are pending. │
48
+ │ │
49
+ │ If these tasks are truly independent, please use Task() instead. │
50
+ └──────────────────────────────────────────────────────────────────────────┘
51
+ """
52
+ # Increment warning count
53
+ attempts = state.get("blocked_sequential_attempts", 0) + 1
54
+ update_session_state({"blocked_sequential_attempts": attempts}, session_id=session_id)
55
+
56
+ return PolicyResult(
57
+ modified_data=event.arguments,
58
+ message=message,
59
+ # We could block here if attempts > threshold
60
+ )
61
+
62
+ return PolicyResult(modified_data=event.arguments)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ policy = ParallelEnforcementPolicy()
67
+ policy.run_as_native()
@@ -9,7 +9,7 @@ Based on oh-my-opencode's parallel execution enforcement pattern.
9
9
 
10
10
  import logging
11
11
  import re
12
- from typing import Any, Dict, Optional
12
+ from typing import Any
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
 
@@ -41,10 +41,10 @@ RULES:
41
41
  """
42
42
 
43
43
  # Track if enforcement was already triggered this session
44
- _enforcement_triggered: Dict[str, bool] = {}
44
+ _enforcement_triggered: dict[str, bool] = {}
45
45
 
46
46
 
47
- async def parallel_enforcer_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
47
+ async def parallel_enforcer_hook(params: dict[str, Any]) -> dict[str, Any] | None:
48
48
  """
49
49
  Post-tool-call hook that triggers after TodoWrite.
50
50
 
@@ -106,9 +106,9 @@ def reset_enforcement(session_id: str = "default"):
106
106
 
107
107
  async def parallel_enforcer_post_tool_hook(
108
108
  tool_name: str,
109
- arguments: Dict[str, Any],
109
+ arguments: dict[str, Any],
110
110
  output: str
111
- ) -> Optional[str]:
111
+ ) -> str | None:
112
112
  """
113
113
  Post-tool-call hook interface for HookManager.
114
114
 
@@ -8,9 +8,10 @@ when implementation tasks are detected. Eliminates timing ambiguity.
8
8
  CRITICAL: Also activates stravinsky mode marker when /stravinsky is invoked,
9
9
  enabling hard blocking of direct tools (Read, Grep, Bash) via stravinsky_mode.py.
10
10
  """
11
+
11
12
  import json
12
- import sys
13
13
  import re
14
+ import sys
14
15
  from pathlib import Path
15
16
 
16
17
  # Marker file that enables hard blocking of direct tools
@@ -20,11 +21,10 @@ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
20
21
  def detect_stravinsky_invocation(prompt):
21
22
  """Detect if /stravinsky skill is being invoked."""
22
23
  patterns = [
23
- r'/stravinsky',
24
- r'<command-name>/stravinsky</command-name>',
25
- r'stravinsky orchestrator',
26
- r'ultrawork',
27
- r'ultrathink',
24
+ r"/stravinsky",
25
+ r"<command-name>/stravinsky</command-name>",
26
+ r"stravinsky orchestrator",
27
+ r"\bultrawork\b",
28
28
  ]
29
29
  prompt_lower = prompt.lower()
30
30
  return any(re.search(p, prompt_lower) for p in patterns)
@@ -36,16 +36,28 @@ def activate_stravinsky_mode():
36
36
  config = {"active": True, "reason": "invoked via /stravinsky skill"}
37
37
  STRAVINSKY_MODE_FILE.write_text(json.dumps(config))
38
38
  return True
39
- except IOError:
39
+ except OSError:
40
40
  return False
41
41
 
42
42
 
43
43
  def detect_implementation_task(prompt):
44
44
  """Detect if prompt is an implementation task requiring parallel execution."""
45
45
  keywords = [
46
- 'implement', 'add', 'create', 'build', 'refactor', 'fix',
47
- 'update', 'modify', 'change', 'develop', 'write code',
48
- 'feature', 'bug fix', 'enhancement', 'integrate'
46
+ "implement",
47
+ "add",
48
+ "create",
49
+ "build",
50
+ "refactor",
51
+ "fix",
52
+ "update",
53
+ "modify",
54
+ "change",
55
+ "develop",
56
+ "write code",
57
+ "feature",
58
+ "bug fix",
59
+ "enhancement",
60
+ "integrate",
49
61
  ]
50
62
 
51
63
  prompt_lower = prompt.lower()