stravinsky 0.2.52__py3-none-any.whl → 0.2.67__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of stravinsky might be problematic. Click here for more details.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/cli/__init__.py +6 -0
- mcp_bridge/cli/install_hooks.py +1265 -0
- mcp_bridge/cli/session_report.py +585 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
- mcp_bridge/hooks/README.md +215 -0
- mcp_bridge/hooks/__init__.py +117 -63
- mcp_bridge/hooks/edit_recovery.py +42 -37
- mcp_bridge/hooks/git_noninteractive.py +89 -0
- mcp_bridge/hooks/keyword_detector.py +30 -0
- mcp_bridge/hooks/notification_hook.py +103 -0
- mcp_bridge/hooks/parallel_execution.py +111 -0
- mcp_bridge/hooks/pre_compact.py +82 -183
- mcp_bridge/hooks/rules_injector.py +507 -0
- mcp_bridge/hooks/session_notifier.py +125 -0
- mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
- mcp_bridge/hooks/subagent_stop.py +98 -0
- mcp_bridge/hooks/task_validator.py +73 -0
- mcp_bridge/hooks/tmux_manager.py +141 -0
- mcp_bridge/hooks/todo_continuation.py +90 -0
- mcp_bridge/hooks/todo_delegation.py +88 -0
- mcp_bridge/hooks/tool_messaging.py +164 -0
- mcp_bridge/hooks/truncator.py +21 -17
- mcp_bridge/prompts/multimodal.py +24 -3
- mcp_bridge/server.py +12 -1
- mcp_bridge/server_tools.py +5 -0
- mcp_bridge/tools/agent_manager.py +30 -11
- mcp_bridge/tools/code_search.py +81 -9
- mcp_bridge/tools/lsp/tools.py +6 -2
- mcp_bridge/tools/model_invoke.py +76 -1
- mcp_bridge/tools/templates.py +32 -18
- stravinsky-0.2.67.dist-info/METADATA +284 -0
- {stravinsky-0.2.52.dist-info → stravinsky-0.2.67.dist-info}/RECORD +36 -23
- stravinsky-0.2.67.dist-info/entry_points.txt +5 -0
- mcp_bridge/native_hooks/edit_recovery.py +0 -46
- mcp_bridge/native_hooks/todo_delegation.py +0 -54
- mcp_bridge/native_hooks/truncator.py +0 -23
- stravinsky-0.2.52.dist-info/METADATA +0 -204
- stravinsky-0.2.52.dist-info/entry_points.txt +0 -3
- /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
- {stravinsky-0.2.52.dist-info → stravinsky-0.2.67.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,
|
|
@@ -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,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())
|
mcp_bridge/hooks/pre_compact.py
CHANGED
|
@@ -1,224 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
1
2
|
"""
|
|
2
|
-
PreCompact
|
|
3
|
+
PreCompact hook: Context preservation before compaction.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
Cannot block compaction (exit 2 only shows error).
|
|
9
12
|
"""
|
|
10
13
|
|
|
11
|
-
import
|
|
12
|
-
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import List, Dict, Any
|
|
13
19
|
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
STRAVINSKY_MODE_FILE = Path.home() / ".stravinsky_mode"
|
|
22
|
+
STATE_DIR = Path.home() / ".claude" / "state"
|
|
23
|
+
COMPACTION_LOG = STATE_DIR / "compaction.jsonl"
|
|
18
24
|
|
|
19
|
-
#
|
|
25
|
+
# Patterns to preserve
|
|
20
26
|
PRESERVE_PATTERNS = [
|
|
21
|
-
# Architecture decisions
|
|
22
27
|
"ARCHITECTURE:",
|
|
23
28
|
"DESIGN DECISION:",
|
|
24
|
-
"## Architecture",
|
|
25
|
-
|
|
26
|
-
# Important constraints
|
|
27
29
|
"CONSTRAINT:",
|
|
28
30
|
"REQUIREMENT:",
|
|
29
31
|
"MUST NOT:",
|
|
30
32
|
"NEVER:",
|
|
31
|
-
|
|
32
|
-
# Session state
|
|
33
|
+
"CRITICAL ERROR:",
|
|
33
34
|
"CURRENT TASK:",
|
|
34
35
|
"BLOCKED BY:",
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
# Critical errors
|
|
38
|
-
"CRITICAL ERROR:",
|
|
39
|
-
"SECURITY ISSUE:",
|
|
40
|
-
"BREAKING CHANGE:",
|
|
36
|
+
"[STRAVINSKY MODE]",
|
|
37
|
+
"PARALLEL_DELEGATION:",
|
|
41
38
|
]
|
|
42
39
|
|
|
43
|
-
# Memory anchors to inject into compaction
|
|
44
|
-
MEMORY_ANCHORS: List[str] = []
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def register_memory_anchor(anchor: str, priority: str = "normal"):
|
|
48
|
-
"""
|
|
49
|
-
Register a memory anchor to preserve during compaction.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
anchor: The text to preserve
|
|
53
|
-
priority: "critical" or "normal"
|
|
54
|
-
"""
|
|
55
|
-
if priority == "critical":
|
|
56
|
-
MEMORY_ANCHORS.insert(0, f"[CRITICAL] {anchor}")
|
|
57
|
-
else:
|
|
58
|
-
MEMORY_ANCHORS.append(anchor)
|
|
59
|
-
|
|
60
|
-
# Limit to 10 anchors to prevent bloat
|
|
61
|
-
while len(MEMORY_ANCHORS) > 10:
|
|
62
|
-
MEMORY_ANCHORS.pop()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def clear_memory_anchors():
|
|
66
|
-
"""Clear all registered memory anchors."""
|
|
67
|
-
MEMORY_ANCHORS.clear()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
async def pre_compact_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
71
|
-
"""
|
|
72
|
-
Pre-model-invoke hook that runs before context compaction.
|
|
73
|
-
|
|
74
|
-
Uses Gemini to intelligently extract and preserve critical context
|
|
75
|
-
that should survive summarization.
|
|
76
|
-
"""
|
|
77
|
-
global _in_preservation
|
|
78
|
-
|
|
79
|
-
# Prevent recursive calls
|
|
80
|
-
if _in_preservation:
|
|
81
|
-
return None
|
|
82
|
-
|
|
83
|
-
prompt = params.get("prompt", "")
|
|
84
|
-
|
|
85
|
-
# Only activate for compaction-related prompts
|
|
86
|
-
if not _is_compaction_prompt(prompt):
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
# Collect pattern-matched context
|
|
90
|
-
preserved_context = _extract_preserved_context(prompt)
|
|
91
|
-
preserved_context.extend(MEMORY_ANCHORS)
|
|
92
|
-
|
|
93
|
-
# Use Gemini for intelligent context extraction if prompt is long
|
|
94
|
-
if len(prompt) > 50000:
|
|
95
|
-
try:
|
|
96
|
-
_in_preservation = True
|
|
97
|
-
gemini_context = await _extract_context_with_gemini(prompt)
|
|
98
|
-
if gemini_context:
|
|
99
|
-
preserved_context.extend(gemini_context)
|
|
100
|
-
except Exception as e:
|
|
101
|
-
logger.warning(f"[PreCompactHook] Gemini extraction failed: {e}")
|
|
102
|
-
finally:
|
|
103
|
-
_in_preservation = False
|
|
104
|
-
|
|
105
|
-
if not preserved_context:
|
|
106
|
-
return None
|
|
107
|
-
|
|
108
|
-
# Build preservation section
|
|
109
|
-
preservation_section = _build_preservation_section(preserved_context)
|
|
110
|
-
|
|
111
|
-
logger.info(f"[PreCompactHook] Preserving {len(preserved_context)} context items")
|
|
112
|
-
|
|
113
|
-
# Inject into prompt
|
|
114
|
-
modified_prompt = prompt + "\n\n" + preservation_section
|
|
115
|
-
|
|
116
|
-
return {**params, "prompt": modified_prompt}
|
|
117
40
|
|
|
41
|
+
def ensure_state_dir():
|
|
42
|
+
"""Ensure state directory exists."""
|
|
43
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
118
44
|
|
|
119
|
-
async def _extract_context_with_gemini(prompt: str) -> List[str]:
|
|
120
|
-
"""
|
|
121
|
-
Use Gemini to intelligently extract critical context to preserve.
|
|
122
45
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
List of critical context items to preserve
|
|
128
|
-
"""
|
|
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}
|
|
129
50
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
truncated = prompt[:max_chars] if len(prompt) > max_chars else prompt
|
|
135
|
-
|
|
136
|
-
extraction_prompt = f"""Analyze this conversation and extract ONLY the most critical information that MUST be preserved during summarization.
|
|
137
|
-
|
|
138
|
-
Focus on:
|
|
139
|
-
1. Architecture decisions and their rationale
|
|
140
|
-
2. Critical constraints or requirements
|
|
141
|
-
3. Important error patterns or debugging insights
|
|
142
|
-
4. Key file paths and their purposes
|
|
143
|
-
5. Unfinished tasks or blocking issues
|
|
144
|
-
|
|
145
|
-
Return a bullet list of critical items (max 10). Be extremely concise.
|
|
146
|
-
|
|
147
|
-
CONVERSATION:
|
|
148
|
-
{truncated}
|
|
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}
|
|
149
55
|
|
|
150
|
-
CRITICAL ITEMS TO PRESERVE:"""
|
|
151
56
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
temperature=0.1,
|
|
157
|
-
)
|
|
57
|
+
def extract_preserved_context(prompt: str) -> List[str]:
|
|
58
|
+
"""Extract context matching preservation patterns."""
|
|
59
|
+
preserved = []
|
|
60
|
+
lines = prompt.split("\n")
|
|
158
61
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
161
69
|
|
|
162
|
-
|
|
163
|
-
lines = result.strip().split("\n")
|
|
164
|
-
items = []
|
|
165
|
-
for line in lines:
|
|
166
|
-
line = line.strip()
|
|
167
|
-
if line.startswith(("-", "*", "•")) or (len(line) > 1 and line[0].isdigit() and line[1] in ".):"):
|
|
168
|
-
# Clean up the bullet
|
|
169
|
-
item = line.lstrip("-*•0123456789.): ").strip()
|
|
170
|
-
if item and len(item) > 10:
|
|
171
|
-
items.append(item)
|
|
70
|
+
return preserved[:15] # Max 15 items
|
|
172
71
|
|
|
173
|
-
return items[:10] # Max 10 items
|
|
174
72
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
73
|
+
def log_compaction(preserved: List[str], stravinsky_active: bool):
|
|
74
|
+
"""Log compaction event for audit."""
|
|
75
|
+
ensure_state_dir()
|
|
178
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
|
+
}
|
|
179
83
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
"reduce context size",
|
|
186
|
-
"context window",
|
|
187
|
-
"summarization",
|
|
188
|
-
]
|
|
84
|
+
try:
|
|
85
|
+
with COMPACTION_LOG.open("a") as f:
|
|
86
|
+
f.write(json.dumps(entry) + "\n")
|
|
87
|
+
except IOError:
|
|
88
|
+
pass
|
|
189
89
|
|
|
190
|
-
prompt_lower = prompt.lower()
|
|
191
|
-
return any(signal in prompt_lower for signal in compaction_signals)
|
|
192
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
|
|
193
97
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
preserved = []
|
|
197
|
-
lines = prompt.split("\n")
|
|
98
|
+
prompt = hook_input.get("prompt", "")
|
|
99
|
+
trigger = hook_input.get("trigger", "auto")
|
|
198
100
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
# Capture the line and next 2 lines for context
|
|
203
|
-
context_lines = lines[i:i+3]
|
|
204
|
-
preserved.append("\n".join(context_lines))
|
|
205
|
-
break
|
|
101
|
+
# Get stravinsky mode state
|
|
102
|
+
strav_state = get_stravinsky_mode_state()
|
|
103
|
+
stravinsky_active = strav_state.get("active", False)
|
|
206
104
|
|
|
207
|
-
|
|
105
|
+
# Extract preserved context
|
|
106
|
+
preserved = extract_preserved_context(prompt)
|
|
208
107
|
|
|
108
|
+
# Log compaction event
|
|
109
|
+
log_compaction(preserved, stravinsky_active)
|
|
209
110
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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)
|
|
214
118
|
|
|
215
|
-
|
|
119
|
+
return 0
|
|
216
120
|
|
|
217
|
-
"""
|
|
218
|
-
for i, item in enumerate(context_items, 1):
|
|
219
|
-
section += f"{i}. {item}\n\n"
|
|
220
121
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"""
|
|
224
|
-
return section
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
sys.exit(main())
|