Skip to content
This repository was archived by the owner on Jul 3, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
215 changes: 132 additions & 83 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Incremental session indexing and proposal generation for the [Pi coding agent](https://github.com/earendil-works/pi).

pi-prospector reads your Pi session transcripts, indexes them into a local SQLite database, and uses an LLM to propose improvements to your prompts, skills, and configuration — without applying them. You decide what to develop.
pi-prospector is a **Pi extension** that reads your Pi session transcripts, indexes them into a local SQLite database, and uses an LLM to propose improvements to your prompts, skills, and configuration — without applying them. You decide what to develop.

## How it works

Expand All @@ -11,162 +11,211 @@ Pi sessions (~/.pi/agent/sessions/)
┌─────────────────────┐
│ prospect sync │ ← Incremental. Only new lines are processed.
│ (no LLM, fast) │ Detects forks. Deduplicates shared message trees.
│ /prospect-sync │ ← Incremental. Only new lines are processed.
│ (no LLM, fast) │ Detects forks. Deduplicates shared message trees.
│ Also runs turn-pair- │ core (deterministic) analysis on new sessions.
└────────┬────────────┘
┌─────────────────────┐
sessions.db │ ← All session data, messages, and proposals
│ (SQLite + FTS5) │
prospector.db │ ← All session data, messages, analysis nodes,
│ (SQLite + FTS5) │ edges, and proposals
└────────┬────────────┘
┌─────────────────────┐
│ prospect analyze │ ← Runs an LLM over unprocessed sessions.
│ (uses Pi provider) │ Generates proposals. Does NOT edit any files.
│ /prospect-analyze │ ← Runs LLM analyzers over unprocessed sessions.
│ (uses Ollama or │ Generates proposals. Does NOT edit any files.
│ Pi's model) │
└────────┬────────────┘
┌─────────────────────┐
│ proposals table │ ← status: new / accepted / rejected
│ in sessions.db │ Each proposal records when it was made.
│ proposals table │ ← status: open / applied / duplicate
│ in prospector.db │ Each proposal deduplicates by content hash.
└────────┬────────────┘
┌─────────────────────┐
│ Pi tool: prospect │ ← Your coding agent lists proposals, accepts
│ /prospect command or rejects them, requests syncs, checks stats.
│ Pi tool: prospect │ ← Your coding agent syncs, checks stats,
│ /prospect commands lists/accepts/rejects proposals.
└─────────────────────┘
```

## Install

```bash
pi install git:github:nicolas-marchildon/pi-prospector
# From local path (for development):
pi install /path/to/pi-prospector

# From git (when published):
pi install git:github.com:v2nic/pi-prospector
```

Requires pi with an LLM API key configured for at least one provider. You choose which model to use for analysis.
Requires [Ollama](https://ollama.com) running locally for LLM-backed analysis. The deterministic analyzer (`turn-pair-core`) works without any LLM.

## Commands
## Usage

### `/prospect sync`
### `/prospect-sync`

Index session files into the database. No LLM is called. Fast and cheap.
Index session files into the database. Also runs deterministic analysis on new sessions.

- Scans `~/.pi/agent/sessions/` for new or modified `.jsonl` files
- Parses each file line-by-line, starting from the last line previously processed (incremental)
- Detects sessions that forked from another session via the `parentSession` header — shared message trees are stored once, not duplicated
- Tracks a cursor per session file: `{session_id, last_line, last_modified}`
- Re-indexes a file only if its modification time has changed since the last sync

Run this as often as you like. It's idempotent and incremental.

### `/prospect analyze [--limit N] [--model provider/model]`
- Parses each file line-by-line, starting from the last line processed (incremental)
- Detects sessions that forked from another via `parentSession` header
- After sync, runs `turn-pair-core` (deterministic) analysis on unanalyzed sessions
- No LLM required — fast and free

Run an LLM over sessions that have been synced but not yet analyzed. Generates proposals and inserts them into the database.
### `/prospect-analyze [--limit N] [--model model-spec]`

- Processes sessions in chronological order (oldest first) by default
- `--limit N`: only analyze N sessions (default: all unprocessed)
- `--model provider/model`: which Pi provider model to use (default: the model from `~/.pi/agent/prospector.json`, falls back to the current session model)
- Calls the Pi AI library (`@earendil-works/pi-ai`) directly — no subprocess, no extra session
- Each proposal records `created_at` so you can tell whether a session segment predates or postdates a given recommendation
- Analyze is incremental: it processes new or changed sessions regardless of whether past proposals from those sessions were accepted, rejected, or applied. The indexer and the analyzer are independent — sync always indexes new data, analyze always generates proposals from unprocessed data
- Proposals are never auto-applied. They sit in the database with status `new` until you decide
Run LLM analysis over sessions that have been synced but not yet analyzed.

### `/prospect stats`
- `--limit N`: only analyze N sessions
- `--model model-spec`: Ollama model spec (e.g. `glm-5.1:cloud`, `deepseek-v4-flash:cloud`)
- If `--model` is not specified, falls back to `model` in config, then Pi's current model
- Runs all registered analyzers in topological order:
1. `turn-pair-core` — deterministic user/assistant pair analysis
2. `turn-pair-llm` — LLM-backed classification of pairs
3. `session-overview` — LLM summary and sentiment arc analysis

Print a summary of the database:
### `/prospect-stats`

- Total sessions indexed
- Total messages (user + assistant) and tool responses
- Number of messages processed by the LLM
- Number of proposals by status (new / accepted / rejected)
Print database statistics: sessions indexed, messages, analysis nodes/edges/runs, proposals by status.

### `/prospect proposals [--status new|accepted|rejected]`
### `/prospect-proposals [status]`

List proposals from the database, optionally filtered by status.
List proposals, optionally filtered by status (`open`, `applied`, `rejected`).

Each proposal shows:
- **ID** — unique identifier
- **Target** — what this proposal suggests changing (e.g. `AGENTS.md § Tool usage`, `skill/debug-typescript-errors`)
- **Severity** — `friction` | `correction` | `waste` | `suggestion`
- **Summary** — one-line description of the proposed change
- **Created** — when the proposal was generated
- **Session** — which session triggered it
- **Status** — `new`, `accepted`, or `rejected`
### `/prospect-accept <id>`

### `/prospect accept <id>`
Mark a proposal as applied. Does **not** implement the proposal — only updates status.

Mark a proposal as accepted. This does **not** apply the proposal — it only updates the status. You then ask your Pi coding agent to implement it.

### `/prospect reject <id>`
### `/prospect-reject <id>`

Mark a proposal as rejected.

## Pi tool: `prospect`
### Pi tool: `prospect`

When installed, pi-prospector registers a `prospect` tool that the Pi coding agent can call during sessions:
The extension also registers a `prospect` tool that the Pi coding agent can call during sessions:

| Action | What it does |
|--------|-------------|
| `sync` | Index new/modified sessions into the database |
| `stats` | Return sync and proposal statistics |
| `list_proposals` | List proposals, optionally filtered by status |
| `accept` | Mark a proposal as accepted |
| `accept` | Mark a proposal as applied |
| `reject` | Mark a proposal as rejected |
| `analyze` | Run the LLM over unprocessed sessions |

This lets you say things like "show me new proposals" or "sync my sessions and check stats" directly in a Pi conversation.
This lets you say things like "show me open proposals" or "sync my sessions" directly in a Pi conversation.

## What gets analyzed
## Analyzer framework

pi-prospector reads **only what is inside Pi session files**. It does not read Pi configuration files, `AGENTS.md`, skill files, or any other artifact directly. The session file contains:
The analysis framework implements the design from `docs/analyzer-design-c.md`:

- User messages (what you said)
- Assistant messages (what the agent said, including thinking)
- Tool calls and tool results (what the agent did)
- Compaction summaries (what was retained after context compression)
- Model changes and thinking level changes

The system prompt is not stored in session files and is not captured in v1.

## Timestamps
- **`turn-pair-core`** — deterministic. Identifies user/assistant pairs, detects tool patterns, computes metrics (turn count, error count, retry count, etc.)
- **`turn-pair-llm`** — LLM-backed. Classifies each pair as one of 10 friction categories (error-retry, tool-correction, etc.)
- **`session-overview`** — LLM-backed. Generates a summary, sentiment arc, and session classification

Each proposal records `created_at`. Each session message has a `timestamp`. These are stored in the database in case you want to correlate proposals with session activity later. v1 does nothing with this information beyond storing it.
Each analyzer:
1. Has an `id`, `versionId`, dependencies on other analyzers, and prompts
2. Plans analysis units from messages + dependency nodes
3. Runs each unit, producing nodes and edges
4. Nodes are deduplicated by input hash (re-running is safe)
5. Proposals are materialized from matching node kinds

## Fork deduplication
## Database

Pi sessions are stored as trees. When you branch a session with `/tree`, the new session file has a `parentSession` header pointing to the original. Messages before the branch point are shared.
Location: `~/.pi/agent/prospector.db` (configurable)

During sync, pi-prospector:

1. Reads the `parentSession` header from each session file
2. Resolves the parent session file
3. Stores shared messages once, tagged with the original session
4. Marks the forked session as starting from the branch point

This means analyzing a forked session only processes the **new** messages after the fork — not the entire conversation history again.
Tables:
- `sessions`, `messages` — synced from Pi session files
- `analyzer_defs`, `analyzer_versions`, `prompts`, `analyzer_configs` — analyzer metadata
- `analysis_runs`, `analysis_nodes`, `analysis_edges`, `analysis_progress` — analysis results
- `proposals` — LLM-generated improvement proposals (v2 schema with dedup)

## Configuration

Create `~/.pi/agent/prospector.json`:

```json
{
"model": "openrouter/deepseek-v4-flash",
"dbPath": "~/.pi/agent/prospector.db"
"model": "glm-5.1:cloud",
"dbPath": "~/.pi/agent/prospector.db",
"modelTiers": {
"cheap": "deepseek-v4-flash:cloud",
"mid": "glm-5.1:cloud",
"expensive": "glm-5.1:latest"
}
}
```

| Field | Default | Description |
|-------|---------|-------------|
| `model` | *(current session model)* | Provider and model to use for analysis, in `provider/model` format. Must be a model Pi has an API key for. Cheaper models like `openrouter/deepseek-v4-flash` or `gemma4:26b` work well for analysis. Override per-run with `--model`. |
| `model` | *(Pi's current model)* | Ollama model spec for LLM analysis |
| `dbPath` | `~/.pi/agent/prospector.db` | Path to the SQLite database |
| `modelTiers` | *(built-in defaults)* | Model tier mapping for cheap/mid/expensive analysis |

The model must correspond to a provider Pi already has credentials for (configured via `/login` or API keys). Any model Pi supports works — pick based on cost vs. quality. For backfill, a cheap model is recommended.
## Development

```bash
# Type check
npx tsc --noEmit

# Run unit + component tests
node --import tsx --test tests/unit/*.test.ts tests/component/*.test.ts

# Run integration tests
node --import tsx --test test/integration/*.ts

# Install extension locally for testing
pi install /path/to/pi-prospector
```

## Architecture

```
src/
├── index.ts # Extension entry point (registers commands + tool)
├── pi-stubs.ts # Type stubs for @earendil-works/pi-coding-agent
├── config.ts # Configuration loading
├── sync/ # Session scanning and parsing (no LLM)
│ ├── scanner.ts # Discovers .jsonl session files
│ ├── parser.ts # Parses session entries (v0.5+ format)
│ ├── cursor.ts # Tracks sync progress
│ └── forks.ts # Resolves parent sessions
├── db/
│ ├── schema.ts # Migrations (001: sync, 002: analysis)
│ ├── queries.ts # Sync queries (sessions, messages, proposals v2)
│ └── analysis-queries.ts # Analysis queries (runs, nodes, edges, progress)
├── analyze/
│ ├── types.ts # TypeBox schemas for all analysis data shapes
│ ├── framework.ts # AnalyzerFramework: orchestrates planning + execution
│ ├── model-tiers.ts # Model tier resolution (cheap/mid/expensive)
│ ├── input-hash.ts # Input deduplication via SHA-256
│ ├── ollama-llm.ts # Ollama LLM backend (localhost:11434)
│ ├── edge-kinds.ts # Edge kind constants + validation
│ ├── proposal-materializer.ts # Generates proposals from analysis nodes
│ └── analyzers/
│ ├── turn-pair-core/ # Deterministic pair analysis
│ ├── turn-pair-llm/ # LLM-backed friction classification
│ └── session-overview/ # LLM summary + sentiment arc
└── commands/
├── sync.ts # /prospect-sync
├── analyze.ts # /prospect-analyze
├── stats.ts # /prospect-stats
├── proposals.ts # /prospect-proposals, /prospect-accept, /prospect-reject
└── tool.ts # prospect tool (for LLM to call)
```

## What gets analyzed

pi-prospector reads **only what is inside Pi session files**. It does not read Pi configuration files, `AGENTS.md`, skill files, or any other artifact directly. The session file contains:

- User messages (what you said)
- Assistant messages (what the agent said, including thinking)
- Tool calls and tool results (what the agent did)
- Compaction summaries (what was retained after context compression)
- Model changes and thinking level changes

## License

Expand Down
55 changes: 55 additions & 0 deletions src/analyze/analyzers/session-overview/compress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Map-reduce compression for large sessions.
*/

import type { LLMRequest, LLMResponse } from "../../types.js";

export interface Segment { index: number; text: string; pairCount: number; }

export function splitDigestIntoSegments(markdown: string, maxCharsPerSegment: number = 8000): Segment[] {
if (markdown.length <= maxCharsPerSegment) return [{ index: 0, text: markdown, pairCount: 0 }];
const segments: Segment[] = [];
const lines = markdown.split("\n");
let currentLines: string[] = [];
let currentLen = 0;
let segIdx = 0;
for (const line of lines) {
if (currentLen + line.length > maxCharsPerSegment && currentLines.length > 0) {
segments.push({ index: segIdx++, text: currentLines.join("\n"), pairCount: currentLines.filter(l => l.startsWith("|")).length - 1 });
const overlapLines = currentLines.slice(-3);
currentLines = [...overlapLines, line];
currentLen = currentLines.join("\n").length + line.length;
} else {
currentLines.push(line);
currentLen += line.length + 1;
}
}
if (currentLines.length > 0) segments.push({ index: segIdx, text: currentLines.join("\n"), pairCount: currentLines.filter(l => l.startsWith("|")).length - 1 });
return segments;
}

export async function mapPhase(segment: Segment, llm: (req: LLMRequest) => Promise<LLMResponse>, modelSpec: string): Promise<string> {
const response = await llm({ model: modelSpec, systemPrompt: SESSION_MAP_SYSTEM_PROMPT, userPrompt: `Analyze this session segment:\n\n${segment.text}`, maxTokens: 1024, temperature: 0.2 });
return response.content ?? "(no content)";
}

const SESSION_MAP_SYSTEM_PROMPT = `You are a session analyst. Summarize the key findings from this session segment. Focus on friction, corrections, waste, and quality.`;

export const SESSION_REDUCE_PROMPT = `You are a session analyst. Given segment summaries and aggregated statistics, produce a complete session analysis including:\n1. Session summary (2–3 sentences)\n2. Key friction points with severity\n3. Improvement proposals targeting specific config, skills, or documentation\n4. Sentiment arc across the session\n\nEach proposal should have: target_type, target_path, title, summary, detail, evidence, confidence (0.0–1.0), severity.\n\nCall the submit_session_analysis tool with your findings.`;

export const SESSION_OVERVIEW_TOOL_NAME = "submit_session_analysis";

export const SESSION_OVERVIEW_TOOL_SCHEMA = {
name: SESSION_OVERVIEW_TOOL_NAME,
description: "Submit session overview analysis with proposals",
parameters: {
type: "object" as const,
properties: {
session_summary: { type: "string" as const, description: "2–3 sentence summary" },
key_friction_points: { type: "array" as const, items: { type: "object" as const, properties: { description: { type: "string" as const }, pair_node_id: { type: "string" as const }, severity: { type: "string" as const, enum: ["low", "medium", "high"] } }, required: ["description", "pair_node_id", "severity"] } },
improvement_proposals: { type: "array" as const, items: { type: "object" as const, properties: { target_type: { type: "string" as const, enum: ["agents_md", "system_md", "skill", "extension_prompt", "tool_output", "repo_doc", "config"] }, target_path: { type: "string" as const }, title: { type: "string" as const }, summary: { type: "string" as const }, detail: { type: "string" as const }, evidence: { type: "string" as const }, confidence: { type: "number" as const }, severity: { type: "string" as const, enum: ["friction", "correction", "waste", "suggestion", "insight"] } }, required: ["target_type", "title", "summary", "severity", "confidence"] } },
sentiment_arc: { type: "array" as const, items: { type: "object" as const, properties: { segment: { type: "number" as const }, sentiment: { type: "string" as const }, key_event: { type: "string" as const } }, required: ["segment", "sentiment", "key_event"] } },
},
required: ["session_summary", "key_friction_points", "improvement_proposals"],
},
};
Loading
Loading