wayfind 2.0.53 → 2.0.55

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.
@@ -979,6 +979,13 @@ function getEntryContent(entryId, options = {}) {
979
979
  return null;
980
980
  }
981
981
 
982
+ // ── Distilled entries ───────────────────────────────────────────────────
983
+ if (entry.source === 'distilled') {
984
+ try {
985
+ return fs.readFileSync(path.join(storePath, 'distilled', `${entryId}.md`), 'utf8');
986
+ } catch { return null; }
987
+ }
988
+
982
989
  // ── Conversation entries ────────────────────────────────────────────────
983
990
  if (entry.source === 'conversation') {
984
991
  // Try journal path first (for --export'd conversations)
package/bin/distill.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
4
+ const path = require('path');
3
5
  const contentStore = require('./content-store');
4
6
  const llm = require('./connectors/llm');
5
7
 
@@ -319,6 +321,16 @@ async function distillEntries(options = {}) {
319
321
 
320
322
  index.entryCount = Object.keys(index.entries).length;
321
323
  backend.saveIndex(index);
324
+
325
+ // Persist merged content to disk so it survives export/import
326
+ try {
327
+ const distilledDir = path.join(storePath, 'distilled');
328
+ if (!fs.existsSync(distilledDir)) fs.mkdirSync(distilledDir, { recursive: true });
329
+ fs.writeFileSync(path.join(distilledDir, `${id}.md`), content, 'utf8');
330
+ } catch (writeErr) {
331
+ console.log(` Warning: could not persist distilled content: ${writeErr.message}`);
332
+ }
333
+
322
334
  totalStats.merged += cluster.length;
323
335
  } catch (err) {
324
336
  console.log(` Merge failed for cluster: ${err.message}`);
@@ -343,6 +355,114 @@ async function distillEntries(options = {}) {
343
355
  return totalStats;
344
356
  }
345
357
 
358
+ // ── Export / Import ──────────────────────────────────────────────────────────
359
+
360
+ /**
361
+ * Export all LLM-merged distilled entries (with content) from the content store.
362
+ * Used by the GHA distillation pipeline to commit results back to the team repo.
363
+ * @param {string} [storePath]
364
+ * @returns {Array<Object>}
365
+ */
366
+ function exportDistilled(storePath) {
367
+ const resolvedPath = storePath || contentStore.resolveStorePath();
368
+ const backend = contentStore.getBackend(resolvedPath);
369
+ const index = backend.loadIndex();
370
+ const distilledDir = path.join(resolvedPath, 'distilled');
371
+ const entries = [];
372
+
373
+ for (const [id, entry] of Object.entries(index.entries || {})) {
374
+ if (!entry.distilledFrom || !entry.distilledFrom.length) continue;
375
+
376
+ let content = null;
377
+ try {
378
+ content = fs.readFileSync(path.join(distilledDir, `${id}.md`), 'utf8');
379
+ } catch { /* content not persisted yet — skip */ }
380
+ if (!content) continue;
381
+
382
+ entries.push({
383
+ id,
384
+ date: entry.date,
385
+ repo: entry.repo,
386
+ title: entry.title,
387
+ content,
388
+ distill_tier: entry.distillTier,
389
+ distilled_from: entry.distilledFrom,
390
+ distilled_at: entry.distilledAt,
391
+ content_hash: entry.contentHash,
392
+ quality_score: entry.qualityScore || 3,
393
+ tags: entry.tags || [],
394
+ });
395
+ }
396
+
397
+ return entries;
398
+ }
399
+
400
+ /**
401
+ * Idempotently import distilled entries (from a team repo's distilled.json) into
402
+ * the local content store. Entries already present (by content_hash) are skipped.
403
+ * @param {Array<Object>} entries - Array of entries from exportDistilled()
404
+ * @param {string} [storePath]
405
+ * @returns {{ imported: number, skipped: number }}
406
+ */
407
+ function importDistilled(entries, storePath) {
408
+ const resolvedPath = storePath || contentStore.resolveStorePath();
409
+ const backend = contentStore.getBackend(resolvedPath);
410
+ const index = backend.loadIndex();
411
+ const distilledDir = path.join(resolvedPath, 'distilled');
412
+
413
+ const existingHashes = new Set(
414
+ Object.values(index.entries || {}).map(e => e.contentHash).filter(Boolean)
415
+ );
416
+
417
+ let imported = 0;
418
+ let skipped = 0;
419
+
420
+ for (const entry of entries) {
421
+ if (existingHashes.has(entry.content_hash)) {
422
+ skipped++;
423
+ continue;
424
+ }
425
+
426
+ // Persist content file
427
+ try {
428
+ if (!fs.existsSync(distilledDir)) fs.mkdirSync(distilledDir, { recursive: true });
429
+ fs.writeFileSync(path.join(distilledDir, `${entry.id}.md`), entry.content || '', 'utf8');
430
+ } catch (writeErr) {
431
+ console.log(` Warning: could not write content for ${entry.id}: ${writeErr.message}`);
432
+ continue;
433
+ }
434
+
435
+ index.entries[entry.id] = {
436
+ date: entry.date,
437
+ repo: entry.repo,
438
+ title: entry.title,
439
+ source: 'distilled',
440
+ user: '',
441
+ drifted: false,
442
+ contentHash: entry.content_hash,
443
+ contentLength: (entry.content || '').length,
444
+ tags: entry.tags || [],
445
+ hasEmbedding: false,
446
+ hasReasoning: true,
447
+ hasAlternatives: false,
448
+ qualityScore: entry.quality_score || 3,
449
+ distillTier: entry.distill_tier,
450
+ distilledFrom: entry.distilled_from,
451
+ distilledAt: entry.distilled_at,
452
+ };
453
+
454
+ existingHashes.add(entry.content_hash);
455
+ imported++;
456
+ }
457
+
458
+ if (imported > 0) {
459
+ index.entryCount = Object.keys(index.entries).length;
460
+ backend.saveIndex(index);
461
+ }
462
+
463
+ return { imported, skipped };
464
+ }
465
+
346
466
  // ── Exports ─────────────────────────────────────────────────────────────────
347
467
 
348
468
  module.exports = {
@@ -351,5 +471,7 @@ module.exports = {
351
471
  deduplicateGroup,
352
472
  mergeEntries,
353
473
  titleSimilarity,
474
+ exportDistilled,
475
+ importDistilled,
354
476
  TIERS,
355
477
  };
@@ -1366,6 +1366,45 @@ async function runReindex(args) {
1366
1366
 
1367
1367
  async function runDistill(args) {
1368
1368
  const distill = require('./distill');
1369
+
1370
+ // Sub-subcommands: export and import
1371
+ const sub = args[0];
1372
+
1373
+ if (sub === 'export') {
1374
+ const outputIdx = args.indexOf('--output');
1375
+ const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
1376
+ const entries = distill.exportDistilled();
1377
+ const payload = JSON.stringify({ version: '1', generated_at: new Date().toISOString(), entries }, null, 2);
1378
+ if (outputPath) {
1379
+ const outDir = path.dirname(outputPath);
1380
+ if (outDir && outDir !== '.') fs.mkdirSync(outDir, { recursive: true });
1381
+ fs.writeFileSync(outputPath, payload, 'utf8');
1382
+ console.log(`Exported ${entries.length} distilled entries → ${outputPath}`);
1383
+ } else {
1384
+ process.stdout.write(payload + '\n');
1385
+ }
1386
+ return;
1387
+ }
1388
+
1389
+ if (sub === 'import') {
1390
+ const filePath = args[1];
1391
+ if (!filePath) {
1392
+ console.error('Usage: wayfind distill import <path/to/distilled.json>');
1393
+ process.exit(1);
1394
+ }
1395
+ let payload;
1396
+ try {
1397
+ payload = JSON.parse(fs.readFileSync(filePath, 'utf8'));
1398
+ } catch (err) {
1399
+ console.error(`Could not read ${filePath}: ${err.message}`);
1400
+ process.exit(1);
1401
+ }
1402
+ const entries = Array.isArray(payload) ? payload : (payload.entries || []);
1403
+ const { imported, skipped } = distill.importDistilled(entries);
1404
+ console.log(`Import complete: ${imported} new entries, ${skipped} already present`);
1405
+ return;
1406
+ }
1407
+
1369
1408
  const dryRun = args.includes('--dry-run');
1370
1409
  const tierIdx = args.indexOf('--tier');
1371
1410
  const tier = (tierIdx !== -1 && args[tierIdx + 1]) ? args[tierIdx + 1] : 'daily';
@@ -3638,6 +3677,19 @@ async function contextPull(args) {
3638
3677
  }
3639
3678
  } catch (_) {}
3640
3679
  }
3680
+ // Import distilled entries if the team repo has a distilled.json
3681
+ const distilledJson = path.join(teamPath, '.wayfind', 'distilled.json');
3682
+ if (fs.existsSync(distilledJson)) {
3683
+ try {
3684
+ const { importDistilled } = require('./distill');
3685
+ const payload = JSON.parse(fs.readFileSync(distilledJson, 'utf8'));
3686
+ const entries = Array.isArray(payload) ? payload : (payload.entries || []);
3687
+ const { imported } = importDistilled(entries);
3688
+ if (!quiet && imported > 0) {
3689
+ log(`[wayfind] Imported ${imported} distilled entries from team repo`);
3690
+ }
3691
+ } catch (_) {}
3692
+ }
3641
3693
  } else if (result.error && result.error.code === 'ETIMEDOUT') {
3642
3694
  log('[wayfind] Team-context pull timed out — using local state');
3643
3695
  } else {
@@ -5762,7 +5814,7 @@ const COMMANDS = {
5762
5814
  console.log(`\nUpdating ${containerName} (compose: ${composeDir})...`);
5763
5815
  const pullResult = spawnSync('docker', ['compose', 'pull'], { cwd: composeDir, stdio: 'inherit' });
5764
5816
  if (!pullResult.error && pullResult.status === 0) {
5765
- spawnSync('docker', ['compose', 'up', '-d'], { cwd: composeDir, stdio: 'inherit' });
5817
+ spawnSync('docker', ['compose', 'up', '-d', '--force-recreate'], { cwd: composeDir, stdio: 'inherit' });
5766
5818
  console.log(`${containerName} updated.`);
5767
5819
 
5768
5820
  // Post-deploy smoke check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.53",
3
+ "version": "2.0.55",
4
4
  "description": "Team decision trail for AI-assisted development. The connective tissue between product, engineering, and strategy.",
5
5
  "bin": {
6
6
  "wayfind": "./bin/team-context.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.53",
3
+ "version": "2.0.55",
4
4
  "description": "Team decision trail for AI-assisted development. Session memory, decision journals, and team digests.",
5
5
  "author": {
6
6
  "name": "Wayfind",
@@ -0,0 +1,72 @@
1
+ # Wayfind distillation pipeline
2
+ #
3
+ # Copy this file to .github/workflows/wayfind-distill.yml in your team-context repo.
4
+ #
5
+ # What it does:
6
+ # - Runs on every push that touches journals/
7
+ # - Indexes all journals into a fresh content store
8
+ # - Runs the distillation pipeline (dedup + LLM merge for daily/weekly/archive tiers)
9
+ # - Exports distilled entries to .wayfind/distilled.json
10
+ # - Commits the result back to the repo
11
+ #
12
+ # Team members get the distilled entries automatically on their next `wayfind context pull`.
13
+ #
14
+ # Required secrets:
15
+ # ANTHROPIC_API_KEY — for LLM merge calls during distillation
16
+ #
17
+ # Optional: set WAYFIND_DISTILL_TIER to 'daily', 'weekly', 'archive', or 'all' (default: all)
18
+
19
+ name: Wayfind Distillation
20
+
21
+ on:
22
+ push:
23
+ paths:
24
+ - 'journals/**'
25
+ workflow_dispatch:
26
+
27
+ jobs:
28
+ distill:
29
+ runs-on: ubuntu-latest
30
+ permissions:
31
+ contents: write
32
+
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ with:
36
+ token: ${{ secrets.GITHUB_TOKEN }}
37
+
38
+ - uses: actions/setup-node@v4
39
+ with:
40
+ node-version: '20'
41
+
42
+ - name: Install Wayfind
43
+ run: npm install -g wayfind
44
+
45
+ - name: Index journals
46
+ env:
47
+ TEAM_CONTEXT_STORE_PATH: ${{ github.workspace }}/.wayfind/store
48
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
49
+ run: wayfind index-journals --dir ./journals --no-embeddings
50
+
51
+ - name: Distill
52
+ env:
53
+ TEAM_CONTEXT_STORE_PATH: ${{ github.workspace }}/.wayfind/store
54
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
55
+ run: wayfind distill --tier ${{ vars.WAYFIND_DISTILL_TIER || 'all' }}
56
+
57
+ - name: Export distilled entries
58
+ env:
59
+ TEAM_CONTEXT_STORE_PATH: ${{ github.workspace }}/.wayfind/store
60
+ run: wayfind distill export --output .wayfind/distilled.json
61
+
62
+ - name: Commit distilled.json if changed
63
+ run: |
64
+ git config user.name "github-actions[bot]"
65
+ git config user.email "github-actions[bot]@users.noreply.github.com"
66
+ git add .wayfind/distilled.json
67
+ if git diff --cached --quiet; then
68
+ echo "No changes to distilled.json"
69
+ else
70
+ git commit -m "chore: update distilled context [skip ci]"
71
+ git push
72
+ fi