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
|
@@ -3,49 +3,84 @@ Todo Continuation Enforcer Hook.
|
|
|
3
3
|
|
|
4
4
|
Prevents early stopping when pending todos exist.
|
|
5
5
|
Injects a system reminder forcing the agent to complete all todos.
|
|
6
|
+
Includes evidence extraction and verification to prevent vague completion claims.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
9
|
-
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
10
12
|
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
12
14
|
|
|
13
15
|
TODO_CONTINUATION_REMINDER = """
|
|
14
|
-
[SYSTEM REMINDER - TODO CONTINUATION]
|
|
16
|
+
[SYSTEM REMINDER - TODO CONTINUATION & VERIFICATION]
|
|
15
17
|
|
|
16
18
|
You have pending todos that are NOT yet completed. You MUST continue working.
|
|
17
19
|
|
|
18
20
|
**Pending Todos:**
|
|
19
21
|
{pending_todos}
|
|
20
22
|
|
|
21
|
-
**
|
|
22
|
-
1. You CANNOT
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
23
|
+
**CRITICAL RULES:**
|
|
24
|
+
1. You CANNOT mark a todo completed without CONCRETE EVIDENCE
|
|
25
|
+
2. Evidence = file paths with line numbers (e.g., src/auth.ts:45-67) or tool output
|
|
26
|
+
3. Vague claims like "I created the file" will be REJECTED
|
|
27
|
+
4. Each completed todo MUST include: `✅ [Todo] - Evidence: path/to/file.py:123`
|
|
28
|
+
5. If you cannot provide evidence, the todo is NOT complete - keep working
|
|
29
|
+
6. Use Read tool to verify file contents before claiming completion
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
**Example GOOD completion:**
|
|
32
|
+
✅ Create auth validation → Evidence: src/auth.ts:45-67 (validateJWT function implemented)
|
|
33
|
+
|
|
34
|
+
**Example BAD completion (will be REJECTED):**
|
|
35
|
+
✅ Create auth validation → I created the validation logic
|
|
36
|
+
|
|
37
|
+
{verification_failures}
|
|
38
|
+
|
|
39
|
+
CONTINUE WORKING NOW with evidence-backed completions.
|
|
28
40
|
"""
|
|
29
41
|
|
|
30
42
|
|
|
31
|
-
async def todo_continuation_hook(params:
|
|
43
|
+
async def todo_continuation_hook(params: dict[str, Any]) -> dict[str, Any] | None:
|
|
32
44
|
"""
|
|
33
45
|
Pre-model invoke hook that checks for pending todos.
|
|
34
46
|
|
|
35
47
|
If pending todos exist, injects a reminder into the prompt
|
|
36
48
|
forcing the agent to continue working.
|
|
49
|
+
|
|
50
|
+
Also extracts evidence from agent output and verifies claims.
|
|
37
51
|
"""
|
|
38
52
|
prompt = params.get("prompt", "")
|
|
39
53
|
|
|
54
|
+
# Extract pending todos
|
|
40
55
|
pending_todos = _extract_pending_todos(prompt)
|
|
41
56
|
|
|
57
|
+
# Extract verification failures from previous output (if any)
|
|
58
|
+
verification_failures = ""
|
|
59
|
+
skip_verification = params.get("skip_verification", False)
|
|
60
|
+
|
|
61
|
+
if not skip_verification:
|
|
62
|
+
# Check if there's recent output to verify
|
|
63
|
+
# This would come from previous agent turns
|
|
64
|
+
previous_output = params.get("previous_output", "")
|
|
65
|
+
if previous_output:
|
|
66
|
+
verification_failures = _verify_agent_claims(previous_output)
|
|
67
|
+
|
|
42
68
|
if pending_todos:
|
|
43
69
|
logger.info(
|
|
44
70
|
f"[TodoEnforcer] Found {len(pending_todos)} pending todos, injecting continuation reminder"
|
|
45
71
|
)
|
|
46
72
|
|
|
47
73
|
todos_formatted = "\n".join(f"- [ ] {todo}" for todo in pending_todos)
|
|
48
|
-
|
|
74
|
+
|
|
75
|
+
# Format verification failures if any
|
|
76
|
+
failures_text = ""
|
|
77
|
+
if verification_failures:
|
|
78
|
+
failures_text = f"\n\n⚠️ VERIFICATION FAILURES FROM PREVIOUS TURN:\n{verification_failures}\n"
|
|
79
|
+
|
|
80
|
+
reminder = TODO_CONTINUATION_REMINDER.format(
|
|
81
|
+
pending_todos=todos_formatted,
|
|
82
|
+
verification_failures=failures_text
|
|
83
|
+
)
|
|
49
84
|
|
|
50
85
|
modified_prompt = prompt + "\n\n" + reminder
|
|
51
86
|
params["prompt"] = modified_prompt
|
|
@@ -73,3 +108,138 @@ def _extract_pending_todos(prompt: str) -> list:
|
|
|
73
108
|
pass
|
|
74
109
|
|
|
75
110
|
return pending
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _extract_evidence(output: str) -> dict[str, list[str]]:
|
|
114
|
+
"""
|
|
115
|
+
Extract evidence references (file paths, URLs) from agent output.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict with keys: 'files', 'urls', 'commands'
|
|
119
|
+
"""
|
|
120
|
+
evidence = {
|
|
121
|
+
"files": [],
|
|
122
|
+
"urls": [],
|
|
123
|
+
"commands": []
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# File path pattern: src/auth.ts:45 or /path/to/file.py or path/file.js:10-20
|
|
127
|
+
# Matches common file extensions and optional line numbers
|
|
128
|
+
file_pattern = r'(?:^|[\s\(\[])([\w/\._-]+\.(?:py|ts|js|tsx|jsx|go|rs|java|c|cpp|h|hpp|md|json|yaml|yml|toml|sh|rb|php|swift|kt))(?::(\d+)(?:-(\d+))?)?'
|
|
129
|
+
|
|
130
|
+
for match in re.finditer(file_pattern, output, re.MULTILINE):
|
|
131
|
+
file_path = match.group(1)
|
|
132
|
+
line_start = match.group(2)
|
|
133
|
+
line_end = match.group(3)
|
|
134
|
+
|
|
135
|
+
# Build reference string
|
|
136
|
+
if line_start:
|
|
137
|
+
if line_end:
|
|
138
|
+
ref = f"{file_path}:{line_start}-{line_end}"
|
|
139
|
+
else:
|
|
140
|
+
ref = f"{file_path}:{line_start}"
|
|
141
|
+
else:
|
|
142
|
+
ref = file_path
|
|
143
|
+
|
|
144
|
+
evidence["files"].append(ref)
|
|
145
|
+
|
|
146
|
+
# URL pattern
|
|
147
|
+
url_pattern = r'https?://[^\s\)\]>]+'
|
|
148
|
+
evidence["urls"] = re.findall(url_pattern, output)
|
|
149
|
+
|
|
150
|
+
# Command/tool usage pattern (e.g., "Used Read tool", "Ran grep")
|
|
151
|
+
command_pattern = r'(?:Used|Ran|Called|Executed)\s+(\w+(?:\s+\w+)?)\s+(?:tool|command)'
|
|
152
|
+
evidence["commands"] = re.findall(command_pattern, output, re.IGNORECASE)
|
|
153
|
+
|
|
154
|
+
return evidence
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _verify_file_claim(claim: str, file_references: list[str]) -> dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Verify a completion claim has file evidence.
|
|
160
|
+
|
|
161
|
+
This is a synchronous check - actual file existence verification
|
|
162
|
+
would require async Read tool access (not available in hooks).
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dict with 'verified' (bool) and 'reason' (str)
|
|
166
|
+
"""
|
|
167
|
+
# Check if claim has any file references
|
|
168
|
+
if not file_references:
|
|
169
|
+
return {
|
|
170
|
+
"verified": False,
|
|
171
|
+
"reason": "No file paths provided as evidence"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Check for vague language that indicates lack of actual work
|
|
175
|
+
vague_patterns = [
|
|
176
|
+
r'\bI\s+(?:created|made|wrote|added|implemented)\b', # "I created..."
|
|
177
|
+
r'\b(?:should|will|would)\s+(?:create|add|implement)\b', # Future tense
|
|
178
|
+
r'\b(?:basically|essentially|just|simply)\b', # Minimizing language
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
claim_lower = claim.lower()
|
|
182
|
+
vague_count = sum(1 for pattern in vague_patterns if re.search(pattern, claim_lower))
|
|
183
|
+
|
|
184
|
+
if vague_count >= 2:
|
|
185
|
+
return {
|
|
186
|
+
"verified": False,
|
|
187
|
+
"reason": f"Claim uses vague language without concrete evidence. Files mentioned: {', '.join(file_references[:3])}"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# If we have file references and no vague language, consider it verified
|
|
191
|
+
# (Actual file content verification would happen in a post-hook with Read access)
|
|
192
|
+
return {
|
|
193
|
+
"verified": True,
|
|
194
|
+
"reason": f"Evidence provided: {', '.join(file_references[:3])}"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _verify_agent_claims(output: str) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Verify agent claims against actual evidence.
|
|
201
|
+
|
|
202
|
+
Extracts completion claims and checks for concrete evidence.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Formatted string of verification failures (empty if all verified)
|
|
206
|
+
"""
|
|
207
|
+
# Extract evidence from output
|
|
208
|
+
evidence = _extract_evidence(output)
|
|
209
|
+
|
|
210
|
+
# Look for completion claims (✅, "completed", "done", etc.)
|
|
211
|
+
completion_patterns = [
|
|
212
|
+
r'✅\s+(.+?)(?:\n|$)', # Checkmark pattern
|
|
213
|
+
r'(?:Completed|Finished|Done):\s*(.+?)(?:\n|$)', # Explicit completion
|
|
214
|
+
r'"status":\s*"completed".*?"content":\s*"(.+?)"', # JSON todo format
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
claims = []
|
|
218
|
+
for pattern in completion_patterns:
|
|
219
|
+
matches = re.finditer(pattern, output, re.IGNORECASE | re.DOTALL)
|
|
220
|
+
for match in matches:
|
|
221
|
+
claim_text = match.group(1).strip()
|
|
222
|
+
if claim_text:
|
|
223
|
+
claims.append(claim_text)
|
|
224
|
+
|
|
225
|
+
if not claims:
|
|
226
|
+
# No completion claims found, nothing to verify
|
|
227
|
+
return ""
|
|
228
|
+
|
|
229
|
+
# Verify each claim
|
|
230
|
+
failures = []
|
|
231
|
+
for claim in claims:
|
|
232
|
+
verification = _verify_file_claim(claim, evidence["files"])
|
|
233
|
+
if not verification["verified"]:
|
|
234
|
+
failures.append(f"- {claim[:100]}... → {verification['reason']}")
|
|
235
|
+
|
|
236
|
+
if failures:
|
|
237
|
+
return "\n".join([
|
|
238
|
+
"The following completion claims lack concrete evidence:",
|
|
239
|
+
*failures,
|
|
240
|
+
"",
|
|
241
|
+
"REQUIRED: Provide file paths with line numbers (e.g., src/auth.ts:45-67)",
|
|
242
|
+
"Use the Read tool to verify files exist before claiming completion."
|
|
243
|
+
])
|
|
244
|
+
|
|
245
|
+
return ""
|
|
@@ -4,9 +4,13 @@ PostToolUse hook for user-friendly tool messaging.
|
|
|
4
4
|
|
|
5
5
|
Outputs concise messages about which agent/tool was used and what it did.
|
|
6
6
|
Format examples:
|
|
7
|
-
- ast-grep('Searching for authentication patterns')
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- 🔧 ast-grep:stravinsky('Searching for authentication patterns')
|
|
8
|
+
- 🟡 get_file_contents:github('Fetching src/main.py from user/repo')
|
|
9
|
+
- 🟣 searchCode:grep-app('Searching GitHub for auth patterns')
|
|
10
|
+
- 🔵 web_search_exa:MCP_DOCKER('Web search for Docker best practices')
|
|
11
|
+
- 🟤 find_code:ast-grep('AST search for class definitions')
|
|
12
|
+
- 🎯 delphi:gpt-5.2-medium('Analyzing architecture trade-offs')
|
|
13
|
+
- 🎯 explore:gemini-3-flash('Finding all API endpoints')
|
|
10
14
|
"""
|
|
11
15
|
|
|
12
16
|
import json
|
|
@@ -23,7 +27,16 @@ AGENT_MODELS = {
|
|
|
23
27
|
"delphi": "gpt-5.2-medium",
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
#
|
|
30
|
+
# MCP Server emoji mappings
|
|
31
|
+
SERVER_EMOJIS = {
|
|
32
|
+
"github": "🟡",
|
|
33
|
+
"ast-grep": "🟤",
|
|
34
|
+
"grep-app": "🟣",
|
|
35
|
+
"MCP_DOCKER": "🔵",
|
|
36
|
+
"stravinsky": "🔧",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Tool display names (legacy mapping for simple tools)
|
|
27
40
|
TOOL_NAMES = {
|
|
28
41
|
"mcp__stravinsky__ast_grep_search": "ast-grep",
|
|
29
42
|
"mcp__stravinsky__grep_search": "grep",
|
|
@@ -41,10 +54,98 @@ TOOL_NAMES = {
|
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
|
|
57
|
+
def parse_mcp_tool_name(tool_name: str) -> tuple[str, str, str]:
|
|
58
|
+
"""
|
|
59
|
+
Parse MCP tool name into (server, tool_type, emoji).
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
mcp__github__get_file_contents -> ("github", "get_file_contents", "🟡")
|
|
63
|
+
mcp__stravinsky__grep_search -> ("stravinsky", "grep", "🔧")
|
|
64
|
+
mcp__ast-grep__find_code -> ("ast-grep", "find_code", "🟤")
|
|
65
|
+
"""
|
|
66
|
+
if not tool_name.startswith("mcp__"):
|
|
67
|
+
return ("unknown", tool_name, "🔧")
|
|
68
|
+
|
|
69
|
+
# Remove mcp__ prefix and split by __
|
|
70
|
+
parts = tool_name[5:].split("__", 1)
|
|
71
|
+
if len(parts) != 2:
|
|
72
|
+
return ("unknown", tool_name, "🔧")
|
|
73
|
+
|
|
74
|
+
server = parts[0]
|
|
75
|
+
tool_type = parts[1]
|
|
76
|
+
|
|
77
|
+
# Get emoji for server
|
|
78
|
+
emoji = SERVER_EMOJIS.get(server, "🔧")
|
|
79
|
+
|
|
80
|
+
# Get simplified tool name if available
|
|
81
|
+
simple_name = TOOL_NAMES.get(tool_name, tool_type)
|
|
82
|
+
|
|
83
|
+
return (server, simple_name, emoji)
|
|
84
|
+
|
|
85
|
+
|
|
44
86
|
def extract_description(tool_name: str, params: dict) -> str:
|
|
45
87
|
"""Extract a concise description of what the tool did."""
|
|
46
88
|
|
|
47
|
-
#
|
|
89
|
+
# GitHub tools
|
|
90
|
+
if "github" in tool_name.lower():
|
|
91
|
+
if "get_file_contents" in tool_name:
|
|
92
|
+
path = params.get("path", "")
|
|
93
|
+
repo = params.get("repo", "")
|
|
94
|
+
owner = params.get("owner", "")
|
|
95
|
+
return f"Fetching {path} from {owner}/{repo}"
|
|
96
|
+
elif "create_or_update_file" in tool_name:
|
|
97
|
+
path = params.get("path", "")
|
|
98
|
+
return f"Updating {path}"
|
|
99
|
+
elif "search_repositories" in tool_name:
|
|
100
|
+
query = params.get("query", "")
|
|
101
|
+
return f"Searching repos for '{query[:40]}'"
|
|
102
|
+
elif "search_code" in tool_name:
|
|
103
|
+
q = params.get("q", "")
|
|
104
|
+
return f"Searching code for '{q[:40]}'"
|
|
105
|
+
elif "create_pull_request" in tool_name:
|
|
106
|
+
title = params.get("title", "")
|
|
107
|
+
return f"Creating PR: {title[:40]}"
|
|
108
|
+
elif "get_pull_request" in tool_name or "list_pull_requests" in tool_name:
|
|
109
|
+
return "Fetching PR details"
|
|
110
|
+
return "GitHub operation"
|
|
111
|
+
|
|
112
|
+
# MCP_DOCKER tools
|
|
113
|
+
if "MCP_DOCKER" in tool_name:
|
|
114
|
+
if "web_search_exa" in tool_name:
|
|
115
|
+
query = params.get("query", "")
|
|
116
|
+
return f"Web search: '{query[:40]}'"
|
|
117
|
+
elif "create_entities" in tool_name:
|
|
118
|
+
entities = params.get("entities", [])
|
|
119
|
+
count = len(entities)
|
|
120
|
+
return f"Creating {count} knowledge graph entities"
|
|
121
|
+
elif "search_nodes" in tool_name:
|
|
122
|
+
query = params.get("query", "")
|
|
123
|
+
return f"Searching knowledge graph for '{query[:40]}'"
|
|
124
|
+
return "Knowledge graph operation"
|
|
125
|
+
|
|
126
|
+
# ast-grep tools
|
|
127
|
+
if "ast-grep" in tool_name or "ast_grep" in tool_name:
|
|
128
|
+
if "find_code" in tool_name or "search" in tool_name:
|
|
129
|
+
pattern = params.get("pattern", "")
|
|
130
|
+
return f"AST search for '{pattern[:40]}'"
|
|
131
|
+
elif "test_match" in tool_name:
|
|
132
|
+
return "Testing AST pattern"
|
|
133
|
+
elif "dump_syntax" in tool_name:
|
|
134
|
+
return "Dumping syntax tree"
|
|
135
|
+
return "AST operation"
|
|
136
|
+
|
|
137
|
+
# grep-app tools
|
|
138
|
+
if "grep-app" in tool_name or "grep_app" in tool_name:
|
|
139
|
+
if "searchCode" in tool_name:
|
|
140
|
+
query = params.get("query", "")
|
|
141
|
+
return f"Searching GitHub for '{query[:40]}'"
|
|
142
|
+
elif "github_file" in tool_name:
|
|
143
|
+
path = params.get("path", "")
|
|
144
|
+
repo = params.get("repo", "")
|
|
145
|
+
return f"Fetching {path} from {repo}"
|
|
146
|
+
return "grep.app search"
|
|
147
|
+
|
|
148
|
+
# AST-grep (stravinsky)
|
|
48
149
|
if "ast_grep" in tool_name:
|
|
49
150
|
pattern = params.get("pattern", "")
|
|
50
151
|
directory = params.get("directory", ".")
|
|
@@ -136,9 +237,6 @@ def main():
|
|
|
136
237
|
if not (tool_name.startswith("mcp__") or tool_name == "Task"):
|
|
137
238
|
sys.exit(0)
|
|
138
239
|
|
|
139
|
-
# Get tool display name
|
|
140
|
-
display_name = TOOL_NAMES.get(tool_name, tool_name)
|
|
141
|
-
|
|
142
240
|
# Special handling for Task delegations
|
|
143
241
|
if tool_name == "Task":
|
|
144
242
|
subagent_type = params.get("subagent_type", "unknown")
|
|
@@ -148,9 +246,14 @@ def main():
|
|
|
148
246
|
# Show full agent delegation message
|
|
149
247
|
print(f"🎯 {subagent_type}:{model}('{description}')", file=sys.stderr)
|
|
150
248
|
else:
|
|
151
|
-
#
|
|
249
|
+
# Parse MCP tool name to get server, tool_type, and emoji
|
|
250
|
+
server, tool_type, emoji = parse_mcp_tool_name(tool_name)
|
|
251
|
+
|
|
252
|
+
# Get description of what the tool did
|
|
152
253
|
description = extract_description(tool_name, params)
|
|
153
|
-
|
|
254
|
+
|
|
255
|
+
# Format output: emoji tool_type:server('description')
|
|
256
|
+
print(f"{emoji} {tool_type}:{server}('{description}')", file=sys.stderr)
|
|
154
257
|
|
|
155
258
|
sys.exit(0)
|
|
156
259
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .events import EventType, HookPolicy, PolicyResult, ToolCallEvent
|
|
2
|
+
from ..utils.truncation import truncate_output, TruncationStrategy
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TruncationPolicy(HookPolicy):
|
|
6
|
+
def __init__(self, max_chars: int = 20000):
|
|
7
|
+
self.max_chars = max_chars
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def event_type(self) -> EventType:
|
|
11
|
+
return EventType.POST_TOOL_CALL
|
|
12
|
+
|
|
13
|
+
async def evaluate(self, event: ToolCallEvent) -> PolicyResult:
|
|
14
|
+
if not event.output or len(event.output) <= self.max_chars:
|
|
15
|
+
return PolicyResult(modified_data=event.output)
|
|
16
|
+
|
|
17
|
+
# Skip truncation for read_file since it handles its own truncation with log-awareness
|
|
18
|
+
if event.tool_name == "read_file":
|
|
19
|
+
return PolicyResult(modified_data=event.output)
|
|
20
|
+
|
|
21
|
+
# Use middle truncation for general tool outputs
|
|
22
|
+
modified = truncate_output(
|
|
23
|
+
event.output,
|
|
24
|
+
limit=self.max_chars,
|
|
25
|
+
strategy=TruncationStrategy.MIDDLE
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return PolicyResult(
|
|
29
|
+
modified_data=modified,
|
|
30
|
+
message=modified, # Message is what gets printed in run_as_native
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
policy = TruncationPolicy()
|
|
36
|
+
policy.run_as_native()
|
|
37
|
+
|
mcp_bridge/hooks/truncator.py
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from dataclasses import dataclass, asdict
|
|
6
|
+
import threading
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Approximate costs per 1M tokens (Input/Output)
|
|
12
|
+
MODEL_COSTS = {
|
|
13
|
+
"gemini-3-flash": (0.075, 0.30),
|
|
14
|
+
"gemini-3-pro": (1.25, 5.00),
|
|
15
|
+
"gpt-5.2-codex": (2.50, 10.00), # Estimated based on GPT-4o
|
|
16
|
+
"gpt-4o": (2.50, 10.00),
|
|
17
|
+
"claude-3-5-sonnet": (3.00, 15.00),
|
|
18
|
+
"claude-3-5-haiku": (0.25, 1.25),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class CostRecord:
|
|
23
|
+
timestamp: float
|
|
24
|
+
model: str
|
|
25
|
+
input_tokens: int
|
|
26
|
+
output_tokens: int
|
|
27
|
+
cost: float
|
|
28
|
+
agent_type: str
|
|
29
|
+
task_id: str
|
|
30
|
+
session_id: str
|
|
31
|
+
|
|
32
|
+
class CostTracker:
|
|
33
|
+
_instance = None
|
|
34
|
+
_lock = threading.Lock()
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.file_path = Path.home() / ".stravinsky" / "usage.jsonl"
|
|
38
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def get_instance(cls):
|
|
42
|
+
if cls._instance is None:
|
|
43
|
+
with cls._lock:
|
|
44
|
+
if cls._instance is None:
|
|
45
|
+
cls._instance = cls()
|
|
46
|
+
return cls._instance
|
|
47
|
+
|
|
48
|
+
def calculate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float:
|
|
49
|
+
# Default to Flash pricing if unknown
|
|
50
|
+
input_price, output_price = MODEL_COSTS.get(model, MODEL_COSTS["gemini-3-flash"])
|
|
51
|
+
return (input_tokens / 1_000_000 * input_price) + (output_tokens / 1_000_000 * output_price)
|
|
52
|
+
|
|
53
|
+
def track_usage(self, model: str, input_tokens: int, output_tokens: int, agent_type: str = "unknown", task_id: str = ""):
|
|
54
|
+
cost = self.calculate_cost(model, input_tokens, output_tokens)
|
|
55
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
|
|
56
|
+
|
|
57
|
+
record = CostRecord(
|
|
58
|
+
timestamp=time.time(),
|
|
59
|
+
model=model,
|
|
60
|
+
input_tokens=input_tokens,
|
|
61
|
+
output_tokens=output_tokens,
|
|
62
|
+
cost=cost,
|
|
63
|
+
agent_type=agent_type,
|
|
64
|
+
task_id=task_id,
|
|
65
|
+
session_id=session_id
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(self.file_path, "a") as f:
|
|
70
|
+
f.write(json.dumps(asdict(record)) + "\n")
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Failed to write usage record: {e}")
|
|
73
|
+
|
|
74
|
+
def get_session_summary(self, session_id: str | None = None) -> dict:
|
|
75
|
+
if session_id is None:
|
|
76
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
|
|
77
|
+
|
|
78
|
+
total_cost = 0.0
|
|
79
|
+
total_tokens = 0
|
|
80
|
+
by_agent = {}
|
|
81
|
+
|
|
82
|
+
if not self.file_path.exists():
|
|
83
|
+
return {"total_cost": 0.0, "total_tokens": 0, "by_agent": {}}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(self.file_path, "r") as f:
|
|
87
|
+
for line in f:
|
|
88
|
+
try:
|
|
89
|
+
data = json.loads(line)
|
|
90
|
+
if data.get("session_id") == session_id:
|
|
91
|
+
cost = data.get("cost", 0.0)
|
|
92
|
+
tokens = data.get("input_tokens", 0) + data.get("output_tokens", 0)
|
|
93
|
+
agent = data.get("agent_type", "unknown")
|
|
94
|
+
|
|
95
|
+
total_cost += cost
|
|
96
|
+
total_tokens += tokens
|
|
97
|
+
|
|
98
|
+
if agent not in by_agent:
|
|
99
|
+
by_agent[agent] = {"cost": 0.0, "tokens": 0}
|
|
100
|
+
by_agent[agent]["cost"] += cost
|
|
101
|
+
by_agent[agent]["tokens"] += tokens
|
|
102
|
+
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
continue
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Failed to read usage records: {e}")
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"total_cost": total_cost,
|
|
110
|
+
"total_tokens": total_tokens,
|
|
111
|
+
"by_agent": by_agent
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
def get_cost_tracker():
|
|
115
|
+
return CostTracker.get_instance()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Native Search Wrapper - Optional Rust integration for performance.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
import asyncio
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from typing import List, Dict, Any, Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Attempt to import the native module
|
|
14
|
+
try:
|
|
15
|
+
import stravinsky_native
|
|
16
|
+
HAS_NATIVE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_NATIVE = False
|
|
19
|
+
logger.debug("stravinsky_native module not found. Falling back to CLI tools.")
|
|
20
|
+
|
|
21
|
+
_executor: Optional[ThreadPoolExecutor] = None
|
|
22
|
+
|
|
23
|
+
def get_executor() -> ThreadPoolExecutor:
|
|
24
|
+
"""Get the singleton thread pool executor."""
|
|
25
|
+
global _executor
|
|
26
|
+
if _executor is None:
|
|
27
|
+
# Limit worker threads to avoid overwhelming the system
|
|
28
|
+
_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="native_ffi")
|
|
29
|
+
return _executor
|
|
30
|
+
|
|
31
|
+
async def native_glob_files(pattern: str, directory: str = ".") -> Optional[List[str]]:
|
|
32
|
+
"""
|
|
33
|
+
Find files matching a glob pattern using Rust implementation.
|
|
34
|
+
"""
|
|
35
|
+
if not HAS_NATIVE:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Convert to absolute path for Rust
|
|
40
|
+
abs_dir = os.path.abspath(directory)
|
|
41
|
+
loop = asyncio.get_running_loop()
|
|
42
|
+
|
|
43
|
+
# Offload blocking FFI call to thread pool
|
|
44
|
+
return await loop.run_in_executor(
|
|
45
|
+
get_executor(),
|
|
46
|
+
stravinsky_native.glob_files,
|
|
47
|
+
abs_dir,
|
|
48
|
+
pattern
|
|
49
|
+
)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(f"Native glob_files failed: {e}")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
async def native_grep_search(pattern: str, directory: str = ".", case_sensitive: bool = False) -> Optional[List[Dict[str, Any]]]:
|
|
55
|
+
"""
|
|
56
|
+
Fast text search using Rust implementation.
|
|
57
|
+
"""
|
|
58
|
+
if not HAS_NATIVE:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
abs_dir = os.path.abspath(directory)
|
|
63
|
+
loop = asyncio.get_running_loop()
|
|
64
|
+
|
|
65
|
+
return await loop.run_in_executor(
|
|
66
|
+
get_executor(),
|
|
67
|
+
stravinsky_native.grep_search,
|
|
68
|
+
pattern,
|
|
69
|
+
abs_dir,
|
|
70
|
+
case_sensitive
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Native grep_search failed: {e}")
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
async def native_chunk_code(content: str, language: str) -> Optional[List[Dict[str, Any]]]:
|
|
77
|
+
"""
|
|
78
|
+
AST-aware code chunking using Rust/tree-sitter.
|
|
79
|
+
"""
|
|
80
|
+
if not HAS_NATIVE:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
loop = asyncio.get_running_loop()
|
|
85
|
+
return await loop.run_in_executor(
|
|
86
|
+
get_executor(),
|
|
87
|
+
stravinsky_native.chunk_code,
|
|
88
|
+
content,
|
|
89
|
+
language
|
|
90
|
+
)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"Native chunk_code failed: {e}")
|
|
93
|
+
return None
|