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
|
@@ -3,11 +3,11 @@ Directory context injector hook.
|
|
|
3
3
|
Automatically finds and injects local AGENTS.md or README.md content based on the current context.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import os
|
|
7
6
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
|
|
10
|
+
async def directory_context_hook(params: dict[str, Any]) -> dict[str, Any] | None:
|
|
11
11
|
"""
|
|
12
12
|
Search for AGENTS.md or README.md in the current working directory and inject them.
|
|
13
13
|
"""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from .events import EventType, HookPolicy, PolicyResult, ToolCallEvent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EditRecoveryPolicy(HookPolicy):
|
|
7
|
+
"""
|
|
8
|
+
Policy to provide recovery guidance when Edit/MultiEdit tools fail.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def event_type(self) -> EventType:
|
|
13
|
+
return EventType.POST_TOOL_CALL
|
|
14
|
+
|
|
15
|
+
async def evaluate(self, event: ToolCallEvent) -> PolicyResult:
|
|
16
|
+
if event.tool_name not in ["Edit", "MultiEdit", "replace"]:
|
|
17
|
+
return PolicyResult(modified_data=event.output)
|
|
18
|
+
|
|
19
|
+
if not event.output:
|
|
20
|
+
return PolicyResult(modified_data=event.output)
|
|
21
|
+
|
|
22
|
+
# Error patterns
|
|
23
|
+
error_patterns = [
|
|
24
|
+
r"oldString not found",
|
|
25
|
+
r"oldString matched multiple times",
|
|
26
|
+
r"line numbers out of range",
|
|
27
|
+
r"does not match exactly",
|
|
28
|
+
r"failed to find the target string",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
recovery_needed = any(re.search(p, event.output, 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
|
+
return PolicyResult(
|
|
40
|
+
modified_data=event.output + correction,
|
|
41
|
+
message=correction,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return PolicyResult(modified_data=event.output)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
policy = EditRecoveryPolicy()
|
|
49
|
+
policy.run_as_native()
|
|
@@ -10,7 +10,7 @@ Cleans up empty/malformed messages:
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
import re
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -181,7 +181,7 @@ def sanitize_message_blocks(prompt: str) -> tuple[str, int]:
|
|
|
181
181
|
return prompt, sanitized_count
|
|
182
182
|
|
|
183
183
|
|
|
184
|
-
async def empty_message_sanitizer_hook(params:
|
|
184
|
+
async def empty_message_sanitizer_hook(params: dict[str, Any]) -> dict[str, Any] | None:
|
|
185
185
|
"""
|
|
186
186
|
Pre-model invoke hook that sanitizes empty and malformed message content.
|
|
187
187
|
|
|
@@ -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
|
|
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:
|
|
45
|
-
) ->
|
|
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(
|
|
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 (
|
|
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
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
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
|
-
</
|
|
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"\
|
|
118
|
-
r"\
|
|
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:
|
|
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
|
"""
|
mcp_bridge/hooks/manager.py
CHANGED
|
@@ -4,7 +4,8 @@ Provides interception points for tool calls and model invocations.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
|
-
from
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from typing import Any, Optional
|
|
8
9
|
|
|
9
10
|
try:
|
|
10
11
|
from mcp_bridge.config.hook_config import is_hook_enabled
|
|
@@ -32,21 +33,21 @@ class HookManager:
|
|
|
32
33
|
_instance = None
|
|
33
34
|
|
|
34
35
|
def __init__(self):
|
|
35
|
-
self.pre_tool_call_hooks:
|
|
36
|
-
Callable[[str,
|
|
36
|
+
self.pre_tool_call_hooks: list[
|
|
37
|
+
Callable[[str, dict[str, Any]], Awaitable[dict[str, Any] | None]]
|
|
37
38
|
] = []
|
|
38
|
-
self.post_tool_call_hooks:
|
|
39
|
-
Callable[[str,
|
|
39
|
+
self.post_tool_call_hooks: list[
|
|
40
|
+
Callable[[str, dict[str, Any], str], Awaitable[str | None]]
|
|
40
41
|
] = []
|
|
41
|
-
self.pre_model_invoke_hooks:
|
|
42
|
-
Callable[[
|
|
42
|
+
self.pre_model_invoke_hooks: list[
|
|
43
|
+
Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
|
|
43
44
|
] = []
|
|
44
45
|
# New hook types based on oh-my-opencode patterns
|
|
45
|
-
self.session_idle_hooks:
|
|
46
|
-
Callable[[
|
|
46
|
+
self.session_idle_hooks: list[
|
|
47
|
+
Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
|
|
47
48
|
] = []
|
|
48
|
-
self.pre_compact_hooks:
|
|
49
|
-
Callable[[
|
|
49
|
+
self.pre_compact_hooks: list[
|
|
50
|
+
Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
|
|
50
51
|
] = []
|
|
51
52
|
|
|
52
53
|
@classmethod
|
|
@@ -56,38 +57,50 @@ class HookManager:
|
|
|
56
57
|
return cls._instance
|
|
57
58
|
|
|
58
59
|
def register_pre_tool_call(
|
|
59
|
-
self, hook: Callable[[str,
|
|
60
|
+
self, hook: Callable[[str, dict[str, Any]], Awaitable[dict[str, Any] | None]]
|
|
60
61
|
):
|
|
61
62
|
"""Run before a tool is called. Can modify arguments or return early result."""
|
|
62
63
|
self.pre_tool_call_hooks.append(hook)
|
|
63
64
|
|
|
64
65
|
def register_post_tool_call(
|
|
65
|
-
self, hook: Callable[[str,
|
|
66
|
+
self, hook: Callable[[str, dict[str, Any], str], Awaitable[str | None]]
|
|
66
67
|
):
|
|
67
68
|
"""Run after a tool call. Can modify or recover from tool output/error."""
|
|
68
69
|
self.post_tool_call_hooks.append(hook)
|
|
69
70
|
|
|
70
71
|
def register_pre_model_invoke(
|
|
71
|
-
self, hook: Callable[[
|
|
72
|
+
self, hook: Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
|
|
72
73
|
):
|
|
73
74
|
"""Run before model invocation. Can modify prompt or parameters."""
|
|
74
75
|
self.pre_model_invoke_hooks.append(hook)
|
|
75
76
|
|
|
76
77
|
def register_session_idle(
|
|
77
|
-
self, hook: Callable[[
|
|
78
|
+
self, hook: Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
|
|
78
79
|
):
|
|
79
80
|
"""Run when session becomes idle. Can inject continuation prompts."""
|
|
80
81
|
self.session_idle_hooks.append(hook)
|
|
81
82
|
|
|
82
83
|
def register_pre_compact(
|
|
83
|
-
self, hook: Callable[[
|
|
84
|
+
self, hook: Callable[[dict[str, Any]], Awaitable[dict[str, Any] | None]]
|
|
84
85
|
):
|
|
85
86
|
"""Run before context compaction. Can preserve critical context."""
|
|
86
87
|
self.pre_compact_hooks.append(hook)
|
|
87
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
|
+
|
|
88
101
|
async def execute_pre_tool_call(
|
|
89
|
-
self, tool_name: str, arguments:
|
|
90
|
-
) ->
|
|
102
|
+
self, tool_name: str, arguments: dict[str, Any]
|
|
103
|
+
) -> dict[str, Any]:
|
|
91
104
|
"""Executes all pre-tool call hooks."""
|
|
92
105
|
current_args = arguments
|
|
93
106
|
for hook in self.pre_tool_call_hooks:
|
|
@@ -100,7 +113,7 @@ class HookManager:
|
|
|
100
113
|
return current_args
|
|
101
114
|
|
|
102
115
|
async def execute_post_tool_call(
|
|
103
|
-
self, tool_name: str, arguments:
|
|
116
|
+
self, tool_name: str, arguments: dict[str, Any], output: str
|
|
104
117
|
) -> str:
|
|
105
118
|
"""Executes all post-tool call hooks."""
|
|
106
119
|
current_output = output
|
|
@@ -113,7 +126,7 @@ class HookManager:
|
|
|
113
126
|
logger.error(f"[HookManager] Error in post_tool_call hook {hook.__name__}: {e}")
|
|
114
127
|
return current_output
|
|
115
128
|
|
|
116
|
-
async def execute_pre_model_invoke(self, params:
|
|
129
|
+
async def execute_pre_model_invoke(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
117
130
|
"""Executes all pre-model invoke hooks."""
|
|
118
131
|
current_params = params
|
|
119
132
|
for hook in self.pre_model_invoke_hooks:
|
|
@@ -125,7 +138,7 @@ class HookManager:
|
|
|
125
138
|
logger.error(f"[HookManager] Error in pre_model_invoke hook {hook.__name__}: {e}")
|
|
126
139
|
return current_params
|
|
127
140
|
|
|
128
|
-
async def execute_session_idle(self, params:
|
|
141
|
+
async def execute_session_idle(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
129
142
|
"""Executes all session idle hooks (Stop hook pattern)."""
|
|
130
143
|
current_params = params
|
|
131
144
|
for hook in self.session_idle_hooks:
|
|
@@ -137,7 +150,7 @@ class HookManager:
|
|
|
137
150
|
logger.error(f"[HookManager] Error in session_idle hook {hook.__name__}: {e}")
|
|
138
151
|
return current_params
|
|
139
152
|
|
|
140
|
-
async def execute_pre_compact(self, params:
|
|
153
|
+
async def execute_pre_compact(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
141
154
|
"""Executes all pre-compact hooks (context preservation)."""
|
|
142
155
|
current_params = params
|
|
143
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) ->
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
44
|
+
_enforcement_triggered: dict[str, bool] = {}
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
async def parallel_enforcer_hook(params:
|
|
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:
|
|
109
|
+
arguments: dict[str, Any],
|
|
110
110
|
output: str
|
|
111
|
-
) ->
|
|
111
|
+
) -> str | None:
|
|
112
112
|
"""
|
|
113
113
|
Post-tool-call hook interface for HookManager.
|
|
114
114
|
|