@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/index.js CHANGED
@@ -124,6 +124,7 @@ async function memList(input, ctx) {
124
124
  }
125
125
 
126
126
  // src/tools/mem-save.ts
127
+ import { createHash } from "crypto";
127
128
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
128
129
  import { existsSync as existsSync4 } from "fs";
129
130
  import path3 from "path";
@@ -135,7 +136,9 @@ import {
135
136
  } from "@hiveai/core";
136
137
  import { z as z4 } from "zod";
137
138
  var MemSaveInputSchema = {
138
- type: z4.enum(["convention", "decision", "gotcha", "architecture", "glossary", "attempt"]).describe("Kind of memory being saved. Use 'attempt' for failed approaches (auto-validated)."),
139
+ type: z4.enum(["convention", "decision", "gotcha", "architecture", "glossary", "attempt", "session_recap"]).describe(
140
+ "Kind of memory being saved. Use 'attempt' for failed approaches (auto-validated). Use 'session_recap' via mem_session_end instead."
141
+ ),
139
142
  slug: z4.string().min(1).describe("Short human-readable identifier \u2014 becomes part of the filename"),
140
143
  body: z4.string().describe("Markdown body of the memory"),
141
144
  scope: z4.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope: personal | team | module"),
@@ -145,14 +148,61 @@ var MemSaveInputSchema = {
145
148
  author: z4.string().optional().describe("Author handle or email"),
146
149
  paths: z4.array(z4.string()).default([]).describe("Anchor paths (file paths this memory references)"),
147
150
  symbols: z4.array(z4.string()).default([]).describe("Anchor symbols (function/class names this memory references)"),
148
- commit: z4.string().optional().describe("Anchor commit SHA (for staleness detection later)")
151
+ commit: z4.string().optional().describe("Anchor commit SHA (for staleness detection later)"),
152
+ topic: z4.string().optional().describe(
153
+ "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."
154
+ )
149
155
  };
156
+ function bodyHash(body) {
157
+ return createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
158
+ }
150
159
  async function memSave(input, ctx) {
151
160
  if (!existsSync4(ctx.paths.haiveDir)) {
152
161
  throw new Error(
153
162
  `No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`
154
163
  );
155
164
  }
165
+ const existing = existsSync4(ctx.paths.memoriesDir) ? await loadMemoriesFromDir2(ctx.paths.memoriesDir) : [];
166
+ const incomingHash = bodyHash(input.body);
167
+ const hashDuplicate = existing.find(
168
+ ({ memory }) => bodyHash(memory.body) === incomingHash && memory.frontmatter.scope === input.scope
169
+ );
170
+ if (hashDuplicate) {
171
+ throw new Error(
172
+ `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.`
173
+ );
174
+ }
175
+ if (input.topic) {
176
+ const topicMatch = existing.find(
177
+ ({ memory }) => memory.frontmatter.topic === input.topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
178
+ );
179
+ if (topicMatch) {
180
+ const fm = topicMatch.memory.frontmatter;
181
+ const newFrontmatter = {
182
+ ...fm,
183
+ body: input.body,
184
+ tags: input.tags.length ? input.tags : fm.tags,
185
+ revision_count: (fm.revision_count ?? 0) + 1,
186
+ anchor: {
187
+ commit: input.commit ?? fm.anchor.commit,
188
+ paths: input.paths.length ? input.paths : fm.anchor.paths,
189
+ symbols: input.symbols.length ? input.symbols : fm.anchor.symbols
190
+ }
191
+ };
192
+ await writeFile2(
193
+ topicMatch.filePath,
194
+ serializeMemory({ frontmatter: newFrontmatter, body: input.body }),
195
+ "utf8"
196
+ );
197
+ return {
198
+ id: fm.id,
199
+ scope: fm.scope,
200
+ file_path: topicMatch.filePath,
201
+ action: "updated",
202
+ revision_count: newFrontmatter.revision_count
203
+ };
204
+ }
205
+ }
156
206
  const frontmatter = buildFrontmatter({
157
207
  type: input.type,
158
208
  slug: input.slug,
@@ -163,7 +213,8 @@ async function memSave(input, ctx) {
163
213
  author: input.author,
164
214
  paths: input.paths,
165
215
  symbols: input.symbols,
166
- commit: input.commit
216
+ commit: input.commit,
217
+ topic: input.topic
167
218
  });
168
219
  const file = memoryFilePath(
169
220
  ctx.paths,
@@ -176,15 +227,16 @@ async function memSave(input, ctx) {
176
227
  throw new Error(`Memory already exists at ${file}`);
177
228
  }
178
229
  let warning;
179
- if (existsSync4(ctx.paths.memoriesDir)) {
180
- const existing = await loadMemoriesFromDir2(ctx.paths.memoriesDir);
230
+ let similar_found;
231
+ if (existing.length > 0) {
181
232
  const slugTokens = input.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
182
233
  const similar = existing.filter(({ memory }) => {
183
234
  const id = memory.frontmatter.id.toLowerCase();
184
235
  return slugTokens.length >= 2 && slugTokens.filter((t) => id.includes(t)).length >= Math.ceil(slugTokens.length * 0.6);
185
236
  });
186
237
  if (similar.length > 0) {
187
- warning = `Possible duplicate detected. Similar memories: ${similar.map((m) => m.memory.frontmatter.id).join(", ")}. Consider updating one of these instead.`;
238
+ similar_found = similar.map((m) => m.memory.frontmatter.id);
239
+ warning = `Possible duplicate: similar memories already exist (${similar_found.join(", ")}). Consider updating one of these instead.`;
188
240
  }
189
241
  }
190
242
  await writeFile2(file, serializeMemory({ frontmatter, body: input.body }), "utf8");
@@ -192,7 +244,9 @@ async function memSave(input, ctx) {
192
244
  id: frontmatter.id,
193
245
  scope: frontmatter.scope,
194
246
  file_path: file,
195
- ...warning ? { warning } : {}
247
+ action: "created",
248
+ ...warning ? { warning } : {},
249
+ ...similar_found ? { similar_found } : {}
196
250
  };
197
251
  }
198
252
 
@@ -526,10 +580,14 @@ async function memForFiles(input, ctx) {
526
580
  seen.add(loaded.memory.frontmatter.id);
527
581
  }
528
582
  }
583
+ const pathSegments = extractPathSegments(input.files);
529
584
  for (const loaded of all) {
530
585
  if (seen.has(loaded.memory.frontmatter.id)) continue;
531
586
  const fm = loaded.memory.frontmatter;
532
- const moduleHit = fm.module && inferred.includes(fm.module) || fm.tags.some((t) => inferred.includes(t));
587
+ const moduleHit = fm.module && inferred.includes(fm.module) || fm.tags.some((t) => inferred.includes(t)) || fm.tags.some((t) => {
588
+ const tl = t.toLowerCase();
589
+ return pathSegments.has(tl) || pathSegments.has(tl.replace(/[-_]/g, ""));
590
+ });
533
591
  if (moduleHit) {
534
592
  byModule.push(toMatch(loaded, "module", usage));
535
593
  seen.add(fm.id);
@@ -571,6 +629,52 @@ function toMatch(loaded, reason, usage) {
571
629
  body: loaded.memory.body
572
630
  };
573
631
  }
632
+ function extractPathSegments(files) {
633
+ const GENERIC = /* @__PURE__ */ new Set([
634
+ "src",
635
+ "main",
636
+ "java",
637
+ "kotlin",
638
+ "python",
639
+ "go",
640
+ "lib",
641
+ "libs",
642
+ "com",
643
+ "org",
644
+ "net",
645
+ "io",
646
+ "app",
647
+ "apps",
648
+ "pkg",
649
+ "internal",
650
+ "test",
651
+ "tests",
652
+ "spec",
653
+ "specs",
654
+ "impl",
655
+ "domain",
656
+ "shared",
657
+ "resources",
658
+ "static",
659
+ "assets",
660
+ "config",
661
+ "configs"
662
+ ]);
663
+ const out = /* @__PURE__ */ new Set();
664
+ for (const file of files) {
665
+ const parts = file.replace(/\\/g, "/").split("/");
666
+ for (const part of parts) {
667
+ const seg = part.toLowerCase().replace(/\.[^.]+$/, "");
668
+ if (seg.length >= 3 && !GENERIC.has(seg) && /^[a-z]/.test(seg)) {
669
+ out.add(seg);
670
+ for (const sub of seg.split(/[-_]/).filter((s) => s.length >= 3)) {
671
+ out.add(sub);
672
+ }
673
+ }
674
+ }
675
+ }
676
+ return out;
677
+ }
574
678
  async function loadModuleContexts(ctx, modules, enabled) {
575
679
  if (!enabled || modules.length === 0) return [];
576
680
  if (!existsSync8(ctx.paths.modulesContextDir)) return [];
@@ -850,10 +954,172 @@ async function memTried(input, ctx) {
850
954
  return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
851
955
  }
852
956
 
853
- // src/tools/get-briefing.ts
854
- import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
957
+ // src/tools/mem-observe.ts
958
+ import { mkdir as mkdir4, writeFile as writeFile8 } from "fs/promises";
855
959
  import { existsSync as existsSync15 } from "fs";
856
960
  import path6 from "path";
961
+ import {
962
+ buildFrontmatter as buildFrontmatter3,
963
+ memoryFilePath as memoryFilePath3,
964
+ serializeMemory as serializeMemory7
965
+ } from "@hiveai/core";
966
+ import { z as z15 } from "zod";
967
+ var MemObserveInputSchema = {
968
+ what: z15.string().min(1).describe("Short title: what did you observe? (e.g. 'MobilePaymentController has two @RequestBody on handleWebhook')"),
969
+ where: z15.string().min(1).describe("File path(s) where the issue lives \u2014 be specific"),
970
+ impact: z15.string().min(1).describe("What breaks or could break because of this (e.g. 'Spring MVC rejects the handler at startup')"),
971
+ fix: z15.string().optional().describe("Suggested fix or workaround (optional \u2014 leave empty if unknown)"),
972
+ scope: z15.enum(["personal", "team", "module"]).default("team").describe("Visibility scope \u2014 defaults to team since discoveries benefit everyone"),
973
+ module: z15.string().optional().describe("Module name (required when scope=module)"),
974
+ tags: z15.array(z15.string()).default([]).describe("Tags for filtering"),
975
+ author: z15.string().optional().describe("Author handle or email")
976
+ };
977
+ async function memObserve(input, ctx) {
978
+ if (!existsSync15(ctx.paths.haiveDir)) {
979
+ throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`);
980
+ }
981
+ const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-");
982
+ const anchorPaths = input.where.split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
983
+ const baseFm = buildFrontmatter3({
984
+ type: "gotcha",
985
+ slug,
986
+ scope: input.scope,
987
+ module: input.module,
988
+ tags: input.tags,
989
+ paths: anchorPaths,
990
+ author: input.author
991
+ });
992
+ const frontmatter = { ...baseFm, status: "validated" };
993
+ const lines = [`# ${input.what}`, ""];
994
+ lines.push(`**Where:** \`${input.where}\``);
995
+ lines.push("", `**Impact:** ${input.impact}`);
996
+ if (input.fix) {
997
+ lines.push("", `**Fix/workaround:** ${input.fix}`);
998
+ }
999
+ const body = lines.join("\n") + "\n";
1000
+ const file = memoryFilePath3(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1001
+ await mkdir4(path6.dirname(file), { recursive: true });
1002
+ if (existsSync15(file)) {
1003
+ throw new Error(`Memory already exists at ${file}`);
1004
+ }
1005
+ await writeFile8(file, serializeMemory7({ frontmatter, body }), "utf8");
1006
+ return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
1007
+ }
1008
+
1009
+ // src/tools/mem-session-end.ts
1010
+ import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
1011
+ import { existsSync as existsSync16 } from "fs";
1012
+ import path7 from "path";
1013
+ import {
1014
+ buildFrontmatter as buildFrontmatter4,
1015
+ loadMemoriesFromDir as loadMemoriesFromDir12,
1016
+ memoryFilePath as memoryFilePath4,
1017
+ serializeMemory as serializeMemory8
1018
+ } from "@hiveai/core";
1019
+ import { z as z16 } from "zod";
1020
+ var MemSessionEndInputSchema = {
1021
+ goal: z16.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1022
+ accomplished: z16.string().describe("What was actually done \u2014 bullet list recommended"),
1023
+ discoveries: z16.string().default("").describe(
1024
+ "Any bugs, inconsistencies, surprises, or missing knowledge found during this session. Empty if nothing surprising was found."
1025
+ ),
1026
+ files_touched: z16.array(z16.string()).default([]).describe("Key files that were read or modified \u2014 used as anchor paths"),
1027
+ next_steps: z16.string().default("").describe("What should happen next (for the next session or a teammate)"),
1028
+ scope: z16.enum(["personal", "team", "module"]).default("personal").describe("Visibility: personal = private to you, team = shared with the team"),
1029
+ module: z16.string().optional().describe("Module name (required when scope=module)")
1030
+ };
1031
+ function recapTopic(scope, module) {
1032
+ return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
1033
+ }
1034
+ function buildBody(input) {
1035
+ const lines = [];
1036
+ lines.push(`## Goal
1037
+ ${input.goal}`);
1038
+ lines.push(`
1039
+ ## Accomplished
1040
+ ${input.accomplished}`);
1041
+ if (input.discoveries.trim()) {
1042
+ lines.push(`
1043
+ ## Discoveries & surprises
1044
+ ${input.discoveries}`);
1045
+ }
1046
+ if (input.files_touched.length > 0) {
1047
+ lines.push(`
1048
+ ## Files touched
1049
+ ${input.files_touched.map((f) => `- \`${f}\``).join("\n")}`);
1050
+ }
1051
+ if (input.next_steps.trim()) {
1052
+ lines.push(`
1053
+ ## Next steps
1054
+ ${input.next_steps}`);
1055
+ }
1056
+ return lines.join("\n");
1057
+ }
1058
+ async function memSessionEnd(input, ctx) {
1059
+ if (!existsSync16(ctx.paths.haiveDir)) {
1060
+ throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`);
1061
+ }
1062
+ const body = buildBody(input);
1063
+ const topic = recapTopic(input.scope, input.module);
1064
+ const existing = existsSync16(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1065
+ const topicMatch = existing.find(
1066
+ ({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
1067
+ );
1068
+ if (topicMatch) {
1069
+ const fm = topicMatch.memory.frontmatter;
1070
+ const revisionCount = (fm.revision_count ?? 0) + 1;
1071
+ const newFrontmatter = {
1072
+ ...fm,
1073
+ revision_count: revisionCount,
1074
+ anchor: {
1075
+ ...fm.anchor,
1076
+ paths: input.files_touched.length ? input.files_touched : fm.anchor.paths
1077
+ }
1078
+ };
1079
+ await writeFile9(
1080
+ topicMatch.filePath,
1081
+ serializeMemory8({ frontmatter: newFrontmatter, body }),
1082
+ "utf8"
1083
+ );
1084
+ return {
1085
+ id: fm.id,
1086
+ scope: fm.scope,
1087
+ file_path: topicMatch.filePath,
1088
+ action: "updated",
1089
+ revision_count: revisionCount
1090
+ };
1091
+ }
1092
+ const frontmatter = buildFrontmatter4({
1093
+ type: "session_recap",
1094
+ slug: "session-recap",
1095
+ scope: input.scope,
1096
+ module: input.module,
1097
+ tags: ["session", "recap"],
1098
+ paths: input.files_touched,
1099
+ topic,
1100
+ status: "validated"
1101
+ });
1102
+ const file = memoryFilePath4(
1103
+ ctx.paths,
1104
+ frontmatter.scope,
1105
+ frontmatter.id,
1106
+ frontmatter.module
1107
+ );
1108
+ await mkdir5(path7.dirname(file), { recursive: true });
1109
+ await writeFile9(file, serializeMemory8({ frontmatter, body }), "utf8");
1110
+ return {
1111
+ id: frontmatter.id,
1112
+ scope: frontmatter.scope,
1113
+ file_path: file,
1114
+ action: "created",
1115
+ revision_count: 0
1116
+ };
1117
+ }
1118
+
1119
+ // src/tools/get-briefing.ts
1120
+ import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
1121
+ import { existsSync as existsSync17 } from "fs";
1122
+ import path8 from "path";
857
1123
  import {
858
1124
  allocateBudget,
859
1125
  deriveConfidence as deriveConfidence4,
@@ -862,31 +1128,31 @@ import {
862
1128
  inferModulesFromPaths as inferModulesFromPaths2,
863
1129
  isDecaying,
864
1130
  literalMatchesAllTokens as literalMatchesAllTokens2,
865
- loadMemoriesFromDir as loadMemoriesFromDir12,
1131
+ loadMemoriesFromDir as loadMemoriesFromDir13,
866
1132
  loadUsageIndex as loadUsageIndex7,
867
1133
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
868
1134
  tokenizeQuery as tokenizeQuery2,
869
1135
  trackReads as trackReads3,
870
1136
  truncateToTokens
871
1137
  } from "@hiveai/core";
872
- import { z as z15 } from "zod";
1138
+ import { z as z17 } from "zod";
873
1139
  var GetBriefingInputSchema = {
874
- task: z15.string().optional().describe(
1140
+ task: z17.string().optional().describe(
875
1141
  "What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
876
1142
  ),
877
- files: z15.array(z15.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
878
- max_tokens: z15.number().int().positive().default(8e3).describe(
1143
+ files: z17.array(z17.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
1144
+ max_tokens: z17.number().int().positive().default(8e3).describe(
879
1145
  "Approximate token budget for the entire briefing. Each section is allocated a share and truncated to fit."
880
1146
  ),
881
- max_memories: z15.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
882
- include_project_context: z15.boolean().default(true),
883
- include_module_contexts: z15.boolean().default(true),
884
- semantic: z15.boolean().default(true).describe(
1147
+ max_memories: z17.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
1148
+ include_project_context: z17.boolean().default(true),
1149
+ include_module_contexts: z17.boolean().default(true),
1150
+ semantic: z17.boolean().default(true).describe(
885
1151
  "Use semantic ranking when a task is provided (requires `haive embeddings index`)."
886
1152
  ),
887
- include_stale: z15.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
888
- track: z15.boolean().default(true).describe("Increment read_count on returned memories"),
889
- format: z15.enum(["full", "compact"]).default("full").describe(
1153
+ include_stale: z17.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
1154
+ track: z17.boolean().default(true).describe("Increment read_count on returned memories"),
1155
+ format: z17.enum(["full", "compact"]).default("full").describe(
890
1156
  "Output format: 'full' returns complete memory bodies; 'compact' returns id + 1-line summary only (call mem_get for details)."
891
1157
  )
892
1158
  };
@@ -896,12 +1162,27 @@ async function getBriefing(input, ctx) {
896
1162
  let searchMode = "literal";
897
1163
  let usage = { version: 1, updated_at: "", by_id: {} };
898
1164
  let byId = /* @__PURE__ */ new Map();
899
- if (existsSync15(ctx.paths.memoriesDir)) {
900
- const allLoaded = await loadMemoriesFromDir12(ctx.paths.memoriesDir);
1165
+ let lastSession;
1166
+ if (existsSync17(ctx.paths.memoriesDir)) {
1167
+ const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1168
+ const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
1169
+ (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
1170
+ );
1171
+ if (recaps.length > 0) {
1172
+ const r = recaps[0];
1173
+ const fm = r.memory.frontmatter;
1174
+ lastSession = {
1175
+ id: fm.id,
1176
+ scope: fm.scope,
1177
+ revision_count: fm.revision_count ?? 0,
1178
+ body: r.memory.body
1179
+ };
1180
+ }
901
1181
  const allMemories = allLoaded.filter(({ memory }) => {
902
1182
  const s = memory.frontmatter.status;
903
1183
  if (s === "rejected" || s === "deprecated") return false;
904
1184
  if (!input.include_stale && s === "stale") return false;
1185
+ if (memory.frontmatter.type === "session_recap") return false;
905
1186
  return true;
906
1187
  });
907
1188
  usage = await loadUsageIndex7(ctx.paths);
@@ -991,7 +1272,7 @@ async function getBriefing(input, ctx) {
991
1272
  await trackReads3(ctx.paths, memories.map((m) => m.id));
992
1273
  }
993
1274
  }
994
- const projectContext = input.include_project_context && existsSync15(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1275
+ const projectContext = input.include_project_context && existsSync17(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
995
1276
  const moduleContents = input.include_module_contexts ? await loadModuleContexts2(ctx, inferred) : [];
996
1277
  const memoriesText = memories.map((m) => {
997
1278
  const unverified = m.status === "proposed" ? " [UNVERIFIED \u2014 not yet validated]" : "";
@@ -1057,6 +1338,7 @@ ${m.content}`).join("\n\n---\n\n"),
1057
1338
  ...input.task ? { task: input.task } : {},
1058
1339
  search_mode: searchMode,
1059
1340
  inferred_modules: inferred,
1341
+ ...lastSession ? { last_session: lastSession } : {},
1060
1342
  project_context: projectContext ? { content: projectSlice.text, truncated: projectSlice.truncated } : null,
1061
1343
  module_contexts: trimmedModules,
1062
1344
  memories: outputMemories,
@@ -1092,15 +1374,15 @@ async function trySemanticHits(ctx, task, limit) {
1092
1374
  }
1093
1375
  async function loadModuleContexts2(ctx, modules) {
1094
1376
  if (modules.length === 0) return [];
1095
- if (!existsSync15(ctx.paths.modulesContextDir)) return [];
1377
+ if (!existsSync17(ctx.paths.modulesContextDir)) return [];
1096
1378
  const available = new Set(
1097
1379
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
1098
1380
  );
1099
1381
  const out = [];
1100
1382
  for (const m of modules) {
1101
1383
  if (!available.has(m)) continue;
1102
- const file = path6.join(ctx.paths.modulesContextDir, m, "context.md");
1103
- if (existsSync15(file)) {
1384
+ const file = path8.join(ctx.paths.modulesContextDir, m, "context.md");
1385
+ if (existsSync17(file)) {
1104
1386
  out.push({ name: m, content: await readFile3(file, "utf8") });
1105
1387
  }
1106
1388
  }
@@ -1109,11 +1391,11 @@ async function loadModuleContexts2(ctx, modules) {
1109
1391
 
1110
1392
  // src/tools/code-map.ts
1111
1393
  import { loadCodeMap, queryCodeMap } from "@hiveai/core";
1112
- import { z as z16 } from "zod";
1394
+ import { z as z18 } from "zod";
1113
1395
  var CodeMapInputSchema = {
1114
- file: z16.string().optional().describe("Filter to files whose path contains this substring"),
1115
- symbol: z16.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
1116
- max_files: z16.number().int().positive().default(40).describe("Cap on returned files")
1396
+ file: z18.string().optional().describe("Filter to files whose path contains this substring"),
1397
+ symbol: z18.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
1398
+ max_files: z18.number().int().positive().default(40).describe("Cap on returned files")
1117
1399
  };
1118
1400
  async function codeMapTool(input, ctx) {
1119
1401
  const map = await loadCodeMap(ctx.paths);
@@ -1139,18 +1421,18 @@ async function codeMapTool(input, ctx) {
1139
1421
  }
1140
1422
 
1141
1423
  // src/tools/mem-diff.ts
1142
- import { existsSync as existsSync16 } from "fs";
1143
- import { loadMemoriesFromDir as loadMemoriesFromDir13 } from "@hiveai/core";
1144
- import { z as z17 } from "zod";
1424
+ import { existsSync as existsSync18 } from "fs";
1425
+ import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
1426
+ import { z as z19 } from "zod";
1145
1427
  var MemDiffInputSchema = {
1146
- id_a: z17.string().min(1).describe("First memory id"),
1147
- id_b: z17.string().min(1).describe("Second memory id")
1428
+ id_a: z19.string().min(1).describe("First memory id"),
1429
+ id_b: z19.string().min(1).describe("Second memory id")
1148
1430
  };
1149
1431
  async function memDiff(input, ctx) {
1150
- if (!existsSync16(ctx.paths.memoriesDir)) {
1432
+ if (!existsSync18(ctx.paths.memoriesDir)) {
1151
1433
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
1152
1434
  }
1153
- const all = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1435
+ const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
1154
1436
  const foundA = all.find((m) => m.memory.frontmatter.id === input.id_a);
1155
1437
  const foundB = all.find((m) => m.memory.frontmatter.id === input.id_b);
1156
1438
  if (!foundA) throw new Error(`No memory with id "${input.id_a}".`);
@@ -1184,12 +1466,12 @@ async function memDiff(input, ctx) {
1184
1466
  }
1185
1467
 
1186
1468
  // src/prompts/bootstrap-project.ts
1187
- import { z as z18 } from "zod";
1469
+ import { z as z20 } from "zod";
1188
1470
  var BootstrapProjectArgsSchema = {
1189
- module: z18.string().optional().describe(
1471
+ module: z20.string().optional().describe(
1190
1472
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
1191
1473
  ),
1192
- focus: z18.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
1474
+ focus: z20.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
1193
1475
  };
1194
1476
  var ROOT_TEMPLATE = `# Project context
1195
1477
 
@@ -1271,10 +1553,10 @@ ${template}\`\`\`
1271
1553
  }
1272
1554
 
1273
1555
  // src/prompts/post-task.ts
1274
- import { z as z19 } from "zod";
1556
+ import { z as z21 } from "zod";
1275
1557
  var PostTaskArgsSchema = {
1276
- task_summary: z19.string().optional().describe("One sentence describing what you just did"),
1277
- files_touched: z19.array(z19.string()).optional().describe("Files you created or modified during the task")
1558
+ task_summary: z21.string().optional().describe("One sentence describing what you just did"),
1559
+ files_touched: z21.array(z21.string()).optional().describe("Files you created or modified during the task")
1278
1560
  };
1279
1561
  function postTaskPrompt(args, ctx) {
1280
1562
  const taskLine = args.task_summary ? `
@@ -1290,6 +1572,19 @@ Project root: \`${ctx.paths.root}\`
1290
1572
 
1291
1573
  Go through each item. If the answer is yes, call the corresponding tool immediately.
1292
1574
 
1575
+ ### 0. Did you read existing code and discover bugs, inconsistencies, or security gaps that weren't in the briefing?
1576
+ This is the most important question. Deep code reading surfaces issues that no memory captures yet.
1577
+ Examples of things to look for:
1578
+ - A method with an invalid signature (e.g. two \`@RequestBody\` on the same handler)
1579
+ - A configuration that looks wrong or missing (e.g. webhook path not whitelisted in SecurityConfig)
1580
+ - A component scan / DI issue (e.g. a Spring bean not picked up because the package isn't scanned)
1581
+ - A DB constraint that will break when you add a new enum value
1582
+ - A hardcoded value that should be dynamic (e.g. hardcoded tenant id "default-tenant")
1583
+ - Anything that will silently break in production
1584
+
1585
+ \u2192 If yes, call **\`mem_save\`** with \`type="gotcha"\`, \`scope="team"\`, and **anchor it to the file** with \`paths\`.
1586
+ This transforms your discovery into institutional knowledge that protects every future agent.
1587
+
1293
1588
  ### 1. Did you try an approach that failed?
1294
1589
  \u2192 If yes, call **\`mem_tried\`** with:
1295
1590
  - \`what\`: the approach you tried (e.g. "importing gray-matter with ESM dynamic import")
@@ -1304,7 +1599,7 @@ Go through each item. If the answer is yes, call the corresponding tool immediat
1304
1599
  ### 3. Did you make an architectural decision?
1305
1600
  \u2192 If yes, call **\`mem_save\`** with \`type="decision"\` and document the WHY (constraints, tradeoffs), not just the what
1306
1601
 
1307
- ### 4. Did you hit a non-obvious bug or surprising behavior?
1602
+ ### 4. Did you hit a non-obvious bug or surprising behavior in a library or framework?
1308
1603
  \u2192 If yes, call **\`mem_save\`** with \`type="gotcha"\` and anchor it to the relevant file paths
1309
1604
 
1310
1605
  ### 5. Did you find that an existing memory is outdated or wrong?
@@ -1316,8 +1611,20 @@ Go through each item. If the answer is yes, call the corresponding tool immediat
1316
1611
  - Anchor memories to file paths when possible (the \`paths\` field) \u2014 this enables staleness detection.
1317
1612
  - Prefer \`scope="team"\` for anything a teammate or future agent would benefit from.
1318
1613
  - Skip sections where you genuinely have nothing to add. Don't fabricate memories.
1614
+ - **Question 0 is not optional** \u2014 always scan your exploration history for code-level discoveries.
1615
+
1616
+ ### 6. Close the session \u2014 always
1617
+ Call **\`mem_session_end\`** with:
1618
+ - \`goal\`: what you set out to do
1619
+ - \`accomplished\`: what was actually done (bullet list)
1620
+ - \`discoveries\`: anything surprising or broken found during this session (leave empty if none)
1621
+ - \`files_touched\`: the key files you read or modified
1622
+ - \`next_steps\`: what remains for the next session or a teammate
1623
+ - \`scope\`: "team" if this task affects the whole team, "personal" otherwise
1319
1624
 
1320
- When done, respond with a brief summary: "Saved N memories: [list of IDs]" or "Nothing new to save."
1625
+ 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.
1626
+
1627
+ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
1321
1628
  `;
1322
1629
  return {
1323
1630
  description: "Post-task reflection: capture what you learned before closing the session",
@@ -1331,12 +1638,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]" or "N
1331
1638
  }
1332
1639
 
1333
1640
  // src/prompts/import-docs.ts
1334
- import { z as z20 } from "zod";
1641
+ import { z as z22 } from "zod";
1335
1642
  var ImportDocsArgsSchema = {
1336
- content: z20.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
1337
- source: z20.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
1338
- scope: z20.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
1339
- dry_run: z20.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
1643
+ content: z22.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
1644
+ source: z22.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
1645
+ scope: z22.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
1646
+ dry_run: z22.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
1340
1647
  };
1341
1648
  function importDocsPrompt(args, ctx) {
1342
1649
  const sourceLine = args.source ? `
@@ -1401,7 +1708,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
1401
1708
 
1402
1709
  // src/server.ts
1403
1710
  var SERVER_NAME = "haive";
1404
- var SERVER_VERSION = "0.2.10";
1711
+ var SERVER_VERSION = "0.2.12";
1405
1712
  function jsonResult(data) {
1406
1713
  return {
1407
1714
  content: [
@@ -1520,6 +1827,18 @@ function createHaiveServer(options = {}) {
1520
1827
  MemDiffInputSchema,
1521
1828
  async (input) => jsonResult(await memDiff(input, context))
1522
1829
  );
1830
+ server.tool(
1831
+ "mem_observe",
1832
+ "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.",
1833
+ MemObserveInputSchema,
1834
+ async (input) => jsonResult(await memObserve(input, context))
1835
+ );
1836
+ server.tool(
1837
+ "mem_session_end",
1838
+ "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.",
1839
+ MemSessionEndInputSchema,
1840
+ async (input) => jsonResult(await memSessionEnd(input, context))
1841
+ );
1523
1842
  server.prompt(
1524
1843
  "bootstrap_project",
1525
1844
  "Instructions for the AI client to analyze the project and save the context.",