engrm 0.4.5 → 0.4.7

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.
package/README.md CHANGED
@@ -1,41 +1,79 @@
1
1
  # Engrm
2
2
 
3
- **Shared memory and delivery review for AI coding agents.** Engrm keeps context, decisions, and project state moving across your machines, your team, and the agents you switch between.
3
+ [![License: FSL-1.1-ALv2](https://img.shields.io/badge/license-FSL--1.1--ALv2-blue)](./LICENSE)
4
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-green)](https://nodejs.org)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/)
6
+ [![MCP Compatible](https://img.shields.io/badge/MCP-compatible-purple)](https://modelcontextprotocol.io)
4
7
 
5
- For npm users, Engrm runs on Node.js 18+ and does not require Bun to be installed.
8
+ **The only AI memory that syncs across devices and agents.**
6
9
 
7
- ```
8
- npx engrm init
9
- ```
10
+ Cross-device persistent memory for OpenClaw, Claude Code, Codex, and any MCP-compatible agent. Start free with 2 devices.
11
+
12
+ [Get Started](https://engrm.dev) • [Documentation](https://engrm.dev/developers) • [Blog](https://engrm.dev/blog)
13
+
14
+ ---
15
+
16
+ ## Why Engrm?
17
+
18
+ - **Cross-device sync** — Fix a bug on your laptop, continue on your desktop. No other memory tool does this.
19
+ - **Cross-agent compatible** — Works with OpenClaw, Claude Code, Codex, Cursor, Windsurf, Cline, Zed
20
+ - **Free tier** — 2 devices, 5,000 observations, full sync. £0 forever.
21
+ - **Offline-first** — Local SQLite + sqlite-vec. <50ms search. Works on a plane.
22
+ - **Delivery Review** — Compare what was promised vs what shipped
23
+ - **Sentinel** — Real-time code audit before changes land
24
+ - **Team memory** — Share insights across your whole team (Team plan)
25
+
26
+ ---
27
+
28
+ ## vs Other Memory Tools
29
+
30
+ | Feature | Engrm Free | Supermemory Pro | mem0 |
31
+ |---------|------------|-----------------|------|
32
+ | **Cost** | £0 | $20/mo | ~$2/mo + usage |
33
+ | **Cross-device** | ✅ 2 devices | ❌ Single device | ❌ Single device |
34
+ | **OpenClaw plugin** | ✅ Native | ✅ (Pro required) | ✅ (usage costs) |
35
+ | **Works with Claude/Codex** | ✅ | ❌ | ❌ |
36
+ | **Delivery Review** | ✅ | ❌ | ❌ |
37
+ | **Sentinel** | ✅ (Vibe+) | ❌ | ❌ |
10
38
 
11
- Public beta. Engrm is built for Claude Code, Codex, OpenClaw skills, and other MCP-native coding workflows. The current source of truth for agent capability differences is [AGENT_SUPPORT.md](AGENT_SUPPORT.md).
39
+ [Read the full comparison ](https://engrm.dev/blog/engrm-openclaw-cross-device-memory)
12
40
 
13
41
  ---
14
42
 
15
- ## What It Does
43
+ ## Installation
16
44
 
17
- Your AI agent forgets everything between sessions. Engrm fixes that, and helps you check whether the work really matched the brief.
45
+ ### For OpenClaw Users
18
46
 
19
- - **Works across agents** — Claude Code and Codex integrate directly, and OpenClaw can use Engrm through published skill bundles
20
- - **Remembers across devices** — fix a bug on your laptop, continue on your desktop with full context
21
- - **Shares with your team** — one developer's hard-won insight becomes everyone's knowledge
22
- - **Reviews delivery** — tie plans, decisions, and sessions together so you can see what actually shipped
23
- - **Works offline** — local SQLite is the source of truth; syncs when connected
24
- - **Guards your code** Sentinel audits changes in real-time before they land
47
+ ```bash
48
+ # 1. Install the plugin
49
+ openclaw plugins install engrm-openclaw-plugin
50
+
51
+ # 2. Restart OpenClaw
52
+ # Quit and reopen, or restart gateway
25
53
 
26
- ## Quick Start
54
+ # 3. Connect Engrm in chat
55
+ /engrm connect
56
+
57
+ # 4. Verify
58
+ /engrm status
59
+ ```
27
60
 
28
- ### 1. Sign up
61
+ **What works:**
62
+ - ✅ Session startup memory injection
63
+ - ✅ Automatic session capture
64
+ - ✅ Cross-device sync (unique to Engrm)
65
+ - ✅ `/engrm` slash commands
66
+ - ✅ Sentinel advisory mode (Vibe+ plans)
29
67
 
30
- Visit [engrm.dev](https://engrm.dev) and create an account.
68
+ **Blog:** [Engrm Now Supports OpenClaw →](https://engrm.dev/blog/engrm-openclaw-cross-device-memory)
31
69
 
32
- ### 2. Install
70
+ ### For Claude Code / Codex
33
71
 
34
72
  ```bash
35
73
  npx engrm init
36
74
  ```
37
75
 
38
- This opens your browser for authentication, writes config to `~/.engrm/`, and registers Engrm in Claude Code and Codex when those configs are available. OpenClaw support is provided through the packaged skills in [`openclaw/`](openclaw/). Takes about 30 seconds.
76
+ This auto-configures MCP servers and hooks in `~/.claude.json` and `~/.codex/config.toml`.
39
77
 
40
78
  **Alternative methods:**
41
79
  ```bash
@@ -49,9 +87,15 @@ npx engrm init --url=https://vector.internal.company.com
49
87
  npx engrm init --manual
50
88
  ```
51
89
 
52
- ### 3. Use your agent normally
90
+ For npm users, Engrm runs on Node.js 18+ and does not require Bun to be installed.
53
91
 
54
- That's it. Engrm works in the background:
92
+ ---
93
+
94
+ ## How It Works
95
+
96
+ ### Background Operation
97
+
98
+ Engrm works automatically:
55
99
 
56
100
  - **Session start** — injects relevant project memory into context
57
101
  - **While you work** — captures observations from tool use where the agent exposes that hook surface
@@ -69,7 +113,7 @@ That's it. Engrm works in the background:
69
113
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70
114
  ```
71
115
 
72
- ### 4. Check status
116
+ ### Check Status
73
117
 
74
118
  ```bash
75
119
  npx engrm status
@@ -95,49 +139,47 @@ Engrm Status
95
139
  Security: 3 findings (1 high, 2 medium)
96
140
  ```
97
141
 
98
- ---
99
-
100
- ## How It Works
142
+ ### Architecture
101
143
 
144
+ **Claude Code session:**
102
145
  ```
103
- Claude Code session
104
-
105
- ├─ SessionStart hook ──→ inject relevant memory into context
106
-
107
- ├─ PreToolUse hook ────→ Sentinel audits Edit/Write (optional)
108
-
109
- ├─ PostToolUse hook ───→ extract observations from tool results
110
-
111
- ├─ PreCompact hook ────→ re-inject memory before context compression
112
-
113
- ├─ ElicitationResult ──→ capture MCP form submissions
114
-
115
- └─ Stop hook ──────────→ session digest + sync + summary
116
-
117
-
118
- Local SQLite (FTS5 + sqlite-vec)
119
-
120
- (sync every 30s)
121
- Candengo Vector (cloud)
122
-
123
-
124
- Available on all your devices + team members
146
+
147
+ ├─ SessionStart hook ──→ inject relevant memory into context
148
+
149
+ ├─ PreToolUse hook ────→ Sentinel audits Edit/Write (optional)
150
+
151
+ ├─ PostToolUse hook ───→ extract observations from tool results
152
+
153
+ ├─ PreCompact hook ────→ re-inject memory before context compression
154
+
155
+ ├─ ElicitationResult ──→ capture MCP form submissions
156
+
157
+ └─ Stop hook ──────────→ session digest + sync + summary
158
+
159
+
160
+ Local SQLite (FTS5 + sqlite-vec)
161
+
162
+ ▼ (sync every 30s)
163
+ Candengo Vector (cloud)
164
+
165
+
166
+ Available on all your devices + team members
125
167
  ```
126
168
 
169
+ **Codex session:**
127
170
  ```
128
- Codex session
129
-
130
- ├─ SessionStart hook ──→ inject relevant memory into context
131
-
132
- ├─ MCP tools ──────────→ search, save, inspect, message, stats
133
-
134
- └─ Stop hook ──────────→ session digest + sync + summary
171
+
172
+ ├─ SessionStart hook ──→ inject relevant memory into context
173
+
174
+ ├─ MCP tools ──────────→ search, save, inspect, message, stats
175
+
176
+ └─ Stop hook ──────────→ session digest + sync + summary
135
177
  ```
136
178
 
137
- ### Agent Support
179
+ ### Agent Capability Matrix
138
180
 
139
181
  | Capability | Claude Code | Codex | OpenClaw |
140
- |---|---|---|---|
182
+ |-----------|-------------|-------|----------|
141
183
  | MCP server tools | ✓ | ✓ | Via skills / MCP |
142
184
  | Session-start context injection | ✓ | ✓ | Via skill-guided workflow |
143
185
  | Stop/session summary hook | ✓ | ✓ | Via skill-guided workflow |
@@ -146,7 +188,11 @@ Codex session
146
188
  | Pre-compact reinjection | ✓ | Not exposed | Not exposed |
147
189
  | ElicitationResult capture | ✓ | Not exposed | Not exposed |
148
190
 
149
- OpenClaw support is packaged in [`openclaw/`](openclaw/) as `engrm-memory`, `engrm-delivery-review`, and `engrm-sentinel` skills for ClawHub-style distribution.
191
+ See [AGENT_SUPPORT.md](AGENT_SUPPORT.md) for detailed comparison.
192
+
193
+ ---
194
+
195
+ ## Features
150
196
 
151
197
  ### MCP Tools
152
198
 
@@ -166,7 +212,7 @@ The MCP server exposes tools that supported agents can call directly:
166
212
  ### Observation Types
167
213
 
168
214
  | Type | What it captures |
169
- |------|-----------------|
215
+ |------|------------------|
170
216
  | `discovery` | Learning about existing systems or codebases |
171
217
  | `bugfix` | Something was broken, now fixed |
172
218
  | `decision` | Architectural or design choice with rationale |
@@ -176,50 +222,55 @@ The MCP server exposes tools that supported agents can call directly:
176
222
  | `pattern` | Recurring issue or technique |
177
223
  | `digest` | Session summary (auto-generated) |
178
224
 
179
- ---
180
-
181
- ## Features
182
-
183
225
  ### Hybrid Search
226
+
184
227
  Local FTS5 + sqlite-vec (all-MiniLM-L6-v2, 384 dims) combined with Candengo Vector's BGE-M3 semantic search. Results merged via Reciprocal Rank Fusion.
185
228
 
186
- ### Sentinel — Real-Time Code Audit
187
- LLM-powered review of every Edit/Write before it executes. Catches security issues, anti-patterns, and drift from team decisions.
229
+ ### Sentinel
230
+
231
+ LLM-powered review of every `Edit`/`Write` before it executes. Catches security issues, anti-patterns, and drift from team decisions.
188
232
 
189
233
  ```
190
- ⚠️ Sentinel: SQL query uses string concatenation instead of parameterized query
234
+ ⚠️ Sentinel: SQL query uses string concatenation instead of parameterized query
191
235
  Rule: sql-injection
192
236
  (Advisory mode — change allowed)
193
237
  ```
194
238
 
195
- 5 built-in rule packs: `security`, `auth`, `api`, `react`, `database`.
239
+ **Built-in rule packs:** security, auth, api, react, database.
196
240
 
197
241
  ```bash
198
- npx engrm sentinel init-rules # Install all rule packs
199
- npx engrm sentinel rules # List available packs
242
+ npx engrm sentinel init-rules # Install all rule packs
243
+ npx engrm sentinel rules # List available packs
200
244
  ```
201
245
 
202
- ### Starter Packs
246
+ ### Knowledge Packs
247
+
203
248
  Pre-loaded knowledge for your tech stack. Detected automatically on session start.
204
249
 
205
- Available: `typescript-patterns`, `nextjs-patterns`, `node-security`, `python-django`, `react-gotchas`, `api-best-practices`, `web-security`
250
+ **Available:** typescript-patterns, nextjs-patterns, node-security, python-django, react-gotchas, api-best-practices, web-security
206
251
 
207
252
  ```bash
208
253
  npx engrm install-pack typescript-patterns
209
254
  ```
210
255
 
211
256
  ### Secret Scrubbing
257
+
212
258
  Multi-layer regex scanning for API keys, passwords, tokens, and credentials. Sensitive content is redacted before storage and sync. Custom patterns configurable in `~/.engrm/settings.json`.
213
259
 
214
- ### Observation Lifecycle
260
+ ### Retention & Aging
261
+
215
262
  Observations age gracefully: **active** (30 days, full weight) → **aging** (0.7x search weight) → **archived** (compacted into digests) → **purged** (after 12 months). Pinned observations never age.
216
263
 
217
264
  ---
218
265
 
219
266
  ## Pricing
220
267
 
268
+ **Free tier stays free forever.** No bait-and-switch.
269
+
270
+ Start with 2 devices and 5,000 observations. Upgrade when you need more.
271
+
221
272
  | | Free | Vibe | Pro | Team |
222
- |---|---|---|---|---|
273
+ |---|------|------|-----|------|
223
274
  | **Price** | £0 | £5.99/mo | £9.99/mo | £12.99/seat/mo |
224
275
  | **Observations** | 5,000 | 25,000 | 100,000 | Unlimited |
225
276
  | **Devices** | 2 | 3 | 5 | Unlimited |
@@ -232,7 +283,7 @@ Sign up at [engrm.dev](https://engrm.dev).
232
283
 
233
284
  ---
234
285
 
235
- ## Self-Hosting
286
+ ## Self-Hosted
236
287
 
237
288
  Point Engrm at your own [Candengo Vector](https://www.candengo.com) instance:
238
289
 
@@ -246,11 +297,11 @@ Candengo Vector provides the backend: BGE-M3 hybrid search, multi-tenant namespa
246
297
 
247
298
  ## Configuration
248
299
 
249
- ### User config: `~/.engrm/settings.json`
300
+ ### `~/.engrm/settings.json`
250
301
 
251
302
  Created by `engrm init`. Contains API credentials, sync settings, search preferences, secret scrubbing patterns, and Sentinel configuration.
252
303
 
253
- ### Project config: `.engrm.json` (optional)
304
+ ### `.engrm-project.json`
254
305
 
255
306
  Place in your project root to override project identity for non-git projects:
256
307
 
@@ -261,24 +312,25 @@ Place in your project root to override project identity for non-git projects:
261
312
  }
262
313
  ```
263
314
 
264
- ### Agent integration
315
+ ### Agent Auto-Registration
265
316
 
266
317
  Engrm auto-registers in:
318
+
267
319
  - `~/.claude.json` — MCP server (`engrm`)
268
320
  - `~/.claude/settings.json` — 6 lifecycle hooks
269
321
  - `~/.codex/config.toml` — MCP server (`engrm`) + `codex_hooks` feature flag
270
- - `~/.codex/hooks.json` — `SessionStart` and `Stop` hooks
322
+ - `~/.codex/hooks.json` — SessionStart and Stop hooks
271
323
 
272
324
  ---
273
325
 
274
- ## Tech Stack
326
+ ## Technical Stack
275
327
 
276
- - **Runtime**: TypeScript, runs on Bun (dev) or Node.js 18+ (npm)
277
- - **Local storage**: SQLite via better-sqlite3, FTS5 full-text search, sqlite-vec for embeddings
278
- - **Embeddings**: all-MiniLM-L6-v2 via @xenova/transformers (384 dims, ~23MB)
279
- - **Remote backend**: Candengo Vector (BGE-M3, Qdrant, hybrid dense+sparse search)
280
- - **MCP**: @modelcontextprotocol/sdk (stdio transport)
281
- - **AI extraction**: @anthropic-ai/claude-agent-sdk (optional, for richer observations)
328
+ - **Runtime:** TypeScript, runs on Bun (dev) or Node.js 18+ (npm)
329
+ - **Local storage:** SQLite via `better-sqlite3`, FTS5 full-text search, `sqlite-vec` for embeddings
330
+ - **Embeddings:** all-MiniLM-L6-v2 via `@xenova/transformers` (384 dims, ~23MB)
331
+ - **Remote backend:** Candengo Vector (BGE-M3, Qdrant, hybrid dense+sparse search)
332
+ - **MCP:** `@modelcontextprotocol/sdk` (stdio transport)
333
+ - **AI extraction:** `@anthropic-ai/claude-agent-sdk` (optional, for richer observations)
282
334
 
283
335
  ---
284
336
 
@@ -286,23 +338,41 @@ Engrm auto-registers in:
286
338
 
287
339
  **FSL-1.1-ALv2** (Functional Source License) — part of the [Fair Source](https://fair.io) movement.
288
340
 
289
- - Free to use, modify, and self-host
290
- - You cannot offer this as a competing hosted service
291
- - Each version converts to Apache 2.0 after 2 years
292
- - Sentinel is a separate proprietary product
341
+ - Free to use, modify, and self-host
342
+ - You cannot offer this as a competing hosted service
343
+ - Each version converts to Apache 2.0 after 2 years
344
+ - ⚠️ Sentinel is a separate proprietary product
293
345
 
294
346
  See [LICENSE](LICENSE) for full terms.
295
347
 
296
348
  ---
297
349
 
298
- ## Project
350
+ ## Documentation
299
351
 
300
352
  - Architecture: [ARCHITECTURE.md](ARCHITECTURE.md)
301
353
  - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
302
354
  - Security: [SECURITY.md](SECURITY.md)
303
355
  - Roadmap: [ROADMAP.md](ROADMAP.md)
304
356
 
305
- Maintainers: run `node scripts/check-public-docs.mjs` to verify the repo only contains the approved public docs set at the root.
357
+ **Maintainers:** run `node scripts/check-public-docs.mjs` to verify the repo only contains the approved public docs set at the root.
358
+
359
+ ---
360
+
361
+ ## Resources
362
+
363
+ - [Documentation](https://engrm.dev/developers)
364
+ - [Blog](https://engrm.dev/blog)
365
+ - [Pricing](https://engrm.dev/pricing)
366
+ - [Sentinel](https://engrm.dev/sentinel)
367
+
368
+ ## Community
369
+
370
+ - [Twitter/X](https://twitter.com/engrm_dev)
371
+ - [GitHub Issues](https://github.com/dr12hes/engrm/issues)
372
+
373
+ ---
374
+
375
+ **Found this useful?** ⭐ Star this repo to help other developers discover Engrm.
306
376
 
307
377
  ---
308
378
 
package/dist/cli.js CHANGED
@@ -1072,6 +1072,31 @@ function getOutboxStats(db) {
1072
1072
  return stats;
1073
1073
  }
1074
1074
 
1075
+ // src/intelligence/value-signals.ts
1076
+ var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
1077
+ function computeSessionValueSignals(observations, securityFindings = []) {
1078
+ const decisionsCount = observations.filter((o) => o.type === "decision").length;
1079
+ const lessonsCount = observations.filter((o) => LESSON_TYPES.has(o.type)).length;
1080
+ const discoveriesCount = observations.filter((o) => o.type === "discovery").length;
1081
+ const featuresCount = observations.filter((o) => o.type === "feature").length;
1082
+ const refactorsCount = observations.filter((o) => o.type === "refactor").length;
1083
+ const repeatedPatternsCount = observations.filter((o) => o.type === "pattern").length;
1084
+ const hasRequestSignal = observations.some((o) => ["feature", "decision", "change", "bugfix", "discovery"].includes(o.type));
1085
+ const hasCompletionSignal = observations.some((o) => ["feature", "change", "refactor", "bugfix"].includes(o.type));
1086
+ return {
1087
+ decisions_count: decisionsCount,
1088
+ lessons_count: lessonsCount,
1089
+ discoveries_count: discoveriesCount,
1090
+ features_count: featuresCount,
1091
+ refactors_count: refactorsCount,
1092
+ repeated_patterns_count: repeatedPatternsCount,
1093
+ security_findings_count: securityFindings.length,
1094
+ critical_security_findings_count: securityFindings.filter((f) => f.severity === "critical").length,
1095
+ delivery_review_ready: hasRequestSignal && hasCompletionSignal,
1096
+ vibe_guardian_active: securityFindings.length > 0
1097
+ };
1098
+ }
1099
+
1075
1100
  // src/storage/migrations.ts
1076
1101
  var MIGRATIONS2 = [
1077
1102
  {
@@ -2104,6 +2129,80 @@ function findDuplicate(newTitle, candidates) {
2104
2129
  return bestMatch;
2105
2130
  }
2106
2131
 
2132
+ // src/capture/facts.ts
2133
+ var FACT_ELIGIBLE_TYPES = new Set([
2134
+ "bugfix",
2135
+ "decision",
2136
+ "discovery",
2137
+ "pattern",
2138
+ "feature",
2139
+ "refactor",
2140
+ "change"
2141
+ ]);
2142
+ function buildStructuredFacts(input) {
2143
+ const seedFacts = dedupeFacts(input.facts ?? []);
2144
+ if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
2145
+ return seedFacts;
2146
+ }
2147
+ const derived = [...seedFacts];
2148
+ if (seedFacts.length === 0 && looksMeaningful(input.title)) {
2149
+ derived.push(input.title.trim());
2150
+ }
2151
+ for (const sentence of extractNarrativeFacts(input.narrative)) {
2152
+ derived.push(sentence);
2153
+ }
2154
+ const fileFact = buildFilesFact(input.filesModified);
2155
+ if (fileFact) {
2156
+ derived.push(fileFact);
2157
+ }
2158
+ return dedupeFacts(derived).slice(0, 4);
2159
+ }
2160
+ function extractNarrativeFacts(narrative) {
2161
+ if (!narrative)
2162
+ return [];
2163
+ const cleaned = narrative.replace(/\s+/g, " ").trim();
2164
+ if (cleaned.length < 24)
2165
+ return [];
2166
+ const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
2167
+ return parts.slice(0, 2);
2168
+ }
2169
+ function buildFilesFact(filesModified) {
2170
+ if (!filesModified || filesModified.length === 0)
2171
+ return null;
2172
+ const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
2173
+ if (cleaned.length === 0)
2174
+ return null;
2175
+ if (cleaned.length === 1) {
2176
+ return `Touched ${cleaned[0]}`;
2177
+ }
2178
+ return `Touched ${cleaned.join(", ")}`;
2179
+ }
2180
+ function dedupeFacts(facts) {
2181
+ const seen = new Set;
2182
+ const result = [];
2183
+ for (const fact of facts) {
2184
+ const cleaned = fact.trim().replace(/\s+/g, " ");
2185
+ if (!looksMeaningful(cleaned))
2186
+ continue;
2187
+ const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
2188
+ if (!key || seen.has(key))
2189
+ continue;
2190
+ seen.add(key);
2191
+ result.push(cleaned);
2192
+ }
2193
+ return result;
2194
+ }
2195
+ function looksMeaningful(value) {
2196
+ const cleaned = value.trim();
2197
+ if (cleaned.length < 12)
2198
+ return false;
2199
+ if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
2200
+ return false;
2201
+ if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
2202
+ return false;
2203
+ return true;
2204
+ }
2205
+
2107
2206
  // src/storage/projects.ts
2108
2207
  import { execSync } from "node:child_process";
2109
2208
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
@@ -2495,10 +2594,17 @@ async function saveObservation(db, config, input) {
2495
2594
  const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
2496
2595
  const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
2497
2596
  const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
2498
- const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
2499
2597
  const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
2500
2598
  const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
2501
2599
  const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
2600
+ const structuredFacts = buildStructuredFacts({
2601
+ type: input.type,
2602
+ title: input.title,
2603
+ narrative: input.narrative,
2604
+ facts: input.facts,
2605
+ filesModified
2606
+ });
2607
+ const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
2502
2608
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
2503
2609
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
2504
2610
  let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
@@ -3195,6 +3301,27 @@ function handleStatus() {
3195
3301
  } catch {}
3196
3302
  const summaryCount = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
3197
3303
  console.log(` Sessions: ${summaryCount} summarised`);
3304
+ try {
3305
+ const activeObservations = db.db.query(`SELECT * FROM observations
3306
+ WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
3307
+ const securityFindings = db.db.query(`SELECT * FROM security_findings
3308
+ ORDER BY created_at_epoch DESC
3309
+ LIMIT 500`).all();
3310
+ const signals = computeSessionValueSignals(activeObservations, securityFindings);
3311
+ const signalParts = [
3312
+ `lessons: ${signals.lessons_count}`,
3313
+ `decisions: ${signals.decisions_count}`,
3314
+ `discoveries: ${signals.discoveries_count}`,
3315
+ `features: ${signals.features_count}`
3316
+ ];
3317
+ if (signals.repeated_patterns_count > 0) {
3318
+ signalParts.push(`patterns: ${signals.repeated_patterns_count}`);
3319
+ }
3320
+ console.log(` Value: ${signalParts.join(", ")}`);
3321
+ if (signals.security_findings_count > 0 || signals.delivery_review_ready) {
3322
+ console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"}, ` + `${signals.security_findings_count} finding${signals.security_findings_count === 1 ? "" : "s"}`);
3323
+ }
3324
+ } catch {}
3198
3325
  try {
3199
3326
  const lastSummary = db.db.query(`SELECT request, created_at_epoch FROM session_summaries
3200
3327
  ORDER BY created_at_epoch DESC LIMIT 1`).get();
@@ -250,6 +250,80 @@ function findDuplicate(newTitle, candidates) {
250
250
  return bestMatch;
251
251
  }
252
252
 
253
+ // src/capture/facts.ts
254
+ var FACT_ELIGIBLE_TYPES = new Set([
255
+ "bugfix",
256
+ "decision",
257
+ "discovery",
258
+ "pattern",
259
+ "feature",
260
+ "refactor",
261
+ "change"
262
+ ]);
263
+ function buildStructuredFacts(input) {
264
+ const seedFacts = dedupeFacts(input.facts ?? []);
265
+ if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
266
+ return seedFacts;
267
+ }
268
+ const derived = [...seedFacts];
269
+ if (seedFacts.length === 0 && looksMeaningful(input.title)) {
270
+ derived.push(input.title.trim());
271
+ }
272
+ for (const sentence of extractNarrativeFacts(input.narrative)) {
273
+ derived.push(sentence);
274
+ }
275
+ const fileFact = buildFilesFact(input.filesModified);
276
+ if (fileFact) {
277
+ derived.push(fileFact);
278
+ }
279
+ return dedupeFacts(derived).slice(0, 4);
280
+ }
281
+ function extractNarrativeFacts(narrative) {
282
+ if (!narrative)
283
+ return [];
284
+ const cleaned = narrative.replace(/\s+/g, " ").trim();
285
+ if (cleaned.length < 24)
286
+ return [];
287
+ const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
288
+ return parts.slice(0, 2);
289
+ }
290
+ function buildFilesFact(filesModified) {
291
+ if (!filesModified || filesModified.length === 0)
292
+ return null;
293
+ const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
294
+ if (cleaned.length === 0)
295
+ return null;
296
+ if (cleaned.length === 1) {
297
+ return `Touched ${cleaned[0]}`;
298
+ }
299
+ return `Touched ${cleaned.join(", ")}`;
300
+ }
301
+ function dedupeFacts(facts) {
302
+ const seen = new Set;
303
+ const result = [];
304
+ for (const fact of facts) {
305
+ const cleaned = fact.trim().replace(/\s+/g, " ");
306
+ if (!looksMeaningful(cleaned))
307
+ continue;
308
+ const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
309
+ if (!key || seen.has(key))
310
+ continue;
311
+ seen.add(key);
312
+ result.push(cleaned);
313
+ }
314
+ return result;
315
+ }
316
+ function looksMeaningful(value) {
317
+ const cleaned = value.trim();
318
+ if (cleaned.length < 12)
319
+ return false;
320
+ if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
321
+ return false;
322
+ if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
323
+ return false;
324
+ return true;
325
+ }
326
+
253
327
  // src/storage/projects.ts
254
328
  import { execSync } from "node:child_process";
255
329
  import { existsSync, readFileSync } from "node:fs";
@@ -630,10 +704,17 @@ async function saveObservation(db, config, input) {
630
704
  const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
631
705
  const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
632
706
  const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
633
- const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
634
707
  const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
635
708
  const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
636
709
  const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
710
+ const structuredFacts = buildStructuredFacts({
711
+ type: input.type,
712
+ title: input.title,
713
+ narrative: input.narrative,
714
+ facts: input.facts,
715
+ filesModified
716
+ });
717
+ const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
637
718
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
638
719
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
639
720
  let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;