wayfind 2.0.31 → 2.0.33

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.
@@ -126,7 +126,9 @@ function getBackend(storePath) {
126
126
  migrateFromJson(backend, storePath);
127
127
  cache[storePath] = backend;
128
128
  return backend;
129
- } catch {
129
+ } catch (err) {
130
+ console.error('Warning: SQLite backend failed to initialize, falling back to JSON.', err.message);
131
+ cache[storePath + ':fallback'] = true;
130
132
  const backend = new JsonBackend(storePath);
131
133
  backend.open();
132
134
  cache[storePath] = backend;
@@ -164,8 +166,23 @@ function getBackendType(storePath) {
164
166
  return null;
165
167
  }
166
168
 
169
+ /**
170
+ * Returns detailed backend info including whether it was a silent fallback.
171
+ *
172
+ * @param {string} storePath
173
+ * @returns {{ type: 'sqlite'|'json'|'unknown', storePath: string, fallback: boolean }|null}
174
+ */
175
+ function getBackendInfo(storePath) {
176
+ const backend = cache[storePath];
177
+ if (!backend) return null;
178
+ const type = backend instanceof JsonBackend ? 'json' :
179
+ (backend.constructor && backend.constructor.name === 'SqliteBackend') ? 'sqlite' : 'unknown';
180
+ return { type, storePath, fallback: !!cache[storePath + ':fallback'] };
181
+ }
182
+
167
183
  module.exports = {
168
184
  getBackend,
169
185
  clearCache,
170
186
  getBackendType,
187
+ getBackendInfo,
171
188
  };
@@ -3283,9 +3283,12 @@ async function runContext(args) {
3283
3283
  case 'default':
3284
3284
  contextSetDefault(subArgs);
3285
3285
  break;
3286
+ case 'pull':
3287
+ contextPull(subArgs);
3288
+ break;
3286
3289
  default:
3287
3290
  console.error(`Unknown context subcommand: ${sub}`);
3288
- console.error('Available: init, sync, show, add, bind, list, default');
3291
+ console.error('Available: init, sync, show, add, bind, list, default, pull');
3289
3292
  process.exit(1);
3290
3293
  }
3291
3294
  }
@@ -3428,6 +3431,61 @@ function contextSync() {
3428
3431
  console.log(`\nSynced ${synced} file(s) to .claude/context/`);
3429
3432
  }
3430
3433
 
3434
+ /**
3435
+ * Pull latest from the team-context repo so session state reflects
3436
+ * other engineers' recent work. Called by the session-start hook.
3437
+ *
3438
+ * Behavior:
3439
+ * - Pulls the team-context repo (journals, signals, shared context)
3440
+ * - Skips gracefully if offline, if pull fails, or if repo is dirty
3441
+ * - Does NOT touch the current working repo — devs handle that themselves
3442
+ * - Designed to run within the session-start hook's timeout
3443
+ *
3444
+ * @param {string[]} args - CLI arguments (--quiet suppresses output)
3445
+ */
3446
+ function contextPull(args) {
3447
+ const quiet = args.includes('--quiet');
3448
+ const background = args.includes('--background');
3449
+ const log = quiet ? () => {} : console.log;
3450
+
3451
+ const teamPath = getTeamContextPath();
3452
+ if (!teamPath || !fs.existsSync(path.join(teamPath, '.git'))) {
3453
+ if (!quiet) log('[wayfind] No team-context repo configured — skipping pull');
3454
+ return;
3455
+ }
3456
+
3457
+ const markerFile = path.join(teamPath, '.last-pull');
3458
+
3459
+ if (background) {
3460
+ // Fire-and-forget — don't block session start
3461
+ const child = require('child_process').spawn(
3462
+ process.execPath,
3463
+ [__filename, 'context', 'pull', '--quiet'],
3464
+ { stdio: 'ignore', detached: true, env: { ...process.env } }
3465
+ );
3466
+ child.unref();
3467
+ return;
3468
+ }
3469
+
3470
+ const result = spawnSync('git', ['-C', teamPath, 'pull', '--rebase', '--autostash', '--quiet'], {
3471
+ stdio: 'pipe',
3472
+ timeout: 10000,
3473
+ });
3474
+
3475
+ if (result.status === 0) {
3476
+ log('[wayfind] Pulled latest team-context');
3477
+ // Mark success — doctor checks this to warn on prolonged failures
3478
+ try { fs.writeFileSync(markerFile, new Date().toISOString()); } catch {}
3479
+ } else if (result.error && result.error.code === 'ETIMEDOUT') {
3480
+ log('[wayfind] Team-context pull timed out — using local state');
3481
+ } else {
3482
+ const stderr = (result.stderr || '').toString().trim();
3483
+ if (stderr && !quiet) {
3484
+ console.error(`[wayfind] Team-context pull skipped: ${stderr.split('\n')[0]}`);
3485
+ }
3486
+ }
3487
+ }
3488
+
3431
3489
  function contextShow() {
3432
3490
  const config = readContextConfig();
3433
3491
  const repoBinding = readRepoTeamBinding();
@@ -4312,10 +4370,10 @@ function ensureContainerConfig() {
4312
4370
  }
4313
4371
  }
4314
4372
 
4315
- // GitHub connector
4316
- if (!config.github && process.env.GITHUB_TOKEN) {
4373
+ // GitHub connector — create if missing, or fix transport if mounted config has gh-cli
4374
+ if (process.env.GITHUB_TOKEN) {
4317
4375
  const repos = process.env.TEAM_CONTEXT_GITHUB_REPOS;
4318
- if (repos) {
4376
+ if (!config.github && repos) {
4319
4377
  config.github = {
4320
4378
  transport: 'https',
4321
4379
  token: process.env.GITHUB_TOKEN,
@@ -4324,6 +4382,19 @@ function ensureContainerConfig() {
4324
4382
  last_pull: null,
4325
4383
  };
4326
4384
  changed = true;
4385
+ } else if (config.github) {
4386
+ // Override gh-cli transport in container context — gh binary isn't available
4387
+ if (config.github.transport === 'gh-cli') {
4388
+ config.github.transport = 'https';
4389
+ config.github.token = process.env.GITHUB_TOKEN;
4390
+ config.github.token_env = 'GITHUB_TOKEN';
4391
+ changed = true;
4392
+ }
4393
+ // Backfill repos from env if config has none or env specifies them
4394
+ if (repos && (!config.github.repos || config.github.repos.length === 0)) {
4395
+ config.github.repos = repos.split(',').map((r) => r.trim());
4396
+ changed = true;
4397
+ }
4327
4398
  }
4328
4399
  }
4329
4400
 
@@ -5001,8 +5072,9 @@ const COMMANDS = {
5001
5072
  }
5002
5073
  console.log('Sanitization passed — no proprietary content detected.');
5003
5074
 
5004
- // Show what changed
5005
- const diffResult = spawnSync('git', ['status', '--short'], { cwd: tmpDir, stdio: 'pipe' });
5075
+ // Refresh git index so status detects copied files reliably
5076
+ spawnSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
5077
+ const diffResult = spawnSync('git', ['diff', '--cached', '--name-only'], { cwd: tmpDir, stdio: 'pipe' });
5006
5078
  const changes = (diffResult.stdout || '').toString().trim();
5007
5079
 
5008
5080
  if (!changes) {
package/doctor.sh CHANGED
@@ -269,6 +269,54 @@ check_memory_files() {
269
269
  [ "$COUNT" -gt 0 ] && ok "$COUNT memory file(s) found" || info "No memory files yet"
270
270
  }
271
271
 
272
+ check_team_context_freshness() {
273
+ echo ""
274
+ echo "Team context sync"
275
+
276
+ # Find team-context path via wayfind CLI
277
+ local TEAM_PATH=""
278
+ if [ -f "$HOME/repos/greg/wayfind/bin/team-context.js" ]; then
279
+ TEAM_PATH=$(node "$HOME/repos/greg/wayfind/bin/team-context.js" context show 2>/dev/null | grep 'Path:' | head -1 | sed 's/.*Path: *//' || true)
280
+ elif command -v wayfind >/dev/null 2>&1; then
281
+ TEAM_PATH=$(wayfind context show 2>/dev/null | grep 'Path:' | head -1 | sed 's/.*Path: *//' || true)
282
+ fi
283
+
284
+ if [ -z "$TEAM_PATH" ] || [ ! -d "$TEAM_PATH" ]; then
285
+ info "No team-context repo configured (optional)"
286
+ return
287
+ fi
288
+
289
+ local MARKER="$TEAM_PATH/.last-pull"
290
+ if [ ! -f "$MARKER" ]; then
291
+ # No pull has ever succeeded — check if this is a fresh install
292
+ # by looking at the git log age instead
293
+ local LAST_COMMIT
294
+ LAST_COMMIT=$(git -C "$TEAM_PATH" log -1 --format='%ci' 2>/dev/null || echo "")
295
+ if [ -z "$LAST_COMMIT" ]; then
296
+ info "Team-context repo exists but has no commits"
297
+ else
298
+ ok "Team-context configured (pull marker will appear after next session start)"
299
+ fi
300
+ return
301
+ fi
302
+
303
+ local PULL_TIME
304
+ PULL_TIME=$(cat "$MARKER")
305
+ local PULL_EPOCH
306
+ PULL_EPOCH=$(date -d "$PULL_TIME" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${PULL_TIME%%.*}" +%s 2>/dev/null || echo 0)
307
+ local NOW_EPOCH
308
+ NOW_EPOCH=$(date +%s)
309
+ local AGE_HOURS=$(( (NOW_EPOCH - PULL_EPOCH) / 3600 ))
310
+
311
+ if [ "$AGE_HOURS" -gt 24 ]; then
312
+ warn "Team-context last pulled ${AGE_HOURS}h ago — other engineers' context may be stale"
313
+ info "Run: wayfind context pull"
314
+ ISSUES=$((ISSUES + 1))
315
+ else
316
+ ok "Team-context pulled ${AGE_HOURS}h ago"
317
+ fi
318
+ }
319
+
272
320
  check_team_versions() {
273
321
  echo ""
274
322
  echo "Team version compliance"
@@ -309,17 +357,116 @@ check_storage_backend() {
309
357
  echo ""
310
358
  echo "Storage backend"
311
359
 
360
+ set +e # Temporarily disable errexit — node calls may fail in test environments
312
361
  # Check for env var override
313
362
  if [ -n "${TEAM_CONTEXT_STORAGE_BACKEND:-}" ]; then
314
363
  info "TEAM_CONTEXT_STORAGE_BACKEND=$TEAM_CONTEXT_STORAGE_BACKEND (env override)"
315
364
  fi
316
365
 
317
- # Check if better-sqlite3 is available
318
- if node -e "require('better-sqlite3')" 2>/dev/null; then
319
- ok "Storage backend: sqlite (better-sqlite3 available)"
366
+ local WAYFIND_DIR="${WAYFIND_DIR:-$HOME/.claude/team-context}"
367
+ local SCRIPT_DIR
368
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
369
+
370
+ # ── 1. Which backend is active ───────────────────────────────────────────
371
+ local BACKEND_RESULT=""
372
+ BACKEND_RESULT=$(node -e "
373
+ try {
374
+ const storage = require('$SCRIPT_DIR/bin/storage/index.js');
375
+ const storePath = '$WAYFIND_DIR';
376
+ storage.getBackend(storePath);
377
+ const info = storage.getBackendInfo(storePath);
378
+ if (!info) { console.log('NONE'); process.exit(0); }
379
+ console.log(JSON.stringify(info));
380
+ } catch (e) {
381
+ console.log('SKIP:' + e.message.split('\n')[0]);
382
+ }
383
+ " 2>/dev/null) || BACKEND_RESULT="SKIP:storage module not available"
384
+
385
+ if [[ "$BACKEND_RESULT" == SKIP:* ]]; then
386
+ [ "$VERBOSE" = true ] && info "Storage check skipped: ${BACKEND_RESULT#SKIP:}"
387
+ elif [[ "$BACKEND_RESULT" == "NONE" ]]; then
388
+ info "No storage backend initialized (team-context not configured)"
389
+ else
390
+ local BACKEND_TYPE
391
+ BACKEND_TYPE=$(echo "$BACKEND_RESULT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(d.type)" 2>/dev/null || echo "unknown")
392
+ local IS_FALLBACK
393
+ IS_FALLBACK=$(echo "$BACKEND_RESULT" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(d.fallback)" 2>/dev/null || echo "false")
394
+
395
+ if [ "$BACKEND_TYPE" = "sqlite" ]; then
396
+ ok "Active backend: sqlite"
397
+ elif [ "$BACKEND_TYPE" = "json" ]; then
398
+ ok "Active backend: json"
399
+ else
400
+ warn "Active backend: $BACKEND_TYPE (unexpected)"
401
+ fi
402
+
403
+ # ── 2. Fallback detection ────────────────────────────────────────────
404
+ if [ "$IS_FALLBACK" = "true" ]; then
405
+ warn "JSON backend is a fallback — better-sqlite3 was expected but failed to load"
406
+ info "Run: npm install -g better-sqlite3 (or reinstall wayfind)"
407
+ ISSUES=$((ISSUES + 1))
408
+ elif [ "$BACKEND_TYPE" = "json" ]; then
409
+ # Not a runtime fallback — check if sqlite3 is available but unused
410
+ if node -e "require('better-sqlite3')" 2>/dev/null; then
411
+ warn "better-sqlite3 is installed but backend is JSON — possible unexpected fallback"
412
+ info "Check TEAM_CONTEXT_STORAGE_BACKEND env var or re-run setup"
413
+ ISSUES=$((ISSUES + 1))
414
+ else
415
+ [ "$VERBOSE" = true ] && info "better-sqlite3 not installed — JSON is expected"
416
+ fi
417
+ fi
418
+ fi
419
+
420
+ # ── 3. Signal freshness (connectors.json last_pull) ──────────────────────
421
+ local CONNECTORS="$WAYFIND_DIR/connectors.json"
422
+ if [ -f "$CONNECTORS" ]; then
423
+ local FRESHNESS_RESULT
424
+ FRESHNESS_RESULT=$(node -e "
425
+ const fs = require('fs');
426
+ const config = JSON.parse(fs.readFileSync('$CONNECTORS', 'utf8'));
427
+ const connectors = ['github', 'intercom', 'notion'];
428
+ const now = Date.now();
429
+ const DAY = 24 * 60 * 60 * 1000;
430
+ const results = [];
431
+ for (const name of connectors) {
432
+ const c = config[name];
433
+ if (!c) continue;
434
+ const lp = c.last_pull;
435
+ if (!lp) {
436
+ results.push('WARN:' + name + ':never pulled');
437
+ } else {
438
+ const age = now - new Date(lp).getTime();
439
+ const hours = Math.floor(age / (60 * 60 * 1000));
440
+ if (age > DAY) {
441
+ results.push('WARN:' + name + ':' + hours + 'h ago');
442
+ } else {
443
+ results.push('OK:' + name + ':' + hours + 'h ago');
444
+ }
445
+ }
446
+ }
447
+ console.log(results.join('|'));
448
+ " 2>/dev/null) || FRESHNESS_RESULT=""
449
+
450
+ if [ -n "$FRESHNESS_RESULT" ]; then
451
+ IFS='|' read -ra ENTRIES <<< "$FRESHNESS_RESULT"
452
+ for entry in "${ENTRIES[@]}"; do
453
+ local STATUS="${entry%%:*}"
454
+ local REST="${entry#*:}"
455
+ local NAME="${REST%%:*}"
456
+ local DETAIL="${REST#*:}"
457
+ if [ "$STATUS" = "WARN" ]; then
458
+ warn "Signal $NAME: $DETAIL"
459
+ info "Run: wayfind pull $NAME"
460
+ ISSUES=$((ISSUES + 1))
461
+ else
462
+ ok "Signal $NAME: last pull $DETAIL"
463
+ fi
464
+ done
465
+ fi
320
466
  else
321
- ok "Storage backend: json (better-sqlite3 not installedusing JSON fallback)"
467
+ [ "$VERBOSE" = true ] && info "No connectors.json signal freshness check skipped"
322
468
  fi
469
+ set -e # Restore errexit
323
470
  }
324
471
 
325
472
  # Run checks
@@ -330,6 +477,7 @@ echo "══════════════════════"
330
477
  check_hook_registered
331
478
  check_global_state
332
479
  check_backup
480
+ check_team_context_freshness
333
481
  check_team_versions
334
482
  check_storage_backend
335
483
  check_memory_files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.31",
3
+ "version": "2.0.33",
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"
@@ -25,6 +25,10 @@ if [ -z "$WAYFIND" ]; then
25
25
  exit 0
26
26
  fi
27
27
 
28
+ # Pull latest team-context in the background so this session sees
29
+ # other engineers' recent work without blocking session start
30
+ $WAYFIND context pull --quiet --background 2>/dev/null || true
31
+
28
32
  # Rebuild Active Projects table (idempotent, concurrent-safe)
29
33
  $WAYFIND status --write --quiet 2>/dev/null || true
30
34