kibi-mcp 0.15.0 → 0.15.3
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/diagnostics.js +51 -0
- package/dist/server/docs.js +8 -7
- package/dist/server/session.js +37 -0
- package/dist/server/tools.js +78 -3
- package/dist/tools/autopilot-generate.js +14 -1
- package/dist/tools/upsert.js +116 -1
- package/dist/tools-config.js +90 -0
- package/package.json +2 -2
package/dist/diagnostics.js
CHANGED
|
@@ -89,6 +89,57 @@ export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
|
|
|
89
89
|
},
|
|
90
90
|
};
|
|
91
91
|
// implements REQ-002
|
|
92
|
+
export function classifyDiagnosticError(error) {
|
|
93
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
94
|
+
const message = err.message;
|
|
95
|
+
const lower = message.toLowerCase();
|
|
96
|
+
if (lower.includes("stale_snapshot")) {
|
|
97
|
+
return buildErrorFields(err, "stale_snapshot", "persistence", "KB snapshot is stale; refresh/retry after the latest KB state is attached.");
|
|
98
|
+
}
|
|
99
|
+
if (lower.includes("unknown option") && lower.includes("h for help")) {
|
|
100
|
+
return buildErrorFields(err, "prolog_unknown_option", "prolog_runtime", "Prolog rejected startup/module/query options; inspect MCP package wiring and Prolog invocation.");
|
|
101
|
+
}
|
|
102
|
+
if (lower.includes("prolog process not started")) {
|
|
103
|
+
return buildErrorFields(err, "prolog_process_not_started", "prolog_lifecycle", "Prolog process is unavailable; restart the MCP server before retrying.");
|
|
104
|
+
}
|
|
105
|
+
if (lower.includes("resetting prolog worker") ||
|
|
106
|
+
lower.includes("prolog worker reset")) {
|
|
107
|
+
return buildErrorFields(err, "prolog_worker_reset", "prolog_lifecycle", "Prolog worker was reset so the next MCP call can start from a fresh worker.");
|
|
108
|
+
}
|
|
109
|
+
if (/timed out after \d+ms/i.test(message) ||
|
|
110
|
+
lower.includes("tool timeout")) {
|
|
111
|
+
return buildErrorFields(err, "tool_timeout", "tool_timeout", "MCP tool execution exceeded its bounded timeout.");
|
|
112
|
+
}
|
|
113
|
+
if (lower.includes("coarsely while granular symbols are available")) {
|
|
114
|
+
return buildErrorFields(err, "coarse_symbol_linkage", "validation", "Symbol traceability targeted a coarse file/module while narrower exported symbols exist.");
|
|
115
|
+
}
|
|
116
|
+
if (message.startsWith("Entity validation failed:")) {
|
|
117
|
+
return buildErrorFields(err, "entity_validation_failed", "validation", "Entity payload failed schema validation.");
|
|
118
|
+
}
|
|
119
|
+
if (message.startsWith("Relationship validation failed")) {
|
|
120
|
+
return buildErrorFields(err, "relationship_validation_failed", "validation", "Relationship payload failed schema validation.");
|
|
121
|
+
}
|
|
122
|
+
if (message.startsWith("Relationship source must match the upserted entity")) {
|
|
123
|
+
return buildErrorFields(err, "relationship_source_mismatch", "validation", "Relationship source did not match the entity being upserted.");
|
|
124
|
+
}
|
|
125
|
+
if (lower.includes("module load failed")) {
|
|
126
|
+
return buildErrorFields(err, "prolog_module_load_failed", "prolog_runtime", "Prolog failed to load an execution module.");
|
|
127
|
+
}
|
|
128
|
+
if (lower.includes("query failed")) {
|
|
129
|
+
return buildErrorFields(err, "prolog_query_failed", "prolog_runtime", "Prolog query execution failed.");
|
|
130
|
+
}
|
|
131
|
+
return buildErrorFields(err, "handler_error", "handler", "Unhandled MCP handler error.");
|
|
132
|
+
}
|
|
133
|
+
function buildErrorFields(error, category, stage, summary) {
|
|
134
|
+
return {
|
|
135
|
+
error_name: error.name,
|
|
136
|
+
error_message: error.message,
|
|
137
|
+
error_category: category,
|
|
138
|
+
error_stage: stage,
|
|
139
|
+
error_summary: summary,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// implements REQ-002
|
|
92
143
|
export function deriveDiagnosticFields(toolName, args, telemetry, result) {
|
|
93
144
|
const fields = {
|
|
94
145
|
telemetry_status: telemetry ? "provided" : "missing",
|
package/dist/server/docs.js
CHANGED
|
@@ -48,7 +48,7 @@ export const PROMPTS = [
|
|
|
48
48
|
text: [
|
|
49
49
|
"# Kibi Interactive Activation Workflow",
|
|
50
50
|
"",
|
|
51
|
-
"Use this workflow to onboard a new or empty repository into Kibi through interactive discovery.",
|
|
51
|
+
"Use this post-hoc workflow to onboard a new or empty repository into Kibi through interactive discovery.",
|
|
52
52
|
"",
|
|
53
53
|
"## Step 1: Gather Declared Context",
|
|
54
54
|
"",
|
|
@@ -63,27 +63,28 @@ export const PROMPTS = [
|
|
|
63
63
|
"Call `kb_autopilot_generate` with the gathered context to synthesize candidate entities.",
|
|
64
64
|
"",
|
|
65
65
|
"This tool is **read-only**. It returns additive `structuredContent` with:",
|
|
66
|
-
"- `promptBlock`: review text that
|
|
66
|
+
"- `promptBlock`: review text that must be surfaced before writes",
|
|
67
67
|
"- `recommendedActions`: agent-facing next steps, including any REQ/SCEN/TEST authoring routed for manual handling",
|
|
68
68
|
"- `declaredContext`: the user-provided bootstrap context",
|
|
69
69
|
"- `confidence`: confidence summary for the generated output",
|
|
70
70
|
"- `bootstrapMode`: current KB state (e.g., `root_uninitialized`)",
|
|
71
71
|
"- `candidates`: synthesized entities grounded in declared context and source evidence",
|
|
72
|
+
"- `applyPlan`: exact sequential `kb_upsert` payloads for approved candidates",
|
|
72
73
|
"- `discoverySummary`: source-backed discovery notes",
|
|
73
74
|
"",
|
|
74
|
-
"## Step 3:
|
|
75
|
+
"## Step 3: Preview and Approval",
|
|
75
76
|
"",
|
|
76
|
-
"Surface the `promptBlock
|
|
77
|
+
"Surface the `promptBlock`, a summary of `candidates`, and the exact `structuredContent.applyPlan` payloads. Wait for explicit user approval before proceeding to writes.",
|
|
77
78
|
"",
|
|
78
79
|
"## Step 4: Apply Candidates",
|
|
79
80
|
"",
|
|
80
81
|
"Apply candidates sequentially using `kb_upsert`.",
|
|
81
|
-
"1. Execute
|
|
82
|
+
"1. Execute `structuredContent.applyPlan` sequentially in listed order.",
|
|
82
83
|
"2. Confirm success of each `kb_upsert` before moving to the next.",
|
|
83
84
|
"3. Run `kb_check` after the batch to verify KB integrity.",
|
|
84
85
|
"",
|
|
85
86
|
"## Rules",
|
|
86
|
-
"-
|
|
87
|
+
"- Never apply bootstrap writes without user-facing preview and explicit approval.",
|
|
87
88
|
"- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
|
|
88
89
|
"- Guidance must stay MCP-only; do not suggest `kibi` CLI commands.",
|
|
89
90
|
].join("\n"),
|
|
@@ -110,7 +111,7 @@ export const PROMPTS = [
|
|
|
110
111
|
"Core modeling principles:",
|
|
111
112
|
"- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
|
|
112
113
|
"- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
|
|
113
|
-
"- High-confidence
|
|
114
|
+
"- High-confidence `kb_model_requirement` output is deterministic; `/init-kibi` bootstrap writes still require preview and explicit approval.",
|
|
114
115
|
"- Low-confidence claims (< 0.7) are downgraded to `observation` facts to prevent false-positive contradictions.",
|
|
115
116
|
"- Only strict domain facts participate in contradiction inference; observation and meta facts are non-blocking notes.",
|
|
116
117
|
"- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
|
package/dist/server/session.js
CHANGED
|
@@ -38,6 +38,7 @@ export let prologProcess = null;
|
|
|
38
38
|
let isInitialized = false;
|
|
39
39
|
export let activeBranchName = "develop";
|
|
40
40
|
let ensurePrologTail = Promise.resolve();
|
|
41
|
+
let prologResetGeneration = 0;
|
|
41
42
|
export let isShuttingDown = false;
|
|
42
43
|
let shutdownTimeout = null;
|
|
43
44
|
export const inFlightRequests = new Map();
|
|
@@ -47,6 +48,7 @@ export function resetSessionStateForTests() {
|
|
|
47
48
|
isInitialized = false;
|
|
48
49
|
activeBranchName = "develop";
|
|
49
50
|
ensurePrologTail = Promise.resolve();
|
|
51
|
+
prologResetGeneration = 0;
|
|
50
52
|
isShuttingDown = false;
|
|
51
53
|
inFlightRequests.clear();
|
|
52
54
|
if (shutdownTimeout) {
|
|
@@ -138,8 +140,38 @@ export async function initiateGracefulShutdown(exitCode = 0) {
|
|
|
138
140
|
process.exit(exitCode);
|
|
139
141
|
}
|
|
140
142
|
// implements REQ-008
|
|
143
|
+
export async function resetProlog(reason) {
|
|
144
|
+
debugLog(`[KIBI-MCP] Resetting Prolog worker: ${reason}`);
|
|
145
|
+
prologResetGeneration += 1;
|
|
146
|
+
const current = prologProcess;
|
|
147
|
+
prologProcess = null;
|
|
148
|
+
isInitialized = false;
|
|
149
|
+
if (current) {
|
|
150
|
+
try {
|
|
151
|
+
await current.terminate();
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error("[KIBI-MCP] Error resetting Prolog worker:", error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// implements REQ-008
|
|
141
159
|
async function ensurePrologUnsafe() {
|
|
160
|
+
const generationAtStart = prologResetGeneration;
|
|
142
161
|
const workspaceRoot = sessionDeps.resolveWorkspaceRoot();
|
|
162
|
+
const assertGeneration = async () => {
|
|
163
|
+
if (generationAtStart !== prologResetGeneration) {
|
|
164
|
+
const current = prologProcess;
|
|
165
|
+
prologProcess = null;
|
|
166
|
+
isInitialized = false;
|
|
167
|
+
if (current) {
|
|
168
|
+
await current.terminate().catch((error) => {
|
|
169
|
+
console.error("[KIBI-MCP] Error terminating stale Prolog after reset generation change:", error);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
throw new Error("Prolog worker reset while initialization was in progress");
|
|
173
|
+
}
|
|
174
|
+
};
|
|
143
175
|
// Determine target branch: respect KIBI_BRANCH override or resolve from git
|
|
144
176
|
const envBranch = getBranchOverride();
|
|
145
177
|
let targetBranch;
|
|
@@ -170,10 +202,12 @@ async function ensurePrologUnsafe() {
|
|
|
170
202
|
debugLog(`[KIBI-MCP] Branch changed: ${activeBranchName} -> ${targetBranch}`);
|
|
171
203
|
// Persist and detach from old KB
|
|
172
204
|
const saveResult = await prologProcess.query("kb_save");
|
|
205
|
+
await assertGeneration();
|
|
173
206
|
if (!saveResult.success) {
|
|
174
207
|
throw new Error(`Failed to save old KB before detach: ${saveResult.error || "Unknown error"}`);
|
|
175
208
|
}
|
|
176
209
|
const detachResult = await prologProcess.query("kb_detach");
|
|
210
|
+
await assertGeneration();
|
|
177
211
|
if (!detachResult.success) {
|
|
178
212
|
debugLog(`[KIBI-MCP] Warning: failed to detach from old KB: ${detachResult.error || "Unknown error"}`);
|
|
179
213
|
// Continue anyway - we'll try to attach to the new KB
|
|
@@ -183,6 +217,7 @@ async function ensurePrologUnsafe() {
|
|
|
183
217
|
const newKbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
184
218
|
// Attach to new branch KB
|
|
185
219
|
const attachResult = await prologProcess.query(`kb_attach('${newKbPath}')`);
|
|
220
|
+
await assertGeneration();
|
|
186
221
|
if (!attachResult.success) {
|
|
187
222
|
throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
|
|
188
223
|
}
|
|
@@ -195,6 +230,7 @@ async function ensurePrologUnsafe() {
|
|
|
195
230
|
debugLog("[KIBI-MCP] Initializing Prolog process...");
|
|
196
231
|
prologProcess = new sessionDeps.PrologProcess({ timeout: 120000 });
|
|
197
232
|
await prologProcess.start();
|
|
233
|
+
await assertGeneration();
|
|
198
234
|
// Startup debug: resolve which kibi-cli is being used and its version (best-effort).
|
|
199
235
|
// Gate all output under KIBI_MCP_DEBUG and write only to stderr via debugLog.
|
|
200
236
|
if (isMcpDebugEnabled()) {
|
|
@@ -235,6 +271,7 @@ async function ensurePrologUnsafe() {
|
|
|
235
271
|
ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
236
272
|
const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
237
273
|
const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
|
|
274
|
+
await assertGeneration();
|
|
238
275
|
if (!attachResult.success) {
|
|
239
276
|
throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
|
|
240
277
|
}
|
package/dist/server/tools.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import process from "node:process";
|
|
1
19
|
import { z } from "zod";
|
|
2
|
-
import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
|
|
20
|
+
import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, classifyDiagnosticError, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
|
|
3
21
|
import { isMcpDebugEnabled } from "../env.js";
|
|
4
22
|
import { TOOLS } from "../tools-config.js";
|
|
5
23
|
import { handleKbAutopilotGenerate, } from "../tools/autopilot-generate.js";
|
|
@@ -14,6 +32,8 @@ import { handleKbSearch } from "../tools/search.js";
|
|
|
14
32
|
import { handleKbSkillsList, handleKbSkillsLoad, handleKbSkillsRead, } from "../tools/skills.js";
|
|
15
33
|
import { handleKbStatus } from "../tools/status.js";
|
|
16
34
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
35
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 90_000;
|
|
36
|
+
const TOOL_TIMEOUT_ENV = "KIBI_MCP_TOOL_TIMEOUT_MS";
|
|
17
37
|
const defaultToolsServerDeps = {
|
|
18
38
|
getSessionModule: () => import("./session.js"),
|
|
19
39
|
};
|
|
@@ -40,6 +60,7 @@ async function getSessionModule() {
|
|
|
40
60
|
const DEFAULT_TOOLS_RUNTIME = {
|
|
41
61
|
diagnosticModeEnabled: () => DIAGNOSTIC_MODE_ENABLED,
|
|
42
62
|
appendUsageLogLine,
|
|
63
|
+
classifyDiagnosticError,
|
|
43
64
|
deriveDiagnosticFields,
|
|
44
65
|
extractToolCallPayload,
|
|
45
66
|
// INTENTIONAL: TOOLS is imported as a Zod-inferred schema type; ToolConfig is the
|
|
@@ -48,6 +69,7 @@ const DEFAULT_TOOLS_RUNTIME = {
|
|
|
48
69
|
tools: TOOLS,
|
|
49
70
|
activeBranchName: async () => (await getSessionModule()).activeBranchName,
|
|
50
71
|
ensureProlog: async () => (await getSessionModule()).ensureProlog(),
|
|
72
|
+
resetProlog: async (reason) => (await getSessionModule()).resetProlog(reason),
|
|
51
73
|
inFlightRequests: async () => (await getSessionModule()).inFlightRequests,
|
|
52
74
|
isShuttingDown: async () => (await getSessionModule()).isShuttingDown,
|
|
53
75
|
prologProcess: async () => (await getSessionModule()).prologProcess,
|
|
@@ -73,6 +95,43 @@ function debugLog(...args) {
|
|
|
73
95
|
}
|
|
74
96
|
}
|
|
75
97
|
// implements REQ-002
|
|
98
|
+
function getToolTimeoutMs() {
|
|
99
|
+
const raw = process.env[TOOL_TIMEOUT_ENV]?.trim();
|
|
100
|
+
if (!raw) {
|
|
101
|
+
return DEFAULT_TOOL_TIMEOUT_MS;
|
|
102
|
+
}
|
|
103
|
+
const parsed = Number(raw);
|
|
104
|
+
return Number.isSafeInteger(parsed) && parsed > 0
|
|
105
|
+
? parsed
|
|
106
|
+
: DEFAULT_TOOL_TIMEOUT_MS;
|
|
107
|
+
}
|
|
108
|
+
// implements REQ-002
|
|
109
|
+
function createToolTimeoutError(toolName, timeoutMs) {
|
|
110
|
+
return new Error(`Tool ${toolName} timed out after ${timeoutMs}ms`);
|
|
111
|
+
}
|
|
112
|
+
// implements REQ-002
|
|
113
|
+
async function withToolTimeout(toolName, operation, onTimeout) {
|
|
114
|
+
const timeoutMs = getToolTimeoutMs();
|
|
115
|
+
let timeout;
|
|
116
|
+
try {
|
|
117
|
+
return await Promise.race([
|
|
118
|
+
operation,
|
|
119
|
+
new Promise((_, reject) => {
|
|
120
|
+
timeout = setTimeout(() => {
|
|
121
|
+
const error = createToolTimeoutError(toolName, timeoutMs);
|
|
122
|
+
reject(error);
|
|
123
|
+
void onTimeout(error, timeoutMs);
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
}),
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (timeout) {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// implements REQ-002
|
|
76
135
|
export function jsonSchemaToZod(schema) {
|
|
77
136
|
if (!schema || typeof schema !== "object") {
|
|
78
137
|
return z.any();
|
|
@@ -209,9 +268,21 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
209
268
|
const trackedRequests = await runtime.inFlightRequests();
|
|
210
269
|
const handlerPromise = handler(businessArgs);
|
|
211
270
|
trackedRequests.set(requestId, handlerPromise);
|
|
271
|
+
let resetAttempted = false;
|
|
272
|
+
let resetSucceeded = false;
|
|
273
|
+
let resetError = null;
|
|
212
274
|
try {
|
|
213
275
|
// Execute handler
|
|
214
|
-
const result = await handlerPromise
|
|
276
|
+
const result = await withToolTimeout(name, handlerPromise, async () => {
|
|
277
|
+
resetAttempted = true;
|
|
278
|
+
try {
|
|
279
|
+
await runtime.resetProlog(`tool timeout: ${name}`);
|
|
280
|
+
resetSucceeded = true;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
resetError = error instanceof Error ? error.message : String(error);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
215
286
|
// Log usage in diagnostic mode
|
|
216
287
|
if (diagnosticModeEnabled) {
|
|
217
288
|
const finishedAt = new Date();
|
|
@@ -240,6 +311,7 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
240
311
|
if (diagnosticModeEnabled) {
|
|
241
312
|
const finishedAt = new Date();
|
|
242
313
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
314
|
+
const diagnosticErrorFields = runtime.classifyDiagnosticError(err);
|
|
243
315
|
const processHandle = await runtime.prologProcess();
|
|
244
316
|
const branchName = await runtime.activeBranchName();
|
|
245
317
|
runtime.appendUsageLogLine({
|
|
@@ -254,7 +326,10 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
254
326
|
duration_ms: finishedAt.getTime() - startedAt.getTime(),
|
|
255
327
|
prolog_pid: processHandle?.getPid() ?? null,
|
|
256
328
|
active_branch: branchName,
|
|
257
|
-
|
|
329
|
+
reset_attempted: resetAttempted,
|
|
330
|
+
reset_succeeded: resetSucceeded,
|
|
331
|
+
reset_error: resetError,
|
|
332
|
+
...diagnosticErrorFields,
|
|
258
333
|
});
|
|
259
334
|
}
|
|
260
335
|
throw error;
|
|
@@ -60,6 +60,13 @@ function countCandidatesByType(candidateRecords) {
|
|
|
60
60
|
}
|
|
61
61
|
return counts;
|
|
62
62
|
}
|
|
63
|
+
function buildApplyPlan(candidateRecords) {
|
|
64
|
+
return candidateRecords.flatMap((candidate) => {
|
|
65
|
+
if (!Array.isArray(candidate.applyPlan))
|
|
66
|
+
return [];
|
|
67
|
+
return candidate.applyPlan.filter((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
63
70
|
function formatCandidateTypeCounts(candidateRecords) {
|
|
64
71
|
const counts = countCandidatesByType(candidateRecords);
|
|
65
72
|
return Object.keys(counts)
|
|
@@ -573,6 +580,7 @@ _prolog, args) {
|
|
|
573
580
|
// best-effort only; ignore failures here so generation can continue
|
|
574
581
|
}
|
|
575
582
|
const candidateRecords = Array.from(seenByKey.values());
|
|
583
|
+
const applyPlan = buildApplyPlan(candidateRecords);
|
|
576
584
|
const payoffSummary = buildPayoffSummary(candidateRecords);
|
|
577
585
|
const promptBlock = buildPromptBlock(workspaceRoot, activationState, activation.activationMode, activation.reason, activation.applyBlocked, declaredContext, candidateRecords, sourceOnlySignals, activationDiscovery.summary.scanWarnings);
|
|
578
586
|
const confidence = buildConfidence(activation.activationMode, activation.applyBlocked, declaredContext, candidateRecords, sourceOnlySignals, promptBlock);
|
|
@@ -585,6 +593,9 @@ _prolog, args) {
|
|
|
585
593
|
const effectiveTldr = confidence.level === "low" && !activation.applyBlocked
|
|
586
594
|
? `Low-confidence bootstrap (${confidence.score}): review diagnostics before proceeding. ${tldr}`
|
|
587
595
|
: tldr;
|
|
596
|
+
const effectiveText = applyPlan.length > 0
|
|
597
|
+
? `${effectiveTldr} Review structuredContent.applyPlan for exact sequential kb_upsert payloads before requesting approval.`
|
|
598
|
+
: effectiveTldr;
|
|
588
599
|
const structuredContent = {
|
|
589
600
|
activationState,
|
|
590
601
|
activationMode: activation.activationMode,
|
|
@@ -602,6 +613,7 @@ _prolog, args) {
|
|
|
602
613
|
declaredContext,
|
|
603
614
|
discoverySummary: activationDiscovery.summary,
|
|
604
615
|
candidates: candidateRecords,
|
|
616
|
+
applyPlan,
|
|
605
617
|
suppressedCandidates: suppressed,
|
|
606
618
|
payoffSummary,
|
|
607
619
|
};
|
|
@@ -609,12 +621,13 @@ _prolog, args) {
|
|
|
609
621
|
content: [
|
|
610
622
|
{
|
|
611
623
|
type: "text",
|
|
612
|
-
text:
|
|
624
|
+
text: effectiveText,
|
|
613
625
|
},
|
|
614
626
|
],
|
|
615
627
|
structuredContent,
|
|
616
628
|
migrationWarning,
|
|
617
629
|
candidates: candidateRecords,
|
|
630
|
+
applyPlan,
|
|
618
631
|
suppressedCandidates: suppressed,
|
|
619
632
|
payoffSummary,
|
|
620
633
|
};
|
package/dist/tools/upsert.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
/*
|
|
2
4
|
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
5
|
Copyright (C) 2026 Piotr Franczyk
|
|
@@ -19,12 +21,45 @@ import Ajv from "ajv";
|
|
|
19
21
|
import { escapeAtom, toPrologAtom, toPrologString, } from "kibi-cli/prolog/codec";
|
|
20
22
|
import entitySchema from "kibi-cli/schemas/entity";
|
|
21
23
|
import relationshipSchema from "kibi-cli/schemas/relationship";
|
|
24
|
+
import { Project, ScriptKind } from "ts-morph";
|
|
22
25
|
import { isMcpDebugEnabled } from "../env.js";
|
|
23
26
|
import { refreshCoordinatesForSymbolId } from "./symbols.js";
|
|
24
27
|
let refreshCoordinatesForSymbolIdImpl = refreshCoordinatesForSymbolId;
|
|
25
28
|
const ajv = new Ajv({ strict: false });
|
|
26
|
-
const
|
|
29
|
+
const entitySchemaRecord = entitySchema;
|
|
30
|
+
const entitySchemaProperties = entitySchemaRecord.properties;
|
|
31
|
+
const normalizedEntitySchemaProperties = entitySchemaProperties !== null &&
|
|
32
|
+
typeof entitySchemaProperties === "object" &&
|
|
33
|
+
!Array.isArray(entitySchemaProperties)
|
|
34
|
+
? entitySchemaProperties
|
|
35
|
+
: {};
|
|
36
|
+
const validateEntity = ajv.compile({
|
|
37
|
+
...entitySchemaRecord,
|
|
38
|
+
properties: {
|
|
39
|
+
...normalizedEntitySchemaProperties,
|
|
40
|
+
granularity_reason: {
|
|
41
|
+
type: "string",
|
|
42
|
+
enum: [
|
|
43
|
+
"config-artifact",
|
|
44
|
+
"module-level-behavior",
|
|
45
|
+
"extractor-miss",
|
|
46
|
+
"legacy-link",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
27
51
|
const validateRelationship = ajv.compile(relationshipSchema);
|
|
52
|
+
const TRACEABILITY_RELATIONSHIP_TYPES = new Set([
|
|
53
|
+
"implements",
|
|
54
|
+
"covered_by",
|
|
55
|
+
"executable_for",
|
|
56
|
+
]);
|
|
57
|
+
const ALLOWED_GRANULARITY_REASONS = new Set([
|
|
58
|
+
"config-artifact",
|
|
59
|
+
"module-level-behavior",
|
|
60
|
+
"extractor-miss",
|
|
61
|
+
"legacy-link",
|
|
62
|
+
]);
|
|
28
63
|
/**
|
|
29
64
|
* Handle kb.upsert tool calls
|
|
30
65
|
* Accepts { type, id, properties } — the flat format matching the tool schema.
|
|
@@ -76,6 +111,7 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
76
111
|
}
|
|
77
112
|
}
|
|
78
113
|
validateRelationshipSources(id, relationships);
|
|
114
|
+
validateSymbolGranularity(entity, relationships);
|
|
79
115
|
// Validate strict-lane fact_kind pairing for constrains/requires_property
|
|
80
116
|
// implements REQ-011
|
|
81
117
|
await validateStrictLanePairing(prolog, relationships);
|
|
@@ -185,6 +221,85 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
185
221
|
throw new Error(`Upsert execution failed: ${message}`);
|
|
186
222
|
}
|
|
187
223
|
}
|
|
224
|
+
function chooseScriptKind(filePath) {
|
|
225
|
+
const lower = filePath.toLowerCase();
|
|
226
|
+
if (lower.endsWith(".tsx"))
|
|
227
|
+
return ScriptKind.TSX;
|
|
228
|
+
if (lower.endsWith(".ts") ||
|
|
229
|
+
lower.endsWith(".mts") ||
|
|
230
|
+
lower.endsWith(".cts")) {
|
|
231
|
+
return ScriptKind.TS;
|
|
232
|
+
}
|
|
233
|
+
if (lower.endsWith(".jsx"))
|
|
234
|
+
return ScriptKind.JSX;
|
|
235
|
+
return ScriptKind.JS;
|
|
236
|
+
}
|
|
237
|
+
function hasTraceabilityRelationship(relationships) {
|
|
238
|
+
return relationships.some((relationship) => typeof relationship.type === "string" &&
|
|
239
|
+
TRACEABILITY_RELATIONSHIP_TYPES.has(relationship.type));
|
|
240
|
+
}
|
|
241
|
+
function hasAllowedGranularityReason(entity) {
|
|
242
|
+
const reason = entity.granularity_reason;
|
|
243
|
+
return typeof reason === "string" && ALLOWED_GRANULARITY_REASONS.has(reason);
|
|
244
|
+
}
|
|
245
|
+
function collectNarrowExportNames(filePath, content) {
|
|
246
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
247
|
+
const sourceFile = project.createSourceFile(`${filePath}::granularity`, content, {
|
|
248
|
+
overwrite: true,
|
|
249
|
+
scriptKind: chooseScriptKind(filePath),
|
|
250
|
+
});
|
|
251
|
+
const names = new Set();
|
|
252
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
253
|
+
if (fn.isExported()) {
|
|
254
|
+
const name = fn.getName();
|
|
255
|
+
if (name)
|
|
256
|
+
names.add(name);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const cls of sourceFile.getClasses()) {
|
|
260
|
+
if (cls.isExported()) {
|
|
261
|
+
const name = cls.getName();
|
|
262
|
+
if (name)
|
|
263
|
+
names.add(name);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const iface of sourceFile.getInterfaces()) {
|
|
267
|
+
if (iface.isExported())
|
|
268
|
+
names.add(iface.getName());
|
|
269
|
+
}
|
|
270
|
+
for (const alias of sourceFile.getTypeAliases()) {
|
|
271
|
+
if (alias.isExported())
|
|
272
|
+
names.add(alias.getName());
|
|
273
|
+
}
|
|
274
|
+
for (const en of sourceFile.getEnums()) {
|
|
275
|
+
if (en.isExported())
|
|
276
|
+
names.add(en.getName());
|
|
277
|
+
}
|
|
278
|
+
return [...names].sort();
|
|
279
|
+
}
|
|
280
|
+
function validateSymbolGranularity(entity, relationships) {
|
|
281
|
+
if (entity.type !== "symbol")
|
|
282
|
+
return;
|
|
283
|
+
if (!hasTraceabilityRelationship(relationships))
|
|
284
|
+
return;
|
|
285
|
+
if (hasAllowedGranularityReason(entity))
|
|
286
|
+
return;
|
|
287
|
+
if (typeof entity.sourceFile !== "string")
|
|
288
|
+
return;
|
|
289
|
+
if (typeof entity.title !== "string")
|
|
290
|
+
return;
|
|
291
|
+
const sourcePath = path.isAbsolute(entity.sourceFile)
|
|
292
|
+
? entity.sourceFile
|
|
293
|
+
: path.resolve(process.cwd(), entity.sourceFile);
|
|
294
|
+
if (!existsSync(sourcePath))
|
|
295
|
+
return;
|
|
296
|
+
const narrowNames = collectNarrowExportNames(entity.sourceFile, readFileSync(sourcePath, "utf8"));
|
|
297
|
+
if (narrowNames.length === 0)
|
|
298
|
+
return;
|
|
299
|
+
if (narrowNames.includes(entity.title))
|
|
300
|
+
return;
|
|
301
|
+
throw new Error(`Symbol ${String(entity.id)} links ${entity.sourceFile} coarsely while granular symbols are available: ${narrowNames.join(", ")}. Move relationships to the narrow symbol or set granularity_reason to config-artifact, module-level-behavior, extractor-miss, or legacy-link.`);
|
|
302
|
+
}
|
|
188
303
|
export const __test__ = {
|
|
189
304
|
// implements REQ-vscode-traceability
|
|
190
305
|
setRefreshCoordinatesForSymbolIdForTests(fn) {
|
package/dist/tools-config.js
CHANGED
|
@@ -337,6 +337,96 @@ const BASE_TOOLS = [
|
|
|
337
337
|
type: "string",
|
|
338
338
|
description: "Optional text anchor/reference. Example: 'requirements.md#L40'.",
|
|
339
339
|
},
|
|
340
|
+
sourceFile: {
|
|
341
|
+
type: "string",
|
|
342
|
+
description: "Optional code source file for symbol entities. Example: 'src/auth/login.ts'.",
|
|
343
|
+
},
|
|
344
|
+
granularity_reason: {
|
|
345
|
+
type: "string",
|
|
346
|
+
enum: [
|
|
347
|
+
"config-artifact",
|
|
348
|
+
"module-level-behavior",
|
|
349
|
+
"extractor-miss",
|
|
350
|
+
"legacy-link",
|
|
351
|
+
],
|
|
352
|
+
description: "Optional justification for a coarse file/module-level symbol traceability relationship when narrower function/class/type symbols exist.",
|
|
353
|
+
},
|
|
354
|
+
fact_kind: {
|
|
355
|
+
type: "string",
|
|
356
|
+
enum: [
|
|
357
|
+
"subject",
|
|
358
|
+
"property_value",
|
|
359
|
+
"observation",
|
|
360
|
+
"meta",
|
|
361
|
+
"predicate_schema",
|
|
362
|
+
"predicate",
|
|
363
|
+
],
|
|
364
|
+
description: "Optional fact lane kind for fact entities. Strict lane uses 'subject' and 'property_value'; context lane uses 'observation' or 'meta'.",
|
|
365
|
+
},
|
|
366
|
+
subject_key: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Optional canonical subject key for strict fact entities. Example: 'user.session'.",
|
|
369
|
+
},
|
|
370
|
+
property_key: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "Optional canonical property key for property_value facts. Example: 'session.timeout_minutes'.",
|
|
373
|
+
},
|
|
374
|
+
operator: {
|
|
375
|
+
type: "string",
|
|
376
|
+
enum: ["eq", "neq", "lt", "lte", "gt", "gte"],
|
|
377
|
+
description: "Optional comparison operator for property_value facts. Example: 'eq'.",
|
|
378
|
+
},
|
|
379
|
+
value_type: {
|
|
380
|
+
type: "string",
|
|
381
|
+
enum: ["string", "int", "number", "bool"],
|
|
382
|
+
description: "Optional typed value discriminator for property_value facts.",
|
|
383
|
+
},
|
|
384
|
+
value_string: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "Optional string value for property_value facts.",
|
|
387
|
+
},
|
|
388
|
+
value_int: {
|
|
389
|
+
type: "integer",
|
|
390
|
+
description: "Optional integer value for property_value facts.",
|
|
391
|
+
},
|
|
392
|
+
value_number: {
|
|
393
|
+
type: "number",
|
|
394
|
+
description: "Optional number value for property_value facts.",
|
|
395
|
+
},
|
|
396
|
+
value_bool: {
|
|
397
|
+
type: "boolean",
|
|
398
|
+
description: "Optional boolean value for property_value facts.",
|
|
399
|
+
},
|
|
400
|
+
unit: {
|
|
401
|
+
type: "string",
|
|
402
|
+
description: "Optional unit for numeric property_value facts.",
|
|
403
|
+
},
|
|
404
|
+
scope: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "Optional scope qualifier for fact entities.",
|
|
407
|
+
},
|
|
408
|
+
polarity: {
|
|
409
|
+
type: "string",
|
|
410
|
+
enum: ["require", "forbid", "assert", "deny"],
|
|
411
|
+
description: "Optional polarity for property_value or predicate facts.",
|
|
412
|
+
},
|
|
413
|
+
closed_world: {
|
|
414
|
+
type: "boolean",
|
|
415
|
+
description: "Optional closed-world marker for strict fact interpretation.",
|
|
416
|
+
},
|
|
417
|
+
canonical_key: {
|
|
418
|
+
type: "string",
|
|
419
|
+
description: "Optional canonical identity key for predicate or strict fact claims.",
|
|
420
|
+
},
|
|
421
|
+
predicate_name: {
|
|
422
|
+
type: "string",
|
|
423
|
+
description: "Optional predicate name for ontology predicate facts.",
|
|
424
|
+
},
|
|
425
|
+
predicate_args: {
|
|
426
|
+
type: "array",
|
|
427
|
+
items: { type: "string" },
|
|
428
|
+
description: "Optional ordered predicate arguments for ontology predicate facts.",
|
|
429
|
+
},
|
|
340
430
|
},
|
|
341
431
|
required: ["title", "status"],
|
|
342
432
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
6
6
|
"ajv": "^8.18.0",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"fast-glob": "^3.2.12",
|
|
10
10
|
"gray-matter": "^4.0.3",
|
|
11
11
|
"js-yaml": "^4.1.0",
|
|
12
|
-
"kibi-cli": "^0.12.
|
|
12
|
+
"kibi-cli": "^0.12.3",
|
|
13
13
|
"kibi-core": "^0.6.0",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|