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,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,11 @@
1
+ from enum import Enum
2
+
3
+ class OrchestrationPhase(Enum):
4
+ CLASSIFY = "classify"
5
+ CONTEXT = "context"
6
+ WISDOM = "wisdom"
7
+ PLAN = "plan"
8
+ VALIDATE = "validate"
9
+ DELEGATE = "delegate"
10
+ EXECUTE = "execute"
11
+ VERIFY = "verify"
@@ -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
+ """
@@ -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",
@@ -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 FIRST STEP)
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
 
@@ -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: Optional[str] = None,
181
- existing_patterns: Optional[str] = None,
178
+ project_context: str | None = None,
179
+ existing_patterns: str | None = None,
182
180
  ) -> str:
183
181
  """
184
182
  Generate the complete planner prompt.