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.
- 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 +0 -1
- 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/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +3 -5
- mcp_bridge/config/rate_limits.py +108 -13
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +14 -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 +35 -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/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 +3 -4
- 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 +363 -34
- mcp_bridge/server_tools.py +298 -6
- mcp_bridge/tools/__init__.py +19 -8
- mcp_bridge/tools/agent_manager.py +549 -799
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +54 -51
- 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 +8 -8
- mcp_bridge/tools/lsp/manager.py +51 -28
- mcp_bridge/tools/lsp/tools.py +98 -65
- mcp_bridge/tools/model_invoke.py +1047 -152
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +132 -49
- 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 +677 -92
- 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 +33 -37
- mcp_bridge/update_manager_pypi.py +6 -8
- 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.18.dist-info → stravinsky-0.4.66.dist-info}/METADATA +84 -35
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.4.18.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.4.18.dist-info/RECORD +0 -88
- {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse hook: Comment Quality Enforcer (oh-my-opencode parity)
|
|
4
|
+
|
|
5
|
+
Fires BEFORE git commit/push operations to check for low-quality comments.
|
|
6
|
+
Challenges comments that just restate what the code does.
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
- 0: Allow the operation to proceed
|
|
10
|
+
- 2: Block the operation (hard block)
|
|
11
|
+
|
|
12
|
+
Trigger: PreToolUse on Bash tool when command contains 'git commit' or 'git push'
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Tuple, Optional
|
|
20
|
+
|
|
21
|
+
# Patterns that indicate low-quality comments
|
|
22
|
+
LOW_QUALITY_PATTERNS = [
|
|
23
|
+
# Comments that just describe what code literally does
|
|
24
|
+
r"#\s*(?:set|get|return|call|create|initialize|init)\s+\w+",
|
|
25
|
+
# Comments that are just variable/function names repeated
|
|
26
|
+
r"#\s*\w+\s*$",
|
|
27
|
+
# Empty or trivial comments
|
|
28
|
+
r"#\s*(?:TODO|FIXME|XXX|HACK)?\s*$",
|
|
29
|
+
# Comments that state the obvious
|
|
30
|
+
r"#\s*(?:loop|iterate|check|if|else|for|while)\s+(?:through|over|if|the)?\s*\w*\s*$",
|
|
31
|
+
# Comments like "# increment i" or "# add 1 to x"
|
|
32
|
+
r"#\s*(?:increment|decrement|add|subtract|multiply|divide)\s+\w+",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Patterns for GOOD comments we should NOT flag
|
|
36
|
+
GOOD_COMMENT_PATTERNS = [
|
|
37
|
+
# Docstrings and multi-line comments explaining WHY
|
|
38
|
+
r'""".*"""',
|
|
39
|
+
r"'''.*'''",
|
|
40
|
+
# Comments explaining business logic or reasoning
|
|
41
|
+
r"#\s*(?:because|since|note|important|warning|caution|reason|why|rationale)",
|
|
42
|
+
# Comments with URLs or references
|
|
43
|
+
r"#\s*(?:see|ref|https?://|link)",
|
|
44
|
+
# Type hints or type comments
|
|
45
|
+
r"#\s*type:",
|
|
46
|
+
# Pragma or directive comments
|
|
47
|
+
r"#\s*(?:noqa|type:|pragma|pylint|flake8)",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_low_quality_comment(comment: str) -> bool:
|
|
52
|
+
"""Check if a comment is low-quality (just restates code)."""
|
|
53
|
+
comment_lower = comment.lower().strip()
|
|
54
|
+
|
|
55
|
+
# Skip if it matches a good pattern
|
|
56
|
+
for pattern in GOOD_COMMENT_PATTERNS:
|
|
57
|
+
if re.search(pattern, comment_lower, re.IGNORECASE):
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
# Check against low-quality patterns
|
|
61
|
+
for pattern in LOW_QUALITY_PATTERNS:
|
|
62
|
+
if re.search(pattern, comment_lower, re.IGNORECASE):
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
# Very short comments (< 10 chars after #) are often low quality
|
|
66
|
+
content = comment.replace("#", "").strip()
|
|
67
|
+
if len(content) < 10 and not any(c in content for c in ["!", "?", ":", "TODO", "FIXME"]):
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_added_comments_from_diff(diff_text: str) -> List[Tuple[str, str]]:
|
|
74
|
+
"""
|
|
75
|
+
Extract newly added comments from a git diff.
|
|
76
|
+
Returns list of (filename, comment) tuples.
|
|
77
|
+
"""
|
|
78
|
+
added_comments = []
|
|
79
|
+
current_file = None
|
|
80
|
+
|
|
81
|
+
for line in diff_text.split("\n"):
|
|
82
|
+
# Track which file we're in
|
|
83
|
+
if line.startswith("+++ b/"):
|
|
84
|
+
current_file = line[6:]
|
|
85
|
+
# Only look at added lines (starting with +, but not +++)
|
|
86
|
+
elif line.startswith("+") and not line.startswith("+++"):
|
|
87
|
+
content = line[1:] # Remove the leading +
|
|
88
|
+
# Check for Python comments
|
|
89
|
+
if "#" in content and not content.strip().startswith("#!"):
|
|
90
|
+
# Extract the comment part
|
|
91
|
+
comment_match = re.search(r"#.*$", content)
|
|
92
|
+
if comment_match:
|
|
93
|
+
comment = comment_match.group(0)
|
|
94
|
+
if current_file:
|
|
95
|
+
added_comments.append((current_file, comment))
|
|
96
|
+
|
|
97
|
+
return added_comments
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def check_staged_diff() -> Optional[str]:
|
|
101
|
+
"""
|
|
102
|
+
Check the staged diff for low-quality comments.
|
|
103
|
+
Returns a warning message if issues found, None otherwise.
|
|
104
|
+
"""
|
|
105
|
+
import subprocess
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
# Get the staged diff
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
["git", "diff", "--cached", "--unified=0"], capture_output=True, text=True, timeout=10
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if result.returncode != 0:
|
|
114
|
+
return None # Can't get diff, allow commit
|
|
115
|
+
|
|
116
|
+
diff_text = result.stdout
|
|
117
|
+
if not diff_text:
|
|
118
|
+
return None # No staged changes
|
|
119
|
+
|
|
120
|
+
# Extract and check comments
|
|
121
|
+
added_comments = extract_added_comments_from_diff(diff_text)
|
|
122
|
+
low_quality = []
|
|
123
|
+
|
|
124
|
+
for filename, comment in added_comments:
|
|
125
|
+
if is_low_quality_comment(comment):
|
|
126
|
+
low_quality.append((filename, comment))
|
|
127
|
+
|
|
128
|
+
if low_quality:
|
|
129
|
+
warning = "⚠️ **Comment Quality Check Failed**\n\n"
|
|
130
|
+
warning += "The following comments appear to just restate the code:\n\n"
|
|
131
|
+
for filename, comment in low_quality[:5]: # Limit to 5 examples
|
|
132
|
+
warning += f"- `{filename}`: `{comment[:50]}...`\n"
|
|
133
|
+
warning += "\n**Good comments explain WHY, not WHAT.**\n"
|
|
134
|
+
warning += "Consider removing or improving these comments.\n\n"
|
|
135
|
+
warning += "To proceed anyway, use: `git commit --no-verify`"
|
|
136
|
+
return warning
|
|
137
|
+
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
except subprocess.TimeoutExpired:
|
|
141
|
+
return None # Timeout, allow commit
|
|
142
|
+
except FileNotFoundError:
|
|
143
|
+
return None # Git not found, allow commit
|
|
144
|
+
except Exception:
|
|
145
|
+
return None # Any other error, allow commit
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_git_commit_command(command: str) -> bool:
|
|
149
|
+
"""Check if the command is a git commit or push."""
|
|
150
|
+
command_lower = command.lower()
|
|
151
|
+
return any(
|
|
152
|
+
pattern in command_lower
|
|
153
|
+
for pattern in [
|
|
154
|
+
"git commit",
|
|
155
|
+
"git push",
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
try:
|
|
162
|
+
hook_input = json.load(sys.stdin)
|
|
163
|
+
except (json.JSONDecodeError, EOFError):
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
tool_name = hook_input.get("tool_name", "")
|
|
167
|
+
tool_input = hook_input.get("tool_input", {})
|
|
168
|
+
|
|
169
|
+
# Only check Bash commands
|
|
170
|
+
if tool_name != "Bash":
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
command = tool_input.get("command", "")
|
|
174
|
+
|
|
175
|
+
# Only check git commit/push commands
|
|
176
|
+
if not is_git_commit_command(command):
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
# Check for low-quality comments in staged diff
|
|
180
|
+
warning = check_staged_diff()
|
|
181
|
+
|
|
182
|
+
if warning:
|
|
183
|
+
# Output warning to stderr (shown to user)
|
|
184
|
+
print(warning, file=sys.stderr)
|
|
185
|
+
# Return 0 to allow but warn, or 2 to block
|
|
186
|
+
# We'll warn but allow - users can use --no-verify to skip
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
sys.exit(main())
|
|
@@ -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,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Context Monitor Hook - Pre-emptive Compact at 70%
|
|
4
|
+
|
|
5
|
+
Fires on UserPromptSubmit to monitor context usage.
|
|
6
|
+
At 70%: Generates a pre-emptive compact summary.
|
|
7
|
+
At 85%: Critical warning.
|
|
8
|
+
|
|
9
|
+
This hook provides PROACTIVE context management, not reactive.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
# Context estimation constants
|
|
18
|
+
MAX_CONTEXT_TOKENS = 200000
|
|
19
|
+
CHARS_PER_TOKEN = 4
|
|
20
|
+
PREEMPTIVE_THRESHOLD = 0.70 # 70%
|
|
21
|
+
CRITICAL_THRESHOLD = 0.85 # 85%
|
|
22
|
+
|
|
23
|
+
# State tracking
|
|
24
|
+
STATE_DIR = Path.home() / ".claude" / "state"
|
|
25
|
+
CONTEXT_STATE_FILE = STATE_DIR / "context_monitor.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def estimate_tokens(text: str) -> int:
|
|
29
|
+
"""Estimate token count from character count."""
|
|
30
|
+
return len(text) // CHARS_PER_TOKEN
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_usage_percentage(text: str) -> float:
|
|
34
|
+
"""Calculate context window usage as percentage."""
|
|
35
|
+
tokens = estimate_tokens(text)
|
|
36
|
+
return tokens / MAX_CONTEXT_TOKENS
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ensure_state_dir():
|
|
40
|
+
"""Ensure state directory exists."""
|
|
41
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_last_compact_tokens() -> int:
|
|
45
|
+
"""Get token count at last compact recommendation."""
|
|
46
|
+
try:
|
|
47
|
+
if CONTEXT_STATE_FILE.exists():
|
|
48
|
+
state = json.loads(CONTEXT_STATE_FILE.read_text())
|
|
49
|
+
return state.get("last_compact_tokens", 0)
|
|
50
|
+
except (json.JSONDecodeError, IOError):
|
|
51
|
+
pass
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def set_last_compact_tokens(tokens: int):
|
|
56
|
+
"""Record token count at compact recommendation."""
|
|
57
|
+
ensure_state_dir()
|
|
58
|
+
try:
|
|
59
|
+
state = {"last_compact_tokens": tokens, "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
60
|
+
CONTEXT_STATE_FILE.write_text(json.dumps(state))
|
|
61
|
+
except IOError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def generate_compact_summary_prompt(conversation: str, usage_pct: float) -> str:
|
|
66
|
+
"""Generate the summary request to inject into response."""
|
|
67
|
+
return f"""
|
|
68
|
+
<system-reminder>
|
|
69
|
+
**[PRE-EMPTIVE CONTEXT OPTIMIZATION - {usage_pct:.1%} USAGE]**
|
|
70
|
+
|
|
71
|
+
Context window at {usage_pct:.1%} capacity. To prevent forced truncation, generate a summary NOW.
|
|
72
|
+
|
|
73
|
+
REQUIRED: Before continuing, output a structured summary:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
## Context Summary (Pre-emptive Compact)
|
|
77
|
+
|
|
78
|
+
### Current Task
|
|
79
|
+
[What are we working on right now?]
|
|
80
|
+
|
|
81
|
+
### Key Decisions Made
|
|
82
|
+
- [Decision 1]
|
|
83
|
+
- [Decision 2]
|
|
84
|
+
|
|
85
|
+
### Files Modified/Created
|
|
86
|
+
- [file1.py]: [what changed]
|
|
87
|
+
- [file2.py]: [what changed]
|
|
88
|
+
|
|
89
|
+
### Critical Constraints
|
|
90
|
+
- [Any MUST NOT or NEVER rules]
|
|
91
|
+
- [Architectural decisions]
|
|
92
|
+
|
|
93
|
+
### Next Steps
|
|
94
|
+
1. [Immediate next action]
|
|
95
|
+
2. [Following action]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This summary will be preserved when automatic compaction occurs.
|
|
99
|
+
</system-reminder>
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main():
|
|
104
|
+
"""Main hook entry point."""
|
|
105
|
+
try:
|
|
106
|
+
hook_input = json.load(sys.stdin)
|
|
107
|
+
except (json.JSONDecodeError, EOFError):
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
# Get the full conversation/prompt
|
|
111
|
+
conversation = hook_input.get("prompt", "")
|
|
112
|
+
if not conversation:
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
# Calculate usage
|
|
116
|
+
tokens = estimate_tokens(conversation)
|
|
117
|
+
usage = get_usage_percentage(conversation)
|
|
118
|
+
|
|
119
|
+
# Check if we already triggered at this token level (avoid spam)
|
|
120
|
+
last_compact = get_last_compact_tokens()
|
|
121
|
+
|
|
122
|
+
# Only trigger once per ~10% increase
|
|
123
|
+
token_threshold = int(MAX_CONTEXT_TOKENS * 0.10) # ~20k tokens
|
|
124
|
+
already_triggered = (tokens - last_compact) < token_threshold
|
|
125
|
+
|
|
126
|
+
if usage >= CRITICAL_THRESHOLD:
|
|
127
|
+
# Critical warning - always show
|
|
128
|
+
print(f"\n⚠️ **CRITICAL: Context at {usage:.1%}** - Forced compaction imminent!", file=sys.stderr)
|
|
129
|
+
print(" Generate summary NOW or context will be truncated.", file=sys.stderr)
|
|
130
|
+
|
|
131
|
+
if not already_triggered:
|
|
132
|
+
set_last_compact_tokens(tokens)
|
|
133
|
+
# Inject summary request
|
|
134
|
+
summary_prompt = generate_compact_summary_prompt(conversation, usage)
|
|
135
|
+
print(summary_prompt, file=sys.stderr)
|
|
136
|
+
|
|
137
|
+
elif usage >= PREEMPTIVE_THRESHOLD:
|
|
138
|
+
# Pre-emptive optimization
|
|
139
|
+
if not already_triggered:
|
|
140
|
+
print(f"\n📊 **Context at {usage:.1%}** - Pre-emptive optimization recommended", file=sys.stderr)
|
|
141
|
+
print(f" Estimated tokens: {tokens:,} / {MAX_CONTEXT_TOKENS:,}", file=sys.stderr)
|
|
142
|
+
print(" Headroom remaining: ~{:.0f}%".format((1 - usage) * 100), file=sys.stderr)
|
|
143
|
+
|
|
144
|
+
set_last_compact_tokens(tokens)
|
|
145
|
+
# Inject summary request
|
|
146
|
+
summary_prompt = generate_compact_summary_prompt(conversation, usage)
|
|
147
|
+
print(summary_prompt, file=sys.stderr)
|
|
148
|
+
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
sys.exit(main())
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_project_dir():
|
|
9
|
+
return Path(os.environ.get("CLAUDE_CWD", "."))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_dependency_graph():
|
|
13
|
+
graph_file = get_project_dir() / ".claude/task_dependencies.json"
|
|
14
|
+
if graph_file.exists():
|
|
15
|
+
try:
|
|
16
|
+
return json.loads(graph_file.read_text())
|
|
17
|
+
except Exception:
|
|
18
|
+
pass
|
|
19
|
+
return {"dependencies": {}}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def save_dependency_graph(graph):
|
|
23
|
+
graph_file = get_project_dir() / ".claude/task_dependencies.json"
|
|
24
|
+
graph_file.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
graph_file.write_text(json.dumps(graph, indent=2))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DEPENDENCY_KEYWORDS = ["after", "depends on", "requires", "once", "when", "then"]
|
|
29
|
+
PARALLEL_KEYWORDS = ["also", "meanwhile", "simultaneously", "and", "plus"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_todo_dependencies(todos):
|
|
33
|
+
dependencies = {}
|
|
34
|
+
|
|
35
|
+
for todo in todos:
|
|
36
|
+
todo_id = todo.get("id")
|
|
37
|
+
content = todo.get("content", "").lower()
|
|
38
|
+
|
|
39
|
+
has_dependency = any(kw in content for kw in DEPENDENCY_KEYWORDS)
|
|
40
|
+
is_parallel = any(kw in content for kw in PARALLEL_KEYWORDS)
|
|
41
|
+
|
|
42
|
+
dependencies[todo_id] = {
|
|
43
|
+
"deps": [],
|
|
44
|
+
"independent": not has_dependency,
|
|
45
|
+
"parallel_safe": is_parallel or not has_dependency,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return dependencies
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
try:
|
|
53
|
+
hook_input = json.load(sys.stdin)
|
|
54
|
+
except (json.JSONDecodeError, EOFError):
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
tool_name = hook_input.get("tool_name", "")
|
|
58
|
+
if tool_name != "TodoWrite":
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
tool_input = hook_input.get("tool_input", {})
|
|
62
|
+
todos = tool_input.get("todos", [])
|
|
63
|
+
|
|
64
|
+
graph = get_dependency_graph()
|
|
65
|
+
dependencies = parse_todo_dependencies(todos)
|
|
66
|
+
graph["dependencies"] = dependencies
|
|
67
|
+
save_dependency_graph(graph)
|
|
68
|
+
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
sys.exit(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,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_project_dir():
|
|
10
|
+
return Path(os.environ.get("CLAUDE_CWD", "."))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_execution_state():
|
|
14
|
+
state_file = get_project_dir() / ".claude/execution_state.json"
|
|
15
|
+
if state_file.exists():
|
|
16
|
+
try:
|
|
17
|
+
return json.loads(state_file.read_text())
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"last_10_tools": [],
|
|
23
|
+
"last_task_spawn_index": -1,
|
|
24
|
+
"pending_todos": 0,
|
|
25
|
+
"parallel_mode_active": False,
|
|
26
|
+
"last_updated": None,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def save_execution_state(state):
|
|
31
|
+
state_file = get_project_dir() / ".claude/execution_state.json"
|
|
32
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
state["last_updated"] = datetime.now().isoformat()
|
|
34
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
try:
|
|
39
|
+
hook_input = json.load(sys.stdin)
|
|
40
|
+
except (json.JSONDecodeError, EOFError):
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
tool_name = hook_input.get("tool_name", "")
|
|
44
|
+
|
|
45
|
+
state = get_execution_state()
|
|
46
|
+
state["last_10_tools"].append(tool_name)
|
|
47
|
+
state["last_10_tools"] = state["last_10_tools"][-10:]
|
|
48
|
+
|
|
49
|
+
if tool_name == "Task":
|
|
50
|
+
try:
|
|
51
|
+
index = len(state["last_10_tools"]) - 1
|
|
52
|
+
state["last_task_spawn_index"] = index
|
|
53
|
+
except:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
if tool_name == "TodoWrite":
|
|
57
|
+
tool_input = hook_input.get("tool_input", {})
|
|
58
|
+
todos = tool_input.get("todos", [])
|
|
59
|
+
pending = sum(1 for t in todos if t.get("status") == "pending")
|
|
60
|
+
state["pending_todos"] = pending
|
|
61
|
+
state["parallel_mode_active"] = pending >= 2
|
|
62
|
+
|
|
63
|
+
save_execution_state(state)
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
sys.exit(main())
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Notification hook for agent spawn messages.
|
|
4
|
+
|
|
5
|
+
Fires on Notification events to output user-friendly messages about
|
|
6
|
+
which agent was spawned, what model it uses, and what task it's doing.
|
|
7
|
+
|
|
8
|
+
Format: spawned {agent_type}:{model}('{description}')
|
|
9
|
+
Example: spawned delphi:gpt-5.2-medium('Debug xyz code')
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Optional, Dict, Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Agent display model mappings
|
|
18
|
+
AGENT_DISPLAY_MODELS = {
|
|
19
|
+
"explore": "gemini-3-flash",
|
|
20
|
+
"dewey": "gemini-3-flash",
|
|
21
|
+
"document_writer": "gemini-3-flash",
|
|
22
|
+
"multimodal": "gemini-3-flash",
|
|
23
|
+
"frontend": "gemini-3-pro-high",
|
|
24
|
+
"delphi": "gpt-5.2-medium",
|
|
25
|
+
"planner": "opus-4.5",
|
|
26
|
+
"code-reviewer": "sonnet-4.5",
|
|
27
|
+
"debugger": "sonnet-4.5",
|
|
28
|
+
"_default": "sonnet-4.5",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_agent_info(message: str) -> Optional[Dict[str, str]]:
|
|
33
|
+
"""
|
|
34
|
+
Extract agent spawn information from notification message.
|
|
35
|
+
|
|
36
|
+
Looks for patterns like:
|
|
37
|
+
- "Agent explore spawned for task..."
|
|
38
|
+
- "Spawned delphi agent: description"
|
|
39
|
+
- Task tool delegation messages
|
|
40
|
+
"""
|
|
41
|
+
message_lower = message.lower()
|
|
42
|
+
|
|
43
|
+
# Try to extract agent type from message
|
|
44
|
+
agent_type = None
|
|
45
|
+
description = ""
|
|
46
|
+
|
|
47
|
+
for agent in AGENT_DISPLAY_MODELS.keys():
|
|
48
|
+
if agent == "_default":
|
|
49
|
+
continue
|
|
50
|
+
if agent in message_lower:
|
|
51
|
+
agent_type = agent
|
|
52
|
+
# Extract description after agent name
|
|
53
|
+
idx = message_lower.find(agent)
|
|
54
|
+
description = message[idx + len(agent):].strip()[:60]
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
if not agent_type:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# Clean up description
|
|
61
|
+
description = description.strip(":-() ")
|
|
62
|
+
if not description:
|
|
63
|
+
description = "task delegated"
|
|
64
|
+
|
|
65
|
+
display_model = AGENT_DISPLAY_MODELS.get(agent_type, AGENT_DISPLAY_MODELS["_default"])
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"agent_type": agent_type,
|
|
69
|
+
"model": display_model,
|
|
70
|
+
"description": description,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
"""Main hook entry point."""
|
|
76
|
+
try:
|
|
77
|
+
hook_input = json.load(sys.stdin)
|
|
78
|
+
except (json.JSONDecodeError, EOFError):
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
# Get notification message
|
|
82
|
+
message = hook_input.get("message", "")
|
|
83
|
+
notification_type = hook_input.get("notification_type", "")
|
|
84
|
+
|
|
85
|
+
# Only process agent-related notifications
|
|
86
|
+
agent_keywords = ["agent", "spawn", "delegat", "task"]
|
|
87
|
+
if not any(kw in message.lower() for kw in agent_keywords):
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
# Extract agent info
|
|
91
|
+
agent_info = extract_agent_info(message)
|
|
92
|
+
if not agent_info:
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
# Format and output
|
|
96
|
+
output = f"spawned {agent_info['agent_type']}:{agent_info['model']}('{agent_info['description']}')"
|
|
97
|
+
print(output, file=sys.stderr)
|
|
98
|
+
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
sys.exit(main())
|