musememory 2.3.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.agents/artifacts/2026-10-20-lifeos-memory-distillation-plan.md +865 -0
  2. package/.agents/artifacts/HANDOFF.md +22 -0
  3. package/.agents/artifacts/audit-and-realigned-roadmap-20261015.md +152 -0
  4. package/.agents/artifacts/bench-graph-baseline.json +519 -0
  5. package/.agents/artifacts/bench-graph-baseline.md +139 -0
  6. package/.agents/artifacts/brief-dayos-inspired-site-redo.md +89 -0
  7. package/.agents/artifacts/handoff-20261015-143202.md +109 -0
  8. package/.agents/artifacts/plan-competitor-adaptation-dashboard-graph.md +99 -0
  9. package/.agents/artifacts/session-report-2026-10-01-memory-gaps-and-website.md +94 -0
  10. package/.agents/artifacts/wave-3-sdd-ledger.md +18 -0
  11. package/.agents/artifacts/wave-4-sdd-ledger.md +18 -0
  12. package/.agents/context/architecture.md +1 -0
  13. package/.agents/context/current.md +222 -3
  14. package/.agents/context/decisions.md +4 -0
  15. package/.agents/context/deep-research-agentic-memory-evolution.md +261 -0
  16. package/.agents/context/deep-research-memory-adaptation-2026-10.md +107 -0
  17. package/.agents/context/deep-research-memory-improvement-2026-10.md +420 -0
  18. package/.agents/context/deep-research-memory-mechanisms-2026-09.md +334 -0
  19. package/.agents/context/index.md +47 -2
  20. package/.agents/context/product.md +139 -73
  21. package/.agents/context/roadmap.md +369 -7
  22. package/.agents/context/ticket-plan.md +553 -0
  23. package/.agents/plans/2026-10-20-wave-22-failure-attribution-negative-mining-design.md +198 -0
  24. package/.agents/plans/2026-10-20-wave-22-failure-attribution-negative-mining-plan.md +274 -0
  25. package/.agents/plans/EVOLUTION_PLAN.md +351 -0
  26. package/.agents/plans/local-embeddings-llama-cpp.md +37 -0
  27. package/.agents/plans/recall-eval-harness.md +31 -0
  28. package/.agents/research/ecosystem-teardown-2026-10.md +289 -0
  29. package/.agents/research/research-brief-agent-memory-migrator.md +1 -1
  30. package/.agents/research/research-brief-walrus-memory-architecture.md +189 -0
  31. package/.agents/research/retirement-audit-2026-10.md +45 -0
  32. package/.agents/standards/README.md +42 -0
  33. package/.agents/standards/frontend-astro.md +111 -29
  34. package/.agents/standards/git-workflow.md +5 -0
  35. package/.agents/standards/workflows.md +13 -0
  36. package/README.md +300 -253
  37. package/claims.json +15 -0
  38. package/dist/index.js +20784 -9111
  39. package/docs/agent-reference.md +199 -0
  40. package/docs/cli-reference.md +263 -0
  41. package/docs/mcp-tool-matrix.json +121 -0
  42. package/package.json +30 -10
  43. package/schema.json +40 -2
@@ -0,0 +1,865 @@
1
+ # LifeOS Memory Distillation & Engine Hardening Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Distill LifeOS Cortex's defensive in-lock mutation guards (`ESUSPECT_SHRINK`, `ESUSPECT_EROSION`), pre-mutation snapshot ring buffer, and multi-channel knowledge distillation router into `musememory` without flat-file regressions.
6
+
7
+ **Architecture:** Implement in-lock mutation safety validation in `src/quality/guards.ts` and integrate it with a diff-preserving `CURRENT.md` parser in `src/compaction/engine.ts` to prevent constraint destruction. Add a 30-snapshot rolling ring buffer in `src/snapshot.ts` before mutations, and implement a 3-lane (`system`, `content`, `health`) stateful knowledge distillation router in `src/cognition/distill_router.ts` with CLI and MCP surfaces.
8
+
9
+ **Tech Stack:** TypeScript, Bun (`bun test`, `bun run build`, `bunx tsc --noEmit`), SQLite 3 (in-memory/file).
10
+
11
+ ## Global Constraints
12
+ - Target tickets: `T-324` (CURRENT.md merge semantics), `T-050` (Loss guard), `T-177` (Pre-governance snapshots).
13
+ - Zero external daemons required (strictly offline-capable and in-process per ADR-001 / `T-367`).
14
+ - Zero secret leaks (Vibeguard protocol on all routed outputs).
15
+ - Full gate validation must pass on every commit: `bunx tsc --noEmit`, `bun test`, `bun run build`.
16
+
17
+ ---
18
+
19
+ ### Task 1: In-Lock Defensive Mutation Guards (`src/quality/guards.ts`)
20
+
21
+ **Files:**
22
+ - Create: `src/quality/guards.ts`
23
+ - Modify: `src/quality/index.ts`
24
+ - Test: `test/mutation_guards.test.ts`
25
+
26
+ **Interfaces:**
27
+ - Produces: `verifyMutationSafety(priorCount: number, newCount: number, additionsCount: number, options?: MutationSafetyOptions): void`
28
+ - Produces: `MutationSafetyError` class with code `"ESUSPECT_SHRINK" | "ESUSPECT_EROSION"`.
29
+
30
+ - [ ] **Step 1: Write the failing unit tests for mutation guards**
31
+
32
+ Create `test/mutation_guards.test.ts`:
33
+ ```typescript
34
+ import { describe, expect, it } from "bun:test";
35
+ import { verifyMutationSafety, MutationSafetyError } from "../src/quality/guards.ts";
36
+
37
+ describe("verifyMutationSafety", () => {
38
+ it("allows safe modifications and additions", () => {
39
+ expect(() => verifyMutationSafety(10, 12, 3)).not.toThrow();
40
+ expect(() => verifyMutationSafety(10, 10, 1)).not.toThrow();
41
+ expect(() => verifyMutationSafety(10, 9, 1)).not.toThrow();
42
+ });
43
+
44
+ it("throws ESUSPECT_SHRINK when dropping below floor without bypass", () => {
45
+ expect(() => verifyMutationSafety(5, 2, 0)).toThrow(MutationSafetyError);
46
+ try {
47
+ verifyMutationSafety(5, 2, 0);
48
+ } catch (err) {
49
+ expect(err).toBeInstanceOf(MutationSafetyError);
50
+ expect((err as MutationSafetyError).code).toBe("ESUSPECT_SHRINK");
51
+ }
52
+ });
53
+
54
+ it("throws ESUSPECT_SHRINK when dropping >50% entries with zero additions", () => {
55
+ expect(() => verifyMutationSafety(20, 9, 0)).toThrow(MutationSafetyError);
56
+ });
57
+
58
+ it("throws ESUSPECT_EROSION when net-dropping 2 or more entries with zero additions", () => {
59
+ expect(() => verifyMutationSafety(10, 8, 0)).toThrow(MutationSafetyError);
60
+ try {
61
+ verifyMutationSafety(10, 8, 0);
62
+ } catch (err) {
63
+ expect(err).toBeInstanceOf(MutationSafetyError);
64
+ expect((err as MutationSafetyError).code).toBe("ESUSPECT_EROSION");
65
+ }
66
+ });
67
+
68
+ it("permits 1-entry drop with zero additions without error", () => {
69
+ expect(() => verifyMutationSafety(10, 9, 0)).not.toThrow();
70
+ });
71
+
72
+ it("bypasses all checks when allowDrastic is true", () => {
73
+ expect(() => verifyMutationSafety(20, 1, 0, { allowDrastic: true })).not.toThrow();
74
+ });
75
+ });
76
+ ```
77
+
78
+ - [ ] **Step 2: Run test to verify it fails**
79
+
80
+ Run: `bun test test/mutation_guards.test.ts`
81
+ Expected: FAIL with module `../src/quality/guards.ts` not found.
82
+
83
+ - [ ] **Step 3: Implement `src/quality/guards.ts`**
84
+
85
+ Write `src/quality/guards.ts`:
86
+ ```typescript
87
+ export interface MutationSafetyOptions {
88
+ allowDrastic?: boolean;
89
+ minSafeFloor?: number;
90
+ }
91
+
92
+ export class MutationSafetyError extends Error {
93
+ constructor(
94
+ public readonly code: "ESUSPECT_SHRINK" | "ESUSPECT_EROSION",
95
+ public readonly priorCount: number,
96
+ public readonly newCount: number,
97
+ public readonly additionsCount: number,
98
+ message: string,
99
+ ) {
100
+ super(message);
101
+ this.name = "MutationSafetyError";
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Validates mutation safety before writing modified sets.
107
+ * Prevents catastrophic shrinkage and slow erosion.
108
+ */
109
+ export function verifyMutationSafety(
110
+ priorCount: number,
111
+ newCount: number,
112
+ additionsCount: number,
113
+ options?: MutationSafetyOptions,
114
+ ): void {
115
+ if (options?.allowDrastic) {
116
+ return;
117
+ }
118
+
119
+ const floor = options?.minSafeFloor ?? 3;
120
+
121
+ // Catastrophic shrink: dropping below minimum floor
122
+ if (priorCount >= floor && newCount < floor) {
123
+ throw new MutationSafetyError(
124
+ "ESUSPECT_SHRINK",
125
+ priorCount,
126
+ newCount,
127
+ additionsCount,
128
+ `Mutation rejected: entry count reduced below floor (${newCount} < ${floor}, prior: ${priorCount})`,
129
+ );
130
+ }
131
+
132
+ // Mass deletion check: dropping >50% of entries with 0 additions
133
+ if (additionsCount === 0 && priorCount > 0) {
134
+ const dropped = priorCount - newCount;
135
+ if (dropped / priorCount > 0.5) {
136
+ throw new MutationSafetyError(
137
+ "ESUSPECT_SHRINK",
138
+ priorCount,
139
+ newCount,
140
+ additionsCount,
141
+ `Mutation rejected: mass deletion detected (${dropped}/${priorCount} dropped with 0 additions)`,
142
+ );
143
+ }
144
+ }
145
+
146
+ // Slow erosion check: dropping 2+ entries with 0 additions
147
+ if (additionsCount === 0 && priorCount - newCount >= 2) {
148
+ throw new MutationSafetyError(
149
+ "ESUSPECT_EROSION",
150
+ priorCount,
151
+ newCount,
152
+ additionsCount,
153
+ `Mutation rejected: slow erosion detected (net drop of ${priorCount - newCount} entries with 0 additions)`,
154
+ );
155
+ }
156
+ }
157
+ ```
158
+
159
+ Re-export in `src/quality/index.ts`.
160
+
161
+ - [ ] **Step 4: Run test to verify it passes**
162
+
163
+ Run: `bun test test/mutation_guards.test.ts`
164
+ Expected: PASS with 6/6 tests passing.
165
+
166
+ - [ ] **Step 5: Commit**
167
+
168
+ ```bash
169
+ git add src/quality/guards.ts src/quality/index.ts test/mutation_guards.test.ts
170
+ git commit -m "feat(quality): add in-lock mutation safety guards (closes T-050)"
171
+ ```
172
+
173
+ ---
174
+
175
+ ### Task 2: Diff-Preserving `CURRENT.md` Compaction Merge (`src/compaction/engine.ts`)
176
+
177
+ **Files:**
178
+ - Modify: `src/compaction/engine.ts`
179
+ - Test: `test/current_compaction_guard.test.ts`
180
+
181
+ **Interfaces:**
182
+ - Consumes: `verifyMutationSafety` from `src/quality/guards.ts`
183
+ - Produces: `parseCurrentConstraints(content: string): string[]`
184
+ - Produces: `mergeCurrentConstraints(existingText: string, proposed: string[], options?: { allowDrastic?: boolean }): string[]`
185
+
186
+ - [ ] **Step 1: Write the failing tests for diff-preserving `CURRENT.md` merge**
187
+
188
+ Create `test/current_compaction_guard.test.ts`:
189
+ ```typescript
190
+ import { describe, expect, it, beforeEach, afterEach } from "bun:test";
191
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
192
+ import { join } from "node:path";
193
+ import { generateSessionHandoff, parseCurrentConstraints, mergeCurrentConstraints } from "../src/compaction/engine.ts";
194
+
195
+ const TEST_DIR = join(import.meta.dir, ".tmp_compaction_test");
196
+
197
+ describe("CURRENT.md Diff-Preserving Merge (T-324)", () => {
198
+ beforeEach(() => {
199
+ mkdirSync(TEST_DIR, { recursive: true });
200
+ });
201
+
202
+ afterEach(() => {
203
+ rmSync(TEST_DIR, { recursive: true, force: true });
204
+ });
205
+
206
+ it("parses active constraints from existing CURRENT.md", () => {
207
+ const raw = `# CURRENT.md
208
+ ## šŸ”’ 1. Active Working Constraints & Invariants
209
+ - Zero unmasked credentials committed (Vibeguard Protocol).
210
+ - Fail fast with explicit errors rather than swallowing failures.
211
+ - Always use cursor pagination.
212
+
213
+ ## šŸŽÆ 2. High-Level Goal
214
+ Build feature.`;
215
+
216
+ const constraints = parseCurrentConstraints(raw);
217
+ expect(constraints).toHaveLength(3);
218
+ expect(constraints).toContain("Always use cursor pagination.");
219
+ });
220
+
221
+ it("unions existing hand-added constraints with new handoff constraints", () => {
222
+ const existing = `# CURRENT.md
223
+ ## šŸ”’ 1. Active Working Constraints & Invariants
224
+ - Existing custom rule 1
225
+ - Existing custom rule 2`;
226
+
227
+ const merged = mergeCurrentConstraints(existing, ["New proposed rule"]);
228
+ expect(merged).toHaveLength(3);
229
+ expect(merged).toContain("Existing custom rule 1");
230
+ expect(merged).toContain("Existing custom rule 2");
231
+ expect(merged).toContain("New proposed rule");
232
+ });
233
+
234
+ it("generateSessionHandoff preserves existing hand-added constraints on disk", () => {
235
+ const currentMdPath = join(TEST_DIR, "CURRENT.md");
236
+ writeFileSync(currentMdPath, `# CURRENT.md
237
+ ## šŸ”’ 1. Active Working Constraints & Invariants
238
+ - Persistent invariant Alpha
239
+ - Persistent invariant Beta
240
+ `, "utf8");
241
+
242
+ generateSessionHandoff(TEST_DIR, {
243
+ highLevelGoal: "Next milestone",
244
+ currentArchitecture: "Clean arch",
245
+ completedTasks: ["Task 1 done"],
246
+ openTasks: ["Task 2 pending"],
247
+ nextConcreteTask: "Execute Task 2",
248
+ activeConstraints: ["Fresh constraint Gamma"],
249
+ });
250
+
251
+ const updated = readFileSync(currentMdPath, "utf8");
252
+ expect(updated).toContain("Persistent invariant Alpha");
253
+ expect(updated).toContain("Persistent invariant Beta");
254
+ expect(updated).toContain("Fresh constraint Gamma");
255
+ });
256
+ });
257
+ ```
258
+
259
+ - [ ] **Step 2: Run test to verify it fails**
260
+
261
+ Run: `bun test test/current_compaction_guard.test.ts`
262
+ Expected: FAIL with `parseCurrentConstraints is not exported`.
263
+
264
+ - [ ] **Step 3: Implement constraint parser & merge in `src/compaction/engine.ts`**
265
+
266
+ In `src/compaction/engine.ts`:
267
+ 1. Import `verifyMutationSafety` from `../quality/guards.ts`.
268
+ 2. Implement `parseCurrentConstraints(content: string): string[]`:
269
+ - Match content under `## šŸ”’ 1. Active Working Constraints & Invariants` (or `## Active Constraints`).
270
+ - Extract bulleted lines `- ...` or `* ...`, stripping prefixes.
271
+ 3. Implement `mergeCurrentConstraints(existingText: string, proposed: string[], options?: { allowDrastic?: boolean }): string[]`:
272
+ - Parse existing constraints.
273
+ - Dedup and union proposed with existing (matching normalized lowercase trimmed strings).
274
+ - Compute additions count = proposed items not previously present.
275
+ - Call `verifyMutationSafety(existing.length, union.length, additionsCount, options)`.
276
+ - Return merged array.
277
+ 4. In `commitHandoff()`:
278
+ - Check if `CURRENT.md` already exists on disk.
279
+ - If it exists, read it and pass to `mergeCurrentConstraints(existingText, invariants.activeConstraints ?? [])`.
280
+ - Update `invariants.activeConstraints` with the merged list before rendering.
281
+
282
+ - [ ] **Step 4: Run test to verify it passes**
283
+
284
+ Run: `bun test test/current_compaction_guard.test.ts`
285
+ Expected: PASS with 3/3 tests passing.
286
+
287
+ - [ ] **Step 5: Commit**
288
+
289
+ ```bash
290
+ git add src/compaction/engine.ts test/current_compaction_guard.test.ts
291
+ git commit -m "feat(compaction): add diff-preserving CURRENT.md merge semantics (closes T-324)"
292
+ ```
293
+
294
+ ---
295
+
296
+ ### Task 3: Pre-Mutation Snapshot Ring Buffer (`src/snapshot.ts`)
297
+
298
+ **Files:**
299
+ - Modify: `src/snapshot.ts`
300
+ - Test: `test/snapshot_ring.test.ts`
301
+
302
+ **Interfaces:**
303
+ - Produces: `captureSnapshotRing(target: "current" | "state", memoryDir: string, maxSnapshots?: number): SnapshotRingResult`
304
+ - Produces: `listSnapshotRing(target: "current" | "state", memoryDir: string): string[]`
305
+ - Produces: `restoreSnapshotRing(snapshotName: string, memoryDir: string): void`
306
+
307
+ - [ ] **Step 1: Write the failing tests for snapshot ring buffer**
308
+
309
+ Create `test/snapshot_ring.test.ts`:
310
+ ```typescript
311
+ import { describe, expect, it, beforeEach, afterEach } from "bun:test";
312
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
313
+ import { join } from "node:path";
314
+ import { captureSnapshotRing, listSnapshotRing, restoreSnapshotRing } from "../src/snapshot.ts";
315
+
316
+ const TEST_DIR = join(import.meta.dir, ".tmp_snapshot_test");
317
+
318
+ describe("Pre-Mutation Snapshot Ring Buffer (T-177)", () => {
319
+ beforeEach(() => {
320
+ mkdirSync(TEST_DIR, { recursive: true });
321
+ writeFileSync(join(TEST_DIR, "CURRENT.md"), "Initial Content v1\n", "utf8");
322
+ });
323
+
324
+ afterEach(() => {
325
+ rmSync(TEST_DIR, { recursive: true, force: true });
326
+ });
327
+
328
+ it("captures snapshot to .memory/snapshots/ with timestamp", () => {
329
+ const res = captureSnapshotRing("current", TEST_DIR);
330
+ expect(existsSync(res.snapshotPath)).toBe(true);
331
+ expect(res.target).toBe("current");
332
+ });
333
+
334
+ it("rotates ring buffer to max 30 snapshots", () => {
335
+ for (let i = 0; i < 35; i++) {
336
+ writeFileSync(join(TEST_DIR, "CURRENT.md"), `Content iteration ${i}\n`, "utf8");
337
+ captureSnapshotRing("current", TEST_DIR, 30);
338
+ }
339
+
340
+ const snapshots = listSnapshotRing("current", TEST_DIR);
341
+ expect(snapshots.length).toBe(30);
342
+ });
343
+
344
+ it("restores snapshot accurately", () => {
345
+ const firstCapture = captureSnapshotRing("current", TEST_DIR);
346
+ const snapName = firstCapture.snapshotName;
347
+
348
+ writeFileSync(join(TEST_DIR, "CURRENT.md"), "Corrupted Content v2\n", "utf8");
349
+ expect(readFileSync(join(TEST_DIR, "CURRENT.md"), "utf8")).toBe("Corrupted Content v2\n");
350
+
351
+ restoreSnapshotRing(snapName, TEST_DIR);
352
+ expect(readFileSync(join(TEST_DIR, "CURRENT.md"), "utf8")).toBe("Initial Content v1\n");
353
+ });
354
+ });
355
+ ```
356
+
357
+ - [ ] **Step 2: Run test to verify it fails**
358
+
359
+ Run: `bun test test/snapshot_ring.test.ts`
360
+ Expected: FAIL with `captureSnapshotRing is not exported`.
361
+
362
+ - [ ] **Step 3: Implement snapshot ring buffer in `src/snapshot.ts`**
363
+
364
+ Add to `src/snapshot.ts`:
365
+ ```typescript
366
+ export interface SnapshotRingResult {
367
+ snapshotPath: string;
368
+ snapshotName: string;
369
+ target: "current" | "state";
370
+ prunedCount: number;
371
+ }
372
+
373
+ export function captureSnapshotRing(
374
+ target: "current" | "state",
375
+ memoryDir: string,
376
+ maxSnapshots: number = 30,
377
+ ): SnapshotRingResult {
378
+ const snapshotsDir = join(memoryDir, "snapshots");
379
+ mkdirSync(snapshotsDir, { recursive: true });
380
+
381
+ const sourceFile = target === "current" ? "CURRENT.md" : "state.db";
382
+ const sourcePath = join(memoryDir, sourceFile);
383
+
384
+ if (!existsSync(sourcePath)) {
385
+ throw new Error(`Cannot capture snapshot: ${sourcePath} does not exist`);
386
+ }
387
+
388
+ const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
389
+ const ext = target === "current" ? "md" : "db";
390
+ const snapshotName = `${target}__${isoTimestamp}.${ext}`;
391
+ const snapshotPath = join(snapshotsDir, snapshotName);
392
+
393
+ const content = readFileSync(sourcePath);
394
+ writeFileSync(snapshotPath, content);
395
+
396
+ // Rotate ring buffer
397
+ const prefix = `${target}__`;
398
+ const existing = readdirSync(snapshotsDir)
399
+ .filter((f) => f.startsWith(prefix) && f.endsWith(`.${ext}`))
400
+ .sort(); // Lexicographical sort by timestamp
401
+
402
+ let prunedCount = 0;
403
+ if (existing.length > maxSnapshots) {
404
+ const toDelete = existing.slice(0, existing.length - maxSnapshots);
405
+ for (const f of toDelete) {
406
+ try {
407
+ unlinkSync(join(snapshotsDir, f));
408
+ prunedCount++;
409
+ } catch {
410
+ // ignore deletion failures
411
+ }
412
+ }
413
+ }
414
+
415
+ return { snapshotPath, snapshotName, target, prunedCount };
416
+ }
417
+
418
+ export function listSnapshotRing(target: "current" | "state", memoryDir: string): string[] {
419
+ const snapshotsDir = join(memoryDir, "snapshots");
420
+ if (!existsSync(snapshotsDir)) return [];
421
+ const ext = target === "current" ? "md" : "db";
422
+ return readdirSync(snapshotsDir)
423
+ .filter((f) => f.startsWith(`${target}__`) && f.endsWith(`.${ext}`))
424
+ .sort()
425
+ .reverse();
426
+ }
427
+
428
+ export function restoreSnapshotRing(snapshotName: string, memoryDir: string): void {
429
+ const snapshotsDir = join(memoryDir, "snapshots");
430
+ const snapPath = join(snapshotsDir, snapshotName);
431
+ if (!existsSync(snapPath)) {
432
+ throw new Error(`Snapshot not found: ${snapPath}`);
433
+ }
434
+
435
+ const targetFile = snapshotName.startsWith("current__") ? "CURRENT.md" : "state.db";
436
+ const targetPath = join(memoryDir, targetFile);
437
+
438
+ const content = readFileSync(snapPath);
439
+ writeFileSync(targetPath, content);
440
+ }
441
+ ```
442
+
443
+ - [ ] **Step 4: Wire snapshot capture into `commitHandoff` in `src/compaction/engine.ts`**
444
+
445
+ Before `writeFileSync(currentMdPath, markdownContent)`:
446
+ ```typescript
447
+ if (existsSync(currentMdPath)) {
448
+ captureSnapshotRing("current", memoryDir, 30);
449
+ }
450
+ ```
451
+
452
+ - [ ] **Step 5: Run test to verify it passes**
453
+
454
+ Run: `bun test test/snapshot_ring.test.ts`
455
+ Expected: PASS with 3/3 tests passing.
456
+
457
+ - [ ] **Step 6: Commit**
458
+
459
+ ```bash
460
+ git add src/snapshot.ts src/compaction/engine.ts test/snapshot_ring.test.ts
461
+ git commit -m "feat(snapshot): add pre-mutation 30-snapshot ring buffer (closes T-177)"
462
+ ```
463
+
464
+ ---
465
+
466
+ ### Task 4: Multi-Channel Distillation Router (`src/cognition/distill_router.ts`)
467
+
468
+ **Files:**
469
+ - Create: `src/cognition/distill_router.ts`
470
+ - Modify: `src/cognition/index.ts`
471
+ - Test: `test/distill_router.test.ts`
472
+
473
+ **Interfaces:**
474
+ - Produces: `runDistillRouter(store: Store, options?: DistillRouterOptions): DistillRouterResult`
475
+
476
+ - [ ] **Step 1: Write the failing tests for distill router**
477
+
478
+ Create `test/distill_router.test.ts`:
479
+ ```typescript
480
+ import { describe, expect, it, beforeEach, afterEach } from "bun:test";
481
+ import { mkdirSync, rmSync, existsSync, readFileSync } from "node:fs";
482
+ import { join } from "node:path";
483
+ import { openStore, save, propose } from "../src/store.ts";
484
+ import { runDistillRouter } from "../src/cognition/distill_router.ts";
485
+
486
+ const TEST_DIR = join(import.meta.dir, ".tmp_distill_router_test");
487
+
488
+ describe("Multi-Channel Distillation Router", () => {
489
+ beforeEach(() => {
490
+ mkdirSync(TEST_DIR, { recursive: true });
491
+ });
492
+
493
+ afterEach(() => {
494
+ rmSync(TEST_DIR, { recursive: true, force: true });
495
+ });
496
+
497
+ it("routes fix/rule to system, architecture to content, and conflict to health", () => {
498
+ const store = openStore({ memoryDir: TEST_DIR });
499
+
500
+ save(store, {
501
+ id: "mem_fix_1",
502
+ title: "Fix for DB lock timeout",
503
+ content: "Increased WAL busy timeout to 5000ms.",
504
+ type: "fix",
505
+ status: "confirmed",
506
+ tags: ["sqlite", "lock"],
507
+ project: "core",
508
+ });
509
+
510
+ save(store, {
511
+ id: "mem_arch_1",
512
+ title: "Decoupled Event Outbox Pattern",
513
+ content: "Adopted transactional outbox pattern.",
514
+ type: "architecture",
515
+ status: "confirmed",
516
+ tags: ["architecture", "outbox"],
517
+ project: "core",
518
+ });
519
+
520
+ save(store, {
521
+ id: "mem_conf_1",
522
+ title: "Contradictory Port Numbers",
523
+ content: "Port is 3000 vs 8080.",
524
+ type: "fact",
525
+ status: "conflicted",
526
+ tags: ["networking"],
527
+ project: "core",
528
+ });
529
+
530
+ const result = runDistillRouter(store, { workspaceRoot: TEST_DIR });
531
+ expect(result.routed.system.length).toBe(1);
532
+ expect(result.routed.system[0].id).toBe("mem_fix_1");
533
+ expect(result.routed.content.length).toBe(1);
534
+ expect(result.routed.content[0].id).toBe("mem_arch_1");
535
+ expect(result.routed.health.length).toBe(1);
536
+ expect(result.routed.health[0].id).toBe("mem_conf_1");
537
+ });
538
+
539
+ it("deduplicates already routed items via distill_state.json", () => {
540
+ const store = openStore({ memoryDir: TEST_DIR });
541
+
542
+ save(store, {
543
+ id: "mem_fix_2",
544
+ title: "Fix for memory leak",
545
+ content: "Closed stream handles.",
546
+ type: "fix",
547
+ status: "confirmed",
548
+ project: "core",
549
+ });
550
+
551
+ const firstRun = runDistillRouter(store, { workspaceRoot: TEST_DIR });
552
+ expect(firstRun.total_routed).toBe(1);
553
+
554
+ const secondRun = runDistillRouter(store, { workspaceRoot: TEST_DIR });
555
+ expect(secondRun.total_routed).toBe(0);
556
+ expect(secondRun.skipped_surfaced).toBeGreaterThanOrEqual(1);
557
+ });
558
+ });
559
+ ```
560
+
561
+ - [ ] **Step 2: Run test to verify it fails**
562
+
563
+ Run: `bun test test/distill_router.test.ts`
564
+ Expected: FAIL with module not found.
565
+
566
+ - [ ] **Step 3: Implement `src/cognition/distill_router.ts`**
567
+
568
+ Write `src/cognition/distill_router.ts`:
569
+ ```typescript
570
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
571
+ import { join } from "node:path";
572
+ import { createHash } from "node:crypto";
573
+ import type { Store } from "../store.ts";
574
+ import { list } from "../store.ts";
575
+ import type { MemoryEntry } from "../types.ts";
576
+ import { scanSecrets } from "../secrets.ts";
577
+
578
+ export interface DistillRouterOptions {
579
+ workspaceRoot?: string;
580
+ force?: boolean;
581
+ dryRun?: boolean;
582
+ }
583
+
584
+ export interface RoutedItem {
585
+ id: string;
586
+ title: string;
587
+ lane: "system" | "content" | "health";
588
+ summary: string;
589
+ path?: string;
590
+ }
591
+
592
+ export interface DistillRouterResult {
593
+ ok: boolean;
594
+ routed: {
595
+ system: RoutedItem[];
596
+ content: RoutedItem[];
597
+ health: RoutedItem[];
598
+ };
599
+ total_routed: number;
600
+ skipped_surfaced: number;
601
+ }
602
+
603
+ interface DistillState {
604
+ schema_version: number;
605
+ surfaced_ids: Record<string, string>;
606
+ item_hashes: Record<string, string>;
607
+ last_run: string | null;
608
+ }
609
+
610
+ function loadState(statePath: string): DistillState {
611
+ if (existsSync(statePath)) {
612
+ try {
613
+ return JSON.parse(readFileSync(statePath, "utf8"));
614
+ } catch {
615
+ // fallback
616
+ }
617
+ }
618
+ return {
619
+ schema_version: 1,
620
+ surfaced_ids: {},
621
+ item_hashes: {},
622
+ last_run: null,
623
+ };
624
+ }
625
+
626
+ export function runDistillRouter(store: Store, options?: DistillRouterOptions): DistillRouterResult {
627
+ const root = options?.workspaceRoot ?? store.memoryDir ?? process.cwd();
628
+ const statePath = join(store.memoryDir, "distill_state.json");
629
+ const state = loadState(statePath);
630
+
631
+ const entries = list(store);
632
+ const result: DistillRouterResult = {
633
+ ok: true,
634
+ routed: { system: [], content: [], health: [] },
635
+ total_routed: 0,
636
+ skipped_surfaced: 0,
637
+ };
638
+
639
+ const todayIso = new Date().toISOString();
640
+
641
+ for (const e of entries) {
642
+ const hash = createHash("sha256").update(e.title + e.content).digest("hex").slice(0, 16);
643
+
644
+ if (!options?.force && (state.surfaced_ids[e.id] || state.item_hashes[hash])) {
645
+ result.skipped_surfaced++;
646
+ continue;
647
+ }
648
+
649
+ // Health Lane: Conflicted entries or active contradictions
650
+ if (e.status === "conflicted") {
651
+ result.routed.health.push({
652
+ id: e.id,
653
+ title: e.title,
654
+ lane: "health",
655
+ summary: `Contradiction detected: ${e.title}`,
656
+ });
657
+ state.surfaced_ids[e.id] = todayIso;
658
+ state.item_hashes[hash] = todayIso;
659
+ continue;
660
+ }
661
+
662
+ // System Lane: Bug fixes, operational rules
663
+ if (e.type === "fix" || e.type === "rule" || (e.tags && e.tags.includes("distilled-fix"))) {
664
+ result.routed.system.push({
665
+ id: e.id,
666
+ title: e.title,
667
+ lane: "system",
668
+ summary: e.content.slice(0, 150),
669
+ });
670
+ state.surfaced_ids[e.id] = todayIso;
671
+ state.item_hashes[hash] = todayIso;
672
+ continue;
673
+ }
674
+
675
+ // Content Lane: Architecture decisions and concepts
676
+ if (e.type === "architecture" || e.type === "concept" || (e.salience && e.salience >= 0.8)) {
677
+ result.routed.content.push({
678
+ id: e.id,
679
+ title: e.title,
680
+ lane: "content",
681
+ summary: e.content.slice(0, 150),
682
+ });
683
+ state.surfaced_ids[e.id] = todayIso;
684
+ state.item_hashes[hash] = todayIso;
685
+ continue;
686
+ }
687
+ }
688
+
689
+ result.total_routed =
690
+ result.routed.system.length + result.routed.content.length + result.routed.health.length;
691
+
692
+ if (!options?.dryRun && result.total_routed > 0) {
693
+ state.last_run = todayIso;
694
+ writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
695
+
696
+ // Stage draft files for content lane if .agents/drafts exists or can be created
697
+ if (result.routed.content.length > 0) {
698
+ const draftsDir = join(root, ".agents", "drafts");
699
+ try {
700
+ mkdirSync(draftsDir, { recursive: true });
701
+ for (const item of result.routed.content) {
702
+ const slug = item.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40);
703
+ const draftPath = join(draftsDir, `draft-${slug}.md`);
704
+ const body = `# ${item.title}\n\n> Sourced from Memory ID: \`${item.id}\`\n\n${item.summary}\n`;
705
+ if (scanSecrets(body).length === 0) {
706
+ writeFileSync(draftPath, body, "utf8");
707
+ item.path = draftPath;
708
+ }
709
+ }
710
+ } catch {
711
+ // ignore draft writing errors in hermetic test envs
712
+ }
713
+ }
714
+ }
715
+
716
+ return result;
717
+ }
718
+ ```
719
+
720
+ Re-export in `src/cognition/index.ts`.
721
+
722
+ - [ ] **Step 4: Run test to verify it passes**
723
+
724
+ Run: `bun test test/distill_router.test.ts`
725
+ Expected: PASS with 2/2 tests passing.
726
+
727
+ - [ ] **Step 5: Commit**
728
+
729
+ ```bash
730
+ git add src/cognition/distill_router.ts src/cognition/index.ts test/distill_router.test.ts
731
+ git commit -m "feat(cognition): add multi-channel distillation router with stateful dedupe"
732
+ ```
733
+
734
+ ---
735
+
736
+ ### Task 5: Wire CLI & MCP Surfaces
737
+
738
+ **Files:**
739
+ - Modify: `src/cli/lifecycle.ts`
740
+ - Modify: `src/cli.ts`
741
+ - Modify: `src/mcp.ts`
742
+ - Test: `test/mcp_distill_route.test.ts`
743
+
744
+ - [ ] **Step 1: Write the failing tests for MCP `memory_distill_route`**
745
+
746
+ Create `test/mcp_distill_route.test.ts`:
747
+ ```typescript
748
+ import { describe, expect, it, beforeEach, afterEach } from "bun:test";
749
+ import { mkdirSync, rmSync } from "node:fs";
750
+ import { join } from "node:path";
751
+ import { openStore, save } from "../src/store.ts";
752
+ import { runDistillRouter } from "../src/cognition/distill_router.ts";
753
+
754
+ const TEST_DIR = join(import.meta.dir, ".tmp_mcp_distill_route_test");
755
+
756
+ describe("MCP memory_distill_route Tool Handler", () => {
757
+ beforeEach(() => {
758
+ mkdirSync(TEST_DIR, { recursive: true });
759
+ });
760
+
761
+ afterEach(() => {
762
+ rmSync(TEST_DIR, { recursive: true, force: true });
763
+ });
764
+
765
+ it("returns structured JSON across 3 lanes", () => {
766
+ const store = openStore({ memoryDir: TEST_DIR });
767
+ save(store, {
768
+ id: "mem_fix_mcp",
769
+ title: "Tool registration patch",
770
+ content: "Exported tool schemas.",
771
+ type: "fix",
772
+ status: "confirmed",
773
+ project: "core",
774
+ });
775
+
776
+ const res = runDistillRouter(store, { workspaceRoot: TEST_DIR });
777
+ expect(res.ok).toBe(true);
778
+ expect(res.routed.system.length).toBe(1);
779
+ expect(res.total_routed).toBe(1);
780
+ });
781
+ });
782
+ ```
783
+
784
+ - [ ] **Step 2: Add CLI command handler in `src/cli/lifecycle.ts` and wire into `src/cli.ts`**
785
+
786
+ Add `handleDistillRouteCommand` to `src/cli/lifecycle.ts`:
787
+ ```typescript
788
+ export async function handleDistillRouteCommand(args: ParsedArgs): Promise<void> {
789
+ const root = requireRoot();
790
+ const store = openStore({ memoryDir: root });
791
+ const force = Boolean(args.flags["force"]);
792
+ const dryRun = Boolean(args.flags["dry-run"]);
793
+ const result = runDistillRouter(store, { workspaceRoot: process.cwd(), force, dryRun });
794
+
795
+ console.log(`\n🧠 Multi-Channel Knowledge Distillation Summary:`);
796
+ console.log(`- System Lane (Fixes/Rules): ${result.routed.system.length}`);
797
+ console.log(`- Content Lane (Architecture/Concepts): ${result.routed.content.length}`);
798
+ console.log(`- Health Lane (Contradictions): ${result.routed.health.length}`);
799
+ console.log(`- Skipped Previously Surfaced: ${result.skipped_surfaced}`);
800
+ console.log(`- Total Routed: ${result.total_routed}\n`);
801
+ }
802
+ ```
803
+
804
+ In `src/cli.ts`, map `"distill-route"` to `handleDistillRouteCommand`.
805
+
806
+ - [ ] **Step 3: Register MCP Tool in `src/mcp.ts`**
807
+
808
+ 1. Add tool definition to `ListToolsRequestSchema`:
809
+ ```typescript
810
+ {
811
+ name: "memory_distill_route",
812
+ description: "Runs the multi-channel knowledge distillation router, directing confirmed memories into system upgrades, content drafts, and health alerts with stateful deduplication.",
813
+ inputSchema: {
814
+ type: "object",
815
+ properties: {
816
+ force: { type: "boolean", description: "Bypass deduplication and re-route previously surfaced items." },
817
+ dry_run: { type: "boolean", description: "Simulate distillation without writing state.json." }
818
+ }
819
+ }
820
+ }
821
+ ```
822
+
823
+ 2. Add case `"memory_distill_route"` in `CallToolRequestSchema` calling `runDistillRouter`.
824
+
825
+ - [ ] **Step 4: Run test to verify it passes**
826
+
827
+ Run: `bun test test/mcp_distill_route.test.ts`
828
+ Expected: PASS.
829
+
830
+ - [ ] **Step 5: Commit**
831
+
832
+ ```bash
833
+ git add src/cli/lifecycle.ts src/cli.ts src/mcp.ts test/mcp_distill_route.test.ts
834
+ git commit -m "feat(mcp,cli): register memory_distill_route tool and CLI handler"
835
+ ```
836
+
837
+ ---
838
+
839
+ ### Task 6: Full Gate Verification & Ticket Documentation Pass
840
+
841
+ **Files:**
842
+ - Modify: `.agents/context/ticket-plan.md`
843
+ - Modify: `.agents/context/current.md`
844
+
845
+ - [ ] **Step 1: Run Full Gate Verification**
846
+
847
+ Run the oracle commands:
848
+ 1. `bunx tsc --noEmit` āž” Must exit 0.
849
+ 2. `bun test` āž” All suites pass.
850
+ 3. `bun run build` āž” Clean build.
851
+
852
+ - [ ] **Step 2: Update Ticket Plan & Current Context**
853
+
854
+ In `.agents/context/ticket-plan.md`:
855
+ - Mark `T-324`, `T-050`, and `T-177` as `[x] landed Wave 21`.
856
+
857
+ In `.agents/context/current.md`:
858
+ - Document Wave 21 feature release notes (Defensive Mutation Guards, Pre-Mutation 30-Snapshot Ring Buffer, Diff-Preserving `CURRENT.md` Compaction Merge, and Multi-Channel Distillation Router).
859
+
860
+ - [ ] **Step 3: Commit**
861
+
862
+ ```bash
863
+ git add .agents/context/ticket-plan.md .agents/context/current.md
864
+ git commit -m "docs(context): record Wave 21 landing (T-324, T-050, T-177)"
865
+ ```