mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 03:13:29 +08:00
feat: project-scoped instinct isolation
* feat: add project-scoped instinct isolation * fix(continuous-learning-v2): harden instinct loading and promotion safety; sync v2.1 command docs * fix(ci): make copilot-setup-steps a valid GitHub Actions workflow * fix(hooks): stabilize docs warning inline JS regex parsing
This commit is contained in:
@@ -4,52 +4,20 @@
|
||||
# Captures tool use events for pattern analysis.
|
||||
# Claude Code passes hook data via stdin as JSON.
|
||||
#
|
||||
# Hook config (in ~/.claude/settings.json):
|
||||
# v2.1: Project-scoped observations — detects current project context
|
||||
# and writes observations to project-specific directory.
|
||||
#
|
||||
# If installed as a plugin, use ${CLAUDE_PLUGIN_ROOT}:
|
||||
# {
|
||||
# "hooks": {
|
||||
# "PreToolUse": [{
|
||||
# "matcher": "*",
|
||||
# "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh pre" }]
|
||||
# }],
|
||||
# "PostToolUse": [{
|
||||
# "matcher": "*",
|
||||
# "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh post" }]
|
||||
# }]
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# If installed manually to ~/.claude/skills:
|
||||
# {
|
||||
# "hooks": {
|
||||
# "PreToolUse": [{
|
||||
# "matcher": "*",
|
||||
# "hooks": [{ "type": "command", "command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh pre" }]
|
||||
# }],
|
||||
# "PostToolUse": [{
|
||||
# "matcher": "*",
|
||||
# "hooks": [{ "type": "command", "command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh post" }]
|
||||
# }]
|
||||
# }
|
||||
# }
|
||||
# Registered via plugin hooks/hooks.json (auto-loaded when plugin is enabled).
|
||||
# Can also be registered manually in ~/.claude/settings.json.
|
||||
|
||||
set -e
|
||||
|
||||
# Hook phase from CLI argument: "pre" (PreToolUse) or "post" (PostToolUse)
|
||||
HOOK_PHASE="${1:-post}"
|
||||
|
||||
CONFIG_DIR="${HOME}/.claude/homunculus"
|
||||
OBSERVATIONS_FILE="${CONFIG_DIR}/observations.jsonl"
|
||||
MAX_FILE_SIZE_MB=10
|
||||
|
||||
# Ensure directory exists
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# Skip if disabled
|
||||
if [ -f "$CONFIG_DIR/disabled" ]; then
|
||||
exit 0
|
||||
fi
|
||||
# ─────────────────────────────────────────────
|
||||
# Read stdin first (before project detection)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
# Read JSON from stdin (Claude Code hook format)
|
||||
INPUT_JSON=$(cat)
|
||||
@@ -59,6 +27,51 @@ if [ -z "$INPUT_JSON" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Extract cwd from stdin for project detection
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
# Extract cwd from the hook JSON to use for project detection.
|
||||
# This avoids spawning a separate git subprocess when cwd is available.
|
||||
STDIN_CWD=$(echo "$INPUT_JSON" | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
cwd = data.get("cwd", "")
|
||||
print(cwd)
|
||||
except(KeyError, TypeError, ValueError):
|
||||
print("")
|
||||
' 2>/dev/null || echo "")
|
||||
|
||||
# If cwd was provided in stdin, use it for project detection
|
||||
if [ -n "$STDIN_CWD" ] && [ -d "$STDIN_CWD" ]; then
|
||||
export CLAUDE_PROJECT_DIR="$STDIN_CWD"
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Project detection
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Source shared project detection helper
|
||||
# This sets: PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
||||
source "${SKILL_ROOT}/scripts/detect-project.sh"
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Configuration
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
CONFIG_DIR="${HOME}/.claude/homunculus"
|
||||
OBSERVATIONS_FILE="${PROJECT_DIR}/observations.jsonl"
|
||||
MAX_FILE_SIZE_MB=10
|
||||
|
||||
# Skip if disabled
|
||||
if [ -f "$CONFIG_DIR/disabled" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse using python via stdin pipe (safe for all JSON payloads)
|
||||
# Pass HOOK_PHASE via env var since Claude Code does not include hook type in stdin JSON
|
||||
PARSED=$(echo "$INPUT_JSON" | HOOK_PHASE="$HOOK_PHASE" python3 -c '
|
||||
@@ -80,6 +93,8 @@ try:
|
||||
tool_input = data.get("tool_input", data.get("input", {}))
|
||||
tool_output = data.get("tool_output", data.get("output", ""))
|
||||
session_id = data.get("session_id", "unknown")
|
||||
tool_use_id = data.get("tool_use_id", "")
|
||||
cwd = data.get("cwd", "")
|
||||
|
||||
# Truncate large inputs/outputs
|
||||
if isinstance(tool_input, dict):
|
||||
@@ -88,24 +103,26 @@ try:
|
||||
tool_input_str = str(tool_input)[:5000]
|
||||
|
||||
if isinstance(tool_output, dict):
|
||||
tool_output_str = json.dumps(tool_output)[:5000]
|
||||
tool_response_str = json.dumps(tool_output)[:5000]
|
||||
else:
|
||||
tool_output_str = str(tool_output)[:5000]
|
||||
tool_response_str = str(tool_output)[:5000]
|
||||
|
||||
print(json.dumps({
|
||||
"parsed": True,
|
||||
"event": event,
|
||||
"tool": tool_name,
|
||||
"input": tool_input_str if event == "tool_start" else None,
|
||||
"output": tool_output_str if event == "tool_complete" else None,
|
||||
"session": session_id
|
||||
"output": tool_response_str if event == "tool_complete" else None,
|
||||
"session": session_id,
|
||||
"tool_use_id": tool_use_id,
|
||||
"cwd": cwd
|
||||
}))
|
||||
except Exception as e:
|
||||
print(json.dumps({"parsed": False, "error": str(e)}))
|
||||
')
|
||||
|
||||
# Check if parsing succeeded
|
||||
PARSED_OK=$(echo "$PARSED" | python3 -c "import json,sys; print(json.load(sys.stdin).get('parsed', False))")
|
||||
PARSED_OK=$(echo "$PARSED" | python3 -c "import json,sys; print(json.load(sys.stdin).get('parsed', False))" 2>/dev/null || echo "False")
|
||||
|
||||
if [ "$PARSED_OK" != "True" ]; then
|
||||
# Fallback: log raw input for debugging
|
||||
@@ -119,20 +136,23 @@ print(json.dumps({'timestamp': os.environ['TIMESTAMP'], 'event': 'parse_error',
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Archive if file too large
|
||||
# Archive if file too large (atomic: rename with unique suffix to avoid race)
|
||||
if [ -f "$OBSERVATIONS_FILE" ]; then
|
||||
file_size_mb=$(du -m "$OBSERVATIONS_FILE" 2>/dev/null | cut -f1)
|
||||
if [ "${file_size_mb:-0}" -ge "$MAX_FILE_SIZE_MB" ]; then
|
||||
archive_dir="${CONFIG_DIR}/observations.archive"
|
||||
archive_dir="${PROJECT_DIR}/observations.archive"
|
||||
mkdir -p "$archive_dir"
|
||||
mv "$OBSERVATIONS_FILE" "$archive_dir/observations-$(date +%Y%m%d-%H%M%S).jsonl"
|
||||
mv "$OBSERVATIONS_FILE" "$archive_dir/observations-$(date +%Y%m%d-%H%M%S)-$$.jsonl" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build and write observation
|
||||
# Build and write observation (now includes project context)
|
||||
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
export PROJECT_ID_ENV="$PROJECT_ID"
|
||||
export PROJECT_NAME_ENV="$PROJECT_NAME"
|
||||
export TIMESTAMP="$timestamp"
|
||||
|
||||
echo "$PARSED" | python3 -c "
|
||||
import json, sys, os
|
||||
|
||||
@@ -141,10 +161,12 @@ observation = {
|
||||
'timestamp': os.environ['TIMESTAMP'],
|
||||
'event': parsed['event'],
|
||||
'tool': parsed['tool'],
|
||||
'session': parsed['session']
|
||||
'session': parsed['session'],
|
||||
'project_id': os.environ.get('PROJECT_ID_ENV', 'global'),
|
||||
'project_name': os.environ.get('PROJECT_NAME_ENV', 'global')
|
||||
}
|
||||
|
||||
if parsed['input'] is not None:
|
||||
if parsed['input']:
|
||||
observation['input'] = parsed['input']
|
||||
if parsed['output'] is not None:
|
||||
observation['output'] = parsed['output']
|
||||
@@ -152,13 +174,14 @@ if parsed['output'] is not None:
|
||||
print(json.dumps(observation))
|
||||
" >> "$OBSERVATIONS_FILE"
|
||||
|
||||
# Signal observer if running
|
||||
OBSERVER_PID_FILE="${CONFIG_DIR}/.observer.pid"
|
||||
if [ -f "$OBSERVER_PID_FILE" ]; then
|
||||
observer_pid=$(cat "$OBSERVER_PID_FILE")
|
||||
if kill -0 "$observer_pid" 2>/dev/null; then
|
||||
kill -USR1 "$observer_pid" 2>/dev/null || true
|
||||
# Signal observer if running (check both project-scoped and global observer)
|
||||
for pid_file in "${PROJECT_DIR}/.observer.pid" "${CONFIG_DIR}/.observer.pid"; do
|
||||
if [ -f "$pid_file" ]; then
|
||||
observer_pid=$(cat "$pid_file")
|
||||
if kill -0 "$observer_pid" 2>/dev/null; then
|
||||
kill -USR1 "$observer_pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user