Update design doc and implementation plan for proper LLM integration

- design-doc.md: Rewrote Section 3 (Pipeline Architecture) to describe the
  real LLM integration: dual-provider LLM client (Pi native + external),
  SHA-256 disk cache in ~/.cache/pi-project-map/, parallelization with
  4-8 concurrent requests, retries with exponential backoff, hard-error
  policy, context limit protection. Added new Section 4 with concrete
  file-level and package-level prompts.
- implementation-plan.md: Replaced old milestone schedule with current
  status and detailed M7 tasks for LLM integration: LLM client abstraction,
  external API client, Pi LLM client, disk cache, parallel batching with
  retries, rewriting llm-extract.ts, context limits, CLI/extension wiring,
  and tests.
- Minor formatting cleanup in pi-extension.ts and src/cli.ts

All 16 tests pass. TypeScript compiles clean.
This commit is contained in:
2026-06-09 21:45:44 +02:00
parent 93c2ac60c5
commit 7b67205d43
4 changed files with 326 additions and 210 deletions
+107 -13
View File
@@ -75,9 +75,10 @@ Two independent extraction layers contribute to the same output file.
#### Layer 1: LLM-Based Extraction (All Files) #### Layer 1: LLM-Based Extraction (All Files)
- **Input**: Raw file contents of every non-excluded file in the directory. - **Input**: Raw file contents of every non-excluded file in the directory.
- **Output**: One-line purpose description, architectural role, and cross-file relationships. - **Output**: Purpose description, architectural role, and cross-file relationships.
- **Applies to**: Code files, config files, Dockerfiles, READMEs, YAML, JSON, shell scripts — everything. - **Applies to**: Code files, config files, Dockerfiles, READMEs, YAML, JSON, shell scripts — everything.
- **When it runs**: Once per file during init; again on changed files during patching. - **When it runs**: Once per file during init; again on changed files during patching.
- **Implementation**: Calls an actual LLM (not regex heuristics). Inside Pi, it uses Pi's built-in LLM via the ExtensionAPI. Standalone CLI falls back to an external LLM API (OpenAI-compatible).
#### Layer 2: AST-Based Extraction (Code Files Only) #### Layer 2: AST-Based Extraction (Code Files Only)
- **Input**: Source code of files where a tree-sitter or LSP parser is available. - **Input**: Source code of files where a tree-sitter or LSP parser is available.
@@ -93,17 +94,55 @@ The two layers merge into a single line per file under `## files`:
^ LLM ^ LLM ^ AST ^ AST + LLM ^ LLM ^ LLM ^ AST ^ AST + LLM
``` ```
- File name and one-line purpose: LLM. - File name and purpose: LLM.
- Exported symbols and signatures: AST (augmented by LLM if AST unavailable). - Exported symbols and signatures: AST (augmented by LLM if AST unavailable).
- Dependency list: AST for imports; LLM for inferred architectural dependencies. - Dependency list: AST for imports; LLM for inferred architectural dependencies.
### LLM Client Architecture
The LLM client is abstracted behind a unified interface:
```typescript
interface LLMClient {
complete(prompt: string): Promise<string>;
}
```
Two implementations:
1. **PiLLMClient** (Pi extension): Uses `ctx.model` or `ctx.modelRegistry` to invoke Pi's configured LLM. Called from `pi-extension.ts` when the skill runs inside Pi.
2. **ExternalLLMClient** (standalone CLI): Calls an external OpenAI-compatible API. Configured via environment variable (e.g., `OPENAI_API_KEY`) or config file.
### Caching
LLM results are cached to avoid re-querying unchanged files.
- **Key**: SHA-256 hash of file contents.
- **Storage**: JSON file at `~/.cache/pi-project-map/llm-cache.json`.
- **Behavior**: Before calling the LLM, compute the file hash and check the cache. If hit, reuse the cached result. If miss, call the LLM and store the result.
- **Invalidation**: Cache entries are implicitly invalidated when the file content changes (because the hash changes). There is no TTL; the cache is append-only.
### Parallelization and Rate Limiting
- **Concurrency**: 4-8 LLM calls in parallel, controlled by `p-limit`.
- **Batch delays**: A small delay (e.g., 100ms) is inserted between batches to avoid triggering rate limits.
- **Retry policy**: Each LLM call retries up to 3 times with exponential backoff (1s, 2s, 4s). If all retries fail, the entire operation stops with a hard error.
### Error Handling
- **Hard error on failure**: If an LLM call fails after all retries, `init` or `patch` stops immediately and prints a clear error. There is no heuristic fallback. The user must resolve the issue (set API key, wait for rate limit, check network).
- **Context limit protection**: Files larger than the LLM's context window are truncated from the end (with a note in the prompt) before being sent.
### Init Pipeline ### Init Pipeline
``` ```
For each directory (depth-first): For each directory (depth-first):
1. List all non-excluded files. 1. List all non-excluded files.
2. For each file: 2. For each file (parallel, 4-8 concurrent):
a. Run LLM extraction (purpose, role). a. Compute SHA-256 of file contents.
b. If code file + parser available: run AST extraction (symbols, imports). b. Check disk cache. If hit, use cached result.
c. If miss: call LLM (with retries/backoff) to extract purpose and role.
d. Store result in cache.
e. If code file + parser available: run AST extraction (symbols, imports).
3. Merge per-file outputs into lines. 3. Merge per-file outputs into lines.
4. Run LLM on merged lines + directory context to generate: 4. Run LLM on merged lines + directory context to generate:
- `## role` (package-level summary) - `## role` (package-level summary)
@@ -117,13 +156,68 @@ When agent edits file(s) in directory:
1. Determine patch strategy: 1. Determine patch strategy:
- If directory has < 10 files: full rewrite. - If directory has < 10 files: full rewrite.
- Else: section-level patch for changed file(s) only. - Else: section-level patch for changed file(s) only.
2. Re-run LLM extraction on changed file(s). 2. For each changed file:
a. Recompute SHA-256.
b. Check cache. If miss or stale, call LLM with retries/backoff.
3. Re-run AST extraction on changed file(s) if applicable. 3. Re-run AST extraction on changed file(s) if applicable.
4. Update `## files` section (rewrite or patch). 4. Update `## files` section (rewrite or patch).
5. Update `## dirty` flag if full regeneration is deferred. 5. Update `## dirty` flag if full regeneration is deferred.
``` ```
## 4. Consumption Model ## 4. LLM Prompt Design
### File-Level Prompt
The LLM prompt for a single file is designed to produce a structured, concise analysis.
```
You are analyzing a source file for a project map. Read the file below and summarize:
1. PURPOSE: What does this file do? Describe its role in the project (2-3 sentences max).
2. DEPENDENCIES: What does this file depend on? List internal modules/packages and external libraries.
3. KEY CONCEPTS: Mention any important patterns, algorithms, or domain concepts.
File path: <file-path>
```
<file-contents-truncated>
```
Respond in this exact format:
PURPOSE: <concise description>
DEPS: <comma-separated list, or "none">
CONCEPTS: <comma-separated list, or "none">
```
### Package-Level Prompt
After all file summaries are collected for a directory, a second LLM call synthesizes the package role and architecture.
```
You are analyzing a directory in a software project. Below is a list of files in this directory with their purposes.
Directory: <dir-path>
Files:
- <file1>: <purpose1>
- <file2>: <purpose2>
...
Respond in this exact format:
ROLE: <one-line description of this directory's role in the project>
ARCH: <2-4 sentences describing architecture, data flow, patterns, and design decisions>
```
### Output Parsing
The LLM client's response is parsed to extract `PURPOSE`, `DEPS`, `CONCEPTS`, `ROLE`, and `ARCH` fields. These are merged with AST data into the final `.pi-map.md` format.
### Context Limit Protection
- Files are truncated from the end if they exceed a configurable max token budget (default: 4000 tokens of source).
- A marker `[...truncated]` is appended to the truncated content so the LLM knows it is not seeing the full file.
- Very large binary or generated files are skipped entirely for LLM analysis (they still appear in `.pi-map.md` with a note like "Large/generated file").
## 5. Consumption Model
### Session Start ### Session Start
1. Agent discovers all `.pi-map.md` files (e.g., via `find . -name ".pi-map.md"`). 1. Agent discovers all `.pi-map.md` files (e.g., via `find . -name ".pi-map.md"`).
@@ -139,7 +233,7 @@ When agent edits file(s) in directory:
- For very large projects, the agent may summarize or prune the initial read, keeping only the top N levels of the hierarchy in active context. - For very large projects, the agent may summarize or prune the initial read, keeping only the top N levels of the hierarchy in active context.
- The skill can provide a "context budget" parameter: max tokens to spend on analysis files. - The skill can provide a "context budget" parameter: max tokens to spend on analysis files.
## 5. Stale Data Mitigation ## 6. Stale Data Mitigation
### Combined Strategy ### Combined Strategy
@@ -167,7 +261,7 @@ When agent edits file(s) in directory:
- If validation finds staleness beyond a threshold (e.g., > 3 dirty packages), the skill recommends a full re-init. - If validation finds staleness beyond a threshold (e.g., > 3 dirty packages), the skill recommends a full re-init.
- The agent can also trigger re-init for a specific subtree. - The agent can also trigger re-init for a specific subtree.
## 6. Scope Boundaries and Non-Goals ## 7. Scope Boundaries and Non-Goals
### In Scope ### In Scope
- Every directory in the project gets a `.pi-map.md` file. - Every directory in the project gets a `.pi-map.md` file.
@@ -182,7 +276,7 @@ When agent edits file(s) in directory:
- **Cross-project analysis**: Each project is independent. No global index across repos. - **Cross-project analysis**: Each project is independent. No global index across repos.
- **IDE integration**: This is a Pi agent skill, not a VS Code extension or LSP server. - **IDE integration**: This is a Pi agent skill, not a VS Code extension or LSP server.
## 7. Pi Skill Package Structure ## 8. Pi Skill Package Structure
``` ```
pi-project-map/ pi-project-map/
@@ -212,7 +306,7 @@ pi-project-map/
- On every prompt, the skill appends a lightweight instruction: - On every prompt, the skill appends a lightweight instruction:
> "If you modify any source file, run `project-map:patch <path>` to update the analysis. If you suspect staleness, run `project-map:validate`." > "If you modify any source file, run `project-map:patch <path>` to update the analysis. If you suspect staleness, run `project-map:validate`."
## 8. Risks and Tradeoffs ## 9. Risks and Tradeoffs
| Risk | Likelihood | Impact | Mitigation | | Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------| |------|-----------|--------|------------|
@@ -223,7 +317,7 @@ pi-project-map/
| Overlap with LSP/typedoc | Low | Low | This is agent-context, not IDE tooling. Different use case. | | Overlap with LSP/typedoc | Low | Low | This is agent-context, not IDE tooling. Different use case. |
| AST parser unavailable | Medium | Low | Graceful fallback to LLM-only extraction | | AST parser unavailable | Medium | Low | Graceful fallback to LLM-only extraction |
## 9. Concrete Example: Full Project Snapshot ## 10. Concrete Example: Full Project Snapshot
``` ```
project-root/ project-root/
@@ -250,7 +344,7 @@ project-root/
Each `.pi-map.md` follows the format in Section 2, creating a navigable hierarchy. Each `.pi-map.md` follows the format in Section 2, creating a navigable hierarchy.
## 10. Future Extensions ## 11. Future Extensions
- **Cross-reference graph**: A top-level `project-graph.md` linking all packages with dependency arrows. - **Cross-reference graph**: A top-level `project-graph.md` linking all packages with dependency arrows.
- **Search index**: A lightweight FTS5 index over all `.pi-map.md` files for fast symbol lookup. - **Search index**: A lightweight FTS5 index over all `.pi-map.md` files for fast symbol lookup.
+176 -181
View File
@@ -6,241 +6,236 @@ Build a Pi skill package (`pi-project-map`) that generates and maintains a hiera
--- ---
## Milestones ## Current Status
### M1: Foundation and Format (Week 1) - **M1: Foundation and Format** — ✅ Complete
**Goal**: A working dense-markdown formatter and the ability to generate `.pi-map.md` files from a directory listing. - **M2: Heuristic Extraction** — ✅ Complete (placeholder only; to be replaced by real LLM)
- **M3: AST Extraction** — ✅ Complete
- **M4: Patch and Update** — ✅ Complete
- **M5: Validation and `--fix`** — ✅ Complete
- **M6: Pi Skill Integration** — ✅ Complete (basic)
- **M7: Proper LLM Integration** — 🔄 In Progress / Next up
#### Tasks The remaining major work is to replace the heuristic "LLM" extraction with **actual LLM API calls**.
1. **Bootstrap the npm package**
- Create `package.json`, `tsconfig.json`, basic CLI entry point.
- Set up test runner (vitest or jest).
- Create stub `SKILL.md` for Pi integration.
2. **Implement the formatter (`src/format.ts`)**
- Define the `.pi-map.md` schema as a TypeScript interface.
- Implement `renderPackageMap(data) -> string` that outputs dense markdown.
- Implement `parsePackageMap(markdown) -> data` for reading existing files.
- Unit tests for round-trip serialization.
3. **Implement file discovery (`src/discover.ts`)**
- Walk a directory tree.
- Respect `.gitignore` and a built-in ignore list (node_modules, .git, dist, etc.).
- Return a flat list of `(dirPath, filePaths[])` tuples.
- Unit tests with fixture directories.
4. **Implement init skeleton (`src/init.ts`)**
- Given a project root, create one `.pi-map.md` per directory.
- Populate `## files` with filenames only (no analysis yet).
- Populate `## role` and `## arch` with placeholder text.
- Command: `project-map:init --root <path>`
**Acceptance Criteria**
- Running `project-map:init` on a test repo creates `.pi-map.md` in every directory.
- Files match `.gitignore` rules correctly.
- Output format follows the dense-markdown spec from the design doc.
**Dependencies**: None.
--- ---
### M2: LLM Extraction Layer (Week 2) ## Remaining Milestone: M7 — Proper LLM Integration
**Goal**: The LLM layer generates one-line purposes and package-level architecture notes.
#### Tasks **Goal**: Replace the regex heuristic `llm-extract.ts` with real LLM calls, including dual-provider support (Pi native + external fallback), disk caching, parallelization, and retries.
1. **Implement LLM file extraction (`src/llm-extract.ts`)**
- Prompt template: given a file's contents and path, return a one-line purpose and a list of exported symbols (if any).
- Prompt template: given a file's contents, infer its dependencies.
- Integrate with Pi's LLM tool calls (or a generic OpenAI-compatible client for standalone testing).
- Add caching: hash file contents, skip LLM call if unchanged.
2. **Implement LLM package extraction (`src/llm-extract.ts`)** **Estimated time**: 1 week of focused work.
- Prompt template: given all per-file summaries in a directory, generate `## role` and `## arch`.
- Keep prompts token-efficient (truncate large files, focus on headers/exports).
3. **Wire LLM layer into init (`src/init.ts`)**
- After file discovery, run LLM extraction on every file.
- Then run package-level LLM extraction.
- Write fully populated `.pi-map.md` files.
4. **Add configuration (`src/config.ts`)**
- Configurable ignore patterns.
- Configurable LLM model and token limits.
- Configurable context budget for large projects.
**Acceptance Criteria**
- `project-map:init` produces `.pi-map.md` files with meaningful `## files`, `## role`, and `## arch` sections.
- Caching works: second run on unchanged repo is fast.
- Unit tests mock LLM responses to verify prompt structure and output parsing.
**Dependencies**: M1.
--- ---
### M3: AST Extraction Layer (Week 3) ### Task 1: LLM Client Abstraction (`src/llm-client.ts`)
**Goal**: Code files get precise symbol and dependency data from tree-sitter or LSP.
#### Tasks Create a unified interface for LLM calls.
1. **Set up tree-sitter (`src/ast-extract.ts`)**
- Add tree-sitter dependencies for target languages (TypeScript, Python, Go, Rust as first-class).
- Implement `extractSymbols(filePath, language) -> { exports, imports, types }`.
- Implement `extractDependencies(filePath, language) -> string[]`.
2. **Language detection** ```typescript
- Map file extensions to tree-sitter grammars. interface LLMClient {
- Graceful fallback to LLM-only if no parser available. complete(prompt: string): Promise<string>;
}
```
3. **Merge AST + LLM outputs (`src/merge.ts`)** Sub-tasks:
- Combine AST symbols with LLM purpose descriptions. - Define the `LLMClient` interface.
- AST provides `exp:` and `dep:`; LLM provides the one-line purpose. - Add a factory function `createLLMClient(mode: 'pi' | 'external', options)`.
- If AST and LLM disagree on dependencies, prefer AST for imports, LLM for inferred architectural deps. - Handle mode selection:
- `'pi'`: used inside `pi-extension.ts` with Pi's model registry.
4. **Wire AST into init (`src/init.ts`)** - `'external'`: used in standalone CLI with OpenAI-compatible API.
- For each code file, run AST extraction in parallel with LLM extraction.
- Merge results before formatting.
**Acceptance Criteria** **Acceptance Criteria**
- TypeScript/Python files show precise exported function/class names in `exp:`. - `LLMClient` interface exists and compiles.
- Import statements are captured in `dep:`. - Factory correctly selects implementation based on mode.
- Files with no AST parser still get LLM-only analysis.
- Unit tests with sample source files verify symbol extraction accuracy.
**Dependencies**: M2.
--- ---
### M4: Patch and Update (Week 4) ### Task 2: External LLM Client (`src/external-llm-client.ts`)
**Goal**: The agent can incrementally update `.pi-map.md` files after editing source files.
#### Tasks Implement the standalone CLI LLM client using an OpenAI-compatible API.
1. **Implement patch strategy (`src/patch.ts`)**
- Determine directory size (file count).
- If < 10 files: trigger full rewrite of that directory's `.pi-map.md`.
- If >= 10 files: parse existing `.pi-map.md`, identify the section(s) for changed file(s), and rewrite only those lines.
2. **Implement dirty markers (`src/patch.ts`)** Sub-tasks:
- When a section-level patch occurs, append a dirty note: - Add `openai` (or a lightweight fetch-based client) as a dependency.
```markdown - Read configuration from:
## dirty - Environment variable `OPENAI_API_KEY` (or `ANTHROPIC_API_KEY`, etc.)
2024-06-09T14:32:00Z: tokens.ts patched (section-level) - Config file `.pi-project-map.json` fields: `llmProvider`, `llmModel`, `llmBaseUrl`
``` - Implement `complete(prompt)` using chat completions API.
- Default to `gpt-4o-mini` or similar cheap model.
3. **Command: `project-map:patch <file-path>`**
- Given a changed file path, find its directory.
- Re-run LLM + AST extraction on that file only.
- Apply patch strategy.
- Update `.pi-map.md`.
4. **Command: `project-map:reinit [path]`**
- Force full re-init of entire project or a subtree.
- Clear all dirty markers.
**Acceptance Criteria** **Acceptance Criteria**
- Editing one file in a small package triggers full rewrite of that package's `.pi-map.md`. - `project-map init` works standalone with `OPENAI_API_KEY` set.
- Editing one file in a large package patches only the relevant line. - Missing API key produces a clear, actionable error message.
- Dirty markers are correctly added on section-level patches. - Failed API call throws a descriptive `LLMError`.
- Re-init clears dirty markers and regenerates everything.
**Dependencies**: M3.
--- ---
### M5: Validation and Consistency (Week 5) ### Task 3: Pi LLM Client (`src/pi-llm-client.ts`)
**Goal**: The agent can detect and recover from stale analysis files.
#### Tasks Implement the Pi-native LLM client for use inside the extension.
1. **Implement validation (`src/validate.ts`)**
- Walk all `.pi-map.md` files.
- For each listed file, check existence (catch deletions).
- For each directory, check for new files not in `.pi-map.md`.
- For code files with AST support, compare listed `exp:` against actual symbols.
- Report discrepancies with severity (missing, orphaned, stale-signature).
2. **Implement reconciliation (`src/validate.ts`)** Sub-tasks:
- Option `--fix`: automatically patch or flag discrepancies. - Accept Pi's `ExtensionContext` or model registry as a constructor argument.
- Option `--reinit-if <threshold>`: if > N discrepancies, recommend full re-init. - Use `ctx.model` / `ctx.modelRegistry` to get the configured model and API key.
- Call the provider directly (likely using the same OpenAI-compatible endpoint Pi uses).
3. **Command: `project-map:validate`** - If Pi does not expose direct LLM calls, fall back to emitting a tool call / follow-up message pattern.
- Run checks and print a report.
- Exit code 0 if clean, 1 if discrepancies found.
**Acceptance Criteria** **Acceptance Criteria**
- Validation detects a deleted file that still appears in `.pi-map.md`. - Pi extension can successfully call an LLM when running inside Pi.
- Validation detects a new file not yet in `.pi-map.md`. - Errors surface clearly to the user.
- Validation detects a changed function signature.
- `--fix` patches or flags all found issues.
**Dependencies**: M4.
--- ---
### M6: Pi Integration (Week 6) ### Task 4: Disk Cache (`src/llm-cache.ts`)
**Goal**: The skill is installable and functional within the Pi agent harness.
#### Tasks Implement persistent SHA-256 → LLM result caching.
1. **Finalize `SKILL.md`**
- Document all custom tools: `project-map:init`, `project-map:patch`, `project-map:validate`, `project-map:reinit`.
- Document the dense-markdown format for the agent.
- Include usage examples and configuration options.
2. **Implement prompt hook (`hooks/on-prompt.ts`)** Sub-tasks:
- Inject a lightweight instruction into every prompt: - Cache directory: `~/.cache/pi-project-map/` (create if missing).
> "If you modify any source file, run `project-map:patch <path>`. If you suspect staleness, run `project-map:validate`." - Cache file: `llm-cache.json` (simple JSON object).
- Make injection configurable (toggle on/off, customize message). - Functions:
- `getCached(hash: string): string | null`
3. **Auto-inject session-start behavior** - `setCached(hash: string, result: string): void`
- On session start, if `.pi-map.md` files exist, the skill auto-reads them into context. - Ensure atomic writes (write to temp file, rename).
- If they don't exist, the skill offers to run `project-map:init`. - Add cache size limit (e.g., 10,000 entries, LRU eviction).
- If dirty markers exist, the skill warns the agent.
4. **Packaging and publishing**
- Ensure `package.json` has correct `bin` entries.
- Write human README with install instructions.
- Tag and publish to npm (or internal registry).
**Acceptance Criteria** **Acceptance Criteria**
- Installing the skill in Pi makes the four tools available. - Second `init` run on unchanged files does not call LLM.
- The prompt hook injects maintenance instructions reliably. - Cache persists across process restarts.
- Session start auto-detects and loads `.pi-map.md` files. - Corrupted cache file does not crash the tool.
- The skill can be published and installed via npm.
**Dependencies**: M5. ---
### Task 5: Parallelization and Retries (`src/llm-batch.ts`)
Run LLM requests in parallel with retries and backoff.
Sub-tasks:
- Add `p-limit` dependency for concurrency control.
- Default concurrency: 4 (configurable via `.pi-project-map.json` `llmConcurrency`).
- Add small delay (100ms) between batches.
- Implement retry logic: 3 retries, delays 1s → 2s → 4s.
- On final failure, throw a hard error and stop the entire process.
**Acceptance Criteria**
- 100-file project completes significantly faster than sequential.
- Simulated transient failures are retried and recovered.
- Persistent failure stops the tool with a clear error.
---
### Task 6: Rewrite `llm-extract.ts` to Use Real LLM
Replace regex heuristics with LLM calls.
Sub-tasks:
- Accept an `LLMClient` in `extractFileLLM` and `extractPackageLLM`.
- Check disk cache before calling LLM.
- Construct file-level prompt (see design doc Section 4).
- Parse response into `purpose`, `deps`, `concepts`.
- Construct package-level prompt.
- Parse response into `role`, `arch`.
- Remove heuristic code from production path; keep only for test mocks if useful.
**Acceptance Criteria**
- `llm-extract.ts` calls the LLM client for every file.
- Output includes rich, non-trivial descriptions for typical files.
- Unit tests mock the LLM client to verify prompt structure and parsing.
---
### Task 7: Context Limit Handling
Protect against oversized files.
Sub-tasks:
- Measure prompt + file content tokens (approximate: 1 token ≈ 4 chars for ASCII).
- If file exceeds max context budget (configurable, default 4000 tokens), truncate from the end.
- Append `[...truncated]` marker in the prompt.
- Skip LLM for binary/generated files over a hard limit (e.g., 50KB) and mark them as "Large/generated file".
**Acceptance Criteria**
- A 1MB minified JS file does not crash or consume excessive tokens.
- Truncated files still produce useful output.
---
### Task 8: Update CLI and Extension
Wire the new LLM client into all entry points.
Sub-tasks:
- `src/cli.ts`: create external LLM client, pass into `initProject` / `patchFile`.
- `pi-extension.ts`: create Pi LLM client, pass into tools.
- Update `init.ts` and `patch.ts` signatures to accept an optional `LLMClient`.
- Add CLI flag `--llm-provider=openai` for explicit selection.
- Update error handling to catch `LLMError` and print helpful messages.
**Acceptance Criteria**
- CLI works with external API key.
- Extension works inside Pi (if Pi exposes LLM access).
- Clear errors on misconfiguration.
---
### Task 9: Update Tests
Sub-tasks:
- Replace heuristic tests with mocked LLM client tests.
- Add integration test: create a fake LLM client, run `initProject`, verify output contains LLM-provided text.
- Add cache test: verify cache hit skips LLM call.
- Add retry test: verify transient failures retry, persistent failures hard-stop.
**Acceptance Criteria**
- All tests pass.
- Test coverage includes: LLM client, cache, batching, prompt parsing, error handling.
--- ---
## Sequencing and Dependencies ## Sequencing and Dependencies
``` ```
M1 (Foundation) → M2 (LLM) → M3 (AST) → M4 (Patch) → M5 (Validate) → M6 (Pi Integration) Task 1 (LLMClient interface)
├── Task 2 (External client)
├── Task 3 (Pi client)
├── Task 4 (Cache)
├── Task 5 (Batch + retries)
├── Task 6 (Rewrite llm-extract.ts)
├── Task 7 (Context limits)
├── Task 8 (Wire CLI + extension)
└── Task 9 (Tests)
``` ```
No parallel tracks — each milestone builds on the previous. Total estimated time: 6 weeks at a steady pace, or 3-4 weeks with focused effort.
--- ---
## Validation Criteria (Overall) ## Validation Criteria (for LLM Integration)
1. **Functional**: `project-map:init` correctly generates `.pi-map.md` for a test repo of 50+ files across 10+ directories. 1. **Real LLM calls**: `llm-extract.ts` invokes the configured LLM client for every file.
2. **Accuracy**: AST-extracted symbols match actual source code (verified by unit tests). 2. **Cache hit**: Second `init` on unchanged repo completes with zero LLM calls.
3. **Freshness**: After editing a file, `project-map:patch` updates the analysis within 5 seconds. 3. **Parallel speed**: 100-file project init completes in under 30 seconds (assuming average LLM latency 500ms).
4. **Consistency**: `project-map:validate` detects 100% of synthetic staleness scenarios (deleted files, new files, signature changes). 4. **Retry works**: Transient 429/5xx errors are retried; permanent failures stop with a clear error.
5. **Token efficiency**: A 100-file project's full `.pi-map.md` set fits within a 4k token budget. 5. **Context limit safety**: Files > max token budget are truncated, not rejected.
6. **Pi integration**: The skill installs cleanly, tools are discoverable, and the prompt hook works. 6. **Dual provider**: CLI uses external API; Pi extension uses Pi's LLM.
7. **Quality**: LLM output is visibly richer than the old heuristic output (verified by manual inspection).
--- ---
## Rollout Plan ## Rollout Plan
1. **Internal dogfooding** (Week 7): Use the skill on 2-3 real projects. Gather feedback on format density and agent behavior. 1. **Test on real projects** (Day 1-2): Run `init` on 2-3 real codebases with the new LLM integration.
2. **Format refinement** (Week 8): Adjust abbreviations, section structure, and dirty-marker format based on dogfooding. 2. **Cost audit** (Day 3): Measure token usage per project; adjust defaults if too expensive.
3. **Open beta** (Week 9): Share with a small group of Pi users. Collect bug reports and feature requests. 3. **Prompt tuning** (Day 4-5): Iterate prompt design based on output quality.
4. **v1.0 release** (Week 10): Stable API, documented format, published to npm. 4. **Release** (Day 6-7): Publish updated npm package, update Pi extension docs.
--- ---
## Open Questions for Implementation ## Completed Milestones (for reference)
- Which tree-sitter grammars to bundle by default? (Start with TypeScript, Python, Go.) - **M1**: Foundation and Format
- Should the LLM extraction use the agent's current model or a dedicated cheaper model? - **M2**: Heuristic Extraction (placeholder, to be replaced by M7)
- How to handle monorepos with multiple `.gitignore` files at different depths? - **M3**: AST Extraction
- Should `.pi-map.md` files be committed to git or kept in `.gitignore`? (Recommendation: `.gitignore` — they are derived artifacts.) - **M4**: Patch and Update
- **M5**: Validation and `--fix`
- **M6**: Pi Skill Integration
+16 -4
View File
@@ -59,7 +59,11 @@ export default function (pi: ExtensionAPI) {
}), }),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) { async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const targetPath = params.path || ctx.cwd; const targetPath = params.path || ctx.cwd;
const result = runCommand("init", targetPath === ctx.cwd ? [] : [targetPath], ctx.cwd); const result = runCommand(
"init",
targetPath === ctx.cwd ? [] : [targetPath],
ctx.cwd,
);
return { return {
content: [{ type: "text", text: result.stdout || result.stderr }], content: [{ type: "text", text: result.stdout || result.stderr }],
details: { success: result.success, cwd: ctx.cwd }, details: { success: result.success, cwd: ctx.cwd },
@@ -70,7 +74,8 @@ export default function (pi: ExtensionAPI) {
pi.registerTool({ pi.registerTool({
name: "project_map_patch", name: "project_map_patch",
label: "Project Map Patch", label: "Project Map Patch",
description: "Update .pi-map.md for the directory containing a changed file", description:
"Update .pi-map.md for the directory containing a changed file",
promptSnippet: "Update project analysis after editing a source file", promptSnippet: "Update project analysis after editing a source file",
promptGuidelines: [ promptGuidelines: [
"Use project_map_patch immediately after editing any source file", "Use project_map_patch immediately after editing any source file",
@@ -108,10 +113,17 @@ export default function (pi: ExtensionAPI) {
}), }),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) { async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const targetPath = params.path || ctx.cwd; const targetPath = params.path || ctx.cwd;
const result = runCommand("validate", targetPath === ctx.cwd ? [] : [targetPath], ctx.cwd); const result = runCommand(
"validate",
targetPath === ctx.cwd ? [] : [targetPath],
ctx.cwd,
);
return { return {
content: [{ type: "text", text: result.stdout || result.stderr }], content: [{ type: "text", text: result.stdout || result.stderr }],
details: { success: result.success, clean: result.stdout.includes("clean") }, details: {
success: result.success,
clean: result.stdout.includes("clean"),
},
}; };
}, },
}); });
+27 -12
View File
@@ -13,12 +13,24 @@ function printUsage() {
console.log(`${pc.bold("project-map")} — hierarchical project analysis for Pi agents console.log(`${pc.bold("project-map")} — hierarchical project analysis for Pi agents
`); `);
console.log(`${pc.bold("Usage:")}`); console.log(`${pc.bold("Usage:")}`);
console.log(` project-map ${pc.cyan("init")} [path] Generate .pi-map.md files for all directories`); console.log(
console.log(` project-map ${pc.cyan("patch")} <file> Update analysis for a changed file's directory`); ` project-map ${pc.cyan("init")} [path] Generate .pi-map.md files for all directories`,
console.log(` project-map ${pc.cyan("validate")} [--fix] [path] Check for stale/missing/orphaned entries`); );
console.log(` project-map ${pc.cyan("reinit")} [path] Force full regeneration`); console.log(
console.log(` project-map ${pc.cyan("--help")} Show this help message`); ` project-map ${pc.cyan("patch")} <file> Update analysis for a changed file's directory`,
console.log(` project-map ${pc.cyan("--version")} Show version\n`); );
console.log(
` project-map ${pc.cyan("validate")} [--fix] [path] Check for stale/missing/orphaned entries`,
);
console.log(
` project-map ${pc.cyan("reinit")} [path] Force full regeneration`,
);
console.log(
` project-map ${pc.cyan("--help")} Show this help message`,
);
console.log(
` project-map ${pc.cyan("--version")} Show version\n`,
);
console.log(`${pc.bold("Examples:")}`); console.log(`${pc.bold("Examples:")}`);
console.log(` project-map init`); console.log(` project-map init`);
console.log(` project-map patch src/components/Button.tsx`); console.log(` project-map patch src/components/Button.tsx`);
@@ -32,10 +44,9 @@ function printVersion() {
} }
function formatCount(count: number, label: string): string { function formatCount(count: number, label: string): string {
const plural = const plural = label.endsWith("y")
label.endsWith("y") ? `${label.slice(0, -1)}ies`
? `${label.slice(0, -1)}ies` : `${label}${count === 1 ? "" : "s"}`;
: `${label}${count === 1 ? "" : "s"}`;
return `${pc.bold(String(count))} ${count === 1 ? label : plural}`; return `${pc.bold(String(count))} ${count === 1 ? label : plural}`;
} }
@@ -78,7 +89,9 @@ async function main() {
} }
case "patch": { case "patch": {
if (!args[1]) { if (!args[1]) {
console.error(`${pc.red("Error:")} Missing file path. Usage: project-map patch <file>`); console.error(
`${pc.red("Error:")} Missing file path. Usage: project-map patch <file>`,
);
process.exit(1); process.exit(1);
} }
await patchFile(args[1]); await patchFile(args[1]);
@@ -113,7 +126,9 @@ async function main() {
const targetPath = args[1] || "."; const targetPath = args[1] || ".";
const start = Date.now(); const start = Date.now();
const entries = discoverProject(targetPath); const entries = discoverProject(targetPath);
console.log(`Regenerating ${formatCount(entries.length, ".pi-map.md file")}...`); console.log(
`Regenerating ${formatCount(entries.length, ".pi-map.md file")}...`,
);
await reinitPath(targetPath, { verbose: false }); await reinitPath(targetPath, { verbose: false });
const elapsed = ((Date.now() - start) / 1000).toFixed(1); const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log(`${pc.green("✓")} Regenerated in ${elapsed}s`); console.log(`${pc.green("✓")} Regenerated in ${elapsed}s`);