gitmem-mcp 1.4.3 → 1.5.1
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/CHANGELOG.md +25 -0
- package/bin/gitmem.js +2 -1
- package/dist/hooks/format-utils.js +4 -0
- package/dist/schemas/session-close.d.ts +15 -15
- package/dist/schemas/session-close.js +3 -3
- package/dist/server.js +13 -0
- package/dist/services/doc-chunker.d.ts +45 -0
- package/dist/services/doc-chunker.js +208 -0
- package/dist/services/doc-index.d.ts +88 -0
- package/dist/services/doc-index.js +328 -0
- package/dist/services/enforcement.js +0 -1
- package/dist/tools/definitions.d.ts +688 -0
- package/dist/tools/definitions.js +87 -0
- package/dist/tools/index-docs.d.ts +30 -0
- package/dist/tools/index-docs.js +163 -0
- package/dist/tools/prepare-context.js +7 -0
- package/dist/tools/recall.js +10 -1
- package/dist/tools/search-docs.d.ts +38 -0
- package/dist/tools/search-docs.js +94 -0
- package/dist/tools/search.js +11 -1
- package/dist/tools/session-close.js +45 -2
- package/dist/tools/session-start.js +26 -1
- package/package.json +1 -1
|
@@ -2297,6 +2297,92 @@ export const TOOLS = [
|
|
|
2297
2297
|
required: ["type", "tool", "description", "severity"],
|
|
2298
2298
|
},
|
|
2299
2299
|
},
|
|
2300
|
+
{
|
|
2301
|
+
name: "index_docs",
|
|
2302
|
+
description: "Scan a directory of markdown files, chunk them, embed them, and store them in a local doc index for semantic search. Supports incremental indexing: only re-processes changed files. Use search_docs to query the indexed docs.",
|
|
2303
|
+
inputSchema: {
|
|
2304
|
+
type: "object",
|
|
2305
|
+
properties: {
|
|
2306
|
+
directory: {
|
|
2307
|
+
type: "string",
|
|
2308
|
+
description: "Absolute path to directory containing .md files to index",
|
|
2309
|
+
},
|
|
2310
|
+
project: {
|
|
2311
|
+
type: "string",
|
|
2312
|
+
description: "Project namespace (e.g., 'my-project'). Scopes sessions and searches.",
|
|
2313
|
+
},
|
|
2314
|
+
exclude: {
|
|
2315
|
+
type: "array",
|
|
2316
|
+
items: { type: "string" },
|
|
2317
|
+
description: "Directory names to exclude (default: ['_archive', 'node_modules', '.git'])",
|
|
2318
|
+
},
|
|
2319
|
+
force: {
|
|
2320
|
+
type: "boolean",
|
|
2321
|
+
description: "Force re-index all files even if unchanged (default: false)",
|
|
2322
|
+
},
|
|
2323
|
+
clear: {
|
|
2324
|
+
type: "boolean",
|
|
2325
|
+
description: "Clear the doc index for this project before indexing (default: false)",
|
|
2326
|
+
},
|
|
2327
|
+
},
|
|
2328
|
+
required: ["directory"],
|
|
2329
|
+
},
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
name: "search_docs",
|
|
2333
|
+
description: "Search indexed repository documentation using semantic similarity (pro/dev tier) or BM25 keyword search (free tier). Returns relevant chunks with file paths for targeted reading. Index docs first with index_docs.",
|
|
2334
|
+
inputSchema: {
|
|
2335
|
+
type: "object",
|
|
2336
|
+
properties: {
|
|
2337
|
+
query: {
|
|
2338
|
+
type: "string",
|
|
2339
|
+
description: "Natural language search query (e.g., 'how does authentication work', 'database schema')",
|
|
2340
|
+
},
|
|
2341
|
+
project: {
|
|
2342
|
+
type: "string",
|
|
2343
|
+
description: "Project namespace (e.g., 'my-project'). Scopes sessions and searches.",
|
|
2344
|
+
},
|
|
2345
|
+
category: {
|
|
2346
|
+
type: "string",
|
|
2347
|
+
description: "Filter results to a specific category (directory name, e.g., 'architecture', 'research')",
|
|
2348
|
+
},
|
|
2349
|
+
match_count: {
|
|
2350
|
+
type: "number",
|
|
2351
|
+
description: "Maximum number of results to return (default: 5)",
|
|
2352
|
+
},
|
|
2353
|
+
},
|
|
2354
|
+
required: ["query"],
|
|
2355
|
+
},
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
name: "gitmem-idx",
|
|
2359
|
+
description: "Alias for index_docs. Scan a directory of markdown files and index them for semantic search.",
|
|
2360
|
+
inputSchema: {
|
|
2361
|
+
type: "object",
|
|
2362
|
+
properties: {
|
|
2363
|
+
directory: { type: "string", description: "Absolute path to directory containing .md files" },
|
|
2364
|
+
project: { type: "string", description: "Project namespace" },
|
|
2365
|
+
exclude: { type: "array", items: { type: "string" }, description: "Directory names to exclude" },
|
|
2366
|
+
force: { type: "boolean", description: "Force re-index all files" },
|
|
2367
|
+
clear: { type: "boolean", description: "Clear index before indexing" },
|
|
2368
|
+
},
|
|
2369
|
+
required: ["directory"],
|
|
2370
|
+
},
|
|
2371
|
+
},
|
|
2372
|
+
{
|
|
2373
|
+
name: "gitmem-sd",
|
|
2374
|
+
description: "Alias for search_docs. Search indexed repository documentation.",
|
|
2375
|
+
inputSchema: {
|
|
2376
|
+
type: "object",
|
|
2377
|
+
properties: {
|
|
2378
|
+
query: { type: "string", description: "Natural language search query" },
|
|
2379
|
+
project: { type: "string", description: "Project namespace" },
|
|
2380
|
+
category: { type: "string", description: "Filter by category" },
|
|
2381
|
+
match_count: { type: "number", description: "Max results (default: 5)" },
|
|
2382
|
+
},
|
|
2383
|
+
required: ["query"],
|
|
2384
|
+
},
|
|
2385
|
+
},
|
|
2300
2386
|
];
|
|
2301
2387
|
/**
|
|
2302
2388
|
* Alias tool names — filtered out by default to reduce context window cost.
|
|
@@ -2312,6 +2398,7 @@ export const ALIAS_TOOL_NAMES = new Set([
|
|
|
2312
2398
|
"gitmem-pc", "gitmem-ao",
|
|
2313
2399
|
"gitmem-lt", "gitmem-rt", "gitmem-ct", "gitmem-ps", "gitmem-ds",
|
|
2314
2400
|
"gitmem-cleanup", "gitmem-health", "gitmem-al", "gitmem-graph", "gitmem-fb",
|
|
2401
|
+
"gitmem-idx", "gitmem-sd",
|
|
2315
2402
|
// gm-* aliases
|
|
2316
2403
|
"gm-open", "gm-confirm", "gm-reflect", "gm-refresh", "gm-close",
|
|
2317
2404
|
"gm-scar", "gm-search", "gm-log", "gm-analyze",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index_docs Tool
|
|
3
|
+
*
|
|
4
|
+
* Scans a directory of markdown files, chunks them, embeds them,
|
|
5
|
+
* and stores them in the doc index for semantic search.
|
|
6
|
+
*
|
|
7
|
+
* Supports incremental indexing: only re-processes changed files.
|
|
8
|
+
*/
|
|
9
|
+
import type { Project } from "../types/index.js";
|
|
10
|
+
export interface IndexDocsParams {
|
|
11
|
+
directory: string;
|
|
12
|
+
project?: Project;
|
|
13
|
+
exclude?: string[];
|
|
14
|
+
force?: boolean;
|
|
15
|
+
clear?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface IndexDocsResult {
|
|
18
|
+
directory: string;
|
|
19
|
+
project: string;
|
|
20
|
+
files_scanned: number;
|
|
21
|
+
files_changed: number;
|
|
22
|
+
files_unchanged: number;
|
|
23
|
+
chunks_indexed: number;
|
|
24
|
+
chunks_embedded: number;
|
|
25
|
+
errors: number;
|
|
26
|
+
categories: Record<string, number>;
|
|
27
|
+
display: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function indexDocs(params: IndexDocsParams): Promise<IndexDocsResult>;
|
|
30
|
+
//# sourceMappingURL=index-docs.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index_docs Tool
|
|
3
|
+
*
|
|
4
|
+
* Scans a directory of markdown files, chunks them, embeds them,
|
|
5
|
+
* and stores them in the doc index for semantic search.
|
|
6
|
+
*
|
|
7
|
+
* Supports incremental indexing: only re-processes changed files.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import { scanDirectory, chunkDocument } from "../services/doc-chunker.js";
|
|
11
|
+
import { indexChunks, getChangedFiles, getIndexStats, clearDocIndex, } from "../services/doc-index.js";
|
|
12
|
+
import { getProject } from "../services/session-state.js";
|
|
13
|
+
import { wrapDisplay, productLine, dimText } from "../services/display-protocol.js";
|
|
14
|
+
export async function indexDocs(params) {
|
|
15
|
+
const directory = params.directory;
|
|
16
|
+
const project = params.project || getProject() || "default";
|
|
17
|
+
const exclude = params.exclude || ["_archive", "node_modules", ".git"];
|
|
18
|
+
const force = params.force || false;
|
|
19
|
+
// Validate directory exists
|
|
20
|
+
if (!fs.existsSync(directory)) {
|
|
21
|
+
const display = wrapDisplay([
|
|
22
|
+
productLine("index_docs", "error"),
|
|
23
|
+
"",
|
|
24
|
+
`Directory not found: ${directory}`,
|
|
25
|
+
"",
|
|
26
|
+
"Provide an absolute path to a directory containing .md files.",
|
|
27
|
+
].join("\n"));
|
|
28
|
+
return {
|
|
29
|
+
directory,
|
|
30
|
+
project,
|
|
31
|
+
files_scanned: 0,
|
|
32
|
+
files_changed: 0,
|
|
33
|
+
files_unchanged: 0,
|
|
34
|
+
chunks_indexed: 0,
|
|
35
|
+
chunks_embedded: 0,
|
|
36
|
+
errors: 1,
|
|
37
|
+
categories: {},
|
|
38
|
+
display,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Handle clear request
|
|
42
|
+
if (params.clear) {
|
|
43
|
+
const removed = clearDocIndex(project);
|
|
44
|
+
const display = wrapDisplay([
|
|
45
|
+
productLine("index_docs", `cleared ${removed} chunks for project="${project}"`),
|
|
46
|
+
].join("\n"));
|
|
47
|
+
return {
|
|
48
|
+
directory,
|
|
49
|
+
project,
|
|
50
|
+
files_scanned: 0,
|
|
51
|
+
files_changed: 0,
|
|
52
|
+
files_unchanged: 0,
|
|
53
|
+
chunks_indexed: 0,
|
|
54
|
+
chunks_embedded: 0,
|
|
55
|
+
errors: 0,
|
|
56
|
+
categories: {},
|
|
57
|
+
display,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Scan directory for .md files
|
|
61
|
+
const files = scanDirectory(directory, { exclude });
|
|
62
|
+
if (files.length === 0) {
|
|
63
|
+
const display = wrapDisplay([
|
|
64
|
+
productLine("index_docs", "no markdown files found"),
|
|
65
|
+
"",
|
|
66
|
+
`Scanned: ${directory}`,
|
|
67
|
+
`Excluded: ${exclude.join(", ")}`,
|
|
68
|
+
].join("\n"));
|
|
69
|
+
return {
|
|
70
|
+
directory,
|
|
71
|
+
project,
|
|
72
|
+
files_scanned: 0,
|
|
73
|
+
files_changed: 0,
|
|
74
|
+
files_unchanged: 0,
|
|
75
|
+
chunks_indexed: 0,
|
|
76
|
+
chunks_embedded: 0,
|
|
77
|
+
errors: 0,
|
|
78
|
+
categories: {},
|
|
79
|
+
display,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Incremental indexing: check which files changed
|
|
83
|
+
const fileHashes = new Map(files.map((f) => [f.relative_path, f.hash]));
|
|
84
|
+
let filesToProcess = files;
|
|
85
|
+
let filesUnchanged = 0;
|
|
86
|
+
if (!force) {
|
|
87
|
+
const changes = getChangedFiles(fileHashes, project);
|
|
88
|
+
const changedSet = new Set([...changes.changed, ...changes.new_files]);
|
|
89
|
+
if (changedSet.size === 0) {
|
|
90
|
+
const stats = getIndexStats(project);
|
|
91
|
+
const display = wrapDisplay([
|
|
92
|
+
productLine("index_docs", "up to date — no changes detected"),
|
|
93
|
+
"",
|
|
94
|
+
`${files.length} files scanned, all unchanged`,
|
|
95
|
+
`${stats.total_chunks} chunks in index`,
|
|
96
|
+
"",
|
|
97
|
+
dimText("Use force=true to re-index all files"),
|
|
98
|
+
].join("\n"));
|
|
99
|
+
return {
|
|
100
|
+
directory,
|
|
101
|
+
project,
|
|
102
|
+
files_scanned: files.length,
|
|
103
|
+
files_changed: 0,
|
|
104
|
+
files_unchanged: files.length,
|
|
105
|
+
chunks_indexed: 0,
|
|
106
|
+
chunks_embedded: 0,
|
|
107
|
+
errors: 0,
|
|
108
|
+
categories: stats.categories,
|
|
109
|
+
display,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
filesToProcess = files.filter((f) => changedSet.has(f.relative_path));
|
|
113
|
+
filesUnchanged = files.length - filesToProcess.length;
|
|
114
|
+
}
|
|
115
|
+
// Chunk the changed files
|
|
116
|
+
const allChunks = [];
|
|
117
|
+
for (const file of filesToProcess) {
|
|
118
|
+
allChunks.push(...chunkDocument(file));
|
|
119
|
+
}
|
|
120
|
+
// Index chunks (embed + store)
|
|
121
|
+
const result = await indexChunks(allChunks, project);
|
|
122
|
+
// Get updated stats
|
|
123
|
+
const stats = getIndexStats(project);
|
|
124
|
+
// Build display
|
|
125
|
+
const lines = [];
|
|
126
|
+
lines.push(productLine("index_docs", `${filesToProcess.length} files → ${result.indexed} chunks`));
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push(`Directory: ${directory}`);
|
|
129
|
+
lines.push(`Project: ${project}`);
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push(`Files scanned: ${files.length}`);
|
|
132
|
+
lines.push(`Files changed: ${filesToProcess.length}`);
|
|
133
|
+
lines.push(`Files unchanged: ${filesUnchanged}`);
|
|
134
|
+
lines.push(`Chunks indexed: ${result.indexed}`);
|
|
135
|
+
lines.push(`Chunks embedded: ${result.embedded}`);
|
|
136
|
+
if (result.errors > 0) {
|
|
137
|
+
lines.push(`Errors: ${result.errors}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push("");
|
|
140
|
+
// Category breakdown
|
|
141
|
+
if (Object.keys(stats.categories).length > 0) {
|
|
142
|
+
lines.push("Categories:");
|
|
143
|
+
for (const [cat, count] of Object.entries(stats.categories).sort((a, b) => b[1] - a[1])) {
|
|
144
|
+
lines.push(` ${cat.padEnd(20)} ${count} chunks`);
|
|
145
|
+
}
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
148
|
+
lines.push(`Total index: ${stats.total_chunks} chunks across ${stats.total_files} files`);
|
|
149
|
+
const display = wrapDisplay(lines.join("\n"));
|
|
150
|
+
return {
|
|
151
|
+
directory,
|
|
152
|
+
project,
|
|
153
|
+
files_scanned: files.length,
|
|
154
|
+
files_changed: filesToProcess.length,
|
|
155
|
+
files_unchanged: filesUnchanged,
|
|
156
|
+
chunks_indexed: result.indexed,
|
|
157
|
+
chunks_embedded: result.embedded,
|
|
158
|
+
errors: result.errors,
|
|
159
|
+
categories: stats.categories,
|
|
160
|
+
display,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=index-docs.js.map
|
|
@@ -40,6 +40,13 @@ Proceed with caution — this may be new territory without documented lessons.`;
|
|
|
40
40
|
formatNudgeHeader(scars.length),
|
|
41
41
|
"",
|
|
42
42
|
];
|
|
43
|
+
// Citation protocol — provenance enforcement for any downstream claims
|
|
44
|
+
// Placed BEFORE results so agents see it before processing scars
|
|
45
|
+
lines.push("───────────────────────────────────────────────────");
|
|
46
|
+
lines.push("CITATION RULE: When referencing facts from these scars, cite the record ID.");
|
|
47
|
+
lines.push("Example: \"Edge improved to 3.07 [id:48ebca14]\" — not paraphrased numbers.");
|
|
48
|
+
lines.push("If you cannot cite a specific record for a claim, say \"not in institutional memory.\"");
|
|
49
|
+
lines.push("");
|
|
43
50
|
// Blocking verification requirements first
|
|
44
51
|
const blockingScars = scars.filter((s) => s.required_verification?.blocking);
|
|
45
52
|
if (blockingScars.length > 0) {
|
package/dist/tools/recall.js
CHANGED
|
@@ -48,6 +48,13 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
|
|
|
48
48
|
lines.push(`${dimText("No project-specific lessons yet. Use create_learning to capture your first.")}`);
|
|
49
49
|
lines.push("");
|
|
50
50
|
}
|
|
51
|
+
// Citation protocol — provenance enforcement for any downstream claims
|
|
52
|
+
// Placed BEFORE results so agents see it before processing scars
|
|
53
|
+
lines.push("───────────────────────────────────────────────────");
|
|
54
|
+
lines.push("CITATION RULE: When referencing facts from these scars, cite the record ID.");
|
|
55
|
+
lines.push("Example: \"Edge improved to 3.07 [id:48ebca14]\" — not paraphrased numbers.");
|
|
56
|
+
lines.push("If you cannot cite a specific record for a claim, say \"not in institutional memory.\"");
|
|
57
|
+
lines.push("");
|
|
51
58
|
// Display blocking verification requirements FIRST and prominently
|
|
52
59
|
if (scarsWithVerification.length > 0) {
|
|
53
60
|
lines.push(`${ANSI.yellow}VERIFICATION REQUIRED${ANSI.reset}`);
|
|
@@ -73,7 +80,9 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
|
|
|
73
80
|
for (const scar of scars) {
|
|
74
81
|
const sev = SEV[scar.severity] || "[?]";
|
|
75
82
|
const starterTag = scar.is_starter ? ` ${dimText("[starter]")}` : "";
|
|
76
|
-
|
|
83
|
+
// Confidence tier: marginal matches (< 0.55) get flagged — 66% N/A rate in this range
|
|
84
|
+
const confidenceTag = scar.similarity < 0.55 ? ` ${dimText("[low confidence]")}` : "";
|
|
85
|
+
lines.push(`${sev} **${scar.title}** (${scar.severity}, ${scar.similarity.toFixed(2)}) ${dimText(`id:${scar.id.slice(0, 8)}`)}${starterTag}${confidenceTag}`);
|
|
77
86
|
// Inline archival hint: scars with high dismiss rates get annotated
|
|
78
87
|
if (dismissals) {
|
|
79
88
|
const counts = dismissals.get(scar.id);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search_docs Tool
|
|
3
|
+
*
|
|
4
|
+
* Semantic search over indexed repository documentation.
|
|
5
|
+
* Returns relevant chunks with file paths for targeted reading.
|
|
6
|
+
*
|
|
7
|
+
* Like search (for scars), but for docs.
|
|
8
|
+
*/
|
|
9
|
+
import type { Project } from "../types/index.js";
|
|
10
|
+
export interface SearchDocsParams {
|
|
11
|
+
query: string;
|
|
12
|
+
project?: Project;
|
|
13
|
+
category?: string;
|
|
14
|
+
match_count?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SearchDocsResultEntry {
|
|
17
|
+
id: string;
|
|
18
|
+
file_path: string;
|
|
19
|
+
chunk_index: number;
|
|
20
|
+
title: string;
|
|
21
|
+
section_title: string;
|
|
22
|
+
category: string;
|
|
23
|
+
content: string;
|
|
24
|
+
similarity: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SearchDocsResult {
|
|
27
|
+
query: string;
|
|
28
|
+
project: string;
|
|
29
|
+
results: SearchDocsResultEntry[];
|
|
30
|
+
total_found: number;
|
|
31
|
+
index_stats: {
|
|
32
|
+
total_chunks: number;
|
|
33
|
+
total_files: number;
|
|
34
|
+
};
|
|
35
|
+
display: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function searchDocsHandler(params: SearchDocsParams): Promise<SearchDocsResult>;
|
|
38
|
+
//# sourceMappingURL=search-docs.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search_docs Tool
|
|
3
|
+
*
|
|
4
|
+
* Semantic search over indexed repository documentation.
|
|
5
|
+
* Returns relevant chunks with file paths for targeted reading.
|
|
6
|
+
*
|
|
7
|
+
* Like search (for scars), but for docs.
|
|
8
|
+
*/
|
|
9
|
+
import { searchDocs as doSearch, getIndexStats } from "../services/doc-index.js";
|
|
10
|
+
import { getProject } from "../services/session-state.js";
|
|
11
|
+
import { wrapDisplay, productLine, dimText, truncate } from "../services/display-protocol.js";
|
|
12
|
+
export async function searchDocsHandler(params) {
|
|
13
|
+
const query = params.query;
|
|
14
|
+
const project = params.project || getProject() || "default";
|
|
15
|
+
const category = params.category;
|
|
16
|
+
const matchCount = params.match_count || 5;
|
|
17
|
+
// Check if index exists
|
|
18
|
+
const stats = getIndexStats(project);
|
|
19
|
+
if (stats.total_chunks === 0) {
|
|
20
|
+
const display = wrapDisplay([
|
|
21
|
+
productLine("search_docs", "no docs indexed"),
|
|
22
|
+
"",
|
|
23
|
+
`No documents indexed for project="${project}".`,
|
|
24
|
+
"",
|
|
25
|
+
"Index docs first:",
|
|
26
|
+
' index_docs({ directory: "/path/to/docs", project: "my-project" })',
|
|
27
|
+
].join("\n"));
|
|
28
|
+
return {
|
|
29
|
+
query,
|
|
30
|
+
project,
|
|
31
|
+
results: [],
|
|
32
|
+
total_found: 0,
|
|
33
|
+
index_stats: {
|
|
34
|
+
total_chunks: stats.total_chunks,
|
|
35
|
+
total_files: stats.total_files,
|
|
36
|
+
},
|
|
37
|
+
display,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Search
|
|
41
|
+
const results = await doSearch(query, {
|
|
42
|
+
project,
|
|
43
|
+
category,
|
|
44
|
+
match_count: matchCount,
|
|
45
|
+
});
|
|
46
|
+
// Build display
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push(productLine("search_docs", `${results.length} results · "${truncate(query, 60)}"`));
|
|
49
|
+
if (category) {
|
|
50
|
+
lines.push(`Category filter: ${category}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push("");
|
|
53
|
+
if (results.length === 0) {
|
|
54
|
+
lines.push("No matching docs found.");
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push(`Index contains ${stats.total_chunks} chunks across ${stats.total_files} files.`);
|
|
57
|
+
lines.push("Available categories: " + Object.keys(stats.categories).join(", "));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Show results with file paths prominently
|
|
61
|
+
for (const r of results) {
|
|
62
|
+
const sim = `(${r.similarity.toFixed(2)})`;
|
|
63
|
+
const conf = r.similarity < 0.55 ? ` ${dimText("[low confidence]")}` : "";
|
|
64
|
+
const section = r.section_title ? ` > ${r.section_title}` : "";
|
|
65
|
+
lines.push(`${r.file_path}${section} ${sim}${conf}`);
|
|
66
|
+
lines.push(` ${truncate(r.title, 60)} [${r.category}]`);
|
|
67
|
+
lines.push(` ${truncate(r.content, 120)}`);
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
lines.push(dimText(`Searched ${stats.total_chunks} chunks across ${stats.total_files} files`));
|
|
71
|
+
}
|
|
72
|
+
const display = wrapDisplay(lines.join("\n"));
|
|
73
|
+
return {
|
|
74
|
+
query,
|
|
75
|
+
project,
|
|
76
|
+
results: results.map((r) => ({
|
|
77
|
+
id: r.id,
|
|
78
|
+
file_path: r.file_path,
|
|
79
|
+
chunk_index: r.chunk_index,
|
|
80
|
+
title: r.title,
|
|
81
|
+
section_title: r.section_title,
|
|
82
|
+
category: r.category,
|
|
83
|
+
content: r.content,
|
|
84
|
+
similarity: r.similarity,
|
|
85
|
+
})),
|
|
86
|
+
total_found: results.length,
|
|
87
|
+
index_stats: {
|
|
88
|
+
total_chunks: stats.total_chunks,
|
|
89
|
+
total_files: stats.total_files,
|
|
90
|
+
},
|
|
91
|
+
display,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=search-docs.js.map
|
package/dist/tools/search.js
CHANGED
|
@@ -30,6 +30,14 @@ function buildSearchDisplay(results, total_found, query, filters) {
|
|
|
30
30
|
if (fp.length > 0)
|
|
31
31
|
lines.push(`Filters: ${fp.join(", ")}`);
|
|
32
32
|
lines.push("");
|
|
33
|
+
// Citation protocol — provenance enforcement for any downstream claims
|
|
34
|
+
// Placed BEFORE results so agents see it before processing results
|
|
35
|
+
if (results.length > 0) {
|
|
36
|
+
lines.push("───────────────────────────────────────────────────");
|
|
37
|
+
lines.push("CITATION RULE: When referencing facts from these results, cite the record ID.");
|
|
38
|
+
lines.push("If you cannot cite a specific record for a claim, say \"not in institutional memory.\"");
|
|
39
|
+
lines.push("");
|
|
40
|
+
}
|
|
33
41
|
if (results.length === 0) {
|
|
34
42
|
lines.push("No results found.");
|
|
35
43
|
return wrapDisplay(lines.join("\n"));
|
|
@@ -40,7 +48,9 @@ function buildSearchDisplay(results, total_found, query, filters) {
|
|
|
40
48
|
const t = truncate(r.title, 50);
|
|
41
49
|
const sim = `(${r.similarity.toFixed(2)})`;
|
|
42
50
|
const issue = r.source_linear_issue ? ` ${r.source_linear_issue}` : "";
|
|
43
|
-
|
|
51
|
+
// Confidence tier: marginal matches (< 0.55) get flagged
|
|
52
|
+
const conf = r.similarity < 0.55 ? ` ${dimText("[low confidence]")}` : "";
|
|
53
|
+
lines.push(`${te} ${se} ${t.padEnd(52)} ${sim}${issue}${conf}`);
|
|
44
54
|
const starterTag = r.is_starter ? ` ${dimText("[starter]")}` : "";
|
|
45
55
|
lines.push(` ${truncate(r.description, 72)} id:${r.id.slice(0, 8)}${starterTag}`);
|
|
46
56
|
}
|
|
@@ -12,7 +12,7 @@ import * as supabase from "../services/supabase-client.js";
|
|
|
12
12
|
import { embed, isEmbeddingAvailable } from "../services/embedding.js";
|
|
13
13
|
import { hasSupabase, getTableName } from "../services/tier.js";
|
|
14
14
|
import { getStorage } from "../services/storage.js";
|
|
15
|
-
import { clearCurrentSession, getSurfacedScars, getConfirmations, getReflections, getObservations, getChildren, getThreads, getSessionActivity } from "../services/session-state.js";
|
|
15
|
+
import { clearCurrentSession, getSurfacedScars, getConfirmations, getReflections, getObservations, getChildren, getThreads, getSessionActivity, isRecallCalled } from "../services/session-state.js";
|
|
16
16
|
import { normalizeThreads, mergeThreadStates, migrateStringThread, saveThreadsFile } from "../services/thread-manager.js"; //
|
|
17
17
|
import { deduplicateThreadList } from "../services/thread-dedup.js";
|
|
18
18
|
import { syncThreadsToSupabase, loadOpenThreadEmbeddings } from "../services/thread-supabase.js";
|
|
@@ -858,13 +858,56 @@ export async function sessionClose(params) {
|
|
|
858
858
|
`(${Math.round(activity.duration_min)} min, no substantive activity). ` +
|
|
859
859
|
`Proceeding with standard as requested.`);
|
|
860
860
|
}
|
|
861
|
+
// Hard gate: quick close requires session under 30 minutes
|
|
862
|
+
if (params.close_type === "quick" && activity.duration_min >= 30) {
|
|
863
|
+
return {
|
|
864
|
+
success: false,
|
|
865
|
+
session_id: params.session_id || "",
|
|
866
|
+
close_compliance: {
|
|
867
|
+
close_type: "quick",
|
|
868
|
+
agent: detectAgent().agent,
|
|
869
|
+
checklist_displayed: false,
|
|
870
|
+
questions_answered_by_agent: false,
|
|
871
|
+
human_asked_for_corrections: false,
|
|
872
|
+
learnings_stored: 0,
|
|
873
|
+
scars_applied: 0,
|
|
874
|
+
},
|
|
875
|
+
validation_errors: [
|
|
876
|
+
`Session has been active for ${Math.round(activity.duration_min)} minutes. ` +
|
|
877
|
+
`Quick close requires sessions under 30 minutes. Use close_type: "standard".`,
|
|
878
|
+
],
|
|
879
|
+
performance: buildPerformanceData("session_close", timer.stop(), 0),
|
|
880
|
+
};
|
|
881
|
+
}
|
|
861
882
|
if (params.close_type === "quick" && recommendedLevel === "full") {
|
|
862
|
-
// Warn but don't reject — agent chose quick on a substantive session
|
|
883
|
+
// Warn but don't reject — agent chose quick on a substantive session under 30 min
|
|
863
884
|
console.error(`[session_close] Warning: "quick" close on substantive session ` +
|
|
864
885
|
`(${Math.round(activity.duration_min)} min, ${activity.recall_count} recalls, ` +
|
|
865
886
|
`${activity.observation_count} observations). Consider "standard" close.`);
|
|
866
887
|
}
|
|
867
888
|
}
|
|
889
|
+
// Hard gate: standard close requires at least one recall() call
|
|
890
|
+
// Exemptions: quick (micro sessions), autonomous (CODA-1), hasReflection (agent already wrote full reflection)
|
|
891
|
+
if (params.close_type === "standard" && !isRecallCalled() && !hasReflection) {
|
|
892
|
+
return {
|
|
893
|
+
success: false,
|
|
894
|
+
session_id: params.session_id || "",
|
|
895
|
+
close_compliance: {
|
|
896
|
+
close_type: "standard",
|
|
897
|
+
agent: detectAgent().agent,
|
|
898
|
+
checklist_displayed: false,
|
|
899
|
+
questions_answered_by_agent: false,
|
|
900
|
+
human_asked_for_corrections: false,
|
|
901
|
+
learnings_stored: 0,
|
|
902
|
+
scars_applied: 0,
|
|
903
|
+
},
|
|
904
|
+
validation_errors: [
|
|
905
|
+
`No recall() was run this session. Standard close requires at least one recall. ` +
|
|
906
|
+
`Run recall("session close ceremony") first, then retry session_close.`,
|
|
907
|
+
],
|
|
908
|
+
performance: buildPerformanceData("session_close", timer.stop(), 0),
|
|
909
|
+
};
|
|
910
|
+
}
|
|
868
911
|
// Free tier: simple local persistence, skip Supabase recovery and compliance
|
|
869
912
|
if (!hasSupabase()) {
|
|
870
913
|
return sessionCloseFree(params, timer);
|
|
@@ -503,6 +503,7 @@ function restoreSessionState(existing, fallbackAgent) {
|
|
|
503
503
|
agent: existing.agent || fallbackAgent,
|
|
504
504
|
linearIssue: existing.linear_issue,
|
|
505
505
|
startedAt,
|
|
506
|
+
project: existing.project,
|
|
506
507
|
};
|
|
507
508
|
}
|
|
508
509
|
/**
|
|
@@ -675,6 +676,20 @@ function formatStartDisplay(result, displayInfoMap, isFirstSession) {
|
|
|
675
676
|
if (result.project)
|
|
676
677
|
parts.push(result.project);
|
|
677
678
|
visual.push(dimText(parts.join(" · ")));
|
|
679
|
+
// Line 3: duration + surfaced scars for resumed/refreshed sessions
|
|
680
|
+
if (result.resumed || result.refreshed) {
|
|
681
|
+
const session = getCurrentSession();
|
|
682
|
+
if (session?.startedAt) {
|
|
683
|
+
const durationMs = Date.now() - session.startedAt.getTime();
|
|
684
|
+
const totalMin = Math.floor(durationMs / 60000);
|
|
685
|
+
const hours = Math.floor(totalMin / 60);
|
|
686
|
+
const mins = totalMin % 60;
|
|
687
|
+
const durationStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
688
|
+
const scarCount = session.surfacedScars?.length || 0;
|
|
689
|
+
const scarSuffix = scarCount > 0 ? ` · ${scarCount} scars loaded from earlier` : "";
|
|
690
|
+
visual.push(dimText(`Session active for: ${durationStr}${scarSuffix}`));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
678
693
|
// Threads section — top 5 by vitality, truncated to 60 chars
|
|
679
694
|
const hasThreads = result.open_threads && result.open_threads.length > 0;
|
|
680
695
|
const hasDecisions = result.recent_decisions && result.recent_decisions.length > 0;
|
|
@@ -743,10 +758,20 @@ export async function sessionStart(params) {
|
|
|
743
758
|
// 1. Detect agent (or use provided)
|
|
744
759
|
const env = detectAgent();
|
|
745
760
|
const agent = params.agent_identity || env.agent;
|
|
746
|
-
|
|
761
|
+
let project = params.project || getConfigProject() || "default";
|
|
747
762
|
// Check for existing active session — reuse session_id but still load full context
|
|
748
763
|
const existingSession = checkExistingSession(agent, params.force);
|
|
749
764
|
const isResuming = existingSession !== null;
|
|
765
|
+
// When resuming, prefer the stored project from the existing session.
|
|
766
|
+
// This prevents project drift after context compaction — the agent may pass
|
|
767
|
+
// the wrong project (e.g., from CLAUDE.md defaults) but the stored session
|
|
768
|
+
// knows the real project.
|
|
769
|
+
if (isResuming && existingSession?.project) {
|
|
770
|
+
if (existingSession.project !== project) {
|
|
771
|
+
console.error(`[session_start] Project override on resume: ${project} → ${existingSession.project} (from stored session)`);
|
|
772
|
+
}
|
|
773
|
+
project = existingSession.project;
|
|
774
|
+
}
|
|
750
775
|
// t-f7c2fa01: When force:true kills an existing session, carry forward its startedAt
|
|
751
776
|
// so session_close duration reflects the full conversation, not just the new session.
|
|
752
777
|
// Also carry forward activity counts (recalls, observations) so standard close isn't rejected.
|