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,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()
|