engrm 0.4.4 → 0.4.5

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.
@@ -2496,7 +2496,7 @@ function formatSplashScreen(data) {
2496
2496
  }
2497
2497
  function formatVisibleStartupBrief(context) {
2498
2498
  const lines = [];
2499
- const latest = context.summaries?.[0];
2499
+ const latest = pickBestSummary(context);
2500
2500
  if (latest) {
2501
2501
  const sections = [
2502
2502
  ["Investigated", latest.investigated],
@@ -2511,8 +2511,8 @@ function formatVisibleStartupBrief(context) {
2511
2511
  }
2512
2512
  }
2513
2513
  }
2514
- if (context.staleDecisions && context.staleDecisions.length > 0) {
2515
- const stale = context.staleDecisions[0];
2514
+ const stale = pickRelevantStaleDecision(context, latest);
2515
+ if (stale) {
2516
2516
  lines.push(`${c2.yellow}Watch:${c2.reset} ${truncateInline(`Decision still looks unfinished: ${stale.title}`, 170)}`);
2517
2517
  }
2518
2518
  if (lines.length === 0 && context.observations.length > 0) {
@@ -2526,12 +2526,127 @@ function formatVisibleStartupBrief(context) {
2526
2526
  function toSplashBullet(value, maxLen) {
2527
2527
  if (!value)
2528
2528
  return null;
2529
- const cleaned = value.split(`
2530
- `).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).join("; ");
2529
+ const cleaned = dedupeFragments(value.split(`
2530
+ `).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).join("; "));
2531
2531
  if (!cleaned)
2532
2532
  return null;
2533
2533
  return truncateInline(cleaned, maxLen);
2534
2534
  }
2535
+ function pickBestSummary(context) {
2536
+ const summaries = context.summaries || [];
2537
+ if (!summaries.length)
2538
+ return null;
2539
+ return [...summaries].sort((a, b) => scoreSummary(b) - scoreSummary(a))[0] ?? null;
2540
+ }
2541
+ function scoreSummary(summary) {
2542
+ let score = 0;
2543
+ if (summary.request)
2544
+ score += 3;
2545
+ if (summary.investigated)
2546
+ score += 4;
2547
+ if (summary.learned)
2548
+ score += 5;
2549
+ if (summary.completed)
2550
+ score += 5;
2551
+ if (summary.next_steps)
2552
+ score += 4;
2553
+ score += Math.min(4, sectionItemCount(summary.completed) + sectionItemCount(summary.learned));
2554
+ return score;
2555
+ }
2556
+ function sectionItemCount(value) {
2557
+ if (!value)
2558
+ return 0;
2559
+ return value.split(`
2560
+ `).map((line) => line.trim()).filter(Boolean).length;
2561
+ }
2562
+ function dedupeFragments(text) {
2563
+ const parts = text.split(";").map((part) => part.trim()).filter(Boolean);
2564
+ const seen = new Set;
2565
+ const deduped = [];
2566
+ for (const part of parts) {
2567
+ const normalized = part.toLowerCase();
2568
+ if (seen.has(normalized))
2569
+ continue;
2570
+ seen.add(normalized);
2571
+ deduped.push(part);
2572
+ }
2573
+ return deduped.join("; ");
2574
+ }
2575
+ function pickRelevantStaleDecision(context, summary) {
2576
+ const stale = context.staleDecisions || [];
2577
+ if (!stale.length)
2578
+ return null;
2579
+ const summaryText = [
2580
+ summary?.request,
2581
+ summary?.investigated,
2582
+ summary?.learned,
2583
+ summary?.completed,
2584
+ summary?.next_steps
2585
+ ].filter(Boolean).join(" ");
2586
+ let best = null;
2587
+ let bestScore = 0;
2588
+ for (const item of stale) {
2589
+ if ((item.days_ago ?? 999) > 21)
2590
+ continue;
2591
+ const overlap = keywordOverlap(item.title || "", summaryText);
2592
+ const similarity = item.best_match_similarity ?? 0;
2593
+ const score = overlap * 4 + similarity;
2594
+ if (score > bestScore && overlap > 0) {
2595
+ best = item;
2596
+ bestScore = score;
2597
+ }
2598
+ }
2599
+ return best;
2600
+ }
2601
+ function keywordOverlap(a, b) {
2602
+ if (!a || !b)
2603
+ return 0;
2604
+ const stop = new Set([
2605
+ "the",
2606
+ "and",
2607
+ "for",
2608
+ "with",
2609
+ "from",
2610
+ "into",
2611
+ "this",
2612
+ "that",
2613
+ "was",
2614
+ "were",
2615
+ "have",
2616
+ "has",
2617
+ "had",
2618
+ "but",
2619
+ "not",
2620
+ "you",
2621
+ "your",
2622
+ "our",
2623
+ "their",
2624
+ "about",
2625
+ "added",
2626
+ "fixed",
2627
+ "created",
2628
+ "updated",
2629
+ "modified",
2630
+ "changed",
2631
+ "investigate",
2632
+ "next",
2633
+ "steps",
2634
+ "decision",
2635
+ "still",
2636
+ "looks",
2637
+ "unfinished"
2638
+ ]);
2639
+ const wordsA = new Set(a.toLowerCase().match(/[a-z0-9_+-]{4,}/g)?.filter((w) => !stop.has(w)) || []);
2640
+ const wordsB = new Set(b.toLowerCase().match(/[a-z0-9_+-]{4,}/g)?.filter((w) => !stop.has(w)) || []);
2641
+ if (!wordsA.size || !wordsB.size)
2642
+ return 0;
2643
+ let overlap = 0;
2644
+ for (const word of wordsA) {
2645
+ if (wordsB.has(word))
2646
+ overlap++;
2647
+ }
2648
+ return overlap / Math.max(1, Math.min(wordsA.size, wordsB.size));
2649
+ }
2535
2650
  function truncateInline(text, maxLen) {
2536
2651
  const compact = text.replace(/\s+/g, " ").trim();
2537
2652
  if (compact.length <= maxLen)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Cross-device, team-shared memory layer for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",