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.

Files changed (184) 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 +0 -1
  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/__init__.py +2 -2
  11. mcp_bridge/config/hook_config.py +3 -5
  12. mcp_bridge/config/rate_limits.py +108 -13
  13. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  14. mcp_bridge/hooks/__init__.py +14 -4
  15. mcp_bridge/hooks/agent_reminder.py +4 -4
  16. mcp_bridge/hooks/auto_slash_command.py +5 -5
  17. mcp_bridge/hooks/budget_optimizer.py +2 -2
  18. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  19. mcp_bridge/hooks/comment_checker.py +3 -4
  20. mcp_bridge/hooks/compaction.py +2 -2
  21. mcp_bridge/hooks/context.py +2 -1
  22. mcp_bridge/hooks/context_monitor.py +2 -2
  23. mcp_bridge/hooks/delegation_policy.py +85 -0
  24. mcp_bridge/hooks/directory_context.py +3 -3
  25. mcp_bridge/hooks/edit_recovery.py +3 -2
  26. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  27. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  28. mcp_bridge/hooks/events.py +160 -0
  29. mcp_bridge/hooks/git_noninteractive.py +4 -4
  30. mcp_bridge/hooks/keyword_detector.py +8 -10
  31. mcp_bridge/hooks/manager.py +35 -22
  32. mcp_bridge/hooks/notification_hook.py +13 -6
  33. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  34. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  35. mcp_bridge/hooks/parallel_execution.py +22 -10
  36. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  37. mcp_bridge/hooks/pre_compact.py +8 -9
  38. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  39. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  40. mcp_bridge/hooks/routing_notifications.py +80 -0
  41. mcp_bridge/hooks/rules_injector.py +11 -19
  42. mcp_bridge/hooks/session_idle.py +4 -4
  43. mcp_bridge/hooks/session_notifier.py +4 -4
  44. mcp_bridge/hooks/session_recovery.py +4 -5
  45. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  46. mcp_bridge/hooks/subagent_stop.py +1 -3
  47. mcp_bridge/hooks/task_validator.py +2 -2
  48. mcp_bridge/hooks/tmux_manager.py +7 -8
  49. mcp_bridge/hooks/todo_delegation.py +4 -1
  50. mcp_bridge/hooks/todo_enforcer.py +180 -10
  51. mcp_bridge/hooks/truncation_policy.py +37 -0
  52. mcp_bridge/hooks/truncator.py +1 -2
  53. mcp_bridge/metrics/cost_tracker.py +115 -0
  54. mcp_bridge/native_search.py +93 -0
  55. mcp_bridge/native_watcher.py +118 -0
  56. mcp_bridge/notifications.py +3 -4
  57. mcp_bridge/orchestrator/enums.py +11 -0
  58. mcp_bridge/orchestrator/router.py +165 -0
  59. mcp_bridge/orchestrator/state.py +32 -0
  60. mcp_bridge/orchestrator/visualization.py +14 -0
  61. mcp_bridge/orchestrator/wisdom.py +34 -0
  62. mcp_bridge/prompts/__init__.py +1 -8
  63. mcp_bridge/prompts/dewey.py +1 -1
  64. mcp_bridge/prompts/planner.py +2 -4
  65. mcp_bridge/prompts/stravinsky.py +53 -31
  66. mcp_bridge/proxy/__init__.py +0 -0
  67. mcp_bridge/proxy/client.py +70 -0
  68. mcp_bridge/proxy/model_server.py +157 -0
  69. mcp_bridge/routing/__init__.py +43 -0
  70. mcp_bridge/routing/config.py +250 -0
  71. mcp_bridge/routing/model_tiers.py +135 -0
  72. mcp_bridge/routing/provider_state.py +261 -0
  73. mcp_bridge/routing/task_classifier.py +190 -0
  74. mcp_bridge/server.py +363 -34
  75. mcp_bridge/server_tools.py +298 -6
  76. mcp_bridge/tools/__init__.py +19 -8
  77. mcp_bridge/tools/agent_manager.py +549 -799
  78. mcp_bridge/tools/background_tasks.py +13 -17
  79. mcp_bridge/tools/code_search.py +54 -51
  80. mcp_bridge/tools/continuous_loop.py +0 -1
  81. mcp_bridge/tools/dashboard.py +19 -0
  82. mcp_bridge/tools/find_code.py +296 -0
  83. mcp_bridge/tools/init.py +1 -0
  84. mcp_bridge/tools/list_directory.py +42 -0
  85. mcp_bridge/tools/lsp/__init__.py +8 -8
  86. mcp_bridge/tools/lsp/manager.py +51 -28
  87. mcp_bridge/tools/lsp/tools.py +98 -65
  88. mcp_bridge/tools/model_invoke.py +1047 -152
  89. mcp_bridge/tools/mux_client.py +75 -0
  90. mcp_bridge/tools/project_context.py +1 -2
  91. mcp_bridge/tools/query_classifier.py +132 -49
  92. mcp_bridge/tools/read_file.py +84 -0
  93. mcp_bridge/tools/replace.py +45 -0
  94. mcp_bridge/tools/run_shell_command.py +38 -0
  95. mcp_bridge/tools/search_enhancements.py +347 -0
  96. mcp_bridge/tools/semantic_search.py +677 -92
  97. mcp_bridge/tools/session_manager.py +0 -2
  98. mcp_bridge/tools/skill_loader.py +0 -1
  99. mcp_bridge/tools/task_runner.py +5 -7
  100. mcp_bridge/tools/templates.py +3 -3
  101. mcp_bridge/tools/tool_search.py +331 -0
  102. mcp_bridge/tools/write_file.py +29 -0
  103. mcp_bridge/update_manager.py +33 -37
  104. mcp_bridge/update_manager_pypi.py +6 -8
  105. mcp_bridge/utils/cache.py +82 -0
  106. mcp_bridge/utils/process.py +71 -0
  107. mcp_bridge/utils/session_state.py +51 -0
  108. mcp_bridge/utils/truncation.py +76 -0
  109. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/METADATA +84 -35
  110. stravinsky-0.4.66.dist-info/RECORD +198 -0
  111. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  112. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  113. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  114. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  115. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  116. stravinsky_claude_assets/agents/debugger.md +254 -0
  117. stravinsky_claude_assets/agents/delphi.md +495 -0
  118. stravinsky_claude_assets/agents/dewey.md +248 -0
  119. stravinsky_claude_assets/agents/explore.md +1198 -0
  120. stravinsky_claude_assets/agents/frontend.md +472 -0
  121. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  122. stravinsky_claude_assets/agents/momus.md +464 -0
  123. stravinsky_claude_assets/agents/research-lead.md +141 -0
  124. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  125. stravinsky_claude_assets/commands/delphi.md +9 -0
  126. stravinsky_claude_assets/commands/dewey.md +54 -0
  127. stravinsky_claude_assets/commands/git-master.md +112 -0
  128. stravinsky_claude_assets/commands/index.md +49 -0
  129. stravinsky_claude_assets/commands/publish.md +86 -0
  130. stravinsky_claude_assets/commands/review.md +73 -0
  131. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  132. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  133. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  134. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  135. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  136. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  137. stravinsky_claude_assets/commands/str/clean.md +97 -0
  138. stravinsky_claude_assets/commands/str/continue.md +38 -0
  139. stravinsky_claude_assets/commands/str/index.md +199 -0
  140. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  141. stravinsky_claude_assets/commands/str/search.md +205 -0
  142. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  143. stravinsky_claude_assets/commands/str/stats.md +71 -0
  144. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  145. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  146. stravinsky_claude_assets/commands/str/watch.md +45 -0
  147. stravinsky_claude_assets/commands/strav.md +53 -0
  148. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  149. stravinsky_claude_assets/commands/verify.md +60 -0
  150. stravinsky_claude_assets/commands/version.md +5 -0
  151. stravinsky_claude_assets/hooks/README.md +248 -0
  152. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  153. stravinsky_claude_assets/hooks/context.py +38 -0
  154. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  155. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  156. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  157. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  158. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  159. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  160. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  161. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  162. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  163. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  164. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  165. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  166. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  167. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  168. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  169. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  170. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  171. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  172. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  173. stravinsky_claude_assets/hooks/truncator.py +23 -0
  174. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  175. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  176. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  177. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  178. stravinsky_claude_assets/settings.json +152 -0
  179. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  180. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  181. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  182. stravinsky_claude_assets/task_dependencies.json +34 -0
  183. stravinsky-0.4.18.dist-info/RECORD +0 -88
  184. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PostAssistantMessage hook: RALPH Loop (Relentless Autonomous Labor Protocol with Hardening Loop)
4
+
5
+ Automatically continues working on pending todos after assistant completes a response.
6
+ Prevents the assistant from stopping with incomplete work.
7
+
8
+ SAFETY: Maximum 10 auto-continuations per session to prevent infinite loops.
9
+
10
+ Named after the mythological Sisyphus who relentlessly pushed the boulder uphill.
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import sys
16
+ from pathlib import Path
17
+ from datetime import datetime
18
+
19
+
20
+ # Use CLAUDE_CWD for reliable project directory resolution
21
+ def get_project_dir() -> Path:
22
+ """Get project directory from CLAUDE_CWD env var or fallback to cwd."""
23
+ return Path(os.environ.get("CLAUDE_CWD", "."))
24
+
25
+
26
+ # State tracking for RALPH loop safety
27
+ RALPH_STATE_FILE = get_project_dir() / ".claude" / "ralph_state.json"
28
+
29
+
30
+ def get_ralph_state() -> dict:
31
+ """Get current RALPH loop state."""
32
+ if RALPH_STATE_FILE.exists():
33
+ try:
34
+ return json.loads(RALPH_STATE_FILE.read_text())
35
+ except Exception:
36
+ pass
37
+
38
+ return {
39
+ "continuation_count": 0,
40
+ "last_reset": datetime.now().isoformat(),
41
+ "max_continuations": 10,
42
+ }
43
+
44
+
45
+ def save_ralph_state(state: dict):
46
+ """Save RALPH loop state."""
47
+ try:
48
+ RALPH_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
49
+ RALPH_STATE_FILE.write_text(json.dumps(state, indent=2))
50
+ except Exception:
51
+ pass
52
+
53
+
54
+ def reset_ralph_state_if_needed(state: dict) -> dict:
55
+ """Reset continuation count if last reset was >1 hour ago."""
56
+ try:
57
+ last_reset = datetime.fromisoformat(state.get("last_reset", datetime.now().isoformat()))
58
+ hours_since_reset = (datetime.now() - last_reset).total_seconds() / 3600
59
+
60
+ if hours_since_reset > 1:
61
+ state["continuation_count"] = 0
62
+ state["last_reset"] = datetime.now().isoformat()
63
+ except Exception:
64
+ pass
65
+
66
+ return state
67
+
68
+
69
+ def get_todo_state() -> dict:
70
+ """Get current todo state from Claude Code session or local cache."""
71
+ todo_cache = get_project_dir() / ".claude" / "todo_state.json"
72
+
73
+ if todo_cache.exists():
74
+ try:
75
+ return json.loads(todo_cache.read_text())
76
+ except Exception:
77
+ pass
78
+
79
+ return {"todos": []}
80
+
81
+
82
+ def main():
83
+ try:
84
+ # Read hook input from stdin
85
+ hook_input = json.load(sys.stdin)
86
+ except (json.JSONDecodeError, EOFError):
87
+ return 0
88
+
89
+ # Get RALPH state and reset if needed
90
+ ralph_state = get_ralph_state()
91
+ ralph_state = reset_ralph_state_if_needed(ralph_state)
92
+
93
+ # Safety check: prevent infinite loops
94
+ if ralph_state["continuation_count"] >= ralph_state["max_continuations"]:
95
+ print(
96
+ f"\n⚠️ RALPH Loop safety limit reached ({ralph_state['max_continuations']} auto-continuations).\n",
97
+ file=sys.stderr,
98
+ )
99
+ print(f"Stopping auto-continuation. Will resume on next user prompt.\n", file=sys.stderr)
100
+ # Reset for next session
101
+ ralph_state["continuation_count"] = 0
102
+ ralph_state["last_reset"] = datetime.now().isoformat()
103
+ save_ralph_state(ralph_state)
104
+ return 0
105
+
106
+ # Get current todo state
107
+ todo_state = get_todo_state()
108
+ todos = todo_state.get("todos", [])
109
+
110
+ if not todos:
111
+ # No todos tracked, nothing to continue
112
+ return 0
113
+
114
+ # Count incomplete todos
115
+ in_progress = [t for t in todos if t.get("status") == "in_progress"]
116
+ pending = [t for t in todos if t.get("status") == "pending"]
117
+
118
+ if not in_progress and not pending:
119
+ # All todos complete, reset RALPH state
120
+ ralph_state["continuation_count"] = 0
121
+ ralph_state["last_reset"] = datetime.now().isoformat()
122
+ save_ralph_state(ralph_state)
123
+ return 0
124
+
125
+ # There are incomplete todos - trigger auto-continuation
126
+ ralph_state["continuation_count"] += 1
127
+ save_ralph_state(ralph_state)
128
+
129
+ # Build continuation message
130
+ continuation_msg = [
131
+ "",
132
+ "═" * 80,
133
+ f"🔄 RALPH Loop: Auto-Continuation {ralph_state['continuation_count']}/{ralph_state['max_continuations']}",
134
+ "═" * 80,
135
+ "",
136
+ ]
137
+
138
+ if in_progress:
139
+ continuation_msg.append(f"📌 IN_PROGRESS ({len(in_progress)} items):")
140
+ for t in in_progress:
141
+ continuation_msg.append(f" • {t.get('content', 'Unknown task')}")
142
+ continuation_msg.append("")
143
+
144
+ if pending:
145
+ continuation_msg.append(f"📋 PENDING ({len(pending)} items):")
146
+ for t in pending[:5]: # Show max 5 pending
147
+ continuation_msg.append(f" • {t.get('content', 'Unknown task')}")
148
+ if len(pending) > 5:
149
+ continuation_msg.append(f" ... and {len(pending) - 5} more")
150
+ continuation_msg.append("")
151
+
152
+ continuation_msg.extend(
153
+ [
154
+ "⚡ CONTINUE WORKING:",
155
+ " 1. Mark current IN_PROGRESS todo as COMPLETED if done",
156
+ " 2. Move to next PENDING todo",
157
+ " 3. DO NOT stop until all todos are complete",
158
+ "",
159
+ "═" * 80,
160
+ "",
161
+ ]
162
+ )
163
+
164
+ # Inject continuation as a system message that triggers another assistant response
165
+ print("\n".join(continuation_msg), file=sys.stderr)
166
+
167
+ # Return non-zero to signal that work is incomplete and should continue
168
+ # This tells Claude Code to prompt the assistant for another response
169
+ return 1
170
+
171
+
172
+ if __name__ == "__main__":
173
+ sys.exit(main())
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PostToolUse hook: Session Recovery (oh-my-opencode parity)
4
+
5
+ Detects API failures, thinking block errors, and rate limits.
6
+ Logs recovery events and provides guidance for auto-retry.
7
+
8
+ This hook monitors tool responses for error patterns and:
9
+ 1. Logs errors to ~/.claude/state/recovery.jsonl
10
+ 2. Injects recovery guidance when errors are detected
11
+ 3. Tracks failure patterns for debugging
12
+
13
+ Exit codes:
14
+ - 0: Always (this hook only observes and logs)
15
+ """
16
+
17
+ import json
18
+ import sys
19
+ import os
20
+ import re
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+ from typing import Optional, Dict, Any
24
+
25
+ # State directory for recovery logs
26
+ STATE_DIR = Path.home() / ".claude" / "state"
27
+ RECOVERY_LOG = STATE_DIR / "recovery.jsonl"
28
+
29
+ # Error patterns to detect
30
+ ERROR_PATTERNS = {
31
+ "thinking_block": [
32
+ r"thinking block",
33
+ r"extended thinking",
34
+ r"thinking budget exceeded",
35
+ r"thinking timeout",
36
+ ],
37
+ "rate_limit": [
38
+ r"rate limit",
39
+ r"too many requests",
40
+ r"429",
41
+ r"quota exceeded",
42
+ r"throttl",
43
+ ],
44
+ "api_timeout": [
45
+ r"timeout",
46
+ r"timed out",
47
+ r"connection reset",
48
+ r"connection refused",
49
+ r"ETIMEDOUT",
50
+ r"ECONNRESET",
51
+ ],
52
+ "api_error": [
53
+ r"API error",
54
+ r"internal server error",
55
+ r"500",
56
+ r"502",
57
+ r"503",
58
+ r"504",
59
+ r"service unavailable",
60
+ ],
61
+ "context_overflow": [
62
+ r"context length",
63
+ r"token limit",
64
+ r"max tokens",
65
+ r"context window",
66
+ r"too long",
67
+ ],
68
+ "auth_error": [
69
+ r"unauthorized",
70
+ r"401",
71
+ r"403",
72
+ r"forbidden",
73
+ r"authentication failed",
74
+ r"token expired",
75
+ ],
76
+ }
77
+
78
+ # Recovery suggestions for each error type
79
+ RECOVERY_SUGGESTIONS = {
80
+ "thinking_block": """
81
+ **Thinking Block Error Detected**
82
+
83
+ The model's extended thinking was interrupted. Recovery options:
84
+ 1. Retry the same request (often works on second attempt)
85
+ 2. Break the task into smaller steps
86
+ 3. Reduce thinking budget if explicitly set
87
+
88
+ Automatic retry recommended.
89
+ """,
90
+ "rate_limit": """
91
+ **Rate Limit Hit**
92
+
93
+ You've hit the API rate limit. Recovery options:
94
+ 1. Wait 30-60 seconds before retrying
95
+ 2. Reduce parallel agent spawns
96
+ 3. Use cheaper models (gemini-3-flash) for exploration
97
+
98
+ Exponential backoff recommended: wait 30s, then 60s, then 120s.
99
+ """,
100
+ "api_timeout": """
101
+ **API Timeout**
102
+
103
+ The API request timed out. Recovery options:
104
+ 1. Retry immediately (network glitch)
105
+ 2. Check internet connection
106
+ 3. Reduce request complexity
107
+
108
+ Retry recommended.
109
+ """,
110
+ "api_error": """
111
+ **API Error**
112
+
113
+ The API returned an error. Recovery options:
114
+ 1. Wait 10 seconds and retry
115
+ 2. Check API status page
116
+ 3. Try a different model
117
+
118
+ Retry with backoff recommended.
119
+ """,
120
+ "context_overflow": """
121
+ **Context Window Overflow**
122
+
123
+ The request exceeded the context limit. Recovery options:
124
+ 1. Use /compact to reduce context
125
+ 2. Break the task into smaller steps
126
+ 3. Focus on specific files rather than entire codebase
127
+
128
+ Context reduction required before retry.
129
+ """,
130
+ "auth_error": """
131
+ **Authentication Error**
132
+
133
+ Authentication failed. Recovery options:
134
+ 1. Run `stravinsky-auth login gemini` or `stravinsky-auth login openai`
135
+ 2. Check if tokens have expired
136
+ 3. Verify API credentials
137
+
138
+ Re-authentication required.
139
+ """,
140
+ }
141
+
142
+
143
+ def ensure_state_dir():
144
+ """Ensure the state directory exists."""
145
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
146
+
147
+
148
+ def detect_error_type(text: str) -> Optional[str]:
149
+ """
150
+ Detect the type of error from response text.
151
+ Returns the error type or None if no error detected.
152
+ """
153
+ text_lower = text.lower()
154
+
155
+ for error_type, patterns in ERROR_PATTERNS.items():
156
+ for pattern in patterns:
157
+ if re.search(pattern, text_lower, re.IGNORECASE):
158
+ return error_type
159
+
160
+ return None
161
+
162
+
163
+ def log_recovery_event(
164
+ error_type: str, tool_name: str, response_snippet: str, session_id: Optional[str] = None
165
+ ):
166
+ """Log a recovery event to the JSONL file."""
167
+ ensure_state_dir()
168
+
169
+ event = {
170
+ "timestamp": datetime.now().isoformat(),
171
+ "error_type": error_type,
172
+ "tool_name": tool_name,
173
+ "response_snippet": response_snippet[:500], # Truncate
174
+ "session_id": session_id,
175
+ }
176
+
177
+ try:
178
+ with open(RECOVERY_LOG, "a") as f:
179
+ f.write(json.dumps(event) + "\n")
180
+ except Exception:
181
+ pass # Don't fail on logging errors
182
+
183
+
184
+ def get_recent_failures(minutes: int = 5) -> int:
185
+ """Count recent failures within the specified time window."""
186
+ if not RECOVERY_LOG.exists():
187
+ return 0
188
+
189
+ cutoff = datetime.now().timestamp() - (minutes * 60)
190
+ count = 0
191
+
192
+ try:
193
+ with open(RECOVERY_LOG, "r") as f:
194
+ for line in f:
195
+ try:
196
+ event = json.loads(line)
197
+ event_time = datetime.fromisoformat(event["timestamp"]).timestamp()
198
+ if event_time > cutoff:
199
+ count += 1
200
+ except (json.JSONDecodeError, KeyError, ValueError):
201
+ continue
202
+ except Exception:
203
+ pass
204
+
205
+ return count
206
+
207
+
208
+ def main():
209
+ try:
210
+ hook_input = json.load(sys.stdin)
211
+ except (json.JSONDecodeError, EOFError):
212
+ return 0
213
+
214
+ tool_name = hook_input.get("tool_name", "")
215
+ tool_response = hook_input.get("tool_response", "")
216
+ session_id = hook_input.get("session_id")
217
+
218
+ # Convert response to string if needed
219
+ if isinstance(tool_response, dict):
220
+ response_text = json.dumps(tool_response)
221
+ else:
222
+ response_text = str(tool_response)
223
+
224
+ # Detect error type
225
+ error_type = detect_error_type(response_text)
226
+
227
+ if error_type:
228
+ # Log the event
229
+ log_recovery_event(
230
+ error_type=error_type,
231
+ tool_name=tool_name,
232
+ response_snippet=response_text[:500],
233
+ session_id=session_id,
234
+ )
235
+
236
+ # Check for repeated failures
237
+ recent_count = get_recent_failures(minutes=5)
238
+
239
+ # Output recovery suggestion
240
+ suggestion = RECOVERY_SUGGESTIONS.get(error_type, "")
241
+
242
+ output = {
243
+ "error_detected": True,
244
+ "error_type": error_type,
245
+ "recent_failures": recent_count,
246
+ "suggestion": suggestion.strip(),
247
+ }
248
+
249
+ # If many recent failures, add escalation notice
250
+ if recent_count >= 3:
251
+ output["escalation"] = (
252
+ f"⚠️ {recent_count} failures in last 5 minutes. "
253
+ "Consider pausing and investigating the root cause."
254
+ )
255
+
256
+ # Print as JSON for downstream processing
257
+ print(json.dumps(output))
258
+
259
+ return 0
260
+
261
+
262
+ if __name__ == "__main__":
263
+ sys.exit(main())
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env -S uv run --script
2
+
3
+ # /// script
4
+ # requires-python = ">=3.8"
5
+ # dependencies = [
6
+ # "anthropic",
7
+ # "python-dotenv",
8
+ # ]
9
+
10
+ """
11
+ Fix Stop Hook - Calls stravinsky_metrics.py on Stop events.
12
+
13
+ This hook is triggered by .claude/settings.json on Stop/SubagentStop.
14
+ It queries Stravinsky's cost tracker and sends a StravinskyMetrics event to the dashboard.
15
+ """
16
+
17
+ import os
18
+ import sys
19
+ import subprocess
20
+ from pathlib import Path
21
+
22
+ # Add hooks directory to path for script imports
23
+ hooks_dir = Path(__file__).parent
24
+ sys.path.insert(0, str(hooks_dir))
25
+
26
+
27
+ def send_stravinsky_metrics(session_id: str) -> bool:
28
+ """Call stravinsky_metrics.py to query and send metrics to dashboard."""
29
+ script_path = hooks_dir / "stravinsky_metrics.py"
30
+
31
+ if not script_path.exists():
32
+ print(f"Error: stravinsky_metrics.py not found at {script_path}", file=sys.stderr)
33
+ return False
34
+
35
+ try:
36
+ # Build command
37
+ cmd = [
38
+ "uv",
39
+ "run",
40
+ "--script",
41
+ str(script_path),
42
+ "--session-id",
43
+ session_id,
44
+ "--summarize", # Generate summary and send as event
45
+ ]
46
+
47
+ # Run command
48
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=False)
49
+
50
+ if result.returncode != 0:
51
+ print(
52
+ f"Error: stravinsky_metrics.py failed with exit code {result.returncode}",
53
+ file=sys.stderr,
54
+ )
55
+ print(f"stderr: {result.stderr}", file=sys.stderr)
56
+ return False
57
+
58
+ return True
59
+
60
+ except subprocess.TimeoutExpired:
61
+ print(f"Error: stravinsky_metrics.py timed out after 10 seconds", file=sys.stderr)
62
+ return False
63
+
64
+ except Exception as e:
65
+ print(f"Error: stravinsky_metrics.py execution failed: {e}", file=sys.stderr)
66
+ return False
67
+
68
+
69
+ def main():
70
+ # Get session_id from environment or use default
71
+ session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
72
+
73
+ # Determine hook type
74
+ hook_type = os.environ.get("CLAUDE_HOOK_EVENT_TYPE", "Stop")
75
+
76
+ # Send metrics to dashboard
77
+ success = send_stravinsky_metrics(session_id)
78
+
79
+ if not success:
80
+ sys.exit(1)
81
+
82
+ print(
83
+ f"✓ Successfully sent Stravinsky metrics for session {session_id} to dashboard",
84
+ file=sys.stderr,
85
+ )
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stravinsky Metrics Hook Script
4
+
5
+ Queries Stravinsky's internal metrics and sends them to the observability dashboard.
6
+ This hook is triggered on Stop/SubagentStop events.
7
+
8
+ Usage:
9
+ stravinsky_metrics.py --session-id <session_id>
10
+
11
+ Environment Variables:
12
+ CLAUDE_SESSION_ID: Fallback session ID if --session-id not provided
13
+
14
+ Output:
15
+ Sends StravinskyMetrics event to dashboard via send_event.py
16
+
17
+ Metrics Collected:
18
+ - Total session cost (USD)
19
+ - Total tokens used
20
+ - Per-agent cost and token breakdown
21
+ - Per-model cost and token breakdown (TODO)
22
+ - Agent count (active/total) (TODO)
23
+
24
+ Error Handling:
25
+ Returns non-zero exit code on error to prevent blocking Claude Code operations.
26
+ """
27
+
28
+ import sys
29
+ import os
30
+ import json
31
+ import argparse
32
+ import subprocess
33
+ from pathlib import Path
34
+ from datetime import datetime
35
+ from typing import Dict, Any
36
+ from mcp_bridge.metrics.cost_tracker import CostTracker
37
+
38
+
39
+ def load_usage_data(session_id: str) -> Dict[str, Any]:
40
+ """
41
+ Load usage data from Stravinsky's cost tracker.
42
+
43
+ Uses CostTracker.get_session_summary() to get metrics for a session.
44
+
45
+ Args:
46
+ session_id: Claude Code session ID
47
+
48
+ Returns:
49
+ Dictionary with metrics data
50
+ """
51
+ summary = CostTracker.get_instance().get_session_summary(session_id)
52
+
53
+ # Transform CostTracker summary to expected format
54
+ metrics = {
55
+ "total_cost": summary.get("total_cost", 0),
56
+ "total_tokens": summary.get("total_tokens", 0),
57
+ "by_agent": summary.get("by_agent", {}),
58
+ "by_model": {}, # Extract from summary if available
59
+ }
60
+
61
+ return metrics
62
+
63
+
64
+
65
+
66
+ def send_metrics_event(session_id: str, metrics: Dict[str, Any]) -> bool:
67
+ """
68
+ Send metrics event to observability dashboard via send_event.py.
69
+
70
+ Args:
71
+ session_id: Claude Code session ID
72
+ metrics: Metrics data dictionary
73
+
74
+ Returns:
75
+ True if successful, False otherwise
76
+ """
77
+ hook_dir = Path(__file__).parent
78
+ send_event_script = hook_dir / "send_event.py"
79
+
80
+ if not send_event_script.exists():
81
+ print(f"Error: send_event.py not found at {send_event_script}", file=sys.stderr)
82
+ return False
83
+
84
+ event_data = {
85
+ "session_id": session_id,
86
+ "metrics": metrics,
87
+ "timestamp": datetime.now().isoformat(),
88
+ }
89
+
90
+ cmd = [
91
+ "uv",
92
+ "run",
93
+ str(send_event_script),
94
+ "--source-app",
95
+ "stravinsky",
96
+ "--event-type",
97
+ "StravinskyMetrics",
98
+ "--summarize",
99
+ ]
100
+
101
+ try:
102
+ result = subprocess.run(
103
+ cmd, input=json.dumps(event_data), capture_output=True, text=True, timeout=10
104
+ )
105
+
106
+ if result.returncode != 0:
107
+ print(f"Error from send_event.py: {result.stderr}", file=sys.stderr)
108
+ return False
109
+
110
+ return True
111
+
112
+ except subprocess.TimeoutExpired:
113
+ print("Error: send_event.py timed out", file=sys.stderr)
114
+ return False
115
+ except Exception as e:
116
+ print(f"Error running send_event.py: {e}", file=sys.stderr)
117
+ return False
118
+
119
+
120
+ def main():
121
+ parser = argparse.ArgumentParser(description="Query Stravinsky metrics and send to dashboard")
122
+ parser.add_argument(
123
+ "--session-id", help="Claude Code session ID (falls back to CLAUDE_SESSION_ID env var)"
124
+ )
125
+ parser.add_argument(
126
+ "--dry-run",
127
+ action="store_true",
128
+ help="Print metrics without sending to dashboard (for testing)",
129
+ )
130
+
131
+ args = parser.parse_args()
132
+
133
+ session_id = args.session_id or os.environ.get("CLAUDE_SESSION_ID", "default")
134
+
135
+ if not session_id or session_id == "default":
136
+ print("Warning: No session ID provided, using 'default'", file=sys.stderr)
137
+
138
+ metrics = load_usage_data(session_id)
139
+
140
+ if metrics is None:
141
+ print("Error: Failed to load metrics", file=sys.stderr)
142
+ sys.exit(1)
143
+
144
+ if args.dry_run:
145
+ print(json.dumps(metrics, indent=2))
146
+ sys.exit(0)
147
+
148
+ print(f"Stravinsky Metrics for session {session_id}:", file=sys.stderr)
149
+ print(f" Total Cost: ${metrics['total_cost']:.6f}", file=sys.stderr)
150
+ print(f" Total Tokens: {metrics['total_tokens']:,}", file=sys.stderr)
151
+ print(f" Agents: {len(metrics['by_agent'])}", file=sys.stderr)
152
+
153
+ success = send_metrics_event(session_id, metrics)
154
+
155
+ if success:
156
+ print("Metrics sent to dashboard successfully", file=sys.stderr)
157
+ sys.exit(0)
158
+ else:
159
+ print("Failed to send metrics to dashboard", file=sys.stderr)
160
+ sys.exit(1)
161
+
162
+
163
+ if __name__ == "__main__":
164
+ main()