kibi-mcp 1.0.0 → 2.1.0

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.
@@ -140,12 +140,12 @@ function appendMutationFields(fields, args) {
140
140
  fields.mutation_target = `${type}:${id}`;
141
141
  }
142
142
  const RECEIPT_GAP_CODES = new Set([
143
- "missing_verification_receipt",
144
- "stale_verification_receipt",
145
- "failed_verification_receipt",
146
- "invalid_verification_receipt",
147
- "verification_contract_mismatch",
148
- "verification_snapshot_unavailable",
143
+ "missing_proof_receipt",
144
+ "stale_proof_receipt",
145
+ "failed_proof_receipt",
146
+ "invalid_proof_receipt",
147
+ "proof_contract_mismatch",
148
+ "proof_snapshot_unavailable",
149
149
  ]);
150
150
  const MAX_COVERAGE_RECEIPT_GAPS = 50;
151
151
  function coverageReceiptGapDetails(rows) {
@@ -214,8 +214,8 @@ function appendCoverageFields(fields, args, structuredContent) {
214
214
  const metadata = isRecord(structuredContent.meta)
215
215
  ? structuredContent.meta
216
216
  : undefined;
217
- if (typeof metadata?.verificationSnapshot === "string") {
218
- fields.coverage_verification_snapshot = metadata.verificationSnapshot;
217
+ if (typeof metadata?.proofSnapshot === "string") {
218
+ fields.coverage_proof_snapshot = metadata.proofSnapshot;
219
219
  }
220
220
  }
221
221
  function appendPredicateSuggestionFields(fields, structuredContent) {
package/dist/env.js CHANGED
@@ -16,8 +16,7 @@
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
18
  import fs from "node:fs";
19
- import { resolveEnvFilePath, resolveWorkspaceRoot } from "./workspace.js";
20
- const DEFAULT_ENV_FILE = ".env";
19
+ import { KIBI_PROJECT_ENV_FILE, bootstrapKibiEnvironment, inspectSecretSource, parseEnvContent, resolveEnvFilePath, resolveKibiProjectEnvPath, resolveKibiWorkspaceRoot, } from "kibi-runtime";
21
20
  function getEnvValue(key) {
22
21
  const value = process.env[key];
23
22
  return typeof value === "string" ? value : undefined;
@@ -26,9 +25,10 @@ function getTrimmedEnvValue(key) {
26
25
  const value = getEnvValue(key)?.trim();
27
26
  return value ? value : undefined;
28
27
  }
28
+ /** Preferred project env file name (`.env.kibi` or `KIBI_ENV_FILE` override). */
29
29
  export function getEnvFileName() {
30
30
  // implements REQ-002
31
- return getTrimmedEnvValue("KIBI_ENV_FILE") ?? DEFAULT_ENV_FILE;
31
+ return getTrimmedEnvValue("KIBI_ENV_FILE") ?? KIBI_PROJECT_ENV_FILE;
32
32
  }
33
33
  export function isMcpDebugEnabled() {
34
34
  // implements REQ-002
@@ -51,12 +51,26 @@ export function getCoreModulePathOverride(fileName) {
51
51
  const envKey = `KIBI_${fileName.replace(/\W/g, "_").toUpperCase()}_PATH`;
52
52
  return getEnvValue(envKey);
53
53
  }
54
+ /**
55
+ * Harness-independent bootstrap: process → project `.env.kibi`/`KIBI_ENV_FILE`
56
+ * → user `~/.config/kibi/env`, plus legacy `.env` gap-fill.
57
+ * Prefer calling from `startServer()` only.
58
+ */
54
59
  export function loadDefaultEnvFile() {
55
60
  // implements REQ-002
56
- const envFileName = getEnvFileName();
57
- const workspaceRoot = resolveWorkspaceRoot();
58
- return loadEnvFile({ envFileName, workspaceRoot });
61
+ const result = bootstrapKibiEnvironment();
62
+ const keysLoaded = [
63
+ ...result.keysLoadedFromUser,
64
+ ...result.keysLoadedFromProject,
65
+ ...result.keysLoadedFromLegacy,
66
+ ];
67
+ return {
68
+ loaded: keysLoaded.length > 0,
69
+ envFilePath: result.projectEnvPath,
70
+ keysLoaded,
71
+ };
59
72
  }
73
+ /** @deprecated Prefer bootstrapKibiEnvironment; kept for targeted file tests. */
60
74
  export function loadEnvFile(options) {
61
75
  const { envFileName, workspaceRoot } = options;
62
76
  const envFilePath = resolveEnvFilePath(envFileName, workspaceRoot);
@@ -80,27 +94,4 @@ export function loadEnvFile(options) {
80
94
  return { loaded: false, envFilePath, keysLoaded };
81
95
  }
82
96
  }
83
- function parseEnvContent(content) {
84
- const lines = content.split(/\r?\n/);
85
- const entries = [];
86
- for (const rawLine of lines) {
87
- const line = rawLine.trim();
88
- if (!line || line.startsWith("#")) {
89
- continue;
90
- }
91
- const eqIndex = line.indexOf("=");
92
- if (eqIndex <= 0) {
93
- continue;
94
- }
95
- const key = line.substring(0, eqIndex).trim();
96
- let value = line.substring(eqIndex + 1).trim();
97
- if (value.startsWith('"') && value.endsWith('"')) {
98
- value = value.slice(1, -1);
99
- }
100
- else if (value.startsWith("'") && value.endsWith("'")) {
101
- value = value.slice(1, -1);
102
- }
103
- entries.push({ key, value });
104
- }
105
- return entries;
106
- }
97
+ export { resolveEnvFilePath, resolveKibiProjectEnvPath, resolveKibiWorkspaceRoot, inspectSecretSource, };
@@ -1,16 +1,38 @@
1
- import { resolveBranchAttachment } from "kibi-runtime";
1
+ import { CapabilityRegistryCache, ensureCapabilityRegistry, resolveBranchAttachment, } from "kibi-runtime";
2
+ export function attachedContextWithProlog(withAttachment, prolog) {
3
+ return { ...withAttachment, prolog };
4
+ }
5
+ function resolvePluginsOption(plugins) {
6
+ if (plugins === undefined)
7
+ return undefined;
8
+ if (typeof plugins === "function")
9
+ return plugins;
10
+ return async () => plugins;
11
+ }
2
12
  // implements REQ-kibi-operation-interface-parity
3
13
  export function createMcpRuntime(session) {
4
14
  const sessionPrologs = new WeakMap();
15
+ // Per-runtime cache — not a process-global singleton.
16
+ const pluginCache = new CapabilityRegistryCache();
5
17
  return {
6
18
  open: async (spec, options = {}) => {
7
19
  const fs = options.fs ?? session.fs;
8
20
  const git = options.git ?? session.git;
9
21
  const net = options.net ?? session.net;
22
+ const pluginsOption = options.plugins ?? session.plugins;
23
+ const injectedPlugins = resolvePluginsOption(pluginsOption);
24
+ const workspaceRoot = options.workspaceRoot ?? session.workspaceRoot;
25
+ const ensurePlugins = async () => {
26
+ if (injectedPlugins)
27
+ return injectedPlugins();
28
+ return ensureCapabilityRegistry(workspaceRoot, { cache: pluginCache });
29
+ };
10
30
  const context = {
11
- workspaceRoot: options.workspaceRoot ?? session.workspaceRoot,
31
+ workspaceRoot,
12
32
  signal: options.signal ?? session.signal ?? new AbortController().signal,
13
33
  clock: options.clock ?? session.clock ?? (() => new Date()),
34
+ ensurePlugins,
35
+ ...(pluginsOption !== undefined ? { plugins: pluginsOption } : {}),
14
36
  ...(fs ? { fs } : {}),
15
37
  ...(fs ? { sourceFirst: true } : {}),
16
38
  ...(git ? { git } : {}),
@@ -51,7 +73,7 @@ export function createMcpRuntime(session) {
51
73
  throw new Error(`Legacy branch attachment requires explicit migration before '${spec.name}'; run kibi branch migrate --from ${attachment.kbBranch} --to ${attachment.gitBranch} --apply`);
52
74
  }
53
75
  if (options.prolog) {
54
- return { ...withAttachment, prolog: options.prolog };
76
+ return attachedContextWithProlog(withAttachment, options.prolog);
55
77
  }
56
78
  const sessionProlog = await session.ensureProlog();
57
79
  const operationContext = {
@@ -0,0 +1,107 @@
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";
19
+ const MAX_FINISHED_JOBS = 32;
20
+ const jobs = new Map();
21
+ let jobCounter = 0;
22
+ function nextJobId(tool) {
23
+ jobCounter += 1;
24
+ const random = Math.random().toString(36).slice(2, 10);
25
+ return `job-${tool}-${jobCounter.toString(36)}-${random}`;
26
+ }
27
+ function evictOldestFinished() {
28
+ for (const [id, record] of jobs) {
29
+ if (jobs.size <= MAX_FINISHED_JOBS)
30
+ break;
31
+ if (record.status !== "running") {
32
+ jobs.delete(id);
33
+ }
34
+ }
35
+ }
36
+ /**
37
+ * Start a background job for `tool`. The factory's promise runs detached;
38
+ * callers return the `kibi.job.v1` receipt immediately so the MCP request is
39
+ * never held open for the duration of the operation.
40
+ */
41
+ // implements REQ-002
42
+ export function startJob(tool, factory) {
43
+ const jobId = nextJobId(tool);
44
+ const record = {
45
+ jobId,
46
+ tool,
47
+ startedAt: new Date().toISOString(),
48
+ status: "running",
49
+ };
50
+ jobs.set(jobId, record);
51
+ void (async () => {
52
+ try {
53
+ const result = await factory();
54
+ record.status = "succeeded";
55
+ record.finishedAt = new Date().toISOString();
56
+ record.result =
57
+ result !== null && typeof result === "object"
58
+ ? result
59
+ : { value: result };
60
+ }
61
+ catch (error) {
62
+ record.status = "failed";
63
+ record.finishedAt = new Date().toISOString();
64
+ record.error = error instanceof Error ? error.message : String(error);
65
+ }
66
+ finally {
67
+ evictOldestFinished();
68
+ }
69
+ })().catch((error) => {
70
+ // Defensive: the internal bookkeeping above must never crash the server
71
+ // process via an unhandled rejection.
72
+ process.stderr.write(`[kibi-mcp] job bookkeeping error for ${jobId}: ${error instanceof Error ? error.message : String(error)}\n`);
73
+ });
74
+ return {
75
+ kibiProtocol: 1,
76
+ jobVersion: "kibi.job.v1",
77
+ jobId,
78
+ tool,
79
+ status: "running",
80
+ pollWith: "kb_job_status",
81
+ };
82
+ }
83
+ // implements REQ-002
84
+ export function getJob(jobId) {
85
+ return jobs.get(jobId);
86
+ }
87
+ /** Public JSON shape for kb_job_status responses. */
88
+ // implements REQ-002
89
+ export function jobSnapshot(job) {
90
+ return {
91
+ kibiProtocol: 1,
92
+ jobVersion: "kibi.job.v1",
93
+ jobId: job.jobId,
94
+ tool: job.tool,
95
+ status: job.status,
96
+ startedAt: job.startedAt,
97
+ ...(job.finishedAt === undefined ? {} : { finishedAt: job.finishedAt }),
98
+ ...(job.result === undefined ? {} : { result: job.result }),
99
+ ...(job.error === undefined ? {} : { error: job.error }),
100
+ };
101
+ }
102
+ /** Test seam: drop all job state. */
103
+ // implements REQ-002
104
+ export function resetJobsForTests() {
105
+ jobs.clear();
106
+ jobCounter = 0;
107
+ }
@@ -1,4 +1,18 @@
1
1
  import { z } from "zod";
2
+ export function firstDefined(items) {
3
+ return items[0];
4
+ }
5
+ export function describedAnySchema(description) {
6
+ const anySchema = z.any();
7
+ return description ? anySchema.describe(description) : anySchema;
8
+ }
9
+ export function singleLiteralOrAny(literalSchemas, description) {
10
+ const single = firstDefined(literalSchemas);
11
+ if (!single) {
12
+ return describedAnySchema(description);
13
+ }
14
+ return description ? single.describe(description) : single;
15
+ }
2
16
  function hasRequiredProperties(value, schema) {
3
17
  if (schema === null || typeof schema !== "object")
4
18
  return false;
@@ -64,11 +78,7 @@ export function jsonSchemaToZod(schema) {
64
78
  }
65
79
  const literalSchemas = literals.map((value) => z.literal(value));
66
80
  if (literalSchemas.length === 1) {
67
- const single = literalSchemas[0];
68
- if (!single) {
69
- return description ? z.any().describe(description) : z.any();
70
- }
71
- return description ? single.describe(description) : single;
81
+ return singleLiteralOrAny(literalSchemas, description);
72
82
  }
73
83
  const union = z.union(literalSchemas);
74
84
  return description ? union.describe(description) : union;
@@ -78,6 +88,20 @@ export function jsonSchemaToZod(schema) {
78
88
  : typeof obj.type === "string"
79
89
  ? [obj.type]
80
90
  : [];
91
+ // JSON Schema anyOf unions become real Zod unions so MCP output
92
+ // validation enforces the declared alternatives instead of silently
93
+ // degrading to z.any() — e.g. kb_check's synchronous payload versus its
94
+ // kibi.job.v1 async receipt. oneOf is intentionally left degrading: its
95
+ // required-guard branches carry no properties and would loosen input
96
+ // validation instead of tightening it.
97
+ const anyOfVariants = Array.isArray(obj.anyOf) ? obj.anyOf : [];
98
+ if (anyOfVariants.length > 0) {
99
+ const variants = anyOfVariants.map((variant) => jsonSchemaToZod(variant));
100
+ const union = z.union(variants);
101
+ return typeof obj.description === "string"
102
+ ? union.describe(obj.description)
103
+ : union;
104
+ }
81
105
  // JSON Schema nullable fields are represented as a type union in the
82
106
  // catalog. Build a real Zod union so MCP publishes and validates the same
83
107
  // nullability instead of silently degrading the field to z.any().
@@ -195,10 +219,7 @@ export function jsonSchemaToZod(schema) {
195
219
  const description = typeof obj.description === "string" ? obj.description : undefined;
196
220
  return description ? n.describe(description) : n;
197
221
  }
198
- default: {
199
- const anySchema = z.any();
200
- const description = typeof obj.description === "string" ? obj.description : undefined;
201
- return description ? anySchema.describe(description) : anySchema;
202
- }
222
+ default:
223
+ return describedAnySchema(typeof obj.description === "string" ? obj.description : undefined);
203
224
  }
204
225
  }
@@ -37,35 +37,53 @@ const defaultSessionDeps = {
37
37
  resolveWorkspaceRoot,
38
38
  };
39
39
  let sessionDeps = { ...defaultSessionDeps };
40
- export let prologProcess = null;
41
- let isInitialized = false;
42
- export let activeBranchName = "develop";
43
- let ensurePrologTail = Promise.resolve();
44
- let prologResetGeneration = 0;
45
- export let attachedBranchKbPath = null;
46
- let attachedBranchStamp = null;
40
+ const state = {
41
+ prologProcess: null,
42
+ isInitialized: false,
43
+ activeBranchName: "develop",
44
+ ensurePrologTail: Promise.resolve(),
45
+ prologResetGeneration: 0,
46
+ attachedBranchKbPath: null,
47
+ attachedBranchStamp: null,
48
+ isShuttingDown: false,
49
+ shutdownTimeout: null,
50
+ };
51
+ // implements REQ-008
52
+ export function getPrologProcess() {
53
+ return state.prologProcess;
54
+ }
55
+ // implements REQ-mcp-kb-freshness
56
+ export function getActiveBranchName() {
57
+ return state.activeBranchName;
58
+ }
59
+ // implements REQ-008
60
+ export function getAttachedBranchKbPath() {
61
+ return state.attachedBranchKbPath;
62
+ }
63
+ // implements REQ-008
64
+ export function getIsShuttingDown() {
65
+ return state.isShuttingDown;
66
+ }
67
+ export const inFlightRequests = new Map();
47
68
  // implements REQ-mcp-kb-freshness
48
69
  // covered_by TEST-mcp-kb-freshness
49
70
  export function updateAttachedBranchStamp(stamp) {
50
- attachedBranchStamp = stamp;
71
+ state.attachedBranchStamp = stamp;
51
72
  }
52
- export let isShuttingDown = false;
53
- let shutdownTimeout = null;
54
- export const inFlightRequests = new Map();
55
73
  // implements REQ-008
56
74
  export function resetSessionStateForTests() {
57
- prologProcess = null;
58
- isInitialized = false;
59
- activeBranchName = "develop";
60
- ensurePrologTail = Promise.resolve();
61
- prologResetGeneration = 0;
62
- attachedBranchKbPath = null;
63
- attachedBranchStamp = null;
64
- isShuttingDown = false;
75
+ state.prologProcess = null;
76
+ state.isInitialized = false;
77
+ state.activeBranchName = "develop";
78
+ state.ensurePrologTail = Promise.resolve();
79
+ state.prologResetGeneration = 0;
80
+ state.attachedBranchKbPath = null;
81
+ state.attachedBranchStamp = null;
82
+ state.isShuttingDown = false;
65
83
  inFlightRequests.clear();
66
- if (shutdownTimeout) {
67
- clearTimeout(shutdownTimeout);
68
- shutdownTimeout = null;
84
+ if (state.shutdownTimeout) {
85
+ clearTimeout(state.shutdownTimeout);
86
+ state.shutdownTimeout = null;
69
87
  }
70
88
  }
71
89
  export function _setSessionDepsForTests(
@@ -77,6 +95,10 @@ export function _resetSessionDepsForTests() {
77
95
  // implements REQ-008
78
96
  sessionDeps = { ...defaultSessionDeps };
79
97
  }
98
+ // implements REQ-008
99
+ export function _setPrologProcessForTests(process) {
100
+ state.prologProcess = process;
101
+ }
80
102
  function debugLog(...args) {
81
103
  if (isMcpDebugEnabled()) {
82
104
  console.error(...args);
@@ -108,13 +130,13 @@ export function ensureBranchKbExists(workspaceRoot, branch) {
108
130
  return true;
109
131
  }
110
132
  export async function initiateGracefulShutdown(exitCode = 0) {
111
- if (isShuttingDown) {
133
+ if (state.isShuttingDown) {
112
134
  return;
113
135
  }
114
- isShuttingDown = true;
136
+ state.isShuttingDown = true;
115
137
  debugLog(`[KIBI-MCP] Initiating graceful shutdown (exit code: ${exitCode})`);
116
- const currentProlog = prologProcess;
117
- prologProcess = null;
138
+ const currentProlog = state.prologProcess;
139
+ state.prologProcess = null;
118
140
  if (currentProlog?.isRunning()) {
119
141
  debugLog("[KIBI-MCP] Cancelling active Prolog work...");
120
142
  try {
@@ -129,7 +151,7 @@ export async function initiateGracefulShutdown(exitCode = 0) {
129
151
  if (inFlightRequests.size > 0) {
130
152
  debugLog(`[KIBI-MCP] Waiting for ${inFlightRequests.size} in-flight requests to complete...`);
131
153
  const timeoutPromise = new Promise((_, reject) => {
132
- shutdownTimeout = setTimeout(() => {
154
+ state.shutdownTimeout = setTimeout(() => {
133
155
  reject(new Error("Shutdown timeout"));
134
156
  }, 10000); // 10 second timeout
135
157
  });
@@ -144,9 +166,9 @@ export async function initiateGracefulShutdown(exitCode = 0) {
144
166
  console.error("[KIBI-MCP] Shutdown timeout reached, forcing exit");
145
167
  }
146
168
  finally {
147
- if (shutdownTimeout) {
148
- clearTimeout(shutdownTimeout);
149
- shutdownTimeout = null;
169
+ if (state.shutdownTimeout) {
170
+ clearTimeout(state.shutdownTimeout);
171
+ state.shutdownTimeout = null;
150
172
  }
151
173
  }
152
174
  }
@@ -156,12 +178,12 @@ export async function initiateGracefulShutdown(exitCode = 0) {
156
178
  // implements REQ-008
157
179
  export async function resetProlog(reason) {
158
180
  debugLog(`[KIBI-MCP] Resetting Prolog worker: ${reason}`);
159
- prologResetGeneration += 1;
160
- const current = prologProcess;
161
- prologProcess = null;
162
- isInitialized = false;
163
- attachedBranchKbPath = null;
164
- attachedBranchStamp = null;
181
+ state.prologResetGeneration += 1;
182
+ const current = state.prologProcess;
183
+ state.prologProcess = null;
184
+ state.isInitialized = false;
185
+ state.attachedBranchKbPath = null;
186
+ state.attachedBranchStamp = null;
165
187
  if (current) {
166
188
  try {
167
189
  await current.terminate();
@@ -202,14 +224,14 @@ function usesBranchKbPath(kbPath) {
202
224
  }
203
225
  // implements REQ-008
204
226
  async function ensurePrologUnsafe() {
205
- const generationAtStart = prologResetGeneration;
227
+ const generationAtStart = state.prologResetGeneration;
206
228
  const workspaceRoot = sessionDeps.resolveWorkspaceRoot();
207
229
  const useEngine = sessionDeps.PrologProcess === defaultSessionDeps.PrologProcess;
208
230
  const assertGeneration = async () => {
209
- if (generationAtStart !== prologResetGeneration) {
210
- const current = prologProcess;
211
- prologProcess = null;
212
- isInitialized = false;
231
+ if (generationAtStart !== state.prologResetGeneration) {
232
+ const current = state.prologProcess;
233
+ state.prologProcess = null;
234
+ state.isInitialized = false;
213
235
  if (current) {
214
236
  await current.terminate().catch((error) => {
215
237
  console.error("[KIBI-MCP] Error terminating stale Prolog after reset generation change:", error);
@@ -240,22 +262,22 @@ async function ensurePrologUnsafe() {
240
262
  targetBranch = branchResult.kbBranch;
241
263
  }
242
264
  // Check if we need to switch branches
243
- if (isInitialized && useEngine && prologProcess?.isRunning()) {
265
+ if (state.isInitialized && useEngine && state.prologProcess?.isRunning()) {
244
266
  const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
245
- if (targetBranch === activeBranchName) {
246
- attachedBranchKbPath = kbPath;
247
- attachedBranchStamp = await readBranchKbStamp(kbPath);
248
- return prologProcess;
267
+ if (targetBranch === state.activeBranchName) {
268
+ state.attachedBranchKbPath = kbPath;
269
+ state.attachedBranchStamp = await readBranchKbStamp(kbPath);
270
+ return state.prologProcess;
249
271
  }
250
- await prologProcess.terminate();
251
- prologProcess = null;
252
- isInitialized = false;
253
- attachedBranchKbPath = null;
254
- attachedBranchStamp = null;
272
+ await state.prologProcess.terminate();
273
+ state.prologProcess = null;
274
+ state.isInitialized = false;
275
+ state.attachedBranchKbPath = null;
276
+ state.attachedBranchStamp = null;
255
277
  }
256
- if (isInitialized && prologProcess?.isRunning()) {
278
+ if (state.isInitialized && state.prologProcess?.isRunning()) {
257
279
  const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
258
- if (targetBranch === activeBranchName) {
280
+ if (targetBranch === state.activeBranchName) {
259
281
  const currentStamp = await readBranchKbStamp(kbPath);
260
282
  if (usesBranchKbPath(kbPath) &&
261
283
  (currentStamp.rdfMissing ||
@@ -263,29 +285,29 @@ async function ensurePrologUnsafe() {
263
285
  currentStamp.errorMessage !== null)) {
264
286
  throw new KbRefreshError(`KB refresh failed: branch KB snapshot is unstable: ${describeBranchKbStamp(currentStamp)}`);
265
287
  }
266
- const shouldRefresh = attachedBranchKbPath === kbPath &&
267
- attachedBranchStamp !== null &&
288
+ const shouldRefresh = state.attachedBranchKbPath === kbPath &&
289
+ state.attachedBranchStamp !== null &&
268
290
  usesBranchKbPath(kbPath) &&
269
- !sameBranchKbStamp(currentStamp, attachedBranchStamp);
291
+ !sameBranchKbStamp(currentStamp, state.attachedBranchStamp);
270
292
  if (shouldRefresh) {
271
- attachedBranchStamp = await refreshAttachedBranchKbWithRetry(prologProcess, kbPath, currentStamp, assertGeneration);
293
+ state.attachedBranchStamp = await refreshAttachedBranchKbWithRetry(state.prologProcess, kbPath, currentStamp, assertGeneration);
272
294
  }
273
295
  else {
274
- attachedBranchKbPath = kbPath;
275
- attachedBranchStamp = currentStamp;
296
+ state.attachedBranchKbPath = kbPath;
297
+ state.attachedBranchStamp = currentStamp;
276
298
  }
277
- attachedBranchKbPath = kbPath;
278
- return prologProcess;
299
+ state.attachedBranchKbPath = kbPath;
300
+ return state.prologProcess;
279
301
  }
280
302
  // Branch changed - need to detach and re-attach
281
- debugLog(`[KIBI-MCP] Branch changed: ${activeBranchName} -> ${targetBranch}`);
303
+ debugLog(`[KIBI-MCP] Branch changed: ${state.activeBranchName} -> ${targetBranch}`);
282
304
  // Persist and detach from old KB
283
- const saveResult = await prologProcess.query("kb_save");
305
+ const saveResult = await state.prologProcess.query("kb_save");
284
306
  await assertGeneration();
285
307
  if (!saveResult.success) {
286
308
  throw new Error(`Failed to save old KB before detach: ${saveResult.error || "Unknown error"}`);
287
309
  }
288
- const detachResult = await prologProcess.query("kb_detach");
310
+ const detachResult = await state.prologProcess.query("kb_detach");
289
311
  await assertGeneration();
290
312
  if (!detachResult.success) {
291
313
  debugLog(`[KIBI-MCP] Warning: failed to detach from old KB: ${detachResult.error || "Unknown error"}`);
@@ -294,35 +316,35 @@ async function ensurePrologUnsafe() {
294
316
  // Ensure new branch KB exists
295
317
  const createdEmptyBranch = ensureBranchKbExists(workspaceRoot, targetBranch);
296
318
  // Attach to new branch KB
297
- const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
319
+ const attachResult = await state.prologProcess.query(`kb_attach('${kbPath}')`);
298
320
  await assertGeneration();
299
321
  if (!attachResult.success) {
300
322
  throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
301
323
  }
302
324
  if (createdEmptyBranch) {
303
- const initialSaveResult = await prologProcess.query("kb_save");
325
+ const initialSaveResult = await state.prologProcess.query("kb_save");
304
326
  await assertGeneration();
305
327
  if (!initialSaveResult.success) {
306
328
  throw new Error(`Failed to initialize empty branch KB: ${initialSaveResult.error || "Unknown error"}`);
307
329
  }
308
330
  }
309
- activeBranchName = targetBranch;
310
- attachedBranchKbPath = kbPath;
311
- attachedBranchStamp = await readBranchKbStamp(kbPath);
331
+ state.activeBranchName = targetBranch;
332
+ state.attachedBranchKbPath = kbPath;
333
+ state.attachedBranchStamp = await readBranchKbStamp(kbPath);
312
334
  debugLog(`[KIBI-MCP] Re-attached to branch: ${targetBranch}`);
313
335
  debugLog(`[KIBI-MCP] KB path: ${kbPath}`);
314
- return prologProcess;
336
+ return state.prologProcess;
315
337
  }
316
338
  // First initialization
317
339
  debugLog("[KIBI-MCP] Initializing Prolog process...");
318
- prologProcess = useEngine
340
+ state.prologProcess = useEngine
319
341
  ? new EngineClient({
320
342
  workspaceRoot,
321
343
  branch: targetBranch,
322
344
  timeout: 120000,
323
345
  })
324
346
  : new sessionDeps.PrologProcess({ timeout: 120000 });
325
- await prologProcess.start();
347
+ await state.prologProcess.start();
326
348
  await assertGeneration();
327
349
  // Adapters report the package that owns runtime execution. Do not resolve
328
350
  // CLI implementation paths from the MCP process; the runtime package is
@@ -332,34 +354,34 @@ async function ensurePrologUnsafe() {
332
354
  debugLog("[KIBI-MCP] Branch selection:");
333
355
  debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${envBranch || "not set"}`);
334
356
  debugLog(`[KIBI-MCP] Resolved branch: ${targetBranch}`);
335
- activeBranchName = targetBranch;
357
+ state.activeBranchName = targetBranch;
336
358
  const createdEmptyBranch = ensureBranchKbExists(workspaceRoot, targetBranch);
337
359
  const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
338
360
  if (!useEngine) {
339
- const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
361
+ const attachResult = await state.prologProcess.query(`kb_attach('${kbPath}')`);
340
362
  await assertGeneration();
341
363
  if (!attachResult.success) {
342
364
  throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
343
365
  }
344
366
  if (createdEmptyBranch) {
345
- const initialSaveResult = await prologProcess.query("kb_save");
367
+ const initialSaveResult = await state.prologProcess.query("kb_save");
346
368
  await assertGeneration();
347
369
  if (!initialSaveResult.success) {
348
370
  throw new Error(`Failed to initialize empty branch KB: ${initialSaveResult.error || "Unknown error"}`);
349
371
  }
350
372
  }
351
373
  }
352
- attachedBranchKbPath = kbPath;
353
- attachedBranchStamp = await readBranchKbStamp(kbPath);
354
- isInitialized = true;
355
- debugLog(`[KIBI-MCP] Prolog process started (PID: ${prologProcess.getPid()})`);
374
+ state.attachedBranchKbPath = kbPath;
375
+ state.attachedBranchStamp = await readBranchKbStamp(kbPath);
376
+ state.isInitialized = true;
377
+ debugLog(`[KIBI-MCP] Prolog process started (PID: ${state.prologProcess.getPid()})`);
356
378
  debugLog(`[KIBI-MCP] KB attached: ${kbPath}`);
357
- return prologProcess;
379
+ return state.prologProcess;
358
380
  }
359
381
  export async function ensureProlog() {
360
- const previous = ensurePrologTail;
382
+ const previous = state.ensurePrologTail;
361
383
  let release;
362
- ensurePrologTail = new Promise((resolve) => {
384
+ state.ensurePrologTail = new Promise((resolve) => {
363
385
  release = resolve;
364
386
  });
365
387
  await previous;