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,89 @@
1
+ """
2
+ Git Non-Interactive Environment Hook.
3
+
4
+ Prevents git interactive command hangs by prepending environment variables.
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ import shlex
10
+ from typing import Any, Dict, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Patterns for banned interactive git commands
15
+ BANNED_INTERACTIVE_PATTERNS = [
16
+ r"git\s+add\s+.*-p", # git add -p (patch mode)
17
+ r"git\s+add\s+.*--patch",
18
+ r"git\s+commit\s+.*-v", # git commit -v (verbose with diff)
19
+ r"git\s+rebase\s+.*-i", # git rebase -i (interactive)
20
+ r"git\s+rebase\s+.*--interactive",
21
+ r"git\s+add\s+.*-i", # git add -i (interactive)
22
+ r"git\s+add\s+.*--interactive",
23
+ r"git\s+checkout\s+.*-p", # git checkout -p (patch mode)
24
+ r"git\s+reset\s+.*-p", # git reset -p (patch mode)
25
+ ]
26
+
27
+ # Environment variables to set for non-interactive git
28
+ NON_INTERACTIVE_ENV = {
29
+ "GIT_TERMINAL_PROMPT": "0",
30
+ "GIT_EDITOR": "true", # No-op editor
31
+ "GIT_PAGER": "cat", # No paging
32
+ }
33
+
34
+
35
+ def escape_shell_arg(arg: str) -> str:
36
+ """
37
+ Escape shell argument for safe injection.
38
+ """
39
+ # Use shlex.quote for proper escaping
40
+ return shlex.quote(arg)
41
+
42
+
43
+ async def git_noninteractive_hook(
44
+ tool_name: str, arguments: Dict[str, Any]
45
+ ) -> Optional[Dict[str, Any]]:
46
+ """
47
+ Pre-tool-call hook that prepends non-interactive env vars to git commands.
48
+
49
+ Detects interactive git commands and either:
50
+ 1. Warns and blocks them (if highly interactive like -i)
51
+ 2. Prepends env vars to make them non-interactive
52
+ """
53
+ # Only process Bash tool
54
+ if tool_name != "Bash":
55
+ return None
56
+
57
+ command = arguments.get("command", "")
58
+ if not command or "git" not in command.lower():
59
+ return None
60
+
61
+ # Check for banned interactive patterns
62
+ for pattern in BANNED_INTERACTIVE_PATTERNS:
63
+ if re.search(pattern, command, re.IGNORECASE):
64
+ logger.warning(
65
+ f"[GitNonInteractive] Detected interactive git command: {pattern}"
66
+ )
67
+ # Add warning to command output
68
+ warning = (
69
+ f"\n[WARNING] Interactive git command detected: {command}\n"
70
+ f"This may hang. Consider using non-interactive alternatives.\n"
71
+ )
72
+ # Don't block, just warn - user might know what they're doing
73
+ return None
74
+
75
+ # Prepend environment variables to make git non-interactive
76
+ if "git" in command.lower():
77
+ env_prefix = " ".join(
78
+ [f"{k}={escape_shell_arg(v)}" for k, v in NON_INTERACTIVE_ENV.items()]
79
+ )
80
+ modified_command = f"{env_prefix} {command}"
81
+
82
+ logger.info(f"[GitNonInteractive] Prepending non-interactive env vars to git command")
83
+
84
+ # Return modified arguments
85
+ modified_args = arguments.copy()
86
+ modified_args["command"] = modified_command
87
+ return modified_args
88
+
89
+ return None
@@ -84,11 +84,41 @@ IF COMPLEX (architecture, multi-system, debugging after 2+ failures):
84
84
  SYNTHESIZE findings before proceeding.
85
85
  """
86
86
 
87
+ ULTRATHINK_MODE = """[ultrathink-mode]
88
+ ENGAGE MAXIMUM REASONING CAPACITY.
89
+
90
+ Extended thinking mode activated with 32k token thinking budget.
91
+ This enables exhaustive deep reasoning and multi-dimensional analysis.
92
+
93
+ ## REASONING PRINCIPLES
94
+ - **Deep Analysis**: Consider edge cases, security implications, performance impacts
95
+ - **Multi-Perspective**: Analyze from user, developer, system, and security viewpoints
96
+ - **Strategic Planning**: Consult delphi agent for architecture decisions and hard problems
97
+ - **Root Cause**: Don't treat symptoms - identify and address underlying causes
98
+ - **Risk Assessment**: Evaluate trade-offs, failure modes, and mitigation strategies
99
+
100
+ ## THINKING WORKFLOW
101
+ 1. Problem decomposition into atomic components
102
+ 2. Parallel exploration of solution space (spawn agents for research)
103
+ 3. Consult delphi for strategic guidance on complex decisions
104
+ 4. Multi-dimensional trade-off analysis
105
+ 5. Solution synthesis with verification plan
106
+
107
+ ## VERIFICATION
108
+ - Test assumptions against reality
109
+ - Challenge your own reasoning
110
+ - Seek disconfirming evidence
111
+ - Consider second-order effects
112
+
113
+ Use delphi agent for strategic consultation on architecture, debugging, and complex trade-offs.
114
+ """
115
+
87
116
  KEYWORD_PATTERNS = {
88
117
  r"\bironstar\b": IRONSTAR_MODE,
89
118
  r"\birs\b": IRONSTAR_MODE,
90
119
  r"\bultrawork\b": IRONSTAR_MODE,
91
120
  r"\bulw\b": IRONSTAR_MODE,
121
+ r"\bultrathink\b": ULTRATHINK_MODE,
92
122
  r"\bsearch\b": SEARCH_MODE,
93
123
  r"\banalyze\b": ANALYZE_MODE,
94
124
  r"\banalysis\b": ANALYZE_MODE,
@@ -12,6 +12,13 @@ logger = logging.getLogger(__name__)
12
12
  class HookManager:
13
13
  """
14
14
  Manages the registration and execution of hooks.
15
+
16
+ Hook Types:
17
+ - pre_tool_call: Before tool execution (can modify args or block)
18
+ - post_tool_call: After tool execution (can modify output)
19
+ - pre_model_invoke: Before model invocation (can modify prompt/params)
20
+ - session_idle: When session becomes idle (can inject continuation)
21
+ - pre_compact: Before context compaction (can preserve critical context)
15
22
  """
16
23
 
17
24
  _instance = None
@@ -26,6 +33,13 @@ class HookManager:
26
33
  self.pre_model_invoke_hooks: List[
27
34
  Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
28
35
  ] = []
36
+ # New hook types based on oh-my-opencode patterns
37
+ self.session_idle_hooks: List[
38
+ Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
39
+ ] = []
40
+ self.pre_compact_hooks: List[
41
+ Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
42
+ ] = []
29
43
 
30
44
  @classmethod
31
45
  def get_instance(cls):
@@ -51,6 +65,18 @@ class HookManager:
51
65
  """Run before model invocation. Can modify prompt or parameters."""
52
66
  self.pre_model_invoke_hooks.append(hook)
53
67
 
68
+ def register_session_idle(
69
+ self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
70
+ ):
71
+ """Run when session becomes idle. Can inject continuation prompts."""
72
+ self.session_idle_hooks.append(hook)
73
+
74
+ def register_pre_compact(
75
+ self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
76
+ ):
77
+ """Run before context compaction. Can preserve critical context."""
78
+ self.pre_compact_hooks.append(hook)
79
+
54
80
  async def execute_pre_tool_call(
55
81
  self, tool_name: str, arguments: Dict[str, Any]
56
82
  ) -> Dict[str, Any]:
@@ -91,6 +117,30 @@ class HookManager:
91
117
  logger.error(f"[HookManager] Error in pre_model_invoke hook {hook.__name__}: {e}")
92
118
  return current_params
93
119
 
120
+ async def execute_session_idle(self, params: Dict[str, Any]) -> Dict[str, Any]:
121
+ """Executes all session idle hooks (Stop hook pattern)."""
122
+ current_params = params
123
+ for hook in self.session_idle_hooks:
124
+ try:
125
+ modified_params = await hook(current_params)
126
+ if modified_params is not None:
127
+ current_params = modified_params
128
+ except Exception as e:
129
+ logger.error(f"[HookManager] Error in session_idle hook {hook.__name__}: {e}")
130
+ return current_params
131
+
132
+ async def execute_pre_compact(self, params: Dict[str, Any]) -> Dict[str, Any]:
133
+ """Executes all pre-compact hooks (context preservation)."""
134
+ current_params = params
135
+ for hook in self.pre_compact_hooks:
136
+ try:
137
+ modified_params = await hook(current_params)
138
+ if modified_params is not None:
139
+ current_params = modified_params
140
+ except Exception as e:
141
+ logger.error(f"[HookManager] Error in pre_compact hook {hook.__name__}: {e}")
142
+ return current_params
143
+
94
144
 
95
145
  def get_hook_manager() -> HookManager:
96
146
  return HookManager.get_instance()
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Notification hook for agent spawn messages.
4
+
5
+ Fires on Notification events to output user-friendly messages about
6
+ which agent was spawned, what model it uses, and what task it's doing.
7
+
8
+ Format: spawned {agent_type}:{model}('{description}')
9
+ Example: spawned delphi:gpt-5.2-medium('Debug xyz code')
10
+ """
11
+
12
+ import json
13
+ import sys
14
+ from typing import Optional, Dict, Any
15
+
16
+
17
+ # Agent display model mappings
18
+ AGENT_DISPLAY_MODELS = {
19
+ "explore": "gemini-3-flash",
20
+ "dewey": "gemini-3-flash",
21
+ "document_writer": "gemini-3-flash",
22
+ "multimodal": "gemini-3-flash",
23
+ "frontend": "gemini-3-pro-high",
24
+ "delphi": "gpt-5.2-medium",
25
+ "planner": "opus-4.5",
26
+ "code-reviewer": "sonnet-4.5",
27
+ "debugger": "sonnet-4.5",
28
+ "_default": "sonnet-4.5",
29
+ }
30
+
31
+
32
+ def extract_agent_info(message: str) -> Optional[Dict[str, str]]:
33
+ """
34
+ Extract agent spawn information from notification message.
35
+
36
+ Looks for patterns like:
37
+ - "Agent explore spawned for task..."
38
+ - "Spawned delphi agent: description"
39
+ - Task tool delegation messages
40
+ """
41
+ message_lower = message.lower()
42
+
43
+ # Try to extract agent type from message
44
+ agent_type = None
45
+ description = ""
46
+
47
+ for agent in AGENT_DISPLAY_MODELS.keys():
48
+ if agent == "_default":
49
+ continue
50
+ if agent in message_lower:
51
+ agent_type = agent
52
+ # Extract description after agent name
53
+ idx = message_lower.find(agent)
54
+ description = message[idx + len(agent):].strip()[:60]
55
+ break
56
+
57
+ if not agent_type:
58
+ return None
59
+
60
+ # Clean up description
61
+ description = description.strip(":-() ")
62
+ if not description:
63
+ description = "task delegated"
64
+
65
+ display_model = AGENT_DISPLAY_MODELS.get(agent_type, AGENT_DISPLAY_MODELS["_default"])
66
+
67
+ return {
68
+ "agent_type": agent_type,
69
+ "model": display_model,
70
+ "description": description,
71
+ }
72
+
73
+
74
+ def main():
75
+ """Main hook entry point."""
76
+ try:
77
+ hook_input = json.load(sys.stdin)
78
+ except (json.JSONDecodeError, EOFError):
79
+ return 0
80
+
81
+ # Get notification message
82
+ message = hook_input.get("message", "")
83
+ notification_type = hook_input.get("notification_type", "")
84
+
85
+ # Only process agent-related notifications
86
+ agent_keywords = ["agent", "spawn", "delegat", "task"]
87
+ if not any(kw in message.lower() for kw in agent_keywords):
88
+ return 0
89
+
90
+ # Extract agent info
91
+ agent_info = extract_agent_info(message)
92
+ if not agent_info:
93
+ return 0
94
+
95
+ # Format and output
96
+ output = f"spawned {agent_info['agent_type']}:{agent_info['model']}('{agent_info['description']}')"
97
+ print(output, file=sys.stderr)
98
+
99
+ return 0
100
+
101
+
102
+ if __name__ == "__main__":
103
+ sys.exit(main())
@@ -0,0 +1,127 @@
1
+ """
2
+ Parallel Enforcer Hook - Enforce Parallel Agent Spawning.
3
+
4
+ Detects when 2+ independent tasks exist and injects reminders
5
+ to spawn agents in parallel rather than working sequentially.
6
+
7
+ Based on oh-my-opencode's parallel execution enforcement pattern.
8
+ """
9
+
10
+ import logging
11
+ import re
12
+ from typing import Any, Dict, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Parallel enforcement prompt
17
+ PARALLEL_ENFORCEMENT_PROMPT = """
18
+ [PARALLEL EXECUTION REQUIRED]
19
+
20
+ You have {count} independent pending tasks. You MUST spawn agents for ALL of them simultaneously.
21
+
22
+ CORRECT (Parallel - DO THIS):
23
+ ```
24
+ agent_spawn(prompt="Task 1...", agent_type="explore", description="Task 1")
25
+ agent_spawn(prompt="Task 2...", agent_type="explore", description="Task 2")
26
+ agent_spawn(prompt="Task 3...", agent_type="dewey", description="Task 3")
27
+ // All spawned in ONE response, then wait for results
28
+ ```
29
+
30
+ WRONG (Sequential - DO NOT DO THIS):
31
+ ```
32
+ Mark task 1 in_progress -> work on it -> complete
33
+ Mark task 2 in_progress -> work on it -> complete // TOO SLOW!
34
+ ```
35
+
36
+ RULES:
37
+ 1. Spawn ALL independent tasks simultaneously using agent_spawn
38
+ 2. Do NOT mark any task as in_progress until agents are spawned
39
+ 3. Collect results with agent_output AFTER spawning
40
+ 4. Only work sequentially when tasks have dependencies
41
+ """
42
+
43
+ # Track if enforcement was already triggered this session
44
+ _enforcement_triggered: Dict[str, bool] = {}
45
+
46
+
47
+ async def parallel_enforcer_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
48
+ """
49
+ Post-tool-call hook that triggers after TodoWrite.
50
+
51
+ When 2+ pending todos are detected, injects parallel execution
52
+ enforcement prompt to prevent sequential work patterns.
53
+ """
54
+ tool_name = params.get("tool_name", "")
55
+ output = params.get("output", "")
56
+ session_id = params.get("session_id", "default")
57
+
58
+ # Only trigger for TodoWrite calls
59
+ if tool_name.lower() not in ["todowrite", "todo_write"]:
60
+ return None
61
+
62
+ # Count pending todos
63
+ pending_count = _count_pending_todos(output)
64
+
65
+ if pending_count < 2:
66
+ return None
67
+
68
+ # Check if already triggered recently
69
+ if _enforcement_triggered.get(session_id, False):
70
+ return None
71
+
72
+ # Mark as triggered
73
+ _enforcement_triggered[session_id] = True
74
+
75
+ logger.info(f"[ParallelEnforcerHook] Detected {pending_count} pending todos, enforcing parallel execution")
76
+
77
+ # Inject enforcement prompt
78
+ enforcement = PARALLEL_ENFORCEMENT_PROMPT.format(count=pending_count)
79
+ modified_output = output + "\n\n" + enforcement
80
+
81
+ return modified_output
82
+
83
+
84
+ def _count_pending_todos(output: str) -> int:
85
+ """Count the number of pending todos in TodoWrite output."""
86
+ # Pattern matches various pending todo formats
87
+ patterns = [
88
+ r'\[pending\]',
89
+ r'"status":\s*"pending"',
90
+ r"status:\s*pending",
91
+ r"'status':\s*'pending'",
92
+ ]
93
+
94
+ total = 0
95
+ for pattern in patterns:
96
+ matches = re.findall(pattern, output, re.IGNORECASE)
97
+ total += len(matches)
98
+
99
+ return total
100
+
101
+
102
+ def reset_enforcement(session_id: str = "default"):
103
+ """Reset enforcement state for a session."""
104
+ _enforcement_triggered[session_id] = False
105
+
106
+
107
+ async def parallel_enforcer_post_tool_hook(
108
+ tool_name: str,
109
+ arguments: Dict[str, Any],
110
+ output: str
111
+ ) -> Optional[str]:
112
+ """
113
+ Post-tool-call hook interface for HookManager.
114
+
115
+ Wraps parallel_enforcer_hook for the standard hook signature.
116
+ """
117
+ params = {
118
+ "tool_name": tool_name,
119
+ "arguments": arguments,
120
+ "output": output,
121
+ }
122
+
123
+ result = await parallel_enforcer_hook(params)
124
+
125
+ if isinstance(result, str):
126
+ return result
127
+ return None
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ UserPromptSubmit hook: Pre-emptive parallel execution enforcement.
4
+
5
+ Fires BEFORE response generation to inject parallel execution instructions
6
+ when implementation tasks are detected. Eliminates timing ambiguity.
7
+
8
+ CRITICAL: Also activates stravinsky mode marker when /stravinsky is invoked,
9
+ enabling hard blocking of direct tools (Read, Grep, Bash) via stravinsky_mode.py.
10
+ """
11
+ import json
12
+ import sys
13
+ import re
14
+ from pathlib import Path
15
+
16
+ # Marker file that enables hard blocking of direct tools
17
+ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
18
+
19
+
20
+ def detect_stravinsky_invocation(prompt):
21
+ """Detect if /stravinsky skill is being invoked."""
22
+ patterns = [
23
+ r'/stravinsky',
24
+ r'<command-name>/stravinsky</command-name>',
25
+ r'stravinsky orchestrator',
26
+ r'ultrawork',
27
+ r'ultrathink',
28
+ ]
29
+ prompt_lower = prompt.lower()
30
+ return any(re.search(p, prompt_lower) for p in patterns)
31
+
32
+
33
+ def activate_stravinsky_mode():
34
+ """Create marker file to enable hard blocking of direct tools."""
35
+ try:
36
+ config = {"active": True, "reason": "invoked via /stravinsky skill"}
37
+ STRAVINSKY_MODE_FILE.write_text(json.dumps(config))
38
+ return True
39
+ except IOError:
40
+ return False
41
+
42
+
43
+ def detect_implementation_task(prompt):
44
+ """Detect if prompt is an implementation task requiring parallel execution."""
45
+ keywords = [
46
+ 'implement', 'add', 'create', 'build', 'refactor', 'fix',
47
+ 'update', 'modify', 'change', 'develop', 'write code',
48
+ 'feature', 'bug fix', 'enhancement', 'integrate'
49
+ ]
50
+
51
+ prompt_lower = prompt.lower()
52
+ return any(kw in prompt_lower for kw in keywords)
53
+
54
+
55
+ def main():
56
+ try:
57
+ hook_input = json.load(sys.stdin)
58
+ except (json.JSONDecodeError, EOFError):
59
+ return 0
60
+
61
+ prompt = hook_input.get("prompt", "")
62
+
63
+ # CRITICAL: Activate stravinsky mode if /stravinsky is invoked
64
+ # This creates the marker file that enables hard blocking of direct tools
65
+ is_stravinsky = detect_stravinsky_invocation(prompt)
66
+ if is_stravinsky:
67
+ activate_stravinsky_mode()
68
+
69
+ # Only inject for implementation tasks OR stravinsky invocation
70
+ if not detect_implementation_task(prompt) and not is_stravinsky:
71
+ print(prompt)
72
+ return 0
73
+
74
+ # Inject parallel execution instruction BEFORE prompt
75
+ instruction = """
76
+ [🔄 PARALLEL EXECUTION MODE ACTIVE]
77
+
78
+ When you create a TodoWrite with 2+ pending items:
79
+
80
+ ✅ IMMEDIATELY in THIS SAME RESPONSE (do NOT end response after TodoWrite):
81
+ 1. Spawn Task() for EACH independent pending TODO
82
+ 2. Use: Task(subagent_type="explore"|"Plan"|etc., prompt="...", description="...", run_in_background=true)
83
+ 3. Fire ALL Task calls in ONE response block
84
+ 4. Do NOT mark any TODO as in_progress until Task results return
85
+
86
+ ❌ DO NOT:
87
+ - End your response after TodoWrite
88
+ - Mark TODOs in_progress before spawning Tasks
89
+ - Spawn only ONE Task (spawn ALL independent tasks)
90
+ - Wait for "next response" to spawn Tasks
91
+
92
+ Example pattern (all in SAME response):
93
+ ```
94
+ TodoWrite([task1, task2, task3])
95
+ Task(subagent_type="Explore", prompt="Task 1 details", description="Task 1", run_in_background=true)
96
+ Task(subagent_type="Plan", prompt="Task 2 details", description="Task 2", run_in_background=true)
97
+ Task(subagent_type="Explore", prompt="Task 3 details", description="Task 3", run_in_background=true)
98
+ # Continue response - collect results with TaskOutput
99
+ ```
100
+
101
+ ---
102
+
103
+ """
104
+
105
+ modified_prompt = instruction + prompt
106
+ print(modified_prompt)
107
+ return 0
108
+
109
+
110
+ if __name__ == "__main__":
111
+ sys.exit(main())
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PreCompact hook: Context preservation before compaction.
4
+
5
+ Fires before Claude Code compacts conversation context to:
6
+ 1. Preserve critical context patterns
7
+ 2. Maintain stravinsky mode state
8
+ 3. Warn about information loss
9
+ 4. Save state for recovery
10
+
11
+ Cannot block compaction (exit 2 only shows error).
12
+ """
13
+
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+ from datetime import datetime
18
+ from typing import List, Dict, Any
19
+
20
+
21
+ STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
22
+ STATE_DIR = Path.home() / ".claude" / "state"
23
+ COMPACTION_LOG = STATE_DIR / "compaction.jsonl"
24
+
25
+ # Patterns to preserve
26
+ PRESERVE_PATTERNS = [
27
+ "ARCHITECTURE:",
28
+ "DESIGN DECISION:",
29
+ "CONSTRAINT:",
30
+ "REQUIREMENT:",
31
+ "MUST NOT:",
32
+ "NEVER:",
33
+ "CRITICAL ERROR:",
34
+ "CURRENT TASK:",
35
+ "BLOCKED BY:",
36
+ "[STRAVINSKY MODE]",
37
+ "PARALLEL_DELEGATION:",
38
+ ]
39
+
40
+
41
+ def ensure_state_dir():
42
+ """Ensure state directory exists."""
43
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
44
+
45
+
46
+ def get_stravinsky_mode_state() -> Dict[str, Any]:
47
+ """Read stravinsky mode state."""
48
+ if not STRAVINSKY_MODE_FILE.exists():
49
+ return {"active": False}
50
+ try:
51
+ content = STRAVINSKY_MODE_FILE.read_text().strip()
52
+ return json.loads(content) if content else {"active": True}
53
+ except (json.JSONDecodeError, IOError):
54
+ return {"active": True}
55
+
56
+
57
+ def extract_preserved_context(prompt: str) -> List[str]:
58
+ """Extract context matching preservation patterns."""
59
+ preserved = []
60
+ lines = prompt.split("\n")
61
+
62
+ for i, line in enumerate(lines):
63
+ for pattern in PRESERVE_PATTERNS:
64
+ if pattern in line:
65
+ # Capture line + 2 more for context
66
+ context = "\n".join(lines[i:min(i+3, len(lines))])
67
+ preserved.append(context)
68
+ break
69
+
70
+ return preserved[:15] # Max 15 items
71
+
72
+
73
+ def log_compaction(preserved: List[str], stravinsky_active: bool):
74
+ """Log compaction event for audit."""
75
+ ensure_state_dir()
76
+
77
+ entry = {
78
+ "timestamp": datetime.utcnow().isoformat(),
79
+ "preserved_count": len(preserved),
80
+ "stravinsky_mode": stravinsky_active,
81
+ "preview": [p[:50] for p in preserved[:3]],
82
+ }
83
+
84
+ try:
85
+ with COMPACTION_LOG.open("a") as f:
86
+ f.write(json.dumps(entry) + "\n")
87
+ except IOError:
88
+ pass
89
+
90
+
91
+ def main():
92
+ """Main hook entry point."""
93
+ try:
94
+ hook_input = json.load(sys.stdin)
95
+ except (json.JSONDecodeError, EOFError):
96
+ return 0
97
+
98
+ prompt = hook_input.get("prompt", "")
99
+ trigger = hook_input.get("trigger", "auto")
100
+
101
+ # Get stravinsky mode state
102
+ strav_state = get_stravinsky_mode_state()
103
+ stravinsky_active = strav_state.get("active", False)
104
+
105
+ # Extract preserved context
106
+ preserved = extract_preserved_context(prompt)
107
+
108
+ # Log compaction event
109
+ log_compaction(preserved, stravinsky_active)
110
+
111
+ # Output preservation warning
112
+ if preserved or stravinsky_active:
113
+ print(f"\n[PreCompact] Context compaction triggered ({trigger})", file=sys.stderr)
114
+ print(f" Preserved items: {len(preserved)}", file=sys.stderr)
115
+ if stravinsky_active:
116
+ print(" [STRAVINSKY MODE ACTIVE] - State will persist", file=sys.stderr)
117
+ print(" Audit log: ~/.claude/state/compaction.jsonl", file=sys.stderr)
118
+
119
+ return 0
120
+
121
+
122
+ if __name__ == "__main__":
123
+ sys.exit(main())