-
Notifications
You must be signed in to change notification settings - Fork 250
pi: Pensieve pi 适配层(清理 auto-sediment) #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: experimental
Are you sure you want to change the base?
Changes from all commits
bda38c5
4293356
d185994
9364d8f
82ace56
8e3dda5
5c4c598
bf20a4a
d8a832d
f677f9c
597755a
c3ea4ec
f403893
ca4bc1a
e825164
7e0018c
57f29c5
e429c01
8d07a21
1f220de
57461a3
87e221e
9b7b130
0df6ca4
43fdab2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| { | ||
| "_comment": "Pensieve hook declarations. Managed by register-hooks.sh. Do not edit settings.json manually.", | ||
| "identifier_pattern": "run-hook.sh", | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash \"${PENSIEVE_SKILL_ROOT:-$HOME/.claude/skills/pensieve}/.src/scripts/run-hook.sh\" pensieve-session-marker.sh --mode session-start" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "PreToolUse": [ | ||
| { | ||
| "matcher": "Agent", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash \"${PENSIEVE_SKILL_ROOT:-$HOME/.claude/skills/pensieve}/.src/scripts/run-hook.sh\" explore-prehook.sh" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "matcher": "Skill", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash \"${PENSIEVE_SKILL_ROOT:-$HOME/.claude/skills/pensieve}/.src/scripts/run-hook.sh\" planning-prehook.sh" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "matcher": "EnterPlanMode", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash \"${PENSIEVE_SKILL_ROOT:-$HOME/.claude/skills/pensieve}/.src/scripts/run-hook.sh\" planning-prehook.sh" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "PostToolUse": [ | ||
| { | ||
| "matcher": "Write|Edit|MultiEdit", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash \"${PENSIEVE_SKILL_ROOT:-$HOME/.claude/skills/pensieve}/.src/scripts/run-hook.sh\" sync-project-skill-graph.sh" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "Stop": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash \"${PENSIEVE_SKILL_ROOT:-$HOME/.claude/skills/pensieve}/.src/scripts/run-hook.sh\" stop-hook-auto-sediment.sh" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "name": "pensieve", | ||
| "version": "1.2.0", | ||
| "version": "1.5.3", | ||
| "distribution": "shared-skill" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| #!/bin/bash | ||
| # PreToolUse hook for planning-related tools. | ||
| # Triggers on: | ||
| # 1. EnterPlanMode — any project, no gstack needed | ||
| # 2. Skill tool with planning skills (plan-*, autoplan, office-hours) — gstack projects | ||
| # Injects Pensieve knowledge context before planning begins. | ||
| # Gracefully exits for non-planning tools and non-Pensieve projects. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| source "$SCRIPT_DIR/lib.sh" | ||
|
|
||
| # Read hook payload from stdin. | ||
| HOOK_INPUT="" | ||
| if [[ ! -t 0 ]]; then | ||
| HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true) | ||
| fi | ||
|
|
||
| # Determine if this is a planning-related invocation. | ||
| TOOL_NAME="" | ||
| SKILL_NAME="" | ||
| if [[ -n "$HOOK_INPUT" ]] && command -v jq &>/dev/null; then | ||
| TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // ""' 2>/dev/null || true) | ||
| SKILL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_input.skill // ""' 2>/dev/null || true) | ||
| fi | ||
|
|
||
| IS_PLANNING=false | ||
| case "$TOOL_NAME" in | ||
| EnterPlanMode) IS_PLANNING=true ;; | ||
| esac | ||
| case "$SKILL_NAME" in | ||
| plan-*|autoplan|office-hours) IS_PLANNING=true ;; | ||
| esac | ||
|
|
||
| [[ "$IS_PLANNING" == "true" ]] || exit 0 | ||
|
|
||
| # Detect project root and .pensieve/ directory. | ||
| PROJECT_ROOT="$(project_root 2>/dev/null)" || exit 0 | ||
| PENSIEVE_DIR="$PROJECT_ROOT/.pensieve" | ||
| [[ -d "$PENSIEVE_DIR" ]] || exit 0 | ||
|
|
||
| # Read planning pipeline if it exists. | ||
| PLANNING_PIPELINE="$PENSIEVE_DIR/pipelines/run-when-planning.md" | ||
| PIPELINE_CONTENT="" | ||
| if [[ -f "$PLANNING_PIPELINE" ]]; then | ||
| PIPELINE_CONTENT=$(cat "$PLANNING_PIPELINE" 2>/dev/null || true) | ||
| fi | ||
|
|
||
| # Quick grep for decisions with "探索减负" (exploration reduction). | ||
| PRIOR_ART="" | ||
| for dir in "$PENSIEVE_DIR/decisions" "$PENSIEVE_DIR/knowledge" "$PENSIEVE_DIR/maxims"; do | ||
| [[ -d "$dir" ]] || continue | ||
| # Find files with active status | ||
| while IFS= read -r f; do | ||
| [[ -f "$f" ]] || continue | ||
| # Extract first heading and one-line conclusion/summary | ||
| title=$(grep -m1 '^# ' "$f" 2>/dev/null | sed 's/^# //' || true) | ||
| status=$(grep -m1 '^status:' "$f" 2>/dev/null | sed 's/^status:[[:space:]]*//' || true) | ||
| [[ "$status" == "active" ]] || continue | ||
| rel_path="${f#$PENSIEVE_DIR/}" | ||
| PRIOR_ART="${PRIOR_ART}\n- ${rel_path}: ${title}" | ||
| done < <(find "$dir" -name '*.md' -type f 2>/dev/null | LC_ALL=C sort) | ||
| done | ||
|
|
||
| # Build additional context. | ||
| CTX="" | ||
| if [[ -n "$PIPELINE_CONTENT" ]]; then | ||
| CTX="## Planning Pipeline (run-when-planning)\n\n${PIPELINE_CONTENT}" | ||
| fi | ||
| if [[ -n "$PRIOR_ART" ]]; then | ||
| CTX="${CTX}\n\n## Available Pensieve Knowledge\n${PRIOR_ART}" | ||
| fi | ||
|
|
||
| if [[ -z "$CTX" ]]; then | ||
| exit 0 | ||
| fi | ||
|
Comment on lines
+28
to
+77
|
||
|
|
||
| ensure_python_env | ||
| [[ -n "${PYTHON_BIN:-}" ]] || exit 0 | ||
|
|
||
| "$PYTHON_BIN" -c " | ||
| import json, sys | ||
| ctx = sys.stdin.read() | ||
| payload = { | ||
| 'hookSpecificOutput': { | ||
| 'hookEventName': 'PreToolUse', | ||
| 'permissionDecision': 'allow', | ||
| 'additionalContext': ctx, | ||
| }, | ||
| } | ||
| print(json.dumps(payload, ensure_ascii=False)) | ||
| " <<< "$(echo -e "$CTX")" | ||
|
Comment on lines
+82
to
+93
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| #!/bin/bash | ||
| # Register/update Pensieve hooks in ~/.claude/settings.json. | ||
| # Idempotent: safe to run repeatedly. Only touches Pensieve hooks (identified by run-hook.sh pattern). | ||
| # Called by init-project-data.sh and can be run manually. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| source "$SCRIPT_DIR/lib.sh" | ||
|
|
||
| ensure_python_env | ||
| [[ -n "${PYTHON_BIN:-}" ]] || { echo "⚠️ Python not available, hook registration skipped" >&2; exit 0; } | ||
|
|
||
| SKILL_ROOT="$(skill_root_from_script "$SCRIPT_DIR")" | ||
| HOOKS_JSON="$SKILL_ROOT/.src/core/hooks.json" | ||
| SETTINGS_JSON="$HOME/.claude/settings.json" | ||
|
|
||
| [[ -f "$HOOKS_JSON" ]] || { echo "⚠️ hooks.json not found at $HOOKS_JSON" >&2; exit 1; } | ||
|
|
||
| # Ensure settings.json parent directory exists | ||
| mkdir -p "$(dirname "$SETTINGS_JSON")" | ||
|
|
||
| "$PYTHON_BIN" - "$HOOKS_JSON" "$SETTINGS_JSON" <<'PY' | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| hooks_json_path = Path(sys.argv[1]) | ||
| settings_path = Path(sys.argv[2]) | ||
|
|
||
| # Load hook declarations | ||
| hooks_decl = json.loads(hooks_json_path.read_text(encoding="utf-8")) | ||
| identifier = hooks_decl.get("identifier_pattern", "run-hook.sh") | ||
| desired_hooks: dict[str, list] = hooks_decl.get("hooks", {}) | ||
|
|
||
| # Load existing settings | ||
| if settings_path.exists(): | ||
| try: | ||
| settings = json.loads(settings_path.read_text(encoding="utf-8")) | ||
| except (json.JSONDecodeError, OSError): | ||
| settings = {} | ||
| else: | ||
| settings = {} | ||
|
|
||
| if not isinstance(settings, dict): | ||
| settings = {} | ||
|
|
||
| existing_hooks: dict[str, list] = settings.get("hooks", {}) | ||
| if not isinstance(existing_hooks, dict): | ||
| existing_hooks = {} | ||
|
|
||
|
|
||
| def is_pensieve_hook(entry: dict) -> bool: | ||
| """Check if a hook entry belongs to Pensieve.""" | ||
| for hook in entry.get("hooks", []): | ||
| cmd = hook.get("command", "") | ||
| if identifier in cmd: | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| changes: list[str] = [] | ||
|
|
||
| for event_name, desired_entries in desired_hooks.items(): | ||
| current_entries = existing_hooks.get(event_name, []) | ||
| if not isinstance(current_entries, list): | ||
| current_entries = [] | ||
|
|
||
| # Separate non-Pensieve hooks (preserve) from Pensieve hooks (replace) | ||
| non_pensieve = [e for e in current_entries if isinstance(e, dict) and not is_pensieve_hook(e)] | ||
| old_pensieve = [e for e in current_entries if isinstance(e, dict) and is_pensieve_hook(e)] | ||
|
|
||
| # Build new list: non-Pensieve first, then desired Pensieve hooks | ||
| new_entries = non_pensieve + desired_entries | ||
|
|
||
| # Detect changes | ||
| if len(old_pensieve) != len(desired_entries): | ||
| changes.append(f" {event_name}: {len(old_pensieve)} → {len(desired_entries)} Pensieve hook(s)") | ||
| elif json.dumps(old_pensieve, sort_keys=True) != json.dumps(desired_entries, sort_keys=True): | ||
| changes.append(f" {event_name}: updated {len(desired_entries)} Pensieve hook(s)") | ||
|
|
||
| existing_hooks[event_name] = new_entries | ||
|
|
||
| settings["hooks"] = existing_hooks | ||
|
|
||
| # Atomic write | ||
| settings_path.parent.mkdir(parents=True, exist_ok=True) | ||
| payload = json.dumps(settings, ensure_ascii=False, indent=2) + "\n" | ||
| with tempfile.NamedTemporaryFile( | ||
| mode="w", | ||
| encoding="utf-8", | ||
| dir=str(settings_path.parent), | ||
| prefix=settings_path.name + ".", | ||
| suffix=".tmp", | ||
| delete=False, | ||
| ) as tmp: | ||
| tmp.write(payload) | ||
| tmp_path = Path(tmp.name) | ||
| os.replace(tmp_path, settings_path) | ||
|
|
||
| if changes: | ||
| print("✅ Pensieve hooks registered in settings.json:") | ||
| for line in changes: | ||
| print(line) | ||
| else: | ||
| print("✅ Pensieve hooks already up to date in settings.json") | ||
| PY |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hook payload reading uses
timeout 2 cat ...without checking whethertimeoutexists. On macOS (common for Claude Code),timeoutisn’t available by default, which can emitcommand not foundnoise on stderr and may interfere with hook execution. Either avoidtimeouthere (stdin should be finite) or guard withcommand -v timeoutand fall back to plaincat.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are Claude Code hook scripts maintained for CC compatibility. The issues raised are valid but out of scope for this pi-adapter PR — will address in a separate CC-focused PR.