stravinsky 0.4.18__py3-none-any.whl → 0.4.66__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of stravinsky might be problematic. Click here for more details.

Files changed (184) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/__init__.py +16 -6
  3. mcp_bridge/auth/cli.py +202 -11
  4. mcp_bridge/auth/oauth.py +1 -2
  5. mcp_bridge/auth/openai_oauth.py +4 -7
  6. mcp_bridge/auth/token_store.py +0 -1
  7. mcp_bridge/cli/__init__.py +1 -1
  8. mcp_bridge/cli/install_hooks.py +503 -107
  9. mcp_bridge/cli/session_report.py +0 -3
  10. mcp_bridge/config/__init__.py +2 -2
  11. mcp_bridge/config/hook_config.py +3 -5
  12. mcp_bridge/config/rate_limits.py +108 -13
  13. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  14. mcp_bridge/hooks/__init__.py +14 -4
  15. mcp_bridge/hooks/agent_reminder.py +4 -4
  16. mcp_bridge/hooks/auto_slash_command.py +5 -5
  17. mcp_bridge/hooks/budget_optimizer.py +2 -2
  18. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  19. mcp_bridge/hooks/comment_checker.py +3 -4
  20. mcp_bridge/hooks/compaction.py +2 -2
  21. mcp_bridge/hooks/context.py +2 -1
  22. mcp_bridge/hooks/context_monitor.py +2 -2
  23. mcp_bridge/hooks/delegation_policy.py +85 -0
  24. mcp_bridge/hooks/directory_context.py +3 -3
  25. mcp_bridge/hooks/edit_recovery.py +3 -2
  26. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  27. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  28. mcp_bridge/hooks/events.py +160 -0
  29. mcp_bridge/hooks/git_noninteractive.py +4 -4
  30. mcp_bridge/hooks/keyword_detector.py +8 -10
  31. mcp_bridge/hooks/manager.py +35 -22
  32. mcp_bridge/hooks/notification_hook.py +13 -6
  33. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  34. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  35. mcp_bridge/hooks/parallel_execution.py +22 -10
  36. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  37. mcp_bridge/hooks/pre_compact.py +8 -9
  38. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  39. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  40. mcp_bridge/hooks/routing_notifications.py +80 -0
  41. mcp_bridge/hooks/rules_injector.py +11 -19
  42. mcp_bridge/hooks/session_idle.py +4 -4
  43. mcp_bridge/hooks/session_notifier.py +4 -4
  44. mcp_bridge/hooks/session_recovery.py +4 -5
  45. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  46. mcp_bridge/hooks/subagent_stop.py +1 -3
  47. mcp_bridge/hooks/task_validator.py +2 -2
  48. mcp_bridge/hooks/tmux_manager.py +7 -8
  49. mcp_bridge/hooks/todo_delegation.py +4 -1
  50. mcp_bridge/hooks/todo_enforcer.py +180 -10
  51. mcp_bridge/hooks/truncation_policy.py +37 -0
  52. mcp_bridge/hooks/truncator.py +1 -2
  53. mcp_bridge/metrics/cost_tracker.py +115 -0
  54. mcp_bridge/native_search.py +93 -0
  55. mcp_bridge/native_watcher.py +118 -0
  56. mcp_bridge/notifications.py +3 -4
  57. mcp_bridge/orchestrator/enums.py +11 -0
  58. mcp_bridge/orchestrator/router.py +165 -0
  59. mcp_bridge/orchestrator/state.py +32 -0
  60. mcp_bridge/orchestrator/visualization.py +14 -0
  61. mcp_bridge/orchestrator/wisdom.py +34 -0
  62. mcp_bridge/prompts/__init__.py +1 -8
  63. mcp_bridge/prompts/dewey.py +1 -1
  64. mcp_bridge/prompts/planner.py +2 -4
  65. mcp_bridge/prompts/stravinsky.py +53 -31
  66. mcp_bridge/proxy/__init__.py +0 -0
  67. mcp_bridge/proxy/client.py +70 -0
  68. mcp_bridge/proxy/model_server.py +157 -0
  69. mcp_bridge/routing/__init__.py +43 -0
  70. mcp_bridge/routing/config.py +250 -0
  71. mcp_bridge/routing/model_tiers.py +135 -0
  72. mcp_bridge/routing/provider_state.py +261 -0
  73. mcp_bridge/routing/task_classifier.py +190 -0
  74. mcp_bridge/server.py +363 -34
  75. mcp_bridge/server_tools.py +298 -6
  76. mcp_bridge/tools/__init__.py +19 -8
  77. mcp_bridge/tools/agent_manager.py +549 -799
  78. mcp_bridge/tools/background_tasks.py +13 -17
  79. mcp_bridge/tools/code_search.py +54 -51
  80. mcp_bridge/tools/continuous_loop.py +0 -1
  81. mcp_bridge/tools/dashboard.py +19 -0
  82. mcp_bridge/tools/find_code.py +296 -0
  83. mcp_bridge/tools/init.py +1 -0
  84. mcp_bridge/tools/list_directory.py +42 -0
  85. mcp_bridge/tools/lsp/__init__.py +8 -8
  86. mcp_bridge/tools/lsp/manager.py +51 -28
  87. mcp_bridge/tools/lsp/tools.py +98 -65
  88. mcp_bridge/tools/model_invoke.py +1047 -152
  89. mcp_bridge/tools/mux_client.py +75 -0
  90. mcp_bridge/tools/project_context.py +1 -2
  91. mcp_bridge/tools/query_classifier.py +132 -49
  92. mcp_bridge/tools/read_file.py +84 -0
  93. mcp_bridge/tools/replace.py +45 -0
  94. mcp_bridge/tools/run_shell_command.py +38 -0
  95. mcp_bridge/tools/search_enhancements.py +347 -0
  96. mcp_bridge/tools/semantic_search.py +677 -92
  97. mcp_bridge/tools/session_manager.py +0 -2
  98. mcp_bridge/tools/skill_loader.py +0 -1
  99. mcp_bridge/tools/task_runner.py +5 -7
  100. mcp_bridge/tools/templates.py +3 -3
  101. mcp_bridge/tools/tool_search.py +331 -0
  102. mcp_bridge/tools/write_file.py +29 -0
  103. mcp_bridge/update_manager.py +33 -37
  104. mcp_bridge/update_manager_pypi.py +6 -8
  105. mcp_bridge/utils/cache.py +82 -0
  106. mcp_bridge/utils/process.py +71 -0
  107. mcp_bridge/utils/session_state.py +51 -0
  108. mcp_bridge/utils/truncation.py +76 -0
  109. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/METADATA +84 -35
  110. stravinsky-0.4.66.dist-info/RECORD +198 -0
  111. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  112. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  113. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  114. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  115. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  116. stravinsky_claude_assets/agents/debugger.md +254 -0
  117. stravinsky_claude_assets/agents/delphi.md +495 -0
  118. stravinsky_claude_assets/agents/dewey.md +248 -0
  119. stravinsky_claude_assets/agents/explore.md +1198 -0
  120. stravinsky_claude_assets/agents/frontend.md +472 -0
  121. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  122. stravinsky_claude_assets/agents/momus.md +464 -0
  123. stravinsky_claude_assets/agents/research-lead.md +141 -0
  124. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  125. stravinsky_claude_assets/commands/delphi.md +9 -0
  126. stravinsky_claude_assets/commands/dewey.md +54 -0
  127. stravinsky_claude_assets/commands/git-master.md +112 -0
  128. stravinsky_claude_assets/commands/index.md +49 -0
  129. stravinsky_claude_assets/commands/publish.md +86 -0
  130. stravinsky_claude_assets/commands/review.md +73 -0
  131. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  132. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  133. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  134. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  135. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  136. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  137. stravinsky_claude_assets/commands/str/clean.md +97 -0
  138. stravinsky_claude_assets/commands/str/continue.md +38 -0
  139. stravinsky_claude_assets/commands/str/index.md +199 -0
  140. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  141. stravinsky_claude_assets/commands/str/search.md +205 -0
  142. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  143. stravinsky_claude_assets/commands/str/stats.md +71 -0
  144. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  145. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  146. stravinsky_claude_assets/commands/str/watch.md +45 -0
  147. stravinsky_claude_assets/commands/strav.md +53 -0
  148. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  149. stravinsky_claude_assets/commands/verify.md +60 -0
  150. stravinsky_claude_assets/commands/version.md +5 -0
  151. stravinsky_claude_assets/hooks/README.md +248 -0
  152. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  153. stravinsky_claude_assets/hooks/context.py +38 -0
  154. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  155. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  156. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  157. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  158. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  159. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  160. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  161. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  162. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  163. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  164. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  165. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  166. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  167. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  168. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  169. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  170. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  171. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  172. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  173. stravinsky_claude_assets/hooks/truncator.py +23 -0
  174. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  175. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  176. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  177. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  178. stravinsky_claude_assets/settings.json +152 -0
  179. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  180. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  181. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  182. stravinsky_claude_assets/task_dependencies.json +34 -0
  183. stravinsky-0.4.18.dist-info/RECORD +0 -88
  184. {stravinsky-0.4.18.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
@@ -5,19 +5,18 @@ Manages persistent tmux sessions and cleanup.
5
5
  """
6
6
 
7
7
  import logging
8
- import re
9
8
  import shlex
10
9
  import subprocess
11
- from typing import Any, Dict, List, Optional, Set
10
+ from typing import Any
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
15
14
  # Track tmux sessions created by Stravinsky
16
- _tracked_sessions: Set[str] = set()
15
+ _tracked_sessions: set[str] = set()
17
16
  SESSION_PREFIX = "stravinsky-"
18
17
 
19
18
 
20
- def parse_tmux_command(command: str) -> Optional[str]:
19
+ def parse_tmux_command(command: str) -> str | None:
21
20
  """
22
21
  Parse tmux command to extract session name.
23
22
 
@@ -65,8 +64,8 @@ def normalize_session_name(name: str) -> str:
65
64
 
66
65
 
67
66
  async def tmux_manager_hook(
68
- tool_name: str, tool_input: Dict[str, Any], tool_output: Optional[str] = None
69
- ) -> Optional[str]:
67
+ tool_name: str, tool_input: dict[str, Any], tool_output: str | None = None
68
+ ) -> str | None:
70
69
  """
71
70
  Post-tool-call hook that tracks tmux sessions.
72
71
 
@@ -100,7 +99,7 @@ async def tmux_manager_hook(
100
99
  return tool_output
101
100
 
102
101
 
103
- def cleanup_tmux_sessions() -> List[str]:
102
+ def cleanup_tmux_sessions() -> list[str]:
104
103
  """
105
104
  Kill all tracked tmux sessions.
106
105
 
@@ -134,7 +133,7 @@ def cleanup_tmux_sessions() -> List[str]:
134
133
  return killed
135
134
 
136
135
 
137
- def get_tracked_sessions() -> Set[str]:
136
+ def get_tracked_sessions() -> set[str]:
138
137
  """
139
138
  Get set of currently tracked tmux sessions.
140
139
  """
@@ -12,6 +12,7 @@ Works in tandem with:
12
12
  - parallel_execution.py (UserPromptSubmit): Pre-emptive instruction injection
13
13
  - stravinsky_mode.py (PreToolUse): Hard blocking of Read/Grep/Bash tools
14
14
  """
15
+
15
16
  import json
16
17
  import sys
17
18
  from pathlib import Path
@@ -77,7 +78,9 @@ DO NOT:
77
78
 
78
79
  Your NEXT action MUST be multiple Task() calls, one for each independent TODO.
79
80
  """
80
- print(error_message, file=sys.stderr)
81
+ # CRITICAL: Output to stdout so Claude sees the message
82
+ # stderr is not reliably injected into the conversation
83
+ print(error_message)
81
84
 
82
85
  # Exit code 2 = HARD BLOCK in stravinsky mode
83
86
  # Exit code 1 = WARNING otherwise
@@ -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
- from typing import Any, Dict, Optional
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
- **Rules:**
22
- 1. You CANNOT stop or deliver a final answer while todos remain pending
23
- 2. Mark each todo `in_progress` before starting, `completed` immediately after
24
- 3. If a todo is blocked, mark it `cancelled` with explanation and create new actionable todos
25
- 4. Only after ALL todos are `completed` or `cancelled` can you deliver your final answer
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
- CONTINUE WORKING NOW. Do not acknowledge this message - just proceed with the next pending todo.
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: Dict[str, Any]) -> Optional[Dict[str, Any]]:
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
- reminder = TODO_CONTINUATION_REMINDER.format(pending_todos=todos_formatted)
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 ""
@@ -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
+
@@ -1,6 +1,5 @@
1
- import os
2
- import sys
3
1
  import json
2
+ import sys
4
3
 
5
4
  MAX_CHARS = 30000
6
5
 
@@ -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
@@ -0,0 +1,118 @@
1
+ """
2
+ Native Watcher Integration - Rust-based file watching.
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import os
9
+ import subprocess
10
+ import threading
11
+ from pathlib import Path
12
+ from typing import Optional, Callable
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class NativeFileWatcher:
17
+ """
18
+ Python wrapper for the Rust-based stravinsky_watcher binary.
19
+ """
20
+ def __init__(self, project_path: str, on_change: Callable[[str, str], None]):
21
+ self.project_path = os.path.abspath(project_path)
22
+ self.on_change = on_change
23
+ self.process: Optional[subprocess.Popen] = None
24
+ self._stop_event = threading.Event()
25
+ self._thread: Optional[threading.Thread] = None
26
+
27
+ def _get_binary_path(self) -> Path:
28
+ """Find the stravinsky_watcher binary."""
29
+ # Try relative to this file
30
+ root_dir = Path(__file__).parent.parent
31
+ candidates = [
32
+ root_dir / "rust_native" / "target" / "release" / "stravinsky_watcher",
33
+ root_dir / "rust_native" / "target" / "debug" / "stravinsky_watcher",
34
+ ]
35
+
36
+ for c in candidates:
37
+ if c.exists():
38
+ return c
39
+
40
+ raise FileNotFoundError("stravinsky_watcher binary not found. Build it with cargo first.")
41
+
42
+ def start(self):
43
+ """Start the native watcher process in a background thread."""
44
+ if self.process:
45
+ return
46
+
47
+ binary_path = self._get_binary_path()
48
+ logger.info(f"Starting native watcher: {binary_path} {self.project_path}")
49
+
50
+ self.process = subprocess.Popen(
51
+ [str(binary_path), self.project_path],
52
+ stdout=subprocess.PIPE,
53
+ stderr=subprocess.PIPE,
54
+ text=True,
55
+ bufsize=1
56
+ )
57
+
58
+ self._thread = threading.Thread(target=self._read_stdout, daemon=True)
59
+ self._thread.start()
60
+
61
+ def stop(self):
62
+ """Stop the native watcher process."""
63
+ self._stop_event.set()
64
+
65
+ if self.process:
66
+ logger.info(f"Stopping native watcher process (PID: {self.process.pid})")
67
+ # Try to terminate gracefully first
68
+ self.process.terminate()
69
+ try:
70
+ self.process.wait(timeout=1.0)
71
+ except subprocess.TimeoutExpired:
72
+ logger.warning(f"Native watcher (PID: {self.process.pid}) did not terminate, killing...")
73
+ self.process.kill()
74
+ try:
75
+ self.process.wait(timeout=1.0)
76
+ except subprocess.TimeoutExpired:
77
+ logger.error(f"Failed to kill native watcher (PID: {self.process.pid})")
78
+
79
+ # Close streams
80
+ if self.process.stdout:
81
+ self.process.stdout.close()
82
+ if self.process.stderr:
83
+ self.process.stderr.close()
84
+
85
+ self.process = None
86
+
87
+ # Wait for reader thread to exit
88
+ if self._thread and self._thread.is_alive():
89
+ # Don't join with timeout in main thread if it might block,
90
+ # but since we closed stdout, the reader loop should break.
91
+ self._thread.join(timeout=1.0)
92
+ if self._thread.is_alive():
93
+ logger.warning("Native watcher reader thread did not exit cleanly")
94
+ self._thread = None
95
+
96
+ def _read_stdout(self):
97
+ """Read JSON events from the watcher's stdout."""
98
+ if not self.process or not self.process.stdout:
99
+ return
100
+
101
+ for line in self.process.stdout:
102
+ if self._stop_event.is_set():
103
+ break
104
+
105
+ line = line.strip()
106
+ if not line or not line.startswith("{"):
107
+ continue
108
+
109
+ try:
110
+ event = json.loads(line)
111
+ change_type = event.get("type", "unknown")
112
+ path = event.get("path", "")
113
+ self.on_change(change_type, path)
114
+ except json.JSONDecodeError:
115
+ logger.error(f"Failed to decode watcher event: {line}")
116
+
117
+ def is_running(self) -> bool:
118
+ return self.process is not None and self.process.poll() is None