@hiveai/mcp 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -119,6 +119,7 @@ async function memList(input, ctx) {
119
119
  }
120
120
 
121
121
  // src/tools/mem-save.ts
122
+ import { createHash } from "crypto";
122
123
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
123
124
  import { existsSync as existsSync4 } from "fs";
124
125
  import path3 from "path";
@@ -130,7 +131,9 @@ import {
130
131
  } from "@hiveai/core";
131
132
  import { z as z4 } from "zod";
132
133
  var MemSaveInputSchema = {
133
- type: z4.enum(["convention", "decision", "gotcha", "architecture", "glossary", "attempt"]).describe("Kind of memory being saved. Use 'attempt' for failed approaches (auto-validated)."),
134
+ type: z4.enum(["convention", "decision", "gotcha", "architecture", "glossary", "attempt", "session_recap"]).describe(
135
+ "Kind of memory being saved. Use 'attempt' for failed approaches (auto-validated). Use 'session_recap' via mem_session_end instead."
136
+ ),
134
137
  slug: z4.string().min(1).describe("Short human-readable identifier \u2014 becomes part of the filename"),
135
138
  body: z4.string().describe("Markdown body of the memory"),
136
139
  scope: z4.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope: personal | team | module"),
@@ -140,14 +143,61 @@ var MemSaveInputSchema = {
140
143
  author: z4.string().optional().describe("Author handle or email"),
141
144
  paths: z4.array(z4.string()).default([]).describe("Anchor paths (file paths this memory references)"),
142
145
  symbols: z4.array(z4.string()).default([]).describe("Anchor symbols (function/class names this memory references)"),
143
- commit: z4.string().optional().describe("Anchor commit SHA (for staleness detection later)")
146
+ commit: z4.string().optional().describe("Anchor commit SHA (for staleness detection later)"),
147
+ topic: z4.string().optional().describe(
148
+ "Stable key for this memory. If a memory with the same topic already exists in this scope, it is updated in-place (revision_count++). Use for knowledge that evolves over time."
149
+ )
144
150
  };
151
+ function bodyHash(body) {
152
+ return createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
153
+ }
145
154
  async function memSave(input, ctx) {
146
155
  if (!existsSync4(ctx.paths.haiveDir)) {
147
156
  throw new Error(
148
157
  `No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`
149
158
  );
150
159
  }
160
+ const existing = existsSync4(ctx.paths.memoriesDir) ? await loadMemoriesFromDir2(ctx.paths.memoriesDir) : [];
161
+ const incomingHash = bodyHash(input.body);
162
+ const hashDuplicate = existing.find(
163
+ ({ memory }) => bodyHash(memory.body) === incomingHash && memory.frontmatter.scope === input.scope
164
+ );
165
+ if (hashDuplicate) {
166
+ throw new Error(
167
+ `Duplicate content detected \u2014 identical body already saved as "${hashDuplicate.memory.frontmatter.id}". Use mem_update to modify it, or change the body to add new information.`
168
+ );
169
+ }
170
+ if (input.topic) {
171
+ const topicMatch = existing.find(
172
+ ({ memory }) => memory.frontmatter.topic === input.topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
173
+ );
174
+ if (topicMatch) {
175
+ const fm = topicMatch.memory.frontmatter;
176
+ const newFrontmatter = {
177
+ ...fm,
178
+ body: input.body,
179
+ tags: input.tags.length ? input.tags : fm.tags,
180
+ revision_count: (fm.revision_count ?? 0) + 1,
181
+ anchor: {
182
+ commit: input.commit ?? fm.anchor.commit,
183
+ paths: input.paths.length ? input.paths : fm.anchor.paths,
184
+ symbols: input.symbols.length ? input.symbols : fm.anchor.symbols
185
+ }
186
+ };
187
+ await writeFile2(
188
+ topicMatch.filePath,
189
+ serializeMemory({ frontmatter: newFrontmatter, body: input.body }),
190
+ "utf8"
191
+ );
192
+ return {
193
+ id: fm.id,
194
+ scope: fm.scope,
195
+ file_path: topicMatch.filePath,
196
+ action: "updated",
197
+ revision_count: newFrontmatter.revision_count
198
+ };
199
+ }
200
+ }
151
201
  const frontmatter = buildFrontmatter({
152
202
  type: input.type,
153
203
  slug: input.slug,
@@ -158,7 +208,8 @@ async function memSave(input, ctx) {
158
208
  author: input.author,
159
209
  paths: input.paths,
160
210
  symbols: input.symbols,
161
- commit: input.commit
211
+ commit: input.commit,
212
+ topic: input.topic
162
213
  });
163
214
  const file = memoryFilePath(
164
215
  ctx.paths,
@@ -171,15 +222,16 @@ async function memSave(input, ctx) {
171
222
  throw new Error(`Memory already exists at ${file}`);
172
223
  }
173
224
  let warning;
174
- if (existsSync4(ctx.paths.memoriesDir)) {
175
- const existing = await loadMemoriesFromDir2(ctx.paths.memoriesDir);
225
+ let similar_found;
226
+ if (existing.length > 0) {
176
227
  const slugTokens = input.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
177
228
  const similar = existing.filter(({ memory }) => {
178
229
  const id = memory.frontmatter.id.toLowerCase();
179
230
  return slugTokens.length >= 2 && slugTokens.filter((t) => id.includes(t)).length >= Math.ceil(slugTokens.length * 0.6);
180
231
  });
181
232
  if (similar.length > 0) {
182
- warning = `Possible duplicate detected. Similar memories: ${similar.map((m) => m.memory.frontmatter.id).join(", ")}. Consider updating one of these instead.`;
233
+ similar_found = similar.map((m) => m.memory.frontmatter.id);
234
+ warning = `Possible duplicate: similar memories already exist (${similar_found.join(", ")}). Consider updating one of these instead.`;
183
235
  }
184
236
  }
185
237
  await writeFile2(file, serializeMemory({ frontmatter, body: input.body }), "utf8");
@@ -187,7 +239,9 @@ async function memSave(input, ctx) {
187
239
  id: frontmatter.id,
188
240
  scope: frontmatter.scope,
189
241
  file_path: file,
190
- ...warning ? { warning } : {}
242
+ action: "created",
243
+ ...warning ? { warning } : {},
244
+ ...similar_found ? { similar_found } : {}
191
245
  };
192
246
  }
193
247
 
@@ -521,10 +575,14 @@ async function memForFiles(input, ctx) {
521
575
  seen.add(loaded.memory.frontmatter.id);
522
576
  }
523
577
  }
578
+ const pathSegments = extractPathSegments(input.files);
524
579
  for (const loaded of all) {
525
580
  if (seen.has(loaded.memory.frontmatter.id)) continue;
526
581
  const fm = loaded.memory.frontmatter;
527
- const moduleHit = fm.module && inferred.includes(fm.module) || fm.tags.some((t) => inferred.includes(t));
582
+ const moduleHit = fm.module && inferred.includes(fm.module) || fm.tags.some((t) => inferred.includes(t)) || fm.tags.some((t) => {
583
+ const tl = t.toLowerCase();
584
+ return pathSegments.has(tl) || pathSegments.has(tl.replace(/[-_]/g, ""));
585
+ });
528
586
  if (moduleHit) {
529
587
  byModule.push(toMatch(loaded, "module", usage));
530
588
  seen.add(fm.id);
@@ -566,6 +624,52 @@ function toMatch(loaded, reason, usage) {
566
624
  body: loaded.memory.body
567
625
  };
568
626
  }
627
+ function extractPathSegments(files) {
628
+ const GENERIC = /* @__PURE__ */ new Set([
629
+ "src",
630
+ "main",
631
+ "java",
632
+ "kotlin",
633
+ "python",
634
+ "go",
635
+ "lib",
636
+ "libs",
637
+ "com",
638
+ "org",
639
+ "net",
640
+ "io",
641
+ "app",
642
+ "apps",
643
+ "pkg",
644
+ "internal",
645
+ "test",
646
+ "tests",
647
+ "spec",
648
+ "specs",
649
+ "impl",
650
+ "domain",
651
+ "shared",
652
+ "resources",
653
+ "static",
654
+ "assets",
655
+ "config",
656
+ "configs"
657
+ ]);
658
+ const out = /* @__PURE__ */ new Set();
659
+ for (const file of files) {
660
+ const parts = file.replace(/\\/g, "/").split("/");
661
+ for (const part of parts) {
662
+ const seg = part.toLowerCase().replace(/\.[^.]+$/, "");
663
+ if (seg.length >= 3 && !GENERIC.has(seg) && /^[a-z]/.test(seg)) {
664
+ out.add(seg);
665
+ for (const sub of seg.split(/[-_]/).filter((s) => s.length >= 3)) {
666
+ out.add(sub);
667
+ }
668
+ }
669
+ }
670
+ }
671
+ return out;
672
+ }
569
673
  async function loadModuleContexts(ctx, modules, enabled) {
570
674
  if (!enabled || modules.length === 0) return [];
571
675
  if (!existsSync8(ctx.paths.modulesContextDir)) return [];
@@ -845,10 +949,172 @@ async function memTried(input, ctx) {
845
949
  return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
846
950
  }
847
951
 
848
- // src/tools/get-briefing.ts
849
- import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
952
+ // src/tools/mem-observe.ts
953
+ import { mkdir as mkdir4, writeFile as writeFile8 } from "fs/promises";
850
954
  import { existsSync as existsSync15 } from "fs";
851
955
  import path6 from "path";
956
+ import {
957
+ buildFrontmatter as buildFrontmatter3,
958
+ memoryFilePath as memoryFilePath3,
959
+ serializeMemory as serializeMemory7
960
+ } from "@hiveai/core";
961
+ import { z as z15 } from "zod";
962
+ var MemObserveInputSchema = {
963
+ what: z15.string().min(1).describe("Short title: what did you observe? (e.g. 'MobilePaymentController has two @RequestBody on handleWebhook')"),
964
+ where: z15.string().min(1).describe("File path(s) where the issue lives \u2014 be specific"),
965
+ impact: z15.string().min(1).describe("What breaks or could break because of this (e.g. 'Spring MVC rejects the handler at startup')"),
966
+ fix: z15.string().optional().describe("Suggested fix or workaround (optional \u2014 leave empty if unknown)"),
967
+ scope: z15.enum(["personal", "team", "module"]).default("team").describe("Visibility scope \u2014 defaults to team since discoveries benefit everyone"),
968
+ module: z15.string().optional().describe("Module name (required when scope=module)"),
969
+ tags: z15.array(z15.string()).default([]).describe("Tags for filtering"),
970
+ author: z15.string().optional().describe("Author handle or email")
971
+ };
972
+ async function memObserve(input, ctx) {
973
+ if (!existsSync15(ctx.paths.haiveDir)) {
974
+ throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`);
975
+ }
976
+ const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-");
977
+ const anchorPaths = input.where.split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
978
+ const baseFm = buildFrontmatter3({
979
+ type: "gotcha",
980
+ slug,
981
+ scope: input.scope,
982
+ module: input.module,
983
+ tags: input.tags,
984
+ paths: anchorPaths,
985
+ author: input.author
986
+ });
987
+ const frontmatter = { ...baseFm, status: "validated" };
988
+ const lines = [`# ${input.what}`, ""];
989
+ lines.push(`**Where:** \`${input.where}\``);
990
+ lines.push("", `**Impact:** ${input.impact}`);
991
+ if (input.fix) {
992
+ lines.push("", `**Fix/workaround:** ${input.fix}`);
993
+ }
994
+ const body = lines.join("\n") + "\n";
995
+ const file = memoryFilePath3(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
996
+ await mkdir4(path6.dirname(file), { recursive: true });
997
+ if (existsSync15(file)) {
998
+ throw new Error(`Memory already exists at ${file}`);
999
+ }
1000
+ await writeFile8(file, serializeMemory7({ frontmatter, body }), "utf8");
1001
+ return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
1002
+ }
1003
+
1004
+ // src/tools/mem-session-end.ts
1005
+ import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
1006
+ import { existsSync as existsSync16 } from "fs";
1007
+ import path7 from "path";
1008
+ import {
1009
+ buildFrontmatter as buildFrontmatter4,
1010
+ loadMemoriesFromDir as loadMemoriesFromDir12,
1011
+ memoryFilePath as memoryFilePath4,
1012
+ serializeMemory as serializeMemory8
1013
+ } from "@hiveai/core";
1014
+ import { z as z16 } from "zod";
1015
+ var MemSessionEndInputSchema = {
1016
+ goal: z16.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1017
+ accomplished: z16.string().describe("What was actually done \u2014 bullet list recommended"),
1018
+ discoveries: z16.string().default("").describe(
1019
+ "Any bugs, inconsistencies, surprises, or missing knowledge found during this session. Empty if nothing surprising was found."
1020
+ ),
1021
+ files_touched: z16.array(z16.string()).default([]).describe("Key files that were read or modified \u2014 used as anchor paths"),
1022
+ next_steps: z16.string().default("").describe("What should happen next (for the next session or a teammate)"),
1023
+ scope: z16.enum(["personal", "team", "module"]).default("personal").describe("Visibility: personal = private to you, team = shared with the team"),
1024
+ module: z16.string().optional().describe("Module name (required when scope=module)")
1025
+ };
1026
+ function recapTopic(scope, module) {
1027
+ return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
1028
+ }
1029
+ function buildBody(input) {
1030
+ const lines = [];
1031
+ lines.push(`## Goal
1032
+ ${input.goal}`);
1033
+ lines.push(`
1034
+ ## Accomplished
1035
+ ${input.accomplished}`);
1036
+ if (input.discoveries.trim()) {
1037
+ lines.push(`
1038
+ ## Discoveries & surprises
1039
+ ${input.discoveries}`);
1040
+ }
1041
+ if (input.files_touched.length > 0) {
1042
+ lines.push(`
1043
+ ## Files touched
1044
+ ${input.files_touched.map((f) => `- \`${f}\``).join("\n")}`);
1045
+ }
1046
+ if (input.next_steps.trim()) {
1047
+ lines.push(`
1048
+ ## Next steps
1049
+ ${input.next_steps}`);
1050
+ }
1051
+ return lines.join("\n");
1052
+ }
1053
+ async function memSessionEnd(input, ctx) {
1054
+ if (!existsSync16(ctx.paths.haiveDir)) {
1055
+ throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`);
1056
+ }
1057
+ const body = buildBody(input);
1058
+ const topic = recapTopic(input.scope, input.module);
1059
+ const existing = existsSync16(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1060
+ const topicMatch = existing.find(
1061
+ ({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
1062
+ );
1063
+ if (topicMatch) {
1064
+ const fm = topicMatch.memory.frontmatter;
1065
+ const revisionCount = (fm.revision_count ?? 0) + 1;
1066
+ const newFrontmatter = {
1067
+ ...fm,
1068
+ revision_count: revisionCount,
1069
+ anchor: {
1070
+ ...fm.anchor,
1071
+ paths: input.files_touched.length ? input.files_touched : fm.anchor.paths
1072
+ }
1073
+ };
1074
+ await writeFile9(
1075
+ topicMatch.filePath,
1076
+ serializeMemory8({ frontmatter: newFrontmatter, body }),
1077
+ "utf8"
1078
+ );
1079
+ return {
1080
+ id: fm.id,
1081
+ scope: fm.scope,
1082
+ file_path: topicMatch.filePath,
1083
+ action: "updated",
1084
+ revision_count: revisionCount
1085
+ };
1086
+ }
1087
+ const frontmatter = buildFrontmatter4({
1088
+ type: "session_recap",
1089
+ slug: "session-recap",
1090
+ scope: input.scope,
1091
+ module: input.module,
1092
+ tags: ["session", "recap"],
1093
+ paths: input.files_touched,
1094
+ topic,
1095
+ status: "validated"
1096
+ });
1097
+ const file = memoryFilePath4(
1098
+ ctx.paths,
1099
+ frontmatter.scope,
1100
+ frontmatter.id,
1101
+ frontmatter.module
1102
+ );
1103
+ await mkdir5(path7.dirname(file), { recursive: true });
1104
+ await writeFile9(file, serializeMemory8({ frontmatter, body }), "utf8");
1105
+ return {
1106
+ id: frontmatter.id,
1107
+ scope: frontmatter.scope,
1108
+ file_path: file,
1109
+ action: "created",
1110
+ revision_count: 0
1111
+ };
1112
+ }
1113
+
1114
+ // src/tools/get-briefing.ts
1115
+ import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
1116
+ import { existsSync as existsSync17 } from "fs";
1117
+ import path8 from "path";
852
1118
  import {
853
1119
  allocateBudget,
854
1120
  deriveConfidence as deriveConfidence4,
@@ -857,31 +1123,31 @@ import {
857
1123
  inferModulesFromPaths as inferModulesFromPaths2,
858
1124
  isDecaying,
859
1125
  literalMatchesAllTokens as literalMatchesAllTokens2,
860
- loadMemoriesFromDir as loadMemoriesFromDir12,
1126
+ loadMemoriesFromDir as loadMemoriesFromDir13,
861
1127
  loadUsageIndex as loadUsageIndex7,
862
1128
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
863
1129
  tokenizeQuery as tokenizeQuery2,
864
1130
  trackReads as trackReads3,
865
1131
  truncateToTokens
866
1132
  } from "@hiveai/core";
867
- import { z as z15 } from "zod";
1133
+ import { z as z17 } from "zod";
868
1134
  var GetBriefingInputSchema = {
869
- task: z15.string().optional().describe(
1135
+ task: z17.string().optional().describe(
870
1136
  "What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
871
1137
  ),
872
- files: z15.array(z15.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
873
- max_tokens: z15.number().int().positive().default(8e3).describe(
1138
+ files: z17.array(z17.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
1139
+ max_tokens: z17.number().int().positive().default(8e3).describe(
874
1140
  "Approximate token budget for the entire briefing. Each section is allocated a share and truncated to fit."
875
1141
  ),
876
- max_memories: z15.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
877
- include_project_context: z15.boolean().default(true),
878
- include_module_contexts: z15.boolean().default(true),
879
- semantic: z15.boolean().default(true).describe(
1142
+ max_memories: z17.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
1143
+ include_project_context: z17.boolean().default(true),
1144
+ include_module_contexts: z17.boolean().default(true),
1145
+ semantic: z17.boolean().default(true).describe(
880
1146
  "Use semantic ranking when a task is provided (requires `haive embeddings index`)."
881
1147
  ),
882
- include_stale: z15.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
883
- track: z15.boolean().default(true).describe("Increment read_count on returned memories"),
884
- format: z15.enum(["full", "compact"]).default("full").describe(
1148
+ include_stale: z17.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
1149
+ track: z17.boolean().default(true).describe("Increment read_count on returned memories"),
1150
+ format: z17.enum(["full", "compact"]).default("full").describe(
885
1151
  "Output format: 'full' returns complete memory bodies; 'compact' returns id + 1-line summary only (call mem_get for details)."
886
1152
  )
887
1153
  };
@@ -891,12 +1157,27 @@ async function getBriefing(input, ctx) {
891
1157
  let searchMode = "literal";
892
1158
  let usage = { version: 1, updated_at: "", by_id: {} };
893
1159
  let byId = /* @__PURE__ */ new Map();
894
- if (existsSync15(ctx.paths.memoriesDir)) {
895
- const allLoaded = await loadMemoriesFromDir12(ctx.paths.memoriesDir);
1160
+ let lastSession;
1161
+ if (existsSync17(ctx.paths.memoriesDir)) {
1162
+ const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1163
+ const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
1164
+ (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
1165
+ );
1166
+ if (recaps.length > 0) {
1167
+ const r = recaps[0];
1168
+ const fm = r.memory.frontmatter;
1169
+ lastSession = {
1170
+ id: fm.id,
1171
+ scope: fm.scope,
1172
+ revision_count: fm.revision_count ?? 0,
1173
+ body: r.memory.body
1174
+ };
1175
+ }
896
1176
  const allMemories = allLoaded.filter(({ memory }) => {
897
1177
  const s = memory.frontmatter.status;
898
1178
  if (s === "rejected" || s === "deprecated") return false;
899
1179
  if (!input.include_stale && s === "stale") return false;
1180
+ if (memory.frontmatter.type === "session_recap") return false;
900
1181
  return true;
901
1182
  });
902
1183
  usage = await loadUsageIndex7(ctx.paths);
@@ -986,7 +1267,7 @@ async function getBriefing(input, ctx) {
986
1267
  await trackReads3(ctx.paths, memories.map((m) => m.id));
987
1268
  }
988
1269
  }
989
- const projectContext = input.include_project_context && existsSync15(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1270
+ const projectContext = input.include_project_context && existsSync17(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
990
1271
  const moduleContents = input.include_module_contexts ? await loadModuleContexts2(ctx, inferred) : [];
991
1272
  const memoriesText = memories.map((m) => {
992
1273
  const unverified = m.status === "proposed" ? " [UNVERIFIED \u2014 not yet validated]" : "";
@@ -1052,6 +1333,7 @@ ${m.content}`).join("\n\n---\n\n"),
1052
1333
  ...input.task ? { task: input.task } : {},
1053
1334
  search_mode: searchMode,
1054
1335
  inferred_modules: inferred,
1336
+ ...lastSession ? { last_session: lastSession } : {},
1055
1337
  project_context: projectContext ? { content: projectSlice.text, truncated: projectSlice.truncated } : null,
1056
1338
  module_contexts: trimmedModules,
1057
1339
  memories: outputMemories,
@@ -1087,15 +1369,15 @@ async function trySemanticHits(ctx, task, limit) {
1087
1369
  }
1088
1370
  async function loadModuleContexts2(ctx, modules) {
1089
1371
  if (modules.length === 0) return [];
1090
- if (!existsSync15(ctx.paths.modulesContextDir)) return [];
1372
+ if (!existsSync17(ctx.paths.modulesContextDir)) return [];
1091
1373
  const available = new Set(
1092
1374
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
1093
1375
  );
1094
1376
  const out = [];
1095
1377
  for (const m of modules) {
1096
1378
  if (!available.has(m)) continue;
1097
- const file = path6.join(ctx.paths.modulesContextDir, m, "context.md");
1098
- if (existsSync15(file)) {
1379
+ const file = path8.join(ctx.paths.modulesContextDir, m, "context.md");
1380
+ if (existsSync17(file)) {
1099
1381
  out.push({ name: m, content: await readFile3(file, "utf8") });
1100
1382
  }
1101
1383
  }
@@ -1104,11 +1386,11 @@ async function loadModuleContexts2(ctx, modules) {
1104
1386
 
1105
1387
  // src/tools/code-map.ts
1106
1388
  import { loadCodeMap, queryCodeMap } from "@hiveai/core";
1107
- import { z as z16 } from "zod";
1389
+ import { z as z18 } from "zod";
1108
1390
  var CodeMapInputSchema = {
1109
- file: z16.string().optional().describe("Filter to files whose path contains this substring"),
1110
- symbol: z16.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
1111
- max_files: z16.number().int().positive().default(40).describe("Cap on returned files")
1391
+ file: z18.string().optional().describe("Filter to files whose path contains this substring"),
1392
+ symbol: z18.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
1393
+ max_files: z18.number().int().positive().default(40).describe("Cap on returned files")
1112
1394
  };
1113
1395
  async function codeMapTool(input, ctx) {
1114
1396
  const map = await loadCodeMap(ctx.paths);
@@ -1134,18 +1416,18 @@ async function codeMapTool(input, ctx) {
1134
1416
  }
1135
1417
 
1136
1418
  // src/tools/mem-diff.ts
1137
- import { existsSync as existsSync16 } from "fs";
1138
- import { loadMemoriesFromDir as loadMemoriesFromDir13 } from "@hiveai/core";
1139
- import { z as z17 } from "zod";
1419
+ import { existsSync as existsSync18 } from "fs";
1420
+ import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
1421
+ import { z as z19 } from "zod";
1140
1422
  var MemDiffInputSchema = {
1141
- id_a: z17.string().min(1).describe("First memory id"),
1142
- id_b: z17.string().min(1).describe("Second memory id")
1423
+ id_a: z19.string().min(1).describe("First memory id"),
1424
+ id_b: z19.string().min(1).describe("Second memory id")
1143
1425
  };
1144
1426
  async function memDiff(input, ctx) {
1145
- if (!existsSync16(ctx.paths.memoriesDir)) {
1427
+ if (!existsSync18(ctx.paths.memoriesDir)) {
1146
1428
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
1147
1429
  }
1148
- const all = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1430
+ const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
1149
1431
  const foundA = all.find((m) => m.memory.frontmatter.id === input.id_a);
1150
1432
  const foundB = all.find((m) => m.memory.frontmatter.id === input.id_b);
1151
1433
  if (!foundA) throw new Error(`No memory with id "${input.id_a}".`);
@@ -1179,12 +1461,12 @@ async function memDiff(input, ctx) {
1179
1461
  }
1180
1462
 
1181
1463
  // src/prompts/bootstrap-project.ts
1182
- import { z as z18 } from "zod";
1464
+ import { z as z20 } from "zod";
1183
1465
  var BootstrapProjectArgsSchema = {
1184
- module: z18.string().optional().describe(
1466
+ module: z20.string().optional().describe(
1185
1467
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
1186
1468
  ),
1187
- focus: z18.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
1469
+ focus: z20.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
1188
1470
  };
1189
1471
  var ROOT_TEMPLATE = `# Project context
1190
1472
 
@@ -1266,10 +1548,10 @@ ${template}\`\`\`
1266
1548
  }
1267
1549
 
1268
1550
  // src/prompts/post-task.ts
1269
- import { z as z19 } from "zod";
1551
+ import { z as z21 } from "zod";
1270
1552
  var PostTaskArgsSchema = {
1271
- task_summary: z19.string().optional().describe("One sentence describing what you just did"),
1272
- files_touched: z19.array(z19.string()).optional().describe("Files you created or modified during the task")
1553
+ task_summary: z21.string().optional().describe("One sentence describing what you just did"),
1554
+ files_touched: z21.array(z21.string()).optional().describe("Files you created or modified during the task")
1273
1555
  };
1274
1556
  function postTaskPrompt(args, ctx) {
1275
1557
  const taskLine = args.task_summary ? `
@@ -1285,6 +1567,19 @@ Project root: \`${ctx.paths.root}\`
1285
1567
 
1286
1568
  Go through each item. If the answer is yes, call the corresponding tool immediately.
1287
1569
 
1570
+ ### 0. Did you read existing code and discover bugs, inconsistencies, or security gaps that weren't in the briefing?
1571
+ This is the most important question. Deep code reading surfaces issues that no memory captures yet.
1572
+ Examples of things to look for:
1573
+ - A method with an invalid signature (e.g. two \`@RequestBody\` on the same handler)
1574
+ - A configuration that looks wrong or missing (e.g. webhook path not whitelisted in SecurityConfig)
1575
+ - A component scan / DI issue (e.g. a Spring bean not picked up because the package isn't scanned)
1576
+ - A DB constraint that will break when you add a new enum value
1577
+ - A hardcoded value that should be dynamic (e.g. hardcoded tenant id "default-tenant")
1578
+ - Anything that will silently break in production
1579
+
1580
+ \u2192 If yes, call **\`mem_save\`** with \`type="gotcha"\`, \`scope="team"\`, and **anchor it to the file** with \`paths\`.
1581
+ This transforms your discovery into institutional knowledge that protects every future agent.
1582
+
1288
1583
  ### 1. Did you try an approach that failed?
1289
1584
  \u2192 If yes, call **\`mem_tried\`** with:
1290
1585
  - \`what\`: the approach you tried (e.g. "importing gray-matter with ESM dynamic import")
@@ -1299,7 +1594,7 @@ Go through each item. If the answer is yes, call the corresponding tool immediat
1299
1594
  ### 3. Did you make an architectural decision?
1300
1595
  \u2192 If yes, call **\`mem_save\`** with \`type="decision"\` and document the WHY (constraints, tradeoffs), not just the what
1301
1596
 
1302
- ### 4. Did you hit a non-obvious bug or surprising behavior?
1597
+ ### 4. Did you hit a non-obvious bug or surprising behavior in a library or framework?
1303
1598
  \u2192 If yes, call **\`mem_save\`** with \`type="gotcha"\` and anchor it to the relevant file paths
1304
1599
 
1305
1600
  ### 5. Did you find that an existing memory is outdated or wrong?
@@ -1311,8 +1606,20 @@ Go through each item. If the answer is yes, call the corresponding tool immediat
1311
1606
  - Anchor memories to file paths when possible (the \`paths\` field) \u2014 this enables staleness detection.
1312
1607
  - Prefer \`scope="team"\` for anything a teammate or future agent would benefit from.
1313
1608
  - Skip sections where you genuinely have nothing to add. Don't fabricate memories.
1609
+ - **Question 0 is not optional** \u2014 always scan your exploration history for code-level discoveries.
1610
+
1611
+ ### 6. Close the session \u2014 always
1612
+ Call **\`mem_session_end\`** with:
1613
+ - \`goal\`: what you set out to do
1614
+ - \`accomplished\`: what was actually done (bullet list)
1615
+ - \`discoveries\`: anything surprising or broken found during this session (leave empty if none)
1616
+ - \`files_touched\`: the key files you read or modified
1617
+ - \`next_steps\`: what remains for the next session or a teammate
1618
+ - \`scope\`: "team" if this task affects the whole team, "personal" otherwise
1314
1619
 
1315
- When done, respond with a brief summary: "Saved N memories: [list of IDs]" or "Nothing new to save."
1620
+ This creates/updates a single rolling recap that **get_briefing automatically surfaces** at the start of every subsequent session \u2014 no token waste re-explaining what happened.
1621
+
1622
+ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
1316
1623
  `;
1317
1624
  return {
1318
1625
  description: "Post-task reflection: capture what you learned before closing the session",
@@ -1326,12 +1633,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]" or "N
1326
1633
  }
1327
1634
 
1328
1635
  // src/prompts/import-docs.ts
1329
- import { z as z20 } from "zod";
1636
+ import { z as z22 } from "zod";
1330
1637
  var ImportDocsArgsSchema = {
1331
- content: z20.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
1332
- source: z20.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
1333
- scope: z20.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
1334
- dry_run: z20.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
1638
+ content: z22.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
1639
+ source: z22.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
1640
+ scope: z22.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
1641
+ dry_run: z22.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
1335
1642
  };
1336
1643
  function importDocsPrompt(args, ctx) {
1337
1644
  const sourceLine = args.source ? `
@@ -1396,7 +1703,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
1396
1703
 
1397
1704
  // src/server.ts
1398
1705
  var SERVER_NAME = "haive";
1399
- var SERVER_VERSION = "0.2.10";
1706
+ var SERVER_VERSION = "0.2.12";
1400
1707
  function jsonResult(data) {
1401
1708
  return {
1402
1709
  content: [
@@ -1515,6 +1822,18 @@ function createHaiveServer(options = {}) {
1515
1822
  MemDiffInputSchema,
1516
1823
  async (input) => jsonResult(await memDiff(input, context))
1517
1824
  );
1825
+ server.tool(
1826
+ "mem_observe",
1827
+ "Capture a code-level discovery made during exploration: a bug, inconsistency, missing config, or security gap found by reading existing code that was NOT in the briefing. Use this whenever you read code and spot something that could silently break in production. Auto-validated, anchored to file paths for staleness detection. Prefer this over mem_save for reactive discoveries during code reading.",
1828
+ MemObserveInputSchema,
1829
+ async (input) => jsonResult(await memObserve(input, context))
1830
+ );
1831
+ server.tool(
1832
+ "mem_session_end",
1833
+ "Save a structured end-of-session recap (goal / accomplished / discoveries / next steps). Uses topic-upsert: one recap per scope is kept and updated in-place so the next session always has fresh context. Call this before closing every significant session. get_briefing automatically surfaces the latest recap at the top of the next session's briefing.",
1834
+ MemSessionEndInputSchema,
1835
+ async (input) => jsonResult(await memSessionEnd(input, context))
1836
+ );
1518
1837
  server.prompt(
1519
1838
  "bootstrap_project",
1520
1839
  "Instructions for the AI client to analyze the project and save the context.",