stravinsky 0.1.2__py3-none-any.whl → 0.2.7__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 CHANGED
@@ -1,5 +1 @@
1
- # Stravinsky MCP Bridge
2
- # Provides MCP tools for OAuth-authenticated access to Gemini and OpenAI models
3
-
4
- __version__ = "0.1.0"
5
- __author__ = "David Andrews"
1
+ __version__ = "0.2.7"
mcp_bridge/auth/cli.py CHANGED
@@ -9,6 +9,7 @@ import sys
9
9
  import time
10
10
 
11
11
  from .token_store import TokenStore
12
+ from ..tools.init import bootstrap_repo
12
13
  from .oauth import perform_oauth_flow as gemini_oauth, refresh_access_token as gemini_refresh
13
14
  from .openai_oauth import (
14
15
  perform_oauth_flow as openai_oauth,
@@ -184,6 +185,9 @@ def main():
184
185
  help="Provider to refresh token for",
185
186
  )
186
187
 
188
+ # init command
189
+ subparsers.add_parser("init", help="Bootstrap current repository for Stravinsky")
190
+
187
191
  args = parser.parse_args()
188
192
 
189
193
  if not args.command:
@@ -200,6 +204,9 @@ def main():
200
204
  return cmd_status(token_store)
201
205
  elif args.command == "refresh":
202
206
  return cmd_refresh(args.provider, token_store)
207
+ elif args.command == "init":
208
+ print(bootstrap_repo())
209
+ return 0
203
210
 
204
211
  return 0
205
212
 
@@ -0,0 +1,28 @@
1
+ """
2
+ Hooks initialization.
3
+ Registers all Tier 1-3 hooks into the HookManager.
4
+ """
5
+
6
+ from .manager import get_hook_manager
7
+ from .truncator import output_truncator_hook
8
+ from .edit_recovery import edit_error_recovery_hook
9
+ from .directory_context import directory_context_hook
10
+ from .compaction import context_compaction_hook
11
+ from .budget_optimizer import budget_optimizer_hook
12
+
13
+ def initialize_hooks():
14
+ """Register all available hooks."""
15
+ manager = get_hook_manager()
16
+
17
+ # Tier 1
18
+ manager.register_post_tool_call(output_truncator_hook)
19
+ manager.register_post_tool_call(edit_error_recovery_hook)
20
+
21
+ # Tier 2
22
+ manager.register_pre_model_invoke(directory_context_hook)
23
+ manager.register_pre_model_invoke(context_compaction_hook)
24
+
25
+ # Tier 3
26
+ manager.register_pre_model_invoke(budget_optimizer_hook)
27
+
28
+ # initialize_hooks()
@@ -0,0 +1,38 @@
1
+ """
2
+ Thinking budget optimizer hook.
3
+ Analyzes prompt complexity and adjusts thinking_budget for models that support it.
4
+ """
5
+
6
+ from typing import Any, Dict, Optional
7
+
8
+ REASONING_KEYWORDS = [
9
+ "architect", "design", "refactor", "debug", "complex", "optimize",
10
+ "summarize", "analyze", "explain", "why", "review", "strangler"
11
+ ]
12
+
13
+ async def budget_optimizer_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
14
+ """
15
+ Adjusts the thinking_budget based on presence of reasoning-heavy keywords.
16
+ """
17
+ model = params.get("model", "")
18
+ # Only applies to models that typically support reasoning budgets (Gemini 2.0 Thinking, GPT-o1, etc.)
19
+ if not any(m in model for m in ["thinking", "flash-thinking", "o1", "o3"]):
20
+ return None
21
+
22
+ prompt = params.get("prompt", "").lower()
23
+
24
+ # Simple heuristic
25
+ is_complex = any(keyword in prompt for keyword in REASONING_KEYWORDS)
26
+
27
+ current_budget = params.get("thinking_budget", 0)
28
+
29
+ if is_complex and current_budget < 4000:
30
+ # Increase budget for complex tasks
31
+ params["thinking_budget"] = 16000
32
+ return params
33
+ elif not is_complex and current_budget > 2000:
34
+ # Lower budget for simple tasks to save time/cost
35
+ params["thinking_budget"] = 2000
36
+ return params
37
+
38
+ return None
@@ -0,0 +1,32 @@
1
+ """
2
+ Preemptive context compaction hook.
3
+ Monitors context size and injects optimization reminders.
4
+ """
5
+
6
+ from typing import Any, Dict, Optional
7
+
8
+ THRESHOLD_CHARS = 100000 # Roughly 25k-30k tokens for typical LLM text
9
+
10
+ COMPACTION_REMINDER = """
11
+ > **[SYSTEM ALERT - CONTEXT WINDOW NEAR LIMIT]**
12
+ > The current conversation history is reaching its limits. Performance may degrade.
13
+ > Please **STOP** and perform a **Session Compaction**:
14
+ > 1. Summarize all work completed so far in a `TASK_STATE.md` (if not already done).
15
+ > 2. List all pending todos.
16
+ > 3. Clear unnecessary tool outputs from your reasoning.
17
+ > 4. Keep your next responses concise and focused only on the current sub-task.
18
+ """
19
+
20
+ async def context_compaction_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
21
+ """
22
+ Checks prompt length and injects a compaction reminder if it's too large.
23
+ """
24
+ prompt = params.get("prompt", "")
25
+
26
+ if len(prompt) > THRESHOLD_CHARS:
27
+ # Check if we haven't already injected the reminder recently
28
+ if "CONTEXT WINDOW NEAR LIMIT" not in prompt:
29
+ params["prompt"] = COMPACTION_REMINDER + prompt
30
+ return params
31
+
32
+ return None
@@ -0,0 +1,40 @@
1
+ """
2
+ Directory context injector hook.
3
+ Automatically finds and injects local AGENTS.md or README.md content based on the current context.
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional
9
+
10
+ async def directory_context_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
11
+ """
12
+ Search for AGENTS.md or README.md in the current working directory and inject them.
13
+ """
14
+ cwd = Path.cwd()
15
+
16
+ # Check for AGENTS.md or README.md
17
+ target_files = ["AGENTS.md", "README.md"]
18
+ found_file = None
19
+ for filename in target_files:
20
+ if (cwd / filename).exists():
21
+ found_file = cwd / filename
22
+ break
23
+
24
+ if not found_file:
25
+ return None
26
+
27
+ try:
28
+ content = found_file.read_text()
29
+ # Injects as a special system reminder
30
+ injection = f"\n\n### Local Directory Context ({found_file.name}):\n{content}\n"
31
+
32
+ # Modify the prompt if it exists in params
33
+ if "prompt" in params:
34
+ # Add to the beginning of the prompt as a context block
35
+ params["prompt"] = injection + params["prompt"]
36
+ return params
37
+ except Exception:
38
+ pass
39
+
40
+ return None
@@ -0,0 +1,41 @@
1
+ """
2
+ Edit error recovery hook.
3
+ Detects common mistakes in file editing and injects high-priority corrective directives.
4
+ """
5
+
6
+ import re
7
+ from typing import Any, Dict, Optional
8
+
9
+ EDIT_ERROR_PATTERNS = [
10
+ r"oldString and newString must be different",
11
+ r"oldString not found",
12
+ r"oldString found multiple times",
13
+ r"Target content not found",
14
+ r"Multiple occurrences of target content found",
15
+ ]
16
+
17
+ EDIT_RECOVERY_PROMPT = """
18
+ > **[EDIT ERROR - IMMEDIATE ACTION REQUIRED]**
19
+ > You made an Edit mistake. STOP and do this NOW:
20
+ > 1. **READ** the file immediately to see its ACTUAL current state.
21
+ > 2. **VERIFY** what the content really looks like (your assumption was wrong).
22
+ > 3. **APOLOGIZE** briefly to the user for the error.
23
+ > 4. **CONTINUE** with corrected action based on the real file content.
24
+ > **DO NOT** attempt another edit until you've read and verified the file state.
25
+ """
26
+
27
+ async def edit_error_recovery_hook(tool_name: str, arguments: Dict[str, Any], output: str) -> Optional[str]:
28
+ """
29
+ Analyzes tool output for edit errors and appends corrective directives.
30
+ """
31
+ # Check if this is an edit-related tool (handling both built-in and common MCP tools)
32
+ edit_tools = ["replace_file_content", "multi_replace_file_content", "write_to_file", "edit_file", "Edit"]
33
+
34
+ # We also check the output content for common patterns even if the tool name doesn't match perfectly
35
+ is_edit_error = any(re.search(pattern, output, re.IGNORECASE) for pattern in EDIT_ERROR_PATTERNS)
36
+
37
+ if is_edit_error or any(tool in tool_name for tool in edit_tools):
38
+ if any(re.search(pattern, output, re.IGNORECASE) for pattern in EDIT_ERROR_PATTERNS):
39
+ return output + EDIT_RECOVERY_PROMPT
40
+
41
+ return None
@@ -0,0 +1,77 @@
1
+ """
2
+ Modular Hook System for Stravinsky.
3
+ Provides interception points for tool calls and model invocations.
4
+ """
5
+
6
+ import logging
7
+ from typing import Any, Callable, Dict, List, Optional, Awaitable
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class HookManager:
12
+ """
13
+ Manages the registration and execution of hooks.
14
+ """
15
+ _instance = None
16
+
17
+ def __init__(self):
18
+ self.pre_tool_call_hooks: List[Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]] = []
19
+ self.post_tool_call_hooks: List[Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]] = []
20
+ self.pre_model_invoke_hooks: List[Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]] = []
21
+
22
+ @classmethod
23
+ def get_instance(cls):
24
+ if cls._instance is None:
25
+ cls._instance = cls()
26
+ return cls._instance
27
+
28
+ def register_pre_tool_call(self, hook: Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]):
29
+ """Run before a tool is called. Can modify arguments or return early result."""
30
+ self.pre_tool_call_hooks.append(hook)
31
+
32
+ def register_post_tool_call(self, hook: Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]):
33
+ """Run after a tool call. Can modify or recover from tool output/error."""
34
+ self.post_tool_call_hooks.append(hook)
35
+
36
+ def register_pre_model_invoke(self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]):
37
+ """Run before model invocation. Can modify prompt or parameters."""
38
+ self.pre_model_invoke_hooks.append(hook)
39
+
40
+ async def execute_pre_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> Optional[Dict[str, Any]]:
41
+ """Executes all pre-tool call hooks."""
42
+ current_args = arguments
43
+ for hook in self.pre_tool_call_hooks:
44
+ try:
45
+ modified_args = await hook(tool_name, current_args)
46
+ if modified_args is not None:
47
+ current_args = modified_args
48
+ except Exception as e:
49
+ logger.error(f"[HookManager] Error in pre_tool_call hook {hook.__name__}: {e}")
50
+ return current_args
51
+
52
+ async def execute_post_tool_call(self, tool_name: str, arguments: Dict[str, Any], output: str) -> str:
53
+ """Executes all post-tool call hooks."""
54
+ current_output = output
55
+ for hook in self.post_tool_call_hooks:
56
+ try:
57
+ modified_output = await hook(tool_name, arguments, current_output)
58
+ if modified_output is not None:
59
+ current_output = modified_output
60
+ except Exception as e:
61
+ logger.error(f"[HookManager] Error in post_tool_call hook {hook.__name__}: {e}")
62
+ return current_output
63
+
64
+ async def execute_pre_model_invoke(self, params: Dict[str, Any]) -> Dict[str, Any]:
65
+ """Executes all pre-model invoke hooks."""
66
+ current_params = params
67
+ for hook in self.pre_model_invoke_hooks:
68
+ try:
69
+ modified_params = await hook(current_params)
70
+ if modified_params is not None:
71
+ current_params = modified_params
72
+ except Exception as e:
73
+ logger.error(f"[HookManager] Error in pre_model_invoke hook {hook.__name__}: {e}")
74
+ return current_params
75
+
76
+ def get_hook_manager() -> HookManager:
77
+ return HookManager.get_instance()
@@ -0,0 +1,19 @@
1
+ """
2
+ Tool output truncator hook.
3
+ Limits the size of tool outputs to prevent context bloat.
4
+ """
5
+
6
+ from typing import Any, Dict, Optional
7
+
8
+ async def output_truncator_hook(tool_name: str, arguments: Dict[str, Any], output: str) -> Optional[str]:
9
+ """
10
+ Truncates tool output if it exceeds a certain length.
11
+ """
12
+ MAX_LENGTH = 10000 # 10k characters limit
13
+
14
+ if len(output) > MAX_LENGTH:
15
+ truncated = output[:MAX_LENGTH]
16
+ summary = f"\n\n... (Result truncated from {len(output)} chars to {MAX_LENGTH} chars) ..."
17
+ return truncated + summary
18
+
19
+ return None
@@ -0,0 +1,38 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ from pathlib import Path
5
+
6
+ def main():
7
+ try:
8
+ data = json.load(sys.stdin)
9
+ prompt = data.get("prompt", "")
10
+ except Exception:
11
+ return
12
+
13
+ cwd = Path(os.environ.get("CLAUDE_CWD", "."))
14
+
15
+ # Files to look for
16
+ context_files = ["AGENTS.md", "README.md", "CLAUDE.md"]
17
+ found_context = ""
18
+
19
+ for f in context_files:
20
+ path = cwd / f
21
+ if path.exists():
22
+ try:
23
+ content = path.read_text()
24
+ found_context += f"\n\n--- LOCAL CONTEXT: {f} ---\n{content}\n"
25
+ break # Only use one for brevity
26
+ except Exception:
27
+ pass
28
+
29
+ if found_context:
30
+ # Prepend context to prompt
31
+ # We wrap the user prompt to distinguish it
32
+ new_prompt = f"{found_context}\n\n[USER PROMPT]\n{prompt}"
33
+ print(new_prompt)
34
+ else:
35
+ print(prompt)
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1,46 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import re
5
+
6
+ def main():
7
+ # Claude Code PostToolUse inputs via Environment Variables
8
+ tool_name = os.environ.get("CLAUDE_TOOL_NAME")
9
+
10
+ # We only care about Edit/MultiEdit
11
+ if tool_name not in ["Edit", "MultiEdit"]:
12
+ return
13
+
14
+ # Read from stdin (Claude Code passes the tool response via stdin for some hook types,
15
+ # but for PostToolUse it's often better to check the environment variable if available.
16
+ # Actually, the summary says input is a JSON payload.
17
+ try:
18
+ data = json.load(sys.stdin)
19
+ tool_response = data.get("tool_response", "")
20
+ except Exception:
21
+ # Fallback to direct string if not JSON
22
+ return
23
+
24
+ # Error patterns
25
+ error_patterns = [
26
+ r"oldString not found",
27
+ r"oldString matched multiple times",
28
+ r"line numbers out of range"
29
+ ]
30
+
31
+ recovery_needed = any(re.search(p, tool_response, re.IGNORECASE) for p in error_patterns)
32
+
33
+ if recovery_needed:
34
+ correction = (
35
+ "\n\n[SYSTEM RECOVERY] It appears the Edit tool failed to find the target string. "
36
+ "Please call 'Read' on the file again to verify the current content, "
37
+ "then ensure your 'oldString' is an EXACT match including all whitespace."
38
+ )
39
+ # For PostToolUse, stdout is captured and appended/replaces output
40
+ print(tool_response + correction)
41
+ else:
42
+ # No change
43
+ print(tool_response)
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -0,0 +1,23 @@
1
+ import os
2
+ import sys
3
+ import json
4
+
5
+ MAX_CHARS = 10000
6
+
7
+ def main():
8
+ try:
9
+ data = json.load(sys.stdin)
10
+ tool_response = data.get("tool_response", "")
11
+ except Exception:
12
+ return
13
+
14
+ if len(tool_response) > MAX_CHARS:
15
+ header = f"[TRUNCATED - {len(tool_response)} chars reduced to {MAX_CHARS}]\n"
16
+ footer = "\n...[TRUNCATED]"
17
+ truncated = tool_response[:MAX_CHARS]
18
+ print(header + truncated + footer)
19
+ else:
20
+ print(tool_response)
21
+
22
+ if __name__ == "__main__":
23
+ main()
@@ -18,8 +18,9 @@ You are "Stravinsky" - Powerful AI Agent with orchestration capabilities.
18
18
  - Parsing implicit requirements from explicit requests
19
19
  - Adapting to codebase maturity (disciplined vs chaotic)
20
20
  - Delegating specialized work to the right subagents
21
- - **AGGRESSIVE PARALLEL EXECUTION** - spawn multiple subagents simultaneously
22
- - Follows user instructions. NEVER START IMPLEMENTING, UNLESS USER WANTS YOU TO IMPLEMENT SOMETHING EXPLICITLY.
21
+ **AGGRESSIVE PARALLEL EXECUTION (ULTRAWORK)** - spawn multiple subagents simultaneously for research, implementation, and testing.
22
+ - **LSP-FIRST RESEARCH**: You MUST use LSP tools (`lsp_hover`, `lsp_goto_definition`, etc.) before falling back to `grep` or `rg` for Python/TypeScript.
23
+ - Follows user instructions. NEVER START IMPLEMENTING, UNLESS USER WANTS YOU TO IMPLEMENT SOMETHING EXPLICITLY.
23
24
 
24
25
  **Operating Mode**: You NEVER work alone when specialists are available.
25
26
  **DEFAULT: SPAWN PARALLEL AGENTS for any task with 2+ independent components.**
@@ -29,13 +30,14 @@ You are "Stravinsky" - Powerful AI Agent with orchestration capabilities.
29
30
  - Deep research → `agent_spawn` parallel background agents with full tool access
30
31
  - Complex tasks → Break into components and spawn agents IN PARALLEL
31
32
 
32
- ## ULTRATHINK Protocol
33
+ ## ULTRAWORK & ULTRATHINK Protocol
33
34
 
34
- When the user says **"ULTRATHINK"**, **"think harder"**, or **"think hard"**:
35
- 1. **Override brevity** - engage in exhaustive, deep-level reasoning
36
- 2. **Multi-dimensional analysis** - examine through psychological, technical, accessibility, scalability lenses
37
- 3. **Maximum depth** - if reasoning feels easy, dig deeper until logic is irrefutable
38
- 4. **Extended thinking budget** - take the time needed for thorough deliberation
35
+ When the user says **"ULTRAWORK"**, **"ULTRATHINK"**, **"think harder"**, or **"think hard"**:
36
+ 1. **Engage ULTRAWORK** - Immediately spawn 2-4 sub-agents to handle different aspects of the task (research, plan, implementation, verification) in parallel.
37
+ 2. **Override brevity** - engage in exhaustive, deep-level reasoning
38
+ 3. **Multi-dimensional analysis** - examine through psychological, technical, accessibility, scalability lenses
39
+ 4. **Maximum depth** - if reasoning feels easy, dig deeper until logic is irrefutable
40
+ 5. **Extended thinking budget** - take the time needed for thorough deliberation
39
41
 
40
42
  </Role>"""
41
43
 
@@ -99,18 +101,19 @@ Before following existing patterns, assess whether they're worth following.
99
101
 
100
102
  STRAVINSKY_DELEGATION = """## Phase 2 - Parallel Agents & Delegation (DEFAULT BEHAVIOR)
101
103
 
102
- ### DEFAULT: Spawn Parallel Agents
104
+ ### DEFAULT: Spawn Parallel Agents (ULTRAWORK)
103
105
 
104
106
  For ANY task with 2+ independent components:
105
- 1. **Immediately spawn parallel agents** using `agent_spawn`
106
- 2. Continue working on the main task while agents execute
107
- 3. Use `agent_progress` to monitor running agents
108
- 4. Collect results with `agent_output` when ready
107
+ 1. **Immediately spawn parallel agents** using `agent_spawn`.
108
+ 2. **LSP ALWAYS**: For code tasks, ensure at least one agent is dedicated to LSP-based symbol resolution.
109
+ 3. Continue working on the main task while agents execute.
110
+ 4. Use `agent_progress` to monitor running agents.
111
+ 5. Collect results with `agent_output` when ready.
109
112
 
110
- **Examples of parallel spawning:**
111
- - "Add feature X" → Spawn: 1) research agent for examples, 2) explore agent for similar patterns, while you plan
112
- - "Fix bug in Y" → Spawn: 1) debug agent to search logs, 2) explore agent for related code
113
- - "Build component Z" → Spawn: 1) librarian for docs, 2) frontend agent for UI patterns
113
+ **Examples of ULTRAWORK parallel spawning:**
114
+ - "Add feature X" → Spawn: 1) `explore` agent for LSP/Symbol research, 2) `librarian` for external docs, 3) `delphi` for architecture plan.
115
+ - "Fix bug in Y" → Spawn: 1) `debug` agent for log analysis, 2) `explore` agent with LSP to trace call stack.
116
+ - "Build component Z" → Spawn: 1) `frontend` agent for UI, 2) `explore` for backend integration patterns.
114
117
 
115
118
  ### Agent Types:
116
119
  | Type | Purpose |
@@ -151,9 +154,16 @@ When delegating to external models or agents:
151
154
  ```
152
155
 
153
156
  ### After Delegation:
154
- - VERIFY the results work as expected
155
- - VERIFY it follows existing codebase patterns
156
157
  - VERIFY expected result came out
158
+
159
+ ### Agent Reliability Protocol (CRITICAL)
160
+
161
+ If a background agent fails or times out:
162
+ 1. **Analyze Failure**: Use `agent_progress` to see the last available output and logs.
163
+ 2. **Handle Timout**: If status is `failed` with a timeout error, break the task into smaller sub-tasks and respawn multiple agents. Increasing `timeout` is a secondary option.
164
+ 3. **Handle Error**: If the agent errored, refine the prompt to be more specific or provide more context, then use `agent_retry`.
165
+ 4. **Zombie Recovery**: If `agent_progress` detects a "Zombie" (process died), immediately `agent_retry`.
166
+ 5. **Escalation**: If a task fails 2 consecutive times, stop and ask the user or consult Delphi.
157
167
  """
158
168
 
159
169