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.
- 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 +112 -11
- 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/MANIFEST_SCHEMA.md +305 -0
- mcp_bridge/config/README.md +276 -0
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +247 -0
- mcp_bridge/config/hooks_manifest.json +138 -0
- mcp_bridge/config/rate_limits.py +317 -0
- mcp_bridge/config/skills_manifest.json +128 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +19 -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 +43 -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/tool_messaging.py +113 -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 +150 -0
- 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 +542 -59
- mcp_bridge/server_tools.py +738 -6
- mcp_bridge/tools/__init__.py +40 -25
- mcp_bridge/tools/agent_manager.py +616 -697
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +70 -53
- 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 +12 -5
- mcp_bridge/tools/lsp/manager.py +471 -0
- mcp_bridge/tools/lsp/tools.py +723 -207
- mcp_bridge/tools/model_invoke.py +1195 -273
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +406 -0
- 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 +3627 -0
- 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 +585 -0
- mcp_bridge/update_manager_pypi.py +297 -0
- 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.66.dist-info/METADATA +517 -0
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.2.67.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.2.67.dist-info/METADATA +0 -284
- stravinsky-0.2.67.dist-info/RECORD +0 -76
- {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Native Watcher Integration - Rust-based file watching.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import threading
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Callable
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class NativeFileWatcher:
|
|
17
|
+
"""
|
|
18
|
+
Python wrapper for the Rust-based stravinsky_watcher binary.
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, project_path: str, on_change: Callable[[str, str], None]):
|
|
21
|
+
self.project_path = os.path.abspath(project_path)
|
|
22
|
+
self.on_change = on_change
|
|
23
|
+
self.process: Optional[subprocess.Popen] = None
|
|
24
|
+
self._stop_event = threading.Event()
|
|
25
|
+
self._thread: Optional[threading.Thread] = None
|
|
26
|
+
|
|
27
|
+
def _get_binary_path(self) -> Path:
|
|
28
|
+
"""Find the stravinsky_watcher binary."""
|
|
29
|
+
# Try relative to this file
|
|
30
|
+
root_dir = Path(__file__).parent.parent
|
|
31
|
+
candidates = [
|
|
32
|
+
root_dir / "rust_native" / "target" / "release" / "stravinsky_watcher",
|
|
33
|
+
root_dir / "rust_native" / "target" / "debug" / "stravinsky_watcher",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
for c in candidates:
|
|
37
|
+
if c.exists():
|
|
38
|
+
return c
|
|
39
|
+
|
|
40
|
+
raise FileNotFoundError("stravinsky_watcher binary not found. Build it with cargo first.")
|
|
41
|
+
|
|
42
|
+
def start(self):
|
|
43
|
+
"""Start the native watcher process in a background thread."""
|
|
44
|
+
if self.process:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
binary_path = self._get_binary_path()
|
|
48
|
+
logger.info(f"Starting native watcher: {binary_path} {self.project_path}")
|
|
49
|
+
|
|
50
|
+
self.process = subprocess.Popen(
|
|
51
|
+
[str(binary_path), self.project_path],
|
|
52
|
+
stdout=subprocess.PIPE,
|
|
53
|
+
stderr=subprocess.PIPE,
|
|
54
|
+
text=True,
|
|
55
|
+
bufsize=1
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self._thread = threading.Thread(target=self._read_stdout, daemon=True)
|
|
59
|
+
self._thread.start()
|
|
60
|
+
|
|
61
|
+
def stop(self):
|
|
62
|
+
"""Stop the native watcher process."""
|
|
63
|
+
self._stop_event.set()
|
|
64
|
+
|
|
65
|
+
if self.process:
|
|
66
|
+
logger.info(f"Stopping native watcher process (PID: {self.process.pid})")
|
|
67
|
+
# Try to terminate gracefully first
|
|
68
|
+
self.process.terminate()
|
|
69
|
+
try:
|
|
70
|
+
self.process.wait(timeout=1.0)
|
|
71
|
+
except subprocess.TimeoutExpired:
|
|
72
|
+
logger.warning(f"Native watcher (PID: {self.process.pid}) did not terminate, killing...")
|
|
73
|
+
self.process.kill()
|
|
74
|
+
try:
|
|
75
|
+
self.process.wait(timeout=1.0)
|
|
76
|
+
except subprocess.TimeoutExpired:
|
|
77
|
+
logger.error(f"Failed to kill native watcher (PID: {self.process.pid})")
|
|
78
|
+
|
|
79
|
+
# Close streams
|
|
80
|
+
if self.process.stdout:
|
|
81
|
+
self.process.stdout.close()
|
|
82
|
+
if self.process.stderr:
|
|
83
|
+
self.process.stderr.close()
|
|
84
|
+
|
|
85
|
+
self.process = None
|
|
86
|
+
|
|
87
|
+
# Wait for reader thread to exit
|
|
88
|
+
if self._thread and self._thread.is_alive():
|
|
89
|
+
# Don't join with timeout in main thread if it might block,
|
|
90
|
+
# but since we closed stdout, the reader loop should break.
|
|
91
|
+
self._thread.join(timeout=1.0)
|
|
92
|
+
if self._thread.is_alive():
|
|
93
|
+
logger.warning("Native watcher reader thread did not exit cleanly")
|
|
94
|
+
self._thread = None
|
|
95
|
+
|
|
96
|
+
def _read_stdout(self):
|
|
97
|
+
"""Read JSON events from the watcher's stdout."""
|
|
98
|
+
if not self.process or not self.process.stdout:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
for line in self.process.stdout:
|
|
102
|
+
if self._stop_event.is_set():
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
line = line.strip()
|
|
106
|
+
if not line or not line.startswith("{"):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
event = json.loads(line)
|
|
111
|
+
change_type = event.get("type", "unknown")
|
|
112
|
+
path = event.get("path", "")
|
|
113
|
+
self.on_change(change_type, path)
|
|
114
|
+
except json.JSONDecodeError:
|
|
115
|
+
logger.error(f"Failed to decode watcher event: {line}")
|
|
116
|
+
|
|
117
|
+
def is_running(self) -> bool:
|
|
118
|
+
return self.process is not None and self.process.poll() is None
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Desktop Notifications Manager for Stravinsky.
|
|
3
|
+
|
|
4
|
+
Provides cross-platform desktop notifications (macOS, Linux, Windows)
|
|
5
|
+
for long-running operations like codebase indexing.
|
|
6
|
+
|
|
7
|
+
Supports:
|
|
8
|
+
- Non-blocking async notifications
|
|
9
|
+
- Platform-specific backends
|
|
10
|
+
- Notification queuing
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import platform
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NotificationManager:
|
|
22
|
+
"""
|
|
23
|
+
Cross-platform desktop notification manager.
|
|
24
|
+
|
|
25
|
+
Provides non-blocking notifications with automatic platform detection.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, app_name: str = "Stravinsky"):
|
|
29
|
+
self.app_name = app_name
|
|
30
|
+
self.system = platform.system()
|
|
31
|
+
|
|
32
|
+
def _get_notification_command(
|
|
33
|
+
self,
|
|
34
|
+
title: str,
|
|
35
|
+
message: str,
|
|
36
|
+
sound: bool = True
|
|
37
|
+
) -> list | None:
|
|
38
|
+
"""Get platform-specific notification command."""
|
|
39
|
+
if self.system == "Darwin": # macOS
|
|
40
|
+
script = f'display notification "{message}" with title "{title}"'
|
|
41
|
+
if sound:
|
|
42
|
+
script += ' sound name "Glass"'
|
|
43
|
+
return ["osascript", "-e", script]
|
|
44
|
+
|
|
45
|
+
elif self.system == "Linux":
|
|
46
|
+
cmd = ["notify-send", "--app-name", self.app_name, title, message]
|
|
47
|
+
if sound:
|
|
48
|
+
cmd.extend(["--urgency=normal"])
|
|
49
|
+
return cmd
|
|
50
|
+
|
|
51
|
+
elif self.system == "Windows":
|
|
52
|
+
ps_script = f"""
|
|
53
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
54
|
+
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
55
|
+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
|
56
|
+
|
|
57
|
+
$template = @"
|
|
58
|
+
<toast>
|
|
59
|
+
<visual>
|
|
60
|
+
<binding template="ToastGeneric">
|
|
61
|
+
<text>{title}</text>
|
|
62
|
+
<text>{message}</text>
|
|
63
|
+
</binding>
|
|
64
|
+
</visual>
|
|
65
|
+
</toast>
|
|
66
|
+
"@
|
|
67
|
+
|
|
68
|
+
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
69
|
+
$xml.LoadXml($template)
|
|
70
|
+
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
71
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("{self.app_name}").Show($toast)
|
|
72
|
+
"""
|
|
73
|
+
return ["powershell", "-Command", ps_script]
|
|
74
|
+
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def _send_notification_sync(
|
|
78
|
+
self,
|
|
79
|
+
title: str,
|
|
80
|
+
message: str,
|
|
81
|
+
sound: bool = True
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""Send notification synchronously (blocking)."""
|
|
84
|
+
cmd = self._get_notification_command(title, message, sound)
|
|
85
|
+
|
|
86
|
+
if not cmd:
|
|
87
|
+
logger.warning(
|
|
88
|
+
f"[Notifications] Desktop notifications not supported on {self.system}"
|
|
89
|
+
)
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
subprocess.Popen(
|
|
94
|
+
cmd,
|
|
95
|
+
stdout=subprocess.DEVNULL,
|
|
96
|
+
stderr=subprocess.DEVNULL,
|
|
97
|
+
start_new_session=True
|
|
98
|
+
)
|
|
99
|
+
logger.debug(f"[Notifications] Sent: {title}")
|
|
100
|
+
return True
|
|
101
|
+
except FileNotFoundError:
|
|
102
|
+
logger.warning(f"[Notifications] Command not found: {cmd[0]}")
|
|
103
|
+
return False
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f"[Notifications] Failed to send notification: {e}")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
async def notify_reindex_start(self, project_path: str) -> bool:
|
|
109
|
+
"""Notify that codebase reindexing has started."""
|
|
110
|
+
path = Path(project_path).name or Path(project_path).parent.name
|
|
111
|
+
title = "Codebase Indexing Started"
|
|
112
|
+
message = f"Indexing {path}..."
|
|
113
|
+
return self._send_notification_sync(title, message, sound=True)
|
|
114
|
+
|
|
115
|
+
async def notify_reindex_complete(self, stats: dict) -> bool:
|
|
116
|
+
"""Notify that codebase reindexing is complete."""
|
|
117
|
+
indexed = stats.get("indexed", 0)
|
|
118
|
+
pruned = stats.get("pruned", 0)
|
|
119
|
+
time_taken = stats.get("time_taken", 0)
|
|
120
|
+
|
|
121
|
+
title = "Codebase Indexing Complete"
|
|
122
|
+
message = f"Indexed {indexed} chunks, pruned {pruned} stale entries in {time_taken}s"
|
|
123
|
+
|
|
124
|
+
return self._send_notification_sync(title, message, sound=True)
|
|
125
|
+
|
|
126
|
+
async def notify_reindex_error(self, error_message: str) -> bool:
|
|
127
|
+
"""Notify that codebase reindexing failed."""
|
|
128
|
+
title = "Codebase Indexing Failed"
|
|
129
|
+
# Truncate long error messages
|
|
130
|
+
message = error_message[:100] + "..." if len(error_message) > 100 else error_message
|
|
131
|
+
|
|
132
|
+
return self._send_notification_sync(title, message, sound=True)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Global singleton instance
|
|
136
|
+
_notification_manager: NotificationManager | None = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_notification_manager() -> NotificationManager:
|
|
140
|
+
"""Get or create the global notification manager instance."""
|
|
141
|
+
global _notification_manager
|
|
142
|
+
if _notification_manager is None:
|
|
143
|
+
_notification_manager = NotificationManager()
|
|
144
|
+
return _notification_manager
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def reset_notification_manager() -> None:
|
|
148
|
+
"""Reset the global notification manager (for testing)."""
|
|
149
|
+
global _notification_manager
|
|
150
|
+
_notification_manager = None
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ..routing.config import load_routing_config
|
|
4
|
+
from ..routing.provider_state import get_provider_tracker
|
|
5
|
+
from ..routing.task_classifier import TaskType, classify_and_route
|
|
6
|
+
from ..routing.model_tiers import get_oauth_fallback_chain
|
|
7
|
+
from .enums import OrchestrationPhase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ModelConfig:
|
|
12
|
+
planning_model: str = "gemini-3-pro" # Default smart
|
|
13
|
+
execution_model: str = "gemini-3-flash" # Default fast
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Router:
|
|
17
|
+
"""
|
|
18
|
+
Intelligent model router with multi-provider fallback and task-based routing.
|
|
19
|
+
|
|
20
|
+
Features:
|
|
21
|
+
- Automatic fallback when providers hit rate limits
|
|
22
|
+
- Task-based routing to optimal models (code gen → OpenAI, docs → Gemini)
|
|
23
|
+
- Provider state tracking with cooldown management
|
|
24
|
+
- Project-local configuration support
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: ModelConfig | None = None, project_path: str = "."):
|
|
28
|
+
self.config = config or ModelConfig()
|
|
29
|
+
self.project_path = project_path
|
|
30
|
+
|
|
31
|
+
# Load routing configuration
|
|
32
|
+
self.routing_config = load_routing_config(project_path)
|
|
33
|
+
|
|
34
|
+
# Get provider state tracker singleton
|
|
35
|
+
self.provider_tracker = get_provider_tracker()
|
|
36
|
+
|
|
37
|
+
def select_model(
|
|
38
|
+
self,
|
|
39
|
+
phase: OrchestrationPhase,
|
|
40
|
+
task_type: TaskType | None = None,
|
|
41
|
+
prompt: str | None = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Selects the best model for the given phase and task type.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
phase: Orchestration phase (PLAN, EXECUTE, etc.)
|
|
48
|
+
task_type: Optional task type override (CODE_GENERATION, DEBUGGING, etc.)
|
|
49
|
+
prompt: Optional prompt text for automatic task classification
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Model identifier string (e.g., "gemini-3-flash", "gpt-5.2-codex")
|
|
53
|
+
"""
|
|
54
|
+
# If task_type is provided or can be inferred, use task-based routing
|
|
55
|
+
if task_type or prompt:
|
|
56
|
+
# classify_and_route returns (TaskType, provider, model) tuple
|
|
57
|
+
# Extract just the TaskType
|
|
58
|
+
if task_type:
|
|
59
|
+
inferred_type = task_type
|
|
60
|
+
else:
|
|
61
|
+
# classify_and_route returns tuple: (TaskType, provider, model)
|
|
62
|
+
classification_result = classify_and_route(prompt or "")
|
|
63
|
+
inferred_type = classification_result[0] # Extract TaskType from tuple
|
|
64
|
+
|
|
65
|
+
model = self._select_by_task_type(inferred_type)
|
|
66
|
+
|
|
67
|
+
# Check provider availability and fallback if needed
|
|
68
|
+
return self._check_availability_and_fallback(model, inferred_type)
|
|
69
|
+
|
|
70
|
+
# Fallback to phase-based routing (legacy behavior)
|
|
71
|
+
if phase in [
|
|
72
|
+
OrchestrationPhase.PLAN,
|
|
73
|
+
OrchestrationPhase.VALIDATE,
|
|
74
|
+
OrchestrationPhase.WISDOM,
|
|
75
|
+
OrchestrationPhase.VERIFY,
|
|
76
|
+
]:
|
|
77
|
+
model = self.config.planning_model
|
|
78
|
+
else:
|
|
79
|
+
model = self.config.execution_model
|
|
80
|
+
|
|
81
|
+
return self._check_availability_and_fallback(model, None)
|
|
82
|
+
|
|
83
|
+
def _select_by_task_type(self, task_type: TaskType) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Select model based on task type using routing config.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
task_type: Classified task type
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Primary model for this task type
|
|
92
|
+
"""
|
|
93
|
+
# Look up task routing rule in config (it's a dict[str, TaskRoutingRule])
|
|
94
|
+
task_name = task_type.name.lower()
|
|
95
|
+
if task_name in self.routing_config.task_routing:
|
|
96
|
+
rule = self.routing_config.task_routing[task_name]
|
|
97
|
+
if rule.model:
|
|
98
|
+
return rule.model
|
|
99
|
+
|
|
100
|
+
# Fallback to default execution model if no rule matches
|
|
101
|
+
return self.config.execution_model
|
|
102
|
+
|
|
103
|
+
def _check_availability_and_fallback(self, model: str, task_type: TaskType | None) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Check provider availability and apply fallback if rate-limited.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
model: Desired model
|
|
109
|
+
task_type: Task type (for finding appropriate fallback)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Available model (original or fallback)
|
|
113
|
+
"""
|
|
114
|
+
# Determine provider from model name
|
|
115
|
+
provider = self._get_provider_from_model(model)
|
|
116
|
+
|
|
117
|
+
# Check provider availability using is_available() method
|
|
118
|
+
if self.provider_tracker.is_available(provider):
|
|
119
|
+
return model
|
|
120
|
+
|
|
121
|
+
# Prefer tier-aware OAuth fallback chain when possible.
|
|
122
|
+
# Note: use_oauth=False candidates represent non-OAuth access
|
|
123
|
+
# (currently meaningful for Gemini API key fallback).
|
|
124
|
+
try:
|
|
125
|
+
oauth_chain = get_oauth_fallback_chain(provider, model)
|
|
126
|
+
except ValueError:
|
|
127
|
+
oauth_chain = []
|
|
128
|
+
|
|
129
|
+
for candidate_provider, candidate_model, use_oauth in oauth_chain:
|
|
130
|
+
if not use_oauth and candidate_provider == "gemini":
|
|
131
|
+
return candidate_model
|
|
132
|
+
if self.provider_tracker.is_available(candidate_provider):
|
|
133
|
+
return candidate_model
|
|
134
|
+
|
|
135
|
+
# Provider unavailable - use global fallback chain (legacy)
|
|
136
|
+
# NOTE: Task-specific fallbacks from routing config are not currently
|
|
137
|
+
# implemented (TaskRoutingRule doesn't have fallback_models field)
|
|
138
|
+
for fallback_provider in self.routing_config.fallback.chain:
|
|
139
|
+
if self.provider_tracker.is_available(fallback_provider):
|
|
140
|
+
# Map provider to default model
|
|
141
|
+
return self._get_default_model_for_provider(fallback_provider)
|
|
142
|
+
|
|
143
|
+
# All providers unavailable - return original model and let caller handle error
|
|
144
|
+
return model
|
|
145
|
+
|
|
146
|
+
def _get_provider_from_model(self, model: str) -> str:
|
|
147
|
+
"""Extract provider name from model identifier."""
|
|
148
|
+
if "gemini" in model.lower():
|
|
149
|
+
return "gemini"
|
|
150
|
+
elif "gpt" in model.lower() or "openai" in model.lower():
|
|
151
|
+
return "openai"
|
|
152
|
+
elif "claude" in model.lower():
|
|
153
|
+
return "claude"
|
|
154
|
+
else:
|
|
155
|
+
# Default to gemini for unknown models
|
|
156
|
+
return "gemini"
|
|
157
|
+
|
|
158
|
+
def _get_default_model_for_provider(self, provider: str) -> str:
|
|
159
|
+
"""Get default model for a provider."""
|
|
160
|
+
defaults = {
|
|
161
|
+
"gemini": "gemini-3-flash",
|
|
162
|
+
"openai": "gpt-5.2-codex",
|
|
163
|
+
"claude": "claude-sonnet-4.5",
|
|
164
|
+
}
|
|
165
|
+
return defaults.get(provider, "gemini-3-flash")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import List, Dict, Optional, Callable
|
|
2
|
+
from .enums import OrchestrationPhase
|
|
3
|
+
|
|
4
|
+
class OrchestratorState:
|
|
5
|
+
def __init__(self, enable_phase_gates: bool = False, approver: Optional[Callable[[], bool]] = None):
|
|
6
|
+
self.current_phase = OrchestrationPhase.CLASSIFY
|
|
7
|
+
self.history: List[OrchestrationPhase] = []
|
|
8
|
+
self.artifacts: Dict[str, str] = {}
|
|
9
|
+
self.enable_phase_gates = enable_phase_gates
|
|
10
|
+
self.approver = approver
|
|
11
|
+
|
|
12
|
+
def transition_to(self, next_phase: OrchestrationPhase):
|
|
13
|
+
"""Transitions to the next phase if requirements are met."""
|
|
14
|
+
self._validate_transition(next_phase)
|
|
15
|
+
|
|
16
|
+
# Phase Gates
|
|
17
|
+
if self.enable_phase_gates and self.approver:
|
|
18
|
+
if not self.approver():
|
|
19
|
+
raise PermissionError(f"Transition to {next_phase} denied by user.")
|
|
20
|
+
|
|
21
|
+
self.history.append(self.current_phase)
|
|
22
|
+
self.current_phase = next_phase
|
|
23
|
+
|
|
24
|
+
def register_artifact(self, name: str, content: str):
|
|
25
|
+
self.artifacts[name] = content
|
|
26
|
+
|
|
27
|
+
def _validate_transition(self, next_phase: OrchestrationPhase):
|
|
28
|
+
"""Enforce strict phase requirements."""
|
|
29
|
+
# Example: Must have plan before validation
|
|
30
|
+
if next_phase == OrchestrationPhase.VALIDATE:
|
|
31
|
+
if "plan.md" not in self.artifacts and self.current_phase == OrchestrationPhase.PLAN:
|
|
32
|
+
raise ValueError("Missing artifact: plan.md is required to enter Validation phase")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .enums import OrchestrationPhase
|
|
2
|
+
|
|
3
|
+
def format_phase_progress(current_phase: OrchestrationPhase) -> str:
|
|
4
|
+
"""Formats the current phase as a progress string."""
|
|
5
|
+
phases = list(OrchestrationPhase)
|
|
6
|
+
total = len(phases)
|
|
7
|
+
|
|
8
|
+
# Find index (1-based)
|
|
9
|
+
try:
|
|
10
|
+
index = phases.index(current_phase) + 1
|
|
11
|
+
except ValueError:
|
|
12
|
+
index = 0
|
|
13
|
+
|
|
14
|
+
return f"[Phase {index}/{total}: {current_phase.name}]"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
class WisdomLoader:
|
|
5
|
+
def __init__(self, wisdom_path: str = ".stravinsky/wisdom.md"):
|
|
6
|
+
self.wisdom_path = wisdom_path
|
|
7
|
+
|
|
8
|
+
def load_wisdom(self) -> str:
|
|
9
|
+
"""Loads project wisdom/learnings."""
|
|
10
|
+
if os.path.exists(self.wisdom_path):
|
|
11
|
+
try:
|
|
12
|
+
with open(self.wisdom_path, "r") as f:
|
|
13
|
+
return f.read()
|
|
14
|
+
except Exception:
|
|
15
|
+
return ""
|
|
16
|
+
return ""
|
|
17
|
+
|
|
18
|
+
class CritiqueGenerator:
|
|
19
|
+
def generate_critique_prompt(self, plan_content: str) -> str:
|
|
20
|
+
"""Generates a prompt for self-critique."""
|
|
21
|
+
return f"""
|
|
22
|
+
You are currently in the CRITIQUE phase.
|
|
23
|
+
Review the following plan and identify potential weaknesses.
|
|
24
|
+
|
|
25
|
+
PLAN:
|
|
26
|
+
{plan_content}
|
|
27
|
+
|
|
28
|
+
INSTRUCTIONS:
|
|
29
|
+
1. List 3 ways this plan could fail (edge cases, race conditions, missing context).
|
|
30
|
+
2. Check if it violates any items in the 'Wisdom' file (if provided).
|
|
31
|
+
3. Propose specific improvements.
|
|
32
|
+
|
|
33
|
+
Respond with your critique.
|
|
34
|
+
"""
|
mcp_bridge/prompts/__init__.py
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
# Agent prompts module
|
|
2
|
-
from . import stravinsky
|
|
3
|
-
from . import delphi
|
|
4
|
-
from . import dewey
|
|
5
|
-
from . import explore
|
|
6
|
-
from . import frontend
|
|
7
|
-
from . import document_writer
|
|
8
|
-
from . import multimodal
|
|
9
|
-
from . import planner
|
|
2
|
+
from . import delphi, dewey, document_writer, explore, frontend, multimodal, planner, stravinsky
|
|
10
3
|
|
|
11
4
|
__all__ = [
|
|
12
5
|
"stravinsky",
|
mcp_bridge/prompts/dewey.py
CHANGED
|
@@ -44,7 +44,7 @@ Your job: Answer questions about open-source libraries by finding **EVIDENCE** w
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
## PHASE 0: REQUEST CLASSIFICATION (MANDATORY
|
|
47
|
+
## PHASE 0: REQUEST CLASSIFICATION (MANDATORY FUWT STEP)
|
|
48
48
|
|
|
49
49
|
Classify EVERY request into one of these categories before taking action:
|
|
50
50
|
|
mcp_bridge/prompts/planner.py
CHANGED
|
@@ -13,8 +13,6 @@ Key capabilities:
|
|
|
13
13
|
- Structured plan output for orchestrator consumption
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from typing import Optional
|
|
17
|
-
|
|
18
16
|
|
|
19
17
|
PLANNER_ROLE = """<Role>
|
|
20
18
|
You are "Planner" - a pre-implementation planning specialist.
|
|
@@ -177,8 +175,8 @@ Before finalizing plan, verify:
|
|
|
177
175
|
|
|
178
176
|
def get_planner_prompt(
|
|
179
177
|
task_description: str,
|
|
180
|
-
project_context:
|
|
181
|
-
existing_patterns:
|
|
178
|
+
project_context: str | None = None,
|
|
179
|
+
existing_patterns: str | None = None,
|
|
182
180
|
) -> str:
|
|
183
181
|
"""
|
|
184
182
|
Generate the complete planner prompt.
|