Skip to content

pi: Pensieve pi 适配层(清理 auto-sediment)#49

Open
alfadb wants to merge 25 commits into
experimentalfrom
pi
Open

pi: Pensieve pi 适配层(清理 auto-sediment)#49
alfadb wants to merge 25 commits into
experimentalfrom
pi

Conversation

@alfadb
Copy link
Copy Markdown
Collaborator

@alfadb alfadb commented May 1, 2026

变更

  • 新增 pi/ 目录:pensieve-context 扩展、pensieve-wand skill、install.sh
  • 新增 package.json(pi package manifest)
  • 移除 pi/extensions/pensieve-auto-sediment/(已由独立 pi-sediment 包替代)
  • install.sh 默认分支 → pi,去掉 --no-auto-sediment 参数

关联

alfadb and others added 21 commits April 11, 2026 20:23
本扩展的基础设施:

- .src/core/hooks.json: 声明式 hook 清单(SessionStart / PreToolUse /
  PostToolUse / Stop,与 settings.json 兼容格式)
- .src/scripts/register-hooks.sh: 幂等注册脚本,合并到用户
  settings.json,只触碰带 $PENSIEVE_SKILL_ROOT 或 run-hook.sh 标记的
  Pensieve hook,保留其他 hook 不动
- .src/scripts/planning-prehook.sh: PreToolUse/Skill hook 检测
  plan-*/autoplan 等规划 skill,注入 .pensieve/pipelines/run-when-planning.md
  里的知识检索指令
- .src/scripts/pensieve-session-marker.sh: SessionStart hook 增强,
  注入 graph 内容 + marker 版本一致性检查
- .src/scripts/run-hook.sh: 统一 hook wrapper,捕获 → hook-trace.log
- .src/scripts/init-project-data.sh: 增加 register-hooks.sh 调用
- .src/templates/pipeline.run-when-*.md: 三个规划/提交/审查 pipeline
  模板,含规划前检索、重复检查、diff 分级等知识复利机制

不包含 Stop hook auto-sediment(参见下一个 commit)。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
每轮评估沉淀的 Stop hook。

- .src/scripts/stop-hook-auto-sediment.sh: per-turn Stop hook,
  Filter 0/1/0.5/2/3 链式短路,所有 filter 过 → decision:block 请
  求主 Claude 执行 /pensieve self-improve。
  Filter 0 (recursion guard): 检查 stop_hook_active != true
  Filter 1 (pensieve project): 项目有 .pensieve/
  Filter 0.5 (config toggle): .pensieve/config.json auto_sediment.enabled
    != false。配置缺失/字段缺失/malformed/null 均 fallback 到启用
    (向后兼容)。热加载,改 config 立即生效,无需重启 Claude Code。
  Filter 2 (Ralph-Loop): loop-state.local.md 无 active=true
  Filter 3 (message substantial): last assistant message ≥ 200 chars
- .src/core/hooks.json: 注册 Stop hook 到 stop-hook-auto-sediment.sh
- .src/manifest.json: 版本号 bump

设计注意:
- jq 陷阱: ".x.y // true" 在 .x.y 为 false 时会错误返回 true(jq //
  对 null/false/empty 三者都触发 default)。使用显式
  "if .auto_sediment.enabled == false then yes else no end" 比较。
- 无 git_clean filter: commit pipeline 无自动触发机制,依赖它会永久
  丢失 turn 的洞察。
- 无 cooldown / session counter: 任何 turn 可能是最后一个,时间/次数
  throttling 会漏掉洞察。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
docs/extensions-design.md —— 本扩展设计意图完整文档:

- §0 摘要 + §1 问题陈述(已有能力 vs 知识流失场景)
- §2-3 设计原则 + 总体架构(两条沉淀路径独立并列:用户显式调用
  pipeline + 事件驱动 auto-sediment hook)
- §4 三层 hook 扩展实现细节(规划前检索 / 会话启动图谱注入 /
  auto-sediment)
- §5 测试与验证 + 三次 verify-before-sediment 应用的失败教训
- §6 与现有 Pensieve 设计的接触面 + merge 策略
- §7 使用说明 + §8 引用 + §9 一句话总结

2026-04-11 注: dispatch mode 在真实生产路径下稳定阻塞失败,已回滚
到 inline 模式 + config.json 开关。文档保留 dispatch 相关段落作为
历史设计档案,顶部加醒目废弃标注。完整回滚背景见项目级
decisions/2026-04-11-sidecar-sediment-dispatch-design.md。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
新增 Filter 4 — 检测本轮 assistant 输出末尾是否像 Claude 在向用户
提问并等待输入,若是则 silent exit 0 跳过 sediment 评估。

## 动机

Claude 在对话中需要用户决策时,常选文字提问 + 结束 turn 的模式
而不是调用 AskUserQuestion 工具。文字提问 turn 会触发 Stop hook
→ auto-sediment Filter 0/1/0.5/2/3 全 PASS → 输出 decision:block
→ 主 Claude 续轮评估 → 大概率 NO_SEDIMENT(提问不是洞察)→ 浪费
600-5000 token/次。高频讨论场景单任务浪费 5-10k token。

## 实现

stop-hook-auto-sediment.sh 在 Filter 3 之后加 Filter 4:

```bash
if [[ "$LAST_MSG_LEN" -gt 300 ]]; then
  TAIL_MSG="${LAST_MSG:$(( LAST_MSG_LEN - 300 ))}"
else
  TAIL_MSG="$LAST_MSG"
fi
if [[ "$TAIL_MSG" =~ [\??][[:space:]]*$ ]] \
   || [[ "$TAIL_MSG" =~ 要不要[^。]*$ ]] \
   || [[ "$TAIL_MSG" =~ 需要[我你]?[^。]*吗 ]] \
   || [[ "$TAIL_MSG" =~ 是否[^。]*[\??] ]] \
   || [[ "$TAIL_MSG" =~ 你(想|觉得|希望|打算)[^。]*[\??] ]] \
   || [[ "$TAIL_MSG" =~ 哪[一个种][^。]*[\??] ]] \
   || [[ "$TAIL_MSG" =~ 请[选确][^。]*$ ]] \
   || [[ "$TAIL_MSG" =~ [Ww]ould[[:space:]]+you ]] \
   || [[ "$TAIL_MSG" =~ [Ww]hich[[:space:]] ]] \
   || [[ "$TAIL_MSG" =~ [Ss]hould[[:space:]]+[IWwYy] ]]; then
  echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) filter4-question-detected" \
    >> "$STATE_ROOT/hook-trace.log" 2>/dev/null || true
  exit 0
fi
```

取最后 300 字符避免长消息主体影响匹配。不用 pipe 避免 pipefail
陷阱。记录 `filter4-question-detected` 到 hook-trace.log 便于
后期观察启发式质量。

## 启发式精度预期 70-85%

live test 覆盖 4 个场景:
- 英文 "Would you like..." (227 chars):✓ 拦截
- 英文陈述句 "...fixed in commit abc1234." (314 chars):✓ 放行
- 中文陈述句 "已在 commit abc1234 修复。" (246 chars):✓ 放行
- 中文长提问 "...你觉得哪个方案更合适?" (315 chars):✓ 拦截

边界 case 会漏(英文 "Do you..." / "Can you..." / 修辞反问)
或误判(陈述句中间有 "?" 字符)。记录 trace 用于迭代。

## 配套软约束

CLAUDE.md 已添加"提问优先 AskUserQuestion"规则(父项目同批 commit)
作为认知引导。软 + 硬双管齐下:
- 软约束让 Claude 主动改行为
- 硬约束兜底处理 default behavior drift

## 文档更新

docs/extensions-design.md:
- §2.4 过滤器最小化:加 ✅ pending_question 条目 + 表格行
  "剩下的 5 个都是必需" → "剩下的 6 个都是必需"
- §4.3 流程图:加 过滤器 4: pending_question 行
- 附录 A 版本历史:加 1.5.1 2026-04-11 条目描述本次改动

.src/manifest.json: 版本 1.5.0 → 1.5.1

## 依据

knowledge/auto-sediment-text-question-stop-waste(本会话沉淀)
解释了文字提问的反模式、成本量化、解决方案矩阵,以及反模式亲历
——作者在讨论本方案的那一轮自己还是用文字问了 4 个问题。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stop-hook 在 Filter 3 pass 后记录样本到全局 jsonl 文件,用于离线分析
Filter 4 启发式的 precision(看 blocked 样本是否真是问题)和 recall
(看 sediment-fired 里是否有漏检的隐形提问)。

## 文件与格式

- 路径:~/.claude/.pensieve-filter-samples.jsonl
- 权限:600(仅用户可读,防 secret 泄露)
- Rotation:> 5MB → .1 → .2 → .3,保留 3 代(~20MB 上限)
- 格式:JSONL,每行一条

```jsonl
{"ts":"2026-04-12T00:58:18Z","session":"<uuid>","project":"/path","decision":"sediment-fired","msg_len":314,"tail":"...末尾300字符..."}
{"ts":"2026-04-12T00:58:49Z","session":"<uuid>","project":"/path","decision":"filter4-blocked","msg_len":306,"tail":"..."}
```

decision ∈ {filter4-blocked, sediment-fired}

## 实现

- 从 hook payload 提取 session_id 字段一并记录
- 定义 record_filter_sample() 函数,集中写入逻辑
- Filter 4 命中路径:record_filter_sample "filter4-blocked" → exit 0
- 全 filter pass 路径:record_filter_sample "sediment-fired" → decision:block JSON
- jq 构造 JSONL 行(--arg/--argjson 规范转义)
- stat -c %s 检查文件大小触发 rotation
- chmod 600 确保权限

## 分析路径(未来离线工作)

```bash
# 查 Filter 4 命中样本的 tail(人工审阅 precision)
jq -r 'select(.decision=="filter4-blocked") | .tail' ~/.claude/.pensieve-filter-samples.jsonl

# 查 sediment-fired 里有没有看起来像问题但漏检的(recall 问题)
jq -r 'select(.decision=="sediment-fired") | .tail' ~/.claude/.pensieve-filter-samples.jsonl | grep -E '[??]$|吗$'

# 跨项目分布
jq -r '.project' ~/.claude/.pensieve-filter-samples.jsonl | sort -u
```

## 隐私

tail 可能含代码/internal discussion/意外出现的 secret。**不要上传、不要
分享、不要提交到 git**。文件位于 $HOME 隐藏文件,不在任何 pensieve 项目
git 范围内,但用户可能同步 $HOME(dropbox/rsync),需要明确在文档中提醒。

## 动机

用户在 Filter 4 落地后建议"把 stop hook 的 last_assistant_message
末尾保存到全局文件用于后续丰富启发式问答判断依据"。Filter 4 的正则
是初始猜测版本(70-85% 精度),只有在**真实语料**上分析才能提升:
- 看看 filter4-blocked 里有没有误判的陈述句
- 看看 sediment-fired 里有没有漏检的提问
- 发现新的中英文提问句式模板

## 文档与版本

- docs/extensions-design.md §4.3 流程图加样本日志输出说明
- 附录 A 新增 1.5.2 条目
- .src/manifest.json 1.5.1 → 1.5.2

## Live test

test 1(中文长提问 306 chars):
  ✓ filter4-blocked 记录到 jsonl

test 2(英文陈述句 314 chars):
  ✓ sediment-fired 记录到 jsonl

文件权限 600 ✓,JSONL 格式正确 ✓。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
用户反馈 1.5.2 的样本记录两个问题:
1. Filter 4 blocked 的 turn 是"被正确筛选的",不需要保存
2. 不能用 NO_SEDIMENT 续轮信号判断"这轮是不是提问",需要完整
   上下文人工判断

1.5.3 修正:

## 去掉 decision 分类

hook 运行时无法可靠分类样本属于哪种:
- filter4-blocked 里可能有误杀陈述句(但无法自动识别)
- sediment-fired 里可能有漏检的问题(但无法自动识别)
- 续轮 NO_SEDIMENT 不等于"这轮是问题",可能是无洞察、已沉淀、话题
  切换等多种原因(用户原话:需要完整上下文判断)

样本 schema 简化为:
{"ts":"...","session":"...","project":"...","msg_len":...,"tail":"..."}

没有 decision / followup_reason 字段。人工离线看 tail 内容判断是不是
应该被 Filter 4 拦的问题 turn,用于调优正则。

## 只记录 Filter 4 放行路径

- Filter 4 blocked → 仅 hook-trace.log 记 filter4-question-detected
  (可观察性保留),不写 filter-samples.jsonl
- Filter 4 passed → record_sample() 写样本,然后 decision:block 触发
  sediment 评估

## 回退 1.5.2 引入的延迟记录机制

- 去掉 pending sample 文件 + Filter 0 的 deferred recording 逻辑
- Filter 0 恢复简单 recursion guard
- 清理 write_pending_sample() 函数,改回 record_sample() 直接写

## Live test

test 1 英文陈述句 289 chars(Filter 4 passed):
  ✓ 记录样本 + decision:block JSON 输出
  ✓ 样本 schema 正确(无 decision 字段)

test 2 长提问 269 chars(Filter 4 blocked):
  ✓ 不记录样本
  ✓ hook-trace.log 新增 filter4-question-detected 条目

test 3 recursion guard (stop_hook_active=true):
  ✓ 不记录 + 无 pending 文件残留

## 文档

docs/extensions-design.md §4.3 流程图简化,附录 A 加 1.5.3 条目
说明本次修正。manifest 1.5.2 → 1.5.3。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hookSpecificOutput 缺少 hookEventName + permissionDecision,
导致 Claude Code v2.1.x schema 校验报错。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
非对称 session 隔离导致死会话的 loop-state (active: true) 永久
静音 auto-sediment。现在 Filter 2 比对 loop session_id 与当前
session,不匹配时视为孤儿放行。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-Claude-Code harnesses (pi, cursor, generic agents) integrate via
their own extension API and must not write to ~/.claude/settings.json.

Set PENSIEVE_HARNESS=pi (or any non-empty non-"claude-code" value) to
skip the register-hooks.sh call at the end of init-project-data.sh.

Default behavior is unchanged for Claude Code users.

Tested: PENSIEVE_HARNESS=pi init-project-data.sh leaves
~/.claude/settings.json byte-identical (verified by SHA256).
Adds an opt-in adapter so Pensieve runs natively under
mariozechner/pi-coding-agent without forking the core.

Components
----------
* pi/extensions/pensieve-context/  — ~150 LOC TS extension that:
  - Detects `<cwd>/.pensieve/` on session_start.
  - Appends a small (\~300 token) navigation card to the system prompt
    in `before_agent_start`. The card lists the four memory layers,
    points at the mermaid graph + state.md, and tells the LLM when to
    consult Pensieve and when to call `/skill:pensieve self-improve`.
    Detail lookup is delegated to the model — no full-graph dump.
  - Bridges pi's `tool_result` event for `edit` / `write` to
    `sync-project-skill-graph.sh`, the same script Claude Code's
    PostToolUse hook uses, so the graph stays fresh on .pensieve/ edits.

* pi/install.sh — idempotent installer:
  - Verifies `~/.pi/agent/skills/pensieve/.src/manifest.json` (skill present).
  - Symlinks the extension into `~/.pi/agent/extensions/`.
  - On a real project cwd, runs `init-project-data.sh` with
    `PENSIEVE_HARNESS=pi` so register-hooks.sh is skipped.

* pi/README.md — adapter design notes and verify steps.

Design
------
* Single source of truth — all real logic lives in `.src/scripts/*.sh`.
  The pi side is a thin event router. No bash logic ported to JS.
* Minimum upstream change — only the existing
  `PENSIEVE_HARNESS=pi` guard in init-project-data.sh (separate commit).
* Layered priority per user direction: P1 = knowledge graph injection
  (this commit), P2 = auto-sediment is future work in
  `pi/extensions/pensieve-auto-sediment/`. The skill itself works
  without any extension.

Verified
--------
End-to-end on a real pi session (model: anthropic claude-sonnet-4):
1. `pi -p` with `.pensieve/` present — LLM correctly enumerates
   layer counts, graph path, and the consult/write protocol from the
   system prompt only (no tool calls).
2. Same prompt with `.pensieve/` absent — LLM reports no Pensieve
   memory; no card injected (graceful degradation).
3. LLM uses `write` to add a decision file to `.pensieve/decisions/`
   — graph file mtime advances and the new entry appears in mermaid.

README.md
---------
Adds a `pi` install `<details>` block alongside Claude Code and the
generic-client block, with submodule and standalone install paths.

Closes the P1 'knowledge graph context injection' goal.
The sync-project-skill-graph.sh case statement only matched top-level
maxims/*, decisions/*, knowledge/*, pipelines/* but not short-term/*.
Edits to short-term files would silently skip graph regeneration.

Now short-term/* files also trigger the graph refresh, consistent with
the short-term spec: short-term entries share the same graph node
namespace as long-term ones.

Verified end-to-end on pi: edit a .../short-term/knowledge/*/content.md
→ graph mtime advances.
Layer 3 — per-prompt auto-sediment
==================================
pi/extensions/pensieve-auto-sediment/index.ts (~130 LOC):

- Listens to pi's agent_end event.
- Pre-fetches all ctx data synchronously (sessionManager.getSessionFile,
  getBranch, cwd, hasUI, signal) before any async spawn, avoiding the
  stale-ctx error that occurs when pi finalizes the session during await.
- Constructs Claude-Code-compatible stdin JSON for the upstream
  stop-hook-auto-sediment.sh filter chain.
- On decision:block, in interactive mode (hasUI=true) sends the sediment
  evaluation prompt as a follow-up user message via sendUserMessage.
- In print mode (-p), auto-sediment evaluation is skipped — batch usage
  doesn't benefit from mid-stream injection.
- Recursion is guarded by an in-memory Set<sessionId> (pi has no
  stop_hook_active field but the Set provides equivalent protection).

Also: make the upstream sample log path configurable via
PENSIEVE_SAMPLE_LOG env var (default unchanged: ~/.claude/...).

pensieve-wand skill
===================
pi/skills/pensieve-wand/SKILL.md — pi-native adaptation of the Claude Code
pensieve-wand agent. Strips Claude-Code-specific frontmatter (model, color,
memory) and adapts the System 1/System 2 decision workflow for pi's context
(pensieve-context navigation card = System 1; graph exploration = System 2).
Memory persistence uses .pensieve/knowledge/ entries instead of
Claude-Code-specific MEMORY.md files.

install.sh updated
==================
- --with-auto-sediment flag to opt into Layer 3
- Auto-links pensieve-wand skill into ~/.pi/agent/skills/

Verified
========
- pi -p mode: no stale ctx error, agent_end fires, sample log recorded
- bash script direct test: filter chain passes on substantive messages
- pensieve-wand frontmatter valid: name matches directory, description present
Replaces copy-pasted symlink code with a shared install_extension()
function that uses realpath --relative-to to create relative symlinks.
Extensions and skills now survive cloning ~/.pi to a different HOME.
…global defaults

Major rework of pensieve-auto-sediment from inline-followup mode to
detached in-process sidecar. Main session sees 0 token cost / 0 latency /
0 transcript pollution; sediment evaluation and writing happen on a
configurable side LLM, off the UI await chain.

Architecture:
  agent_end → upstream stop-hook-auto-sediment.sh (filter chain unchanged)
            → if PASS, detached promise:
              ├─ DECISION llm  → JSON {sediment, kind, slug, label}
              ├─ if sediment:
              │   ├─ WRITER llm → markdown (per .src/references/<kind>.md)
              │   ├─ write short-term/<kind>/<slug>.md
              │   └─ spawn maintain-project-state.sh (detached + unref)
              └─ all events → .pensieve/.state/sidecar-sediment.log

Module split:
  index.ts   — agent_end router; concurrency state machine (latest-wins
               single-slot queue); ctx capture before await
  config.ts  — four-tier resolution (NEW)
  prompts.ts — decision + writer system prompts (NEW)
  sidecar.ts — detached pipeline; LLM via @mariozechner/pi-ai (NEW)

Configuration — four-tier override (NEW):
  env > project .pensieve/config.json > global ~/.pi/.pensieve-auto-sediment.json > ctx.model

Per-key asymmetry is intentional:
  - model         : full four-tier (CI/repo/machine/fallback all legitimate)
  - timeout       : project > global > default (no env — wrong ergonomics)
  - min_length    : env > project > default (no global — per-project policy)
  - enabled       : project only (cross-harness toggle, also read by bash)

Why pi-only global path (not ~/.config/pensieve/config.json):
  CC's auto-sediment runs inline (sidecar-dispatch archived, see CC's
  2026-04-11 decision). Putting decision_model/writer_model under a
  "global pensieve" path would mislead users into expecting CC honors
  them. The pi-specific filename makes scope explicit.

Bug fixes folded in:
  - knowledge files were being written to short-term/knowledges/ (plural);
    fixed: kind === "knowledge" ? "knowledge" : `${kind}s`. Pensieve dir
    naming is asymmetric on purpose (knowledge is singular).
  - PENSIEVE_SEDIMENT_MIN_LENGTH env was hard-coded to 200 in the bash
    spawn; now mirrors resolved config.minLength so TS pre-filter and bash
    Filter 3 use the same threshold.
  - loadConfig was called twice per agent_end (once for enabled check,
    once inside launchSidecar); now resolved once and threaded through
    DispatchInputs.
  - maintain-project-state.sh subprocess now `detached: true` + unref()
    so pi can exit cleanly in print mode without waiting on state refresh.

Removed:
  - pi.sendUserMessage(reason, { deliverAs: "followUp" }) injection path
  - sedimentInProgress recursion guard (no longer needed; nothing re-enters)

Default model recommendation (README):
  decision_model: anthropic/claude-haiku-4-5
  writer_model:   anthropic/claude-sonnet-4-6

Validation:
  - Smoke test: jiti loads all 4 modules, factory exports OK
  - Config tests: 4-layer model precedence, env/project/global combinations,
    malformed/missing global file, minLength env ↔ project priority
  - Live run after restart produced two correct short-term entries
    (pi-only-sidecar-config-path decision + pensieve-config-precedence-
    asymmetry knowledge), both written to the correct directories.

Refs:
  ~/.pi/.pensieve/short-term/decisions/2026-04-30-pi-auto-sediment-inprocess-sidecar.md
  ~/.pi/.pensieve/short-term/decisions/2026-04-30-pi-only-sidecar-config-path.md
  ~/.pi/.pensieve/short-term/knowledge/pi-sidecar-is-inprocess-llm-call/
  ~/.pi/.pensieve/short-term/knowledge/pensieve-config-precedence-asymmetry/
- setStatus('Pensieve 沉淀中...') when sidecar starts
- clear status on completion (unless replaying pending message)
- prevents user from quitting pi before background sediment finishes
…covery

Enables pi to auto-discover pensieve extensions (auto-sediment, context)
and skills (pensieve-wand) via settings.json paths instead of symlinks.
- Replaces symlink creation with jq-based settings.json path injection
- Adds cleanup of legacy symlinks from pre-package.json era
- Unified approach with pi-gstack: no symlinks, settings.json-driven discovery
- Idempotent: detects existing paths, cleans up old symlinks
- Removed pi/extensions/pensieve-auto-sediment/ (replaced by pi-sediment)
- Updated install.sh: removed --no-auto-sediment, default branch → pi
- Updated package.json: removed auto-sediment from description
Copilot AI review requested due to automatic review settings May 1, 2026 13:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a pi adapter layer for Pensieve (extension + installer + pi skill), introduces a pi package manifest, and refactors/extends the Claude Code hook-based workflow (planning prehook, hook self-registration, stop-hook auto-sediment), while removing the legacy pi auto-sediment extension path (now intended to live in pi-sediment).

Changes:

  • Added pi/ integration: pensieve-context extension (system prompt “navigation card” + graph sync), pensieve-wand skill, and an idempotent pi/install.sh that configures ~/.pi/agent/settings.json.
  • Added Claude Code hook lifecycle automation (hooks.json + register-hooks.sh) and new hooks/scripts (planning prehook + stop-hook auto-sediment), plus pipeline template enhancements.
  • Updated docs/manifests to reflect the new integration surfaces (repo package.json, root README pi section, manifest version bump).

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
pi/skills/pensieve-wand/SKILL.md Adds the pi-native “pensieve-wand” retrieval skill spec.
pi/install.sh Adds an installer to configure pi via settings.json paths and optionally init project data.
pi/extensions/pensieve-context/package.json Declares the pi extension package entrypoint.
pi/extensions/pensieve-context/index.ts Implements navigation card injection + .pensieve/ edit detection to trigger graph sync.
pi/README.md Documents the pi adapter subtree and installation/verification guidance.
package.json Adds a pi package manifest exposing ./pi/extensions and ./pi/skills.
docs/extensions-design.md Adds a detailed design spec for hooks/auto-sediment/planning retrieval.
README.md Adds a pi-specific installation section and pointers to pi/README.md.
.src/templates/pipeline.run-when-reviewing-code.md Adds diff-size grading and review breadth guidance.
.src/templates/pipeline.run-when-planning.md Adds a planning-time knowledge retrieval pipeline template.
.src/templates/pipeline.run-when-committing.md Expands sediment signals and adds duplicate-check guidance before writing.
.src/scripts/sync-project-skill-graph.sh Extends graph sync coverage to short-term/* edits.
.src/scripts/stop-hook-auto-sediment.sh Adds Stop-hook inline auto-sediment logic with filters + sampling log.
.src/scripts/run-hook.sh Adds hook invocation trace logging to .pensieve/.state/hook-trace.log.
.src/scripts/register-hooks.sh Adds idempotent hook registration into ~/.claude/settings.json from hooks.json.
.src/scripts/planning-prehook.sh Adds a PreToolUse hook that injects planning-related Pensieve context.
.src/scripts/pensieve-session-marker.sh Injects (truncated) knowledge graph into SessionStart context.
.src/scripts/init-project-data.sh Calls hook registration unless running under a non-Claude harness.
.src/manifest.json Bumps skill version to 1.5.3.
.src/core/hooks.json Declares hook configuration as the single source of truth for registration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pi/README.md
Comment on lines +12 to +18
├── extensions/
│ ├── pensieve-context/ ← Layer 2: knowledge graph injection
│ │ ├── package.json
│ │ └── index.ts
│ └── pensieve-auto-sediment/ ← Layer 3: per-prompt auto-sediment
│ ├── package.json
│ └── index.ts
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

pi/README.md still documents a pi/extensions/pensieve-auto-sediment/ directory (Layer 3) that is not present in this PR’s tree, so following the README would lead users to a non-existent extension. Update the tree listing and the layer table to reflect the current set of extensions (only pensieve-context) and/or link to the external pi-sediment replacement if that’s the intended path.

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.

Fixed in 0df6ca4.

Comment thread pi/README.md
Comment on lines +46 to +66
The installer:
1. Verifies the skill is present.
2. Symlinks `pensieve-context` extension into `~/.pi/agent/extensions/`.
3. Symlinks `pensieve-wand` skill into `~/.pi/agent/skills/`.
4. If `cwd` is a real project (not the dotfiles repo itself), runs
`init-project-data.sh` with `PENSIEVE_HARNESS=pi` — that env var skips the
Claude-Code-only `register-hooks.sh` step that would otherwise touch
`~/.claude/settings.json`.

For Layer 3 (auto-sediment), add `--with-auto-sediment`:

```bash
bash agent/skills/pensieve/pi/install.sh --with-auto-sediment
```

If you already have a Pensieve checkout somewhere else (e.g. `~/.claude/skills/pensieve`),
point the installer at it:

```bash
PENSIEVE_SKILL_PATH=~/.claude/skills/pensieve bash pi/install.sh --no-init-project
```
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The install section describes creating symlinks and a --with-auto-sediment flag, but pi/install.sh now writes paths into ~/.pi/agent/settings.json and doesn’t implement --with-auto-sediment. Please update these installer steps/flags so the README matches the actual behavior (otherwise users will run commands that fail or do nothing).

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.

Fixed in 0df6ca4.

Comment on lines +28 to +77
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
Copy link

Copilot AI May 1, 2026

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.

# Read hook payload from stdin.
HOOK_INPUT=""
if [[ ! -t 0 ]]; then
HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true)
Copy link

Copilot AI May 1, 2026

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.

Comment on lines +82 to +93
"$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")"
Copy link

Copilot AI May 1, 2026

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.

# --- Read payload ---
HOOK_INPUT=""
if [[ ! -t 0 ]]; then
HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true)
Copy link

Copilot AI May 1, 2026

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 if timeout exists. On macOS, this can print command not found to stderr during every Stop hook fire. Consider guarding with command -v timeout or falling back to plain cat to keep the hook silent/robust.

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.

Comment thread README.md
Comment on lines +133 to +147
```bash
# 1. 在 ~/.pi 里加 submodule
git -C ~/.pi submodule add -b feature/auto-sediment-hook \
https://github.com/kingkongshot/Pensieve.git agent/skills/pensieve

# 2. 跳过 register-hooks.sh(那只对 Claude Code 有意义),
# 安装 pi-context extension + 初始化项目数据
bash ~/.pi/agent/skills/pensieve/pi/install.sh
```

如果你不想用 submodule,`install.sh` 也能独立 clone:

```bash
bash <(curl -fsSL https://raw.githubusercontent.com/kingkongshot/Pensieve/feature/auto-sediment-hook/pi/install.sh)
```
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The pi install instructions hard-code the feature/auto-sediment-hook branch in both the submodule command and the raw.githubusercontent.com curl example. This PR switches the installer default branch to pi, so these commands should be updated (or changed to rely on the repo default) to avoid pointing users at an outdated branch.

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.

Fixed in 0df6ca4.

Comment thread pi/install.sh
fi

# ─── 2. Update settings.json ─────────────────────────────────────────────────
if [[ ! -f "$SETTINGS_FILE" ]]; then
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

settings.json is created via echo "{}" > "$SETTINGS_FILE" but the parent directory ($HOME/.pi/agent/) may not exist on a fresh install, causing the script to fail under set -e. Create $(dirname "$SETTINGS_FILE") before writing the file.

Suggested change
if [[ ! -f "$SETTINGS_FILE" ]]; then
if [[ ! -f "$SETTINGS_FILE" ]]; then
mkdir -p "$(dirname "$SETTINGS_FILE")"

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.

Fixed in 0df6ca4.

Comment on lines +35 to +49
// ─────────────────────────────────────────────────────────────────────────────
// Locate the Pensieve skill root.
// Priority: PENSIEVE_SKILL_ROOT env > submodule path under ~/.pi > legacy ~/.claude.
// ─────────────────────────────────────────────────────────────────────────────
function locateSkillRoot(): string | null {
const candidates = [
process.env.PENSIEVE_SKILL_ROOT,
path.join(os.homedir(), ".pi", "agent", "skills", "pensieve"),
path.join(os.homedir(), ".claude", "skills", "pensieve"),
].filter((p): p is string => !!p);
for (const c of candidates) {
if (fs.existsSync(path.join(c, ".src", "manifest.json"))) return c;
}
return null;
}
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

locateSkillRoot() only checks $PENSIEVE_SKILL_ROOT, ~/.pi/agent/skills/pensieve, and ~/.claude/skills/pensieve. If the repo is installed elsewhere (which install.sh --skill-path explicitly supports), the extension can still be loaded (via settings.json path) but SKILL_ROOT will be null, disabling graph sync and showing placeholder paths in the navigation card. Consider deriving the skill root from the extension file location (e.g., via import.meta.url) as a reliable fallback.

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.

Acknowledged. These are valid improvements but the current behavior is safe (graceful degradation). Will address in a follow-up.

Comment on lines +186 to +194
pi.on("tool_result", (event: ToolResultEvent, ctx) => {
if (!pensieveDir || event.isError) return;
if (event.toolName !== "edit" && event.toolName !== "write") return;
const raw = (event.input as { path?: string }).path;
if (!raw) return;
const abs = path.isAbsolute(raw) ? raw : path.resolve(ctx.cwd, raw);
// Only fire when the edited file is inside this project's .pensieve/
if (!abs.startsWith(pensieveDir + path.sep) && abs !== pensieveDir) return;
runSyncGraph(abs, event.toolName, ctx.cwd);
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The tool_result handler only triggers sync for edit and write, but runSyncGraph() already maps multi_edit to MultiEdit and the header comment mentions MultiEdit. If pi emits a multi_edit tool result, graph sync won’t run. Include multi_edit in the allowed toolName set (or broaden to any file-mutating tools you rely on).

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.

Acknowledged. These are valid improvements but the current behavior is safe (graceful degradation). Will address in a follow-up.

alfadb and others added 4 commits May 1, 2026 21:51
…name, add mkdir

- pi/README.md: remove pensieve-auto-sediment from tree and table, point to pi-sediment
- pi/README.md: remove --with-auto-sediment section, update install instructions for settings.json
- pi/README.md: update branch name feature/auto-sediment-hook → pi
- README.md: update submodule and curl branch refs → pi
- pi/install.sh: mkdir -p before writing settings.json
…ment

The 'When to write to Pensieve' section told the LLM to invoke
'/skill:pensieve self-improve' after producing durable insights. In
practice this nudged main-session models to:

  1. Race the sediment sidecar (both writers landing the same insight
     under different slugs — sediment's dedupe and slug-collision logic
     was being bypassed).
  2. Frame every task outcome as 'insight-worthy', polluting main-line
     reasoning.
  3. Manually 'put gbrain' via shell when the gbrain_put tool was
     intentionally not registered (defeating the gbrain extension's
     2026-05-04 read-only main-session decision).

This change keeps the read guidance intact and makes the write contract
explicit:

  - Main session must NOT write/edit under .pensieve/ on its own
  - Main session must NOT shell out to 'gbrain put' / 'bun ...gbrain/cli.ts put'
  - Sediment is the single writer for both stores
  - Exception: user-explicit /skill:pensieve self-improve, /pensieve refine,
    'sediment this conclusion' — main session may run the workflow

We considered a tool_call hook with physical block + bypass flag (pi
extension API supports {block:true,reason:...} for tool_call events),
but reviewing the actual incident rate (zero confirmed main-session
writes under the 2026-05-04 gbrain_put removal — only intent that was
caught and corrected mid-conversation) plus the maintenance overhead
of a triggerable bypass for the legitimate self-improve/refine paths
made the prompt-only fix the right tradeoff. If incidents resurface we
can layer the hook on top.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants