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.
- package/README.md +36 -71
- package/package.json +9 -7
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +10 -0
- package/plugin/commands/recall.md +55 -0
- package/plugin/commands/remember.md +34 -0
- package/plugin/commands/resume.md +45 -0
- package/plugin/commands/stash.md +34 -0
- package/plugin/commands/status.md +33 -0
- package/plugin/dist/hooks/handler.d.ts +3 -1
- package/plugin/dist/hooks/handler.d.ts.map +1 -1
- package/plugin/dist/hooks/handler.js +312 -23
- package/plugin/dist/hooks/handler.js.map +1 -1
- package/plugin/dist/index.d.ts +3 -1
- package/plugin/dist/index.d.ts.map +1 -1
- package/plugin/dist/index.js +2111 -525
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/{observations-Ch0nc47i.d.mts → observations-CorAAc1A.d.mts} +23 -1
- package/plugin/dist/observations-CorAAc1A.d.mts.map +1 -0
- package/plugin/dist/{tool-registry-CZ3mJ4iR.mjs → tool-registry-D8un_AcG.mjs} +932 -13
- package/plugin/dist/tool-registry-D8un_AcG.mjs.map +1 -0
- package/plugin/hooks/hooks.json +6 -6
- package/plugin/laminark.db +0 -0
- package/plugin/package.json +17 -0
- package/plugin/scripts/README.md +19 -1
- package/plugin/scripts/bump-version.sh +24 -19
- package/plugin/scripts/dev-sync.sh +58 -0
- package/plugin/scripts/ensure-deps.sh +5 -2
- package/plugin/scripts/install.sh +115 -39
- package/plugin/scripts/local-install.sh +93 -58
- package/plugin/scripts/uninstall.sh +76 -38
- package/plugin/scripts/update.sh +20 -69
- package/plugin/scripts/verify-install.sh +69 -25
- package/plugin/ui/activity.js +12 -0
- package/plugin/ui/app.js +24 -54
- package/plugin/ui/graph.js +413 -186
- package/plugin/ui/help/activity-feed.png +0 -0
- package/plugin/ui/help/analysis-panel.png +0 -0
- package/plugin/ui/help/graph-toolbar.png +0 -0
- package/plugin/ui/help/graph-view.png +0 -0
- package/plugin/ui/help/settings.png +0 -0
- package/plugin/ui/help/timeline.png +0 -0
- package/plugin/ui/help.js +876 -172
- package/plugin/ui/index.html +506 -242
- package/plugin/ui/settings.js +781 -17
- package/plugin/ui/styles.css +990 -44
- package/plugin/ui/timeline.js +2 -2
- package/plugin/ui/tools.js +826 -0
- package/.claude-plugin/marketplace.json +0 -15
- package/plugin/dist/observations-Ch0nc47i.d.mts.map +0 -1
- package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +0 -1
- 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 {
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|