stravinsky 0.2.40__py3-none-any.whl → 0.3.4__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.
Files changed (56) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/token_refresh.py +130 -0
  3. mcp_bridge/cli/__init__.py +6 -0
  4. mcp_bridge/cli/install_hooks.py +1265 -0
  5. mcp_bridge/cli/session_report.py +585 -0
  6. mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
  7. mcp_bridge/hooks/README.md +215 -0
  8. mcp_bridge/hooks/__init__.py +119 -43
  9. mcp_bridge/hooks/edit_recovery.py +42 -37
  10. mcp_bridge/hooks/git_noninteractive.py +89 -0
  11. mcp_bridge/hooks/keyword_detector.py +30 -0
  12. mcp_bridge/hooks/manager.py +50 -0
  13. mcp_bridge/hooks/notification_hook.py +103 -0
  14. mcp_bridge/hooks/parallel_enforcer.py +127 -0
  15. mcp_bridge/hooks/parallel_execution.py +111 -0
  16. mcp_bridge/hooks/pre_compact.py +123 -0
  17. mcp_bridge/hooks/preemptive_compaction.py +81 -7
  18. mcp_bridge/hooks/rules_injector.py +507 -0
  19. mcp_bridge/hooks/session_idle.py +116 -0
  20. mcp_bridge/hooks/session_notifier.py +125 -0
  21. mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
  22. mcp_bridge/hooks/subagent_stop.py +98 -0
  23. mcp_bridge/hooks/task_validator.py +73 -0
  24. mcp_bridge/hooks/tmux_manager.py +141 -0
  25. mcp_bridge/hooks/todo_continuation.py +90 -0
  26. mcp_bridge/hooks/todo_delegation.py +88 -0
  27. mcp_bridge/hooks/tool_messaging.py +164 -0
  28. mcp_bridge/hooks/truncator.py +21 -17
  29. mcp_bridge/notifications.py +151 -0
  30. mcp_bridge/prompts/__init__.py +3 -1
  31. mcp_bridge/prompts/dewey.py +30 -20
  32. mcp_bridge/prompts/explore.py +46 -8
  33. mcp_bridge/prompts/multimodal.py +24 -3
  34. mcp_bridge/prompts/planner.py +222 -0
  35. mcp_bridge/prompts/stravinsky.py +107 -28
  36. mcp_bridge/server.py +170 -10
  37. mcp_bridge/server_tools.py +554 -32
  38. mcp_bridge/tools/agent_manager.py +316 -106
  39. mcp_bridge/tools/background_tasks.py +2 -1
  40. mcp_bridge/tools/code_search.py +97 -11
  41. mcp_bridge/tools/lsp/__init__.py +7 -0
  42. mcp_bridge/tools/lsp/manager.py +448 -0
  43. mcp_bridge/tools/lsp/tools.py +637 -150
  44. mcp_bridge/tools/model_invoke.py +270 -47
  45. mcp_bridge/tools/semantic_search.py +2492 -0
  46. mcp_bridge/tools/templates.py +32 -18
  47. stravinsky-0.3.4.dist-info/METADATA +420 -0
  48. stravinsky-0.3.4.dist-info/RECORD +79 -0
  49. stravinsky-0.3.4.dist-info/entry_points.txt +5 -0
  50. mcp_bridge/native_hooks/edit_recovery.py +0 -46
  51. mcp_bridge/native_hooks/truncator.py +0 -23
  52. stravinsky-0.2.40.dist-info/METADATA +0 -204
  53. stravinsky-0.2.40.dist-info/RECORD +0 -57
  54. stravinsky-0.2.40.dist-info/entry_points.txt +0 -3
  55. /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
  56. {stravinsky-0.2.40.dist-info → stravinsky-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1265 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stravinsky Hook Installer
4
+
5
+ Installs Stravinsky hooks to ~/.claude/hooks/ directory and updates settings.json.
6
+ Hooks provide enhanced Claude Code behavior including:
7
+ - Parallel execution enforcement
8
+ - Tool output truncation
9
+ - Edit error recovery
10
+ - Context injection
11
+ - Todo continuation
12
+ - Tool messaging
13
+ - Stravinsky mode (orchestrator blocking)
14
+ - Notification handling
15
+ - Subagent completion tracking
16
+ - Pre-compaction context preservation
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import shutil
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import Dict, List, Any
25
+
26
+
27
+ # Hook file contents - these will be written to ~/.claude/hooks/
28
+ HOOKS = {
29
+ "truncator.py": '''import os
30
+ import sys
31
+ import json
32
+
33
+ MAX_CHARS = 30000
34
+
35
+ def main():
36
+ try:
37
+ data = json.load(sys.stdin)
38
+ tool_response = data.get("tool_response", "")
39
+ except Exception:
40
+ return
41
+
42
+ if len(tool_response) > MAX_CHARS:
43
+ header = f"[TRUNCATED - {len(tool_response)} chars reduced to {MAX_CHARS}]\\n"
44
+ footer = "\\n...[TRUNCATED]"
45
+ truncated = tool_response[:MAX_CHARS]
46
+ print(header + truncated + footer)
47
+ else:
48
+ print(tool_response)
49
+
50
+ if __name__ == "__main__":
51
+ main()
52
+ ''',
53
+
54
+ "edit_recovery.py": '''#!/usr/bin/env python3
55
+ """Edit error recovery hook - detects edit failures and forces file reading."""
56
+ import json
57
+ import os
58
+ import sys
59
+
60
+ EDIT_ERROR_PATTERNS = [
61
+ "oldString and newString must be different",
62
+ "oldString not found",
63
+ "oldString found multiple times",
64
+ "Target content not found",
65
+ "Multiple occurrences of target content found",
66
+ ]
67
+
68
+ def main():
69
+ try:
70
+ data = json.load(sys.stdin)
71
+ tool_response = data.get("tool_response", "")
72
+ except Exception:
73
+ return 0
74
+
75
+ # Check for edit errors
76
+ is_edit_error = any(pattern in tool_response for pattern in EDIT_ERROR_PATTERNS)
77
+
78
+ if is_edit_error:
79
+ error_msg = """
80
+ > **[EDIT ERROR - IMMEDIATE ACTION REQUIRED]**
81
+ > You made an Edit mistake. STOP and do this NOW:
82
+ > 1. **READ** the file immediately to see its ACTUAL current state.
83
+ > 2. **VERIFY** what the content really looks like (your assumption was wrong).
84
+ > 3. **APOLOGIZE** briefly to the user for the error.
85
+ > 4. **CONTINUE** with corrected action based on the real file content.
86
+ > **DO NOT** attempt another edit until you've read and verified the file state.
87
+ """
88
+ print(tool_response + error_msg)
89
+ else:
90
+ print(tool_response)
91
+
92
+ return 0
93
+
94
+ if __name__ == "__main__":
95
+ sys.exit(main())
96
+ ''',
97
+
98
+ "context.py": '''import os
99
+ import sys
100
+ import json
101
+ from pathlib import Path
102
+
103
+ def main():
104
+ try:
105
+ data = json.load(sys.stdin)
106
+ prompt = data.get("prompt", "")
107
+ except Exception:
108
+ return
109
+
110
+ cwd = Path(os.environ.get("CLAUDE_CWD", "."))
111
+
112
+ # Files to look for
113
+ context_files = ["AGENTS.md", "README.md", "CLAUDE.md"]
114
+ found_context = ""
115
+
116
+ for f in context_files:
117
+ path = cwd / f
118
+ if path.exists():
119
+ try:
120
+ content = path.read_text()
121
+ found_context += f"\\n\\n--- LOCAL CONTEXT: {f} ---\\n{content}\\n"
122
+ break # Only use one for brevity
123
+ except Exception:
124
+ pass
125
+
126
+ if found_context:
127
+ # Prepend context to prompt
128
+ # We wrap the user prompt to distinguish it
129
+ new_prompt = f"{found_context}\\n\\n[USER PROMPT]\\n{prompt}"
130
+ print(new_prompt)
131
+ else:
132
+ print(prompt)
133
+
134
+ if __name__ == "__main__":
135
+ main()
136
+ ''',
137
+
138
+ "parallel_execution.py": '''#!/usr/bin/env python3
139
+ """
140
+ UserPromptSubmit hook: Pre-emptive parallel execution enforcement.
141
+
142
+ Fires BEFORE response generation to inject parallel execution instructions
143
+ when implementation tasks are detected. Eliminates timing ambiguity.
144
+
145
+ CRITICAL: Also activates stravinsky mode marker when /stravinsky is invoked,
146
+ enabling hard blocking of direct tools (Read, Grep, Bash) via stravinsky_mode.py.
147
+ """
148
+ import json
149
+ import sys
150
+ import re
151
+ from pathlib import Path
152
+
153
+ # Marker file that enables hard blocking of direct tools
154
+ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
155
+
156
+
157
+ def detect_stravinsky_invocation(prompt):
158
+ """Detect if /stravinsky skill is being invoked."""
159
+ patterns = [
160
+ r'/stravinsky',
161
+ r'<command-name>/stravinsky</command-name>',
162
+ r'stravinsky orchestrator',
163
+ r'ultrawork',
164
+ r'ultrathink',
165
+ ]
166
+ prompt_lower = prompt.lower()
167
+ return any(re.search(p, prompt_lower) for p in patterns)
168
+
169
+
170
+ def activate_stravinsky_mode():
171
+ """Create marker file to enable hard blocking of direct tools."""
172
+ try:
173
+ config = {"active": True, "reason": "invoked via /stravinsky skill"}
174
+ STRAVINSKY_MODE_FILE.write_text(json.dumps(config))
175
+ return True
176
+ except IOError:
177
+ return False
178
+
179
+
180
+ def detect_implementation_task(prompt):
181
+ """Detect if prompt is an implementation task requiring parallel execution."""
182
+ keywords = [
183
+ 'implement', 'add', 'create', 'build', 'refactor', 'fix',
184
+ 'update', 'modify', 'change', 'develop', 'write code',
185
+ 'feature', 'bug fix', 'enhancement', 'integrate'
186
+ ]
187
+
188
+ prompt_lower = prompt.lower()
189
+ return any(kw in prompt_lower for kw in keywords)
190
+
191
+
192
+ def main():
193
+ try:
194
+ hook_input = json.load(sys.stdin)
195
+ except (json.JSONDecodeError, EOFError):
196
+ return 0
197
+
198
+ prompt = hook_input.get("prompt", "")
199
+
200
+ # CRITICAL: Activate stravinsky mode if /stravinsky is invoked
201
+ # This creates the marker file that enables hard blocking of direct tools
202
+ is_stravinsky = detect_stravinsky_invocation(prompt)
203
+ if is_stravinsky:
204
+ activate_stravinsky_mode()
205
+
206
+ # Only inject for implementation tasks OR stravinsky invocation
207
+ if not detect_implementation_task(prompt) and not is_stravinsky:
208
+ print(prompt)
209
+ return 0
210
+
211
+ # Inject parallel execution instruction BEFORE prompt
212
+ instruction = """
213
+ [🔄 PARALLEL EXECUTION MODE ACTIVE]
214
+
215
+ When you create a TodoWrite with 2+ pending items:
216
+
217
+ ✅ IMMEDIATELY in THIS SAME RESPONSE (do NOT end response after TodoWrite):
218
+ 1. Spawn Task() for EACH independent pending TODO
219
+ 2. Use: Task(subagent_type="explore"|"Plan"|etc., prompt="...", description="...", run_in_background=true)
220
+ 3. Fire ALL Task calls in ONE response block
221
+ 4. Do NOT mark any TODO as in_progress until Task results return
222
+
223
+ ❌ DO NOT:
224
+ - End your response after TodoWrite
225
+ - Mark TODOs in_progress before spawning Tasks
226
+ - Spawn only ONE Task (spawn ALL independent tasks)
227
+ - Wait for "next response" to spawn Tasks
228
+
229
+ Example pattern (all in SAME response):
230
+ ```
231
+ TodoWrite([task1, task2, task3])
232
+ Task(subagent_type="Explore", prompt="Task 1 details", description="Task 1", run_in_background=true)
233
+ Task(subagent_type="Plan", prompt="Task 2 details", description="Task 2", run_in_background=true)
234
+ Task(subagent_type="Explore", prompt="Task 3 details", description="Task 3", run_in_background=true)
235
+ # Continue response - collect results with TaskOutput
236
+ ```
237
+
238
+ ---
239
+
240
+ """
241
+
242
+ modified_prompt = instruction + prompt
243
+ print(modified_prompt)
244
+ return 0
245
+
246
+
247
+ if __name__ == "__main__":
248
+ sys.exit(main())
249
+ ''',
250
+
251
+ "todo_continuation.py": '''#!/usr/bin/env python3
252
+ """
253
+ UserPromptSubmit hook: Todo Continuation Enforcer
254
+
255
+ Checks if there are incomplete todos (in_progress or pending) and injects
256
+ a reminder to continue working on them before starting new work.
257
+
258
+ Aligned with oh-my-opencode's [SYSTEM REMINDER - TODO CONTINUATION] pattern.
259
+ """
260
+ import json
261
+ import os
262
+ import sys
263
+ from pathlib import Path
264
+
265
+
266
+ def get_todo_state() -> dict:
267
+ """Try to get current todo state from Claude Code session or local cache."""
268
+ # Claude Code stores todo state - we can check via session files
269
+ # For now, we'll use a simple file-based approach
270
+ cwd = Path(os.environ.get("CLAUDE_CWD", "."))
271
+ todo_cache = cwd / ".claude" / "todo_state.json"
272
+
273
+ if todo_cache.exists():
274
+ try:
275
+ return json.loads(todo_cache.read_text())
276
+ except Exception:
277
+ pass
278
+
279
+ return {"todos": []}
280
+
281
+
282
+ def main():
283
+ try:
284
+ data = json.load(sys.stdin)
285
+ prompt = data.get("prompt", "")
286
+ except Exception:
287
+ return 0
288
+
289
+ # Get current todo state
290
+ state = get_todo_state()
291
+ todos = state.get("todos", [])
292
+
293
+ if not todos:
294
+ # No todos tracked, pass through
295
+ print(prompt)
296
+ return 0
297
+
298
+ # Count incomplete todos
299
+ in_progress = [t for t in todos if t.get("status") == "in_progress"]
300
+ pending = [t for t in todos if t.get("status") == "pending"]
301
+
302
+ if not in_progress and not pending:
303
+ # All todos complete, pass through
304
+ print(prompt)
305
+ return 0
306
+
307
+ # Build reminder
308
+ reminder_parts = ["[SYSTEM REMINDER - TODO CONTINUATION]", ""]
309
+
310
+ if in_progress:
311
+ reminder_parts.append(f"IN PROGRESS ({len(in_progress)} items):")
312
+ for t in in_progress:
313
+ reminder_parts.append(f" - {t.get('content', 'Unknown task')}")
314
+ reminder_parts.append("")
315
+
316
+ if pending:
317
+ reminder_parts.append(f"PENDING ({len(pending)} items):")
318
+ for t in pending[:5]: # Show max 5 pending
319
+ reminder_parts.append(f" - {t.get('content', 'Unknown task')}")
320
+ if len(pending) > 5:
321
+ reminder_parts.append(f" ... and {len(pending) - 5} more")
322
+ reminder_parts.append("")
323
+
324
+ reminder_parts.extend([
325
+ "IMPORTANT: You have incomplete work. Before starting anything new:",
326
+ "1. Continue working on IN_PROGRESS todos first",
327
+ "2. If blocked, explain why and move to next PENDING item",
328
+ "3. Only start NEW work if all todos are complete or explicitly abandoned",
329
+ "",
330
+ "---",
331
+ "",
332
+ ])
333
+
334
+ reminder = "\\n".join(reminder_parts)
335
+ print(reminder + prompt)
336
+ return 0
337
+
338
+
339
+ if __name__ == "__main__":
340
+ sys.exit(main())
341
+ ''',
342
+
343
+ "todo_delegation.py": '''#!/usr/bin/env python3
344
+ """
345
+ PostToolUse hook for TodoWrite: CRITICAL parallel execution enforcer.
346
+
347
+ This hook fires AFTER TodoWrite completes. If there are 2+ pending items,
348
+ it outputs a STRONG reminder that Task agents must be spawned immediately.
349
+
350
+ Exit code 2 is used to signal a HARD BLOCK - Claude should see this as
351
+ a failure condition requiring immediate correction.
352
+
353
+ Works in tandem with:
354
+ - parallel_execution.py (UserPromptSubmit): Pre-emptive instruction injection
355
+ - stravinsky_mode.py (PreToolUse): Hard blocking of Read/Grep/Bash tools
356
+ """
357
+ import json
358
+ import sys
359
+ from pathlib import Path
360
+
361
+ # Check if stravinsky mode is active (hard blocking enabled)
362
+ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
363
+
364
+
365
+ def is_stravinsky_mode():
366
+ """Check if hard blocking mode is active."""
367
+ return STRAVINSKY_MODE_FILE.exists()
368
+
369
+
370
+ def main():
371
+ # Read hook input from stdin
372
+ try:
373
+ hook_input = json.load(sys.stdin)
374
+ except (json.JSONDecodeError, EOFError):
375
+ return 0
376
+
377
+ tool_name = hook_input.get("tool_name", "")
378
+
379
+ if tool_name != "TodoWrite":
380
+ return 0
381
+
382
+ # Get the todos that were just written
383
+ tool_input = hook_input.get("tool_input", {})
384
+ todos = tool_input.get("todos", [])
385
+
386
+ # Count pending todos
387
+ pending_count = sum(1 for t in todos if t.get("status") == "pending")
388
+
389
+ if pending_count < 2:
390
+ return 0
391
+
392
+ # Check if stravinsky mode is active
393
+ stravinsky_active = is_stravinsky_mode()
394
+
395
+ # CRITICAL: Output urgent reminder for parallel Task spawning
396
+ mode_warning = ""
397
+ if stravinsky_active:
398
+ mode_warning = """
399
+ ⚠️ STRAVINSKY MODE ACTIVE - Direct tools (Read, Grep, Bash) are BLOCKED.
400
+ You MUST use Task(subagent_type="explore", ...) for ALL file operations.
401
+ """
402
+
403
+ error_message = f"""
404
+ 🚨 PARALLEL DELEGATION REQUIRED 🚨
405
+
406
+ TodoWrite created {pending_count} pending items.
407
+ {mode_warning}
408
+ You MUST spawn Task agents for ALL independent TODOs in THIS SAME RESPONSE.
409
+
410
+ Required pattern (IMMEDIATELY after this message):
411
+ Task(subagent_type="explore", prompt="TODO 1...", description="TODO 1", run_in_background=true)
412
+ Task(subagent_type="explore", prompt="TODO 2...", description="TODO 2", run_in_background=true)
413
+ ...
414
+
415
+ DO NOT:
416
+ - End your response without spawning Tasks
417
+ - Mark TODOs in_progress before spawning Tasks
418
+ - Use Read/Grep/Bash directly (BLOCKED in stravinsky mode)
419
+
420
+ Your NEXT action MUST be multiple Task() calls, one for each independent TODO.
421
+ """
422
+ print(error_message, file=sys.stderr)
423
+
424
+ # Exit code 2 = HARD BLOCK in stravinsky mode
425
+ # Exit code 1 = WARNING otherwise
426
+ return 2 if stravinsky_active else 1
427
+
428
+
429
+ if __name__ == "__main__":
430
+ sys.exit(main())
431
+ ''',
432
+
433
+ "stravinsky_mode.py": '''#!/usr/bin/env python3
434
+ """
435
+ Stravinsky Mode Enforcer Hook
436
+
437
+ This PreToolUse hook blocks native file reading tools (Read, Search, Grep, Bash)
438
+ when stravinsky orchestrator mode is active, forcing use of Task tool for native
439
+ subagent delegation.
440
+
441
+ Stravinsky mode is activated by creating a marker file:
442
+ ~/.stravinsky_mode
443
+
444
+ The /stravinsky command should create this file, and it should be
445
+ removed when the task is complete.
446
+
447
+ Exit codes:
448
+ 0 = Allow the tool to execute
449
+ 2 = Block the tool (reason sent via stderr)
450
+ """
451
+
452
+ import json
453
+ import os
454
+ import sys
455
+ from pathlib import Path
456
+
457
+ # Marker file that indicates stravinsky mode is active
458
+ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
459
+
460
+ # Tools to block when in stravinsky mode
461
+ BLOCKED_TOOLS = {
462
+ "Read",
463
+ "Search",
464
+ "Grep",
465
+ "Bash",
466
+ "MultiEdit",
467
+ "Edit",
468
+ }
469
+
470
+ # Tools that are always allowed
471
+ ALLOWED_TOOLS = {
472
+ "TodoRead",
473
+ "TodoWrite",
474
+ "Task", # Native subagent delegation
475
+ "Agent", # MCP agent tools
476
+ }
477
+
478
+ # Agent routing recommendations
479
+ AGENT_ROUTES = {
480
+ "Read": "explore",
481
+ "Grep": "explore",
482
+ "Search": "explore",
483
+ "Bash": "explore",
484
+ "Edit": "code-reviewer",
485
+ "MultiEdit": "code-reviewer",
486
+ }
487
+
488
+
489
+ def is_stravinsky_mode_active() -> bool:
490
+ """Check if stravinsky orchestrator mode is active."""
491
+ return STRAVINSKY_MODE_FILE.exists()
492
+
493
+
494
+ def read_stravinsky_mode_config() -> dict:
495
+ """Read the stravinsky mode configuration if it exists."""
496
+ if not STRAVINSKY_MODE_FILE.exists():
497
+ return {}
498
+ try:
499
+ return json.loads(STRAVINSKY_MODE_FILE.read_text())
500
+ except (json.JSONDecodeError, IOError):
501
+ return {"active": True}
502
+
503
+
504
+ def main():
505
+ # Read hook input from stdin
506
+ try:
507
+ hook_input = json.loads(sys.stdin.read())
508
+ except json.JSONDecodeError:
509
+ # If we can't parse input, allow the tool
510
+ sys.exit(0)
511
+
512
+ tool_name = hook_input.get("toolName", hook_input.get("tool_name", ""))
513
+ params = hook_input.get("params", {})
514
+
515
+ # Always allow certain tools
516
+ if tool_name in ALLOWED_TOOLS:
517
+ sys.exit(0)
518
+
519
+ # Check if stravinsky mode is active
520
+ if not is_stravinsky_mode_active():
521
+ # Not in stravinsky mode, allow all tools
522
+ sys.exit(0)
523
+
524
+ config = read_stravinsky_mode_config()
525
+
526
+ # Check if this tool should be blocked
527
+ if tool_name in BLOCKED_TOOLS:
528
+ # Determine which agent to delegate to
529
+ agent = AGENT_ROUTES.get(tool_name, "explore")
530
+
531
+ # Get tool context for better messaging
532
+ context = ""
533
+ if tool_name == "Grep":
534
+ pattern = params.get("pattern", "")
535
+ context = f" (searching for '{pattern[:30]}')"
536
+ elif tool_name == "Read":
537
+ file_path = params.get("file_path", "")
538
+ context = f" (reading {os.path.basename(file_path)})" if file_path else ""
539
+
540
+ # User-friendly delegation message
541
+ print(f"🎭 {agent}('Delegating {tool_name}{context}')", file=sys.stderr)
542
+
543
+ # Block the tool and tell Claude why
544
+ reason = f"""⚠️ STRAVINSKY MODE ACTIVE - {tool_name} BLOCKED
545
+
546
+ You are in Stravinsky orchestrator mode. Native tools are disabled.
547
+
548
+ Instead of using {tool_name}, you MUST use Task tool for native subagent delegation:
549
+ - Task(subagent_type="explore", ...) for file reading/searching
550
+ - Task(subagent_type="dewey", ...) for documentation research
551
+ - Task(subagent_type="code-reviewer", ...) for code analysis
552
+ - Task(subagent_type="debugger", ...) for error investigation
553
+ - Task(subagent_type="frontend", ...) for UI/UX work
554
+ - Task(subagent_type="delphi", ...) for strategic architecture decisions
555
+
556
+ Example:
557
+ Task(
558
+ subagent_type="explore",
559
+ prompt="Read and analyze the authentication module",
560
+ description="Analyze auth"
561
+ )
562
+
563
+ To exit stravinsky mode, run:
564
+ rm ~/.stravinsky_mode
565
+ """
566
+ # Send reason to stderr (Claude sees this)
567
+ print(reason, file=sys.stderr)
568
+ # Exit with code 2 to block the tool
569
+ sys.exit(2)
570
+
571
+ # Tool not in block list, allow it
572
+ sys.exit(0)
573
+
574
+
575
+ if __name__ == "__main__":
576
+ main()
577
+ ''',
578
+
579
+ "tool_messaging.py": '''#!/usr/bin/env python3
580
+ """
581
+ PostToolUse hook for user-friendly tool messaging.
582
+
583
+ Outputs concise messages about which agent/tool was used and what it did.
584
+ Format examples:
585
+ - ast-grep('Searching for authentication patterns')
586
+ - delphi:openai/gpt-5.2-medium('Analyzing architecture trade-offs')
587
+ - explore:gemini-3-flash('Finding all API endpoints')
588
+ """
589
+
590
+ import json
591
+ import os
592
+ import sys
593
+
594
+ # Agent model mappings
595
+ AGENT_MODELS = {
596
+ "explore": "gemini-3-flash",
597
+ "dewey": "gemini-3-flash",
598
+ "code-reviewer": "sonnet",
599
+ "debugger": "sonnet",
600
+ "frontend": "gemini-3-pro-high",
601
+ "delphi": "gpt-5.2-medium",
602
+ }
603
+
604
+ # Tool display names
605
+ TOOL_NAMES = {
606
+ "mcp__stravinsky__ast_grep_search": "ast-grep",
607
+ "mcp__stravinsky__grep_search": "grep",
608
+ "mcp__stravinsky__glob_files": "glob",
609
+ "mcp__stravinsky__lsp_diagnostics": "lsp-diagnostics",
610
+ "mcp__stravinsky__lsp_hover": "lsp-hover",
611
+ "mcp__stravinsky__lsp_goto_definition": "lsp-goto-def",
612
+ "mcp__stravinsky__lsp_find_references": "lsp-find-refs",
613
+ "mcp__stravinsky__lsp_document_symbols": "lsp-symbols",
614
+ "mcp__stravinsky__lsp_workspace_symbols": "lsp-workspace-symbols",
615
+ "mcp__stravinsky__invoke_gemini": "gemini",
616
+ "mcp__stravinsky__invoke_openai": "openai",
617
+ "mcp__grep-app__searchCode": "grep.app",
618
+ "mcp__grep-app__github_file": "github-file",
619
+ }
620
+
621
+
622
+ def extract_description(tool_name: str, params: dict) -> str:
623
+ """Extract a concise description of what the tool did."""
624
+
625
+ # AST-grep
626
+ if "ast_grep" in tool_name:
627
+ pattern = params.get("pattern", "")
628
+ directory = params.get("directory", ".")
629
+ return f"Searching AST in {directory} for '{pattern[:40]}...'"
630
+
631
+ # Grep/search
632
+ if "grep_search" in tool_name or "searchCode" in tool_name:
633
+ pattern = params.get("pattern", params.get("query", ""))
634
+ return f"Searching for '{pattern[:40]}...'"
635
+
636
+ # Glob
637
+ if "glob_files" in tool_name:
638
+ pattern = params.get("pattern", "")
639
+ return f"Finding files matching '{pattern}'"
640
+
641
+ # LSP diagnostics
642
+ if "lsp_diagnostics" in tool_name:
643
+ file_path = params.get("file_path", "")
644
+ filename = os.path.basename(file_path) if file_path else "file"
645
+ return f"Checking {filename} for errors"
646
+
647
+ # LSP hover
648
+ if "lsp_hover" in tool_name:
649
+ file_path = params.get("file_path", "")
650
+ line = params.get("line", "")
651
+ filename = os.path.basename(file_path) if file_path else "file"
652
+ return f"Type info for {filename}:{line}"
653
+
654
+ # LSP goto definition
655
+ if "lsp_goto" in tool_name:
656
+ file_path = params.get("file_path", "")
657
+ filename = os.path.basename(file_path) if file_path else "symbol"
658
+ return f"Finding definition in {filename}"
659
+
660
+ # LSP find references
661
+ if "lsp_find_references" in tool_name:
662
+ file_path = params.get("file_path", "")
663
+ filename = os.path.basename(file_path) if file_path else "symbol"
664
+ return f"Finding all references to symbol in {filename}"
665
+
666
+ # LSP symbols
667
+ if "lsp_symbols" in tool_name or "lsp_document_symbols" in tool_name:
668
+ file_path = params.get("file_path", "")
669
+ filename = os.path.basename(file_path) if file_path else "file"
670
+ return f"Getting symbols from {filename}"
671
+
672
+ if "lsp_workspace_symbols" in tool_name:
673
+ query = params.get("query", "")
674
+ return f"Searching workspace for symbol '{query}'"
675
+
676
+ # Gemini invocation
677
+ if "invoke_gemini" in tool_name:
678
+ prompt = params.get("prompt", "")
679
+ # Extract first meaningful line
680
+ first_line = prompt.split('\\n')[0][:50] if prompt else "Processing"
681
+ return first_line
682
+
683
+ # OpenAI invocation
684
+ if "invoke_openai" in tool_name:
685
+ prompt = params.get("prompt", "")
686
+ first_line = prompt.split('\\n')[0][:50] if prompt else "Strategic analysis"
687
+ return first_line
688
+
689
+ # GitHub file fetch
690
+ if "github_file" in tool_name:
691
+ path = params.get("path", "")
692
+ repo = params.get("repo", "")
693
+ return f"Fetching {path} from {repo}"
694
+
695
+ # Task delegation
696
+ if tool_name == "Task":
697
+ subagent_type = params.get("subagent_type", "unknown")
698
+ description = params.get("description", "")
699
+ model = AGENT_MODELS.get(subagent_type, "unknown")
700
+ return f"{subagent_type}:{model}('{description}')"
701
+
702
+ return "Processing"
703
+
704
+
705
+ def main():
706
+ try:
707
+ # Read hook input from stdin
708
+ hook_input = json.loads(sys.stdin.read())
709
+
710
+ tool_name = hook_input.get("toolName", hook_input.get("tool_name", ""))
711
+ params = hook_input.get("params", hook_input.get("tool_input", {}))
712
+
713
+ # Only output messages for MCP tools and Task delegations
714
+ if not (tool_name.startswith("mcp__") or tool_name == "Task"):
715
+ sys.exit(0)
716
+
717
+ # Get tool display name
718
+ display_name = TOOL_NAMES.get(tool_name, tool_name)
719
+
720
+ # Special handling for Task delegations
721
+ if tool_name == "Task":
722
+ subagent_type = params.get("subagent_type", "unknown")
723
+ description = params.get("description", "")
724
+ model = AGENT_MODELS.get(subagent_type, "unknown")
725
+
726
+ # Show full agent delegation message
727
+ print(f"🎯 {subagent_type}:{model}('{description}')", file=sys.stderr)
728
+ else:
729
+ # Regular tool usage
730
+ description = extract_description(tool_name, params)
731
+ print(f"🔧 {display_name}('{description}')", file=sys.stderr)
732
+
733
+ sys.exit(0)
734
+
735
+ except Exception as e:
736
+ # On error, fail silently (don't disrupt workflow)
737
+ print(f"Tool messaging hook error: {e}", file=sys.stderr)
738
+ sys.exit(0)
739
+
740
+
741
+ if __name__ == "__main__":
742
+ main()
743
+ ''',
744
+
745
+ "notification_hook.py": '''#!/usr/bin/env python3
746
+ """
747
+ Notification hook for agent spawn messages.
748
+
749
+ Fires on Notification events to output user-friendly messages about
750
+ which agent was spawned, what model it uses, and what task it's doing.
751
+
752
+ Format: spawned {agent_type}:{model}('{description}')
753
+ Example: spawned delphi:gpt-5.2-medium('Debug xyz code')
754
+ """
755
+
756
+ import json
757
+ import sys
758
+ from typing import Optional, Dict, Any
759
+
760
+
761
+ # Agent display model mappings
762
+ AGENT_DISPLAY_MODELS = {
763
+ "explore": "gemini-3-flash",
764
+ "dewey": "gemini-3-flash",
765
+ "document_writer": "gemini-3-flash",
766
+ "multimodal": "gemini-3-flash",
767
+ "frontend": "gemini-3-pro-high",
768
+ "delphi": "gpt-5.2-medium",
769
+ "planner": "opus-4.5",
770
+ "code-reviewer": "sonnet-4.5",
771
+ "debugger": "sonnet-4.5",
772
+ "_default": "sonnet-4.5",
773
+ }
774
+
775
+
776
+ def extract_agent_info(message: str) -> Optional[Dict[str, str]]:
777
+ """
778
+ Extract agent spawn information from notification message.
779
+
780
+ Looks for patterns like:
781
+ - "Agent explore spawned for task..."
782
+ - "Spawned delphi agent: description"
783
+ - Task tool delegation messages
784
+ """
785
+ message_lower = message.lower()
786
+
787
+ # Try to extract agent type from message
788
+ agent_type = None
789
+ description = ""
790
+
791
+ for agent in AGENT_DISPLAY_MODELS.keys():
792
+ if agent == "_default":
793
+ continue
794
+ if agent in message_lower:
795
+ agent_type = agent
796
+ # Extract description after agent name
797
+ idx = message_lower.find(agent)
798
+ description = message[idx + len(agent):].strip()[:60]
799
+ break
800
+
801
+ if not agent_type:
802
+ return None
803
+
804
+ # Clean up description
805
+ description = description.strip(":-() ")
806
+ if not description:
807
+ description = "task delegated"
808
+
809
+ display_model = AGENT_DISPLAY_MODELS.get(agent_type, AGENT_DISPLAY_MODELS["_default"])
810
+
811
+ return {
812
+ "agent_type": agent_type,
813
+ "model": display_model,
814
+ "description": description,
815
+ }
816
+
817
+
818
+ def main():
819
+ """Main hook entry point."""
820
+ try:
821
+ hook_input = json.load(sys.stdin)
822
+ except (json.JSONDecodeError, EOFError):
823
+ return 0
824
+
825
+ # Get notification message
826
+ message = hook_input.get("message", "")
827
+ notification_type = hook_input.get("notification_type", "")
828
+
829
+ # Only process agent-related notifications
830
+ agent_keywords = ["agent", "spawn", "delegat", "task"]
831
+ if not any(kw in message.lower() for kw in agent_keywords):
832
+ return 0
833
+
834
+ # Extract agent info
835
+ agent_info = extract_agent_info(message)
836
+ if not agent_info:
837
+ return 0
838
+
839
+ # Format and output
840
+ output = f"spawned {agent_info['agent_type']}:{agent_info['model']}('{agent_info['description']}')"
841
+ print(output, file=sys.stderr)
842
+
843
+ return 0
844
+
845
+
846
+ if __name__ == "__main__":
847
+ sys.exit(main())
848
+ ''',
849
+
850
+ "subagent_stop.py": '''#!/usr/bin/env python3
851
+ """
852
+ SubagentStop hook: Handler for agent/subagent completion events.
853
+
854
+ Fires when a Claude Code subagent (Task tool) finishes to:
855
+ 1. Output completion status messages
856
+ 2. Verify agent produced expected output
857
+ 3. Block completion if critical validation fails
858
+ 4. Integrate with TODO tracking
859
+
860
+ Exit codes:
861
+ 0 = Allow completion
862
+ 2 = Block completion (force continuation)
863
+ """
864
+
865
+ import json
866
+ import sys
867
+ from pathlib import Path
868
+ from typing import Optional, Tuple
869
+
870
+
871
+ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
872
+
873
+
874
+ def is_stravinsky_mode() -> bool:
875
+ """Check if stravinsky mode is active."""
876
+ return STRAVINSKY_MODE_FILE.exists()
877
+
878
+
879
+ def extract_subagent_info(hook_input: dict) -> Tuple[str, str, str]:
880
+ """
881
+ Extract subagent information from hook input.
882
+
883
+ Returns: (agent_type, description, status)
884
+ """
885
+ # Try to get from tool parameters or response
886
+ params = hook_input.get("tool_input", hook_input.get("params", {}))
887
+ response = hook_input.get("tool_response", "")
888
+
889
+ agent_type = params.get("subagent_type", "unknown")
890
+ description = params.get("description", "")[:50]
891
+
892
+ # Determine status from response
893
+ status = "completed"
894
+ response_lower = response.lower() if isinstance(response, str) else ""
895
+ if "error" in response_lower or "failed" in response_lower:
896
+ status = "failed"
897
+ elif "timeout" in response_lower:
898
+ status = "timeout"
899
+
900
+ return agent_type, description, status
901
+
902
+
903
+ def format_completion_message(agent_type: str, description: str, status: str) -> str:
904
+ """Format user-friendly completion message."""
905
+ icon = "✓" if status == "completed" else "✗"
906
+ return f"{icon} Subagent {agent_type} {status}: {description}"
907
+
908
+
909
+ def should_block(status: str, agent_type: str) -> bool:
910
+ """
911
+ Determine if we should block completion.
912
+
913
+ Block if:
914
+ - Agent failed AND stravinsky mode active AND critical agent type
915
+ """
916
+ if status != "completed" and is_stravinsky_mode():
917
+ critical_agents = {"delphi", "code-reviewer", "debugger"}
918
+ if agent_type in critical_agents:
919
+ return True
920
+ return False
921
+
922
+
923
+ def main():
924
+ """Main hook entry point."""
925
+ try:
926
+ hook_input = json.load(sys.stdin)
927
+ except (json.JSONDecodeError, EOFError):
928
+ return 0
929
+
930
+ # Extract subagent info
931
+ agent_type, description, status = extract_subagent_info(hook_input)
932
+
933
+ # Output completion message
934
+ message = format_completion_message(agent_type, description, status)
935
+ print(message, file=sys.stderr)
936
+
937
+ # Check if we should block
938
+ if should_block(status, agent_type):
939
+ print(f"\\n⚠️ CRITICAL SUBAGENT FAILURE - {agent_type} failed", file=sys.stderr)
940
+ print("Review the error and retry or delegate to delphi.", file=sys.stderr)
941
+ return 2
942
+
943
+ return 0
944
+
945
+
946
+ if __name__ == "__main__":
947
+ sys.exit(main())
948
+ ''',
949
+
950
+ "pre_compact.py": '''#!/usr/bin/env python3
951
+ """
952
+ PreCompact hook: Context preservation before compaction.
953
+
954
+ Fires before Claude Code compacts conversation context to:
955
+ 1. Preserve critical context patterns
956
+ 2. Maintain stravinsky mode state
957
+ 3. Warn about information loss
958
+ 4. Save state for recovery
959
+
960
+ Cannot block compaction (exit 2 only shows error).
961
+ """
962
+
963
+ import json
964
+ import sys
965
+ from pathlib import Path
966
+ from datetime import datetime
967
+ from typing import List, Dict, Any
968
+
969
+
970
+ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
971
+ STATE_DIR = Path.home() / ".claude" / "state"
972
+ COMPACTION_LOG = STATE_DIR / "compaction.jsonl"
973
+
974
+ # Patterns to preserve
975
+ PRESERVE_PATTERNS = [
976
+ "ARCHITECTURE:",
977
+ "DESIGN DECISION:",
978
+ "CONSTRAINT:",
979
+ "REQUIREMENT:",
980
+ "MUST NOT:",
981
+ "NEVER:",
982
+ "CRITICAL ERROR:",
983
+ "CURRENT TASK:",
984
+ "BLOCKED BY:",
985
+ "[STRAVINSKY MODE]",
986
+ "PARALLEL_DELEGATION:",
987
+ ]
988
+
989
+
990
+ def ensure_state_dir():
991
+ """Ensure state directory exists."""
992
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
993
+
994
+
995
+ def get_stravinsky_mode_state() -> Dict[str, Any]:
996
+ """Read stravinsky mode state."""
997
+ if not STRAVINSKY_MODE_FILE.exists():
998
+ return {"active": False}
999
+ try:
1000
+ content = STRAVINSKY_MODE_FILE.read_text().strip()
1001
+ return json.loads(content) if content else {"active": True}
1002
+ except (json.JSONDecodeError, IOError):
1003
+ return {"active": True}
1004
+
1005
+
1006
+ def extract_preserved_context(prompt: str) -> List[str]:
1007
+ """Extract context matching preservation patterns."""
1008
+ preserved = []
1009
+ lines = prompt.split("\\n")
1010
+
1011
+ for i, line in enumerate(lines):
1012
+ for pattern in PRESERVE_PATTERNS:
1013
+ if pattern in line:
1014
+ # Capture line + 2 more for context
1015
+ context = "\\n".join(lines[i:min(i+3, len(lines))])
1016
+ preserved.append(context)
1017
+ break
1018
+
1019
+ return preserved[:15] # Max 15 items
1020
+
1021
+
1022
+ def log_compaction(preserved: List[str], stravinsky_active: bool):
1023
+ """Log compaction event for audit."""
1024
+ ensure_state_dir()
1025
+
1026
+ entry = {
1027
+ "timestamp": datetime.utcnow().isoformat(),
1028
+ "preserved_count": len(preserved),
1029
+ "stravinsky_mode": stravinsky_active,
1030
+ "preview": [p[:50] for p in preserved[:3]],
1031
+ }
1032
+
1033
+ try:
1034
+ with COMPACTION_LOG.open("a") as f:
1035
+ f.write(json.dumps(entry) + "\\n")
1036
+ except IOError:
1037
+ pass
1038
+
1039
+
1040
+ def main():
1041
+ """Main hook entry point."""
1042
+ try:
1043
+ hook_input = json.load(sys.stdin)
1044
+ except (json.JSONDecodeError, EOFError):
1045
+ return 0
1046
+
1047
+ prompt = hook_input.get("prompt", "")
1048
+ trigger = hook_input.get("trigger", "auto")
1049
+
1050
+ # Get stravinsky mode state
1051
+ strav_state = get_stravinsky_mode_state()
1052
+ stravinsky_active = strav_state.get("active", False)
1053
+
1054
+ # Extract preserved context
1055
+ preserved = extract_preserved_context(prompt)
1056
+
1057
+ # Log compaction event
1058
+ log_compaction(preserved, stravinsky_active)
1059
+
1060
+ # Output preservation warning
1061
+ if preserved or stravinsky_active:
1062
+ print(f"\\n[PreCompact] Context compaction triggered ({trigger})", file=sys.stderr)
1063
+ print(f" Preserved items: {len(preserved)}", file=sys.stderr)
1064
+ if stravinsky_active:
1065
+ print(" [STRAVINSKY MODE ACTIVE] - State will persist", file=sys.stderr)
1066
+ print(" Audit log: ~/.claude/state/compaction.jsonl", file=sys.stderr)
1067
+
1068
+ return 0
1069
+
1070
+
1071
+ if __name__ == "__main__":
1072
+ sys.exit(main())
1073
+ ''',
1074
+ }
1075
+
1076
+
1077
+ # Hook registration configuration for settings.json
1078
+ # IMPORTANT: Uses ~/.claude/hooks/ (global) paths so hooks work in ANY project
1079
+ HOOK_REGISTRATIONS = {
1080
+ "Notification": [
1081
+ {
1082
+ "matcher": "*",
1083
+ "hooks": [
1084
+ {
1085
+ "type": "command",
1086
+ "command": "python3 ~/.claude/hooks/notification_hook.py"
1087
+ }
1088
+ ]
1089
+ }
1090
+ ],
1091
+ "SubagentStop": [
1092
+ {
1093
+ "matcher": "*",
1094
+ "hooks": [
1095
+ {
1096
+ "type": "command",
1097
+ "command": "python3 ~/.claude/hooks/subagent_stop.py"
1098
+ }
1099
+ ]
1100
+ }
1101
+ ],
1102
+ "PreCompact": [
1103
+ {
1104
+ "matcher": "*",
1105
+ "hooks": [
1106
+ {
1107
+ "type": "command",
1108
+ "command": "python3 ~/.claude/hooks/pre_compact.py"
1109
+ }
1110
+ ]
1111
+ }
1112
+ ],
1113
+ "PreToolUse": [
1114
+ {
1115
+ "matcher": "Read,Search,Grep,Bash,Edit,MultiEdit",
1116
+ "hooks": [
1117
+ {
1118
+ "type": "command",
1119
+ "command": "python3 ~/.claude/hooks/stravinsky_mode.py"
1120
+ }
1121
+ ]
1122
+ }
1123
+ ],
1124
+ "UserPromptSubmit": [
1125
+ {
1126
+ "matcher": "*",
1127
+ "hooks": [
1128
+ {
1129
+ "type": "command",
1130
+ "command": "python3 ~/.claude/hooks/parallel_execution.py"
1131
+ },
1132
+ {
1133
+ "type": "command",
1134
+ "command": "python3 ~/.claude/hooks/context.py"
1135
+ },
1136
+ {
1137
+ "type": "command",
1138
+ "command": "python3 ~/.claude/hooks/todo_continuation.py"
1139
+ }
1140
+ ]
1141
+ }
1142
+ ],
1143
+ "PostToolUse": [
1144
+ {
1145
+ "matcher": "*",
1146
+ "hooks": [
1147
+ {
1148
+ "type": "command",
1149
+ "command": "python3 ~/.claude/hooks/truncator.py"
1150
+ }
1151
+ ]
1152
+ },
1153
+ {
1154
+ "matcher": "mcp__stravinsky__*,mcp__grep-app__*,Task",
1155
+ "hooks": [
1156
+ {
1157
+ "type": "command",
1158
+ "command": "python3 ~/.claude/hooks/tool_messaging.py"
1159
+ }
1160
+ ]
1161
+ },
1162
+ {
1163
+ "matcher": "Edit,MultiEdit",
1164
+ "hooks": [
1165
+ {
1166
+ "type": "command",
1167
+ "command": "python3 ~/.claude/hooks/edit_recovery.py"
1168
+ }
1169
+ ]
1170
+ },
1171
+ {
1172
+ "matcher": "TodoWrite",
1173
+ "hooks": [
1174
+ {
1175
+ "type": "command",
1176
+ "command": "python3 ~/.claude/hooks/todo_delegation.py"
1177
+ }
1178
+ ]
1179
+ }
1180
+ ]
1181
+ }
1182
+
1183
+
1184
+ def install_hooks():
1185
+ """Install Stravinsky hooks to ~/.claude/hooks/"""
1186
+
1187
+ # Get home directory
1188
+ home = Path.home()
1189
+ claude_dir = home / ".claude"
1190
+ hooks_dir = claude_dir / "hooks"
1191
+ settings_file = claude_dir / "settings.json"
1192
+
1193
+ print("🚀 Stravinsky Hook Installer")
1194
+ print("=" * 60)
1195
+
1196
+ # Create hooks directory if it doesn't exist
1197
+ if not hooks_dir.exists():
1198
+ print(f"📁 Creating hooks directory: {hooks_dir}")
1199
+ hooks_dir.mkdir(parents=True, exist_ok=True)
1200
+ else:
1201
+ print(f"📁 Hooks directory exists: {hooks_dir}")
1202
+
1203
+ # Install each hook file
1204
+ print(f"\\n📝 Installing {len(HOOKS)} hook files...")
1205
+ for filename, content in HOOKS.items():
1206
+ hook_path = hooks_dir / filename
1207
+ print(f" ✓ {filename}")
1208
+ hook_path.write_text(content)
1209
+ hook_path.chmod(0o755) # Make executable
1210
+
1211
+ # Merge hook registrations into settings.json
1212
+ print(f"\\n⚙️ Updating settings.json...")
1213
+
1214
+ # Load existing settings or create new
1215
+ if settings_file.exists():
1216
+ print(f" 📖 Reading existing settings: {settings_file}")
1217
+ with settings_file.open('r') as f:
1218
+ settings = json.load(f)
1219
+ else:
1220
+ print(f" 📝 Creating new settings file: {settings_file}")
1221
+ settings = {}
1222
+
1223
+ # Merge hooks configuration
1224
+ if "hooks" not in settings:
1225
+ settings["hooks"] = {}
1226
+
1227
+ for hook_type, registrations in HOOK_REGISTRATIONS.items():
1228
+ settings["hooks"][hook_type] = registrations
1229
+ print(f" ✓ Registered {hook_type} hooks")
1230
+
1231
+ # Write updated settings
1232
+ with settings_file.open('w') as f:
1233
+ json.dump(settings, f, indent=2)
1234
+
1235
+ print(f"\\n✅ Installation complete!")
1236
+ print("=" * 60)
1237
+ print(f"\\n📋 Installed hooks:")
1238
+ for filename in HOOKS.keys():
1239
+ print(f" • {filename}")
1240
+
1241
+ print(f"\\n🔧 Hook types registered:")
1242
+ for hook_type in HOOK_REGISTRATIONS.keys():
1243
+ print(f" • {hook_type}")
1244
+
1245
+ print(f"\\n📁 Installation directory: {hooks_dir}")
1246
+ print(f"⚙️ Settings file: {settings_file}")
1247
+ print("\\n🎉 Stravinsky hooks are now active!")
1248
+ print("\\n💡 Tip: Run '/stravinsky' to activate orchestrator mode")
1249
+
1250
+ return 0
1251
+
1252
+
1253
+ def main():
1254
+ """CLI entry point."""
1255
+ try:
1256
+ return install_hooks()
1257
+ except Exception as e:
1258
+ print(f"\\n❌ Installation failed: {e}", file=sys.stderr)
1259
+ import traceback
1260
+ traceback.print_exc()
1261
+ return 1
1262
+
1263
+
1264
+ if __name__ == "__main__":
1265
+ sys.exit(main())