laminark 2.21.8 → 2.21.9

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 (52) hide show
  1. package/README.md +36 -71
  2. package/package.json +9 -7
  3. package/plugin/.claude-plugin/plugin.json +2 -2
  4. package/plugin/CLAUDE.md +10 -0
  5. package/plugin/commands/recall.md +55 -0
  6. package/plugin/commands/remember.md +34 -0
  7. package/plugin/commands/resume.md +45 -0
  8. package/plugin/commands/stash.md +34 -0
  9. package/plugin/commands/status.md +33 -0
  10. package/plugin/dist/hooks/handler.d.ts +3 -1
  11. package/plugin/dist/hooks/handler.d.ts.map +1 -1
  12. package/plugin/dist/hooks/handler.js +312 -23
  13. package/plugin/dist/hooks/handler.js.map +1 -1
  14. package/plugin/dist/index.d.ts +3 -1
  15. package/plugin/dist/index.d.ts.map +1 -1
  16. package/plugin/dist/index.js +2111 -525
  17. package/plugin/dist/index.js.map +1 -1
  18. package/plugin/dist/{observations-Ch0nc47i.d.mts → observations-CorAAc1A.d.mts} +23 -1
  19. package/plugin/dist/observations-CorAAc1A.d.mts.map +1 -0
  20. package/plugin/dist/{tool-registry-CZ3mJ4iR.mjs → tool-registry-D8un_AcG.mjs} +932 -13
  21. package/plugin/dist/tool-registry-D8un_AcG.mjs.map +1 -0
  22. package/plugin/hooks/hooks.json +6 -6
  23. package/plugin/laminark.db +0 -0
  24. package/plugin/package.json +17 -0
  25. package/plugin/scripts/README.md +19 -1
  26. package/plugin/scripts/bump-version.sh +24 -19
  27. package/plugin/scripts/dev-sync.sh +58 -0
  28. package/plugin/scripts/ensure-deps.sh +5 -2
  29. package/plugin/scripts/install.sh +115 -39
  30. package/plugin/scripts/local-install.sh +93 -58
  31. package/plugin/scripts/uninstall.sh +76 -38
  32. package/plugin/scripts/update.sh +20 -69
  33. package/plugin/scripts/verify-install.sh +69 -25
  34. package/plugin/ui/activity.js +12 -0
  35. package/plugin/ui/app.js +24 -54
  36. package/plugin/ui/graph.js +413 -186
  37. package/plugin/ui/help/activity-feed.png +0 -0
  38. package/plugin/ui/help/analysis-panel.png +0 -0
  39. package/plugin/ui/help/graph-toolbar.png +0 -0
  40. package/plugin/ui/help/graph-view.png +0 -0
  41. package/plugin/ui/help/settings.png +0 -0
  42. package/plugin/ui/help/timeline.png +0 -0
  43. package/plugin/ui/help.js +876 -172
  44. package/plugin/ui/index.html +506 -242
  45. package/plugin/ui/settings.js +781 -17
  46. package/plugin/ui/styles.css +990 -44
  47. package/plugin/ui/timeline.js +2 -2
  48. package/plugin/ui/tools.js +826 -0
  49. package/.claude-plugin/marketplace.json +0 -15
  50. package/plugin/dist/observations-Ch0nc47i.d.mts.map +0 -1
  51. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +0 -1
  52. package/plugin/scripts/setup-tmpdir.sh +0 -65
@@ -1,5 +1,5 @@
1
1
  import { i as getProjectHash, n as getDatabaseConfig } from "../config-t8LZeB-u.mjs";
2
- import { C as rowToObservation, D as debug, S as ObservationRepository, _ as SaveGuard, a as ResearchBufferRepository, b as SearchEngine, c as inferToolType, d as getNodeByNameAndType, h as traverseFrom, i as NotificationStore, n as PathRepository, o as extractServerName, r as initPathSchema, s as inferScope, t as ToolRegistryRepository, v as jaccardSimilarity, w as openDatabase, x as SessionRepository } from "../tool-registry-CZ3mJ4iR.mjs";
2
+ import { E as traverseFrom, F as openDatabase, M as SessionRepository, N as ObservationRepository, O as SaveGuard, P as rowToObservation, R as debug, S as getNodeByNameAndType, a as ResearchBufferRepository, c as inferScope, i as NotificationStore, j as SearchEngine, k as jaccardSimilarity, l as inferToolType, n as PathRepository, o as BranchRepository, p as runAutoCleanup, r as initPathSchema, s as extractServerName, t as ToolRegistryRepository } from "../tool-registry-D8un_AcG.mjs";
3
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
5
  import { homedir } from "node:os";
@@ -514,14 +514,14 @@ function assembleSessionContext(db, projectHash, toolRegistry) {
514
514
  //#region src/hooks/config-scanner.ts
515
515
  /**
516
516
  * Extracts a description from YAML frontmatter in a Markdown file.
517
- * Reads only the first 500 bytes for performance.
517
+ * Reads only the first 2000 bytes for performance.
518
518
  */
519
519
  function extractDescription(filePath) {
520
520
  try {
521
521
  const fmMatch = readFileSync(filePath, {
522
522
  encoding: "utf-8",
523
523
  flag: "r"
524
- }).slice(0, 500).match(/^---\n([\s\S]*?)\n---/);
524
+ }).slice(0, 2e3).match(/^---\n([\s\S]*?)\n---/);
525
525
  if (!fmMatch) return null;
526
526
  const descMatch = fmMatch[1].match(/description:\s*(.+)/);
527
527
  return descMatch ? descMatch[1].trim() : null;
@@ -530,6 +530,31 @@ function extractDescription(filePath) {
530
530
  }
531
531
  }
532
532
  /**
533
+ * Extracts trigger hints from a command/skill file for proactive suggestion matching.
534
+ * Reads YAML frontmatter `description` + content from `<objective>` blocks.
535
+ * Returns a concatenated string or null if nothing found.
536
+ */
537
+ function extractTriggerHints(filePath) {
538
+ try {
539
+ const content = readFileSync(filePath, {
540
+ encoding: "utf-8",
541
+ flag: "r"
542
+ });
543
+ const head = content.slice(0, 2e3);
544
+ const parts = [];
545
+ const fmMatch = head.match(/^---\n([\s\S]*?)\n---/);
546
+ if (fmMatch) {
547
+ const descMatch = fmMatch[1].match(/description:\s*(.+)/);
548
+ if (descMatch) parts.push(descMatch[1].trim());
549
+ }
550
+ const objMatch = content.match(/<objective>([\s\S]*?)<\/objective>/);
551
+ if (objMatch) parts.push(objMatch[1].trim());
552
+ return parts.length > 0 ? parts.join(" ") : null;
553
+ } catch {
554
+ return null;
555
+ }
556
+ }
557
+ /**
533
558
  * Scans an .mcp.json file for MCP server entries.
534
559
  * Each server key becomes a wildcard tool entry (individual tool names are not in config).
535
560
  */
@@ -546,7 +571,8 @@ function scanMcpJson(filePath, scope, projectHash, tools) {
546
571
  source: `config:${filePath}`,
547
572
  projectHash,
548
573
  description: null,
549
- serverName
574
+ serverName,
575
+ triggerHints: null
550
576
  });
551
577
  } catch (err) {
552
578
  debug("scanner", "Failed to scan MCP config", {
@@ -571,7 +597,8 @@ function scanClaudeJson(filePath, tools) {
571
597
  source: "config:~/.claude.json",
572
598
  projectHash: null,
573
599
  description: null,
574
- serverName
600
+ serverName,
601
+ triggerHints: null
575
602
  });
576
603
  const projects = config.projects;
577
604
  if (projects && typeof projects === "object") for (const projectEntry of Object.values(projects)) {
@@ -583,7 +610,8 @@ function scanClaudeJson(filePath, tools) {
583
610
  source: "config:~/.claude.json",
584
611
  projectHash: null,
585
612
  description: null,
586
- serverName
613
+ serverName,
614
+ triggerHints: null
587
615
  });
588
616
  }
589
617
  } catch (err) {
@@ -603,7 +631,9 @@ function scanCommands(dirPath, scope, projectHash, tools) {
603
631
  const entries = readdirSync(dirPath, { withFileTypes: true });
604
632
  for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".md")) {
605
633
  const cmdName = `/${basename(entry.name, ".md")}`;
606
- const description = extractDescription(join(dirPath, entry.name));
634
+ const filePath = join(dirPath, entry.name);
635
+ const description = extractDescription(filePath);
636
+ const triggerHints = extractTriggerHints(filePath);
607
637
  tools.push({
608
638
  name: cmdName,
609
639
  toolType: "slash_command",
@@ -611,7 +641,8 @@ function scanCommands(dirPath, scope, projectHash, tools) {
611
641
  source: `config:${dirPath}`,
612
642
  projectHash,
613
643
  description,
614
- serverName: null
644
+ serverName: null,
645
+ triggerHints
615
646
  });
616
647
  } else if (entry.isDirectory()) {
617
648
  const subDir = join(dirPath, entry.name);
@@ -619,7 +650,9 @@ function scanCommands(dirPath, scope, projectHash, tools) {
619
650
  const subEntries = readdirSync(subDir, { withFileTypes: true });
620
651
  for (const subEntry of subEntries) if (subEntry.isFile() && subEntry.name.endsWith(".md")) {
621
652
  const cmdName = `/${entry.name}:${basename(subEntry.name, ".md")}`;
622
- const description = extractDescription(join(subDir, subEntry.name));
653
+ const subFilePath = join(subDir, subEntry.name);
654
+ const description = extractDescription(subFilePath);
655
+ const triggerHints = extractTriggerHints(subFilePath);
623
656
  tools.push({
624
657
  name: cmdName,
625
658
  toolType: "slash_command",
@@ -627,7 +660,8 @@ function scanCommands(dirPath, scope, projectHash, tools) {
627
660
  source: `config:${dirPath}`,
628
661
  projectHash,
629
662
  description,
630
- serverName: null
663
+ serverName: null,
664
+ triggerHints
631
665
  });
632
666
  }
633
667
  } catch {}
@@ -650,6 +684,7 @@ function scanSkills(dirPath, scope, projectHash, tools) {
650
684
  const skillMdPath = join(dirPath, entry.name, "SKILL.md");
651
685
  if (existsSync(skillMdPath)) {
652
686
  const description = extractDescription(skillMdPath);
687
+ const triggerHints = extractTriggerHints(skillMdPath);
653
688
  tools.push({
654
689
  name: entry.name,
655
690
  toolType: "skill",
@@ -657,7 +692,8 @@ function scanSkills(dirPath, scope, projectHash, tools) {
657
692
  source: `config:${dirPath}`,
658
693
  projectHash,
659
694
  description,
660
- serverName: null
695
+ serverName: null,
696
+ triggerHints
661
697
  });
662
698
  }
663
699
  }
@@ -691,7 +727,8 @@ function scanInstalledPlugins(filePath, tools) {
691
727
  source: "config:installed_plugins.json",
692
728
  projectHash: null,
693
729
  description: null,
694
- serverName: null
730
+ serverName: null,
731
+ triggerHints: null
695
732
  });
696
733
  if (typeof inst.installPath === "string") scanMcpJson(join(inst.installPath, ".mcp.json"), "plugin", null, tools);
697
734
  }
@@ -916,7 +953,7 @@ function detectRemovedTools(toolRegistry, scannedTools, projectHash) {
916
953
  *
917
954
  * @returns Context string to write to stdout, or null if no context available
918
955
  */
919
- function handleSessionStart(input, sessionRepo, db, projectHash, toolRegistry, pathRepo) {
956
+ function handleSessionStart(input, sessionRepo, db, projectHash, toolRegistry, pathRepo, branchRepo) {
920
957
  const sessionId = input.session_id;
921
958
  if (!sessionId) {
922
959
  debug("session", "SessionStart missing session_id, skipping");
@@ -1001,6 +1038,15 @@ function handleSessionStart(input, sessionRepo, db, projectHash, toolRegistry, p
1001
1038
  } catch {
1002
1039
  debug("session", "Cross-session path check failed (non-fatal)");
1003
1040
  }
1041
+ if (branchRepo) try {
1042
+ const activeBranch = branchRepo.findRecentActiveBranch();
1043
+ if (activeBranch) {
1044
+ const branchContext = `\n[Laminark] Active work branch carried over:\n ${activeBranch.title ?? activeBranch.id.slice(0, 12)} (${activeBranch.branch_type})\n Stage: ${activeBranch.arc_stage} | Observations: ${activeBranch.observation_count}\n Use query_branches to see all branches.\n`;
1045
+ context = context + branchContext;
1046
+ }
1047
+ } catch {
1048
+ debug("session", "Cross-session branch check failed (non-fatal)");
1049
+ }
1004
1050
  return context + (toolRegistry ? "\nCall report_available_tools with all your tools (built-in and MCP) so Laminark can index them for discovery." : "");
1005
1051
  }
1006
1052
  /**
@@ -1029,7 +1075,7 @@ function handleSessionEnd(input, sessionRepo) {
1029
1075
  *
1030
1076
  * If the session has zero observations, this is a graceful no-op.
1031
1077
  */
1032
- function handleStop(input, obsRepo, sessionRepo) {
1078
+ function handleStop(input, obsRepo, sessionRepo, db, projectHash) {
1033
1079
  const sessionId = input.session_id;
1034
1080
  if (!sessionId) {
1035
1081
  debug("session", "Stop missing session_id, skipping");
@@ -1043,6 +1089,15 @@ function handleStop(input, obsRepo, sessionRepo) {
1043
1089
  summaryLength: result.summary.length
1044
1090
  });
1045
1091
  else debug("session", "No observations to summarize", { sessionId });
1092
+ if (db && projectHash) try {
1093
+ const cleanup = runAutoCleanup(db, projectHash);
1094
+ if (!cleanup.skipped && (cleanup.observationsPurged > 0 || cleanup.orphanNodesRemoved > 0)) debug("session", "Auto-cleanup ran at session end", {
1095
+ observationsPurged: cleanup.observationsPurged,
1096
+ orphanNodesRemoved: cleanup.orphanNodesRemoved
1097
+ });
1098
+ } catch {
1099
+ debug("session", "Auto-cleanup failed (non-fatal)");
1100
+ }
1046
1101
  }
1047
1102
 
1048
1103
  //#endregion
@@ -1529,7 +1584,7 @@ function handlePreToolUse(input, db, projectHash, pathRepo) {
1529
1584
  const results = new SearchEngine(db, projectHash).searchKeyword(query, { limit: 3 });
1530
1585
  for (const result of results) {
1531
1586
  const snippet = result.snippet ? result.snippet.replace(/<\/?mark>/g, "") : truncate(result.observation.content, 120);
1532
- const age = formatAge(result.observation.created_at);
1587
+ const age = formatAge(result.observation.createdAt);
1533
1588
  lines.push(`- ${truncate(snippet, 120)} (${result.observation.source}, ${age})`);
1534
1589
  }
1535
1590
  } catch {
@@ -1587,6 +1642,226 @@ const DEFAULT_ROUTING_CONFIG = {
1587
1642
  patternWindowSize: 5
1588
1643
  };
1589
1644
 
1645
+ //#endregion
1646
+ //#region src/routing/proactive-suggestions.ts
1647
+ /**
1648
+ * Loads a lightweight snapshot of current session context.
1649
+ * Three small queries, each <3ms on a typical database.
1650
+ */
1651
+ function loadContextSnapshot(db, projectHash, sessionId) {
1652
+ let branch = null;
1653
+ try {
1654
+ const row = db.prepare(`
1655
+ SELECT arc_stage, branch_type, observation_count, tool_pattern
1656
+ FROM thought_branches
1657
+ WHERE project_hash = ? AND session_id = ? AND status = 'active'
1658
+ ORDER BY started_at DESC LIMIT 1
1659
+ `).get(projectHash, sessionId);
1660
+ if (row) {
1661
+ let toolPattern = {};
1662
+ try {
1663
+ toolPattern = JSON.parse(row.tool_pattern);
1664
+ } catch {}
1665
+ branch = {
1666
+ arcStage: row.arc_stage,
1667
+ branchType: row.branch_type,
1668
+ observationCount: row.observation_count,
1669
+ toolPattern
1670
+ };
1671
+ }
1672
+ } catch {}
1673
+ let debugPath = null;
1674
+ try {
1675
+ const pathRow = db.prepare(`
1676
+ SELECT dp.status,
1677
+ (SELECT COUNT(*) FROM path_waypoints pw WHERE pw.path_id = dp.id) AS waypoint_count,
1678
+ (SELECT COUNT(*) FROM path_waypoints pw WHERE pw.path_id = dp.id AND pw.waypoint_type = 'error') AS error_count
1679
+ FROM debug_paths dp
1680
+ WHERE dp.project_hash = ? AND dp.status = 'active'
1681
+ ORDER BY dp.started_at DESC LIMIT 1
1682
+ `).get(projectHash);
1683
+ if (pathRow) debugPath = {
1684
+ status: pathRow.status,
1685
+ waypointCount: pathRow.waypoint_count,
1686
+ errorCount: pathRow.error_count
1687
+ };
1688
+ } catch {}
1689
+ let recentClassifications = [];
1690
+ try {
1691
+ recentClassifications = db.prepare(`
1692
+ SELECT classification FROM observations
1693
+ WHERE project_hash = ? AND session_id = ? AND deleted_at IS NULL AND classification IS NOT NULL
1694
+ ORDER BY created_at DESC LIMIT 5
1695
+ `).all(projectHash, sessionId).map((r) => r.classification);
1696
+ } catch {}
1697
+ return {
1698
+ branch,
1699
+ debugPath,
1700
+ recentClassifications
1701
+ };
1702
+ }
1703
+ /**
1704
+ * Rules map context patterns to keyword categories, NOT tool names.
1705
+ * The engine then searches the tool registry for matching tools.
1706
+ */
1707
+ const CONTEXT_RULES = [
1708
+ {
1709
+ id: "debug-session",
1710
+ searchKeywords: [
1711
+ "debug",
1712
+ "error tracking",
1713
+ "issue investigation",
1714
+ "systematic debugging"
1715
+ ],
1716
+ confidence: .8,
1717
+ reason: "Diagnosis stage detected with problems but no active debug path",
1718
+ matches(ctx) {
1719
+ if (!ctx.branch) return false;
1720
+ const inDiagnosis = ctx.branch.arcStage === "diagnosis" || ctx.branch.arcStage === "investigation";
1721
+ const hasProblems = ctx.recentClassifications.some((c) => c === "problem" || c === "error");
1722
+ const noActivePath = !ctx.debugPath;
1723
+ return inDiagnosis && hasProblems && noActivePath;
1724
+ }
1725
+ },
1726
+ {
1727
+ id: "planning-needed",
1728
+ searchKeywords: [
1729
+ "plan",
1730
+ "design",
1731
+ "architecture",
1732
+ "implementation strategy"
1733
+ ],
1734
+ confidence: .7,
1735
+ reason: "Investigation phase with 5+ observations suggests planning would help",
1736
+ matches(ctx) {
1737
+ if (!ctx.branch) return false;
1738
+ const inInvestigation = ctx.branch.arcStage === "investigation";
1739
+ const enoughObservations = ctx.branch.observationCount >= 5;
1740
+ const readTools = (ctx.branch.toolPattern["Read"] ?? 0) + (ctx.branch.toolPattern["Grep"] ?? 0) + (ctx.branch.toolPattern["Glob"] ?? 0);
1741
+ const totalTools = Object.values(ctx.branch.toolPattern).reduce((a, b) => a + b, 0);
1742
+ const mostlyReads = totalTools > 0 && readTools / totalTools > .6;
1743
+ return inInvestigation && enoughObservations && mostlyReads;
1744
+ }
1745
+ },
1746
+ {
1747
+ id: "ready-to-commit",
1748
+ searchKeywords: [
1749
+ "commit",
1750
+ "save changes",
1751
+ "checkpoint"
1752
+ ],
1753
+ confidence: .75,
1754
+ reason: "Execution stage with recent resolutions — good time to commit",
1755
+ matches(ctx) {
1756
+ if (!ctx.branch) return false;
1757
+ const inExecution = ctx.branch.arcStage === "execution";
1758
+ const hasResolutions = ctx.recentClassifications.some((c) => c === "resolution" || c === "success");
1759
+ const recentSuccesses = ctx.recentClassifications.filter((c) => c === "success" || c === "resolution").length;
1760
+ return inExecution && hasResolutions && recentSuccesses >= 2;
1761
+ }
1762
+ },
1763
+ {
1764
+ id: "verify-work",
1765
+ searchKeywords: [
1766
+ "verify",
1767
+ "validate",
1768
+ "test",
1769
+ "acceptance",
1770
+ "UAT"
1771
+ ],
1772
+ confidence: .7,
1773
+ reason: "Feature branch in verification stage",
1774
+ matches(ctx) {
1775
+ if (!ctx.branch) return false;
1776
+ return ctx.branch.branchType === "feature" && ctx.branch.arcStage === "verification";
1777
+ }
1778
+ },
1779
+ {
1780
+ id: "resume-debugging",
1781
+ searchKeywords: [
1782
+ "debug",
1783
+ "continue debugging",
1784
+ "resume investigation"
1785
+ ],
1786
+ confidence: .75,
1787
+ reason: "Active debug path with multiple errors detected",
1788
+ matches(ctx) {
1789
+ if (!ctx.branch || !ctx.debugPath) return false;
1790
+ const inInvestigation = ctx.branch.arcStage === "investigation" || ctx.branch.arcStage === "diagnosis";
1791
+ return ctx.debugPath.status === "active" && inInvestigation && ctx.debugPath.errorCount >= 2;
1792
+ }
1793
+ },
1794
+ {
1795
+ id: "check-progress",
1796
+ searchKeywords: [
1797
+ "progress",
1798
+ "status",
1799
+ "milestone",
1800
+ "overview"
1801
+ ],
1802
+ confidence: .65,
1803
+ reason: "Extended execution — consider reviewing progress",
1804
+ matches(ctx) {
1805
+ if (!ctx.branch) return false;
1806
+ return ctx.branch.arcStage === "execution" && ctx.branch.observationCount >= 10;
1807
+ }
1808
+ }
1809
+ ];
1810
+ /**
1811
+ * Searches suggestable tools for the best match against a set of keywords.
1812
+ * Checks trigger_hints, description, and name for substring matches.
1813
+ *
1814
+ * This is a lightweight in-memory scan, not a DB query.
1815
+ */
1816
+ function findMatchingTool(keywords, suggestableTools) {
1817
+ let best = null;
1818
+ for (const tool of suggestableTools) {
1819
+ const searchText = [
1820
+ tool.trigger_hints ?? "",
1821
+ tool.description ?? "",
1822
+ tool.name
1823
+ ].join(" ").toLowerCase();
1824
+ let matchCount = 0;
1825
+ for (const keyword of keywords) if (searchText.includes(keyword.toLowerCase())) matchCount++;
1826
+ if (matchCount === 0) continue;
1827
+ const relevance = matchCount / keywords.length;
1828
+ if (!best || relevance > best.relevance) best = {
1829
+ tool,
1830
+ relevance
1831
+ };
1832
+ }
1833
+ return best;
1834
+ }
1835
+ /**
1836
+ * Evaluates proactive suggestions by matching context rules against available tools.
1837
+ *
1838
+ * Returns the highest-confidence match (rule confidence * tool relevance) that
1839
+ * exceeds the threshold, or null if nothing qualifies.
1840
+ */
1841
+ function evaluateProactiveSuggestions(ctx, suggestableTools, threshold) {
1842
+ let bestSuggestion = null;
1843
+ let bestScore = 0;
1844
+ for (const rule of CONTEXT_RULES) try {
1845
+ if (!rule.matches(ctx)) continue;
1846
+ const toolMatch = findMatchingTool(rule.searchKeywords, suggestableTools);
1847
+ if (!toolMatch) continue;
1848
+ const combinedScore = rule.confidence * toolMatch.relevance;
1849
+ if (combinedScore > bestScore && combinedScore >= threshold) {
1850
+ bestScore = combinedScore;
1851
+ bestSuggestion = {
1852
+ toolName: toolMatch.tool.name,
1853
+ toolDescription: toolMatch.tool.description,
1854
+ confidence: combinedScore,
1855
+ tier: "proactive",
1856
+ reason: rule.reason
1857
+ };
1858
+ }
1859
+ } catch (err) {
1860
+ debug("proactive", `Rule ${rule.id} failed`, { error: String(err) });
1861
+ }
1862
+ return bestSuggestion;
1863
+ }
1864
+
1590
1865
  //#endregion
1591
1866
  //#region src/routing/heuristic-fallback.ts
1592
1867
  /**
@@ -1721,7 +1996,8 @@ function evaluateHeuristic(recentObservations, suggestableTools, confidenceThres
1721
1996
  /**
1722
1997
  * ConversationRouter orchestrates tool suggestion routing.
1723
1998
  *
1724
- * Combines two tiers of suggestion:
1999
+ * Combines three tiers of suggestion:
2000
+ * - Proactive suggestions: context-aware trigger hint matching (ROUT-05)
1725
2001
  * - Learned patterns: historical tool sequence matching (ROUT-01)
1726
2002
  * - Heuristic fallback: keyword-based cold-start matching (ROUT-04)
1727
2003
  *
@@ -1807,14 +2083,21 @@ var ConversationRouter = class {
1807
2083
  if (suggestableTools.length === 0) return;
1808
2084
  const suggestableNames = new Set(suggestableTools.map((t) => t.name));
1809
2085
  let suggestion = null;
1810
- if (this.countRecentEvents() >= this.config.minEventsForLearned) suggestion = evaluateLearnedPatterns(this.db, sessionId, this.projectHash, suggestableNames, this.config.confidenceThreshold);
2086
+ suggestion = evaluateProactiveSuggestions(loadContextSnapshot(this.db, this.projectHash, sessionId), suggestableTools, this.config.confidenceThreshold);
2087
+ if (!suggestion) {
2088
+ if (this.countRecentEvents() >= this.config.minEventsForLearned) suggestion = evaluateLearnedPatterns(this.db, sessionId, this.projectHash, suggestableNames, this.config.confidenceThreshold);
2089
+ }
1811
2090
  if (!suggestion) suggestion = evaluateHeuristic(this.getRecentObservations(sessionId), suggestableTools, this.config.confidenceThreshold);
1812
2091
  if (!suggestion) return;
1813
2092
  if (suggestion.confidence < this.config.confidenceThreshold) return;
1814
2093
  const notifStore = new NotificationStore(this.db);
1815
- const description = suggestion.toolDescription ? ` -- ${suggestion.toolDescription}` : "";
1816
- const usageHint = suggestion.tier === "learned" ? ` (${suggestion.reason})` : "";
1817
- const message = `Tool suggestion: ${suggestion.toolName}${description}${usageHint}`;
2094
+ let message;
2095
+ if (suggestion.tier === "proactive") message = `[Laminark suggests] ${suggestion.reason} -- try ${suggestion.toolName}`;
2096
+ else {
2097
+ const description = suggestion.toolDescription ? ` -- ${suggestion.toolDescription}` : "";
2098
+ const usageHint = suggestion.tier === "learned" ? ` (${suggestion.reason})` : "";
2099
+ message = `Tool suggestion: ${suggestion.toolName}${description}${usageHint}`;
2100
+ }
1818
2101
  notifStore.add(this.projectHash, message);
1819
2102
  debug("routing", "Suggestion delivered", {
1820
2103
  tool: suggestion.toolName,
@@ -1951,7 +2234,8 @@ function processPostToolUseFiltered(input, obsRepo, researchBuffer, toolRegistry
1951
2234
  source: "hook:PostToolUse",
1952
2235
  projectHash: projectHash ?? null,
1953
2236
  description: null,
1954
- serverName: extractServerName(toolName)
2237
+ serverName: extractServerName(toolName),
2238
+ triggerHints: null
1955
2239
  }, sessionId ?? null, !isFailure);
1956
2240
  if (isFailure) {
1957
2241
  const failures = toolRegistry.getRecentEventsForTool(toolName, projectHash ?? "", 5).filter((e) => e.success === 0).length;
@@ -2087,6 +2371,10 @@ async function main() {
2087
2371
  initPathSchema(laminarkDb.db);
2088
2372
  pathRepo = new PathRepository(laminarkDb.db, projectHash);
2089
2373
  } catch {}
2374
+ let branchRepo;
2375
+ try {
2376
+ branchRepo = new BranchRepository(laminarkDb.db, projectHash);
2377
+ } catch {}
2090
2378
  switch (eventName) {
2091
2379
  case "PreToolUse": {
2092
2380
  const preContext = handlePreToolUse(input, laminarkDb.db, projectHash, pathRepo);
@@ -2098,7 +2386,7 @@ async function main() {
2098
2386
  processPostToolUseFiltered(input, obsRepo, researchBuffer, toolRegistry, projectHash, laminarkDb.db);
2099
2387
  break;
2100
2388
  case "SessionStart": {
2101
- const context = handleSessionStart(input, sessionRepo, laminarkDb.db, projectHash, toolRegistry, pathRepo);
2389
+ const context = handleSessionStart(input, sessionRepo, laminarkDb.db, projectHash, toolRegistry, pathRepo, branchRepo);
2102
2390
  if (context) process.stdout.write(context);
2103
2391
  break;
2104
2392
  }
@@ -2106,7 +2394,7 @@ async function main() {
2106
2394
  handleSessionEnd(input, sessionRepo);
2107
2395
  break;
2108
2396
  case "Stop":
2109
- handleStop(input, obsRepo, sessionRepo);
2397
+ handleStop(input, obsRepo, sessionRepo, laminarkDb.db, projectHash);
2110
2398
  break;
2111
2399
  default:
2112
2400
  debug("hook", "Unknown hook event", { eventName });
@@ -2118,6 +2406,7 @@ async function main() {
2118
2406
  }
2119
2407
  main().catch((err) => {
2120
2408
  debug("hook", "Hook handler error", { error: err.message });
2409
+ process.exit(0);
2121
2410
  });
2122
2411
 
2123
2412
  //#endregion