learning-agent 0.2.2 → 0.2.3

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/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { createHash } from 'crypto';
4
- import { statSync, existsSync, chmodSync, mkdirSync } from 'fs';
5
- import { join, dirname } from 'path';
6
- import Database from 'better-sqlite3';
7
3
  import * as fs from 'fs/promises';
8
- import { readFile, mkdir, appendFile, writeFile, rename } from 'fs/promises';
4
+ import { mkdir, appendFile, readFile, writeFile, rename } from 'fs/promises';
5
+ import { join, dirname } from 'path';
6
+ import { createHash } from 'crypto';
9
7
  import { z } from 'zod';
8
+ import { createRequire } from 'module';
9
+ import { existsSync, statSync, mkdirSync, chmodSync } from 'fs';
10
10
  import chalk from 'chalk';
11
11
  import { resolveModelFile, getLlama } from 'node-llama-cpp';
12
12
  import { homedir } from 'os';
@@ -180,11 +180,45 @@ async function readLessons(repoRoot, options = {}) {
180
180
  }
181
181
  return { lessons: Array.from(lessons.values()), skippedCount };
182
182
  }
183
+ var require2 = createRequire(import.meta.url);
184
+ var sqliteAvailable = null;
185
+ var sqliteWarningLogged = false;
186
+ var DatabaseConstructor = null;
187
+ function isSqliteAvailable() {
188
+ if (sqliteAvailable !== null) {
189
+ return sqliteAvailable;
190
+ }
191
+ try {
192
+ const module = require2("better-sqlite3");
193
+ const Constructor = module.default || module;
194
+ const testDb = new Constructor(":memory:");
195
+ testDb.close();
196
+ DatabaseConstructor = Constructor;
197
+ sqliteAvailable = true;
198
+ } catch {
199
+ sqliteAvailable = false;
200
+ if (!sqliteWarningLogged) {
201
+ console.warn("SQLite unavailable, running in JSONL-only mode");
202
+ sqliteWarningLogged = true;
203
+ }
204
+ }
205
+ return sqliteAvailable;
206
+ }
207
+ function logDegradationWarning() {
208
+ if (!sqliteAvailable && !sqliteWarningLogged) {
209
+ console.warn("SQLite unavailable, running in JSONL-only mode");
210
+ sqliteWarningLogged = true;
211
+ }
212
+ }
213
+ function getDatabaseConstructor() {
214
+ if (!isSqliteAvailable()) {
215
+ return null;
216
+ }
217
+ return DatabaseConstructor;
218
+ }
183
219
 
184
- // src/storage/sqlite.ts
185
- var DB_PATH = ".claude/.cache/lessons.sqlite";
220
+ // src/storage/sqlite/schema.ts
186
221
  var SCHEMA_SQL = `
187
- -- Main lessons table
188
222
  CREATE TABLE IF NOT EXISTS lessons (
189
223
  id TEXT PRIMARY KEY,
190
224
  type TEXT NOT NULL,
@@ -204,7 +238,6 @@ var SCHEMA_SQL = `
204
238
  last_retrieved TEXT,
205
239
  embedding BLOB,
206
240
  content_hash TEXT,
207
- -- v0.2.2 fields
208
241
  invalidated_at TEXT,
209
242
  invalidation_reason TEXT,
210
243
  citation_file TEXT,
@@ -214,29 +247,21 @@ var SCHEMA_SQL = `
214
247
  compacted_at TEXT
215
248
  );
216
249
 
217
- -- FTS5 virtual table for full-text search
218
250
  CREATE VIRTUAL TABLE IF NOT EXISTS lessons_fts USING fts5(
219
- id,
220
- trigger,
221
- insight,
222
- tags,
223
- content='lessons',
224
- content_rowid='rowid'
251
+ id, trigger, insight, tags,
252
+ content='lessons', content_rowid='rowid'
225
253
  );
226
254
 
227
- -- Trigger to sync FTS on INSERT
228
255
  CREATE TRIGGER IF NOT EXISTS lessons_ai AFTER INSERT ON lessons BEGIN
229
256
  INSERT INTO lessons_fts(rowid, id, trigger, insight, tags)
230
257
  VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
231
258
  END;
232
259
 
233
- -- Trigger to sync FTS on DELETE
234
260
  CREATE TRIGGER IF NOT EXISTS lessons_ad AFTER DELETE ON lessons BEGIN
235
261
  INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
236
262
  VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
237
263
  END;
238
264
 
239
- -- Trigger to sync FTS on UPDATE
240
265
  CREATE TRIGGER IF NOT EXISTS lessons_au AFTER UPDATE ON lessons BEGIN
241
266
  INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
242
267
  VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
@@ -244,12 +269,10 @@ var SCHEMA_SQL = `
244
269
  VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
245
270
  END;
246
271
 
247
- -- Index for common queries
248
272
  CREATE INDEX IF NOT EXISTS idx_lessons_created ON lessons(created);
249
273
  CREATE INDEX IF NOT EXISTS idx_lessons_confirmed ON lessons(confirmed);
250
274
  CREATE INDEX IF NOT EXISTS idx_lessons_severity ON lessons(severity);
251
275
 
252
- -- Metadata table for sync tracking
253
276
  CREATE TABLE IF NOT EXISTS metadata (
254
277
  key TEXT PRIMARY KEY,
255
278
  value TEXT NOT NULL
@@ -258,22 +281,54 @@ var SCHEMA_SQL = `
258
281
  function createSchema(database) {
259
282
  database.exec(SCHEMA_SQL);
260
283
  }
284
+
285
+ // src/storage/sqlite/connection.ts
286
+ var DB_PATH = ".claude/.cache/lessons.sqlite";
261
287
  var db = null;
262
- function contentHash(trigger, insight) {
263
- return createHash("sha256").update(`${trigger} ${insight}`).digest("hex");
264
- }
265
- function openDb(repoRoot) {
266
- if (db) return db;
267
- const dbPath = join(repoRoot, DB_PATH);
268
- const dir = dirname(dbPath);
269
- mkdirSync(dir, { recursive: true });
270
- db = new Database(dbPath);
271
- db.pragma("journal_mode = WAL");
288
+ var dbIsInMemory = false;
289
+ function openDb(repoRoot, options = {}) {
290
+ if (!isSqliteAvailable()) {
291
+ return null;
292
+ }
293
+ const { inMemory = false } = options;
294
+ if (db) {
295
+ if (inMemory !== dbIsInMemory) {
296
+ closeDb();
297
+ } else {
298
+ return db;
299
+ }
300
+ }
301
+ const Database = getDatabaseConstructor();
302
+ if (inMemory) {
303
+ db = new Database(":memory:");
304
+ dbIsInMemory = true;
305
+ } else {
306
+ const dbPath = join(repoRoot, DB_PATH);
307
+ const dir = dirname(dbPath);
308
+ mkdirSync(dir, { recursive: true });
309
+ db = new Database(dbPath);
310
+ dbIsInMemory = false;
311
+ db.pragma("journal_mode = WAL");
312
+ }
272
313
  createSchema(db);
273
314
  return db;
274
315
  }
316
+ function closeDb() {
317
+ if (db) {
318
+ db.close();
319
+ db = null;
320
+ dbIsInMemory = false;
321
+ }
322
+ }
323
+ function contentHash(trigger, insight) {
324
+ return createHash("sha256").update(`${trigger} ${insight}`).digest("hex");
325
+ }
275
326
  function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
276
327
  const database = openDb(repoRoot);
328
+ if (!database) {
329
+ logDegradationWarning();
330
+ return null;
331
+ }
277
332
  const row = database.prepare("SELECT embedding, content_hash FROM lessons WHERE id = ?").get(lessonId);
278
333
  if (!row || !row.embedding || !row.content_hash) {
279
334
  return null;
@@ -290,60 +345,14 @@ function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
290
345
  }
291
346
  function setCachedEmbedding(repoRoot, lessonId, embedding, hash) {
292
347
  const database = openDb(repoRoot);
348
+ if (!database) {
349
+ logDegradationWarning();
350
+ return;
351
+ }
293
352
  const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
294
353
  const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
295
354
  database.prepare("UPDATE lessons SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, lessonId);
296
355
  }
297
- function rowToLesson(row) {
298
- const lesson = {
299
- id: row.id,
300
- type: row.type,
301
- trigger: row.trigger,
302
- insight: row.insight,
303
- tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
304
- source: row.source,
305
- context: JSON.parse(row.context),
306
- supersedes: JSON.parse(row.supersedes),
307
- related: JSON.parse(row.related),
308
- created: row.created,
309
- confirmed: row.confirmed === 1
310
- };
311
- if (row.evidence !== null) {
312
- lesson.evidence = row.evidence;
313
- }
314
- if (row.severity !== null) {
315
- lesson.severity = row.severity;
316
- }
317
- if (row.deleted === 1) {
318
- lesson.deleted = true;
319
- }
320
- if (row.retrieval_count > 0) {
321
- lesson.retrievalCount = row.retrieval_count;
322
- }
323
- if (row.invalidated_at !== null) {
324
- lesson.invalidatedAt = row.invalidated_at;
325
- }
326
- if (row.invalidation_reason !== null) {
327
- lesson.invalidationReason = row.invalidation_reason;
328
- }
329
- if (row.citation_file !== null) {
330
- lesson.citation = {
331
- file: row.citation_file,
332
- ...row.citation_line !== null && { line: row.citation_line },
333
- ...row.citation_commit !== null && { commit: row.citation_commit }
334
- };
335
- }
336
- if (row.compaction_level !== null && row.compaction_level !== 0) {
337
- lesson.compactionLevel = row.compaction_level;
338
- }
339
- if (row.compacted_at !== null) {
340
- lesson.compactedAt = row.compacted_at;
341
- }
342
- if (row.last_retrieved !== null) {
343
- lesson.lastRetrieved = row.last_retrieved;
344
- }
345
- return lesson;
346
- }
347
356
  function collectCachedEmbeddings(database) {
348
357
  const cache = /* @__PURE__ */ new Map();
349
358
  const rows = database.prepare("SELECT id, embedding, content_hash FROM lessons WHERE embedding IS NOT NULL").all();
@@ -376,6 +385,10 @@ function setLastSyncMtime(database, mtime) {
376
385
  }
377
386
  async function rebuildIndex(repoRoot) {
378
387
  const database = openDb(repoRoot);
388
+ if (!database) {
389
+ logDegradationWarning();
390
+ return;
391
+ }
379
392
  const { lessons } = await readLessons(repoRoot);
380
393
  const cachedEmbeddings = collectCachedEmbeddings(database);
381
394
  database.exec("DELETE FROM lessons");
@@ -411,7 +424,6 @@ async function rebuildIndex(repoRoot) {
411
424
  last_retrieved: lesson.lastRetrieved ?? null,
412
425
  embedding: hasValidCache ? cached.embedding : null,
413
426
  content_hash: hasValidCache ? cached.contentHash : null,
414
- // v0.2.2 fields
415
427
  invalidated_at: lesson.invalidatedAt ?? null,
416
428
  invalidation_reason: lesson.invalidationReason ?? null,
417
429
  citation_file: lesson.citation?.file ?? null,
@@ -429,12 +441,17 @@ async function rebuildIndex(repoRoot) {
429
441
  }
430
442
  }
431
443
  async function syncIfNeeded(repoRoot, options = {}) {
444
+ if (!isSqliteAvailable()) {
445
+ logDegradationWarning();
446
+ return false;
447
+ }
432
448
  const { force = false } = options;
433
449
  const jsonlMtime = getJsonlMtime(repoRoot);
434
450
  if (jsonlMtime === null && !force) {
435
451
  return false;
436
452
  }
437
453
  const database = openDb(repoRoot);
454
+ if (!database) return false;
438
455
  const lastSyncMtime = getLastSyncMtime(database);
439
456
  const needsRebuild = force || lastSyncMtime === null || jsonlMtime !== null && jsonlMtime > lastSyncMtime;
440
457
  if (needsRebuild) {
@@ -443,28 +460,49 @@ async function syncIfNeeded(repoRoot, options = {}) {
443
460
  }
444
461
  return false;
445
462
  }
446
- async function searchKeyword(repoRoot, query, limit) {
447
- const database = openDb(repoRoot);
448
- const countResult = database.prepare("SELECT COUNT(*) as cnt FROM lessons").get();
449
- if (countResult.cnt === 0) return [];
450
- const rows = database.prepare(
451
- `
452
- SELECT l.*
453
- FROM lessons l
454
- JOIN lessons_fts fts ON l.rowid = fts.rowid
455
- WHERE lessons_fts MATCH ?
456
- AND l.invalidated_at IS NULL
457
- LIMIT ?
458
- `
459
- ).all(query, limit);
460
- if (rows.length > 0) {
461
- incrementRetrievalCount(repoRoot, rows.map((r) => r.id));
463
+
464
+ // src/storage/sqlite/search.ts
465
+ function rowToLesson(row) {
466
+ const lesson = {
467
+ id: row.id,
468
+ type: row.type,
469
+ trigger: row.trigger,
470
+ insight: row.insight,
471
+ tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
472
+ source: row.source,
473
+ context: JSON.parse(row.context),
474
+ supersedes: JSON.parse(row.supersedes),
475
+ related: JSON.parse(row.related),
476
+ created: row.created,
477
+ confirmed: row.confirmed === 1
478
+ };
479
+ if (row.evidence !== null) lesson.evidence = row.evidence;
480
+ if (row.severity !== null) lesson.severity = row.severity;
481
+ if (row.deleted === 1) lesson.deleted = true;
482
+ if (row.retrieval_count > 0) lesson.retrievalCount = row.retrieval_count;
483
+ if (row.invalidated_at !== null) lesson.invalidatedAt = row.invalidated_at;
484
+ if (row.invalidation_reason !== null) lesson.invalidationReason = row.invalidation_reason;
485
+ if (row.citation_file !== null) {
486
+ lesson.citation = {
487
+ file: row.citation_file,
488
+ ...row.citation_line !== null && { line: row.citation_line },
489
+ ...row.citation_commit !== null && { commit: row.citation_commit }
490
+ };
462
491
  }
463
- return rows.map(rowToLesson);
492
+ if (row.compaction_level !== null && row.compaction_level !== 0) {
493
+ lesson.compactionLevel = row.compaction_level;
494
+ }
495
+ if (row.compacted_at !== null) lesson.compactedAt = row.compacted_at;
496
+ if (row.last_retrieved !== null) lesson.lastRetrieved = row.last_retrieved;
497
+ return lesson;
464
498
  }
465
499
  function incrementRetrievalCount(repoRoot, lessonIds) {
466
500
  if (lessonIds.length === 0) return;
467
501
  const database = openDb(repoRoot);
502
+ if (!database) {
503
+ logDegradationWarning();
504
+ return;
505
+ }
468
506
  const now = (/* @__PURE__ */ new Date()).toISOString();
469
507
  const update = database.prepare(`
470
508
  UPDATE lessons
@@ -479,8 +517,36 @@ function incrementRetrievalCount(repoRoot, lessonIds) {
479
517
  });
480
518
  updateMany(lessonIds);
481
519
  }
520
+ async function searchKeyword(repoRoot, query, limit) {
521
+ const database = openDb(repoRoot);
522
+ if (!database) {
523
+ throw new Error(
524
+ "Keyword search requires SQLite (FTS5 required). Install native build tools or use vector search instead."
525
+ );
526
+ }
527
+ const countResult = database.prepare("SELECT COUNT(*) as cnt FROM lessons").get();
528
+ if (countResult.cnt === 0) return [];
529
+ const rows = database.prepare(
530
+ `
531
+ SELECT l.*
532
+ FROM lessons l
533
+ JOIN lessons_fts fts ON l.rowid = fts.rowid
534
+ WHERE lessons_fts MATCH ?
535
+ AND l.invalidated_at IS NULL
536
+ LIMIT ?
537
+ `
538
+ ).all(query, limit);
539
+ if (rows.length > 0) {
540
+ incrementRetrievalCount(repoRoot, rows.map((r) => r.id));
541
+ }
542
+ return rows.map(rowToLesson);
543
+ }
482
544
  function getRetrievalStats(repoRoot) {
483
545
  const database = openDb(repoRoot);
546
+ if (!database) {
547
+ logDegradationWarning();
548
+ return [];
549
+ }
484
550
  const rows = database.prepare("SELECT id, retrieval_count, last_retrieved FROM lessons").all();
485
551
  return rows.map((row) => ({
486
552
  id: row.id,
@@ -489,83 +555,202 @@ function getRetrievalStats(repoRoot) {
489
555
  }));
490
556
  }
491
557
 
492
- // src/capture/quality.ts
493
- var DEFAULT_SIMILARITY_THRESHOLD = 0.8;
494
- async function isNovel(repoRoot, insight, options = {}) {
495
- const threshold = options.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
496
- await syncIfNeeded(repoRoot);
497
- const words = insight.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
498
- if (words.length === 0) {
499
- return { novel: true };
500
- }
501
- const searchQuery = words.join(" OR ");
502
- const results = await searchKeyword(repoRoot, searchQuery, 10);
503
- if (results.length === 0) {
504
- return { novel: true };
558
+ // src/utils.ts
559
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
560
+ function getLessonAgeDays(lesson) {
561
+ const created = new Date(lesson.created).getTime();
562
+ const now = Date.now();
563
+ return Math.floor((now - created) / MS_PER_DAY);
564
+ }
565
+
566
+ // src/storage/compact.ts
567
+ var ARCHIVE_DIR = ".claude/lessons/archive";
568
+ var TOMBSTONE_THRESHOLD = 100;
569
+ var ARCHIVE_AGE_DAYS = 90;
570
+ var MONTH_INDEX_OFFSET = 1;
571
+ var MONTH_PAD_LENGTH = 2;
572
+ function getArchivePath(repoRoot, date) {
573
+ const year = date.getFullYear();
574
+ const month = String(date.getMonth() + MONTH_INDEX_OFFSET).padStart(MONTH_PAD_LENGTH, "0");
575
+ return join(repoRoot, ARCHIVE_DIR, `${year}-${month}.jsonl`);
576
+ }
577
+ async function parseRawJsonlLines(repoRoot) {
578
+ const filePath = join(repoRoot, LESSONS_PATH);
579
+ let content;
580
+ try {
581
+ content = await readFile(filePath, "utf-8");
582
+ } catch {
583
+ return [];
505
584
  }
506
- const insightWords = new Set(insight.toLowerCase().split(/\s+/));
507
- for (const lesson of results) {
508
- const lessonWords = new Set(lesson.insight.toLowerCase().split(/\s+/));
509
- const intersection = [...insightWords].filter((w) => lessonWords.has(w)).length;
510
- const union = (/* @__PURE__ */ new Set([...insightWords, ...lessonWords])).size;
511
- const similarity = union > 0 ? intersection / union : 0;
512
- if (similarity >= threshold) {
513
- return {
514
- novel: false,
515
- reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
516
- existingId: lesson.id
517
- };
518
- }
519
- if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
520
- return {
521
- novel: false,
522
- reason: `Exact duplicate found`,
523
- existingId: lesson.id
524
- };
585
+ const results = [];
586
+ for (const line of content.split("\n")) {
587
+ const trimmed = line.trim();
588
+ if (!trimmed) continue;
589
+ try {
590
+ const parsed = JSON.parse(trimmed);
591
+ results.push({ line: trimmed, parsed });
592
+ } catch {
593
+ results.push({ line: trimmed, parsed: null });
525
594
  }
526
595
  }
527
- return { novel: true };
596
+ return results;
528
597
  }
529
- var MIN_WORD_COUNT = 4;
530
- var VAGUE_PATTERNS = [
531
- /\bwrite better\b/i,
532
- /\bbe careful\b/i,
533
- /\bremember to\b/i,
534
- /\bmake sure\b/i,
535
- /\btry to\b/i,
536
- /\bdouble check\b/i
537
- ];
538
- var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
539
- function isSpecific(insight) {
540
- const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
541
- if (words.length < MIN_WORD_COUNT) {
542
- return { specific: false, reason: "Insight is too short to be actionable" };
543
- }
544
- for (const pattern of VAGUE_PATTERNS) {
545
- if (pattern.test(insight)) {
546
- return { specific: false, reason: "Insight matches a vague pattern" };
598
+ async function countTombstones(repoRoot) {
599
+ const lines = await parseRawJsonlLines(repoRoot);
600
+ let count = 0;
601
+ for (const { parsed } of lines) {
602
+ if (parsed && parsed["deleted"] === true) {
603
+ count++;
547
604
  }
548
605
  }
549
- if (GENERIC_IMPERATIVE_PATTERN.test(insight)) {
550
- return { specific: false, reason: "Insight matches a vague pattern" };
551
- }
552
- return { specific: true };
606
+ return count;
553
607
  }
554
- var ACTION_PATTERNS = [
555
- /\buse\s+.+\s+instead\s+of\b/i,
556
- // "use X instead of Y"
557
- /\bprefer\s+.+\s+(over|to)\b/i,
558
- // "prefer X over Y" or "prefer X to Y"
559
- /\balways\s+.+\s+when\b/i,
560
- // "always X when Y"
561
- /\bnever\s+.+\s+without\b/i,
562
- // "never X without Y"
563
- /\bavoid\s+(using\s+)?\w+/i,
564
- // "avoid X" or "avoid using X"
565
- /\bcheck\s+.+\s+before\b/i,
566
- // "check X before Y"
567
- /^(run|use|add|remove|install|update|configure|set|enable|disable)\s+/i
568
- // Imperative commands at start
608
+ async function needsCompaction(repoRoot) {
609
+ const count = await countTombstones(repoRoot);
610
+ return count >= TOMBSTONE_THRESHOLD;
611
+ }
612
+ async function rewriteWithoutTombstones(repoRoot) {
613
+ const filePath = join(repoRoot, LESSONS_PATH);
614
+ const tempPath = filePath + ".tmp";
615
+ const { lessons } = await readLessons(repoRoot);
616
+ const tombstoneCount = await countTombstones(repoRoot);
617
+ await mkdir(dirname(filePath), { recursive: true });
618
+ const lines = lessons.map((lesson) => JSON.stringify(lesson) + "\n");
619
+ await writeFile(tempPath, lines.join(""), "utf-8");
620
+ await rename(tempPath, filePath);
621
+ return tombstoneCount;
622
+ }
623
+ function shouldArchive(lesson) {
624
+ const ageDays = getLessonAgeDays(lesson);
625
+ return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
626
+ }
627
+ async function archiveOldLessons(repoRoot) {
628
+ const { lessons } = await readLessons(repoRoot);
629
+ const toArchive = [];
630
+ const toKeep = [];
631
+ for (const lesson of lessons) {
632
+ if (shouldArchive(lesson)) {
633
+ toArchive.push(lesson);
634
+ } else {
635
+ toKeep.push(lesson);
636
+ }
637
+ }
638
+ if (toArchive.length === 0) {
639
+ return 0;
640
+ }
641
+ const archiveGroups = /* @__PURE__ */ new Map();
642
+ for (const lesson of toArchive) {
643
+ const created = new Date(lesson.created);
644
+ const archivePath = getArchivePath(repoRoot, created);
645
+ const group = archiveGroups.get(archivePath) ?? [];
646
+ group.push(lesson);
647
+ archiveGroups.set(archivePath, group);
648
+ }
649
+ const archiveDir = join(repoRoot, ARCHIVE_DIR);
650
+ await mkdir(archiveDir, { recursive: true });
651
+ for (const [archivePath, archiveLessons] of archiveGroups) {
652
+ const lines2 = archiveLessons.map((l) => JSON.stringify(l) + "\n").join("");
653
+ await appendFile(archivePath, lines2, "utf-8");
654
+ }
655
+ const filePath = join(repoRoot, LESSONS_PATH);
656
+ const tempPath = filePath + ".tmp";
657
+ await mkdir(dirname(filePath), { recursive: true });
658
+ const lines = toKeep.map((lesson) => JSON.stringify(lesson) + "\n");
659
+ await writeFile(tempPath, lines.join(""), "utf-8");
660
+ await rename(tempPath, filePath);
661
+ return toArchive.length;
662
+ }
663
+ async function compact(repoRoot) {
664
+ const tombstonesBefore = await countTombstones(repoRoot);
665
+ const archived = await archiveOldLessons(repoRoot);
666
+ const tombstonesAfterArchive = await countTombstones(repoRoot);
667
+ await rewriteWithoutTombstones(repoRoot);
668
+ const tombstonesRemoved = archived > 0 ? tombstonesBefore : tombstonesAfterArchive;
669
+ const { lessons } = await readLessons(repoRoot);
670
+ return {
671
+ archived,
672
+ tombstonesRemoved,
673
+ lessonsRemaining: lessons.length
674
+ };
675
+ }
676
+
677
+ // src/capture/quality.ts
678
+ var DEFAULT_SIMILARITY_THRESHOLD = 0.8;
679
+ async function isNovel(repoRoot, insight, options = {}) {
680
+ const threshold = options.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
681
+ await syncIfNeeded(repoRoot);
682
+ const words = insight.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
683
+ if (words.length === 0) {
684
+ return { novel: true };
685
+ }
686
+ const searchQuery = words.join(" OR ");
687
+ const results = await searchKeyword(repoRoot, searchQuery, 10);
688
+ if (results.length === 0) {
689
+ return { novel: true };
690
+ }
691
+ const insightWords = new Set(insight.toLowerCase().split(/\s+/));
692
+ for (const lesson of results) {
693
+ const lessonWords = new Set(lesson.insight.toLowerCase().split(/\s+/));
694
+ const intersection = [...insightWords].filter((w) => lessonWords.has(w)).length;
695
+ const union = (/* @__PURE__ */ new Set([...insightWords, ...lessonWords])).size;
696
+ const similarity = union > 0 ? intersection / union : 0;
697
+ if (similarity >= threshold) {
698
+ return {
699
+ novel: false,
700
+ reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
701
+ existingId: lesson.id
702
+ };
703
+ }
704
+ if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
705
+ return {
706
+ novel: false,
707
+ reason: `Exact duplicate found`,
708
+ existingId: lesson.id
709
+ };
710
+ }
711
+ }
712
+ return { novel: true };
713
+ }
714
+ var MIN_WORD_COUNT = 4;
715
+ var VAGUE_PATTERNS = [
716
+ /\bwrite better\b/i,
717
+ /\bbe careful\b/i,
718
+ /\bremember to\b/i,
719
+ /\bmake sure\b/i,
720
+ /\btry to\b/i,
721
+ /\bdouble check\b/i
722
+ ];
723
+ var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
724
+ function isSpecific(insight) {
725
+ const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
726
+ if (words.length < MIN_WORD_COUNT) {
727
+ return { specific: false, reason: "Insight is too short to be actionable" };
728
+ }
729
+ for (const pattern of VAGUE_PATTERNS) {
730
+ if (pattern.test(insight)) {
731
+ return { specific: false, reason: "Insight matches a vague pattern" };
732
+ }
733
+ }
734
+ if (GENERIC_IMPERATIVE_PATTERN.test(insight)) {
735
+ return { specific: false, reason: "Insight matches a vague pattern" };
736
+ }
737
+ return { specific: true };
738
+ }
739
+ var ACTION_PATTERNS = [
740
+ /\buse\s+.+\s+instead\s+of\b/i,
741
+ // "use X instead of Y"
742
+ /\bprefer\s+.+\s+(over|to)\b/i,
743
+ // "prefer X over Y" or "prefer X to Y"
744
+ /\balways\s+.+\s+when\b/i,
745
+ // "always X when Y"
746
+ /\bnever\s+.+\s+without\b/i,
747
+ // "never X without Y"
748
+ /\bavoid\s+(using\s+)?\w+/i,
749
+ // "avoid X" or "avoid using X"
750
+ /\bcheck\s+.+\s+before\b/i,
751
+ // "check X before Y"
752
+ /^(run|use|add|remove|install|update|configure|set|enable|disable)\s+/i
753
+ // Imperative commands at start
569
754
  ];
570
755
  function isActionable(insight) {
571
756
  for (const pattern of ACTION_PATTERNS) {
@@ -720,181 +905,62 @@ async function parseInputFile(filePath) {
720
905
  }
721
906
  return data;
722
907
  }
723
-
724
- // src/utils.ts
725
- var MS_PER_DAY = 24 * 60 * 60 * 1e3;
726
- function getLessonAgeDays(lesson) {
727
- const created = new Date(lesson.created).getTime();
728
- const now = Date.now();
729
- return Math.floor((now - created) / MS_PER_DAY);
908
+ var out = {
909
+ success: (msg) => console.log(chalk.green("[ok]"), msg),
910
+ error: (msg) => console.error(chalk.red("[error]"), msg),
911
+ info: (msg) => console.log(chalk.blue("[info]"), msg),
912
+ warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
913
+ };
914
+ function getGlobalOpts(cmd) {
915
+ const opts = cmd.optsWithGlobals();
916
+ return {
917
+ verbose: opts.verbose ?? false,
918
+ quiet: opts.quiet ?? false
919
+ };
730
920
  }
921
+ var DEFAULT_SEARCH_LIMIT = "10";
922
+ var DEFAULT_LIST_LIMIT = "20";
923
+ var DEFAULT_CHECK_PLAN_LIMIT = "5";
924
+ var LESSON_COUNT_WARNING_THRESHOLD = 20;
925
+ var AGE_FLAG_THRESHOLD_DAYS = 90;
926
+ var ISO_DATE_PREFIX_LENGTH = 10;
927
+ var AVG_DECIMAL_PLACES = 1;
928
+ var RELEVANCE_DECIMAL_PLACES = 2;
929
+ var JSON_INDENT_SPACES = 2;
731
930
 
732
- // src/storage/compact.ts
733
- var ARCHIVE_DIR = ".claude/lessons/archive";
734
- var TOMBSTONE_THRESHOLD = 100;
735
- var ARCHIVE_AGE_DAYS = 90;
736
- var MONTH_INDEX_OFFSET = 1;
737
- var MONTH_PAD_LENGTH = 2;
738
- function getArchivePath(repoRoot, date) {
739
- const year = date.getFullYear();
740
- const month = String(date.getMonth() + MONTH_INDEX_OFFSET).padStart(MONTH_PAD_LENGTH, "0");
741
- return join(repoRoot, ARCHIVE_DIR, `${year}-${month}.jsonl`);
742
- }
743
- async function parseRawJsonlLines(repoRoot) {
744
- const filePath = join(repoRoot, LESSONS_PATH);
745
- let content;
746
- try {
747
- content = await readFile(filePath, "utf-8");
748
- } catch {
749
- return [];
750
- }
751
- const results = [];
752
- for (const line of content.split("\n")) {
753
- const trimmed = line.trim();
754
- if (!trimmed) continue;
755
- try {
756
- const parsed = JSON.parse(trimmed);
757
- results.push({ line: trimmed, parsed });
758
- } catch {
759
- results.push({ line: trimmed, parsed: null });
760
- }
761
- }
762
- return results;
931
+ // src/commands/capture.ts
932
+ function createLessonFromFlags(trigger, insight, confirmed) {
933
+ return {
934
+ id: generateId(insight),
935
+ type: "quick",
936
+ trigger,
937
+ insight,
938
+ tags: [],
939
+ source: "manual",
940
+ context: { tool: "capture", intent: "manual capture" },
941
+ created: (/* @__PURE__ */ new Date()).toISOString(),
942
+ confirmed,
943
+ supersedes: [],
944
+ related: []
945
+ };
763
946
  }
764
- async function countTombstones(repoRoot) {
765
- const lines = await parseRawJsonlLines(repoRoot);
766
- let count = 0;
767
- for (const { parsed } of lines) {
768
- if (parsed && parsed["deleted"] === true) {
769
- count++;
770
- }
771
- }
772
- return count;
947
+ function outputCaptureJson(lesson, saved) {
948
+ console.log(JSON.stringify({
949
+ id: lesson.id,
950
+ trigger: lesson.trigger,
951
+ insight: lesson.insight,
952
+ type: lesson.type,
953
+ saved
954
+ }));
773
955
  }
774
- async function needsCompaction(repoRoot) {
775
- const count = await countTombstones(repoRoot);
776
- return count >= TOMBSTONE_THRESHOLD;
777
- }
778
- async function rewriteWithoutTombstones(repoRoot) {
779
- const filePath = join(repoRoot, LESSONS_PATH);
780
- const tempPath = filePath + ".tmp";
781
- const { lessons } = await readLessons(repoRoot);
782
- const tombstoneCount = await countTombstones(repoRoot);
783
- await mkdir(dirname(filePath), { recursive: true });
784
- const lines = lessons.map((lesson) => JSON.stringify(lesson) + "\n");
785
- await writeFile(tempPath, lines.join(""), "utf-8");
786
- await rename(tempPath, filePath);
787
- return tombstoneCount;
788
- }
789
- function shouldArchive(lesson) {
790
- const ageDays = getLessonAgeDays(lesson);
791
- return ageDays > ARCHIVE_AGE_DAYS && (lesson.retrievalCount === void 0 || lesson.retrievalCount === 0);
792
- }
793
- async function archiveOldLessons(repoRoot) {
794
- const { lessons } = await readLessons(repoRoot);
795
- const toArchive = [];
796
- const toKeep = [];
797
- for (const lesson of lessons) {
798
- if (shouldArchive(lesson)) {
799
- toArchive.push(lesson);
800
- } else {
801
- toKeep.push(lesson);
802
- }
803
- }
804
- if (toArchive.length === 0) {
805
- return 0;
806
- }
807
- const archiveGroups = /* @__PURE__ */ new Map();
808
- for (const lesson of toArchive) {
809
- const created = new Date(lesson.created);
810
- const archivePath = getArchivePath(repoRoot, created);
811
- const group = archiveGroups.get(archivePath) ?? [];
812
- group.push(lesson);
813
- archiveGroups.set(archivePath, group);
814
- }
815
- const archiveDir = join(repoRoot, ARCHIVE_DIR);
816
- await mkdir(archiveDir, { recursive: true });
817
- for (const [archivePath, archiveLessons] of archiveGroups) {
818
- const lines2 = archiveLessons.map((l) => JSON.stringify(l) + "\n").join("");
819
- await appendFile(archivePath, lines2, "utf-8");
820
- }
821
- const filePath = join(repoRoot, LESSONS_PATH);
822
- const tempPath = filePath + ".tmp";
823
- await mkdir(dirname(filePath), { recursive: true });
824
- const lines = toKeep.map((lesson) => JSON.stringify(lesson) + "\n");
825
- await writeFile(tempPath, lines.join(""), "utf-8");
826
- await rename(tempPath, filePath);
827
- return toArchive.length;
828
- }
829
- async function compact(repoRoot) {
830
- const tombstonesBefore = await countTombstones(repoRoot);
831
- const archived = await archiveOldLessons(repoRoot);
832
- const tombstonesAfterArchive = await countTombstones(repoRoot);
833
- await rewriteWithoutTombstones(repoRoot);
834
- const tombstonesRemoved = archived > 0 ? tombstonesBefore : tombstonesAfterArchive;
835
- const { lessons } = await readLessons(repoRoot);
836
- return {
837
- archived,
838
- tombstonesRemoved,
839
- lessonsRemaining: lessons.length
840
- };
841
- }
842
- var out = {
843
- success: (msg) => console.log(chalk.green("[ok]"), msg),
844
- error: (msg) => console.error(chalk.red("[error]"), msg),
845
- info: (msg) => console.log(chalk.blue("[info]"), msg),
846
- warn: (msg) => console.log(chalk.yellow("[warn]"), msg)
847
- };
848
- function getGlobalOpts(cmd) {
849
- const opts = cmd.optsWithGlobals();
850
- return {
851
- verbose: opts.verbose ?? false,
852
- quiet: opts.quiet ?? false
853
- };
854
- }
855
- var DEFAULT_SEARCH_LIMIT = "10";
856
- var DEFAULT_LIST_LIMIT = "20";
857
- var DEFAULT_CHECK_PLAN_LIMIT = "5";
858
- var LESSON_COUNT_WARNING_THRESHOLD = 20;
859
- var AGE_FLAG_THRESHOLD_DAYS = 90;
860
- var ISO_DATE_PREFIX_LENGTH = 10;
861
- var AVG_DECIMAL_PLACES = 1;
862
- var RELEVANCE_DECIMAL_PLACES = 2;
863
- var JSON_INDENT_SPACES = 2;
864
-
865
- // src/commands/capture.ts
866
- function createLessonFromFlags(trigger, insight, confirmed) {
867
- return {
868
- id: generateId(insight),
869
- type: "quick",
870
- trigger,
871
- insight,
872
- tags: [],
873
- source: "manual",
874
- context: { tool: "capture", intent: "manual capture" },
875
- created: (/* @__PURE__ */ new Date()).toISOString(),
876
- confirmed,
877
- supersedes: [],
878
- related: []
879
- };
880
- }
881
- function outputCaptureJson(lesson, saved) {
882
- console.log(JSON.stringify({
883
- id: lesson.id,
884
- trigger: lesson.trigger,
885
- insight: lesson.insight,
886
- type: lesson.type,
887
- saved
888
- }));
889
- }
890
- function outputCapturePreview(lesson) {
891
- console.log("Lesson captured:");
892
- console.log(` ID: ${lesson.id}`);
893
- console.log(` Trigger: ${lesson.trigger}`);
894
- console.log(` Insight: ${lesson.insight}`);
895
- console.log(` Type: ${lesson.type}`);
896
- console.log(` Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
897
- console.log("\nSave this lesson? [y/n]");
956
+ function outputCapturePreview(lesson) {
957
+ console.log("Lesson captured:");
958
+ console.log(` ID: ${lesson.id}`);
959
+ console.log(` Trigger: ${lesson.trigger}`);
960
+ console.log(` Insight: ${lesson.insight}`);
961
+ console.log(` Type: ${lesson.type}`);
962
+ console.log(` Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
963
+ console.log("\nSave this lesson? [y/n]");
898
964
  }
899
965
  function createLessonFromInputFile(result, confirmed) {
900
966
  return {
@@ -1057,153 +1123,249 @@ Saved as lesson: ${lesson.id}`);
1057
1123
  }
1058
1124
  });
1059
1125
  }
1060
- function registerManagementCommands(program2) {
1061
- program2.command("wrong <id>").description("Mark a lesson as invalid/wrong").option("-r, --reason <text>", "Reason for invalidation").action(async function(id, options) {
1126
+ function formatLessonHuman(lesson) {
1127
+ const lines = [];
1128
+ lines.push(`ID: ${lesson.id}`);
1129
+ lines.push(`Type: ${lesson.type}`);
1130
+ lines.push(`Trigger: ${lesson.trigger}`);
1131
+ lines.push(`Insight: ${lesson.insight}`);
1132
+ if (lesson.evidence) {
1133
+ lines.push(`Evidence: ${lesson.evidence}`);
1134
+ }
1135
+ if (lesson.severity) {
1136
+ lines.push(`Severity: ${lesson.severity}`);
1137
+ }
1138
+ lines.push(`Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
1139
+ lines.push(`Source: ${lesson.source}`);
1140
+ if (lesson.context) {
1141
+ lines.push(`Context: ${lesson.context.tool} - ${lesson.context.intent}`);
1142
+ }
1143
+ lines.push(`Created: ${lesson.created}`);
1144
+ lines.push(`Confirmed: ${lesson.confirmed ? "yes" : "no"}`);
1145
+ if (lesson.supersedes && lesson.supersedes.length > 0) {
1146
+ lines.push(`Supersedes: ${lesson.supersedes.join(", ")}`);
1147
+ }
1148
+ if (lesson.related && lesson.related.length > 0) {
1149
+ lines.push(`Related: ${lesson.related.join(", ")}`);
1150
+ }
1151
+ if (lesson.pattern) {
1152
+ lines.push("Pattern:");
1153
+ lines.push(` Bad: ${lesson.pattern.bad}`);
1154
+ lines.push(` Good: ${lesson.pattern.good}`);
1155
+ }
1156
+ return lines.join("\n");
1157
+ }
1158
+ async function wasLessonDeleted(repoRoot, id) {
1159
+ const filePath = join(repoRoot, LESSONS_PATH);
1160
+ try {
1161
+ const content = await readFile(filePath, "utf-8");
1162
+ const lines = content.split("\n");
1163
+ for (const line of lines) {
1164
+ const trimmed = line.trim();
1165
+ if (!trimmed) continue;
1166
+ try {
1167
+ const record = JSON.parse(trimmed);
1168
+ if (record.id === id && record.deleted === true) {
1169
+ return true;
1170
+ }
1171
+ } catch {
1172
+ }
1173
+ }
1174
+ } catch {
1175
+ }
1176
+ return false;
1177
+ }
1178
+
1179
+ // src/commands/management/crud.ts
1180
+ var SHOW_JSON_INDENT = 2;
1181
+ function registerCrudCommands(program2) {
1182
+ program2.command("show <id>").description("Show details of a specific lesson").option("--json", "Output as JSON").action(async (id, options) => {
1062
1183
  const repoRoot = getRepoRoot();
1063
1184
  const { lessons } = await readLessons(repoRoot);
1064
1185
  const lesson = lessons.find((l) => l.id === id);
1065
1186
  if (!lesson) {
1066
- out.error(`Lesson not found: ${id}`);
1187
+ const wasDeleted = await wasLessonDeleted(repoRoot, id);
1188
+ if (options.json) {
1189
+ console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found` }));
1190
+ } else {
1191
+ out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
1192
+ }
1067
1193
  process.exit(1);
1068
1194
  }
1069
- if (lesson.invalidatedAt) {
1070
- out.warn(`Lesson ${id} is already marked as invalid.`);
1071
- return;
1072
- }
1073
- const updatedLesson = {
1074
- ...lesson,
1075
- invalidatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1076
- ...options.reason !== void 0 && { invalidationReason: options.reason }
1077
- };
1078
- await appendLesson(repoRoot, updatedLesson);
1079
- out.success(`Lesson ${id} marked as invalid.`);
1080
- if (options.reason) {
1081
- console.log(` Reason: ${options.reason}`);
1195
+ if (options.json) {
1196
+ console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
1197
+ } else {
1198
+ console.log(formatLessonHuman(lesson));
1082
1199
  }
1083
1200
  });
1084
- program2.command("validate <id>").description("Re-enable a previously invalidated lesson").action(async function(id) {
1201
+ program2.command("update <id>").description("Update a lesson").option("--insight <text>", "Update insight").option("--trigger <text>", "Update trigger").option("--evidence <text>", "Update evidence").option("--severity <level>", "Update severity (low/medium/high)").option("--tags <tags>", "Update tags (comma-separated)").option("--confirmed <bool>", "Update confirmed status (true/false)").option("--json", "Output as JSON").action(async (id, options) => {
1085
1202
  const repoRoot = getRepoRoot();
1203
+ const hasUpdates = options.insight !== void 0 || options.trigger !== void 0 || options.evidence !== void 0 || options.severity !== void 0 || options.tags !== void 0 || options.confirmed !== void 0;
1204
+ if (!hasUpdates) {
1205
+ if (options.json) {
1206
+ console.log(JSON.stringify({ error: "No fields to update (specify at least one: --insight, --tags, --severity, ...)" }));
1207
+ } else {
1208
+ out.error("No fields to update (specify at least one: --insight, --tags, --severity, ...)");
1209
+ }
1210
+ process.exit(1);
1211
+ }
1086
1212
  const { lessons } = await readLessons(repoRoot);
1087
1213
  const lesson = lessons.find((l) => l.id === id);
1088
1214
  if (!lesson) {
1089
- out.error(`Lesson not found: ${id}`);
1215
+ const wasDeleted = await wasLessonDeleted(repoRoot, id);
1216
+ if (options.json) {
1217
+ console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
1218
+ } else {
1219
+ out.error(wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found`);
1220
+ }
1090
1221
  process.exit(1);
1091
1222
  }
1092
- if (!lesson.invalidatedAt) {
1093
- out.info(`Lesson ${id} is not invalidated.`);
1094
- return;
1223
+ if (options.severity !== void 0) {
1224
+ const result = SeveritySchema.safeParse(options.severity);
1225
+ if (!result.success) {
1226
+ if (options.json) {
1227
+ console.log(JSON.stringify({ error: `Invalid severity '${options.severity}' (must be: high, medium, low)` }));
1228
+ } else {
1229
+ out.error(`Invalid severity '${options.severity}' (must be: high, medium, low)`);
1230
+ }
1231
+ process.exit(1);
1232
+ }
1095
1233
  }
1096
1234
  const updatedLesson = {
1097
- id: lesson.id,
1098
- type: lesson.type,
1099
- trigger: lesson.trigger,
1100
- insight: lesson.insight,
1101
- tags: lesson.tags,
1102
- source: lesson.source,
1103
- context: lesson.context,
1104
- created: lesson.created,
1105
- confirmed: lesson.confirmed,
1106
- supersedes: lesson.supersedes,
1107
- related: lesson.related,
1108
- // Include optional fields if present (excluding invalidation)
1109
- ...lesson.evidence !== void 0 && { evidence: lesson.evidence },
1110
- ...lesson.severity !== void 0 && { severity: lesson.severity },
1111
- ...lesson.pattern !== void 0 && { pattern: lesson.pattern },
1112
- ...lesson.deleted !== void 0 && { deleted: lesson.deleted },
1113
- ...lesson.retrievalCount !== void 0 && { retrievalCount: lesson.retrievalCount },
1114
- ...lesson.citation !== void 0 && { citation: lesson.citation },
1115
- ...lesson.compactionLevel !== void 0 && { compactionLevel: lesson.compactionLevel },
1116
- ...lesson.compactedAt !== void 0 && { compactedAt: lesson.compactedAt },
1117
- ...lesson.lastRetrieved !== void 0 && { lastRetrieved: lesson.lastRetrieved }
1235
+ ...lesson,
1236
+ ...options.insight !== void 0 && { insight: options.insight },
1237
+ ...options.trigger !== void 0 && { trigger: options.trigger },
1238
+ ...options.evidence !== void 0 && { evidence: options.evidence },
1239
+ ...options.severity !== void 0 && { severity: options.severity },
1240
+ ...options.tags !== void 0 && {
1241
+ tags: [...new Set(
1242
+ options.tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
1243
+ )]
1244
+ },
1245
+ ...options.confirmed !== void 0 && { confirmed: options.confirmed === "true" }
1118
1246
  };
1119
- await appendLesson(repoRoot, updatedLesson);
1120
- out.success(`Lesson ${id} re-enabled (validated).`);
1121
- });
1122
- program2.command("compact").description("Compact lessons: archive old lessons and remove tombstones").option("-f, --force", "Run compaction even if below threshold").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
1123
- const repoRoot = getRepoRoot();
1124
- const tombstones = await countTombstones(repoRoot);
1125
- const needs = await needsCompaction(repoRoot);
1126
- if (options.dryRun) {
1127
- console.log("Dry run - no changes will be made.\n");
1128
- console.log(`Tombstones found: ${tombstones}`);
1129
- console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
1130
- return;
1247
+ const validationResult = LessonSchema.safeParse(updatedLesson);
1248
+ if (!validationResult.success) {
1249
+ if (options.json) {
1250
+ console.log(JSON.stringify({ error: `Schema validation failed: ${validationResult.error.message}` }));
1251
+ } else {
1252
+ out.error(`Schema validation failed: ${validationResult.error.message}`);
1253
+ }
1254
+ process.exit(1);
1131
1255
  }
1132
- if (!needs && !options.force) {
1133
- console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
1134
- console.log("Use --force to compact anyway.");
1135
- return;
1256
+ await appendLesson(repoRoot, updatedLesson);
1257
+ await syncIfNeeded(repoRoot);
1258
+ if (options.json) {
1259
+ console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT));
1260
+ } else {
1261
+ out.success(`Updated lesson ${id}`);
1136
1262
  }
1137
- console.log("Running compaction...");
1138
- const result = await compact(repoRoot);
1139
- console.log("\nCompaction complete:");
1140
- console.log(` Archived: ${result.archived} lesson(s)`);
1141
- console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
1142
- console.log(` Lessons remaining: ${result.lessonsRemaining}`);
1143
- await rebuildIndex(repoRoot);
1144
- console.log(" Index rebuilt.");
1145
1263
  });
1146
- program2.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
1264
+ program2.command("delete <ids...>").description("Soft delete lessons (creates tombstone)").option("--json", "Output as JSON").action(async (ids, options) => {
1147
1265
  const repoRoot = getRepoRoot();
1148
- if (options.force) {
1149
- console.log("Forcing index rebuild...");
1150
- await rebuildIndex(repoRoot);
1151
- console.log("Index rebuilt.");
1266
+ const { lessons } = await readLessons(repoRoot);
1267
+ const lessonMap = new Map(lessons.map((l) => [l.id, l]));
1268
+ const deleted = [];
1269
+ const warnings = [];
1270
+ for (const id of ids) {
1271
+ const lesson = lessonMap.get(id);
1272
+ if (!lesson) {
1273
+ const wasDeleted = await wasLessonDeleted(repoRoot, id);
1274
+ warnings.push({ id, message: wasDeleted ? "already deleted" : "not found" });
1275
+ continue;
1276
+ }
1277
+ const tombstone = {
1278
+ ...lesson,
1279
+ deleted: true,
1280
+ deletedAt: (/* @__PURE__ */ new Date()).toISOString()
1281
+ };
1282
+ await appendLesson(repoRoot, tombstone);
1283
+ deleted.push(id);
1284
+ }
1285
+ if (deleted.length > 0) {
1286
+ await syncIfNeeded(repoRoot);
1287
+ }
1288
+ if (options.json) {
1289
+ console.log(JSON.stringify({ deleted, warnings }));
1152
1290
  } else {
1153
- const rebuilt = await syncIfNeeded(repoRoot);
1154
- if (rebuilt) {
1155
- console.log("Index rebuilt (JSONL changed).");
1156
- } else {
1157
- console.log("Index is up to date.");
1291
+ if (deleted.length > 0) {
1292
+ out.success(`Deleted ${deleted.length} lesson(s): ${deleted.join(", ")}`);
1293
+ }
1294
+ for (const warning of warnings) {
1295
+ out.warn(`${warning.id}: ${warning.message}`);
1296
+ }
1297
+ if (deleted.length === 0 && warnings.length > 0) {
1298
+ process.exit(1);
1158
1299
  }
1159
1300
  }
1160
1301
  });
1161
- program2.command("stats").description("Show database health and statistics").action(async () => {
1302
+ }
1303
+
1304
+ // src/commands/management/invalidation.ts
1305
+ function registerInvalidationCommands(program2) {
1306
+ program2.command("wrong <id>").description("Mark a lesson as invalid/wrong").option("-r, --reason <text>", "Reason for invalidation").action(async function(id, options) {
1162
1307
  const repoRoot = getRepoRoot();
1163
- await syncIfNeeded(repoRoot);
1164
1308
  const { lessons } = await readLessons(repoRoot);
1165
- const deletedCount = await countTombstones(repoRoot);
1166
- const totalLessons = lessons.length;
1167
- const retrievalStats = getRetrievalStats(repoRoot);
1168
- const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
1169
- const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
1170
- const jsonlPath = join(repoRoot, LESSONS_PATH);
1171
- const dbPath = join(repoRoot, DB_PATH);
1172
- let dataSize = 0;
1173
- let indexSize = 0;
1174
- try {
1175
- dataSize = statSync(jsonlPath).size;
1176
- } catch {
1309
+ const lesson = lessons.find((l) => l.id === id);
1310
+ if (!lesson) {
1311
+ out.error(`Lesson not found: ${id}`);
1312
+ process.exit(1);
1177
1313
  }
1178
- try {
1179
- indexSize = statSync(dbPath).size;
1180
- } catch {
1314
+ if (lesson.invalidatedAt) {
1315
+ out.warn(`Lesson ${id} is already marked as invalid.`);
1316
+ return;
1181
1317
  }
1182
- const totalSize = dataSize + indexSize;
1183
- let recentCount = 0;
1184
- let mediumCount = 0;
1185
- let oldCount = 0;
1186
- for (const lesson of lessons) {
1187
- const ageDays = getLessonAgeDays(lesson);
1188
- if (ageDays < 30) {
1189
- recentCount++;
1190
- } else if (ageDays <= AGE_FLAG_THRESHOLD_DAYS) {
1191
- mediumCount++;
1192
- } else {
1193
- oldCount++;
1194
- }
1318
+ const updatedLesson = {
1319
+ ...lesson,
1320
+ invalidatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1321
+ ...options.reason !== void 0 && { invalidationReason: options.reason }
1322
+ };
1323
+ await appendLesson(repoRoot, updatedLesson);
1324
+ out.success(`Lesson ${id} marked as invalid.`);
1325
+ if (options.reason) {
1326
+ console.log(` Reason: ${options.reason}`);
1195
1327
  }
1196
- const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
1197
- console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
1198
- if (totalLessons > LESSON_COUNT_WARNING_THRESHOLD) {
1199
- out.warn(`High lesson count may degrade retrieval quality. Consider running \`lna compact\`.`);
1328
+ });
1329
+ program2.command("validate <id>").description("Re-enable a previously invalidated lesson").action(async function(id) {
1330
+ const repoRoot = getRepoRoot();
1331
+ const { lessons } = await readLessons(repoRoot);
1332
+ const lesson = lessons.find((l) => l.id === id);
1333
+ if (!lesson) {
1334
+ out.error(`Lesson not found: ${id}`);
1335
+ process.exit(1);
1200
1336
  }
1201
- if (totalLessons > 0) {
1202
- console.log(`Age: ${recentCount} <30d, ${mediumCount} 30-90d, ${oldCount} >90d`);
1337
+ if (!lesson.invalidatedAt) {
1338
+ out.info(`Lesson ${id} is not invalidated.`);
1339
+ return;
1203
1340
  }
1204
- console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
1205
- console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
1341
+ const updatedLesson = {
1342
+ id: lesson.id,
1343
+ type: lesson.type,
1344
+ trigger: lesson.trigger,
1345
+ insight: lesson.insight,
1346
+ tags: lesson.tags,
1347
+ source: lesson.source,
1348
+ context: lesson.context,
1349
+ created: lesson.created,
1350
+ confirmed: lesson.confirmed,
1351
+ supersedes: lesson.supersedes,
1352
+ related: lesson.related,
1353
+ // Include optional fields if present (excluding invalidation)
1354
+ ...lesson.evidence !== void 0 && { evidence: lesson.evidence },
1355
+ ...lesson.severity !== void 0 && { severity: lesson.severity },
1356
+ ...lesson.pattern !== void 0 && { pattern: lesson.pattern },
1357
+ ...lesson.deleted !== void 0 && { deleted: lesson.deleted },
1358
+ ...lesson.retrievalCount !== void 0 && { retrievalCount: lesson.retrievalCount },
1359
+ ...lesson.citation !== void 0 && { citation: lesson.citation },
1360
+ ...lesson.compactionLevel !== void 0 && { compactionLevel: lesson.compactionLevel },
1361
+ ...lesson.compactedAt !== void 0 && { compactedAt: lesson.compactedAt },
1362
+ ...lesson.lastRetrieved !== void 0 && { lastRetrieved: lesson.lastRetrieved }
1363
+ };
1364
+ await appendLesson(repoRoot, updatedLesson);
1365
+ out.success(`Lesson ${id} re-enabled (validated).`);
1206
1366
  });
1367
+ }
1368
+ function registerIOCommands(program2) {
1207
1369
  program2.command("export").description("Export lessons as JSON to stdout").option("--since <date>", "Only include lessons created after this date (ISO8601)").option("--tags <tags>", "Filter by tags (comma-separated, OR logic)").action(async (options) => {
1208
1370
  const repoRoot = getRepoRoot();
1209
1371
  const { lessons } = await readLessons(repoRoot);
@@ -1276,215 +1438,132 @@ function registerManagementCommands(program2) {
1276
1438
  console.log(`Imported ${imported} ${lessonWord}`);
1277
1439
  }
1278
1440
  });
1279
- const SHOW_JSON_INDENT = 2;
1280
- program2.command("show <id>").description("Show details of a specific lesson").option("--json", "Output as JSON").action(async (id, options) => {
1441
+ }
1442
+ function registerMaintenanceCommands(program2) {
1443
+ program2.command("compact").description("Compact lessons: archive old lessons and remove tombstones").option("-f, --force", "Run compaction even if below threshold").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
1281
1444
  const repoRoot = getRepoRoot();
1282
- const { lessons } = await readLessons(repoRoot);
1283
- const lesson = lessons.find((l) => l.id === id);
1284
- if (!lesson) {
1285
- const filePath = join(repoRoot, LESSONS_PATH);
1286
- let wasDeleted = false;
1287
- try {
1288
- const content = await readFile(filePath, "utf-8");
1289
- const lines = content.split("\n");
1290
- for (const line of lines) {
1291
- const trimmed = line.trim();
1292
- if (!trimmed) continue;
1293
- try {
1294
- const record = JSON.parse(trimmed);
1295
- if (record.id === id && record.deleted === true) {
1296
- wasDeleted = true;
1297
- break;
1298
- }
1299
- } catch {
1300
- }
1301
- }
1302
- } catch {
1303
- }
1304
- if (options.json) {
1305
- console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found` }));
1306
- } else {
1307
- out.error(wasDeleted ? `Lesson ${id} not found (deleted)` : `Lesson ${id} not found`);
1308
- }
1309
- process.exit(1);
1445
+ const tombstones = await countTombstones(repoRoot);
1446
+ const needs = await needsCompaction(repoRoot);
1447
+ if (options.dryRun) {
1448
+ console.log("Dry run - no changes will be made.\n");
1449
+ console.log(`Tombstones found: ${tombstones}`);
1450
+ console.log(`Compaction needed: ${needs ? "yes" : "no"}`);
1451
+ return;
1310
1452
  }
1311
- if (options.json) {
1312
- console.log(JSON.stringify(lesson, null, SHOW_JSON_INDENT));
1313
- } else {
1314
- console.log(formatLessonHuman(lesson));
1453
+ if (!needs && !options.force) {
1454
+ console.log(`Compaction not needed (${tombstones} tombstones, threshold is ${TOMBSTONE_THRESHOLD}).`);
1455
+ console.log("Use --force to compact anyway.");
1456
+ return;
1315
1457
  }
1458
+ console.log("Running compaction...");
1459
+ const result = await compact(repoRoot);
1460
+ console.log("\nCompaction complete:");
1461
+ console.log(` Archived: ${result.archived} lesson(s)`);
1462
+ console.log(` Tombstones removed: ${result.tombstonesRemoved}`);
1463
+ console.log(` Lessons remaining: ${result.lessonsRemaining}`);
1464
+ await rebuildIndex(repoRoot);
1465
+ console.log(" Index rebuilt.");
1316
1466
  });
1317
- program2.command("update <id>").description("Update a lesson").option("--insight <text>", "Update insight").option("--trigger <text>", "Update trigger").option("--evidence <text>", "Update evidence").option("--severity <level>", "Update severity (low/medium/high)").option("--tags <tags>", "Update tags (comma-separated)").option("--confirmed <bool>", "Update confirmed status (true/false)").option("--json", "Output as JSON").action(async (id, options) => {
1467
+ program2.command("rebuild").description("Rebuild SQLite index from JSONL").option("-f, --force", "Force rebuild even if unchanged").action(async (options) => {
1318
1468
  const repoRoot = getRepoRoot();
1319
- const hasUpdates = options.insight !== void 0 || options.trigger !== void 0 || options.evidence !== void 0 || options.severity !== void 0 || options.tags !== void 0 || options.confirmed !== void 0;
1320
- if (!hasUpdates) {
1321
- if (options.json) {
1322
- console.log(JSON.stringify({ error: "No fields to update (specify at least one: --insight, --tags, --severity, ...)" }));
1469
+ if (options.force) {
1470
+ console.log("Forcing index rebuild...");
1471
+ await rebuildIndex(repoRoot);
1472
+ console.log("Index rebuilt.");
1473
+ } else {
1474
+ const rebuilt = await syncIfNeeded(repoRoot);
1475
+ if (rebuilt) {
1476
+ console.log("Index rebuilt (JSONL changed).");
1323
1477
  } else {
1324
- out.error("No fields to update (specify at least one: --insight, --tags, --severity, ...)");
1478
+ console.log("Index is up to date.");
1325
1479
  }
1326
- process.exit(1);
1327
1480
  }
1481
+ });
1482
+ program2.command("stats").description("Show database health and statistics").action(async () => {
1483
+ const repoRoot = getRepoRoot();
1484
+ await syncIfNeeded(repoRoot);
1328
1485
  const { lessons } = await readLessons(repoRoot);
1329
- const lesson = lessons.find((l) => l.id === id);
1330
- if (!lesson) {
1331
- const filePath = join(repoRoot, LESSONS_PATH);
1332
- let wasDeleted = false;
1333
- try {
1334
- const content = await readFile(filePath, "utf-8");
1335
- const lines = content.split("\n");
1336
- for (const line of lines) {
1337
- const trimmed = line.trim();
1338
- if (!trimmed) continue;
1339
- try {
1340
- const record = JSON.parse(trimmed);
1341
- if (record.id === id && record.deleted === true) {
1342
- wasDeleted = true;
1343
- break;
1344
- }
1345
- } catch {
1346
- }
1347
- }
1348
- } catch {
1349
- }
1350
- if (options.json) {
1351
- console.log(JSON.stringify({ error: wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found` }));
1352
- } else {
1353
- out.error(wasDeleted ? `Lesson ${id} is deleted` : `Lesson ${id} not found`);
1354
- }
1355
- process.exit(1);
1356
- }
1357
- if (options.severity !== void 0) {
1358
- const result = SeveritySchema.safeParse(options.severity);
1359
- if (!result.success) {
1360
- if (options.json) {
1361
- console.log(JSON.stringify({ error: `Invalid severity '${options.severity}' (must be: high, medium, low)` }));
1362
- } else {
1363
- out.error(`Invalid severity '${options.severity}' (must be: high, medium, low)`);
1364
- }
1365
- process.exit(1);
1366
- }
1367
- }
1368
- const updatedLesson = {
1369
- ...lesson,
1370
- ...options.insight !== void 0 && { insight: options.insight },
1371
- ...options.trigger !== void 0 && { trigger: options.trigger },
1372
- ...options.evidence !== void 0 && { evidence: options.evidence },
1373
- ...options.severity !== void 0 && { severity: options.severity },
1374
- ...options.tags !== void 0 && {
1375
- tags: [...new Set(
1376
- options.tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
1377
- )]
1378
- },
1379
- ...options.confirmed !== void 0 && { confirmed: options.confirmed === "true" }
1380
- };
1381
- const validationResult = LessonSchema.safeParse(updatedLesson);
1382
- if (!validationResult.success) {
1383
- if (options.json) {
1384
- console.log(JSON.stringify({ error: `Schema validation failed: ${validationResult.error.message}` }));
1385
- } else {
1386
- out.error(`Schema validation failed: ${validationResult.error.message}`);
1387
- }
1388
- process.exit(1);
1486
+ const deletedCount = await countTombstones(repoRoot);
1487
+ const totalLessons = lessons.length;
1488
+ const retrievalStats = getRetrievalStats(repoRoot);
1489
+ const totalRetrievals = retrievalStats.reduce((sum, s) => sum + s.count, 0);
1490
+ const avgRetrievals = totalLessons > 0 ? (totalRetrievals / totalLessons).toFixed(AVG_DECIMAL_PLACES) : "0.0";
1491
+ const jsonlPath = join(repoRoot, LESSONS_PATH);
1492
+ const dbPath = join(repoRoot, DB_PATH);
1493
+ let dataSize = 0;
1494
+ let indexSize = 0;
1495
+ try {
1496
+ dataSize = statSync(jsonlPath).size;
1497
+ } catch {
1389
1498
  }
1390
- await appendLesson(repoRoot, updatedLesson);
1391
- await syncIfNeeded(repoRoot);
1392
- if (options.json) {
1393
- console.log(JSON.stringify(updatedLesson, null, SHOW_JSON_INDENT));
1394
- } else {
1395
- out.success(`Updated lesson ${id}`);
1499
+ try {
1500
+ indexSize = statSync(dbPath).size;
1501
+ } catch {
1396
1502
  }
1397
- });
1398
- program2.command("delete <ids...>").description("Soft delete lessons (creates tombstone)").option("--json", "Output as JSON").action(async (ids, options) => {
1399
- const repoRoot = getRepoRoot();
1400
- const { lessons } = await readLessons(repoRoot);
1401
- const lessonMap = new Map(lessons.map((l) => [l.id, l]));
1402
- const deleted = [];
1403
- const warnings = [];
1404
- for (const id of ids) {
1405
- const lesson = lessonMap.get(id);
1406
- if (!lesson) {
1407
- const wasDeleted = await wasLessonDeleted(repoRoot, id);
1408
- warnings.push({ id, message: wasDeleted ? "already deleted" : "not found" });
1409
- continue;
1503
+ const totalSize = dataSize + indexSize;
1504
+ let recentCount = 0;
1505
+ let mediumCount = 0;
1506
+ let oldCount = 0;
1507
+ for (const lesson of lessons) {
1508
+ const ageDays = getLessonAgeDays(lesson);
1509
+ if (ageDays < 30) {
1510
+ recentCount++;
1511
+ } else if (ageDays <= AGE_FLAG_THRESHOLD_DAYS) {
1512
+ mediumCount++;
1513
+ } else {
1514
+ oldCount++;
1410
1515
  }
1411
- const tombstone = {
1412
- ...lesson,
1413
- deleted: true,
1414
- deletedAt: (/* @__PURE__ */ new Date()).toISOString()
1415
- };
1416
- await appendLesson(repoRoot, tombstone);
1417
- deleted.push(id);
1418
1516
  }
1419
- if (deleted.length > 0) {
1420
- await syncIfNeeded(repoRoot);
1517
+ const deletedInfo = deletedCount > 0 ? ` (${deletedCount} deleted)` : "";
1518
+ console.log(`Lessons: ${totalLessons} total${deletedInfo}`);
1519
+ if (totalLessons > LESSON_COUNT_WARNING_THRESHOLD) {
1520
+ out.warn(`High lesson count may degrade retrieval quality. Consider running \`lna compact\`.`);
1421
1521
  }
1422
- if (options.json) {
1423
- console.log(JSON.stringify({ deleted, warnings }));
1424
- } else {
1425
- if (deleted.length > 0) {
1426
- out.success(`Deleted ${deleted.length} lesson(s): ${deleted.join(", ")}`);
1427
- }
1428
- for (const warning of warnings) {
1429
- out.warn(`${warning.id}: ${warning.message}`);
1430
- }
1431
- if (deleted.length === 0 && warnings.length > 0) {
1432
- process.exit(1);
1433
- }
1522
+ if (totalLessons > 0) {
1523
+ console.log(`Age: ${recentCount} <30d, ${mediumCount} 30-90d, ${oldCount} >90d`);
1434
1524
  }
1525
+ console.log(`Retrievals: ${totalRetrievals} total, ${avgRetrievals} avg per lesson`);
1526
+ console.log(`Storage: ${formatBytes(totalSize)} (index: ${formatBytes(indexSize)}, data: ${formatBytes(dataSize)})`);
1435
1527
  });
1436
1528
  }
1437
- function formatLessonHuman(lesson) {
1438
- const lines = [];
1439
- lines.push(`ID: ${lesson.id}`);
1440
- lines.push(`Type: ${lesson.type}`);
1441
- lines.push(`Trigger: ${lesson.trigger}`);
1442
- lines.push(`Insight: ${lesson.insight}`);
1443
- if (lesson.evidence) {
1444
- lines.push(`Evidence: ${lesson.evidence}`);
1445
- }
1446
- if (lesson.severity) {
1447
- lines.push(`Severity: ${lesson.severity}`);
1448
- }
1449
- lines.push(`Tags: ${lesson.tags.length > 0 ? lesson.tags.join(", ") : "(none)"}`);
1450
- lines.push(`Source: ${lesson.source}`);
1451
- if (lesson.context) {
1452
- lines.push(`Context: ${lesson.context.tool} - ${lesson.context.intent}`);
1453
- }
1454
- lines.push(`Created: ${lesson.created}`);
1455
- lines.push(`Confirmed: ${lesson.confirmed ? "yes" : "no"}`);
1456
- if (lesson.supersedes && lesson.supersedes.length > 0) {
1457
- lines.push(`Supersedes: ${lesson.supersedes.join(", ")}`);
1458
- }
1459
- if (lesson.related && lesson.related.length > 0) {
1460
- lines.push(`Related: ${lesson.related.join(", ")}`);
1461
- }
1462
- if (lesson.pattern) {
1463
- lines.push("Pattern:");
1464
- lines.push(` Bad: ${lesson.pattern.bad}`);
1465
- lines.push(` Good: ${lesson.pattern.good}`);
1466
- }
1467
- return lines.join("\n");
1529
+
1530
+ // src/commands/management/prime.ts
1531
+ var PRIME_WORKFLOW_CONTEXT = `# Learning Agent Workflow
1532
+
1533
+ ## Core Rules
1534
+ - **NEVER** edit .claude/lessons/index.jsonl directly
1535
+ - Use CLI commands: \`lna learn\`, \`lna list\`, \`lna show\`
1536
+ - Lessons load automatically at session start
1537
+
1538
+ ## When to Capture Lessons
1539
+ - User corrects you ("no", "wrong", "actually...")
1540
+ - You self-correct after multiple attempts
1541
+ - Test fails then you fix it
1542
+
1543
+ ## Commands
1544
+ - \`lna learn "insight"\` - Capture a lesson
1545
+ - \`lna list\` - Show all lessons
1546
+ - \`lna check-plan --plan "..."\` - Get relevant lessons for plan
1547
+ - \`lna stats\` - Show database health
1548
+
1549
+ ## Quality Gate (ALL must pass before proposing)
1550
+ - Novel (not already stored)
1551
+ - Specific (clear guidance)
1552
+ - Actionable (obvious what to do)
1553
+ `;
1554
+ function registerPrimeCommand(program2) {
1555
+ program2.command("prime").description("Output workflow context for Claude Code").action(() => {
1556
+ console.log(PRIME_WORKFLOW_CONTEXT);
1557
+ });
1468
1558
  }
1469
- async function wasLessonDeleted(repoRoot, id) {
1470
- const filePath = join(repoRoot, LESSONS_PATH);
1471
- try {
1472
- const content = await readFile(filePath, "utf-8");
1473
- const lines = content.split("\n");
1474
- for (const line of lines) {
1475
- const trimmed = line.trim();
1476
- if (!trimmed) continue;
1477
- try {
1478
- const record = JSON.parse(trimmed);
1479
- if (record.id === id && record.deleted === true) {
1480
- return true;
1481
- }
1482
- } catch {
1483
- }
1484
- }
1485
- } catch {
1486
- }
1487
- return false;
1559
+
1560
+ // src/commands/management/index.ts
1561
+ function registerManagementCommands(program2) {
1562
+ registerInvalidationCommands(program2);
1563
+ registerMaintenanceCommands(program2);
1564
+ registerIOCommands(program2);
1565
+ registerPrimeCommand(program2);
1566
+ registerCrudCommands(program2);
1488
1567
  }
1489
1568
  var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
1490
1569
  var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
@@ -1617,7 +1696,7 @@ async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
1617
1696
  return { lessons: topLessons, message };
1618
1697
  }
1619
1698
  function formatLessonsCheck(lessons) {
1620
- const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
1699
+ const header = "Lessons Check\n" + "\u2500".repeat(40);
1621
1700
  if (lessons.length === 0) {
1622
1701
  return `${header}
1623
1702
  No relevant lessons found for this plan.`;
@@ -1632,7 +1711,7 @@ ${lessonLines.join("\n")}`;
1632
1711
  }
1633
1712
 
1634
1713
  // src/index.ts
1635
- var VERSION = "0.1.0";
1714
+ var VERSION = "0.2.3";
1636
1715
 
1637
1716
  // src/commands/retrieval.ts
1638
1717
  async function readPlanFromStdin() {
@@ -1807,12 +1886,12 @@ function registerRetrievalCommands(program2) {
1807
1886
  if (options.json) {
1808
1887
  console.log(JSON.stringify({
1809
1888
  error: "Embedding model not available",
1810
- action: "Run: npx learning-agent download-model"
1889
+ action: "Run: npx lna download-model"
1811
1890
  }));
1812
1891
  } else {
1813
1892
  out.error("Embedding model not available");
1814
1893
  console.log("");
1815
- console.log("Run: npx learning-agent download-model");
1894
+ console.log("Run: npx lna download-model");
1816
1895
  }
1817
1896
  process.exit(1);
1818
1897
  }
@@ -1838,34 +1917,69 @@ function registerRetrievalCommands(program2) {
1838
1917
  }
1839
1918
  });
1840
1919
  }
1920
+
1921
+ // src/commands/setup/templates.ts
1841
1922
  var PRE_COMMIT_MESSAGE = `Before committing, have you captured any valuable lessons from this session?
1842
1923
  Consider: corrections, mistakes, or insights worth remembering.
1843
1924
 
1844
1925
  To capture a lesson:
1845
- npx learning-agent capture --trigger "what happened" --insight "what to do" --yes`;
1926
+ npx lna capture --trigger "what happened" --insight "what to do" --yes`;
1846
1927
  var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
1847
1928
  # Learning Agent pre-commit hook
1848
1929
  # Reminds Claude to consider capturing lessons before commits
1849
1930
 
1850
- npx learning-agent hooks run pre-commit
1931
+ npx lna hooks run pre-commit
1932
+ `;
1933
+ var HOOK_MARKER = "# Learning Agent pre-commit hook";
1934
+ var LEARNING_AGENT_HOOK_BLOCK = `
1935
+ # Learning Agent pre-commit hook (appended)
1936
+ npx lna hooks run pre-commit
1851
1937
  `;
1852
- var CLAUDE_HOOK_MARKER = "learning-agent load-session";
1938
+ var CLAUDE_HOOK_MARKERS = ["lna load-session", "learning-agent load-session"];
1853
1939
  var CLAUDE_HOOK_CONFIG = {
1854
1940
  matcher: "startup|resume|compact",
1855
1941
  hooks: [
1856
1942
  {
1857
1943
  type: "command",
1858
- command: "npx learning-agent load-session 2>/dev/null || true"
1944
+ command: "npx lna load-session 2>/dev/null || true"
1859
1945
  }
1860
1946
  ]
1861
1947
  };
1862
- var HOOK_MARKER = "# Learning Agent pre-commit hook";
1863
1948
  var LEARNING_AGENT_SECTION_HEADER = "## Learning Agent Integration";
1949
+ var CLAUDE_REF_START_MARKER = "<!-- learning-agent:claude-ref:start -->";
1950
+ var CLAUDE_REF_END_MARKER = "<!-- learning-agent:claude-ref:end -->";
1951
+ var CLAUDE_MD_REFERENCE = `
1952
+ ${CLAUDE_REF_START_MARKER}
1953
+ ## Learning Agent
1954
+ See AGENTS.md for lesson capture workflow.
1955
+ ${CLAUDE_REF_END_MARKER}
1956
+ `;
1957
+ var AGENTS_SECTION_START_MARKER = "<!-- learning-agent:start -->";
1958
+ var AGENTS_SECTION_END_MARKER = "<!-- learning-agent:end -->";
1864
1959
  var AGENTS_MD_TEMPLATE = `
1960
+ ${AGENTS_SECTION_START_MARKER}
1865
1961
  ## Learning Agent Integration
1866
1962
 
1867
1963
  This project uses learning-agent for session memory.
1868
1964
 
1965
+ ### CRITICAL RULES
1966
+
1967
+ #### Never Edit JSONL Directly
1968
+
1969
+ **WARNING: NEVER edit .claude/lessons/index.jsonl directly.**
1970
+
1971
+ The JSONL file is the source of truth and requires:
1972
+ - Proper ID generation
1973
+ - Schema validation
1974
+ - SQLite index sync
1975
+
1976
+ Always use CLI commands:
1977
+ - \`npx lna learn "insight"\` - Add a lesson
1978
+ - \`npx lna update <id> --insight "new"\` - Modify a lesson
1979
+ - \`npx lna delete <id>\` - Remove a lesson
1980
+
1981
+ Manual edits will break validation and corrupt the SQLite sync.
1982
+
1869
1983
  ### Retrieval Points
1870
1984
 
1871
1985
  - **Session start**: High-severity lessons loaded automatically
@@ -1876,7 +1990,7 @@ This project uses learning-agent for session memory.
1876
1990
  **BEFORE implementing any plan**, run:
1877
1991
 
1878
1992
  \`\`\`bash
1879
- npx learning-agent check-plan --plan "your plan description" --json
1993
+ npx lna check-plan --plan "your plan description" --json
1880
1994
  \`\`\`
1881
1995
 
1882
1996
  Display results as a **Lessons Check** section after your plan:
@@ -1889,6 +2003,33 @@ Display results as a **Lessons Check** section after your plan:
1889
2003
 
1890
2004
  Consider each lesson while implementing.
1891
2005
 
2006
+ ### When to Capture Lessons (Detection Triggers)
2007
+
2008
+ Watch for these patterns and propose \`lna learn\`:
2009
+
2010
+ **User correction**: User says "no", "wrong", "actually..."
2011
+ - Action: Propose a lesson capturing the correct approach
2012
+
2013
+ **Self-correction**: You fix after multiple attempts (edit -> fail -> re-edit)
2014
+ - Action: Propose a lesson about what finally worked
2015
+
2016
+ **Test failure fix**: Test fails -> you diagnose -> fix -> passes
2017
+ - Action: Propose a lesson about the root cause and solution
2018
+
2019
+ ### Auto-Invoke Trigger Phrases
2020
+
2021
+ **Capture triggers** (propose \`lna learn\`):
2022
+ - "that worked" - User confirms a solution worked
2023
+ - "fixed it" - Problem was resolved
2024
+ - "my mistake" - User acknowledges an error
2025
+ - "actually use X" - User specifies a preference
2026
+
2027
+ **Retrieval triggers** (run \`lna check-plan\` or \`lna search\`):
2028
+ - "similar issue" - User recalls a past problem
2029
+ - "we had this before" - Reference to previous experience
2030
+ - "seen this" - Pattern recognition
2031
+ - "remember when" - Memory recall request
2032
+
1892
2033
  ### Proposing Lessons
1893
2034
 
1894
2035
  Propose when: user correction, self-correction, test failure fix, or manual request.
@@ -1912,45 +2053,428 @@ Before closing a session, reflect on lessons learned:
1912
2053
  1. **Review**: What mistakes or corrections happened?
1913
2054
  2. **Quality gate**: Is it novel, specific, actionable?
1914
2055
  3. **Propose**: "Learned: [insight]. Save? [y/n]"
1915
- 4. **Capture**: \`npx learning-agent capture --trigger "..." --insight "..." --yes\`
2056
+ 4. **Capture**: \`npx lna capture --trigger "..." --insight "..." --yes\`
1916
2057
 
1917
2058
  ### CLI Commands
1918
2059
 
1919
2060
  \`\`\`bash
1920
- npx learning-agent load-session --json # Session start
1921
- npx learning-agent check-plan --plan "..." --json # Before implementing
1922
- npx learning-agent capture --trigger "..." --insight "..." --yes
2061
+ npx lna load-session --json # Session start
2062
+ npx lna check-plan --plan "..." --json # Before implementing
2063
+ npx lna learn "insight" # Capture a lesson
2064
+ npx lna capture --trigger "..." --insight "..." --yes
1923
2065
  \`\`\`
1924
2066
 
1925
2067
  See [AGENTS.md](https://github.com/Nathandela/learning_agent/blob/main/AGENTS.md) for full documentation.
2068
+ ${AGENTS_SECTION_END_MARKER}
1926
2069
  `;
1927
- function hasLearningAgentSection(content) {
1928
- return content.includes(LEARNING_AGENT_SECTION_HEADER);
2070
+ var SLASH_COMMANDS = {
2071
+ "learn.md": `Capture a lesson from this session.
2072
+
2073
+ Usage: /learn <insight>
2074
+
2075
+ Examples:
2076
+ - /learn "Always use Polars for large CSV files"
2077
+ - /learn "API requires X-Request-ID header"
2078
+
2079
+ \`\`\`bash
2080
+ npx lna learn "$ARGUMENTS"
2081
+ \`\`\`
2082
+ `,
2083
+ "check-plan.md": `Retrieve relevant lessons for a plan before implementing.
2084
+
2085
+ Usage: /check-plan <plan description>
2086
+
2087
+ \`\`\`bash
2088
+ npx lna check-plan --plan "$ARGUMENTS" --json
2089
+ \`\`\`
2090
+ `,
2091
+ "list.md": `Show all stored lessons.
2092
+
2093
+ \`\`\`bash
2094
+ npx lna list
2095
+ \`\`\`
2096
+ `,
2097
+ "prime.md": `Load learning-agent workflow context after compaction or context loss.
2098
+
2099
+ \`\`\`bash
2100
+ npx lna prime
2101
+ \`\`\`
2102
+ `,
2103
+ "show.md": `Show details of a specific lesson.
2104
+
2105
+ Usage: /show <lesson-id>
2106
+
2107
+ \`\`\`bash
2108
+ npx lna show "$ARGUMENTS"
2109
+ \`\`\`
2110
+ `,
2111
+ "wrong.md": `Mark a lesson as incorrect or invalid.
2112
+
2113
+ Usage: /wrong <lesson-id>
2114
+
2115
+ \`\`\`bash
2116
+ npx lna wrong "$ARGUMENTS"
2117
+ \`\`\`
2118
+ `,
2119
+ "stats.md": `Show learning-agent database statistics and health.
2120
+
2121
+ \`\`\`bash
2122
+ npx lna stats
2123
+ \`\`\`
2124
+ `
2125
+ };
2126
+ var PLUGIN_MANIFEST = {
2127
+ name: "learning-agent",
2128
+ description: "Session memory for Claude Code - capture and retrieve lessons",
2129
+ version: "0.2.2",
2130
+ author: {
2131
+ name: "Nathan Delacr\xE9taz",
2132
+ url: "https://github.com/Nathandela"
2133
+ },
2134
+ repository: "https://github.com/Nathandela/learning_agent",
2135
+ license: "MIT",
2136
+ hooks: {
2137
+ SessionStart: [
2138
+ {
2139
+ matcher: "",
2140
+ hooks: [
2141
+ { type: "command", command: "npx lna prime 2>/dev/null || true" },
2142
+ { type: "command", command: "npx lna load-session 2>/dev/null || true" }
2143
+ ]
2144
+ }
2145
+ ],
2146
+ PreCompact: [
2147
+ {
2148
+ matcher: "",
2149
+ hooks: [{ type: "command", command: "npx lna prime 2>/dev/null || true" }]
2150
+ }
2151
+ ]
2152
+ }
2153
+ };
2154
+
2155
+ // src/commands/setup/claude-helpers.ts
2156
+ function getClaudeSettingsPath(global) {
2157
+ if (global) {
2158
+ return join(homedir(), ".claude", "settings.json");
2159
+ }
2160
+ const repoRoot = getRepoRoot();
2161
+ return join(repoRoot, ".claude", "settings.json");
1929
2162
  }
1930
- async function createLessonsDirectory(repoRoot) {
1931
- const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
1932
- await mkdir(lessonsDir, { recursive: true });
2163
+ async function readClaudeSettings(settingsPath) {
2164
+ if (!existsSync(settingsPath)) {
2165
+ return {};
2166
+ }
2167
+ const content = await readFile(settingsPath, "utf-8");
2168
+ return JSON.parse(content);
1933
2169
  }
1934
- async function createIndexFile(repoRoot) {
1935
- const indexPath = join(repoRoot, LESSONS_PATH);
1936
- if (!existsSync(indexPath)) {
1937
- await writeFile(indexPath, "", "utf-8");
2170
+ function hasClaudeHook(settings) {
2171
+ const hooks = settings.hooks;
2172
+ if (!hooks?.SessionStart) return false;
2173
+ return hooks.SessionStart.some((entry) => {
2174
+ const hookEntry = entry;
2175
+ return hookEntry.hooks?.some(
2176
+ (h) => CLAUDE_HOOK_MARKERS.some((marker) => h.command?.includes(marker))
2177
+ );
2178
+ });
2179
+ }
2180
+ function addLearningAgentHook(settings) {
2181
+ if (!settings.hooks) {
2182
+ settings.hooks = {};
1938
2183
  }
2184
+ const hooks = settings.hooks;
2185
+ if (!hooks.SessionStart) {
2186
+ hooks.SessionStart = [];
2187
+ }
2188
+ hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
1939
2189
  }
1940
- async function updateAgentsMd(repoRoot) {
2190
+ function removeLearningAgentHook(settings) {
2191
+ const hooks = settings.hooks;
2192
+ if (!hooks?.SessionStart) return false;
2193
+ const originalLength = hooks.SessionStart.length;
2194
+ hooks.SessionStart = hooks.SessionStart.filter((entry) => {
2195
+ const hookEntry = entry;
2196
+ return !hookEntry.hooks?.some(
2197
+ (h) => CLAUDE_HOOK_MARKERS.some((marker) => h.command?.includes(marker))
2198
+ );
2199
+ });
2200
+ return hooks.SessionStart.length < originalLength;
2201
+ }
2202
+ async function writeClaudeSettings(settingsPath, settings) {
2203
+ const dir = dirname(settingsPath);
2204
+ await mkdir(dir, { recursive: true });
2205
+ const tempPath = settingsPath + ".tmp";
2206
+ await writeFile(tempPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2207
+ await rename(tempPath, settingsPath);
2208
+ }
2209
+ async function installClaudeHooksForInit(repoRoot) {
2210
+ const settingsPath = join(repoRoot, ".claude", "settings.json");
2211
+ let settings;
2212
+ try {
2213
+ settings = await readClaudeSettings(settingsPath);
2214
+ } catch {
2215
+ return { installed: false, action: "error", error: "Failed to parse settings.json" };
2216
+ }
2217
+ if (hasClaudeHook(settings)) {
2218
+ return { installed: true, action: "already_installed" };
2219
+ }
2220
+ try {
2221
+ addLearningAgentHook(settings);
2222
+ await writeClaudeSettings(settingsPath, settings);
2223
+ return { installed: true, action: "installed" };
2224
+ } catch (err) {
2225
+ return { installed: false, action: "error", error: String(err) };
2226
+ }
2227
+ }
2228
+ async function removeAgentsSection(repoRoot) {
1941
2229
  const agentsPath = join(repoRoot, "AGENTS.md");
1942
- let content = "";
1943
- let existed = false;
1944
- if (existsSync(agentsPath)) {
1945
- content = await readFile(agentsPath, "utf-8");
1946
- existed = true;
1947
- if (hasLearningAgentSection(content)) {
1948
- return false;
2230
+ if (!existsSync(agentsPath)) {
2231
+ return false;
2232
+ }
2233
+ const content = await readFile(agentsPath, "utf-8");
2234
+ const startIdx = content.indexOf(AGENTS_SECTION_START_MARKER);
2235
+ const endIdx = content.indexOf(AGENTS_SECTION_END_MARKER);
2236
+ if (startIdx === -1 || endIdx === -1) {
2237
+ return false;
2238
+ }
2239
+ const before = content.slice(0, startIdx);
2240
+ const after = content.slice(endIdx + AGENTS_SECTION_END_MARKER.length);
2241
+ const newContent = (before.trimEnd() + after).trim();
2242
+ if (newContent.length > 0) {
2243
+ await writeFile(agentsPath, newContent + "\n", "utf-8");
2244
+ } else {
2245
+ await writeFile(agentsPath, "", "utf-8");
2246
+ }
2247
+ return true;
2248
+ }
2249
+ async function removeClaudeMdReference(repoRoot) {
2250
+ const claudeMdPath = join(repoRoot, ".claude", "CLAUDE.md");
2251
+ if (!existsSync(claudeMdPath)) {
2252
+ return false;
2253
+ }
2254
+ const content = await readFile(claudeMdPath, "utf-8");
2255
+ const startIdx = content.indexOf(CLAUDE_REF_START_MARKER);
2256
+ const endIdx = content.indexOf(CLAUDE_REF_END_MARKER);
2257
+ if (startIdx === -1 || endIdx === -1) {
2258
+ return false;
2259
+ }
2260
+ const before = content.slice(0, startIdx);
2261
+ const after = content.slice(endIdx + CLAUDE_REF_END_MARKER.length);
2262
+ const newContent = (before.trimEnd() + after).trim();
2263
+ if (newContent.length > 0) {
2264
+ await writeFile(claudeMdPath, newContent + "\n", "utf-8");
2265
+ } else {
2266
+ await writeFile(claudeMdPath, "", "utf-8");
2267
+ }
2268
+ return true;
2269
+ }
2270
+
2271
+ // src/commands/setup/claude.ts
2272
+ function registerClaudeCommand(program2) {
2273
+ const setupCommand = program2.command("setup").description("Setup integrations");
2274
+ setupCommand.command("claude").description("Install Claude Code SessionStart hooks").option("--global", "Install to global ~/.claude/ instead of project").option("--uninstall", "Remove learning-agent hooks").option("--status", "Check status of Claude Code integration").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
2275
+ const settingsPath = getClaudeSettingsPath(options.global ?? false);
2276
+ const displayPath = options.global ? "~/.claude/settings.json" : ".claude/settings.json";
2277
+ let settings;
2278
+ try {
2279
+ settings = await readClaudeSettings(settingsPath);
2280
+ } catch {
2281
+ if (options.json) {
2282
+ console.log(JSON.stringify({ error: "Failed to parse settings file" }));
2283
+ } else {
2284
+ out.error("Failed to parse settings file. Check if JSON is valid.");
2285
+ }
2286
+ process.exit(1);
2287
+ }
2288
+ const alreadyInstalled = hasClaudeHook(settings);
2289
+ if (options.status) {
2290
+ const repoRoot = getRepoRoot();
2291
+ const learnMdPath = join(repoRoot, ".claude", "commands", "learn.md");
2292
+ const checkPlanMdPath = join(repoRoot, ".claude", "commands", "check-plan.md");
2293
+ const learnExists = existsSync(learnMdPath);
2294
+ const checkPlanExists = existsSync(checkPlanMdPath);
2295
+ let status;
2296
+ if (alreadyInstalled && learnExists && checkPlanExists) {
2297
+ status = "connected";
2298
+ } else if (alreadyInstalled || learnExists || checkPlanExists) {
2299
+ status = "partial";
2300
+ } else {
2301
+ status = "disconnected";
2302
+ }
2303
+ const result = {
2304
+ settingsFile: displayPath,
2305
+ exists: existsSync(settingsPath),
2306
+ validJson: true,
2307
+ // We already parsed it above
2308
+ hookInstalled: alreadyInstalled,
2309
+ slashCommands: {
2310
+ learn: learnExists,
2311
+ checkPlan: checkPlanExists
2312
+ },
2313
+ status
2314
+ };
2315
+ if (options.json) {
2316
+ console.log(JSON.stringify(result, null, 2));
2317
+ } else {
2318
+ console.log("Claude Code Integration Status");
2319
+ console.log("\u2500".repeat(40));
2320
+ console.log("");
2321
+ console.log(`Settings file: ${displayPath}`);
2322
+ console.log(` ${result.exists ? "[ok]" : "[missing]"} File exists`);
2323
+ console.log(` ${result.validJson ? "[ok]" : "[error]"} Valid JSON`);
2324
+ console.log(` ${result.hookInstalled ? "[ok]" : "[warn]"} SessionStart hook installed`);
2325
+ console.log("");
2326
+ console.log("Slash commands:");
2327
+ console.log(` ${learnExists ? "[ok]" : "[warn]"} /learn command`);
2328
+ console.log(` ${checkPlanExists ? "[ok]" : "[warn]"} /check-plan command`);
2329
+ console.log("");
2330
+ if (status === "connected") {
2331
+ out.success("All checks passed. Integration is connected.");
2332
+ } else if (status === "partial") {
2333
+ out.warn("Partial setup detected.");
2334
+ console.log("");
2335
+ console.log("Run 'npx lna init' to complete setup.");
2336
+ } else {
2337
+ out.error("Not connected.");
2338
+ console.log("");
2339
+ console.log("Run 'npx lna init' to set up Learning Agent.");
2340
+ }
2341
+ }
2342
+ return;
2343
+ }
2344
+ if (options.uninstall) {
2345
+ const repoRoot = getRepoRoot();
2346
+ if (options.dryRun) {
2347
+ if (options.json) {
2348
+ console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
2349
+ } else {
2350
+ if (alreadyInstalled) {
2351
+ console.log(`Would remove learning-agent hooks from ${displayPath}`);
2352
+ } else {
2353
+ console.log("No learning-agent hooks to remove");
2354
+ }
2355
+ }
2356
+ return;
2357
+ }
2358
+ const removedHook = removeLearningAgentHook(settings);
2359
+ if (removedHook) {
2360
+ await writeClaudeSettings(settingsPath, settings);
2361
+ }
2362
+ const removedAgents = await removeAgentsSection(repoRoot);
2363
+ const removedClaudeMd = await removeClaudeMdReference(repoRoot);
2364
+ const anyRemoved = removedHook || removedAgents || removedClaudeMd;
2365
+ if (anyRemoved) {
2366
+ if (options.json) {
2367
+ console.log(JSON.stringify({
2368
+ installed: false,
2369
+ location: displayPath,
2370
+ action: "removed",
2371
+ agentsMdRemoved: removedAgents,
2372
+ claudeMdRemoved: removedClaudeMd
2373
+ }));
2374
+ } else {
2375
+ out.success("Learning agent hooks removed");
2376
+ if (removedHook) {
2377
+ console.log(` Settings: ${displayPath}`);
2378
+ }
2379
+ if (removedAgents) {
2380
+ console.log(" AGENTS.md: Learning Agent section removed");
2381
+ }
2382
+ if (removedClaudeMd) {
2383
+ console.log(" CLAUDE.md: Learning Agent reference removed");
2384
+ }
2385
+ }
2386
+ } else {
2387
+ if (options.json) {
2388
+ console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
2389
+ } else {
2390
+ out.info("No learning agent hooks to remove");
2391
+ if (options.global) {
2392
+ console.log(" Hint: Try without --global to check project settings.");
2393
+ } else {
2394
+ console.log(" Hint: Try with --global flag to check global settings.");
2395
+ }
2396
+ }
2397
+ }
2398
+ return;
2399
+ }
2400
+ if (options.dryRun) {
2401
+ if (options.json) {
2402
+ console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
2403
+ } else {
2404
+ if (alreadyInstalled) {
2405
+ console.log("Learning agent hooks already installed");
2406
+ } else {
2407
+ console.log(`Would install learning-agent hooks to ${displayPath}`);
2408
+ }
2409
+ }
2410
+ return;
2411
+ }
2412
+ if (alreadyInstalled) {
2413
+ if (options.json) {
2414
+ console.log(JSON.stringify({
2415
+ installed: true,
2416
+ location: displayPath,
2417
+ hooks: ["SessionStart"],
2418
+ action: "unchanged"
2419
+ }));
2420
+ } else {
2421
+ out.info("Learning agent hooks already installed");
2422
+ console.log(` Location: ${displayPath}`);
2423
+ }
2424
+ return;
2425
+ }
2426
+ const fileExists = existsSync(settingsPath);
2427
+ addLearningAgentHook(settings);
2428
+ await writeClaudeSettings(settingsPath, settings);
2429
+ if (options.json) {
2430
+ console.log(JSON.stringify({
2431
+ installed: true,
2432
+ location: displayPath,
2433
+ hooks: ["SessionStart"],
2434
+ action: fileExists ? "updated" : "created"
2435
+ }));
2436
+ } else {
2437
+ out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
2438
+ console.log(` Location: ${displayPath}`);
2439
+ console.log(" Hook: SessionStart (startup|resume|compact)");
2440
+ console.log("");
2441
+ console.log("Lessons will be loaded automatically at session start.");
2442
+ if (!options.global) {
2443
+ console.log("");
2444
+ console.log("Note: Project hooks override global hooks.");
2445
+ }
2446
+ }
2447
+ });
2448
+ }
2449
+ function registerDownloadModelCommand(program2) {
2450
+ program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
2451
+ const alreadyExisted = isModelAvailable();
2452
+ if (alreadyExisted) {
2453
+ const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
2454
+ const size2 = statSync(modelPath2).size;
2455
+ if (options.json) {
2456
+ console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
2457
+ } else {
2458
+ console.log("Model already exists.");
2459
+ console.log(`Path: ${modelPath2}`);
2460
+ console.log(`Size: ${formatBytes(size2)}`);
2461
+ }
2462
+ return;
2463
+ }
2464
+ if (!options.json) {
2465
+ console.log("Downloading embedding model...");
2466
+ }
2467
+ const modelPath = await resolveModel({ cli: !options.json });
2468
+ const size = statSync(modelPath).size;
2469
+ if (options.json) {
2470
+ console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
2471
+ } else {
2472
+ console.log(`
2473
+ Model downloaded successfully!`);
2474
+ console.log(`Path: ${modelPath}`);
2475
+ console.log(`Size: ${formatBytes(size)}`);
1949
2476
  }
1950
- }
1951
- const newContent = existed ? content.trimEnd() + "\n" + AGENTS_MD_TEMPLATE : AGENTS_MD_TEMPLATE.trim() + "\n";
1952
- await writeFile(agentsPath, newContent, "utf-8");
1953
- return true;
2477
+ });
1954
2478
  }
1955
2479
  var HOOK_FILE_MODE = 493;
1956
2480
  function hasLearningAgentHook(content) {
@@ -1973,10 +2497,6 @@ async function getGitHooksDir(repoRoot) {
1973
2497
  const defaultHooksDir = join(gitDir, "hooks");
1974
2498
  return existsSync(defaultHooksDir) ? defaultHooksDir : null;
1975
2499
  }
1976
- var LEARNING_AGENT_HOOK_BLOCK = `
1977
- # Learning Agent pre-commit hook (appended)
1978
- npx learning-agent hooks run pre-commit
1979
- `;
1980
2500
  function findFirstTopLevelExitLine(lines) {
1981
2501
  let insideFunction = 0;
1982
2502
  let heredocDelimiter = null;
@@ -2037,57 +2557,97 @@ async function installPreCommitHook(repoRoot) {
2037
2557
  chmodSync(hookPath, HOOK_FILE_MODE);
2038
2558
  return true;
2039
2559
  }
2040
- function getClaudeSettingsPath(global) {
2041
- if (global) {
2042
- return join(homedir(), ".claude", "settings.json");
2043
- }
2044
- const repoRoot = getRepoRoot();
2045
- return join(repoRoot, ".claude", "settings.json");
2560
+ function registerHooksCommand(program2) {
2561
+ const hooksCommand = program2.command("hooks").description("Git hooks management");
2562
+ hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
2563
+ if (hook === "pre-commit") {
2564
+ if (options.json) {
2565
+ console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
2566
+ } else {
2567
+ console.log(PRE_COMMIT_MESSAGE);
2568
+ }
2569
+ } else {
2570
+ if (options.json) {
2571
+ console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
2572
+ } else {
2573
+ out.error(`Unknown hook: ${hook}`);
2574
+ }
2575
+ process.exit(1);
2576
+ }
2577
+ });
2046
2578
  }
2047
- async function readClaudeSettings(settingsPath) {
2048
- if (!existsSync(settingsPath)) {
2049
- return {};
2579
+ function hasLearningAgentSection(content) {
2580
+ return content.includes(LEARNING_AGENT_SECTION_HEADER);
2581
+ }
2582
+ function hasClaudeMdReference(content) {
2583
+ return content.includes("Learning Agent") || content.includes(CLAUDE_REF_START_MARKER);
2584
+ }
2585
+ async function ensureClaudeMdReference(repoRoot) {
2586
+ const claudeMdPath = join(repoRoot, ".claude", "CLAUDE.md");
2587
+ await mkdir(join(repoRoot, ".claude"), { recursive: true });
2588
+ if (!existsSync(claudeMdPath)) {
2589
+ const content2 = `# Project Instructions
2590
+ ${CLAUDE_MD_REFERENCE}`;
2591
+ await writeFile(claudeMdPath, content2, "utf-8");
2592
+ return true;
2050
2593
  }
2051
- const content = await readFile(settingsPath, "utf-8");
2052
- return JSON.parse(content);
2594
+ const content = await readFile(claudeMdPath, "utf-8");
2595
+ if (hasClaudeMdReference(content)) {
2596
+ return false;
2597
+ }
2598
+ const newContent = content.trimEnd() + "\n" + CLAUDE_MD_REFERENCE;
2599
+ await writeFile(claudeMdPath, newContent, "utf-8");
2600
+ return true;
2053
2601
  }
2054
- function hasClaudeHook(settings) {
2055
- const hooks = settings.hooks;
2056
- if (!hooks?.SessionStart) return false;
2057
- return hooks.SessionStart.some((entry) => {
2058
- const hookEntry = entry;
2059
- return hookEntry.hooks?.some((h) => h.command?.includes(CLAUDE_HOOK_MARKER));
2060
- });
2602
+ async function createLessonsDirectory(repoRoot) {
2603
+ const lessonsDir = dirname(join(repoRoot, LESSONS_PATH));
2604
+ await mkdir(lessonsDir, { recursive: true });
2061
2605
  }
2062
- function addLearningAgentHook(settings) {
2063
- if (!settings.hooks) {
2064
- settings.hooks = {};
2606
+ async function createIndexFile(repoRoot) {
2607
+ const indexPath = join(repoRoot, LESSONS_PATH);
2608
+ if (!existsSync(indexPath)) {
2609
+ await writeFile(indexPath, "", "utf-8");
2065
2610
  }
2066
- const hooks = settings.hooks;
2067
- if (!hooks.SessionStart) {
2068
- hooks.SessionStart = [];
2611
+ }
2612
+ async function updateAgentsMd(repoRoot) {
2613
+ const agentsPath = join(repoRoot, "AGENTS.md");
2614
+ let content = "";
2615
+ let existed = false;
2616
+ if (existsSync(agentsPath)) {
2617
+ content = await readFile(agentsPath, "utf-8");
2618
+ existed = true;
2619
+ if (hasLearningAgentSection(content)) {
2620
+ return false;
2621
+ }
2069
2622
  }
2070
- hooks.SessionStart.push(CLAUDE_HOOK_CONFIG);
2623
+ const newContent = existed ? content.trimEnd() + "\n" + AGENTS_MD_TEMPLATE : AGENTS_MD_TEMPLATE.trim() + "\n";
2624
+ await writeFile(agentsPath, newContent, "utf-8");
2625
+ return true;
2071
2626
  }
2072
- function removeLearningAgentHook(settings) {
2073
- const hooks = settings.hooks;
2074
- if (!hooks?.SessionStart) return false;
2075
- const originalLength = hooks.SessionStart.length;
2076
- hooks.SessionStart = hooks.SessionStart.filter((entry) => {
2077
- const hookEntry = entry;
2078
- return !hookEntry.hooks?.some((h) => h.command?.includes(CLAUDE_HOOK_MARKER));
2079
- });
2080
- return hooks.SessionStart.length < originalLength;
2627
+ async function createPluginManifest(repoRoot) {
2628
+ const pluginPath = join(repoRoot, ".claude", "plugin.json");
2629
+ await mkdir(join(repoRoot, ".claude"), { recursive: true });
2630
+ if (existsSync(pluginPath)) {
2631
+ return false;
2632
+ }
2633
+ await writeFile(pluginPath, JSON.stringify(PLUGIN_MANIFEST, null, 2) + "\n", "utf-8");
2634
+ return true;
2081
2635
  }
2082
- async function writeClaudeSettings(settingsPath, settings) {
2083
- const dir = dirname(settingsPath);
2084
- await mkdir(dir, { recursive: true });
2085
- const tempPath = settingsPath + ".tmp";
2086
- await writeFile(tempPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2087
- await rename(tempPath, settingsPath);
2636
+ async function createSlashCommands(repoRoot) {
2637
+ const commandsDir = join(repoRoot, ".claude", "commands");
2638
+ await mkdir(commandsDir, { recursive: true });
2639
+ let created = false;
2640
+ for (const [filename, content] of Object.entries(SLASH_COMMANDS)) {
2641
+ const filePath = join(commandsDir, filename);
2642
+ if (!existsSync(filePath)) {
2643
+ await writeFile(filePath, content, "utf-8");
2644
+ created = true;
2645
+ }
2646
+ }
2647
+ return created;
2088
2648
  }
2089
- function registerSetupCommands(program2) {
2090
- program2.command("init").description("Initialize learning-agent in this repository").option("--skip-agents", "Skip AGENTS.md modification").option("--skip-hooks", "Skip git hooks installation").option("--json", "Output result as JSON").action(async function(options) {
2649
+ function registerInitCommand(program2) {
2650
+ program2.command("init").description("Initialize learning-agent in this repository").option("--skip-agents", "Skip AGENTS.md modification").option("--skip-hooks", "Skip git hooks installation").option("--skip-claude", "Skip Claude Code hooks installation").option("--json", "Output result as JSON").action(async function(options) {
2091
2651
  const repoRoot = getRepoRoot();
2092
2652
  const { quiet } = getGlobalOpts(this);
2093
2653
  await createLessonsDirectory(repoRoot);
@@ -2097,16 +2657,33 @@ function registerSetupCommands(program2) {
2097
2657
  if (!options.skipAgents) {
2098
2658
  agentsMdUpdated = await updateAgentsMd(repoRoot);
2099
2659
  }
2660
+ if (!options.skipAgents) {
2661
+ await ensureClaudeMdReference(repoRoot);
2662
+ }
2663
+ let slashCommandsCreated = false;
2664
+ if (!options.skipAgents) {
2665
+ slashCommandsCreated = await createSlashCommands(repoRoot);
2666
+ }
2667
+ if (!options.skipAgents) {
2668
+ await createPluginManifest(repoRoot);
2669
+ }
2100
2670
  let hooksInstalled = false;
2101
2671
  if (!options.skipHooks) {
2102
2672
  hooksInstalled = await installPreCommitHook(repoRoot);
2103
2673
  }
2674
+ let claudeHooksResult = { action: "error", error: "skipped" };
2675
+ if (!options.skipClaude) {
2676
+ claudeHooksResult = await installClaudeHooksForInit(repoRoot);
2677
+ }
2104
2678
  if (options.json) {
2679
+ const claudeHooksInstalled = claudeHooksResult.action === "installed";
2105
2680
  console.log(JSON.stringify({
2106
2681
  initialized: true,
2107
2682
  lessonsDir,
2108
2683
  agentsMd: agentsMdUpdated,
2109
- hooks: hooksInstalled
2684
+ slashCommands: slashCommandsCreated || !options.skipAgents,
2685
+ hooks: hooksInstalled,
2686
+ claudeHooks: claudeHooksInstalled
2110
2687
  }));
2111
2688
  } else if (!quiet) {
2112
2689
  out.success("Learning agent initialized");
@@ -2118,6 +2695,13 @@ function registerSetupCommands(program2) {
2118
2695
  } else {
2119
2696
  console.log(" AGENTS.md: Already has Learning Agent section");
2120
2697
  }
2698
+ if (slashCommandsCreated) {
2699
+ console.log(" Slash commands: Created (/learn, /check-plan, /list, /prime)");
2700
+ } else if (options.skipAgents) {
2701
+ console.log(" Slash commands: Skipped (--skip-agents)");
2702
+ } else {
2703
+ console.log(" Slash commands: Already exist");
2704
+ }
2121
2705
  if (hooksInstalled) {
2122
2706
  console.log(" Git hooks: pre-commit hook installed");
2123
2707
  } else if (options.skipHooks) {
@@ -2125,156 +2709,42 @@ function registerSetupCommands(program2) {
2125
2709
  } else {
2126
2710
  console.log(" Git hooks: Already installed or not a git repo");
2127
2711
  }
2128
- }
2129
- });
2130
- const hooksCommand = program2.command("hooks").description("Git hooks management");
2131
- hooksCommand.command("run <hook>").description("Run a hook script (called by git hooks)").option("--json", "Output as JSON").action((hook, options) => {
2132
- if (hook === "pre-commit") {
2133
- if (options.json) {
2134
- console.log(JSON.stringify({ hook: "pre-commit", message: PRE_COMMIT_MESSAGE }));
2135
- } else {
2136
- console.log(PRE_COMMIT_MESSAGE);
2137
- }
2138
- } else {
2139
- if (options.json) {
2140
- console.log(JSON.stringify({ error: `Unknown hook: ${hook}` }));
2141
- } else {
2142
- out.error(`Unknown hook: ${hook}`);
2143
- }
2144
- process.exit(1);
2145
- }
2146
- });
2147
- const setupCommand = program2.command("setup").description("Setup integrations");
2148
- setupCommand.command("claude").description("Install Claude Code SessionStart hooks").option("--global", "Install to global ~/.claude/ instead of project").option("--uninstall", "Remove learning-agent hooks").option("--dry-run", "Show what would change without writing").option("--json", "Output as JSON").action(async (options) => {
2149
- const settingsPath = getClaudeSettingsPath(options.global ?? false);
2150
- const displayPath = options.global ? "~/.claude/settings.json" : ".claude/settings.json";
2151
- let settings;
2152
- try {
2153
- settings = await readClaudeSettings(settingsPath);
2154
- } catch {
2155
- if (options.json) {
2156
- console.log(JSON.stringify({ error: "Failed to parse settings file" }));
2157
- } else {
2158
- out.error("Failed to parse settings file. Check if JSON is valid.");
2159
- }
2160
- process.exit(1);
2161
- }
2162
- const alreadyInstalled = hasClaudeHook(settings);
2163
- if (options.uninstall) {
2164
- if (options.dryRun) {
2165
- if (options.json) {
2166
- console.log(JSON.stringify({ dryRun: true, wouldRemove: alreadyInstalled, location: displayPath }));
2167
- } else {
2168
- if (alreadyInstalled) {
2169
- console.log(`Would remove learning-agent hooks from ${displayPath}`);
2170
- } else {
2171
- console.log("No learning-agent hooks to remove");
2172
- }
2173
- }
2174
- return;
2175
- }
2176
- const removed = removeLearningAgentHook(settings);
2177
- if (removed) {
2178
- await writeClaudeSettings(settingsPath, settings);
2179
- if (options.json) {
2180
- console.log(JSON.stringify({ installed: false, location: displayPath, action: "removed" }));
2181
- } else {
2182
- out.success("Learning agent hooks removed");
2183
- console.log(` Location: ${displayPath}`);
2184
- }
2185
- } else {
2186
- if (options.json) {
2187
- console.log(JSON.stringify({ installed: false, location: displayPath, action: "unchanged" }));
2188
- } else {
2189
- out.info("No learning agent hooks to remove");
2190
- if (options.global) {
2191
- console.log(" Hint: Try without --global to check project settings.");
2192
- } else {
2193
- console.log(" Hint: Try with --global flag to check global settings.");
2194
- }
2195
- }
2196
- }
2197
- return;
2198
- }
2199
- if (options.dryRun) {
2200
- if (options.json) {
2201
- console.log(JSON.stringify({ dryRun: true, wouldInstall: !alreadyInstalled, location: displayPath }));
2202
- } else {
2203
- if (alreadyInstalled) {
2204
- console.log("Learning agent hooks already installed");
2205
- } else {
2206
- console.log(`Would install learning-agent hooks to ${displayPath}`);
2207
- }
2208
- }
2209
- return;
2210
- }
2211
- if (alreadyInstalled) {
2212
- if (options.json) {
2213
- console.log(JSON.stringify({
2214
- installed: true,
2215
- location: displayPath,
2216
- hooks: ["SessionStart"],
2217
- action: "unchanged"
2218
- }));
2219
- } else {
2220
- out.info("Learning agent hooks already installed");
2221
- console.log(` Location: ${displayPath}`);
2222
- }
2223
- return;
2224
- }
2225
- const fileExists = existsSync(settingsPath);
2226
- addLearningAgentHook(settings);
2227
- await writeClaudeSettings(settingsPath, settings);
2228
- if (options.json) {
2229
- console.log(JSON.stringify({
2230
- installed: true,
2231
- location: displayPath,
2232
- hooks: ["SessionStart"],
2233
- action: fileExists ? "updated" : "created"
2234
- }));
2235
- } else {
2236
- out.success(options.global ? "Claude Code hooks installed (global)" : "Claude Code hooks installed (project-level)");
2237
- console.log(` Location: ${displayPath}`);
2238
- console.log(" Hook: SessionStart (startup|resume|compact)");
2239
- console.log("");
2240
- console.log("Lessons will be loaded automatically at session start.");
2241
- if (!options.global) {
2242
- console.log("");
2243
- console.log("Note: Project hooks override global hooks.");
2244
- }
2245
- }
2246
- });
2247
- program2.command("download-model").description("Download the embedding model for semantic search").option("--json", "Output as JSON").action(async (options) => {
2248
- const alreadyExisted = isModelAvailable();
2249
- if (alreadyExisted) {
2250
- const modelPath2 = join(homedir(), ".node-llama-cpp", "models", MODEL_FILENAME);
2251
- const size2 = statSync(modelPath2).size;
2252
- if (options.json) {
2253
- console.log(JSON.stringify({ success: true, path: modelPath2, size: size2, alreadyExisted: true }));
2254
- } else {
2255
- console.log("Model already exists.");
2256
- console.log(`Path: ${modelPath2}`);
2257
- console.log(`Size: ${formatBytes(size2)}`);
2712
+ if (options.skipClaude) {
2713
+ console.log(" Claude hooks: Skipped (--skip-claude)");
2714
+ } else if (claudeHooksResult.action === "installed") {
2715
+ console.log(" Claude hooks: Installed to .claude/settings.json");
2716
+ } else if (claudeHooksResult.action === "already_installed") {
2717
+ console.log(" Claude hooks: Already installed");
2718
+ } else if (claudeHooksResult.error) {
2719
+ console.log(` Claude hooks: Error - ${claudeHooksResult.error}`);
2258
2720
  }
2259
- return;
2260
- }
2261
- if (!options.json) {
2262
- console.log("Downloading embedding model...");
2263
- }
2264
- const modelPath = await resolveModel({ cli: !options.json });
2265
- const size = statSync(modelPath).size;
2266
- if (options.json) {
2267
- console.log(JSON.stringify({ success: true, path: modelPath, size, alreadyExisted: false }));
2268
- } else {
2269
- console.log(`
2270
- Model downloaded successfully!`);
2271
- console.log(`Path: ${modelPath}`);
2272
- console.log(`Size: ${formatBytes(size)}`);
2273
2721
  }
2274
2722
  });
2275
2723
  }
2276
2724
 
2725
+ // src/commands/setup/index.ts
2726
+ function registerSetupCommands(program2) {
2727
+ registerInitCommand(program2);
2728
+ registerHooksCommand(program2);
2729
+ registerClaudeCommand(program2);
2730
+ registerDownloadModelCommand(program2);
2731
+ }
2732
+
2277
2733
  // src/cli.ts
2734
+ function cleanup() {
2735
+ try {
2736
+ closeDb();
2737
+ } catch {
2738
+ }
2739
+ }
2740
+ process.on("SIGINT", () => {
2741
+ cleanup();
2742
+ process.exit(0);
2743
+ });
2744
+ process.on("SIGTERM", () => {
2745
+ cleanup();
2746
+ process.exit(0);
2747
+ });
2278
2748
  var program = new Command();
2279
2749
  program.option("-v, --verbose", "Show detailed output").option("-q, --quiet", "Suppress non-essential output");
2280
2750
  program.name("learning-agent").description("Repository-scoped learning system for Claude Code").version(VERSION);