claude-memory-agent 2.0.0
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.
- package/.env.example +107 -0
- package/README.md +200 -0
- package/agent_card.py +512 -0
- package/bin/cli.js +181 -0
- package/bin/postinstall.js +216 -0
- package/config.py +104 -0
- package/dashboard.html +2689 -0
- package/hooks/README.md +196 -0
- package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
- package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/hooks/auto-detect-response.py +348 -0
- package/hooks/auto_capture.py +255 -0
- package/hooks/detect-correction.py +173 -0
- package/hooks/grounding-hook.py +348 -0
- package/hooks/log-tool-use.py +234 -0
- package/hooks/log-user-request.py +208 -0
- package/hooks/pre-tool-decision.py +218 -0
- package/hooks/problem-detector.py +343 -0
- package/hooks/session_end.py +192 -0
- package/hooks/session_start.py +227 -0
- package/install.py +887 -0
- package/main.py +2859 -0
- package/manager.py +997 -0
- package/package.json +55 -0
- package/requirements.txt +8 -0
- package/run_server.py +136 -0
- package/services/__init__.py +50 -0
- package/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
- package/services/__pycache__/auth.cpython-312.pyc +0 -0
- package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
- package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
- package/services/__pycache__/confidence.cpython-312.pyc +0 -0
- package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
- package/services/__pycache__/database.cpython-312.pyc +0 -0
- package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
- package/services/__pycache__/insights.cpython-312.pyc +0 -0
- package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
- package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
- package/services/__pycache__/timeline.cpython-312.pyc +0 -0
- package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
- package/services/__pycache__/websocket.cpython-312.pyc +0 -0
- package/services/agent_registry.py +753 -0
- package/services/auth.py +331 -0
- package/services/auto_inject.py +250 -0
- package/services/claude_md_sync.py +275 -0
- package/services/cleanup.py +667 -0
- package/services/compaction_flush.py +447 -0
- package/services/confidence.py +301 -0
- package/services/daily_log.py +333 -0
- package/services/database.py +2485 -0
- package/services/embeddings.py +358 -0
- package/services/insights.py +632 -0
- package/services/llm_analyzer.py +595 -0
- package/services/memory_md_sync.py +409 -0
- package/services/retry_queue.py +453 -0
- package/services/timeline.py +579 -0
- package/services/vector_index.py +398 -0
- package/services/websocket.py +257 -0
- package/skills/__init__.py +6 -0
- package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/__pycache__/admin.cpython-312.pyc +0 -0
- package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
- package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
- package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
- package/skills/__pycache__/insights.cpython-312.pyc +0 -0
- package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
- package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
- package/skills/__pycache__/search.cpython-312.pyc +0 -0
- package/skills/__pycache__/state.cpython-312.pyc +0 -0
- package/skills/__pycache__/store.cpython-312.pyc +0 -0
- package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
- package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
- package/skills/__pycache__/verification.cpython-312.pyc +0 -0
- package/skills/admin.py +469 -0
- package/skills/checkpoint.py +198 -0
- package/skills/claude_md.py +363 -0
- package/skills/cleanup.py +241 -0
- package/skills/grounding.py +801 -0
- package/skills/insights.py +231 -0
- package/skills/natural_language.py +277 -0
- package/skills/retrieve.py +67 -0
- package/skills/search.py +213 -0
- package/skills/state.py +182 -0
- package/skills/store.py +179 -0
- package/skills/summarize.py +588 -0
- package/skills/timeline.py +387 -0
- package/skills/verification.py +391 -0
- package/start_daemon.py +155 -0
- package/test_automation.py +221 -0
- package/test_complete.py +338 -0
- package/test_full.py +322 -0
- package/update_system.py +817 -0
- package/verify_db.py +134 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
User Request Logger Hook for Claude Code
|
|
4
|
+
|
|
5
|
+
This script logs user requests to the session timeline automatically.
|
|
6
|
+
Called via UserPromptSubmit hook - logs the request, then grounding-hook injects context.
|
|
7
|
+
|
|
8
|
+
The session file stores JSON with:
|
|
9
|
+
- session_id: The current session ID
|
|
10
|
+
- current_request_id: The event ID of the current user_request (for causal chain linking)
|
|
11
|
+
- request_started_at: Timestamp of when the request started
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import requests
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
# Configure logging to stderr (important for Claude Code hooks)
|
|
24
|
+
logging.basicConfig(
|
|
25
|
+
level=logging.INFO,
|
|
26
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
27
|
+
stream=sys.stderr
|
|
28
|
+
)
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Configuration from environment
|
|
32
|
+
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
33
|
+
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def safe_get(data: Any, *keys, default: Any = None) -> Any:
|
|
37
|
+
"""
|
|
38
|
+
Safely navigate nested data structures (dicts and lists).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data: The data structure to navigate
|
|
42
|
+
*keys: Keys (str for dict) or indices (int for list) to traverse
|
|
43
|
+
default: Value to return if path doesn't exist
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The value at the path, or default if not found
|
|
47
|
+
"""
|
|
48
|
+
for key in keys:
|
|
49
|
+
if data is None:
|
|
50
|
+
return default
|
|
51
|
+
if isinstance(data, dict):
|
|
52
|
+
data = data.get(key, default)
|
|
53
|
+
elif isinstance(data, list) and isinstance(key, int):
|
|
54
|
+
if 0 <= key < len(data):
|
|
55
|
+
data = data[key]
|
|
56
|
+
else:
|
|
57
|
+
return default
|
|
58
|
+
else:
|
|
59
|
+
return default
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_session_data():
|
|
64
|
+
"""Load session data from JSON file."""
|
|
65
|
+
session_file = Path(os.getcwd()) / ".claude_session"
|
|
66
|
+
if session_file.exists():
|
|
67
|
+
try:
|
|
68
|
+
content = session_file.read_text().strip()
|
|
69
|
+
# Try JSON format first
|
|
70
|
+
return json.loads(content)
|
|
71
|
+
except json.JSONDecodeError as e:
|
|
72
|
+
logger.debug(f"JSON decode error, trying legacy format: {e}")
|
|
73
|
+
# Fall back to legacy plain text format (just session_id)
|
|
74
|
+
try:
|
|
75
|
+
content = session_file.read_text().strip()
|
|
76
|
+
return {"session_id": content}
|
|
77
|
+
except (IOError, OSError) as read_err:
|
|
78
|
+
logger.warning(f"Failed to read session file: {read_err}")
|
|
79
|
+
return None
|
|
80
|
+
except (IOError, OSError) as e:
|
|
81
|
+
logger.warning(f"Failed to read session file: {e}")
|
|
82
|
+
return None
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def save_session_data(data: dict):
|
|
87
|
+
"""Save session data to JSON file."""
|
|
88
|
+
session_file = Path(os.getcwd()) / ".claude_session"
|
|
89
|
+
try:
|
|
90
|
+
session_file.write_text(json.dumps(data, indent=2))
|
|
91
|
+
except (IOError, OSError) as e:
|
|
92
|
+
logger.warning(f"Failed to save session data: {e}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_session_id():
|
|
96
|
+
"""Get session ID from environment or file."""
|
|
97
|
+
session_id = os.getenv("CLAUDE_SESSION_ID")
|
|
98
|
+
if session_id:
|
|
99
|
+
return session_id
|
|
100
|
+
|
|
101
|
+
data = load_session_data()
|
|
102
|
+
return data.get("session_id") if data else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def call_memory_agent(skill_id: str, params: dict) -> Optional[dict]:
|
|
106
|
+
"""Call the memory agent API."""
|
|
107
|
+
try:
|
|
108
|
+
response = requests.post(
|
|
109
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
110
|
+
json={
|
|
111
|
+
"jsonrpc": "2.0",
|
|
112
|
+
"id": "log-hook",
|
|
113
|
+
"method": "tasks/send",
|
|
114
|
+
"params": {
|
|
115
|
+
"message": {"parts": [{"type": "text", "text": ""}]},
|
|
116
|
+
"metadata": {
|
|
117
|
+
"skill_id": skill_id,
|
|
118
|
+
"params": params
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
timeout=API_TIMEOUT
|
|
123
|
+
)
|
|
124
|
+
return response.json()
|
|
125
|
+
except requests.RequestException as e:
|
|
126
|
+
logger.debug(f"Memory agent request failed for skill '{skill_id}': {e}")
|
|
127
|
+
return None
|
|
128
|
+
except json.JSONDecodeError as e:
|
|
129
|
+
logger.debug(f"Failed to decode memory agent response for skill '{skill_id}': {e}")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
"""Log the user's request to timeline."""
|
|
135
|
+
# Read hook input from stdin
|
|
136
|
+
try:
|
|
137
|
+
hook_input = json.load(sys.stdin)
|
|
138
|
+
except json.JSONDecodeError as e:
|
|
139
|
+
logger.debug(f"Failed to parse hook input JSON: {e}")
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
except (IOError, OSError) as e:
|
|
142
|
+
logger.debug(f"Failed to read stdin: {e}")
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
# Get user message from hook input
|
|
146
|
+
user_message = hook_input.get("user_prompt", "")
|
|
147
|
+
if not user_message:
|
|
148
|
+
# Try alternative format
|
|
149
|
+
session_messages = hook_input.get("session_messages", [])
|
|
150
|
+
if session_messages:
|
|
151
|
+
last_msg = session_messages[-1]
|
|
152
|
+
if last_msg.get("role") == "user":
|
|
153
|
+
user_message = last_msg.get("content", "")
|
|
154
|
+
|
|
155
|
+
if not user_message:
|
|
156
|
+
sys.exit(0)
|
|
157
|
+
|
|
158
|
+
# Load session data
|
|
159
|
+
session_data = load_session_data()
|
|
160
|
+
if not session_data:
|
|
161
|
+
sys.exit(0)
|
|
162
|
+
|
|
163
|
+
session_id = session_data.get("session_id")
|
|
164
|
+
if not session_id:
|
|
165
|
+
sys.exit(0)
|
|
166
|
+
|
|
167
|
+
# Truncate long messages
|
|
168
|
+
summary = user_message[:200]
|
|
169
|
+
if len(user_message) > 200:
|
|
170
|
+
summary += "..."
|
|
171
|
+
|
|
172
|
+
# Log to timeline
|
|
173
|
+
result = call_memory_agent("timeline_log", {
|
|
174
|
+
"session_id": session_id,
|
|
175
|
+
"event_type": "user_request",
|
|
176
|
+
"summary": summary,
|
|
177
|
+
"details": user_message if len(user_message) > 200 else None,
|
|
178
|
+
"project_path": os.getcwd()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
# Update session state with current goal
|
|
182
|
+
call_memory_agent("state_update", {
|
|
183
|
+
"session_id": session_id,
|
|
184
|
+
"current_goal": summary
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
# Save the event_id as current_request_id for causal chain linking
|
|
188
|
+
if result:
|
|
189
|
+
# Parse result using safe_get - the memory agent returns JSON-RPC format
|
|
190
|
+
# Navigate: result -> artifacts[0] -> parts[0] -> text
|
|
191
|
+
artifact_text = safe_get(result, "result", "artifacts", 0, "parts", 0, "text")
|
|
192
|
+
|
|
193
|
+
if artifact_text:
|
|
194
|
+
try:
|
|
195
|
+
skill_result = json.loads(artifact_text)
|
|
196
|
+
event_id = skill_result.get("event_id")
|
|
197
|
+
if event_id:
|
|
198
|
+
session_data["current_request_id"] = event_id
|
|
199
|
+
session_data["request_started_at"] = datetime.now().isoformat()
|
|
200
|
+
save_session_data(session_data)
|
|
201
|
+
except json.JSONDecodeError as e:
|
|
202
|
+
logger.debug(f"Failed to parse skill result JSON: {e}")
|
|
203
|
+
|
|
204
|
+
sys.exit(0)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
main()
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Pre-Tool Decision Hook for Claude Code
|
|
4
|
+
|
|
5
|
+
This script captures the DECISION (why) before each tool call.
|
|
6
|
+
Called via PreToolUse hook - logs the reasoning before action executes.
|
|
7
|
+
|
|
8
|
+
Creates a chain: user_request → decision → action
|
|
9
|
+
The decision event ID is stored for PostToolUse to link the action.
|
|
10
|
+
|
|
11
|
+
Inspired by A* algorithm: selective capture of important decisions,
|
|
12
|
+
not blind logging of everything (Dijkstra-style).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import requests
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
# Configure logging to stderr (important for Claude Code hooks)
|
|
24
|
+
logging.basicConfig(
|
|
25
|
+
level=logging.INFO,
|
|
26
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
27
|
+
stream=sys.stderr
|
|
28
|
+
)
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Configuration from environment
|
|
32
|
+
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
33
|
+
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
|
|
34
|
+
|
|
35
|
+
# Tools worth capturing decisions for (high-signal actions)
|
|
36
|
+
DECISION_WORTHY_TOOLS = {
|
|
37
|
+
"Edit": "editing",
|
|
38
|
+
"Write": "creating",
|
|
39
|
+
"Bash": "executing",
|
|
40
|
+
"Task": "delegating to agent",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Low-signal tools (just reading, not changing state)
|
|
44
|
+
READ_ONLY_TOOLS = {"Read", "Grep", "Glob", "WebFetch", "WebSearch"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def safe_get(data, *keys, default=None):
|
|
48
|
+
"""Safely navigate nested data structures."""
|
|
49
|
+
for key in keys:
|
|
50
|
+
if data is None:
|
|
51
|
+
return default
|
|
52
|
+
if isinstance(data, dict):
|
|
53
|
+
data = data.get(key, default)
|
|
54
|
+
elif isinstance(data, list) and isinstance(key, int):
|
|
55
|
+
if 0 <= key < len(data):
|
|
56
|
+
data = data[key]
|
|
57
|
+
else:
|
|
58
|
+
return default
|
|
59
|
+
else:
|
|
60
|
+
return default
|
|
61
|
+
return data
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_session_data():
|
|
65
|
+
"""Load session data from JSON file."""
|
|
66
|
+
session_file = Path(os.getcwd()) / ".claude_session"
|
|
67
|
+
if session_file.exists():
|
|
68
|
+
try:
|
|
69
|
+
content = session_file.read_text().strip()
|
|
70
|
+
return json.loads(content)
|
|
71
|
+
except (json.JSONDecodeError, IOError, OSError) as e:
|
|
72
|
+
logger.debug(f"Failed to load session data: {e}")
|
|
73
|
+
return None
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def save_session_data(data: dict):
|
|
78
|
+
"""Save session data to JSON file."""
|
|
79
|
+
session_file = Path(os.getcwd()) / ".claude_session"
|
|
80
|
+
try:
|
|
81
|
+
session_file.write_text(json.dumps(data, indent=2))
|
|
82
|
+
except (IOError, OSError) as e:
|
|
83
|
+
logger.warning(f"Failed to save session data: {e}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def call_memory_agent(skill_id: str, params: dict) -> Optional[dict]:
|
|
87
|
+
"""Call the memory agent API."""
|
|
88
|
+
try:
|
|
89
|
+
response = requests.post(
|
|
90
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
91
|
+
json={
|
|
92
|
+
"jsonrpc": "2.0",
|
|
93
|
+
"id": "pre-tool-hook",
|
|
94
|
+
"method": "tasks/send",
|
|
95
|
+
"params": {
|
|
96
|
+
"message": {"parts": [{"type": "text", "text": ""}]},
|
|
97
|
+
"metadata": {
|
|
98
|
+
"skill_id": skill_id,
|
|
99
|
+
"params": params
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
timeout=API_TIMEOUT
|
|
104
|
+
)
|
|
105
|
+
return response.json()
|
|
106
|
+
except (requests.RequestException, json.JSONDecodeError) as e:
|
|
107
|
+
logger.debug(f"Memory agent request failed: {e}")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def generate_decision_summary(tool_name: str, tool_input: dict) -> str:
|
|
112
|
+
"""Generate a human-readable decision summary.
|
|
113
|
+
|
|
114
|
+
This is the WHY, not the WHAT.
|
|
115
|
+
"""
|
|
116
|
+
verb = DECISION_WORTHY_TOOLS.get(tool_name, "using")
|
|
117
|
+
|
|
118
|
+
if tool_name == "Edit":
|
|
119
|
+
file_path = tool_input.get("file_path", "unknown")
|
|
120
|
+
old_string = tool_input.get("old_string", "")[:50]
|
|
121
|
+
return f"Decided to modify {Path(file_path).name}: changing '{old_string}...'"
|
|
122
|
+
|
|
123
|
+
elif tool_name == "Write":
|
|
124
|
+
file_path = tool_input.get("file_path", "unknown")
|
|
125
|
+
return f"Decided to create/overwrite {Path(file_path).name}"
|
|
126
|
+
|
|
127
|
+
elif tool_name == "Bash":
|
|
128
|
+
command = tool_input.get("command", "")[:80]
|
|
129
|
+
description = tool_input.get("description", "")
|
|
130
|
+
if description:
|
|
131
|
+
return f"Decided to run: {description}"
|
|
132
|
+
return f"Decided to execute: {command}"
|
|
133
|
+
|
|
134
|
+
elif tool_name == "Task":
|
|
135
|
+
agent_type = tool_input.get("subagent_type", "unknown")
|
|
136
|
+
task_desc = tool_input.get("description", "")[:50]
|
|
137
|
+
return f"Decided to delegate to {agent_type} agent: {task_desc}"
|
|
138
|
+
|
|
139
|
+
return f"Decided to use {tool_name}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main():
|
|
143
|
+
"""Capture the decision before tool execution."""
|
|
144
|
+
# Read hook input from stdin
|
|
145
|
+
try:
|
|
146
|
+
hook_input = json.load(sys.stdin)
|
|
147
|
+
except (json.JSONDecodeError, IOError, OSError) as e:
|
|
148
|
+
logger.debug(f"Failed to parse hook input: {e}")
|
|
149
|
+
sys.exit(0)
|
|
150
|
+
|
|
151
|
+
tool_name = hook_input.get("tool_name") or hook_input.get("tool")
|
|
152
|
+
if not tool_name:
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
|
|
155
|
+
# Skip read-only tools (low signal, high noise)
|
|
156
|
+
# This is the A* heuristic: skip nodes that don't lead to the goal
|
|
157
|
+
if tool_name in READ_ONLY_TOOLS:
|
|
158
|
+
sys.exit(0)
|
|
159
|
+
|
|
160
|
+
# Skip if not a decision-worthy tool
|
|
161
|
+
if tool_name not in DECISION_WORTHY_TOOLS:
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
|
|
164
|
+
# Load session data
|
|
165
|
+
session_data = load_session_data()
|
|
166
|
+
if not session_data:
|
|
167
|
+
sys.exit(0)
|
|
168
|
+
|
|
169
|
+
session_id = session_data.get("session_id")
|
|
170
|
+
if not session_id:
|
|
171
|
+
sys.exit(0)
|
|
172
|
+
|
|
173
|
+
# Get root event (user request) for causal chain
|
|
174
|
+
root_event_id = session_data.get("current_request_id")
|
|
175
|
+
|
|
176
|
+
tool_input = hook_input.get("tool_input") or hook_input.get("input") or {}
|
|
177
|
+
|
|
178
|
+
# Generate decision summary (the WHY)
|
|
179
|
+
decision_summary = generate_decision_summary(tool_name, tool_input)
|
|
180
|
+
|
|
181
|
+
# Log the decision event
|
|
182
|
+
log_params = {
|
|
183
|
+
"session_id": session_id,
|
|
184
|
+
"event_type": "decision",
|
|
185
|
+
"summary": decision_summary[:200],
|
|
186
|
+
"details": json.dumps({
|
|
187
|
+
"tool": tool_name,
|
|
188
|
+
"reasoning": "Pre-action decision capture"
|
|
189
|
+
}),
|
|
190
|
+
"project_path": os.getcwd()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Link to causal chain
|
|
194
|
+
if root_event_id:
|
|
195
|
+
log_params["root_event_id"] = root_event_id
|
|
196
|
+
log_params["parent_event_id"] = root_event_id
|
|
197
|
+
|
|
198
|
+
result = call_memory_agent("timeline_log", log_params)
|
|
199
|
+
|
|
200
|
+
# Store the decision event ID so PostToolUse can link to it
|
|
201
|
+
if result:
|
|
202
|
+
artifact_text = safe_get(result, "result", "artifacts", 0, "parts", 0, "text")
|
|
203
|
+
if artifact_text:
|
|
204
|
+
try:
|
|
205
|
+
skill_result = json.loads(artifact_text)
|
|
206
|
+
decision_event_id = skill_result.get("event_id")
|
|
207
|
+
if decision_event_id:
|
|
208
|
+
session_data["current_decision_id"] = decision_event_id
|
|
209
|
+
session_data["pending_tool"] = tool_name
|
|
210
|
+
save_session_data(session_data)
|
|
211
|
+
except json.JSONDecodeError:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
sys.exit(0)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
main()
|