laminark 0.1.0 → 2.21.7

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/.claude-plugin/marketplace.json +15 -0
  2. package/README.md +71 -36
  3. package/package.json +7 -9
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/dist/hooks/handler.d.ts +1 -3
  6. package/plugin/dist/hooks/handler.d.ts.map +1 -1
  7. package/plugin/dist/hooks/handler.js +22 -310
  8. package/plugin/dist/hooks/handler.js.map +1 -1
  9. package/plugin/dist/index.d.ts +1 -3
  10. package/plugin/dist/index.d.ts.map +1 -1
  11. package/plugin/dist/index.js +392 -1895
  12. package/plugin/dist/index.js.map +1 -1
  13. package/plugin/dist/{observations-CorAAc1A.d.mts → observations-Ch0nc47i.d.mts} +1 -23
  14. package/plugin/dist/observations-Ch0nc47i.d.mts.map +1 -0
  15. package/plugin/dist/{tool-registry-e710BvXq.mjs → tool-registry-CZ3mJ4iR.mjs} +13 -932
  16. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +1 -0
  17. package/plugin/hooks/hooks.json +6 -6
  18. package/plugin/scripts/README.md +1 -19
  19. package/plugin/scripts/bump-version.sh +3 -1
  20. package/plugin/scripts/ensure-deps.sh +2 -5
  21. package/plugin/scripts/install.sh +39 -115
  22. package/plugin/scripts/local-install.sh +58 -93
  23. package/plugin/scripts/setup-tmpdir.sh +65 -0
  24. package/plugin/scripts/uninstall.sh +38 -76
  25. package/plugin/scripts/update.sh +69 -20
  26. package/plugin/scripts/verify-install.sh +25 -69
  27. package/plugin/ui/activity.js +0 -12
  28. package/plugin/ui/app.js +54 -24
  29. package/plugin/ui/graph.js +186 -413
  30. package/plugin/ui/help.js +172 -876
  31. package/plugin/ui/index.html +242 -506
  32. package/plugin/ui/settings.js +17 -781
  33. package/plugin/ui/styles.css +44 -990
  34. package/plugin/ui/timeline.js +2 -2
  35. package/plugin/CLAUDE.md +0 -10
  36. package/plugin/commands/recall.md +0 -55
  37. package/plugin/commands/remember.md +0 -34
  38. package/plugin/commands/resume.md +0 -45
  39. package/plugin/commands/stash.md +0 -34
  40. package/plugin/commands/status.md +0 -33
  41. package/plugin/dist/observations-CorAAc1A.d.mts.map +0 -1
  42. package/plugin/dist/tool-registry-e710BvXq.mjs.map +0 -1
  43. package/plugin/laminark.db +0 -0
  44. package/plugin/package.json +0 -17
  45. package/plugin/scripts/dev-sync.sh +0 -58
  46. package/plugin/ui/help/activity-feed.png +0 -0
  47. package/plugin/ui/help/analysis-panel.png +0 -0
  48. package/plugin/ui/help/graph-toolbar.png +0 -0
  49. package/plugin/ui/help/graph-view.png +0 -0
  50. package/plugin/ui/help/settings.png +0 -0
  51. package/plugin/ui/help/timeline.png +0 -0
  52. package/plugin/ui/tools.js +0 -826
@@ -1,18 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as isDebugEnabled, i as getProjectHash, n as getDatabaseConfig, r as getDbPath, t as getConfigDir } from "./config-t8LZeB-u.mjs";
3
- import { A as hybridSearch, C as getNodesByType, D as upsertNode, E as traverseFrom, F as openDatabase, I as MIGRATIONS, L as runMigrations, M as SessionRepository, N as ObservationRepository, O as SaveGuard, R as debug, S as getNodeByNameAndType, T as insertEdge, _ as detectStaleness, a as ResearchBufferRepository, b as countEdgesForNode, c as inferScope, d as executePurge, f as findAnalysis, g as saveHygieneConfig, h as resetHygieneConfig, i as NotificationStore, j as SearchEngine, k as jaccardSimilarity$1, l as inferToolType, m as loadHygieneConfig, n as PathRepository, o as BranchRepository, r as initPathSchema, s as extractServerName, t as ToolRegistryRepository, u as analyzeObservations, v as flagStaleObservation, w as initGraphSchema, x as getEdgesForNode, y as initStalenessSchema, z as debugTimed } from "./tool-registry-e710BvXq.mjs";
4
- import { existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
- import { basename, dirname, join } from "node:path";
3
+ import { C as rowToObservation, D as debug, E as runMigrations, O as debugTimed, S as ObservationRepository, T as MIGRATIONS, _ as SaveGuard, a as ResearchBufferRepository, b as SearchEngine, c as inferToolType, d as getNodeByNameAndType, f as getNodesByType, g as upsertNode, h as traverseFrom, i as NotificationStore, l as countEdgesForNode, m as insertEdge, n as PathRepository, o as extractServerName, p as initGraphSchema, r as initPathSchema, s as inferScope, t as ToolRegistryRepository, u as getEdgesForNode, v as jaccardSimilarity$1, w as openDatabase, x as SessionRepository, y as hybridSearch } from "./tool-registry-CZ3mJ4iR.mjs";
4
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { z } from "zod";
8
8
  import path from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import { readFile, readdir } from "node:fs/promises";
13
- import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
14
12
  import { Worker } from "node:worker_threads";
15
13
  import { fileURLToPath as fileURLToPath$1 } from "node:url";
14
+ import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
16
15
  import { Hono } from "hono";
17
16
  import fs from "fs";
18
17
  import { cors } from "hono/cors";
@@ -372,44 +371,6 @@ async function startServer(server) {
372
371
  debug("mcp", "MCP server started on stdio transport");
373
372
  }
374
373
 
375
- //#endregion
376
- //#region src/config/cross-access.ts
377
- /**
378
- * Cross-Project Access Configuration
379
- *
380
- * Per-project config that controls which other projects' memories
381
- * the current project can read from. Read-only access — no writes
382
- * cross projects.
383
- *
384
- * Config stored at: {configDir}/cross-access-{projectHash}.json
385
- */
386
- const DEFAULTS$4 = { readableProjects: [] };
387
- function getConfigPath(projectHash) {
388
- return join(getConfigDir(), `cross-access-${projectHash}.json`);
389
- }
390
- function loadCrossAccessConfig(projectHash) {
391
- const configPath = getConfigPath(projectHash);
392
- try {
393
- if (!existsSync(configPath)) return { ...DEFAULTS$4 };
394
- const raw = readFileSync(configPath, "utf-8");
395
- const parsed = JSON.parse(raw);
396
- return { readableProjects: Array.isArray(parsed.readableProjects) ? parsed.readableProjects.filter((h) => typeof h === "string") : [] };
397
- } catch {
398
- return { ...DEFAULTS$4 };
399
- }
400
- }
401
- function saveCrossAccessConfig(projectHash, config) {
402
- const configPath = getConfigPath(projectHash);
403
- const validated = { readableProjects: Array.isArray(config.readableProjects) ? config.readableProjects.filter((h) => typeof h === "string" && h !== projectHash) : [] };
404
- writeFileSync(configPath, JSON.stringify(validated, null, 2), "utf-8");
405
- }
406
- function resetCrossAccessConfig(projectHash) {
407
- const configPath = getConfigPath(projectHash);
408
- try {
409
- if (existsSync(configPath)) unlinkSync(configPath);
410
- } catch {}
411
- }
412
-
413
374
  //#endregion
414
375
  //#region src/mcp/token-budget.ts
415
376
  const TOKEN_BUDGET = 2e3;
@@ -438,82 +399,6 @@ function enforceTokenBudget(results, formatResult, budget = TOKEN_BUDGET) {
438
399
  };
439
400
  }
440
401
 
441
- //#endregion
442
- //#region src/config/tool-verbosity-config.ts
443
- /**
444
- * Tool Response Verbosity Configuration
445
- *
446
- * Controls how much detail MCP tool responses include.
447
- * Three levels:
448
- * 1 (minimal): Just confirms the tool ran
449
- * 2 (standard): Shows title/key info (default)
450
- * 3 (verbose): Full formatted text with all details
451
- *
452
- * Configuration is loaded from .laminark/tool-verbosity.json with
453
- * a 5-second cache to avoid repeated disk reads.
454
- */
455
- const DEFAULTS$3 = { level: 2 };
456
- const CACHE_TTL_MS = 5e3;
457
- let cachedConfig = null;
458
- let cachedAt = 0;
459
- /**
460
- * Loads tool verbosity configuration from disk with a 5-second cache.
461
- */
462
- function loadToolVerbosityConfig() {
463
- const now = Date.now();
464
- if (cachedConfig && now - cachedAt < CACHE_TTL_MS) return cachedConfig;
465
- const configPath = join(getConfigDir(), "tool-verbosity.json");
466
- try {
467
- const content = readFileSync(configPath, "utf-8");
468
- const level = JSON.parse(content).level;
469
- if (level === 1 || level === 2 || level === 3) cachedConfig = { level };
470
- else cachedConfig = { ...DEFAULTS$3 };
471
- debug("config", "Loaded tool verbosity config", { level: cachedConfig.level });
472
- } catch {
473
- cachedConfig = { ...DEFAULTS$3 };
474
- }
475
- cachedAt = now;
476
- return cachedConfig;
477
- }
478
- /**
479
- * Saves tool verbosity configuration to disk and invalidates cache.
480
- */
481
- function saveToolVerbosityConfig(config) {
482
- writeFileSync(join(getConfigDir(), "tool-verbosity.json"), JSON.stringify(config, null, 2), "utf-8");
483
- cachedConfig = config;
484
- cachedAt = Date.now();
485
- }
486
- /**
487
- * Resets tool verbosity to defaults by invalidating cache.
488
- */
489
- function resetToolVerbosityConfig() {
490
- cachedConfig = null;
491
- cachedAt = 0;
492
- return { ...DEFAULTS$3 };
493
- }
494
- /**
495
- * Selects the appropriate response text based on the current verbosity level.
496
- *
497
- * Each tool passes three pre-built strings:
498
- * - minimal: Level 1 — just confirms the tool ran
499
- * - standard: Level 2 — shows title/key info
500
- * - verbose: Level 3 — full formatted text
501
- */
502
- function formatResponse(level, minimal, standard, verbose) {
503
- switch (level) {
504
- case 1: return minimal;
505
- case 2: return standard;
506
- case 3: return verbose;
507
- }
508
- }
509
- /**
510
- * Convenience: loads config and selects the response in one call.
511
- */
512
- function verboseResponse(minimal, standard, verbose) {
513
- const { level } = loadToolVerbosityConfig();
514
- return formatResponse(level, minimal, standard, verbose);
515
- }
516
-
517
402
  //#endregion
518
403
  //#region src/mcp/tools/recall.ts
519
404
  function shortId(id) {
@@ -545,19 +430,19 @@ function formatTimelineGroup(date, items) {
545
430
  function formatFullItem(obs) {
546
431
  return `--- ${shortId(obs.id)} | ${obs.title ?? "untitled"} | ${obs.createdAt} ---\n${obs.content}`;
547
432
  }
548
- function prependNotifications$8(notificationStore, projectHash, responseText) {
433
+ function prependNotifications$6(notificationStore, projectHash, responseText) {
549
434
  if (!notificationStore) return responseText;
550
435
  const pending = notificationStore.consumePending(projectHash);
551
436
  if (pending.length === 0) return responseText;
552
437
  return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
553
438
  }
554
- function textResponse$9(text) {
439
+ function textResponse$7(text) {
555
440
  return { content: [{
556
441
  type: "text",
557
442
  text
558
443
  }] };
559
444
  }
560
- function errorResponse$4(text) {
445
+ function errorResponse$3(text) {
561
446
  return {
562
447
  content: [{
563
448
  type: "text",
@@ -566,15 +451,7 @@ function errorResponse$4(text) {
566
451
  isError: true
567
452
  };
568
453
  }
569
- function getProjectNameMap(db) {
570
- const map = /* @__PURE__ */ new Map();
571
- try {
572
- const rows = db.prepare("SELECT project_hash, display_name FROM project_metadata").all();
573
- for (const row of rows) map.set(row.project_hash, row.display_name ?? row.project_hash.slice(0, 8));
574
- } catch {}
575
- return map;
576
- }
577
- function registerRecall(server, db, projectHashRef, worker = null, embeddingStore = null, notificationStore = null, statusCache = null) {
454
+ function registerRecall(server, db, projectHash, worker = null, embeddingStore = null, notificationStore = null, statusCache = null) {
578
455
  server.registerTool("recall", {
579
456
  title: "Recall Memories",
580
457
  description: "Search, view, purge, or restore memories. Search first to find matches, then act on specific results by ID.",
@@ -604,14 +481,13 @@ function registerRecall(server, db, projectHashRef, worker = null, embeddingStor
604
481
  include_purged: z.boolean().default(false).describe("Include soft-deleted items in results (needed for restore)")
605
482
  }
606
483
  }, async (args) => {
607
- const projectHash = projectHashRef.current;
608
- const withNotifications = (text) => textResponse$9(prependNotifications$8(notificationStore, projectHash, text));
484
+ const withNotifications = (text) => textResponse$7(prependNotifications$6(notificationStore, projectHash, text));
609
485
  try {
610
486
  const repo = new ObservationRepository(db, projectHash);
611
487
  const searchEngine = new SearchEngine(db, projectHash);
612
488
  const hasSearch = args.query !== void 0 || args.id !== void 0 || args.title !== void 0;
613
- if (args.ids && hasSearch) return errorResponse$4("Provide either a search query or IDs to act on, not both.");
614
- if ((args.action === "purge" || args.action === "restore") && !args.ids && !args.id) return errorResponse$4(`Provide ids array or id to specify which memories to ${args.action}.`);
489
+ if (args.ids && hasSearch) return errorResponse$3("Provide either a search query or IDs to act on, not both.");
490
+ if ((args.action === "purge" || args.action === "restore") && !args.ids && !args.id) return errorResponse$3(`Provide ids array or id to specify which memories to ${args.action}.`);
615
491
  let observations = [];
616
492
  let searchResults = null;
617
493
  if (args.ids) {
@@ -638,30 +514,6 @@ function registerRecall(server, db, projectHashRef, worker = null, embeddingStor
638
514
  });
639
515
  else searchResults = searchEngine.searchKeyword(args.query, { limit: args.limit });
640
516
  observations = searchResults.map((r) => r.observation);
641
- const crossConfig = loadCrossAccessConfig(projectHash);
642
- if (crossConfig.readableProjects.length > 0) {
643
- const nameMap = getProjectNameMap(db);
644
- for (const otherHash of crossConfig.readableProjects) {
645
- const otherEngine = new SearchEngine(db, otherHash);
646
- let otherResults;
647
- if (embeddingStore) otherResults = await hybridSearch({
648
- searchEngine: otherEngine,
649
- embeddingStore,
650
- worker,
651
- query: args.query,
652
- db,
653
- projectHash: otherHash,
654
- options: { limit: args.limit }
655
- });
656
- else otherResults = otherEngine.searchKeyword(args.query, { limit: args.limit });
657
- if (otherResults.length > 0) {
658
- const projName = nameMap.get(otherHash) ?? otherHash.slice(0, 8);
659
- for (const r of otherResults) r.observation.title = `[${projName}] ${r.observation.title ?? "untitled"}`;
660
- searchResults.push(...otherResults);
661
- observations.push(...otherResults.map((r) => r.observation));
662
- }
663
- }
664
- }
665
517
  } else if (args.title) observations = repo.getByTitle(args.title, {
666
518
  limit: args.limit,
667
519
  includePurged: args.include_purged
@@ -673,21 +525,8 @@ function registerRecall(server, db, projectHashRef, worker = null, embeddingStor
673
525
  if (args.kind && observations.length > 0) observations = observations.filter((obs) => obs.kind === args.kind);
674
526
  if (observations.length === 0) return withNotifications(`No memories found matching '${args.query ?? args.title ?? args.id ?? ""}'. Try broader search terms or check the ID.`);
675
527
  if (args.action === "view") {
676
- const verbosity = loadToolVerbosityConfig().level;
677
- if (verbosity === 1) {
678
- const searchTerm = args.query ?? args.title ?? "query";
679
- return textResponse$9(prependNotifications$8(notificationStore, projectHash, `Found ${observations.length} memories matching "${searchTerm}"`));
680
- }
681
- if (verbosity === 2) {
682
- const lines = observations.map((obs, i) => {
683
- const title = obs.title ?? "untitled";
684
- return `${i + 1}. ${title}`;
685
- });
686
- const footer = `\n---\n${observations.length} result(s)`;
687
- return textResponse$9(prependNotifications$8(notificationStore, projectHash, lines.join("\n") + footer));
688
- }
689
528
  const originalText = formatViewResponse(observations, searchResults, args.detail, args.id !== void 0).content[0].text;
690
- return textResponse$9(prependNotifications$8(notificationStore, projectHash, originalText));
529
+ return textResponse$7(prependNotifications$6(notificationStore, projectHash, originalText));
691
530
  }
692
531
  if (args.action === "purge") {
693
532
  const targetIds = args.ids ?? (args.id ? [args.id] : []);
@@ -719,11 +558,11 @@ function registerRecall(server, db, projectHashRef, worker = null, embeddingStor
719
558
  if (failures.length > 0) msg += ` Not found: ${failures.join(", ")}`;
720
559
  return withNotifications(msg);
721
560
  }
722
- return errorResponse$4(`Unknown action: ${args.action}`);
561
+ return errorResponse$3(`Unknown action: ${args.action}`);
723
562
  } catch (err) {
724
563
  const message = err instanceof Error ? err.message : "Unknown error";
725
564
  debug("mcp", "recall: error", { error: message });
726
- return errorResponse$4(`Recall error: ${message}`);
565
+ return errorResponse$3(`Recall error: ${message}`);
727
566
  }
728
567
  });
729
568
  }
@@ -783,7 +622,7 @@ function formatViewResponse(observations, searchResults, detail, isSingleIdLooku
783
622
  }
784
623
  let footer = `---\n${observations.length} result(s) | ~${tokenEstimate} tokens | detail: ${detail}`;
785
624
  if (truncated) footer += " | truncated (use id for full view)";
786
- return textResponse$9(`${body}\n${footer}`);
625
+ return textResponse$7(`${body}\n${footer}`);
787
626
  }
788
627
  function buildScoreMap(searchResults) {
789
628
  const map = /* @__PURE__ */ new Map();
@@ -809,7 +648,7 @@ function generateTitle(content) {
809
648
  * save_memory persists user-provided text as a new observation with an optional title.
810
649
  * If title is omitted, one is auto-generated from the text content.
811
650
  */
812
- function registerSaveMemory(server, db, projectHashRef, notificationStore = null, worker = null, embeddingStore = null, statusCache = null) {
651
+ function registerSaveMemory(server, db, projectHash, notificationStore = null, worker = null, embeddingStore = null, statusCache = null) {
813
652
  server.registerTool("save_memory", {
814
653
  title: "Save Memory",
815
654
  description: "Save a new memory observation. Provide text content and an optional title. If title is omitted, one is auto-generated from the text.",
@@ -826,7 +665,6 @@ function registerSaveMemory(server, db, projectHashRef, notificationStore = null
826
665
  ]).default("finding").describe("Observation kind: change, reference, finding, decision, or verification")
827
666
  }
828
667
  }, async (args) => {
829
- const projectHash = projectHashRef.current;
830
668
  try {
831
669
  const repo = new ObservationRepository(db, projectHash);
832
670
  const decision = await new SaveGuard(repo, {
@@ -855,7 +693,7 @@ function registerSaveMemory(server, db, projectHashRef, notificationStore = null
855
693
  title: resolvedTitle
856
694
  });
857
695
  statusCache?.markDirty();
858
- let responseText = verboseResponse("Memory saved.", `Saved "${resolvedTitle}"`, `Saved memory "${resolvedTitle}" (id: ${obs.id})`);
696
+ let responseText = `Saved memory "${resolvedTitle}" (id: ${obs.id})`;
859
697
  if (notificationStore) {
860
698
  const pending = notificationStore.consumePending(projectHash);
861
699
  if (pending.length > 0) responseText = pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
@@ -876,248 +714,6 @@ function registerSaveMemory(server, db, projectHashRef, notificationStore = null
876
714
  });
877
715
  }
878
716
 
879
- //#endregion
880
- //#region src/ingestion/markdown-parser.ts
881
- /**
882
- * Parse a markdown file into discrete sections split on ## headings.
883
- *
884
- * - The # (level 1) heading is the doc title, used as prefix: "DocTitle > SectionHeading"
885
- * - ### subsections stay within their parent ## section (not split separately)
886
- * - Sections with empty content after trimming are skipped
887
- * - Content before the first ## heading is skipped
888
- * - ## inside fenced code blocks are not treated as headings
889
- */
890
- function parseMarkdownSections(fileContent, sourceFile) {
891
- const lines = fileContent.split("\n");
892
- const sections = [];
893
- let docTitle = "";
894
- let currentHeading = "";
895
- let currentLines = [];
896
- let sectionIndex = 0;
897
- let inCodeBlock = false;
898
- for (const line of lines) {
899
- if (line.trimStart().startsWith("```")) inCodeBlock = !inCodeBlock;
900
- if (inCodeBlock) {
901
- if (currentHeading) currentLines.push(line);
902
- continue;
903
- }
904
- if (/^# (?!#)/.test(line)) {
905
- docTitle = line.slice(2).trim();
906
- continue;
907
- }
908
- if (/^## (?!#)/.test(line)) {
909
- if (currentHeading) {
910
- const content = currentLines.join("\n").trim();
911
- if (content.length > 0) {
912
- sections.push({
913
- title: docTitle ? `${docTitle} > ${currentHeading}` : currentHeading,
914
- heading: currentHeading,
915
- content,
916
- sourceFile,
917
- sectionIndex
918
- });
919
- sectionIndex++;
920
- }
921
- }
922
- currentHeading = line.slice(3).trim();
923
- currentLines = [];
924
- continue;
925
- }
926
- if (currentHeading) currentLines.push(line);
927
- }
928
- if (currentHeading) {
929
- const content = currentLines.join("\n").trim();
930
- if (content.length > 0) sections.push({
931
- title: docTitle ? `${docTitle} > ${currentHeading}` : currentHeading,
932
- heading: currentHeading,
933
- content,
934
- sourceFile,
935
- sectionIndex
936
- });
937
- }
938
- return sections;
939
- }
940
-
941
- //#endregion
942
- //#region src/ingestion/knowledge-ingester.ts
943
- /**
944
- * Ingests markdown files into the knowledge store.
945
- *
946
- * Creates one observation per ## section, with idempotent re-ingestion
947
- * that cleans up stale sections without duplication.
948
- */
949
- var KnowledgeIngester = class {
950
- db;
951
- projectHash;
952
- constructor(db, projectHash) {
953
- this.db = db;
954
- this.projectHash = projectHash;
955
- }
956
- /**
957
- * Detects the knowledge directory for a project.
958
- * Checks in order:
959
- * 1. {projectRoot}/.planning/codebase/ (GSD output)
960
- * 2. {projectRoot}/.laminark/codebase/
961
- * Returns the first existing directory, or null if none exist.
962
- */
963
- static detectKnowledgeDir(projectRoot) {
964
- const candidates = [join(projectRoot, ".planning", "codebase"), join(projectRoot, ".laminark", "codebase")];
965
- for (const candidate of candidates) try {
966
- if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
967
- } catch {}
968
- return null;
969
- }
970
- /**
971
- * Ingests all markdown files from a directory.
972
- * Reads all files async first, then runs DB operations in a single transaction.
973
- */
974
- async ingestDirectory(dirPath) {
975
- let files;
976
- try {
977
- files = await readdir(dirPath);
978
- } catch {
979
- return {
980
- filesProcessed: 0,
981
- sectionsCreated: 0,
982
- sectionsRemoved: 0
983
- };
984
- }
985
- const mdFiles = files.filter((f) => f.endsWith(".md"));
986
- const fileContents = /* @__PURE__ */ new Map();
987
- for (const file of mdFiles) {
988
- const filePath = join(dirPath, file);
989
- try {
990
- const content = await readFile(filePath, "utf-8");
991
- fileContents.set(file, content);
992
- } catch {}
993
- }
994
- let totalCreated = 0;
995
- let totalRemoved = 0;
996
- for (const [filename, content] of fileContents) {
997
- const stats = this.ingestFileSync(filename, content);
998
- totalCreated += stats.sectionsCreated;
999
- totalRemoved += stats.sectionsRemoved;
1000
- }
1001
- return {
1002
- filesProcessed: fileContents.size,
1003
- sectionsCreated: totalCreated,
1004
- sectionsRemoved: totalRemoved
1005
- };
1006
- }
1007
- /**
1008
- * Ingests a single markdown file.
1009
- * Wraps async file reading with sync ingestion.
1010
- */
1011
- async ingestFile(filePath) {
1012
- try {
1013
- const content = await readFile(filePath, "utf-8");
1014
- const filename = basename(filePath);
1015
- return this.ingestFileSync(filename, content);
1016
- } catch {
1017
- return {
1018
- filesProcessed: 0,
1019
- sectionsCreated: 0,
1020
- sectionsRemoved: 0
1021
- };
1022
- }
1023
- }
1024
- /**
1025
- * Internal sync ingestion method (runs within transaction).
1026
- * Implements idempotent upsert via soft-delete + recreate.
1027
- */
1028
- ingestFileSync(filename, fileContent) {
1029
- const sourceTag = `ingest:${filename}`;
1030
- const sections = parseMarkdownSections(fileContent, filename);
1031
- return this.db.transaction(() => {
1032
- const repo = new ObservationRepository(this.db, this.projectHash);
1033
- const sectionsRemoved = this.db.prepare(`UPDATE observations
1034
- SET deleted_at = datetime('now'), updated_at = datetime('now')
1035
- WHERE project_hash = ? AND source = ? AND deleted_at IS NULL`).run(this.projectHash, sourceTag).changes;
1036
- let sectionsCreated = 0;
1037
- for (const section of sections) {
1038
- repo.createClassified({
1039
- content: section.content,
1040
- title: section.title,
1041
- source: sourceTag,
1042
- kind: "reference",
1043
- sessionId: null
1044
- }, "discovery");
1045
- sectionsCreated++;
1046
- }
1047
- return {
1048
- filesProcessed: 1,
1049
- sectionsCreated,
1050
- sectionsRemoved
1051
- };
1052
- })();
1053
- }
1054
- };
1055
-
1056
- //#endregion
1057
- //#region src/mcp/tools/ingest-knowledge.ts
1058
- /**
1059
- * Registers the ingest_knowledge tool on the MCP server.
1060
- *
1061
- * ingest_knowledge transforms structured markdown documents into per-project
1062
- * reference observations. Supports optional directory path; auto-detects
1063
- * .planning/codebase/ or .laminark/codebase/ from project metadata when
1064
- * directory is omitted.
1065
- */
1066
- function registerIngestKnowledge(server, db, projectHashRef, notificationStore = null, statusCache = null) {
1067
- server.registerTool("ingest_knowledge", {
1068
- title: "Ingest Knowledge",
1069
- description: "Ingest structured markdown documents from a directory into queryable per-project memories. Reads .md files, splits by ## headings, and stores each section as a reference observation. Supports .planning/codebase/ (GSD output) and .laminark/codebase/.",
1070
- inputSchema: { directory: z.string().optional().describe("Directory containing .md files to ingest. If omitted, auto-detects .planning/codebase/ or .laminark/codebase/ using the project path from project_metadata.") }
1071
- }, async (args) => {
1072
- const projectHash = projectHashRef.current;
1073
- try {
1074
- let resolvedDir = args.directory;
1075
- if (!resolvedDir) {
1076
- const row = db.prepare("SELECT project_path FROM project_metadata WHERE project_hash = ? ORDER BY last_seen_at DESC LIMIT 1").get(projectHash);
1077
- if (!row) return {
1078
- content: [{
1079
- type: "text",
1080
- text: "Could not determine project path. Please provide the directory parameter explicitly."
1081
- }],
1082
- isError: true
1083
- };
1084
- const detected = KnowledgeIngester.detectKnowledgeDir(row.project_path);
1085
- if (!detected) return {
1086
- content: [{
1087
- type: "text",
1088
- text: "No knowledge directory found. Expected .planning/codebase/ or .laminark/codebase/ in the project root. Run /gsd:map-codebase first or provide a directory path."
1089
- }],
1090
- isError: true
1091
- };
1092
- resolvedDir = detected;
1093
- }
1094
- const stats = await new KnowledgeIngester(db, projectHash).ingestDirectory(resolvedDir);
1095
- debug("mcp", "ingest_knowledge: completed", {
1096
- directory: resolvedDir,
1097
- stats
1098
- });
1099
- statusCache?.markDirty();
1100
- let finalResponse = verboseResponse(`Ingested ${stats.filesProcessed} files: ${stats.sectionsCreated} sections created, ${stats.sectionsRemoved} stale sections removed.`, `Ingested ${stats.filesProcessed} file(s): ${stats.sectionsCreated} sections created, ${stats.sectionsRemoved} removed.`, `Ingested ${stats.filesProcessed} file(s) from ${resolvedDir}: ${stats.sectionsCreated} sections created, ${stats.sectionsRemoved} stale sections removed.`);
1101
- if (notificationStore) {
1102
- const pending = notificationStore.consumePending(projectHash);
1103
- if (pending.length > 0) finalResponse = pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + finalResponse;
1104
- }
1105
- return { content: [{
1106
- type: "text",
1107
- text: finalResponse
1108
- }] };
1109
- } catch (err) {
1110
- return {
1111
- content: [{
1112
- type: "text",
1113
- text: `Failed to ingest knowledge: ${err instanceof Error ? err.message : "Unknown error"}`
1114
- }],
1115
- isError: true
1116
- };
1117
- }
1118
- });
1119
- }
1120
-
1121
717
  //#endregion
1122
718
  //#region src/commands/resume.ts
1123
719
  /**
@@ -1189,13 +785,13 @@ function formatStashes(stashes) {
1189
785
  if (stashes.length <= 8) return formatDetail(stashes);
1190
786
  return formatCompact(stashes);
1191
787
  }
1192
- function prependNotifications$7(notificationStore, projectHash, responseText) {
788
+ function prependNotifications$5(notificationStore, projectHash, responseText) {
1193
789
  if (!notificationStore) return responseText;
1194
790
  const pending = notificationStore.consumePending(projectHash);
1195
791
  if (pending.length === 0) return responseText;
1196
792
  return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
1197
793
  }
1198
- function textResponse$8(text) {
794
+ function textResponse$6(text) {
1199
795
  return { content: [{
1200
796
  type: "text",
1201
797
  text
@@ -1207,7 +803,7 @@ function textResponse$8(text) {
1207
803
  * Shows recently stashed context threads. Used when the user asks
1208
804
  * "where was I?" or wants to see abandoned conversation threads.
1209
805
  */
1210
- function registerTopicContext(server, db, projectHashRef, notificationStore = null) {
806
+ function registerTopicContext(server, db, projectHash, notificationStore = null) {
1211
807
  const stashManager = new StashManager(db);
1212
808
  server.registerTool("topic_context", {
1213
809
  title: "Topic Context",
@@ -1217,8 +813,7 @@ function registerTopicContext(server, db, projectHashRef, notificationStore = nu
1217
813
  limit: z.number().int().min(1).max(20).default(5).describe("Max threads to return")
1218
814
  }
1219
815
  }, async (args) => {
1220
- const projectHash = projectHashRef.current;
1221
- const withNotifications = (text) => textResponse$8(prependNotifications$7(notificationStore, projectHash, text));
816
+ const withNotifications = (text) => textResponse$6(prependNotifications$5(notificationStore, projectHash, text));
1222
817
  try {
1223
818
  debug("mcp", "topic_context: request", {
1224
819
  query: args.query,
@@ -1230,9 +825,6 @@ function registerTopicContext(server, db, projectHashRef, notificationStore = nu
1230
825
  stashes = stashes.filter((s) => s.topicLabel.toLowerCase().includes(q) || s.summary.toLowerCase().includes(q));
1231
826
  }
1232
827
  if (stashes.length === 0) return withNotifications("No stashed context threads found. You're working in a single thread.");
1233
- const verbosity = loadToolVerbosityConfig().level;
1234
- if (verbosity === 1) return withNotifications(`${stashes.length} stashed thread(s)`);
1235
- if (verbosity === 2) return withNotifications(stashes.map((s, i) => `${i + 1}. ${s.topicLabel} (${timeAgo(s.createdAt)})`).join("\n"));
1236
828
  const formatted = formatStashes(stashes);
1237
829
  const footer = `\n---\n${stashes.length} stashed thread(s) | Use /laminark:resume {id} to restore`;
1238
830
  debug("mcp", "topic_context: returning", { count: stashes.length });
@@ -1240,7 +832,7 @@ function registerTopicContext(server, db, projectHashRef, notificationStore = nu
1240
832
  } catch (err) {
1241
833
  const message = err instanceof Error ? err.message : "Unknown error";
1242
834
  debug("mcp", "topic_context: error", { error: message });
1243
- return textResponse$8(`Error retrieving context threads: ${message}`);
835
+ return textResponse$6(`Error retrieving context threads: ${message}`);
1244
836
  }
1245
837
  });
1246
838
  }
@@ -1345,19 +937,19 @@ function formatAge(isoDate) {
1345
937
  const months = Math.floor(days / 30);
1346
938
  return `${months} month${months !== 1 ? "s" : ""} ago`;
1347
939
  }
1348
- function prependNotifications$6(notificationStore, projectHash, responseText) {
940
+ function prependNotifications$4(notificationStore, projectHash, responseText) {
1349
941
  if (!notificationStore) return responseText;
1350
942
  const pending = notificationStore.consumePending(projectHash);
1351
943
  if (pending.length === 0) return responseText;
1352
944
  return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
1353
945
  }
1354
- function textResponse$7(text) {
946
+ function textResponse$5(text) {
1355
947
  return { content: [{
1356
948
  type: "text",
1357
949
  text
1358
950
  }] };
1359
951
  }
1360
- function errorResponse$3(text) {
952
+ function errorResponse$2(text) {
1361
953
  return {
1362
954
  content: [{
1363
955
  type: "text",
@@ -1372,7 +964,7 @@ function errorResponse$3(text) {
1372
964
  * Allows Claude to search entities by name (exact or fuzzy), filter by type,
1373
965
  * traverse relationships to configurable depth, and see linked observations.
1374
966
  */
1375
- function registerQueryGraph(server, db, projectHashRef, notificationStore = null) {
967
+ function registerQueryGraph(server, db, projectHash, notificationStore = null) {
1376
968
  initGraphSchema(db);
1377
969
  server.registerTool("query_graph", {
1378
970
  title: "Query Knowledge Graph",
@@ -1385,18 +977,17 @@ function registerQueryGraph(server, db, projectHashRef, notificationStore = null
1385
977
  limit: z.number().int().min(1).max(50).default(20).describe("Max root entities to return (default: 20, max: 50)")
1386
978
  }
1387
979
  }, async (args) => {
1388
- const projectHash = projectHashRef.current;
1389
- const withNotifications = (text) => textResponse$7(prependNotifications$6(notificationStore, projectHash, text));
980
+ const withNotifications = (text) => textResponse$5(prependNotifications$4(notificationStore, projectHash, text));
1390
981
  try {
1391
982
  debug("mcp", "query_graph: request", {
1392
983
  query: args.query,
1393
984
  entity_type: args.entity_type,
1394
985
  depth: args.depth
1395
986
  });
1396
- if (args.entity_type !== void 0 && !isEntityType(args.entity_type)) return errorResponse$3(`Invalid entity_type "${args.entity_type}". Valid types: ${ENTITY_TYPES.join(", ")}`);
987
+ if (args.entity_type !== void 0 && !isEntityType(args.entity_type)) return errorResponse$2(`Invalid entity_type "${args.entity_type}". Valid types: ${ENTITY_TYPES.join(", ")}`);
1397
988
  const entityType = args.entity_type;
1398
989
  if (args.relationship_types) {
1399
- for (const rt of args.relationship_types) if (!isRelationshipType(rt)) return errorResponse$3(`Invalid relationship_type "${rt}". Valid types: ${RELATIONSHIP_TYPES.join(", ")}`);
990
+ for (const rt of args.relationship_types) if (!isRelationshipType(rt)) return errorResponse$2(`Invalid relationship_type "${rt}". Valid types: ${RELATIONSHIP_TYPES.join(", ")}`);
1400
991
  }
1401
992
  const relationshipTypes = args.relationship_types;
1402
993
  const rootNodes = [];
@@ -1457,21 +1048,6 @@ function registerQueryGraph(server, db, projectHashRef, notificationStore = null
1457
1048
  createdAt: row.created_at
1458
1049
  });
1459
1050
  }
1460
- const verbosity = loadToolVerbosityConfig().level;
1461
- if (verbosity === 1) {
1462
- const totalTraversals = [...traversalsByNode.values()].reduce((sum, arr) => sum + arr.length, 0);
1463
- return withNotifications(`${rootNodes.length} entities, ${totalTraversals} connections found`);
1464
- }
1465
- if (verbosity === 2) {
1466
- const lines = [];
1467
- lines.push("## Entities Found");
1468
- lines.push("");
1469
- for (const node of rootNodes) {
1470
- const traversals = traversalsByNode.get(node.id) ?? [];
1471
- lines.push(`- ${formatEntityType(node.type)} ${node.name} (${traversals.length} connections)`);
1472
- }
1473
- return withNotifications(lines.join("\n"));
1474
- }
1475
1051
  const formatted = formatResults(rootNodes, traversalsByNode, observations, args.query);
1476
1052
  debug("mcp", "query_graph: returning", {
1477
1053
  rootNodes: rootNodes.length,
@@ -1482,11 +1058,180 @@ function registerQueryGraph(server, db, projectHashRef, notificationStore = null
1482
1058
  } catch (err) {
1483
1059
  const message = err instanceof Error ? err.message : "Unknown error";
1484
1060
  debug("mcp", "query_graph: error", { error: message });
1485
- return errorResponse$3(`Graph query error: ${message}`);
1061
+ return errorResponse$2(`Graph query error: ${message}`);
1486
1062
  }
1487
1063
  });
1488
1064
  }
1489
1065
 
1066
+ //#endregion
1067
+ //#region src/graph/staleness.ts
1068
+ /**
1069
+ * Negation patterns: newer observation negates older one.
1070
+ * Matches when newer text contains negation keywords absent in older text
1071
+ * and both discuss similar subjects.
1072
+ */
1073
+ const NEGATION_KEYWORDS = [
1074
+ "not",
1075
+ "don't",
1076
+ "no longer",
1077
+ "stopped",
1078
+ "never",
1079
+ "doesn't",
1080
+ "won't",
1081
+ "isn't",
1082
+ "aren't",
1083
+ "discontinued"
1084
+ ];
1085
+ /**
1086
+ * Replacement patterns: newer observation explicitly replaces older approach.
1087
+ */
1088
+ const REPLACEMENT_PATTERNS = [
1089
+ /switched\s+(?:from\s+\S+\s+)?to\b/i,
1090
+ /migrated\s+(?:from\s+\S+\s+)?to\b/i,
1091
+ /replaced\s+(?:\S+\s+)?with\b/i,
1092
+ /changed\s+from\b/i,
1093
+ /moved\s+(?:from\s+\S+\s+)?to\b/i,
1094
+ /upgraded\s+(?:from\s+\S+\s+)?to\b/i,
1095
+ /swapped\s+(?:\S+\s+)?(?:for|with)\b/i
1096
+ ];
1097
+ /**
1098
+ * Status change patterns: newer observation marks something as inactive.
1099
+ */
1100
+ const STATUS_CHANGE_KEYWORDS = [
1101
+ "removed",
1102
+ "deleted",
1103
+ "deprecated",
1104
+ "archived",
1105
+ "dropped",
1106
+ "disabled",
1107
+ "decommissioned",
1108
+ "sunset",
1109
+ "abandoned"
1110
+ ];
1111
+ /**
1112
+ * Creates the staleness_flags table if it doesn't exist.
1113
+ * Uses a separate table rather than modifying the observations table,
1114
+ * keeping staleness metadata decoupled from core observation storage.
1115
+ */
1116
+ function initStalenessSchema(db) {
1117
+ db.exec(`
1118
+ CREATE TABLE IF NOT EXISTS staleness_flags (
1119
+ observation_id TEXT PRIMARY KEY,
1120
+ flagged_at TEXT NOT NULL DEFAULT (datetime('now')),
1121
+ reason TEXT NOT NULL,
1122
+ resolved INTEGER NOT NULL DEFAULT 0
1123
+ );
1124
+ CREATE INDEX IF NOT EXISTS idx_staleness_resolved ON staleness_flags(resolved);
1125
+ `);
1126
+ }
1127
+ /**
1128
+ * Detects potential staleness (contradictions) between observations
1129
+ * linked to a specific entity.
1130
+ *
1131
+ * Compares consecutive observation pairs chronologically and checks for:
1132
+ * 1. Negation patterns (newer negates older)
1133
+ * 2. Replacement patterns (newer replaces older approach)
1134
+ * 3. Status change patterns (newer marks something as inactive)
1135
+ *
1136
+ * This is DETECTION ONLY -- no data is modified.
1137
+ *
1138
+ * @param db - better-sqlite3 Database handle
1139
+ * @param entityId - Graph node ID to check observations for
1140
+ * @returns Array of StalenessReport for each detected contradiction
1141
+ */
1142
+ function detectStaleness(db, entityId) {
1143
+ const node = db.prepare("SELECT id, name, type, observation_ids FROM graph_nodes WHERE id = ?").get(entityId);
1144
+ if (!node) return [];
1145
+ const obsIds = JSON.parse(node.observation_ids);
1146
+ if (obsIds.length < 2) return [];
1147
+ const placeholders = obsIds.map(() => "?").join(", ");
1148
+ const observations = db.prepare(`SELECT * FROM observations WHERE id IN (${placeholders}) AND deleted_at IS NULL ORDER BY created_at ASC`).all(...obsIds).map(rowToObservation);
1149
+ if (observations.length < 2) return [];
1150
+ const reports = [];
1151
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1152
+ for (let i = 0; i < observations.length - 1; i++) {
1153
+ const older = observations[i];
1154
+ const newer = observations[i + 1];
1155
+ const reason = detectContradiction(older.content, newer.content);
1156
+ if (reason) reports.push({
1157
+ entityId: node.id,
1158
+ entityName: node.name,
1159
+ entityType: node.type,
1160
+ newerObservation: {
1161
+ id: newer.id,
1162
+ text: newer.content,
1163
+ created_at: newer.createdAt
1164
+ },
1165
+ olderObservation: {
1166
+ id: older.id,
1167
+ text: older.content,
1168
+ created_at: older.createdAt
1169
+ },
1170
+ reason,
1171
+ detectedAt: now
1172
+ });
1173
+ }
1174
+ return reports;
1175
+ }
1176
+ /**
1177
+ * Detects contradiction between two observation texts.
1178
+ * Returns a human-readable reason string, or null if no contradiction found.
1179
+ */
1180
+ function detectContradiction(olderText, newerText) {
1181
+ const olderLower = olderText.toLowerCase();
1182
+ const newerLower = newerText.toLowerCase();
1183
+ const negationResult = detectNegation(olderLower, newerLower);
1184
+ if (negationResult) return negationResult;
1185
+ const replacementResult = detectReplacement(newerLower);
1186
+ if (replacementResult) return replacementResult;
1187
+ const statusResult = detectStatusChange(olderLower, newerLower);
1188
+ if (statusResult) return statusResult;
1189
+ return null;
1190
+ }
1191
+ /**
1192
+ * Detects negation: newer text contains negation keywords that are absent
1193
+ * in the older text, suggesting the newer observation contradicts the older.
1194
+ */
1195
+ function detectNegation(olderLower, newerLower) {
1196
+ for (const keyword of NEGATION_KEYWORDS) if (newerLower.includes(keyword) && !olderLower.includes(keyword)) return `Newer observation contains negation ("${keyword}") not present in older observation`;
1197
+ return null;
1198
+ }
1199
+ /**
1200
+ * Detects replacement: newer text explicitly mentions switching/replacing.
1201
+ */
1202
+ function detectReplacement(newerLower) {
1203
+ for (const pattern of REPLACEMENT_PATTERNS) {
1204
+ const match = newerLower.match(pattern);
1205
+ if (match) return `Newer observation indicates replacement ("${match[0].trim()}")`;
1206
+ }
1207
+ return null;
1208
+ }
1209
+ /**
1210
+ * Detects status change: newer text marks something as removed/deprecated
1211
+ * when the older text described it as active/present.
1212
+ */
1213
+ function detectStatusChange(olderLower, newerLower) {
1214
+ for (const keyword of STATUS_CHANGE_KEYWORDS) if (newerLower.includes(keyword) && !olderLower.includes(keyword)) return `Newer observation indicates status change ("${keyword}")`;
1215
+ return null;
1216
+ }
1217
+ /**
1218
+ * Flags an observation as stale with an advisory reason.
1219
+ *
1220
+ * This flag is advisory -- search can use it to deprioritize but never hide
1221
+ * the observation. The observation remains fully queryable.
1222
+ *
1223
+ * Uses INSERT OR REPLACE to allow re-flagging with an updated reason.
1224
+ *
1225
+ * @param db - better-sqlite3 Database handle
1226
+ * @param observationId - ID of the observation to flag
1227
+ * @param reason - Human-readable explanation of why it's stale
1228
+ */
1229
+ function flagStaleObservation(db, observationId, reason) {
1230
+ initStalenessSchema(db);
1231
+ db.prepare(`INSERT OR REPLACE INTO staleness_flags (observation_id, reason, resolved)
1232
+ VALUES (?, ?, 0)`).run(observationId, reason);
1233
+ }
1234
+
1490
1235
  //#endregion
1491
1236
  //#region src/mcp/tools/graph-stats.ts
1492
1237
  /**
@@ -1584,13 +1329,13 @@ function formatStats(stats) {
1584
1329
  }
1585
1330
  return lines.join("\n");
1586
1331
  }
1587
- function prependNotifications$5(notificationStore, projectHash, responseText) {
1332
+ function prependNotifications$3(notificationStore, projectHash, responseText) {
1588
1333
  if (!notificationStore) return responseText;
1589
1334
  const pending = notificationStore.consumePending(projectHash);
1590
1335
  if (pending.length === 0) return responseText;
1591
1336
  return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
1592
1337
  }
1593
- function textResponse$6(text) {
1338
+ function textResponse$4(text) {
1594
1339
  return { content: [{
1595
1340
  type: "text",
1596
1341
  text
@@ -1603,14 +1348,13 @@ function textResponse$6(text) {
1603
1348
  * type distribution, degree statistics, hotspot nodes, duplicate candidates,
1604
1349
  * and staleness flags. No input parameters -- dashboard view.
1605
1350
  */
1606
- function registerGraphStats(server, db, projectHashRef, notificationStore = null) {
1351
+ function registerGraphStats(server, db, projectHash, notificationStore = null) {
1607
1352
  initGraphSchema(db);
1608
1353
  server.registerTool("graph_stats", {
1609
1354
  title: "Graph Statistics",
1610
1355
  description: "Get knowledge graph statistics: entity counts, relationship distribution, health metrics. Use to understand the state of accumulated knowledge.",
1611
1356
  inputSchema: {}
1612
1357
  }, async () => {
1613
- const projectHash = projectHashRef.current;
1614
1358
  try {
1615
1359
  debug("mcp", "graph_stats: request");
1616
1360
  const stats = collectGraphStats(db);
@@ -1619,162 +1363,42 @@ function registerGraphStats(server, db, projectHashRef, notificationStore = null
1619
1363
  nodes: stats.total_nodes,
1620
1364
  edges: stats.total_edges
1621
1365
  });
1622
- return textResponse$6(prependNotifications$5(notificationStore, projectHash, formatted));
1366
+ return textResponse$4(prependNotifications$3(notificationStore, projectHash, formatted));
1623
1367
  } catch (err) {
1624
1368
  const message = err instanceof Error ? err.message : "Unknown error";
1625
1369
  debug("mcp", "graph_stats: error", { error: message });
1626
- return textResponse$6(`Graph stats error: ${message}`);
1370
+ return textResponse$4(`Graph stats error: ${message}`);
1627
1371
  }
1628
1372
  });
1629
1373
  }
1630
1374
 
1631
1375
  //#endregion
1632
- //#region src/mcp/tools/hygiene.ts
1633
- function formatReport(report, mode, tier) {
1634
- const lines = [];
1635
- lines.push("## Database Hygiene Report");
1636
- lines.push(`Analyzed ${report.totalObservations.toLocaleString()} observations`);
1637
- lines.push("");
1638
- lines.push("### Summary");
1639
- lines.push("| Tier | Count | Action |");
1640
- lines.push("|------|-------|--------|");
1641
- lines.push(`| High (>= 0.7) | ${report.summary.high} | Safe to purge |`);
1642
- lines.push(`| Medium (0.5-0.69) | ${report.summary.medium} | Review recommended |`);
1643
- if (report.summary.low > 0) lines.push(`| Low (< 0.5) | ${report.summary.low} | Kept |`);
1644
- lines.push(`| Orphan graph nodes | ${report.summary.orphanNodeCount} | Dead references |`);
1645
- lines.push("");
1646
- if (report.candidates.length === 0) {
1647
- lines.push("No candidates found matching the selected tier.");
1648
- return lines.join("\n");
1649
- }
1650
- const bySession = /* @__PURE__ */ new Map();
1651
- for (const c of report.candidates) {
1652
- const key = c.sessionId ?? "(no session)";
1653
- const list = bySession.get(key) ?? [];
1654
- list.push(c);
1655
- bySession.set(key, list);
1656
- }
1657
- const tierLabel = tier === "all" ? "All" : tier === "medium" ? "Medium+" : "High";
1658
- lines.push(`### ${tierLabel} Confidence Candidates (showing ${report.candidates.length})`);
1659
- lines.push("");
1660
- for (const [sessionId, candidates] of bySession) {
1661
- const sessionDate = candidates[0]?.createdAt?.substring(0, 10) ?? "";
1662
- lines.push(`#### Session: ${sessionId.substring(0, 8)} (${sessionDate}, ${candidates.length} obs)`);
1663
- lines.push("| ID | Kind | Source | Confidence | Signals | Preview |");
1664
- lines.push("|----|------|--------|------------|---------|---------|");
1665
- for (const c of candidates) {
1666
- const signals = [];
1667
- if (c.signals.orphaned) signals.push("orphaned");
1668
- if (c.signals.islandNode) signals.push("island");
1669
- if (c.signals.noiseClassified) signals.push("noise");
1670
- if (c.signals.shortContent) signals.push("short");
1671
- if (c.signals.autoCaptured) signals.push("auto");
1672
- if (c.signals.stale) signals.push("stale");
1673
- const preview = c.contentPreview.replace(/\|/g, "\\|").replace(/\n/g, " ");
1674
- lines.push(`| ${c.shortId} | ${c.kind} | ${c.source} | ${c.confidence.toFixed(2)} | ${signals.join(",") || "-"} | ${preview} |`);
1675
- }
1676
- lines.push("");
1677
- }
1678
- if (mode === "simulate") lines.push(`_Dry run — no data modified. Use \`hygiene(mode="purge", tier="${tier}")\` to execute._`);
1679
- return lines.join("\n");
1680
- }
1681
- function formatPurgeResult(observationsPurged, orphanNodesRemoved, tier) {
1682
- const lines = [];
1683
- lines.push("## Hygiene Purge Complete");
1684
- lines.push(`- Tier: ${tier}`);
1685
- lines.push(`- Observations soft-deleted: ${observationsPurged}`);
1686
- lines.push(`- Orphan graph nodes removed: ${orphanNodesRemoved}`);
1687
- return lines.join("\n");
1688
- }
1689
- function prependNotifications$4(notificationStore, projectHash, responseText) {
1376
+ //#region src/mcp/tools/status.ts
1377
+ function prependNotifications$2(notificationStore, projectHash, responseText) {
1690
1378
  if (!notificationStore) return responseText;
1691
1379
  const pending = notificationStore.consumePending(projectHash);
1692
1380
  if (pending.length === 0) return responseText;
1693
1381
  return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
1694
1382
  }
1695
- function textResponse$5(text) {
1383
+ function textResponse$3(text) {
1696
1384
  return { content: [{
1697
1385
  type: "text",
1698
1386
  text
1699
1387
  }] };
1700
1388
  }
1701
- function registerHygiene(server, db, projectHashRef, notificationStore = null) {
1702
- server.registerTool("hygiene", {
1703
- title: "Database Hygiene",
1704
- description: "Analyze observations for deletion candidates with confidence scoring. Simulate mode (default) produces a dry-run report. Purge mode soft-deletes candidates and removes dead orphan graph nodes.",
1705
- inputSchema: {
1706
- mode: z.enum(["simulate", "purge"]).default("simulate").describe("simulate = dry-run report, purge = execute deletions"),
1707
- tier: z.enum([
1708
- "high",
1709
- "medium",
1710
- "all"
1711
- ]).default("high").describe("Which confidence tier to act on"),
1712
- session_id: z.string().optional().describe("Optional: scope analysis to a single session"),
1713
- limit: z.number().int().min(1).max(200).default(50).describe("Max results to return")
1714
- }
1715
- }, async (args) => {
1716
- const projectHash = projectHashRef.current;
1389
+ function registerStatus(server, cache, projectHash, notificationStore = null) {
1390
+ server.registerTool("status", {
1391
+ title: "Laminark Status",
1392
+ description: "Show Laminark system status: connection info, memory count, token estimates, and capabilities.",
1393
+ inputSchema: {}
1394
+ }, async () => {
1717
1395
  try {
1718
- const mode = args.mode ?? "simulate";
1719
- const tier = args.tier ?? "high";
1720
- const sessionId = args.session_id;
1721
- const limit = args.limit ?? 50;
1722
- debug("hygiene", "Request", {
1723
- mode,
1724
- tier,
1725
- sessionId,
1726
- limit
1727
- });
1728
- const report = analyzeObservations(db, projectHash, {
1729
- sessionId,
1730
- limit,
1731
- minTier: tier === "all" ? "low" : tier
1732
- });
1733
- if (mode === "purge") {
1734
- const result = executePurge(db, projectHash, report, tier);
1735
- return textResponse$5(prependNotifications$4(notificationStore, projectHash, formatPurgeResult(result.observationsPurged, result.orphanNodesRemoved, tier)));
1736
- }
1737
- return textResponse$5(prependNotifications$4(notificationStore, projectHash, formatReport(report, mode, tier)));
1396
+ debug("mcp", "status: request (cached)");
1397
+ return textResponse$3(prependNotifications$2(notificationStore, projectHash, cache.getFormatted()));
1738
1398
  } catch (err) {
1739
1399
  const message = err instanceof Error ? err.message : "Unknown error";
1740
- debug("hygiene", "Error", { error: message });
1741
- return textResponse$5(`Hygiene analysis error: ${message}`);
1742
- }
1743
- });
1744
- }
1745
-
1746
- //#endregion
1747
- //#region src/mcp/tools/status.ts
1748
- function prependNotifications$3(notificationStore, projectHash, responseText) {
1749
- if (!notificationStore) return responseText;
1750
- const pending = notificationStore.consumePending(projectHash);
1751
- if (pending.length === 0) return responseText;
1752
- return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
1753
- }
1754
- function textResponse$4(text) {
1755
- return { content: [{
1756
- type: "text",
1757
- text
1758
- }] };
1759
- }
1760
- function registerStatus(server, cache, projectHashRef, notificationStore = null) {
1761
- server.registerTool("status", {
1762
- title: "Laminark Status",
1763
- description: "Show Laminark system status: connection info, memory count, token estimates, and capabilities.",
1764
- inputSchema: {}
1765
- }, async () => {
1766
- const projectHash = projectHashRef.current;
1767
- try {
1768
- debug("mcp", "status: request (cached)");
1769
- const verbosity = loadToolVerbosityConfig().level;
1770
- if (verbosity === 1) return textResponse$4(prependNotifications$3(notificationStore, projectHash, "Laminark: connected"));
1771
- const formatted = cache.getFormatted();
1772
- if (verbosity === 2) return textResponse$4(prependNotifications$3(notificationStore, projectHash, formatted.split("\n").slice(0, 8).join("\n")));
1773
- return textResponse$4(prependNotifications$3(notificationStore, projectHash, formatted));
1774
- } catch (err) {
1775
- const message = err instanceof Error ? err.message : "Unknown error";
1776
- debug("mcp", "status: error", { error: message });
1777
- return textResponse$4(`Status error: ${message}`);
1400
+ debug("mcp", "status: error", { error: message });
1401
+ return textResponse$3(`Status error: ${message}`);
1778
1402
  }
1779
1403
  });
1780
1404
  }
@@ -1791,7 +1415,7 @@ function formatUptime(seconds) {
1791
1415
  }
1792
1416
  var StatusCache = class {
1793
1417
  db;
1794
- projectHashRef;
1418
+ projectHash;
1795
1419
  projectPath;
1796
1420
  hasVectorSupport;
1797
1421
  isWorkerReady;
@@ -1800,9 +1424,9 @@ var StatusCache = class {
1800
1424
  /** Uptime snapshot at the time cachedBody was built. */
1801
1425
  builtAtUptime = 0;
1802
1426
  dirty = false;
1803
- constructor(db, projectHashRef, projectPath, hasVectorSupport, isWorkerReady) {
1427
+ constructor(db, projectHash, projectPath, hasVectorSupport, isWorkerReady) {
1804
1428
  this.db = db;
1805
- this.projectHashRef = projectHashRef;
1429
+ this.projectHash = projectHash;
1806
1430
  this.projectPath = projectPath;
1807
1431
  this.hasVectorSupport = hasVectorSupport;
1808
1432
  this.isWorkerReady = isWorkerReady;
@@ -1829,7 +1453,7 @@ var StatusCache = class {
1829
1453
  }
1830
1454
  rebuild() {
1831
1455
  try {
1832
- const ph = this.projectHashRef.current;
1456
+ const ph = this.projectHash;
1833
1457
  const totalObs = this.db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE project_hash = ? AND deleted_at IS NULL").get(ph).cnt;
1834
1458
  const embeddedObs = this.db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE project_hash = ? AND deleted_at IS NULL AND embedding_model IS NOT NULL").get(ph).cnt;
1835
1459
  const deletedObs = this.db.prepare("SELECT COUNT(*) as cnt FROM observations WHERE project_hash = ? AND deleted_at IS NOT NULL").get(ph).cnt;
@@ -1885,13 +1509,13 @@ var StatusCache = class {
1885
1509
 
1886
1510
  //#endregion
1887
1511
  //#region src/mcp/tools/discover-tools.ts
1888
- function textResponse$3(text) {
1512
+ function textResponse$2(text) {
1889
1513
  return { content: [{
1890
1514
  type: "text",
1891
1515
  text
1892
1516
  }] };
1893
1517
  }
1894
- function errorResponse$2(text) {
1518
+ function errorResponse$1(text) {
1895
1519
  return {
1896
1520
  content: [{
1897
1521
  type: "text",
@@ -1900,7 +1524,7 @@ function errorResponse$2(text) {
1900
1524
  isError: true
1901
1525
  };
1902
1526
  }
1903
- function prependNotifications$2(notificationStore, projectHash, responseText) {
1527
+ function prependNotifications$1(notificationStore, projectHash, responseText) {
1904
1528
  if (!notificationStore) return responseText;
1905
1529
  const pending = notificationStore.consumePending(projectHash);
1906
1530
  if (pending.length === 0) return responseText;
@@ -1921,7 +1545,7 @@ function formatToolResult(result, index) {
1921
1545
  * with optional scope filtering. Returns ranked results with scope, usage count,
1922
1546
  * and last used timestamp metadata.
1923
1547
  */
1924
- function registerDiscoverTools(server, toolRegistry, worker, hasVectorSupport, notificationStore, projectHashRef) {
1548
+ function registerDiscoverTools(server, toolRegistry, worker, hasVectorSupport, notificationStore, projectHash) {
1925
1549
  server.registerTool("discover_tools", {
1926
1550
  title: "Discover Tools",
1927
1551
  description: "Search the tool registry to find available tools by keyword or description. Supports semantic search -- \"file manipulation\" finds tools described as \"read and write files\". Returns scope, usage count, and last used timestamp for each result.",
@@ -1935,8 +1559,7 @@ function registerDiscoverTools(server, toolRegistry, worker, hasVectorSupport, n
1935
1559
  limit: z.number().int().min(1).max(50).default(20).describe("Maximum results to return (default: 20)")
1936
1560
  }
1937
1561
  }, async (args) => {
1938
- const projectHash = projectHashRef.current;
1939
- const withNotifications = (text) => textResponse$3(prependNotifications$2(notificationStore, projectHash, text));
1562
+ const withNotifications = (text) => textResponse$2(prependNotifications$1(notificationStore, projectHash, text));
1940
1563
  try {
1941
1564
  debug("mcp", "discover_tools: request", {
1942
1565
  query: args.query,
@@ -1974,14 +1597,14 @@ function registerDiscoverTools(server, toolRegistry, worker, hasVectorSupport, n
1974
1597
  } catch (err) {
1975
1598
  const message = err instanceof Error ? err.message : "Unknown error";
1976
1599
  debug("mcp", "discover_tools: error", { error: message });
1977
- return errorResponse$2(`Discover tools error: ${message}`);
1600
+ return errorResponse$1(`Discover tools error: ${message}`);
1978
1601
  }
1979
1602
  });
1980
1603
  }
1981
1604
 
1982
1605
  //#endregion
1983
1606
  //#region src/mcp/tools/report-tools.ts
1984
- function textResponse$2(text) {
1607
+ function textResponse$1(text) {
1985
1608
  return { content: [{
1986
1609
  type: "text",
1987
1610
  text
@@ -1994,7 +1617,7 @@ function textResponse$2(text) {
1994
1617
  * each into the tool registry. Tool type, scope, and server name are inferred
1995
1618
  * from the tool name using the same parser as PostToolUse organic discovery.
1996
1619
  */
1997
- function registerReportTools(server, toolRegistry, projectHashRef) {
1620
+ function registerReportTools(server, toolRegistry, projectHash) {
1998
1621
  server.registerTool("report_available_tools", {
1999
1622
  title: "Report Available Tools",
2000
1623
  description: "Register all tools available in this session with Laminark. Call this once at session start with every tool name you have access to (built-in and MCP). This populates the tool registry for discovery and routing.",
@@ -2003,7 +1626,6 @@ function registerReportTools(server, toolRegistry, projectHashRef) {
2003
1626
  description: z.string().optional().describe("Brief description of the tool")
2004
1627
  })).min(1).describe("Array of tools available in this session") }
2005
1628
  }, async (args) => {
2006
- const projectHash = projectHashRef.current;
2007
1629
  try {
2008
1630
  let registered = 0;
2009
1631
  let skipped = 0;
@@ -2031,7 +1653,7 @@ function registerReportTools(server, toolRegistry, projectHashRef) {
2031
1653
  registered,
2032
1654
  skipped
2033
1655
  });
2034
- return textResponse$2(`Registered ${registered} tools in the tool registry.${skipped > 0 ? ` Skipped ${skipped} Laminark tools (already known).` : ""}`);
1656
+ return textResponse$1(`Registered ${registered} tools in the tool registry.${skipped > 0 ? ` Skipped ${skipped} Laminark tools (already known).` : ""}`);
2035
1657
  } catch (err) {
2036
1658
  const message = err instanceof Error ? err.message : "Unknown error";
2037
1659
  debug("mcp", "report_available_tools: error", { error: message });
@@ -2048,19 +1670,19 @@ function registerReportTools(server, toolRegistry, projectHashRef) {
2048
1670
 
2049
1671
  //#endregion
2050
1672
  //#region src/mcp/tools/debug-paths.ts
2051
- function prependNotifications$1(notificationStore, projectHash, responseText) {
1673
+ function prependNotifications(notificationStore, projectHash, responseText) {
2052
1674
  if (!notificationStore) return responseText;
2053
1675
  const pending = notificationStore.consumePending(projectHash);
2054
1676
  if (pending.length === 0) return responseText;
2055
1677
  return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
2056
1678
  }
2057
- function textResponse$1(text) {
1679
+ function textResponse(text) {
2058
1680
  return { content: [{
2059
1681
  type: "text",
2060
1682
  text
2061
1683
  }] };
2062
1684
  }
2063
- function errorResponse$1(text) {
1685
+ function errorResponse(text) {
2064
1686
  return {
2065
1687
  content: [{
2066
1688
  type: "text",
@@ -2090,25 +1712,24 @@ function formatKissSummary(raw) {
2090
1712
  *
2091
1713
  * Tools: path_start, path_resolve, path_show, path_list
2092
1714
  */
2093
- function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore, projectHashRef) {
1715
+ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore, projectHash) {
2094
1716
  server.registerTool("path_start", {
2095
1717
  title: "Start Debug Path",
2096
1718
  description: "Explicitly start tracking a debug path. Use when auto-detection hasn't triggered but you're actively debugging.",
2097
1719
  inputSchema: { trigger: z.string().describe("Brief description of the issue being debugged") }
2098
1720
  }, async (args) => {
2099
- const projectHash = projectHashRef.current;
2100
- const withNotifications = (text) => textResponse$1(prependNotifications$1(notificationStore, projectHash, text));
1721
+ const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
2101
1722
  try {
2102
1723
  debug("mcp", "path_start: request", { trigger: args.trigger });
2103
1724
  const existingPathId = pathTracker.getActivePathId();
2104
1725
  const pathId = pathTracker.startManually(args.trigger);
2105
- if (!pathId) return errorResponse$1("Failed to start debug path");
1726
+ if (!pathId) return errorResponse("Failed to start debug path");
2106
1727
  if (existingPathId && existingPathId === pathId) return withNotifications(`Debug path already active: ${pathId}`);
2107
- return withNotifications(verboseResponse("Debug path started.", `Debug path started: ${pathId}`, `Debug path started: ${pathId}\nTracking: ${args.trigger}`));
1728
+ return withNotifications(`Debug path started: ${pathId}\nTracking: ${args.trigger}`);
2108
1729
  } catch (err) {
2109
1730
  const message = err instanceof Error ? err.message : "Unknown error";
2110
1731
  debug("mcp", "path_start: error", { error: message });
2111
- return errorResponse$1(`path_start error: ${message}`);
1732
+ return errorResponse(`path_start error: ${message}`);
2112
1733
  }
2113
1734
  });
2114
1735
  server.registerTool("path_resolve", {
@@ -2116,18 +1737,17 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
2116
1737
  description: "Explicitly resolve the active debug path with a resolution summary. Use when auto-detection hasn't detected resolution.",
2117
1738
  inputSchema: { resolution: z.string().describe("What fixed the issue") }
2118
1739
  }, async (args) => {
2119
- const projectHash = projectHashRef.current;
2120
- const withNotifications = (text) => textResponse$1(prependNotifications$1(notificationStore, projectHash, text));
1740
+ const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
2121
1741
  try {
2122
1742
  debug("mcp", "path_resolve: request", { resolution: args.resolution });
2123
1743
  const pathId = pathTracker.getActivePathId();
2124
- if (!pathId) return errorResponse$1("No active debug path to resolve");
1744
+ if (!pathId) return errorResponse("No active debug path to resolve");
2125
1745
  pathTracker.resolveManually(args.resolution);
2126
- return withNotifications(verboseResponse("Debug path resolved.", `Debug path resolved: ${pathId}`, `Debug path resolved: ${pathId}\nResolution: ${args.resolution}\nKISS summary generating in background...`));
1746
+ return withNotifications(`Debug path resolved: ${pathId}\nResolution: ${args.resolution}\nKISS summary generating in background...`);
2127
1747
  } catch (err) {
2128
1748
  const message = err instanceof Error ? err.message : "Unknown error";
2129
1749
  debug("mcp", "path_resolve: error", { error: message });
2130
- return errorResponse$1(`path_resolve error: ${message}`);
1750
+ return errorResponse(`path_resolve error: ${message}`);
2131
1751
  }
2132
1752
  });
2133
1753
  server.registerTool("path_show", {
@@ -2135,29 +1755,18 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
2135
1755
  description: "Show a debug path with its waypoints and KISS summary.",
2136
1756
  inputSchema: { path_id: z.string().optional().describe("Path ID to show. Omit for active path.") }
2137
1757
  }, async (args) => {
2138
- const projectHash = projectHashRef.current;
2139
- const withNotifications = (text) => textResponse$1(prependNotifications$1(notificationStore, projectHash, text));
1758
+ const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
2140
1759
  try {
2141
1760
  debug("mcp", "path_show: request", { path_id: args.path_id });
2142
1761
  let pathData;
2143
1762
  if (args.path_id) {
2144
1763
  pathData = pathRepo.getPath(args.path_id);
2145
- if (!pathData) return errorResponse$1(`Debug path not found: ${args.path_id}`);
1764
+ if (!pathData) return errorResponse(`Debug path not found: ${args.path_id}`);
2146
1765
  } else {
2147
1766
  pathData = pathRepo.getActivePath();
2148
- if (!pathData) return errorResponse$1("No active debug path");
1767
+ if (!pathData) return errorResponse("No active debug path");
2149
1768
  }
2150
- const verbosity = loadToolVerbosityConfig().level;
2151
- if (verbosity === 1) return withNotifications(`Showing debug path: ${pathData.status}`);
2152
1769
  const waypoints = pathRepo.getWaypoints(pathData.id);
2153
- if (verbosity === 2) {
2154
- const lines = [];
2155
- lines.push(`## Debug Path: ${pathData.id}`);
2156
- lines.push(`**Status:** ${pathData.status} | **Trigger:** ${pathData.trigger_summary}`);
2157
- lines.push(`Waypoints: ${waypoints.length}`);
2158
- if (pathData.resolution_summary) lines.push(`Resolution: ${pathData.resolution_summary}`);
2159
- return withNotifications(lines.join("\n"));
2160
- }
2161
1770
  const lines = [];
2162
1771
  lines.push(`## Debug Path: ${pathData.id}`);
2163
1772
  lines.push(`Status: ${pathData.status}`);
@@ -2179,7 +1788,7 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
2179
1788
  } catch (err) {
2180
1789
  const message = err instanceof Error ? err.message : "Unknown error";
2181
1790
  debug("mcp", "path_show: error", { error: message });
2182
- return errorResponse$1(`path_show error: ${message}`);
1791
+ return errorResponse(`path_show error: ${message}`);
2183
1792
  }
2184
1793
  });
2185
1794
  server.registerTool("path_list", {
@@ -2194,8 +1803,7 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
2194
1803
  limit: z.number().int().min(1).max(50).default(10).describe("Max paths to return")
2195
1804
  }
2196
1805
  }, async (args) => {
2197
- const projectHash = projectHashRef.current;
2198
- const withNotifications = (text) => textResponse$1(prependNotifications$1(notificationStore, projectHash, text));
1806
+ const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
2199
1807
  try {
2200
1808
  debug("mcp", "path_list: request", {
2201
1809
  status: args.status,
@@ -2204,768 +1812,25 @@ function registerDebugPathTools(server, pathRepo, pathTracker, notificationStore
2204
1812
  let paths = pathRepo.listPaths(args.limit);
2205
1813
  if (args.status) paths = paths.filter((p) => p.status === args.status);
2206
1814
  if (paths.length === 0) return withNotifications("No debug paths found");
2207
- const verbosity = loadToolVerbosityConfig().level;
2208
- if (verbosity === 1) return withNotifications(`${paths.length} debug paths found`);
2209
1815
  const lines = [];
2210
- lines.push("## Debug Paths");
2211
- lines.push("");
2212
- if (verbosity === 2) {
2213
- lines.push("| Status | Trigger |");
2214
- lines.push("|--------|---------|");
2215
- for (const p of paths) {
2216
- const trigger = p.trigger_summary.length > 60 ? p.trigger_summary.slice(0, 60) + "..." : p.trigger_summary;
2217
- lines.push(`| ${p.status} | ${trigger} |`);
2218
- }
2219
- } else {
2220
- lines.push("| ID (short) | Status | Trigger | Started | Resolved |");
2221
- lines.push("|------------|--------|---------|---------|----------|");
2222
- for (const p of paths) {
2223
- const shortId = p.id.slice(0, 8);
2224
- const trigger = p.trigger_summary.length > 50 ? p.trigger_summary.slice(0, 50) + "..." : p.trigger_summary;
2225
- const resolved = p.resolved_at ?? "-";
2226
- lines.push(`| ${shortId} | ${p.status} | ${trigger} | ${p.started_at} | ${resolved} |`);
2227
- }
2228
- }
2229
- return withNotifications(lines.join("\n"));
2230
- } catch (err) {
2231
- const message = err instanceof Error ? err.message : "Unknown error";
2232
- debug("mcp", "path_list: error", { error: message });
2233
- return errorResponse$1(`path_list error: ${message}`);
2234
- }
2235
- });
2236
- }
2237
-
2238
- //#endregion
2239
- //#region src/mcp/tools/thought-branches.ts
2240
- function prependNotifications(notificationStore, projectHash, responseText) {
2241
- if (!notificationStore) return responseText;
2242
- const pending = notificationStore.consumePending(projectHash);
2243
- if (pending.length === 0) return responseText;
2244
- return pending.map((n) => `[Laminark] ${n.message}`).join("\n") + "\n\n" + responseText;
2245
- }
2246
- function textResponse(text) {
2247
- return { content: [{
2248
- type: "text",
2249
- text
2250
- }] };
2251
- }
2252
- function errorResponse(text) {
2253
- return {
2254
- content: [{
2255
- type: "text",
2256
- text
2257
- }],
2258
- isError: true
2259
- };
2260
- }
2261
- function registerThoughtBranchTools(server, branchRepo, obsRepo, notificationStore, projectHashRef) {
2262
- server.registerTool("query_branches", {
2263
- title: "Query Thought Branches",
2264
- description: "Search and list thought branches - coherent units of work (investigations, bug fixes, features). Use to see work history and what was investigated, fixed, or built.",
2265
- inputSchema: {
2266
- status: z.enum([
2267
- "active",
2268
- "completed",
2269
- "abandoned",
2270
- "merged"
2271
- ]).optional().describe("Filter by branch status"),
2272
- branch_type: z.enum([
2273
- "investigation",
2274
- "bug_fix",
2275
- "feature",
2276
- "refactor",
2277
- "research",
2278
- "unknown"
2279
- ]).optional().describe("Filter by branch type"),
2280
- limit: z.number().int().min(1).max(50).default(10).describe("Maximum results to return")
2281
- }
2282
- }, async (args) => {
2283
- const projectHash = projectHashRef.current;
2284
- const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
2285
- try {
2286
- debug("mcp", "query_branches: request", {
2287
- status: args.status,
2288
- branch_type: args.branch_type,
2289
- limit: args.limit
2290
- });
2291
- let branches;
2292
- if (args.status) branches = branchRepo.listByStatus(args.status, args.limit);
2293
- else if (args.branch_type) branches = branchRepo.listByType(args.branch_type, args.limit);
2294
- else branches = branchRepo.listBranches(args.limit);
2295
- if (branches.length === 0) return withNotifications("No thought branches found");
2296
- const verbosity = loadToolVerbosityConfig().level;
2297
- if (verbosity === 1) return withNotifications(`${branches.length} branches found`);
2298
- const lines = [];
2299
- lines.push("## Thought Branches");
2300
- lines.push("");
2301
- if (verbosity === 2) {
2302
- lines.push("| Status | Type | Title |");
2303
- lines.push("|--------|------|-------|");
2304
- for (const b of branches) {
2305
- const title = b.title ? b.title.length > 50 ? b.title.slice(0, 50) + "..." : b.title : "-";
2306
- lines.push(`| ${b.status} | ${b.branch_type} | ${title} |`);
2307
- }
2308
- } else {
2309
- lines.push("| ID (short) | Status | Type | Stage | Title | Observations | Started |");
2310
- lines.push("|------------|--------|------|-------|-------|-------------|---------|");
2311
- for (const b of branches) {
2312
- const shortId = b.id.slice(0, 8);
2313
- const title = b.title ? b.title.length > 40 ? b.title.slice(0, 40) + "..." : b.title : "-";
2314
- lines.push(`| ${shortId} | ${b.status} | ${b.branch_type} | ${b.arc_stage} | ${title} | ${b.observation_count} | ${b.started_at} |`);
2315
- }
2316
- }
2317
- return withNotifications(lines.join("\n"));
2318
- } catch (err) {
2319
- const message = err instanceof Error ? err.message : "Unknown error";
2320
- debug("mcp", "query_branches: error", { error: message });
2321
- return errorResponse(`query_branches error: ${message}`);
2322
- }
2323
- });
2324
- server.registerTool("show_branch", {
2325
- title: "Show Thought Branch",
2326
- description: "Show detailed view of a thought branch with observation timeline and arc stage annotations. Trace the full arc of a work unit.",
2327
- inputSchema: { branch_id: z.string().optional().describe("Branch ID to show. Omit for active branch.") }
2328
- }, async (args) => {
2329
- const projectHash = projectHashRef.current;
2330
- const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
2331
- try {
2332
- debug("mcp", "show_branch: request", { branch_id: args.branch_id });
2333
- let branch;
2334
- if (args.branch_id) {
2335
- branch = branchRepo.getBranch(args.branch_id);
2336
- if (!branch) return errorResponse(`Branch not found: ${args.branch_id}`);
2337
- } else {
2338
- branch = branchRepo.getActiveBranch();
2339
- if (!branch) return errorResponse("No active thought branch");
2340
- }
2341
- const verbosity = loadToolVerbosityConfig().level;
2342
- const branchTitle = branch.title ?? branch.id.slice(0, 12);
2343
- if (verbosity === 1) return withNotifications(`Showing "${branchTitle}"`);
2344
- const observations = branchRepo.getObservations(branch.id);
2345
- if (verbosity === 2) {
2346
- const lines = [];
2347
- lines.push(`## ${branchTitle}`);
2348
- lines.push(`**Status:** ${branch.status} | **Type:** ${branch.branch_type} | **Stage:** ${branch.arc_stage}`);
2349
- if (branch.summary) lines.push(branch.summary);
2350
- lines.push(`Observations: ${observations.length}`);
2351
- return withNotifications(lines.join("\n"));
2352
- }
2353
- const lines = [];
2354
- lines.push(`## Thought Branch: ${branchTitle}`);
2355
- lines.push(`**ID:** ${branch.id}`);
2356
- lines.push(`**Status:** ${branch.status}`);
2357
- lines.push(`**Type:** ${branch.branch_type}`);
2358
- lines.push(`**Arc Stage:** ${branch.arc_stage}`);
2359
- lines.push(`**Started:** ${branch.started_at}`);
2360
- if (branch.ended_at) lines.push(`**Ended:** ${branch.ended_at}`);
2361
- if (branch.trigger_source) lines.push(`**Trigger:** ${branch.trigger_source}`);
2362
- if (branch.linked_debug_path_id) lines.push(`**Linked Debug Path:** ${branch.linked_debug_path_id}`);
2363
- lines.push("");
2364
- const tools = Object.entries(branch.tool_pattern).sort(([, a], [, b]) => b - a);
2365
- if (tools.length > 0) {
2366
- lines.push("### Tool Usage");
2367
- for (const [tool, count] of tools) lines.push(`- ${tool}: ${count}`);
2368
- lines.push("");
2369
- }
2370
- if (branch.summary) {
2371
- lines.push("### Summary");
2372
- lines.push(branch.summary);
2373
- lines.push("");
2374
- }
2375
- lines.push(`### Observation Timeline (${observations.length})`);
2376
- for (const bo of observations) {
2377
- const obs = obsRepo.getById(bo.observation_id);
2378
- const content = obs ? obs.title ?? obs.content.slice(0, 100) : bo.observation_id.slice(0, 8);
2379
- const stageTag = bo.arc_stage_at_add ? `[${bo.arc_stage_at_add}]` : "";
2380
- const toolTag = bo.tool_name ? `(${bo.tool_name})` : "";
2381
- lines.push(`${bo.sequence_order}. ${stageTag} ${toolTag} ${content}`);
2382
- }
2383
- return withNotifications(lines.join("\n"));
2384
- } catch (err) {
2385
- const message = err instanceof Error ? err.message : "Unknown error";
2386
- debug("mcp", "show_branch: error", { error: message });
2387
- return errorResponse(`show_branch error: ${message}`);
2388
- }
2389
- });
2390
- server.registerTool("branch_summary", {
2391
- title: "Branch Activity Summary",
2392
- description: "Summary of recent work activity grouped by time window. Shows what was investigated, fixed, built, and where work left off.",
2393
- inputSchema: { hours: z.number().int().min(1).max(168).default(24).describe("Time window in hours (default 24)") }
2394
- }, async (args) => {
2395
- const projectHash = projectHashRef.current;
2396
- const withNotifications = (text) => textResponse(prependNotifications(notificationStore, projectHash, text));
2397
- try {
2398
- debug("mcp", "branch_summary: request", { hours: args.hours });
2399
- const branches = branchRepo.listRecentBranches(args.hours);
2400
- if (branches.length === 0) return withNotifications(`No work branches in the last ${args.hours} hours`);
2401
- const verbosity = loadToolVerbosityConfig().level;
2402
- if (verbosity === 1) return withNotifications(`${branches.length} branches in ${args.hours}h`);
2403
- const active = branches.filter((b) => b.status === "active");
2404
- const completed = branches.filter((b) => b.status === "completed");
2405
- const abandoned = branches.filter((b) => b.status === "abandoned");
2406
- const lines = [];
2407
- lines.push(`## Work Summary (last ${args.hours}h)`);
2408
- lines.push(`**Total branches:** ${branches.length}`);
2409
- lines.push("");
2410
- if (active.length > 0) {
2411
- lines.push("### Active");
2412
- for (const b of active) {
2413
- const title = b.title ?? b.id.slice(0, 8);
2414
- lines.push(verbosity === 2 ? `- ${title} (${b.branch_type})` : `- **${title}** (${b.branch_type}, ${b.arc_stage}) — ${b.observation_count} obs`);
2415
- }
2416
- lines.push("");
2417
- }
2418
- if (completed.length > 0) {
2419
- lines.push("### Completed");
2420
- for (const b of completed) {
2421
- const title = b.title ?? b.id.slice(0, 8);
2422
- const summary = b.summary ? `: ${b.summary.slice(0, 100)}` : "";
2423
- lines.push(verbosity === 2 ? `- ${title} (${b.branch_type})` : `- **${title}** (${b.branch_type})${summary}`);
2424
- }
2425
- lines.push("");
2426
- }
2427
- if (abandoned.length > 0) {
2428
- lines.push("### Abandoned");
2429
- for (const b of abandoned) {
2430
- const title = b.title ?? b.id.slice(0, 8);
2431
- lines.push(verbosity === 2 ? `- ${title} (${b.branch_type})` : `- **${title}** (${b.branch_type}) — ${b.observation_count} obs`);
2432
- }
2433
- lines.push("");
2434
- }
2435
- if (verbosity === 3) {
2436
- const allTools = {};
2437
- for (const b of branches) for (const [tool, count] of Object.entries(b.tool_pattern)) allTools[tool] = (allTools[tool] ?? 0) + count;
2438
- const toolEntries = Object.entries(allTools).sort(([, a], [, b]) => b - a);
2439
- if (toolEntries.length > 0) {
2440
- lines.push("### Tool Distribution");
2441
- for (const [tool, count] of toolEntries.slice(0, 10)) lines.push(`- ${tool}: ${count}`);
2442
- }
2443
- }
2444
- return withNotifications(lines.join("\n"));
2445
- } catch (err) {
2446
- const message = err instanceof Error ? err.message : "Unknown error";
2447
- debug("mcp", "branch_summary: error", { error: message });
2448
- return errorResponse(`branch_summary error: ${message}`);
2449
- }
2450
- });
2451
- }
2452
-
2453
- //#endregion
2454
- //#region src/branches/arc-detector.ts
2455
- const BUILTIN_CATEGORY = {
2456
- "Read": "investigation",
2457
- "Glob": "investigation",
2458
- "Grep": "investigation",
2459
- "WebSearch": "investigation",
2460
- "WebFetch": "investigation",
2461
- "Task": "investigation",
2462
- "AskUserQuestion": "investigation",
2463
- "Write": "write",
2464
- "Edit": "write",
2465
- "NotebookEdit": "write",
2466
- "Bash": "verification",
2467
- "EnterPlanMode": "planning",
2468
- "ExitPlanMode": "planning",
2469
- "TaskCreate": "planning",
2470
- "TaskUpdate": "planning",
2471
- "TaskList": "planning",
2472
- "TaskGet": "planning",
2473
- "Skill": "uncategorized"
2474
- };
2475
- /** Keywords matched against tool descriptions (case-insensitive). */
2476
- const DESCRIPTION_RULES = [
2477
- {
2478
- category: "planning",
2479
- keywords: /\b(plan|todo|task|roadmap|milestone|phase|design|architect)\b/i
2480
- },
2481
- {
2482
- category: "verification",
2483
- keywords: /\b(run|test|build|execute|evaluate|validate|verify|check|assert|lint|compile)\b/i
2484
- },
2485
- {
2486
- category: "write",
2487
- keywords: /\b(write|edit|create|update|save|upload|modify|delete|remove|fill|type|click|select|drag|press|submit|install|deploy|push|commit|insert|drop|replace)\b/i
2488
- },
2489
- {
2490
- category: "investigation",
2491
- keywords: /\b(read|search|query|find|list|get|fetch|browse|snapshot|screenshot|inspect|show|view|discover|status|stats|navigate|hover|recall|monitor|log|trace|debug|profile|measure|analyze|explore)\b/i
2492
- }
2493
- ];
2494
- /**
2495
- * Classify a tool from its description text.
2496
- * Returns null if no confident match.
2497
- */
2498
- function classifyFromDescription(description) {
2499
- for (const rule of DESCRIPTION_RULES) if (rule.keywords.test(description)) return rule.category;
2500
- return null;
2501
- }
2502
- const NAME_RULES = [
2503
- {
2504
- category: "planning",
2505
- pattern: /\b(plan|todo|task|roadmap|phase|milestone)\b/i
2506
- },
2507
- {
2508
- category: "verification",
2509
- pattern: /\b(run|test|build|exec|evaluate|validate|check|verify)\b/i
2510
- },
2511
- {
2512
- category: "write",
2513
- pattern: /\b(write|edit|create|update|save|upload|fill|type|click|select|drag|press|install)\b/i
2514
- },
2515
- {
2516
- category: "investigation",
2517
- pattern: /\b(search|query|find|list|get|read|fetch|browse|snapshot|screenshot|inspect|show|view|recall|discover|status|stats|console|network|navigate|tabs|hover)\b/i
2518
- }
2519
- ];
2520
- function classifyFromName(toolName) {
2521
- const actionPart = toolName.includes("__") ? toolName.substring(toolName.lastIndexOf("__") + 2) : toolName;
2522
- for (const rule of NAME_RULES) if (rule.pattern.test(actionPart)) return rule.category;
2523
- if (toolName.includes("laminark")) return "investigation";
2524
- return "uncategorized";
2525
- }
2526
- const classificationCache = /* @__PURE__ */ new Map();
2527
- let lastRegistryCount = -1;
2528
- /**
2529
- * Re-reads the tool_registry table and classifies every tool by its
2530
- * description. Only rescans when the registry row count has changed.
2531
- *
2532
- * Call on startup and periodically (e.g., during BranchTracker maintenance).
2533
- */
2534
- function primeFromRegistry(db, projectHash) {
2535
- try {
2536
- const currentCount = db.prepare("SELECT COUNT(*) AS cnt FROM tool_registry").get()?.cnt ?? 0;
2537
- if (currentCount === lastRegistryCount && lastRegistryCount >= 0) return;
2538
- const rows = db.prepare(`
2539
- SELECT name, description FROM tool_registry
2540
- WHERE status = 'active'
2541
- AND (scope = 'global' OR project_hash IS NULL OR project_hash = ?)
2542
- `).all(projectHash);
2543
- let primed = 0;
2544
- for (const row of rows) {
2545
- if (BUILTIN_CATEGORY[row.name]) continue;
2546
- let category = null;
2547
- if (row.description) category = classifyFromDescription(row.description);
2548
- if (!category) category = classifyFromName(row.name);
2549
- classificationCache.set(row.name, category);
2550
- primed++;
2551
- }
2552
- lastRegistryCount = currentCount;
2553
- debug("branches", "Arc detector cache primed from registry", {
2554
- registryTools: rows.length,
2555
- primed
2556
- });
2557
- } catch {}
2558
- }
2559
- /**
2560
- * Classify any tool name into an arc category.
2561
- *
2562
- * Priority: built-in table > registry-primed cache > name-pattern fallback.
2563
- */
2564
- function classifyTool(toolName) {
2565
- const cached = classificationCache.get(toolName);
2566
- if (cached) return cached;
2567
- const builtin = BUILTIN_CATEGORY[toolName];
2568
- if (builtin) {
2569
- classificationCache.set(toolName, builtin);
2570
- return builtin;
2571
- }
2572
- const fromName = classifyFromName(toolName);
2573
- classificationCache.set(toolName, fromName);
2574
- return fromName;
2575
- }
2576
- /**
2577
- * Infers the current arc stage from tool usage pattern counts.
2578
- *
2579
- * Handles all tool types: builtins, MCP tools, plugins, skills, slash commands.
2580
- * Uncategorized tools are excluded from ratio calculations so they don't
2581
- * dilute the signal from known tools.
2582
- *
2583
- * @param toolPattern - Map of tool name to usage count within the branch
2584
- * @param classification - Optional dominant observation classification
2585
- * @returns The inferred arc stage
2586
- */
2587
- function inferArcStage(toolPattern, classification) {
2588
- let investigationCount = 0;
2589
- let writeCount = 0;
2590
- let verificationCount = 0;
2591
- let planningCount = 0;
2592
- let categorizedCount = 0;
2593
- for (const [tool, count] of Object.entries(toolPattern)) switch (classifyTool(tool)) {
2594
- case "investigation":
2595
- investigationCount += count;
2596
- categorizedCount += count;
2597
- break;
2598
- case "write":
2599
- writeCount += count;
2600
- categorizedCount += count;
2601
- break;
2602
- case "verification":
2603
- verificationCount += count;
2604
- categorizedCount += count;
2605
- break;
2606
- case "planning":
2607
- planningCount += count;
2608
- categorizedCount += count;
2609
- break;
2610
- case "uncategorized": break;
2611
- }
2612
- if (categorizedCount === 0) return "investigation";
2613
- if (verificationCount > 0 && writeCount > 0) {
2614
- if (verificationCount / categorizedCount > .2) return "verification";
2615
- }
2616
- if (writeCount / categorizedCount > .4) return "execution";
2617
- if (planningCount > 0) {
2618
- if (planningCount / categorizedCount > .1) return "planning";
2619
- }
2620
- if (classification === "problem" && writeCount > 0 && investigationCount > 0) return "diagnosis";
2621
- return "investigation";
2622
- }
2623
-
2624
- //#endregion
2625
- //#region src/config/haiku-config.ts
2626
- function loadHaikuConfig() {
2627
- return {
2628
- model: "claude-haiku-4-5-20251001",
2629
- maxTokensPerCall: 1024
2630
- };
2631
- }
2632
-
2633
- //#endregion
2634
- //#region src/intelligence/haiku-client.ts
2635
- /**
2636
- * Shared Haiku client using Claude Agent SDK V2 session.
2637
- *
2638
- * Routes Haiku calls through the user's Claude Code subscription
2639
- * instead of requiring a separate API key. Uses a persistent session
2640
- * to avoid 12s cold-start overhead on sequential calls.
2641
- *
2642
- * Provides the core infrastructure for all Haiku agent modules:
2643
- * - callHaiku() helper for structured prompt/response calls
2644
- * - extractJsonFromResponse() for defensive JSON parsing
2645
- * - Session reuse across batch processing cycles
2646
- */
2647
- let _session = null;
2648
- function getOrCreateSession() {
2649
- if (!_session) _session = unstable_v2_createSession({
2650
- model: loadHaikuConfig().model,
2651
- permissionMode: "bypassPermissions",
2652
- allowedTools: []
2653
- });
2654
- return _session;
2655
- }
2656
- /**
2657
- * Returns whether Haiku enrichment is available.
2658
- * Always true with subscription auth -- no API key check needed.
2659
- */
2660
- function isHaikuEnabled() {
2661
- return true;
2662
- }
2663
- /**
2664
- * Calls Haiku with a system prompt and user content.
2665
- * Returns the text content from the response.
2666
- *
2667
- * Uses a persistent V2 session to avoid cold-start overhead on sequential calls.
2668
- * System prompt is embedded in the user message since session-level systemPrompt
2669
- * is set at creation time and we need different prompts per agent.
2670
- *
2671
- * @param systemPrompt - Instructions for the model
2672
- * @param userContent - The content to process
2673
- * @param _maxTokens - Kept for signature compatibility (unused -- Agent SDK constrains output via prompts)
2674
- * @throws Error if the Haiku call fails or session expires
2675
- */
2676
- async function callHaiku(systemPrompt, userContent, _maxTokens) {
2677
- const session = getOrCreateSession();
2678
- const fullPrompt = `<instructions>\n${systemPrompt}\n</instructions>\n\n${userContent}`;
2679
- try {
2680
- await session.send(fullPrompt);
2681
- for await (const msg of session.stream()) if (msg.type === "result") {
2682
- if (msg.subtype === "success") return msg.result;
2683
- const errorMsg = ("errors" in msg ? msg.errors : void 0)?.join(", ") ?? msg.subtype;
2684
- throw new Error(`Haiku call failed: ${errorMsg}`);
2685
- }
2686
- return "";
2687
- } catch (error) {
2688
- try {
2689
- _session?.close();
2690
- } catch {}
2691
- _session = null;
2692
- throw error;
2693
- }
2694
- }
2695
- /**
2696
- * Defensive JSON extraction from Haiku response text.
2697
- *
2698
- * Handles common LLM response quirks:
2699
- * - Markdown code fences (```json ... ```)
2700
- * - Explanatory text before/after JSON
2701
- * - Both array and object JSON shapes
2702
- *
2703
- * @throws Error if no JSON structure found in text
2704
- */
2705
- function extractJsonFromResponse(text) {
2706
- const cleaned = text.replace(/```json\s*/g, "").replace(/```\s*/g, "");
2707
- const arrayMatch = cleaned.match(/\[[\s\S]*\]/);
2708
- if (arrayMatch) return JSON.parse(arrayMatch[0]);
2709
- const objMatch = cleaned.match(/\{[\s\S]*\}/);
2710
- if (objMatch) return JSON.parse(objMatch[0]);
2711
- throw new Error("No JSON found in Haiku response");
2712
- }
2713
-
2714
- //#endregion
2715
- //#region src/branches/branch-classifier-agent.ts
2716
- /**
2717
- * Haiku agent for classifying thought branch type and generating title/summary.
2718
- *
2719
- * Uses a single Haiku call to determine:
2720
- * 1. Branch type (investigation, bug_fix, feature, refactor, research)
2721
- * 2. A concise title for the branch
2722
- * 3. An optional summary (for completed branches)
2723
- *
2724
- * Follows the same pattern as haiku-classifier-agent.ts.
2725
- */
2726
- const ClassifyBranchSchema = z.object({
2727
- branch_type: z.enum([
2728
- "investigation",
2729
- "bug_fix",
2730
- "feature",
2731
- "refactor",
2732
- "research"
2733
- ]),
2734
- title: z.string().max(100)
2735
- });
2736
- const SummarizeBranchSchema = z.object({ summary: z.string().max(500) });
2737
- const CLASSIFY_PROMPT = `You classify developer work branches for a knowledge management system.
2738
-
2739
- Given a sequence of observations from a work session, determine:
2740
- 1. branch_type: What kind of work is this?
2741
- - "investigation": Exploring code, reading docs, understanding behavior
2742
- - "bug_fix": Fixing an error, test failure, or unexpected behavior
2743
- - "feature": Building new functionality
2744
- - "refactor": Restructuring existing code without changing behavior
2745
- - "research": Looking up external resources, comparing approaches
2746
-
2747
- 2. title: A concise title (3-8 words) describing the work unit. Use imperative form.
2748
- Examples: "Fix auth token refresh", "Add branch detection system", "Investigate memory leak"
2749
-
2750
- Return JSON: {"branch_type": "...", "title": "..."}
2751
- No markdown, no explanation, ONLY the JSON object.`;
2752
- const SUMMARIZE_PROMPT = `You summarize completed developer work branches for a knowledge management system.
2753
-
2754
- Given a sequence of observations from a completed work branch, write a concise summary (1-3 sentences) that captures:
2755
- - What was the goal
2756
- - What was done
2757
- - What was the outcome
2758
-
2759
- Return JSON: {"summary": "..."}
2760
- No markdown, no explanation, ONLY the JSON object.`;
2761
- /**
2762
- * Classifies a branch type and generates a title from observation content.
2763
- */
2764
- async function classifyBranchWithHaiku(observationTexts, toolPattern) {
2765
- const parsed = extractJsonFromResponse(await callHaiku(CLASSIFY_PROMPT, [
2766
- `Tool usage: ${Object.entries(toolPattern).sort(([, a], [, b]) => b - a).map(([tool, count]) => `${tool}: ${count}`).join(", ")}`,
2767
- "",
2768
- "Observations:",
2769
- ...observationTexts.slice(0, 10).map((t, i) => `${i + 1}. ${t.slice(0, 200)}`)
2770
- ].join("\n"), 256));
2771
- return ClassifyBranchSchema.parse(parsed);
2772
- }
2773
- /**
2774
- * Generates a completion summary for a finished branch.
2775
- */
2776
- async function summarizeBranchWithHaiku(title, branchType, observationTexts) {
2777
- const parsed = extractJsonFromResponse(await callHaiku(SUMMARIZE_PROMPT, [
2778
- `Branch: ${title} (${branchType})`,
2779
- "",
2780
- "Observations:",
2781
- ...observationTexts.slice(0, 15).map((t, i) => `${i + 1}. ${t.slice(0, 200)}`)
2782
- ].join("\n"), 256));
2783
- return SummarizeBranchSchema.parse(parsed);
2784
- }
2785
-
2786
- //#endregion
2787
- //#region src/branches/branch-tracker.ts
2788
- const TIME_GAP_MS = 900 * 1e3;
2789
- var BranchTracker = class {
2790
- state = "idle";
2791
- activeBranchId = null;
2792
- activeProjectHash = null;
2793
- activeSessionId = null;
2794
- lastObservationTime = 0;
2795
- toolPattern = {};
2796
- repo;
2797
- db;
2798
- projectHash;
2799
- constructor(repo, db, projectHash) {
2800
- this.repo = repo;
2801
- this.db = db;
2802
- this.projectHash = projectHash;
2803
- primeFromRegistry(db, projectHash);
2804
- const activeBranch = repo.findRecentActiveBranch();
2805
- if (activeBranch) {
2806
- this.state = "tracking";
2807
- this.activeBranchId = activeBranch.id;
2808
- this.activeProjectHash = activeBranch.project_hash;
2809
- this.activeSessionId = activeBranch.session_id;
2810
- this.toolPattern = activeBranch.tool_pattern;
2811
- this.lastObservationTime = new Date(activeBranch.started_at).getTime();
2812
- debug("branches", "Recovered active branch from DB", { branchId: activeBranch.id });
2813
- }
2814
- }
2815
- /**
2816
- * Process a new observation through the boundary detection state machine.
2817
- * Called from HaikuProcessor after classification (Step 1.6).
2818
- */
2819
- processObservation(obs) {
2820
- const now = Date.now();
2821
- const obsTime = new Date(obs.createdAt).getTime();
2822
- const toolName = this.extractToolName(obs.source);
2823
- const boundary = this.detectBoundary(obs, obsTime);
2824
- if (boundary) {
2825
- if (this.state === "tracking" && this.activeBranchId) this.completeBranch();
2826
- this.startBranch(boundary, obs);
2827
- } else if (this.state === "idle") this.startBranch("session_start", obs);
2828
- if (this.activeBranchId) {
2829
- const arcStage = inferArcStage(this.toolPattern, obs.classification);
2830
- if (toolName) {
2831
- this.toolPattern[toolName] = (this.toolPattern[toolName] ?? 0) + 1;
2832
- this.repo.updateToolPattern(this.activeBranchId, this.toolPattern);
2833
- }
2834
- this.repo.addObservation(this.activeBranchId, obs.id, toolName, arcStage);
2835
- const newStage = inferArcStage(this.toolPattern, obs.classification);
2836
- this.repo.updateArcStage(this.activeBranchId, newStage);
2837
- }
2838
- this.lastObservationTime = obsTime || now;
2839
- this.activeProjectHash = obs.projectHash;
2840
- this.activeSessionId = obs.sessionId ?? this.activeSessionId;
2841
- }
2842
- /**
2843
- * Notify the tracker of a topic shift (from TopicShiftHandler).
2844
- */
2845
- onTopicShift(observationId) {
2846
- if (this.state === "tracking" && this.activeBranchId) {
2847
- this.completeBranch();
2848
- debug("branches", "Topic shift boundary detected", { observationId });
2849
- }
2850
- }
2851
- /**
2852
- * Link the active branch to a debug path (when PathTracker activates).
2853
- */
2854
- linkDebugPath(debugPathId) {
2855
- if (this.activeBranchId) {
2856
- this.repo.linkDebugPath(this.activeBranchId, debugPathId);
2857
- debug("branches", "Linked debug path to branch", {
2858
- branchId: this.activeBranchId,
2859
- debugPathId
2860
- });
2861
- }
2862
- }
2863
- /**
2864
- * Get the active branch ID (for external callers).
2865
- */
2866
- getActiveBranchId() {
2867
- return this.activeBranchId;
2868
- }
2869
- /**
2870
- * Run periodic maintenance tasks:
2871
- * - Classify branches with 3+ observations via Haiku
2872
- * - Generate summaries for recently completed branches
2873
- * - Auto-abandon stale branches (>24h)
2874
- * - Link branches to debug paths
2875
- */
2876
- async runMaintenance() {
2877
- try {
2878
- primeFromRegistry(this.db, this.projectHash);
2879
- const stale = this.repo.findStaleBranches();
2880
- for (const branch of stale) {
2881
- this.repo.abandonBranch(branch.id);
2882
- if (this.activeBranchId === branch.id) {
2883
- this.state = "idle";
2884
- this.activeBranchId = null;
2885
- this.toolPattern = {};
2886
- }
2887
- debug("branches", "Auto-abandoned stale branch", { branchId: branch.id });
2888
- }
2889
- if (isHaikuEnabled()) {
2890
- const unclassified = this.repo.findUnclassifiedBranches(3);
2891
- for (const branch of unclassified) try {
2892
- const observations = this.repo.getObservations(branch.id);
2893
- const obsRepo = new ObservationRepository(this.db, branch.project_hash);
2894
- const texts = observations.map((bo) => {
2895
- const obs = obsRepo.getById(bo.observation_id);
2896
- return obs ? obs.title ? `${obs.title}: ${obs.content}` : obs.content : null;
2897
- }).filter((t) => t !== null);
2898
- if (texts.length === 0) continue;
2899
- const result = await classifyBranchWithHaiku(texts, branch.tool_pattern);
2900
- this.repo.updateClassification(branch.id, result.branch_type, result.title);
2901
- debug("branches", "Branch classified", {
2902
- branchId: branch.id,
2903
- type: result.branch_type,
2904
- title: result.title
2905
- });
2906
- } catch (err) {
2907
- const msg = err instanceof Error ? err.message : String(err);
2908
- debug("branches", "Branch classification failed (non-fatal)", {
2909
- branchId: branch.id,
2910
- error: msg
2911
- });
2912
- }
2913
- const unsummarized = this.repo.findRecentCompletedUnsummarized(2);
2914
- for (const branch of unsummarized) try {
2915
- const observations = this.repo.getObservations(branch.id);
2916
- const obsRepo = new ObservationRepository(this.db, branch.project_hash);
2917
- const texts = observations.map((bo) => {
2918
- const obs = obsRepo.getById(bo.observation_id);
2919
- return obs ? obs.title ? `${obs.title}: ${obs.content}` : obs.content : null;
2920
- }).filter((t) => t !== null);
2921
- if (texts.length === 0) continue;
2922
- const result = await summarizeBranchWithHaiku(branch.title ?? "Untitled", branch.branch_type, texts);
2923
- this.repo.updateSummary(branch.id, result.summary);
2924
- debug("branches", "Branch summarized", { branchId: branch.id });
2925
- } catch (err) {
2926
- const msg = err instanceof Error ? err.message : String(err);
2927
- debug("branches", "Branch summarization failed (non-fatal)", {
2928
- branchId: branch.id,
2929
- error: msg
2930
- });
2931
- }
2932
- }
2933
- } catch (err) {
2934
- debug("branches", "Maintenance error (non-fatal)", { error: err instanceof Error ? err.message : String(err) });
2935
- }
2936
- }
2937
- detectBoundary(obs, obsTime) {
2938
- if (this.activeProjectHash && obs.projectHash !== this.activeProjectHash) return "project_switch";
2939
- if (this.activeSessionId && obs.sessionId && obs.sessionId !== this.activeSessionId) return "session_start";
2940
- if (this.lastObservationTime > 0) {
2941
- if (obsTime - this.lastObservationTime > TIME_GAP_MS) return "time_gap";
2942
- }
2943
- return null;
2944
- }
2945
- startBranch(triggerSource, obs) {
2946
- const branch = this.repo.createBranch(obs.sessionId ?? null, triggerSource, obs.id);
2947
- this.state = "tracking";
2948
- this.activeBranchId = branch.id;
2949
- this.toolPattern = {};
2950
- debug("branches", "New branch started", {
2951
- branchId: branch.id,
2952
- trigger: triggerSource
2953
- });
2954
- }
2955
- completeBranch() {
2956
- if (!this.activeBranchId) return;
2957
- this.repo.completeBranch(this.activeBranchId);
2958
- debug("branches", "Branch completed", { branchId: this.activeBranchId });
2959
- this.state = "idle";
2960
- this.activeBranchId = null;
2961
- this.toolPattern = {};
2962
- }
2963
- extractToolName(source) {
2964
- if (source.startsWith("hook:")) return source.slice(5);
2965
- if (source.startsWith("mcp:")) return source.slice(4);
2966
- return null;
2967
- }
2968
- };
1816
+ lines.push("## Debug Paths");
1817
+ lines.push("");
1818
+ lines.push("| ID (short) | Status | Trigger | Started | Resolved |");
1819
+ lines.push("|------------|--------|---------|---------|----------|");
1820
+ for (const p of paths) {
1821
+ const shortId = p.id.slice(0, 8);
1822
+ const trigger = p.trigger_summary.length > 50 ? p.trigger_summary.slice(0, 50) + "..." : p.trigger_summary;
1823
+ const resolved = p.resolved_at ?? "-";
1824
+ lines.push(`| ${shortId} | ${p.status} | ${trigger} | ${p.started_at} | ${resolved} |`);
1825
+ }
1826
+ return withNotifications(lines.join("\n"));
1827
+ } catch (err) {
1828
+ const message = err instanceof Error ? err.message : "Unknown error";
1829
+ debug("mcp", "path_list: error", { error: message });
1830
+ return errorResponse(`path_list error: ${message}`);
1831
+ }
1832
+ });
1833
+ }
2969
1834
 
2970
1835
  //#endregion
2971
1836
  //#region src/analysis/worker-bridge.ts
@@ -4701,6 +3566,96 @@ var CurationAgent = class {
4701
3566
  }
4702
3567
  };
4703
3568
 
3569
+ //#endregion
3570
+ //#region src/config/haiku-config.ts
3571
+ function loadHaikuConfig() {
3572
+ return {
3573
+ model: "claude-haiku-4-5-20251001",
3574
+ maxTokensPerCall: 1024
3575
+ };
3576
+ }
3577
+
3578
+ //#endregion
3579
+ //#region src/intelligence/haiku-client.ts
3580
+ /**
3581
+ * Shared Haiku client using Claude Agent SDK V2 session.
3582
+ *
3583
+ * Routes Haiku calls through the user's Claude Code subscription
3584
+ * instead of requiring a separate API key. Uses a persistent session
3585
+ * to avoid 12s cold-start overhead on sequential calls.
3586
+ *
3587
+ * Provides the core infrastructure for all Haiku agent modules:
3588
+ * - callHaiku() helper for structured prompt/response calls
3589
+ * - extractJsonFromResponse() for defensive JSON parsing
3590
+ * - Session reuse across batch processing cycles
3591
+ */
3592
+ let _session = null;
3593
+ function getOrCreateSession() {
3594
+ if (!_session) _session = unstable_v2_createSession({
3595
+ model: loadHaikuConfig().model,
3596
+ permissionMode: "bypassPermissions",
3597
+ allowedTools: []
3598
+ });
3599
+ return _session;
3600
+ }
3601
+ /**
3602
+ * Returns whether Haiku enrichment is available.
3603
+ * Always true with subscription auth -- no API key check needed.
3604
+ */
3605
+ function isHaikuEnabled() {
3606
+ return true;
3607
+ }
3608
+ /**
3609
+ * Calls Haiku with a system prompt and user content.
3610
+ * Returns the text content from the response.
3611
+ *
3612
+ * Uses a persistent V2 session to avoid cold-start overhead on sequential calls.
3613
+ * System prompt is embedded in the user message since session-level systemPrompt
3614
+ * is set at creation time and we need different prompts per agent.
3615
+ *
3616
+ * @param systemPrompt - Instructions for the model
3617
+ * @param userContent - The content to process
3618
+ * @param _maxTokens - Kept for signature compatibility (unused -- Agent SDK constrains output via prompts)
3619
+ * @throws Error if the Haiku call fails or session expires
3620
+ */
3621
+ async function callHaiku(systemPrompt, userContent, _maxTokens) {
3622
+ const session = getOrCreateSession();
3623
+ const fullPrompt = `<instructions>\n${systemPrompt}\n</instructions>\n\n${userContent}`;
3624
+ try {
3625
+ await session.send(fullPrompt);
3626
+ for await (const msg of session.stream()) if (msg.type === "result") {
3627
+ if (msg.subtype === "success") return msg.result;
3628
+ const errorMsg = ("errors" in msg ? msg.errors : void 0)?.join(", ") ?? msg.subtype;
3629
+ throw new Error(`Haiku call failed: ${errorMsg}`);
3630
+ }
3631
+ return "";
3632
+ } catch (error) {
3633
+ try {
3634
+ _session?.close();
3635
+ } catch {}
3636
+ _session = null;
3637
+ throw error;
3638
+ }
3639
+ }
3640
+ /**
3641
+ * Defensive JSON extraction from Haiku response text.
3642
+ *
3643
+ * Handles common LLM response quirks:
3644
+ * - Markdown code fences (```json ... ```)
3645
+ * - Explanatory text before/after JSON
3646
+ * - Both array and object JSON shapes
3647
+ *
3648
+ * @throws Error if no JSON structure found in text
3649
+ */
3650
+ function extractJsonFromResponse(text) {
3651
+ const cleaned = text.replace(/```json\s*/g, "").replace(/```\s*/g, "");
3652
+ const arrayMatch = cleaned.match(/\[[\s\S]*\]/);
3653
+ if (arrayMatch) return JSON.parse(arrayMatch[0]);
3654
+ const objMatch = cleaned.match(/\{[\s\S]*\}/);
3655
+ if (objMatch) return JSON.parse(objMatch[0]);
3656
+ throw new Error("No JSON found in Haiku response");
3657
+ }
3658
+
4704
3659
  //#endregion
4705
3660
  //#region src/intelligence/haiku-classifier-agent.ts
4706
3661
  /**
@@ -4751,10 +3706,10 @@ For each observation, determine:
4751
3706
  - "problem": error, bug, failure, or obstacle encountered
4752
3707
  - "solution": fix, resolution, workaround, or decision that resolved something
4753
3708
 
4754
- 3. debug_signal (always, even for noise): Is this related to ACTIVE debugging (the developer hit an actual error)?
4755
- - is_error: Did an actual error/failure OCCUR in this observation? An error message, stack trace, test failure, or build failure that happened RIGHT NOW. NOT research about errors — searching for "reconnection problems" or reading docs about error handling is NOT is_error. The tool itself must have failed or produced an error.
3709
+ 3. debug_signal (always, even for noise): Is this related to debugging?
3710
+ - is_error: Does this contain an error message, test failure, build failure, or exception?
4756
3711
  - is_resolution: Does this indicate a successful fix, passing test, or resolved error?
4757
- - waypoint_hint: If debug-related, what type? "error" (an actual error occurred), "attempt" (trying a fix), "failure" (fix didn't work), "success" (something passed), "pivot" (changing approach), "revert" (undoing a change), "discovery" (learned something new), "resolution" (final fix). null if not debug-related. WebSearch/WebFetch/AskUserQuestion are typically "discovery" or null, NOT "error".
3712
+ - waypoint_hint: If debug-related, what type? "error" (hit an error), "attempt" (trying a fix), "failure" (fix didn't work), "success" (something passed), "pivot" (changing approach), "revert" (undoing a change), "discovery" (learned something), "resolution" (final fix). null if not debug-related.
4758
3713
  - confidence: 0.0-1.0 how confident this is debug activity
4759
3714
 
4760
3715
  Return JSON: {"signal": "noise"|"signal", "classification": "discovery"|"problem"|"solution"|null, "reason": "brief", "debug_signal": {"is_error": bool, "is_resolution": bool, "waypoint_hint": "type"|null, "confidence": 0.0-1.0}|null}
@@ -5162,7 +4117,6 @@ var HaikuProcessor = class {
5162
4117
  batchSize;
5163
4118
  concurrency;
5164
4119
  pathTracker;
5165
- branchTracker;
5166
4120
  timer = null;
5167
4121
  constructor(db, projectHash, opts) {
5168
4122
  this.db = db;
@@ -5171,7 +4125,6 @@ var HaikuProcessor = class {
5171
4125
  this.batchSize = opts?.batchSize ?? 10;
5172
4126
  this.concurrency = opts?.concurrency ?? 3;
5173
4127
  this.pathTracker = opts?.pathTracker ?? null;
5174
- this.branchTracker = opts?.branchTracker ?? null;
5175
4128
  }
5176
4129
  start() {
5177
4130
  if (this.timer) return;
@@ -5195,30 +4148,16 @@ var HaikuProcessor = class {
5195
4148
  }
5196
4149
  async processOnce() {
5197
4150
  if (!isHaikuEnabled()) return;
5198
- const unclassified = ObservationRepository.listAllUnclassified(this.db, this.batchSize);
4151
+ const repo = new ObservationRepository(this.db, this.projectHash);
4152
+ const unclassified = repo.listUnclassified(this.batchSize);
5199
4153
  if (unclassified.length === 0) return;
5200
4154
  debug("haiku", "Processing unclassified observations", { count: unclassified.length });
5201
- const byProject = /* @__PURE__ */ new Map();
5202
- for (const obs of unclassified) {
5203
- const hash = obs.projectHash;
5204
- if (!byProject.has(hash)) byProject.set(hash, []);
5205
- byProject.get(hash).push(obs);
5206
- }
5207
- for (const [hash, projectObs] of byProject) {
5208
- const repo = new ObservationRepository(this.db, hash);
5209
- for (let i = 0; i < projectObs.length; i += this.concurrency) {
5210
- const batch = projectObs.slice(i, i + this.concurrency);
5211
- await Promise.all(batch.map((obs) => this.processOne(obs, repo, hash)));
5212
- }
5213
- }
5214
- if (this.branchTracker) try {
5215
- await this.branchTracker.runMaintenance();
5216
- } catch (err) {
5217
- debug("haiku", "Branch maintenance error (non-fatal)", { error: err instanceof Error ? err.message : String(err) });
4155
+ for (let i = 0; i < unclassified.length; i += this.concurrency) {
4156
+ const batch = unclassified.slice(i, i + this.concurrency);
4157
+ await Promise.all(batch.map((obs) => this.processOne(obs, repo)));
5218
4158
  }
5219
4159
  }
5220
- async processOne(obs, repo, obsProjectHash) {
5221
- const projectHash = obsProjectHash ?? this.projectHash;
4160
+ async processOne(obs, repo) {
5222
4161
  try {
5223
4162
  let classification;
5224
4163
  try {
@@ -5232,23 +4171,6 @@ var HaikuProcessor = class {
5232
4171
  error: msg
5233
4172
  });
5234
4173
  }
5235
- if (this.branchTracker) try {
5236
- this.branchTracker.processObservation({
5237
- id: obs.id,
5238
- content: obs.content,
5239
- source: obs.source,
5240
- projectHash: obsProjectHash ?? this.projectHash,
5241
- sessionId: void 0,
5242
- classification: result.classification,
5243
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
5244
- });
5245
- } catch (branchErr) {
5246
- const msg = branchErr instanceof Error ? branchErr.message : String(branchErr);
5247
- debug("haiku", "Branch tracking failed (non-fatal)", {
5248
- id: obs.id,
5249
- error: msg
5250
- });
5251
- }
5252
4174
  if (result.signal === "noise") {
5253
4175
  repo.updateClassification(obs.id, "noise");
5254
4176
  repo.softDelete(obs.id);
@@ -5290,7 +4212,7 @@ var HaikuProcessor = class {
5290
4212
  name: entity.name,
5291
4213
  metadata: { confidence: entity.confidence },
5292
4214
  observation_ids: [String(obs.id)],
5293
- project_hash: projectHash
4215
+ project_hash: this.projectHash
5294
4216
  });
5295
4217
  persistedNodes.push(node);
5296
4218
  } catch {
@@ -5302,8 +4224,7 @@ var HaikuProcessor = class {
5302
4224
  label: node.name,
5303
4225
  type: node.type,
5304
4226
  observationCount: 1,
5305
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5306
- projectHash
4227
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5307
4228
  });
5308
4229
  debug("haiku", "Entities persisted", {
5309
4230
  id: obs.id,
@@ -5328,7 +4249,7 @@ var HaikuProcessor = class {
5328
4249
  type: rel.type,
5329
4250
  weight: rel.confidence,
5330
4251
  metadata: { source: "haiku" },
5331
- project_hash: projectHash
4252
+ project_hash: this.projectHash
5332
4253
  });
5333
4254
  affectedNodeIds.add(sourceNode.id);
5334
4255
  affectedNodeIds.add(targetNode.id);
@@ -5725,7 +4646,7 @@ apiRoutes.get("/graph", (c) => {
5725
4646
  edgeRows = [];
5726
4647
  }
5727
4648
  const nodeIdSet = new Set(nodes.map((n) => n.id));
5728
- const edges = edgeRows.filter((e) => nodeIdSet.has(e.source_id) && nodeIdSet.has(e.target_id)).map((row) => ({
4649
+ const edges = (typeFilter ? edgeRows.filter((e) => nodeIdSet.has(e.source_id) && nodeIdSet.has(e.target_id)) : edgeRows).map((row) => ({
5729
4650
  id: row.id,
5730
4651
  source: row.source_id,
5731
4652
  target: row.target_id,
@@ -6303,236 +5224,6 @@ apiRoutes.get("/paths/:id", (c) => {
6303
5224
  }
6304
5225
  });
6305
5226
  /**
6306
- * GET /api/tools
6307
- *
6308
- * Returns all tools from tool_registry with usage stats.
6309
- */
6310
- apiRoutes.get("/tools", (c) => {
6311
- const db = getDb$1(c);
6312
- let tools = [];
6313
- try {
6314
- tools = db.prepare(`
6315
- SELECT id, name, tool_type, scope, status, usage_count, server_name, description, last_used_at, discovered_at
6316
- FROM tool_registry
6317
- ORDER BY usage_count DESC, discovered_at DESC
6318
- `).all();
6319
- } catch {}
6320
- return c.json({ tools: tools.map((t) => ({
6321
- id: t.id,
6322
- name: t.name,
6323
- toolType: t.tool_type,
6324
- scope: t.scope,
6325
- status: t.status,
6326
- usageCount: t.usage_count,
6327
- serverName: t.server_name,
6328
- description: t.description,
6329
- lastUsedAt: t.last_used_at,
6330
- discoveredAt: t.discovered_at
6331
- })) });
6332
- });
6333
- /**
6334
- * GET /api/tools/flows
6335
- *
6336
- * Returns edges for the tool topology graph:
6337
- * 1. Pre-computed routing_patterns (preceding_tools -> target_tool)
6338
- * 2. Pairwise co-occurrence from tool_usage_events session sequences
6339
- */
6340
- apiRoutes.get("/tools/flows", (c) => {
6341
- const db = getDb$1(c);
6342
- const projectFilter = getProjectHash$2(c);
6343
- const edges = [];
6344
- const edgeKey = /* @__PURE__ */ new Set();
6345
- try {
6346
- let sql = "SELECT target_tool, preceding_tools, frequency FROM routing_patterns";
6347
- const params = [];
6348
- if (projectFilter) {
6349
- sql += " WHERE project_hash = ?";
6350
- params.push(projectFilter);
6351
- }
6352
- sql += " ORDER BY frequency DESC LIMIT 200";
6353
- const rows = db.prepare(sql).all(...params);
6354
- for (const row of rows) {
6355
- let preceding;
6356
- try {
6357
- preceding = JSON.parse(row.preceding_tools);
6358
- } catch {
6359
- preceding = [];
6360
- }
6361
- for (const src of preceding) {
6362
- const key = src + "->" + row.target_tool;
6363
- if (!edgeKey.has(key)) {
6364
- edgeKey.add(key);
6365
- edges.push({
6366
- source: src,
6367
- target: row.target_tool,
6368
- frequency: row.frequency,
6369
- edgeType: "pattern"
6370
- });
6371
- }
6372
- }
6373
- }
6374
- } catch {}
6375
- try {
6376
- let sql = `
6377
- SELECT session_id, tool_name, created_at
6378
- FROM tool_usage_events
6379
- WHERE session_id IS NOT NULL
6380
- `;
6381
- const params = [];
6382
- if (projectFilter) {
6383
- sql += " AND project_hash = ?";
6384
- params.push(projectFilter);
6385
- }
6386
- sql += " ORDER BY session_id, created_at ASC LIMIT 5000";
6387
- const rows = db.prepare(sql).all(...params);
6388
- const pairFreq = /* @__PURE__ */ new Map();
6389
- let prevSession = "";
6390
- let prevTool = "";
6391
- for (const row of rows) {
6392
- if (row.session_id === prevSession && prevTool && prevTool !== row.tool_name) {
6393
- const key = prevTool + "->" + row.tool_name;
6394
- pairFreq.set(key, (pairFreq.get(key) || 0) + 1);
6395
- }
6396
- prevSession = row.session_id;
6397
- prevTool = row.tool_name;
6398
- }
6399
- for (const [key, freq] of pairFreq) if (!edgeKey.has(key) && freq >= 2) {
6400
- edgeKey.add(key);
6401
- const [source, target] = key.split("->");
6402
- edges.push({
6403
- source,
6404
- target,
6405
- frequency: freq,
6406
- edgeType: "session"
6407
- });
6408
- }
6409
- } catch {}
6410
- return c.json({ edges });
6411
- });
6412
- /**
6413
- * GET /api/tools/:name/stats
6414
- *
6415
- * Returns detailed stats for a single tool.
6416
- */
6417
- apiRoutes.get("/tools/:name/stats", (c) => {
6418
- const db = getDb$1(c);
6419
- const toolName = c.req.param("name");
6420
- const projectFilter = getProjectHash$2(c);
6421
- let tool;
6422
- try {
6423
- tool = db.prepare("SELECT id, name, tool_type, scope, status, usage_count, server_name, description, last_used_at, discovered_at FROM tool_registry WHERE name = ? ORDER BY usage_count DESC LIMIT 1").get(toolName);
6424
- } catch {}
6425
- if (!tool) return c.json({ error: "Tool not found" }, 404);
6426
- let successRate = null;
6427
- let totalEvents = 0;
6428
- try {
6429
- let sql = "SELECT success FROM tool_usage_events WHERE tool_name = ?";
6430
- const params = [toolName];
6431
- if (projectFilter) {
6432
- sql += " AND project_hash = ?";
6433
- params.push(projectFilter);
6434
- }
6435
- sql += " ORDER BY created_at DESC LIMIT 50";
6436
- const events = db.prepare(sql).all(...params);
6437
- totalEvents = events.length;
6438
- if (totalEvents > 0) successRate = events.filter((e) => e.success === 1).length / totalEvents;
6439
- } catch {}
6440
- let sessionsUsedIn = 0;
6441
- try {
6442
- let sql = "SELECT COUNT(DISTINCT session_id) as cnt FROM tool_usage_events WHERE tool_name = ? AND session_id IS NOT NULL";
6443
- const params = [toolName];
6444
- if (projectFilter) {
6445
- sql += " AND project_hash = ?";
6446
- params.push(projectFilter);
6447
- }
6448
- sessionsUsedIn = db.prepare(sql).get(...params)?.cnt ?? 0;
6449
- } catch {}
6450
- let coOccurring = [];
6451
- try {
6452
- let sql = `
6453
- SELECT e2.tool_name as name, COUNT(*) as count
6454
- FROM tool_usage_events e1
6455
- JOIN tool_usage_events e2
6456
- ON e1.session_id = e2.session_id AND e1.tool_name != e2.tool_name
6457
- WHERE e1.tool_name = ? AND e1.session_id IS NOT NULL
6458
- `;
6459
- const params = [toolName];
6460
- if (projectFilter) {
6461
- sql += " AND e1.project_hash = ?";
6462
- params.push(projectFilter);
6463
- }
6464
- sql += " GROUP BY e2.tool_name ORDER BY count DESC LIMIT 10";
6465
- coOccurring = db.prepare(sql).all(...params);
6466
- } catch {}
6467
- return c.json({
6468
- tool: {
6469
- id: tool.id,
6470
- name: tool.name,
6471
- toolType: tool.tool_type,
6472
- scope: tool.scope,
6473
- status: tool.status,
6474
- usageCount: tool.usage_count,
6475
- serverName: tool.server_name,
6476
- description: tool.description,
6477
- lastUsedAt: tool.last_used_at,
6478
- discoveredAt: tool.discovered_at
6479
- },
6480
- successRate,
6481
- totalEvents,
6482
- sessionsUsedIn,
6483
- coOccurring
6484
- });
6485
- });
6486
- /**
6487
- * GET /api/tools/sessions
6488
- *
6489
- * Returns recent session tool sequences for the flow strip.
6490
- */
6491
- apiRoutes.get("/tools/sessions", (c) => {
6492
- const db = getDb$1(c);
6493
- const projectFilter = getProjectHash$2(c);
6494
- const limitStr = c.req.query("limit");
6495
- const limit = limitStr ? Math.min(parseInt(limitStr, 10) || 10, 30) : 10;
6496
- let sessions = [];
6497
- try {
6498
- let sessionSql = `
6499
- SELECT DISTINCT session_id FROM tool_usage_events
6500
- WHERE session_id IS NOT NULL
6501
- `;
6502
- const sessionParams = [];
6503
- if (projectFilter) {
6504
- sessionSql += " AND project_hash = ?";
6505
- sessionParams.push(projectFilter);
6506
- }
6507
- sessionSql += " ORDER BY created_at DESC LIMIT ?";
6508
- sessionParams.push(limit);
6509
- const sessionIds = db.prepare(sessionSql).all(...sessionParams);
6510
- if (sessionIds.length > 0) {
6511
- const placeholders = sessionIds.map(() => "?").join(", ");
6512
- const ids = sessionIds.map((s) => s.session_id);
6513
- const eventRows = db.prepare(`
6514
- SELECT session_id, tool_name, created_at
6515
- FROM tool_usage_events
6516
- WHERE session_id IN (${placeholders})
6517
- ORDER BY session_id, created_at ASC
6518
- `).all(...ids);
6519
- const sessionMap = /* @__PURE__ */ new Map();
6520
- for (const row of eventRows) {
6521
- if (!sessionMap.has(row.session_id)) sessionMap.set(row.session_id, []);
6522
- sessionMap.get(row.session_id).push({
6523
- name: row.tool_name,
6524
- time: row.created_at
6525
- });
6526
- }
6527
- sessions = sessionIds.filter((s) => sessionMap.has(s.session_id)).map((s) => ({
6528
- sessionId: s.session_id,
6529
- tools: sessionMap.get(s.session_id)
6530
- }));
6531
- }
6532
- } catch {}
6533
- return c.json({ sessions });
6534
- });
6535
- /**
6536
5227
  * Finds connected components in the graph via BFS.
6537
5228
  * Shared by /api/graph/analysis and /api/graph/communities.
6538
5229
  */
@@ -6626,14 +5317,6 @@ function safeParseJson(json) {
6626
5317
  *
6627
5318
  * @module web/routes/admin
6628
5319
  */
6629
- const __dirname = dirname(fileURLToPath$1(import.meta.url));
6630
- const LAMINARK_VERSION = (() => {
6631
- try {
6632
- return JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")).version || "unknown";
6633
- } catch {
6634
- return "unknown";
6635
- }
6636
- })();
6637
5320
  function getDb(c) {
6638
5321
  return c.get("db");
6639
5322
  }
@@ -6708,51 +5391,6 @@ adminRoutes.get("/stats", (c) => {
6708
5391
  });
6709
5392
  });
6710
5393
  /**
6711
- * GET /api/admin/system
6712
- *
6713
- * Returns server-scoped system info (not project-scoped).
6714
- */
6715
- adminRoutes.get("/system", (c) => {
6716
- const db = getDb(c);
6717
- const mem = process.memoryUsage();
6718
- let dbSizeBytes = 0;
6719
- let pageCount = 0;
6720
- let pageSize = 4096;
6721
- try {
6722
- const pc = db.pragma("page_count", { simple: true });
6723
- const ps = db.pragma("page_size", { simple: true });
6724
- pageCount = pc;
6725
- pageSize = ps;
6726
- dbSizeBytes = pc * ps;
6727
- } catch {}
6728
- let walSizeBytes = 0;
6729
- try {
6730
- const dbPath = db.name;
6731
- if (dbPath) {
6732
- const walPath = dbPath + "-wal";
6733
- if (existsSync(walPath)) walSizeBytes = statSync(walPath).size;
6734
- }
6735
- } catch {}
6736
- return c.json({
6737
- laminarkVersion: LAMINARK_VERSION,
6738
- nodeVersion: process.version,
6739
- platform: process.platform,
6740
- arch: process.arch,
6741
- uptimeSeconds: Math.floor(process.uptime()),
6742
- memory: {
6743
- rssBytes: mem.rss,
6744
- heapUsedBytes: mem.heapUsed,
6745
- heapTotalBytes: mem.heapTotal
6746
- },
6747
- database: {
6748
- sizeBytes: dbSizeBytes,
6749
- walSizeBytes,
6750
- pageCount,
6751
- pageSize
6752
- }
6753
- });
6754
- });
6755
- /**
6756
5394
  * POST /api/admin/reset
6757
5395
  *
6758
5396
  * Hard-deletes data by group inside a transaction.
@@ -6857,66 +5495,6 @@ adminRoutes.post("/reset", async (c) => {
6857
5495
  scope: scoped ? "project" : "all"
6858
5496
  });
6859
5497
  });
6860
- adminRoutes.get("/hygiene", (c) => {
6861
- const db = getDb(c);
6862
- const project = getProjectHash$1(c);
6863
- if (!project) return c.json({ error: "No project context available" }, 400);
6864
- const tier = c.req.query("tier") || "high";
6865
- const sessionId = c.req.query("session_id");
6866
- const limit = parseInt(c.req.query("limit") || "50", 10);
6867
- const config = loadHygieneConfig();
6868
- const report = analyzeObservations(db, project, {
6869
- sessionId,
6870
- limit,
6871
- minTier: tier === "all" ? "low" : tier,
6872
- config
6873
- });
6874
- return c.json(report);
6875
- });
6876
- adminRoutes.post("/hygiene/purge", async (c) => {
6877
- const db = getDb(c);
6878
- const project = getProjectHash$1(c);
6879
- if (!project) return c.json({ error: "No project context available" }, 400);
6880
- const tier = (await c.req.json()).tier || "high";
6881
- const config = loadHygieneConfig();
6882
- const result = executePurge(db, project, analyzeObservations(db, project, {
6883
- minTier: tier === "all" ? "low" : tier,
6884
- limit: 500,
6885
- config
6886
- }), tier);
6887
- return c.json({
6888
- ok: true,
6889
- observationsPurged: result.observationsPurged,
6890
- orphanNodesRemoved: result.orphanNodesRemoved,
6891
- tier
6892
- });
6893
- });
6894
- adminRoutes.get("/hygiene/find", (c) => {
6895
- const db = getDb(c);
6896
- const project = getProjectHash$1(c);
6897
- if (!project) return c.json({ error: "No project context available" }, 400);
6898
- const report = findAnalysis(db, project, loadHygieneConfig());
6899
- return c.json(report);
6900
- });
6901
- adminRoutes.get("/config/hygiene", (c) => {
6902
- return c.json(loadHygieneConfig());
6903
- });
6904
- adminRoutes.put("/config/hygiene", async (c) => {
6905
- const body = await c.req.json();
6906
- const configPath = join(getConfigDir(), "hygiene.json");
6907
- if (body && body.__reset === true) {
6908
- try {
6909
- if (existsSync(configPath)) unlinkSync(configPath);
6910
- } catch {}
6911
- return c.json(resetHygieneConfig());
6912
- }
6913
- if (typeof body !== "object" || body === null || Array.isArray(body)) return c.json({ error: "Request body must be a JSON object" }, 400);
6914
- const { __reset: _, ...data } = body;
6915
- writeFileSync(configPath, JSON.stringify(data, null, 2), "utf-8");
6916
- const validated = loadHygieneConfig();
6917
- saveHygieneConfig(validated);
6918
- return c.json(validated);
6919
- });
6920
5498
  adminRoutes.get("/config/topic-detection", (c) => {
6921
5499
  return c.json(loadTopicDetectionConfig());
6922
5500
  });
@@ -6955,39 +5533,6 @@ adminRoutes.put("/config/graph-extraction", async (c) => {
6955
5533
  writeFileSync(configPath, JSON.stringify(validated, null, 2), "utf-8");
6956
5534
  return c.json(validated);
6957
5535
  });
6958
- adminRoutes.get("/config/cross-access", (c) => {
6959
- const project = c.req.query("project");
6960
- if (!project) return c.json({ error: "project query parameter is required" }, 400);
6961
- return c.json(loadCrossAccessConfig(project));
6962
- });
6963
- adminRoutes.put("/config/cross-access", async (c) => {
6964
- const project = c.req.query("project");
6965
- if (!project) return c.json({ error: "project query parameter is required" }, 400);
6966
- const body = await c.req.json();
6967
- if (body && body.__reset === true) {
6968
- resetCrossAccessConfig(project);
6969
- return c.json(loadCrossAccessConfig(project));
6970
- }
6971
- if (typeof body !== "object" || body === null || Array.isArray(body)) return c.json({ error: "Request body must be a JSON object" }, 400);
6972
- saveCrossAccessConfig(project, { readableProjects: body.readableProjects || [] });
6973
- return c.json(loadCrossAccessConfig(project));
6974
- });
6975
- adminRoutes.get("/config/tool-verbosity", (c) => {
6976
- return c.json(loadToolVerbosityConfig());
6977
- });
6978
- adminRoutes.put("/config/tool-verbosity", async (c) => {
6979
- const body = await c.req.json();
6980
- if (body && body.__reset === true) {
6981
- const config = resetToolVerbosityConfig();
6982
- saveToolVerbosityConfig(config);
6983
- return c.json(config);
6984
- }
6985
- if (typeof body !== "object" || body === null || Array.isArray(body)) return c.json({ error: "Request body must be a JSON object" }, 400);
6986
- const level = body.level;
6987
- if (level !== 1 && level !== 2 && level !== 3) return c.json({ error: "level must be 1, 2, or 3" }, 400);
6988
- saveToolVerbosityConfig({ level });
6989
- return c.json(loadToolVerbosityConfig());
6990
- });
6991
5536
 
6992
5537
  //#endregion
6993
5538
  //#region src/web/server.ts
@@ -7096,46 +5641,14 @@ const noGui = process.argv.includes("--no_gui");
7096
5641
  const db = openDatabase(getDatabaseConfig());
7097
5642
  initGraphSchema(db.db);
7098
5643
  initPathSchema(db.db);
7099
- var LiveProjectHashRef = class LiveProjectHashRef {
7100
- _current;
7101
- _lastChecked = 0;
7102
- _db;
7103
- static CHECK_INTERVAL_MS = 2e3;
7104
- constructor(sqliteDb) {
7105
- this._db = sqliteDb;
7106
- this._current = this.resolve();
7107
- }
7108
- get current() {
7109
- const now = Date.now();
7110
- if (now - this._lastChecked >= LiveProjectHashRef.CHECK_INTERVAL_MS) {
7111
- this._lastChecked = now;
7112
- const fresh = this.resolve();
7113
- if (fresh !== this._current) {
7114
- debug("mcp", "Project hash refreshed from database", {
7115
- old: this._current,
7116
- new: fresh
7117
- });
7118
- this._current = fresh;
7119
- }
7120
- }
7121
- return this._current;
7122
- }
7123
- resolve() {
7124
- try {
7125
- const row = this._db.prepare("SELECT project_hash FROM project_metadata ORDER BY last_seen_at DESC LIMIT 1").get();
7126
- if (row?.project_hash) return row.project_hash;
7127
- } catch {}
7128
- return getProjectHash(process.cwd());
7129
- }
7130
- };
7131
- const projectHashRef = new LiveProjectHashRef(db.db);
5644
+ const projectHash = getProjectHash(process.cwd());
7132
5645
  let toolRegistry = null;
7133
5646
  try {
7134
5647
  toolRegistry = new ToolRegistryRepository(db.db);
7135
5648
  } catch {
7136
5649
  debug("mcp", "Tool registry not available (pre-migration-16)");
7137
5650
  }
7138
- const embeddingStore = db.hasVectorSupport ? new EmbeddingStore(db.db, projectHashRef.current) : null;
5651
+ const embeddingStore = db.hasVectorSupport ? new EmbeddingStore(db.db, projectHash) : null;
7139
5652
  const worker = new AnalysisWorker();
7140
5653
  worker.start().catch(() => {
7141
5654
  debug("mcp", "Worker failed to start, keyword-only mode");
@@ -7148,7 +5661,7 @@ const adaptiveManager = new AdaptiveThresholdManager({
7148
5661
  alpha: topicConfig.ewmaAlpha
7149
5662
  });
7150
5663
  applyConfig(topicConfig, detector, adaptiveManager);
7151
- const historicalSeed = new ThresholdStore(db.db).loadHistoricalSeed(projectHashRef.current);
5664
+ const historicalSeed = new ThresholdStore(db.db).loadHistoricalSeed(projectHash);
7152
5665
  if (historicalSeed) {
7153
5666
  adaptiveManager.seedFromHistory(historicalSeed.averageDistance, historicalSeed.averageVariance);
7154
5667
  applyConfig(topicConfig, detector, adaptiveManager);
@@ -7159,7 +5672,7 @@ const notificationStore = new NotificationStore(db.db);
7159
5672
  const topicShiftHandler = new TopicShiftHandler({
7160
5673
  detector,
7161
5674
  stashManager,
7162
- observationStore: new ObservationRepository(db.db, projectHashRef.current),
5675
+ observationStore: new ObservationRepository(db.db, projectHash),
7163
5676
  config: topicConfig,
7164
5677
  decisionLogger,
7165
5678
  adaptiveManager
@@ -7174,8 +5687,7 @@ async function processUnembedded() {
7174
5687
  if (!embeddingStore || !worker.isReady()) return;
7175
5688
  const ids = embeddingStore.findUnembedded(10);
7176
5689
  if (ids.length === 0) return;
7177
- const currentHash = projectHashRef.current;
7178
- const obsRepo = new ObservationRepository(db.db, currentHash);
5690
+ const obsRepo = new ObservationRepository(db.db, projectHash);
7179
5691
  let shiftDetectedThisCycle = false;
7180
5692
  for (const id of ids) {
7181
5693
  const obs = obsRepo.getById(id);
@@ -7192,26 +5704,24 @@ async function processUnembedded() {
7192
5704
  id,
7193
5705
  text: obs.content.length > 120 ? obs.content.substring(0, 120) + "..." : obs.content,
7194
5706
  sessionId: obs.sessionId ?? null,
7195
- createdAt: obs.createdAt,
7196
- projectHash: currentHash
5707
+ createdAt: obs.createdAt
7197
5708
  });
7198
5709
  if (topicConfig.enabled && !shiftDetectedThisCycle && TOPIC_SHIFT_SOURCES.has(obs.source)) try {
7199
5710
  const obsWithEmbedding = {
7200
5711
  ...obs,
7201
5712
  embedding
7202
5713
  };
7203
- const result = await topicShiftHandler.handleObservation(obsWithEmbedding, obs.sessionId ?? "unknown", currentHash);
5714
+ const result = await topicShiftHandler.handleObservation(obsWithEmbedding, obs.sessionId ?? "unknown", projectHash);
7204
5715
  if (result.stashed && result.notification) {
7205
5716
  shiftDetectedThisCycle = true;
7206
- notificationStore.add(currentHash, result.notification);
5717
+ notificationStore.add(projectHash, result.notification);
7207
5718
  debug("embed", "Topic shift detected, notification queued", { id });
7208
5719
  broadcast("topic_shift", {
7209
5720
  id: result.notification.substring(0, 32),
7210
5721
  fromTopic: null,
7211
5722
  toTopic: null,
7212
5723
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7213
- confidence: null,
7214
- projectHash: currentHash
5724
+ confidence: null
7215
5725
  });
7216
5726
  }
7217
5727
  } catch (topicErr) {
@@ -7222,7 +5732,7 @@ async function processUnembedded() {
7222
5732
  }
7223
5733
  let researchBufferForFlush = null;
7224
5734
  try {
7225
- researchBufferForFlush = new ResearchBufferRepository(db.db, projectHashRef.current);
5735
+ researchBufferForFlush = new ResearchBufferRepository(db.db, projectHash);
7226
5736
  } catch {}
7227
5737
  async function processUnembeddedTools() {
7228
5738
  if (!toolRegistry || !worker.isReady() || !db.hasVectorSupport) return;
@@ -7249,39 +5759,26 @@ const embedTimer = setInterval(() => {
7249
5759
  } catch {}
7250
5760
  statusCache.refreshIfDirty();
7251
5761
  }, 5e3);
7252
- const statusCache = new StatusCache(db.db, projectHashRef, process.cwd(), db.hasVectorSupport, () => worker.isReady());
5762
+ const statusCache = new StatusCache(db.db, projectHash, process.cwd(), db.hasVectorSupport, () => worker.isReady());
7253
5763
  const server = createServer();
7254
- registerSaveMemory(server, db.db, projectHashRef, notificationStore, worker, embeddingStore, statusCache);
7255
- registerIngestKnowledge(server, db.db, projectHashRef, notificationStore, statusCache);
7256
- registerRecall(server, db.db, projectHashRef, worker, embeddingStore, notificationStore, statusCache);
7257
- registerTopicContext(server, db.db, projectHashRef, notificationStore);
7258
- registerQueryGraph(server, db.db, projectHashRef, notificationStore);
7259
- registerGraphStats(server, db.db, projectHashRef, notificationStore);
7260
- registerHygiene(server, db.db, projectHashRef, notificationStore);
7261
- registerStatus(server, statusCache, projectHashRef, notificationStore);
5764
+ registerSaveMemory(server, db.db, projectHash, notificationStore, worker, embeddingStore, statusCache);
5765
+ registerRecall(server, db.db, projectHash, worker, embeddingStore, notificationStore, statusCache);
5766
+ registerTopicContext(server, db.db, projectHash, notificationStore);
5767
+ registerQueryGraph(server, db.db, projectHash, notificationStore);
5768
+ registerGraphStats(server, db.db, projectHash, notificationStore);
5769
+ registerStatus(server, statusCache, projectHash, notificationStore);
7262
5770
  if (toolRegistry) {
7263
- registerDiscoverTools(server, toolRegistry, worker, db.hasVectorSupport, notificationStore, projectHashRef);
7264
- registerReportTools(server, toolRegistry, projectHashRef);
5771
+ registerDiscoverTools(server, toolRegistry, worker, db.hasVectorSupport, notificationStore, projectHash);
5772
+ registerReportTools(server, toolRegistry, projectHash);
7265
5773
  }
7266
- const pathRepo = new PathRepository(db.db, projectHashRef.current);
5774
+ const pathRepo = new PathRepository(db.db, projectHash);
7267
5775
  const pathTracker = new PathTracker(pathRepo);
7268
- registerDebugPathTools(server, pathRepo, pathTracker, notificationStore, projectHashRef);
7269
- let branchRepo = null;
7270
- let branchTracker = null;
7271
- try {
7272
- branchRepo = new BranchRepository(db.db, projectHashRef.current);
7273
- branchTracker = new BranchTracker(branchRepo, db.db, projectHashRef.current);
7274
- const obsRepoForBranches = new ObservationRepository(db.db, projectHashRef.current);
7275
- registerThoughtBranchTools(server, branchRepo, obsRepoForBranches, notificationStore, projectHashRef);
7276
- } catch {
7277
- debug("mcp", "Branch tracking not available (pre-migration-21)");
7278
- }
7279
- const haikuProcessor = new HaikuProcessor(db.db, projectHashRef.current, {
5776
+ registerDebugPathTools(server, pathRepo, pathTracker, notificationStore, projectHash);
5777
+ const haikuProcessor = new HaikuProcessor(db.db, projectHash, {
7280
5778
  intervalMs: 3e4,
7281
5779
  batchSize: 10,
7282
5780
  concurrency: 3,
7283
- pathTracker,
7284
- branchTracker
5781
+ pathTracker
7285
5782
  });
7286
5783
  startServer(server).then(() => {
7287
5784
  haikuProcessor.start();
@@ -7296,7 +5793,7 @@ if (!noGui) {
7296
5793
  const __filename = fileURLToPath(import.meta.url);
7297
5794
  const __dirname = path.dirname(__filename);
7298
5795
  const uiRoot = path.resolve(__dirname, "..", "ui");
7299
- startWebServer(createWebServer(db.db, uiRoot, projectHashRef.current), webPort);
5796
+ startWebServer(createWebServer(db.db, uiRoot, projectHash), webPort);
7300
5797
  } else debug("mcp", "Web UI disabled (--no_gui)");
7301
5798
  const curationAgent = new CurationAgent(db.db, {
7302
5799
  intervalMs: 300 * 1e3,