@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,351 @@
1
+ # Phase 2: Prompt Retention & Archiving — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` or `executing-plans` to implement this plan task-by-task.
4
+
5
+ **Goal:** Add automatic retention rules and archiving mechanism so ph never grows unbounded. Old/low-quality prompts get moved to `prompts_archive` instead of deleted.
6
+
7
+ **Architecture:** New `prompts_archive` table with same schema as `prompts`. `ph cleanup --retention` scans prompts, applies rules (age, starred, analyzed, relevance), moves qualifying entries to archive. Retention config keys in `PhConfig`.
8
+
9
+ **Tech Stack:** TypeScript 5.9 ESM, better-sqlite3
10
+
11
+ ## Global Constraints
12
+ - ESM only — all imports use `.js` extension
13
+ - `better-sqlite3` synchronous API
14
+ - `prompts` and `prompts_archive` have identical schema
15
+ - Build passes after every task: `npm run build`
16
+ - Archive is append-only (no deletion from archive unless `purgeArchive()` is called)
17
+
18
+ ---
19
+ ### Task 1: `prompts_archive` table + retention config keys + PhDB archive methods
20
+
21
+ **Files:**
22
+ - Modify: `src/config/index.ts` (add retention keys to `PhConfig`)
23
+ - Modify: `src/db/index.ts` (add archive table schema, archive/purge/search methods)
24
+
25
+ **Interfaces:**
26
+ - Produces: `db.archivePrompts(ids)`, `db.searchArchive(options)`, `db.purgeArchive(beforeDate)`, `db.getArchiveStats()` — all on PhDB
27
+ - Produces: retention config keys in `PhConfig`
28
+
29
+ - [ ] **Step 1: Add retention config keys to `PhConfig` in `src/config/index.ts`**
30
+
31
+ Add after `remoteLastPull?: string`:
32
+ ```typescript
33
+ retentionDays?: number; // default: 90 — auto-archive prompts older than N days
34
+ retentionMinStarred?: boolean; // default: true — never archive starred prompts
35
+ retentionMinAnalyzed?: boolean; // default: true — never archive analyzed prompts (has summary or role)
36
+ retentionMinRelevance?: number; // default: 3 — prompts below this relevance are archived first
37
+ ```
38
+
39
+ - [ ] **Step 2: Add archive table creation in `migrate()` in `src/db/index.ts`**
40
+
41
+ Add after `PROJECT_SUMMARIES_SCHEMA` static (around line 39):
42
+ ```typescript
43
+ private static readonly ARCHIVE_SCHEMA = `
44
+ CREATE TABLE IF NOT EXISTS prompts_archive (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ timestamp TEXT NOT NULL,
47
+ tool TEXT NOT NULL,
48
+ prompt TEXT NOT NULL,
49
+ args TEXT NOT NULL DEFAULT '',
50
+ workdir TEXT NOT NULL DEFAULT '',
51
+ hostname TEXT NOT NULL DEFAULT '',
52
+ exit_code INTEGER NOT NULL DEFAULT 0,
53
+ metadata TEXT NOT NULL DEFAULT '{}',
54
+ archived_at TEXT NOT NULL,
55
+ original_id INTEGER
56
+ );
57
+ CREATE INDEX IF NOT EXISTS idx_archive_timestamp ON prompts_archive(timestamp);
58
+ CREATE INDEX IF NOT EXISTS idx_archive_original_id ON prompts_archive(original_id);
59
+ `;
60
+ ```
61
+
62
+ In `migrate()` after line 104 (`this.db.exec(PhDB.PROJECT_SUMMARIES_SCHEMA);`):
63
+ ```typescript
64
+ this.db.exec(PhDB.ARCHIVE_SCHEMA);
65
+ ```
66
+
67
+ - [ ] **Step 3: Add archive methods to PhDB**
68
+
69
+ Add after the `deleteOlderThan` method (around line 439):
70
+
71
+ ```typescript
72
+ archivePrompts(ids: number[]): number {
73
+ if (ids.length === 0) return 0;
74
+ const now = new Date().toISOString();
75
+ const select = this.db.prepare(`
76
+ SELECT id, timestamp, tool, prompt, args, workdir, hostname, exit_code, metadata
77
+ FROM prompts WHERE id = ?
78
+ `);
79
+ const insert = this.db.prepare(`
80
+ INSERT INTO prompts_archive (timestamp, tool, prompt, args, workdir, hostname, exit_code, metadata, archived_at, original_id)
81
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
82
+ `);
83
+ const del = this.db.prepare('DELETE FROM prompts WHERE id = ?');
84
+
85
+ const transaction = this.db.transaction((toArchive: number[]) => {
86
+ let count = 0;
87
+ for (const id of toArchive) {
88
+ const row = select.get(id) as Record<string, unknown> | undefined;
89
+ if (!row) continue;
90
+ insert.run(
91
+ row.timestamp, row.tool, row.prompt, row.args,
92
+ row.workdir, row.hostname, row.exit_code, row.metadata,
93
+ now, id
94
+ );
95
+ del.run(id);
96
+ count++;
97
+ }
98
+ return count;
99
+ });
100
+
101
+ return transaction(ids);
102
+ }
103
+
104
+ searchArchive(opts: { since?: string; until?: string; limit?: number }): Array<Record<string, unknown>> {
105
+ let sql = 'SELECT * FROM prompts_archive WHERE 1=1';
106
+ const params: unknown[] = [];
107
+ if (opts.since) { sql += ' AND timestamp >= ?'; params.push(opts.since); }
108
+ if (opts.until) { sql += ' AND timestamp <= ?'; params.push(opts.until); }
109
+ sql += ' ORDER BY timestamp DESC';
110
+ if (opts.limit) { sql += ' LIMIT ?'; params.push(opts.limit); }
111
+ return this.db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
112
+ }
113
+
114
+ purgeArchive(beforeDate: string): number {
115
+ const result = this.db.prepare('DELETE FROM prompts_archive WHERE archived_at < ?').run(beforeDate);
116
+ return result.changes;
117
+ }
118
+
119
+ getArchiveStats(): { total: number; oldest: string | null; newest: string | null } {
120
+ const row = this.db.prepare(`
121
+ SELECT COUNT(*) as total, MIN(timestamp) as oldest, MAX(timestamp) as newest
122
+ FROM prompts_archive
123
+ `).get() as { total: number; oldest: string | null; newest: string | null };
124
+ return row;
125
+ }
126
+ ```
127
+
128
+ - [ ] **Step 4: Build + verify**
129
+
130
+ Run: `npm run build`
131
+ Expected: Build succeeds
132
+
133
+ - [ ] **Step 5: Commit**
134
+
135
+ ```bash
136
+ git add src/config/index.ts src/db/index.ts
137
+ git commit -m "feat: add prompts_archive table, retention config keys, archive methods"
138
+ ```
139
+
140
+ ---
141
+ ### Task 2: `ph cleanup --retention` logic
142
+
143
+ **Files:**
144
+ - Modify: `src/commands/cleanup.ts` (add `--retention` handler)
145
+
146
+ **Interfaces:**
147
+ - Consumes: `db.archivePrompts()`, `db.searchArchive()` — from Task 1
148
+ - Produces: `ph cleanup --retention` archives prompts per retention rules
149
+
150
+ - [ ] **Step 1: Add retention logic to `cmdCleanup` in `src/commands/cleanup.ts`**
151
+
152
+ Replace the current implementation with one that handles `--retention`. Current file is 77 lines.
153
+
154
+ The retention logic:
155
+ 1. If `--retention` flag is set, archive prompts matching retention rules
156
+ 2. Otherwise run existing rule-based cleanup
157
+
158
+ Full replacement for `cmdCleanup`:
159
+
160
+ ```typescript
161
+ import { PhDB } from '../db/index.js';
162
+ import type { PhConfig } from '../config/index.js';
163
+ import type { PromptMetadata } from '../types.js';
164
+ import { FilterPipeline } from '../filter/index.js';
165
+ import { parseFlags } from './_utils.js';
166
+
167
+ export async function cmdCleanup(db: PhDB, cfg: PhConfig, args: string[]): Promise<void> {
168
+ const { flags } = parseFlags(args);
169
+ const dryRun = Boolean(flags['dry-run']);
170
+ const retention = Boolean(flags['retention']);
171
+
172
+ if (retention) {
173
+ await runRetention(db, cfg, dryRun);
174
+ return;
175
+ }
176
+
177
+ // Original rule-based cleanup
178
+ const minLength = flags['min-length'] ? Number(flags['min-length']) : (cfg.filterMinLength ?? 15);
179
+ const minScore = flags['min-score'] ? Number(flags['min-score']) : (cfg.filterMinRelevance ?? 3);
180
+ const days = flags['days'] ? Number(flags['days']) : undefined;
181
+
182
+ if (days !== undefined) {
183
+ if (dryRun) {
184
+ console.log(`(dry-run) Would delete prompts older than ${days} days.`);
185
+ } else {
186
+ const deleted = db.deleteOlderThan(days);
187
+ console.log(`Deleted ${deleted} prompts older than ${days} days.`);
188
+ }
189
+ }
190
+
191
+ const allEntries = db.search({ limit: 100000 });
192
+ console.log(`Scanning ${allEntries.length} prompts for rule-based cleanup (min-length: ${minLength}, min-score: ${minScore})...`);
193
+
194
+ const toDelete: Array<{ id: number; prompt: string; reason: string }> = [];
195
+ const existingHashes = new Map<string, number>();
196
+
197
+ const filter = new FilterPipeline({ minLength, minRelevance: 0, existingHashes });
198
+
199
+ for (const entry of allEntries) {
200
+ const ruleResult = filter.checkRules(entry.prompt);
201
+ if (!ruleResult.keep) {
202
+ toDelete.push({ id: entry.id, prompt: entry.prompt, reason: `${ruleResult.reason}: ${ruleResult.details ?? ''}` });
203
+ continue;
204
+ }
205
+
206
+ const hash = FilterPipeline.hashPrompt(entry.prompt);
207
+ if (existingHashes.has(hash)) {
208
+ const dupId = existingHashes.get(hash);
209
+ toDelete.push({ id: entry.id, prompt: entry.prompt, reason: `exact_duplicate of #${dupId}` });
210
+ continue;
211
+ }
212
+ existingHashes.set(hash, entry.id);
213
+
214
+ if (minScore > 0) {
215
+ let meta: PromptMetadata = {};
216
+ try { meta = JSON.parse(entry.metadata) as PromptMetadata; } catch {}
217
+ if (meta.relevance !== undefined && meta.relevance < minScore) {
218
+ toDelete.push({ id: entry.id, prompt: entry.prompt, reason: `low_relevance: ${meta.relevance} < ${minScore}` });
219
+ }
220
+ }
221
+ }
222
+
223
+ if (toDelete.length === 0) {
224
+ if (days === undefined) console.log('Nothing else to clean up.');
225
+ return;
226
+ }
227
+
228
+ console.log(`\nCandidates for rule-based deletion: ${toDelete.length}`);
229
+ for (const item of toDelete.slice(0, 30)) {
230
+ const short = item.prompt.replace(/\n/g, ' ').slice(0, 60);
231
+ console.log(` #${String(item.id).padEnd(5)} [${item.reason}] "${short}"`);
232
+ }
233
+ if (toDelete.length > 30) {
234
+ console.log(` ... and ${toDelete.length - 30} more`);
235
+ }
236
+
237
+ if (dryRun) {
238
+ console.log(`\n(dry-run) Would delete ${toDelete.length} more prompts. Run without --dry-run to apply.`);
239
+ return;
240
+ }
241
+
242
+ const ids = toDelete.map(e => e.id);
243
+ const deleted = db.deleteByIds(ids);
244
+ console.log(`\nDeleted ${deleted} prompts.`);
245
+ }
246
+
247
+ async function runRetention(db: PhDB, cfg: PhConfig, dryRun: boolean): Promise<void> {
248
+ const retentionDays = cfg.retentionDays ?? 90;
249
+ const minStarred = cfg.retentionMinStarred ?? true;
250
+ const minAnalyzed = cfg.retentionMinAnalyzed ?? true;
251
+ const minRelevance = cfg.retentionMinRelevance ?? 3;
252
+
253
+ const cutoff = new Date();
254
+ cutoff.setDate(cutoff.getDate() - retentionDays);
255
+ const cutoffStr = cutoff.toISOString();
256
+
257
+ const allEntries = db.search({ limit: 100000 });
258
+ const stats = db.getArchiveStats();
259
+
260
+ console.log(`Retention policy: ${retentionDays} days, minRelevance=${minRelevance}, keepStarred=${minStarred}, keepAnalyzed=${minAnalyzed}`);
261
+ console.log(`Archive: ${stats.total} entries (${stats.oldest ? `oldest ${stats.oldest}` : 'empty'})`);
262
+ console.log(`Scanning ${allEntries.length} prompts...\n`);
263
+
264
+ // Priority 1: relevance < threshold AND age > retentionDays → archive
265
+ // Priority 2: age > retentionDays AND not starred AND not analyzed → archive
266
+ const toArchive: Array<{ id: number; prompt: string; reason: string; meta: PromptMetadata }> = [];
267
+
268
+ for (const entry of allEntries) {
269
+ let meta: PromptMetadata = {};
270
+ try { meta = JSON.parse(entry.metadata) as PromptMetadata; } catch {}
271
+
272
+ const age = entry.timestamp;
273
+ const isOld = age < cutoffStr;
274
+ const isStarred = meta.starred === true;
275
+ const isAnalyzed = Boolean(meta.summary || meta.role);
276
+ const relevance = meta.relevance ?? 5;
277
+
278
+ if (!isOld) continue;
279
+
280
+ // Priority 1: low relevance + old
281
+ if (relevance < minRelevance) {
282
+ toArchive.push({ id: entry.id, prompt: entry.prompt, reason: `low_relevance(${relevance})`, meta });
283
+ continue;
284
+ }
285
+
286
+ // Priority 2: old + not protected
287
+ if (!(minStarred && isStarred) && !(minAnalyzed && isAnalyzed)) {
288
+ toArchive.push({ id: entry.id, prompt: entry.prompt, reason: `old(>${retentionDays}d)`, meta });
289
+ }
290
+ }
291
+
292
+ if (toArchive.length === 0) {
293
+ console.log('No prompts match retention criteria.');
294
+ return;
295
+ }
296
+
297
+ console.log(`Candidates for archiving: ${toArchive.length}`);
298
+ for (const item of toArchive.slice(0, 20)) {
299
+ const short = item.prompt.replace(/\n/g, ' ').slice(0, 60);
300
+ const starred = item.meta.starred ? ' [starred]' : '';
301
+ const analyzed = item.meta.summary ? ' [analyzed]' : '';
302
+ console.log(` #${String(item.id).padEnd(5)} [${item.reason}]${starred}${analyzed} "${short}"`);
303
+ }
304
+ if (toArchive.length > 20) {
305
+ console.log(` ... and ${toArchive.length - 20} more`);
306
+ }
307
+
308
+ if (dryRun) {
309
+ console.log(`\n(dry-run) Would archive ${toArchive.length} prompts. Run without --dry-run to apply.`);
310
+ return;
311
+ }
312
+
313
+ const ids = toArchive.map(e => e.id);
314
+ const archived = db.archivePrompts(ids);
315
+ console.log(`\nArchived ${archived} prompts.`);
316
+
317
+ // Auto-purge: archived entries older than 2x retentionDays
318
+ const purgeCutoff = new Date();
319
+ purgeCutoff.setDate(purgeCutoff.getDate() - retentionDays * 2);
320
+ const purged = db.purgeArchive(purgeCutoff.toISOString());
321
+ if (purged > 0) {
322
+ console.log(`Purged ${purged} archived entries older than ${retentionDays * 2} days.`);
323
+ }
324
+ }
325
+ ```
326
+
327
+ - [ ] **Step 2: Update USAGE in `src/cli.ts`**
328
+
329
+ Find the `ph cleanup` line (around line 59) and update to include `--retention`:
330
+
331
+ Edit from:
332
+ ```
333
+ ph cleanup [--dry-run] [--min-length N] [--min-score N] Remove useless prompts
334
+ ```
335
+ To:
336
+ ```
337
+ ph cleanup [--dry-run] [--min-length N] [--min-score N] Remove useless prompts
338
+ ph cleanup --retention [--dry-run] Archive prompts per retention policy
339
+ ```
340
+
341
+ - [ ] **Step 3: Build + verify**
342
+
343
+ Run: `npm run build`
344
+ Expected: Build succeeds
345
+
346
+ - [ ] **Step 4: Commit**
347
+
348
+ ```bash
349
+ git add src/commands/cleanup.ts src/cli.ts
350
+ git commit -m "feat: add ph cleanup --retention with archive logic"
351
+ ```
@@ -0,0 +1,173 @@
1
+ # ph Evolution: Curated Knowledge System
2
+
3
+ **Date:** 2026-06-12
4
+ **Status:** Design approved
5
+
6
+ ## Vision
7
+
8
+ ph evolves from a passive logger into a **curated knowledge base** for AI-assisted development. It keeps only distilled, meaningful information — condensed memories and significant prompts — while automatically pruning noise. The MCP server becomes the primary interface for agents; the TUI/CLI remain for exploration and management.
9
+
10
+ **Core principle:** *ph should shrink, not grow.* Every week the knowledge becomes more compact and more valuable, not larger and noisier.
11
+
12
+ ## Problem Statement
13
+
14
+ ### Current Issues
15
+ 1. **Append-only memories**: each analysis creates a new row in `memories`. Same project → 10+ near-identical rows saying the same thing.
16
+ 2. **Unlimited prompt storage**: every captured prompt lives forever. Signal-to-noise ratio degrades daily.
17
+ 3. **No retention policy**: starred and analyzed prompts are treated identically to throwaway interactions.
18
+ 4. **MCP is an afterthought**: the most valuable interface (agent-facing) is treated as "one more tool" instead of the primary consumer.
19
+
20
+ ### Desired State (3 months)
21
+ - ph's database is **stable in size** (~a few MB), not growing linearly with usage
22
+ - Each project has **one curated summary** with accumulated key insights and technical decisions
23
+ - Old/irrelevant prompts are **automatically archived** after configurable TTL
24
+ - Agents get **relevant, compact context** via MCP — no noise, no duplicates
25
+ - The user **never worries about bloat** — it's self-maintaining
26
+
27
+ ## Design
28
+
29
+ ### 1. Memory Merging (Hybrid Model)
30
+
31
+ Two levels of memory per project:
32
+
33
+ #### 1a. `project_summary` — Single curated row per project
34
+
35
+ | Column | Description |
36
+ |--------|-------------|
37
+ | `project` | Project name (PK) |
38
+ | `summary` | Cumulated project description (concatenated + condensed) |
39
+ | `key_insights` | Merged JSON array of unique insights |
40
+ | `technical_decisions` | Merged JSON array of unique decisions |
41
+ | `prompt_count` | Number of prompts that contributed |
42
+ | `first_analyzed` | Timestamp of first analysis |
43
+ | `last_analyzed` | Timestamp of last update |
44
+ | `git_context` | Latest git context snapshot |
45
+
46
+ **Behavior:**
47
+ - `upsertProjectSummary()`:
48
+ - Look up existing row by `project`
49
+ - If found: merge `key_insights` (dedup by similarity threshold), merge `technical_decisions` (dedup), update `summary` (newest wins or concatenate), increment `prompt_count`, update `last_analyzed`
50
+ - If not found: insert new row
51
+ - This is a **new table** (`project_summaries`), keeping existing `memories` table intact
52
+
53
+ #### 1b. `memories` — Keep append-only for timeline, but add TTL
54
+
55
+ Existing `memories` table remains append-only for detailed history. But entries older than N days without access get pruned.
56
+
57
+ #### Migration
58
+ - Create `project_summaries` table
59
+ - Run one-time merge: for each project in `memories`, aggregate all entries into one `project_summaries` row
60
+ - New analyses write to BOTH `memories` AND `project_summaries`
61
+ - After migration, MCP tools read from `project_summaries` by default
62
+
63
+ ### 2. Prompt Pruning & Retention
64
+
65
+ #### Retention Rules (configurable via `ph config`)
66
+
67
+ | Config key | Default | Description |
68
+ |-----------|---------|-------------|
69
+ | `retentionDays` | 90 | Auto-archive prompts older than N days |
70
+ | `retentionMinStarred` | true | Starred prompts are never auto-archived |
71
+ | `retentionMinAnalyzed` | true | Analyzed prompts are never auto-archived |
72
+ | `retentionMinRelevance` | 3 | Prompts with relevance < N are pruned first |
73
+
74
+ #### Archive mechanism
75
+
76
+ - New table: `prompts_archive` (same schema as `prompts`)
77
+ - `ph cleanup --retention` moves old/unqualified prompts to `prompts_archive`
78
+ - Archive can be queried with `ph search --archive`
79
+ - Archive can be purged with `ph vacuum --purge-archive`
80
+ - A background TUI indicator shows when archiving is needed
81
+
82
+ #### Pruning priority (ph cleanup run order)
83
+
84
+ 1. Duplicate prompts (exact content match within 7 days) → delete
85
+ 2. Relevance < `retentionMinRelevance` AND age > `retentionDays` → archive
86
+ 3. Age > `retentionDays` AND not starred AND not analyzed → archive
87
+ 4. Archived entries older than 2x `retentionDays` → auto-purge (configurable)
88
+
89
+ ### 3. MCP as Primary Interface
90
+
91
+ #### New MCP Tools
92
+
93
+ | Tool | Description |
94
+ |------|-------------|
95
+ | `check_project_knowledge` | Before implementing: returns what ph knows (merged summary + semantic search). Returns "no knowledge" if none found. |
96
+ | `save_decision` | Agents call this after making a technical decision. Writes directly to `project_summaries`. |
97
+
98
+ #### Enhanced existing tools
99
+
100
+ - `get_project_summary` now reads from `project_summaries` (merged knowledge)
101
+ - `search_project_memory` falls back to `project_summaries` if no semantic results
102
+ - `get_project_context` returns merged summary + recent prompts from last 7 days
103
+
104
+ ### 4. CLI Commands
105
+
106
+ | Command | Description |
107
+ |---------|-------------|
108
+ | `ph cleanup --retention` | Archive prompts per retention policy |
109
+ | `ph cleanup --merge-memories` | One-time merge all `memories` → `project_summaries` |
110
+ | `ph memory-migrate` | Create `project_summaries` table + merge existing data |
111
+ | `ph config set retentionDays 60` | Configure retention |
112
+
113
+ ### 5. TUI Enhancements
114
+
115
+ - Settings panel (already done) → add retention config fields
116
+ - Status indicator in header: "142 prompts (23 archived)"
117
+ - Filter option: `--archived` or view archived prompts
118
+
119
+ ## Data Flow
120
+
121
+ ```
122
+ Hook/Import → ph log → prompts table
123
+ ↓
124
+ [background analysis]
125
+ ↓
126
+ ┌────────────────┴────────────────┐
127
+ ↓ ↓
128
+ memories (append-only, project_summaries
129
+ with TTL) (single row per project,
130
+ cumulated + deduped)
131
+ ↓ ↓
132
+ [ph cleanup --retention] MCP tools read
133
+ → prompts_archive from here by default
134
+ ```
135
+
136
+ ## Migration Path
137
+
138
+ ### Phase 1: Foundation (now)
139
+ 1. Create `project_summaries` table in PhDB
140
+ 2. Add `upsertProjectSummary()` method
141
+ 3. Run one-time merge: for each project, aggregate all `memories` rows into one `project_summaries` row
142
+ 4. Update `_bg-analyze` and `analyzeAll` to write to BOTH tables
143
+
144
+ ### Phase 2: Retention (next)
145
+ 1. Create `prompts_archive` table
146
+ 2. Add retention logic to `ph cleanup`
147
+ 3. Add `retentionDays`, `retentionMinStarred`, `retentionMinAnalyzed`, `retentionMinRelevance` to config
148
+ 4. Wire into `_bg-analyze` for post-analysis archive check
149
+
150
+ ### Phase 3: MCP Evolution
151
+ 1. Add `check_project_knowledge` tool
152
+ 2. Add `save_decision` tool
153
+ 3. Update existing tools to read from `project_summaries`
154
+
155
+ ### Phase 4: Polish
156
+ 1. TUI archive indicator
157
+ 2. `ph search --archive` support
158
+ 3. Auto-purge of old archived entries
159
+
160
+ ## Success Criteria
161
+
162
+ - Database size stabilizes (no growth beyond configured retention window)
163
+ - `project_summaries` has exactly 1 row per project with knowledge
164
+ - MCP `check_project_knowledge` returns useful context before implementation
165
+ - `ph cleanup --retention` can be run daily (cron) without user intervention
166
+ - Zero data loss: archived prompts are queryable, only explicitly purgeable
167
+
168
+ ## Non-Goals
169
+
170
+ - Full-text dedup across projects (separate concern)
171
+ - Cross-project knowledge merging (too risky)
172
+ - Automatic git state tracking (Task 2.4, deferred)
173
+ - Vector search over archived prompts (unnecessary for cold data)
@@ -0,0 +1,54 @@
1
+ # Codex CLI hook for ph
2
+
3
+ The hook imports the active Codex rollout after each completed turn. Codex supplies the transcript path to the `Stop` hook; `ph` pairs user messages with the assistant response and deduplicates turns already captured.
4
+
5
+ ## Install
6
+
7
+ From the ph repository, make the hook executable and link it into your Codex configuration directory:
8
+
9
+ ```sh
10
+ chmod +x hooks/codex/ph-hook.sh
11
+ mkdir -p ~/.codex
12
+ ln -sf "$(pwd)/hooks/codex/ph-hook.sh" ~/.codex/ph-hook.sh
13
+ ```
14
+
15
+ Add this to `~/.codex/hooks.json` (merge it with any existing hooks):
16
+
17
+ ```json
18
+ {
19
+ "hooks": {
20
+ "Stop": [
21
+ {
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "~/.codex/ph-hook.sh"
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
31
+ }
32
+ ```
33
+
34
+ Restart Codex and approve/trust the hook when prompted. If hooks are disabled
35
+ in your Codex configuration, enable them in `~/.codex/config.toml`:
36
+
37
+ ```toml
38
+ [features]
39
+ hooks = true
40
+ ```
41
+
42
+ The hook only reads the
43
+ transcript path supplied by Codex and writes through `ph import`; it does not
44
+ modify Codex session files. Codex describes the transcript path as a convenience
45
+ field and does not guarantee the transcript format as a stable hook interface.
46
+
47
+ ## Import existing sessions
48
+
49
+ ```sh
50
+ ph import codex --dry-run
51
+ ph import codex
52
+ ```
53
+
54
+ Codex rollouts are read from `~/.codex/sessions/`. To import one transcript, the hook uses the same importer with `--file <path>`.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ # Import the active Codex rollout after each completed turn.
3
+ set -u
4
+
5
+ INPUT=$(cat)
6
+ TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{try{process.stdout.write(JSON.parse(s).transcript_path||"")}catch{}})' 2>/dev/null)
7
+ if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
8
+ printf '{}\n'
9
+ exit 0
10
+ fi
11
+
12
+ ph import codex --file "$TRANSCRIPT_PATH" >/dev/null 2>&1 || true
13
+ printf '{}\n'
14
+ exit 0