@gi4nks/ph 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +147 -58
  3. package/dist/cli.js +2292 -1021
  4. package/dist/cli.js.map +1 -1
  5. package/docs/evals/context-retrieval-golden.json +26 -0
  6. package/docs/evals/context-retrieval.md +27 -0
  7. package/docs/hooks.md +13 -7
  8. package/docs/ph-analysis.html +145 -0
  9. package/docs/ph-manual.md +352 -231
  10. package/docs/specs/SPEC-001-architecture-cli.md +82 -0
  11. package/docs/specs/SPEC-002-database-layer.md +105 -0
  12. package/docs/specs/SPEC-003-capture-modes.md +110 -0
  13. package/docs/specs/SPEC-004-filter-metadata.md +82 -0
  14. package/docs/specs/SPEC-005-search.md +102 -0
  15. package/docs/specs/SPEC-006-analysis-pipeline.md +98 -0
  16. package/docs/specs/SPEC-007-memory-summaries.md +96 -0
  17. package/docs/specs/SPEC-008-embeddings.md +90 -0
  18. package/docs/specs/SPEC-009-retention-archive.md +91 -0
  19. package/docs/specs/SPEC-010-remote-sync.md +106 -0
  20. package/docs/specs/SPEC-011-mcp-server.md +94 -0
  21. package/docs/specs/SPEC-012-tui-browser.md +98 -0
  22. package/docs/specs/SPEC-013-importers.md +109 -0
  23. package/docs/specs/SPEC-014-analytics-commands.md +109 -0
  24. package/docs/specs/SPEC-015-config-env.md +85 -0
  25. package/docs/specs/SPEC-INDEX.md +35 -0
  26. package/docs/specs/SPEC-ISSUES.md +122 -0
  27. package/docs/specs/_TEMPLATE.md +65 -0
  28. package/docs/superpowers/plans/2026-06-12-ph-evolution-phase1.md +466 -0
  29. package/docs/superpowers/plans/2026-06-12-ph-evolution-phase2.md +351 -0
  30. package/docs/superpowers/specs/2026-06-12-ph-evolution-design.md +173 -0
  31. package/hooks/codex/README.md +54 -0
  32. package/hooks/codex/ph-hook.sh +14 -0
  33. package/hooks/opencode/ph-plugin.ts +60 -57
  34. package/package.json +13 -12
@@ -0,0 +1,466 @@
1
+ # Phase 1: Memory Merging — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax.
4
+
5
+ **Goal:** Create `project_summaries` table (1 row per project, merged/deduped knowledge), migrate existing `memories` into it, update analysis pipeline and MCP tools to use it.
6
+
7
+ **Architecture:** New table `project_summaries` alongside existing `memories`. Analysis writes to BOTH tables (append-only `memories` for timeline, merged `project_summaries` for curated knowledge). MCP reads from `project_summaries` by default. One-time migration aggregates existing per-project memories into single rows.
8
+
9
+ **Tech Stack:** TypeScript 5.9 ESM, better-sqlite3, sqlite-vec
10
+
11
+ ## Global Constraints
12
+
13
+ - ESM only — all imports use `.js` extension
14
+ - `better-sqlite3` for DB operations (synchronous API)
15
+ - All new methods on `PhDB` class in `src/db/index.ts`
16
+ - Follow existing patterns: snake_case column names, camelCase JS methods
17
+ - Existing `memories` table and `MemoryEntry` type remain unchanged
18
+ - Build passes after every task: `npm run build`
19
+
20
+ ---
21
+ ### Task 1: `ProjectSummary` type + `project_summaries` table schema + auto-migration
22
+
23
+ **Files:**
24
+ - Modify: `src/types.ts` (add `ProjectSummary` interface)
25
+ - Modify: `src/db/index.ts` (add table creation in `ensureSchema`, add `PROJECT_SUMMARIES_SCHEMA`)
26
+
27
+ **Interfaces:**
28
+ - Consumes: nothing other than the existing `PhDB` constructor pattern
29
+ - Produces: `ProjectSummary` type, `project_summaries` table created on DB open
30
+
31
+ - [ ] **Step 1: Add `ProjectSummary` interface to `src/types.ts`**
32
+
33
+ Append after `MemoryEntry` interface:
34
+
35
+ ```typescript
36
+ export interface ProjectSummary {
37
+ project: string;
38
+ summary: string;
39
+ key_insights: string[];
40
+ technical_decisions: string[];
41
+ prompt_count: number;
42
+ first_analyzed: string;
43
+ last_analyzed: string;
44
+ git_context_snapshot?: string;
45
+ }
46
+ ```
47
+
48
+ - [ ] **Step 2: Add SQL CREATE + migration to `src/db/index.ts`**
49
+
50
+ At the top of the PhDB class, add the schema constant:
51
+
52
+ ```typescript
53
+ private static readonly PROJECT_SUMMARIES_SCHEMA = `
54
+ CREATE TABLE IF NOT EXISTS project_summaries (
55
+ project TEXT PRIMARY KEY,
56
+ summary TEXT NOT NULL DEFAULT '',
57
+ key_insights TEXT NOT NULL DEFAULT '[]',
58
+ technical_decisions TEXT NOT NULL DEFAULT '[]',
59
+ prompt_count INTEGER NOT NULL DEFAULT 0,
60
+ first_analyzed TEXT NOT NULL,
61
+ last_analyzed TEXT NOT NULL,
62
+ git_context_snapshot TEXT
63
+ );
64
+ `;
65
+ ```
66
+
67
+ In the `ensureSchema()` method, add after the `memories` table creation:
68
+
69
+ ```typescript
70
+ this.db.exec(PhDB.PROJECT_SUMMARIES_SCHEMA);
71
+ ```
72
+
73
+ - [ ] **Step 3: Build + verify**
74
+
75
+ Run: `npm run build`
76
+ Expected: Build succeeds
77
+
78
+ - [ ] **Step 4: Commit**
79
+
80
+ ```bash
81
+ git add src/types.ts src/db/index.ts
82
+ git commit -m "feat: add project_summaries table and ProjectSummary type"
83
+ ```
84
+
85
+ ---
86
+ ### Task 2: `upsertProjectSummary()` + `getProjectSummary()` methods on PhDB
87
+
88
+ **Files:**
89
+ - Modify: `src/db/index.ts` (add methods, add private hydrate helper)
90
+
91
+ **Interfaces:**
92
+ - Produces: `db.upsertProjectSummary(params)` — merges into existing row or inserts new; `db.getProjectSummary(project)` — returns single row or null; `db.getAllProjectsWithSummaries()` — for migration
93
+
94
+ - [ ] **Step 1: Add `upsertProjectSummary()` and `getProjectSummary()`**
95
+
96
+ Add after `getAllProjectsWithMemories()` (around line 527):
97
+
98
+ ```typescript
99
+ upsertProjectSummary(params: {
100
+ project: string;
101
+ summary: string;
102
+ key_insights: string[];
103
+ technical_decisions: string[];
104
+ git_context_snapshot?: string;
105
+ }): void {
106
+ const existing = this.getProjectSummary(params.project);
107
+ const now = new Date().toISOString();
108
+
109
+ if (existing) {
110
+ // Merge insights — dedup by string equality
111
+ const allInsights = new Set([...existing.key_insights, ...params.key_insights]);
112
+ // Merge decisions — dedup by string equality
113
+ const allDecisions = new Set([...existing.technical_decisions, ...params.technical_decisions]);
114
+
115
+ this.db.prepare(`
116
+ UPDATE project_summaries
117
+ SET summary = ?,
118
+ key_insights = ?,
119
+ technical_decisions = ?,
120
+ prompt_count = prompt_count + 1,
121
+ last_analyzed = ?,
122
+ git_context_snapshot = COALESCE(?, git_context_snapshot)
123
+ WHERE project = ?
124
+ `).run(
125
+ params.summary,
126
+ JSON.stringify([...allInsights]),
127
+ JSON.stringify([...allDecisions]),
128
+ now,
129
+ params.git_context_snapshot || null,
130
+ params.project
131
+ );
132
+ } else {
133
+ this.db.prepare(`
134
+ INSERT INTO project_summaries (project, summary, key_insights, technical_decisions, prompt_count, first_analyzed, last_analyzed, git_context_snapshot)
135
+ VALUES (?, ?, ?, ?, 1, ?, ?, ?)
136
+ `).run(
137
+ params.project,
138
+ params.summary,
139
+ JSON.stringify(params.key_insights),
140
+ JSON.stringify(params.technical_decisions),
141
+ now,
142
+ now,
143
+ params.git_context_snapshot || null
144
+ );
145
+ }
146
+ }
147
+
148
+ getProjectSummary(project: string): ProjectSummary | null {
149
+ const row = this.db.prepare('SELECT * FROM project_summaries WHERE project = ?').get(project) as Record<string, unknown> | undefined;
150
+ if (!row) return null;
151
+ return this.hydrateProjectSummary(row);
152
+ }
153
+
154
+ getAllProjectsWithSummaries(): string[] {
155
+ const rows = this.db.prepare('SELECT project FROM project_summaries ORDER BY project').all() as { project: string }[];
156
+ return rows.map(r => r.project);
157
+ }
158
+ ```
159
+
160
+ - [ ] **Step 2: Add `hydrateProjectSummary()` private method**
161
+
162
+ Add after `hydrateMemory()`:
163
+
164
+ ```typescript
165
+ private hydrateProjectSummary(row: Record<string, unknown>): ProjectSummary {
166
+ return {
167
+ project: row.project as string,
168
+ summary: row.summary as string,
169
+ key_insights: JSON.parse(row.key_insights as string),
170
+ technical_decisions: JSON.parse(row.technical_decisions as string),
171
+ prompt_count: row.prompt_count as number,
172
+ first_analyzed: row.first_analyzed as string,
173
+ last_analyzed: row.last_analyzed as string,
174
+ git_context_snapshot: row.git_context_snapshot as string | undefined,
175
+ };
176
+ }
177
+ ```
178
+
179
+ - [ ] **Step 3: Build + verify**
180
+
181
+ Run: `npm run build`
182
+ Expected: Build succeeds
183
+
184
+ - [ ] **Step 4: Commit**
185
+
186
+ ```bash
187
+ git add src/db/index.ts
188
+ git commit -m "feat: add upsertProjectSummary and getProjectSummary to PhDB"
189
+ ```
190
+
191
+ ---
192
+ ### Task 3: One-time migration command — merge existing memories into summaries
193
+
194
+ **Files:**
195
+ - Create: `src/commands/memory-migrate.ts`
196
+ - Modify: `src/cli.ts` (register `memory-migrate` command)
197
+
198
+ **Interfaces:**
199
+ - Consumes: `db.upsertProjectSummary()`, `db.searchMemories()` — already exist
200
+ - Produces: `ph memory-migrate` CLI command that aggregates all existing memories into project_summaries
201
+
202
+ - [ ] **Step 1: Create `src/commands/memory-migrate.ts`**
203
+
204
+ ```typescript
205
+ import { PhDB } from '../db/index.js';
206
+
207
+ export function cmdMemoryMigrate(db: PhDB): void {
208
+ const projects = db.getAllProjectsWithMemories();
209
+ if (projects.length === 0) {
210
+ console.log('No memories found. Nothing to migrate.');
211
+ return;
212
+ }
213
+
214
+ let count = 0;
215
+ for (const project of projects) {
216
+ const memories = db.searchMemories(project, 1000);
217
+ const allInsights = new Set<string>();
218
+ const allDecisions = new Set<string>();
219
+ let lastSummary = '';
220
+ let gitCtx: string | undefined;
221
+
222
+ for (const mem of memories) {
223
+ if (mem.summary) lastSummary = mem.summary;
224
+ for (const i of mem.key_insights) allInsights.add(i);
225
+ for (const d of mem.technical_decisions) allDecisions.add(d);
226
+ if (mem.git_context_snapshot) gitCtx = mem.git_context_snapshot;
227
+ }
228
+
229
+ db.upsertProjectSummary({
230
+ project,
231
+ summary: lastSummary,
232
+ key_insights: [...allInsights],
233
+ technical_decisions: [...allDecisions],
234
+ git_context_snapshot: gitCtx,
235
+ });
236
+ count++;
237
+ }
238
+
239
+ console.log(`Migrated ${count} project(s) into project_summaries.`);
240
+ }
241
+ ```
242
+
243
+ - [ ] **Step 2: Register `memory-migrate` in `src/cli.ts`**
244
+
245
+ Add import at top:
246
+ ```typescript
247
+ import { cmdMemoryMigrate } from './commands/memory-migrate.js';
248
+ ```
249
+
250
+ Add to usage string (after `mcp` line):
251
+ ```
252
+ ph memory-migrate Merge existing memories into project_summaries
253
+ ```
254
+
255
+ Add case before `case 'mcp'`:
256
+ ```typescript
257
+ case 'memory-migrate':
258
+ cmdMemoryMigrate(db);
259
+ break;
260
+ ```
261
+
262
+ - [ ] **Step 3: Build + verify**
263
+
264
+ Run: `npm run build`
265
+ Expected: Build succeeds
266
+
267
+ - [ ] **Step 4: Run migration**
268
+
269
+ Run: `node dist/cli.js memory-migrate`
270
+ Expected: "Migrated X project(s) into project_summaries."
271
+
272
+ - [ ] **Step 5: Commit**
273
+
274
+ ```bash
275
+ git add src/commands/memory-migrate.ts src/cli.ts
276
+ git commit -m "feat: add ph memory-migrate command"
277
+ ```
278
+
279
+ ---
280
+ ### Task 4: Update analysis pipeline to write to BOTH tables
281
+
282
+ **Files:**
283
+ - Modify: `src/analyzer/index.ts` (update `analyzeAll`)
284
+ - Modify: `src/commands/background-analyze.ts` (update `cmdBackgroundAnalyze`)
285
+
286
+ **Interfaces:**
287
+ - Consumes: `db.upsertProjectSummary()` from Task 2
288
+ - Produces: After each analysis, both `memories` (append-only) AND `project_summaries` (merged) get updated
289
+
290
+ - [ ] **Step 1: Update `analyzeAll` in `src/analyzer/index.ts`**
291
+
292
+ Find the block around line 225-234 where `db.upsertProjectMemory()` is called. Add `db.upsertProjectSummary()` call after it:
293
+
294
+ ```typescript
295
+ if (result.project && result.summary) {
296
+ db.upsertProjectMemory({
297
+ project: result.project,
298
+ prompt_id: entry.id,
299
+ summary: result.summary,
300
+ key_insights: result.key_insights ?? [],
301
+ technical_decisions: result.technical_decisions ?? [],
302
+ });
303
+ // Also update the merged project summary
304
+ db.upsertProjectSummary({
305
+ project: result.project,
306
+ summary: result.summary,
307
+ key_insights: result.key_insights ?? [],
308
+ technical_decisions: result.technical_decisions ?? [],
309
+ });
310
+ }
311
+ ```
312
+
313
+ - [ ] **Step 2: Update `cmdBackgroundAnalyze` in `src/commands/background-analyze.ts`**
314
+
315
+ Find the block around line 36-43 where `db.upsertProjectMemory()` is called. Add `db.upsertProjectSummary()` after it:
316
+
317
+ ```typescript
318
+ if (result.project && result.summary) {
319
+ db.upsertProjectMemory({
320
+ project: result.project,
321
+ prompt_id: id,
322
+ summary: result.summary,
323
+ key_insights: result.key_insights ?? [],
324
+ technical_decisions: result.technical_decisions ?? [],
325
+ });
326
+ db.upsertProjectSummary({
327
+ project: result.project,
328
+ summary: result.summary,
329
+ key_insights: result.key_insights ?? [],
330
+ technical_decisions: result.technical_decisions ?? [],
331
+ });
332
+ }
333
+ ```
334
+
335
+ - [ ] **Step 3: Build + verify**
336
+
337
+ Run: `npm run build`
338
+ Expected: Build succeeds
339
+
340
+ - [ ] **Step 4: Commit**
341
+
342
+ ```bash
343
+ git add src/analyzer/index.ts src/commands/background-analyze.ts
344
+ git commit -m "feat: write to project_summaries from analysis pipeline"
345
+ ```
346
+
347
+ ---
348
+ ### Task 5: Update MCP tools to read from `project_summaries`
349
+
350
+ **Files:**
351
+ - Modify: `src/mcp/server.ts` (update `get_project_summary` and `get_project_context` tools)
352
+
353
+ **Interfaces:**
354
+ - Consumes: `db.getProjectSummary()` from Task 2
355
+ - Produces: MCP tools return merged knowledge from `project_summaries` instead of raw `memories`
356
+
357
+ - [ ] **Step 1: Update `get_project_summary` tool handler**
358
+
359
+ Replace the current implementation (reads from `memories`) with code that reads from `project_summaries`:
360
+
361
+ ```typescript
362
+ if (name === "get_project_summary") {
363
+ const { project } = z.object({
364
+ project: z.string(),
365
+ }).parse(args);
366
+
367
+ const summary = db.getProjectSummary(project);
368
+
369
+ if (!summary) {
370
+ return {
371
+ content: [{ type: "text", text: `No accumulated knowledge for project "${project}". Run "ph analyze" to generate insights.` }],
372
+ };
373
+ }
374
+
375
+ const parts: string[] = [];
376
+ parts.push(`## ${summary.summary || 'Project Memory'}\n`);
377
+ parts.push(`Based on ${summary.prompt_count} interactions.\n`);
378
+ if (summary.key_insights.length > 0) {
379
+ parts.push('**Key Insights:**');
380
+ for (const i of summary.key_insights) parts.push(`- ${i}`);
381
+ parts.push('');
382
+ }
383
+ if (summary.technical_decisions.length > 0) {
384
+ parts.push('**Technical Decisions:**');
385
+ for (const d of summary.technical_decisions) parts.push(`- ${d}`);
386
+ parts.push('');
387
+ }
388
+
389
+ return {
390
+ content: [{ type: "text", text: parts.join('\n') }],
391
+ };
392
+ }
393
+ ```
394
+
395
+ - [ ] **Step 2: Update `get_project_context` tool handler**
396
+
397
+ Modify to also include merged summary from `project_summaries`. Around line 107-143, before reading memories, check for merged summary:
398
+
399
+ ```typescript
400
+ if (name === "get_project_context") {
401
+ const { project, limit = 10 } = z.object({
402
+ project: z.string(),
403
+ limit: z.number().optional(),
404
+ }).parse(args);
405
+
406
+ const parts: string[] = [];
407
+
408
+ // Start with merged project summary if available
409
+ const merged = db.getProjectSummary(project);
410
+ if (merged) {
411
+ parts.push('## Project Knowledge (merged)\n');
412
+ if (merged.summary) parts.push(`${merged.summary}\n`);
413
+ if (merged.key_insights.length > 0) {
414
+ parts.push('Key Insights:');
415
+ for (const i of merged.key_insights) parts.push(` - ${i}`);
416
+ parts.push('');
417
+ }
418
+ if (merged.technical_decisions.length > 0) {
419
+ parts.push('Technical Decisions:');
420
+ for (const d of merged.technical_decisions) parts.push(` - ${d}`);
421
+ parts.push('');
422
+ }
423
+ }
424
+
425
+ // Also pull recent memories (detailed entries)
426
+ const memories = db.searchMemories(project, 3);
427
+ if (memories.length > 0) {
428
+ if (parts.length > 0) parts.push('---\n');
429
+ parts.push('## Recent Memory Entries\n');
430
+ for (const mem of memories) {
431
+ if (mem.summary) parts.push(`- ${mem.summary}`);
432
+ }
433
+ parts.push('');
434
+ }
435
+
436
+ const prompts = db.getProjectMemory(project, limit);
437
+ if (prompts.length > 0) {
438
+ if (parts.length > 0) parts.push('---\n');
439
+ parts.push('## Recent Interactions\n');
440
+ parts.push(formatResultsAsMarkdown(prompts));
441
+ }
442
+
443
+ return {
444
+ content: [{ type: "text", text: parts.join('\n') || 'No context found for this project.' }],
445
+ };
446
+ }
447
+ ```
448
+
449
+ - [ ] **Step 3: Build + verify**
450
+
451
+ Run: `npm run build`
452
+ Expected: Build succeeds
453
+
454
+ - [ ] **Step 4: Commit**
455
+
456
+ ```bash
457
+ git add src/mcp/server.ts
458
+ git commit -m "feat: MCP tools read from project_summaries"
459
+ ```
460
+
461
+ ---
462
+ ## Self-Review Checklist
463
+
464
+ - **Spec coverage:** All Phase 1 items covered: table creation complete, upsert/get methods complete, migration complete, analysis pipeline updates complete, MCP tool updates complete
465
+ - **Placeholder scan:** No TBD, TODO, or incomplete code blocks
466
+ - **Type consistency:** `ProjectSummary` interface consistent across all tasks: `project`, `summary`, `key_insights[]`, `technical_decisions[]`, `prompt_count`, `first_analyzed`, `last_analyzed`, `git_context_snapshot?`