Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bda38c5
feat(hooks): self-management + planning retrieval pipeline
alfadb Apr 11, 2026
4293356
feat(auto-sediment): per-turn Stop hook + .pensieve/config.json toggle
alfadb Apr 11, 2026
d185994
docs(extensions): auto-sediment + 规划前检索 + hook 自管理 设计说明
alfadb Apr 11, 2026
9364d8f
feat(auto-sediment): Filter 4 pending-question detection heuristic
alfadb Apr 12, 2026
82ace56
feat(auto-sediment): global filter-samples.jsonl for heuristic tuning
alfadb Apr 12, 2026
8e3dda5
feat(auto-sediment): 样本记录策略修正 — 只记 Filter 4 passed 不做 decision 分类
alfadb Apr 12, 2026
5c4c598
fix: planning-prehook 补全 PreToolUse hook 输出 schema 字段
alfadb Apr 12, 2026
bf20a4a
fix: Filter 2 加 session 隔离,孤儿 loop 不再阻塞 auto-sediment
alfadb Apr 13, 2026
d8a832d
feat(init): add PENSIEVE_HARNESS guard to skip register-hooks.sh
alfadb Apr 29, 2026
f677f9c
feat(pi): add pi adapter (Layer 1+2)
alfadb Apr 29, 2026
597755a
fix(sync): accept short-term/ edits in graph-sync guard
alfadb Apr 30, 2026
c3ea4ec
feat(pi): add Layer 3 auto-sediment + pensieve-wand skill
alfadb Apr 30, 2026
f403893
fix(install): use relative symlink for pensieve-wand
alfadb Apr 30, 2026
ca4bc1a
fix(install): use relative symlinks for extensions
alfadb Apr 30, 2026
e825164
feat(pi/auto-sediment): in-process sidecar mode + four-tier config + …
Apr 30, 2026
7e0018c
feat(pi): show setStatus in footer during auto-sediment sidecar
alfadb Apr 30, 2026
57f29c5
feat: add package.json with pi manifest for extensions and skills dis…
alfadb Apr 30, 2026
e429c01
refactor(pi): install.sh writes settings.json paths instead of symlinks
alfadb Apr 30, 2026
8d07a21
fix: remove emoji from pensieve status bar label
alfadb May 1, 2026
1f220de
fix: use 📖 icon for pensieve status bar
alfadb May 1, 2026
57461a3
refactor(pi): remove pensieve-auto-sediment, rename branch to pi
alfadb May 1, 2026
87e221e
feat(pi): add gbrain cross-reference to pensieve-context navigation card
alfadb May 1, 2026
9b7b130
fix: note gbrain English-only in pensieve-context card
alfadb May 1, 2026
0df6ca4
fix: address PR review — remove stale auto-sediment refs, fix branch …
alfadb May 1, 2026
43fdab2
fix(pensieve-context): clarify main-session must defer writes to sedi…
May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .src/core/hooks.json
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"
}
]
}
]
}
}
2 changes: 1 addition & 1 deletion .src/manifest.json
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"
}
11 changes: 11 additions & 0 deletions .src/scripts/init-project-data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ else
fi
echo " - runtime state: $STATE_ROOT"

# Register Pensieve hooks in settings.json (idempotent).
# Skip when running under a non-Claude-Code harness (e.g. pi, cursor) — those
# integrate via their own extension API and must not touch ~/.claude/settings.json.
# Set PENSIEVE_HARNESS=pi (or any non-empty non-"claude-code" value) to opt out.
REGISTER_HOOKS="$SKILL_ROOT/.src/scripts/register-hooks.sh"
if [[ -f "$REGISTER_HOOKS" ]] && [[ "${PENSIEVE_HARNESS:-claude-code}" == "claude-code" ]]; then
bash "$REGISTER_HOOKS" || echo "⚠️ Hook registration skipped" >&2
elif [[ "${PENSIEVE_HARNESS:-claude-code}" != "claude-code" ]]; then
echo " - hook registration: skipped (PENSIEVE_HARNESS=$PENSIEVE_HARNESS)"
fi

if [[ -f "$PROJECT_STATE_SCRIPT" ]]; then
if ! bash "$PROJECT_STATE_SCRIPT" --event install --note "seeded project data via init-project-data.sh"; then
echo "⚠️ Generated state update skipped: failed to run maintain-project-state.sh" >&2
Expand Down
9 changes: 9 additions & 0 deletions .src/scripts/pensieve-session-marker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ if initialized and self_check_ok:
if _st_due > 0:
ctx += f"\n\n短期记忆待整理:{_st_due} 条已到期(共 {_st_total} 条)。可运行 pensieve refine 完成处理。"

# Inject knowledge graph into session context (replaces CLAUDE.md "会话启动" instruction).
_graph_file = Path(project_root) / ".pensieve" / ".state" / "pensieve-user-data-graph.md"
if _graph_file.is_file():
_graph_text = _graph_file.read_text(encoding="utf-8", errors="replace")
# Truncate very large graphs to avoid context bloat (keep under ~4k chars).
if len(_graph_text) > 4000:
_graph_text = _graph_text[:4000] + "\n... (truncated)"
ctx += f"\n\n## Pensieve Knowledge Graph\n\n{_graph_text}"

payload = {
"hookSpecificOutput": {
"hookEventName": "SessionStart",
Expand Down
93 changes: 93 additions & 0 deletions .src/scripts/planning-prehook.sh
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)

Copilot AI May 1, 2026

Copy link

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 whether timeout exists. On macOS (common for Claude Code), timeout isn’t available by default, which can emit command not found noise on stderr and may interfere with hook execution. Either avoid timeout here (stdin should be finite) or guard with command -v timeout and fall back to plain cat.

Suggested change
HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true)
if command -v timeout >/dev/null 2>&1; then
HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true)
else
HOOK_INPUT=$(cat 2>/dev/null || true)
fi

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

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.

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

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script exits early with no output in multiple cases ([[ "$IS_PLANNING" == "true" ]] || exit 0, missing .pensieve/, empty CTX, etc.). In this repo, explore-prehook.sh documents that Claude Code requires valid JSON output from PreToolUse hooks even on no-op paths; empty stdout can be treated as an error. Please return a minimal allow JSON payload on all no-op/fallback paths (and only add additionalContext when applicable).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

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.


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

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python handoff uses <<< "$(echo -e "$CTX")". echo -e can reinterpret backslash sequences in the pipeline content / titles, which can corrupt the injected context. Prefer a safer transport (e.g., printf '%s' "$CTX" into stdin) that does not perform escape processing.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

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.

112 changes: 112 additions & 0 deletions .src/scripts/register-hooks.sh
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
17 changes: 16 additions & 1 deletion .src/scripts/run-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,19 @@ export PENSIEVE_SKILL_ROOT="$ROOT"
exit 1
}

exec bash "$TARGET" "$@"
# --- Hook trace logging ---
# Append one line per hook invocation to .state/hook-trace.log for diagnostics.
# Format: ISO-timestamp script-name args... → exit-code
_HOOK_LOG_DIR="${PENSIEVE_PROJECT_ROOT:+$PENSIEVE_PROJECT_ROOT/.pensieve/.state}"
_HOOK_LOG="${_HOOK_LOG_DIR:+$_HOOK_LOG_DIR/hook-trace.log}"

if [[ -n "$_HOOK_LOG" && -d "$_HOOK_LOG_DIR" ]]; then
_hook_rc=0
bash "$TARGET" "$@" || _hook_rc=$?
printf '%s %s %s → %d\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SCRIPT_NAME" "$*" "$_hook_rc" \
>> "$_HOOK_LOG" 2>/dev/null
exit "$_hook_rc"
else
exec bash "$TARGET" "$@"
fi
Loading