kibi-mcp 1.0.0 → 2.0.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) {
@@ -1,4 +1,7 @@
1
1
  import { resolveBranchAttachment } from "kibi-runtime";
2
+ export function attachedContextWithProlog(withAttachment, prolog) {
3
+ return { ...withAttachment, prolog };
4
+ }
2
5
  // implements REQ-kibi-operation-interface-parity
3
6
  export function createMcpRuntime(session) {
4
7
  const sessionPrologs = new WeakMap();
@@ -51,7 +54,7 @@ export function createMcpRuntime(session) {
51
54
  throw new Error(`Legacy branch attachment requires explicit migration before '${spec.name}'; run kibi branch migrate --from ${attachment.kbBranch} --to ${attachment.gitBranch} --apply`);
52
55
  }
53
56
  if (options.prolog) {
54
- return { ...withAttachment, prolog: options.prolog };
57
+ return attachedContextWithProlog(withAttachment, options.prolog);
55
58
  }
56
59
  const sessionProlog = await session.ensureProlog();
57
60
  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;
@@ -195,10 +205,7 @@ export function jsonSchemaToZod(schema) {
195
205
  const description = typeof obj.description === "string" ? obj.description : undefined;
196
206
  return description ? n.describe(description) : n;
197
207
  }
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
- }
208
+ default:
209
+ return describedAnySchema(typeof obj.description === "string" ? obj.description : undefined);
203
210
  }
204
211
  }
@@ -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;
@@ -1,5 +1,6 @@
1
1
  import { getSpec, statusSpec } from "kibi-runtime";
2
- import { executeApplyPlan, executeCompileIntent, executeDelete, executeIngestVerification, executeUpsert, validateUpsertSpec, } from "kibi-runtime";
2
+ import { executeApplyPlan, executeCompileIntent, executeDelete, executeIngestProof, executeUpsert, validateUpsertSpec, } from "kibi-runtime";
3
+ import { startJob } from "./jobs.js";
3
4
  // implements REQ-002, REQ-013
4
5
  export function registerConfiguredTools(server, runtime, registerTool) {
5
6
  const toolDef = (name) => {
@@ -96,7 +97,16 @@ export function registerConfiguredTools(server, runtime, registerTool) {
96
97
  });
97
98
  register({
98
99
  name: "kb_check",
99
- execute: async (context, args) => runtime.handleKbCheck(prologFor(context), args),
100
+ execute: async (context, args) => {
101
+ const { async: asyncMode, ...checkArgs } = args;
102
+ if (asyncMode !== true) {
103
+ return runtime.handleKbCheck(prologFor(context), args);
104
+ }
105
+ // Long-KB full checks can exceed the tool timeout. Detach into a job
106
+ // and return the kibi.job.v1 receipt immediately; the agent polls
107
+ // kb_job_status for the terminal state and full result.
108
+ return startJob("kb_check", () => runtime.handleKbCheck(prologFor(context), checkArgs));
109
+ },
100
110
  });
101
111
  register({
102
112
  name: "kb_model_requirement",
@@ -123,9 +133,9 @@ export function registerConfiguredTools(server, runtime, registerTool) {
123
133
  : executeApplyPlan(args, context),
124
134
  });
125
135
  register({
126
- name: "kb_ingest_verification",
127
- execute: async (context, args) => runtime.handleKbIngestVerification
128
- ? runtime.handleKbIngestVerification(args, context)
129
- : executeIngestVerification(args, context),
136
+ name: "kb_ingest_proof",
137
+ execute: async (context, args) => runtime.handleKbIngestProof
138
+ ? runtime.handleKbIngestProof(args, context)
139
+ : executeIngestProof(args, context),
130
140
  });
131
141
  }
@@ -91,8 +91,8 @@ function adaptProlog(prolog) {
91
91
  }
92
92
  const operationRuntime = createMcpRuntime({
93
93
  workspaceRoot: resolveWorkspaceRoot(),
94
- activeBranchName: async () => (await getSessionModule()).activeBranchName,
95
- attachedBranchKbPath: async () => (await getSessionModule()).attachedBranchKbPath,
94
+ activeBranchName: async () => (await getSessionModule()).getActiveBranchName(),
95
+ attachedBranchKbPath: async () => (await getSessionModule()).getAttachedBranchKbPath(),
96
96
  ensureProlog: async () => (await getSessionModule()).ensureProlog(),
97
97
  adaptProlog,
98
98
  fs: nodeFilesystem,
@@ -100,7 +100,7 @@ const operationRuntime = createMcpRuntime({
100
100
  net: { fetch: (input, init) => globalThis.fetch(input, init) },
101
101
  refreshAttachedBranchStamp: async () => {
102
102
  const session = await getSessionModule();
103
- const kbPath = session.attachedBranchKbPath;
103
+ const kbPath = session.getAttachedBranchKbPath();
104
104
  if (kbPath) {
105
105
  try {
106
106
  session.updateAttachedBranchStamp(await readBranchKbStamp(kbPath));
@@ -124,12 +124,12 @@ export const DEFAULT_TOOLS_RUNTIME = {
124
124
  // runtime interface with looser Record<string, unknown> inputSchema. The cast is safe
125
125
  // because the tool definitions are statically authored and validated at startup.
126
126
  tools: TOOLS,
127
- activeBranchName: async () => (await getSessionModule()).activeBranchName,
127
+ activeBranchName: async () => (await getSessionModule()).getActiveBranchName(),
128
128
  ensureProlog: async () => (await getSessionModule()).ensureProlog(),
129
129
  resetProlog: async (reason) => (await getSessionModule()).resetProlog(reason),
130
130
  inFlightRequests: async () => (await getSessionModule()).inFlightRequests,
131
- isShuttingDown: async () => (await getSessionModule()).isShuttingDown,
132
- prologProcess: async () => (await getSessionModule()).prologProcess,
131
+ isShuttingDown: async () => (await getSessionModule()).getIsShuttingDown(),
132
+ prologProcess: async () => (await getSessionModule()).getPrologProcess(),
133
133
  operationRuntime,
134
134
  handleKbCheck,
135
135
  handleKbCoverage,
@@ -20,6 +20,7 @@ import { executeOperation } from "kibi-runtime";
20
20
  import { operationData, toKibiResult } from "kibi-runtime";
21
21
  import { isMcpDebugEnabled } from "../env.js";
22
22
  import { appendDiagnosticErrorUsage, appendDiagnosticSuccessUsage, } from "./diagnostic-usage.js";
23
+ import { getJob, jobSnapshot } from "./jobs.js";
23
24
  import { jsonSchemaToZod } from "./json-schema-to-zod.js";
24
25
  import { registerConfiguredTools } from "./tool-registration.js";
25
26
  import { DEFAULT_TOOLS_RUNTIME, _resetSessionModulePromise, _setToolsServerDepsForTests, } from "./tools-runtime.js";
@@ -263,4 +264,60 @@ export function registerAllTools(server,
263
264
  // INTENTIONAL: same generic bridge cast as addTool — see comment there.
264
265
  runtime = DEFAULT_TOOLS_RUNTIME) {
265
266
  registerConfiguredTools(server, runtime, addTool);
267
+ registerJobStatusTool(server);
268
+ }
269
+ /**
270
+ * kb_job_status polls background jobs started by long-running operations
271
+ * (currently `kb_check` with `async: true`). This is an MCP-server-native
272
+ * companion to the job receipts, not part of the canonical CLI operation
273
+ * catalog: jobs live in the server process and have no CLI counterpart.
274
+ */
275
+ function registerJobStatusTool(server) {
276
+ addTool(server, "kb_job_status", "Poll a background job started with async:true (e.g. kb_check async jobs). Returns the kibi.job.v1 state: running, succeeded (with the full result), or failed (with the error).", {
277
+ type: "object",
278
+ properties: {
279
+ jobId: {
280
+ type: "string",
281
+ description: "The jobId from the kibi.job.v1 receipt.",
282
+ },
283
+ },
284
+ required: ["jobId"],
285
+ }, async (args) => {
286
+ const jobId = typeof args.jobId === "string" ? args.jobId : "";
287
+ const job = jobId === "" ? undefined : getJob(jobId);
288
+ if (job === undefined) {
289
+ return {
290
+ content: [
291
+ {
292
+ type: "text",
293
+ text: JSON.stringify({
294
+ kibiProtocol: 1,
295
+ jobVersion: "kibi.job.v1",
296
+ jobId,
297
+ status: "unknown",
298
+ error: "No such job. Jobs are process-local and dropped on server restart; check the jobId or re-run the operation with async:true.",
299
+ }),
300
+ },
301
+ ],
302
+ structuredContent: {
303
+ kibiProtocol: 1,
304
+ jobVersion: "kibi.job.v1",
305
+ jobId,
306
+ status: "unknown",
307
+ error: "No such job. Jobs are process-local and dropped on server restart; check the jobId or re-run the operation with async:true.",
308
+ },
309
+ };
310
+ }
311
+ const snapshot = jobSnapshot(job);
312
+ return {
313
+ content: [{ type: "text", text: JSON.stringify(snapshot) }],
314
+ structuredContent: snapshot,
315
+ };
316
+ }, undefined, undefined, undefined, {
317
+ title: "Poll a Kibi background job",
318
+ readOnlyHint: true,
319
+ destructiveHint: false,
320
+ idempotentHint: true,
321
+ openWorldHint: false,
322
+ });
266
323
  }
@@ -15,7 +15,7 @@
15
15
  You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
- import { existsSync, readFileSync, realpathSync } from "node:fs";
18
+ import * as fs from "node:fs";
19
19
  import { createRequire } from "node:module";
20
20
  import path from "node:path";
21
21
  import { fileURLToPath } from "node:url";
@@ -28,35 +28,33 @@ function toEntrypointPath(entrypointUrl) {
28
28
  return path.resolve(entrypointUrl);
29
29
  }
30
30
  function readJson(filePath) {
31
- return JSON.parse(readFileSync(filePath, "utf8"));
31
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
32
+ }
33
+ export function nextAncestorDirectory(current) {
34
+ const parent = path.dirname(current);
35
+ return parent === current ? undefined : parent;
32
36
  }
33
37
  function nearestPackageJson(startPath) {
34
38
  let current = path.dirname(startPath);
35
- while (true) {
39
+ while (current !== undefined) {
36
40
  const candidate = path.join(current, "package.json");
37
- if (existsSync(candidate)) {
41
+ if (fs.existsSync(candidate)) {
38
42
  return candidate;
39
43
  }
40
- const parent = path.dirname(current);
41
- if (parent === current) {
42
- throw new Error(`Unable to find package.json for ${startPath}`);
43
- }
44
- current = parent;
44
+ current = nextAncestorDirectory(current);
45
45
  }
46
+ throw new Error(`Unable to find package.json for ${startPath}`);
46
47
  }
47
48
  function nearestPackageJsonFromDirectory(startPath) {
48
49
  let current = path.resolve(startPath);
49
- while (true) {
50
+ while (current !== undefined) {
50
51
  const candidate = path.join(current, "package.json");
51
- if (existsSync(candidate)) {
52
+ if (fs.existsSync(candidate)) {
52
53
  return candidate;
53
54
  }
54
- const parent = path.dirname(current);
55
- if (parent === current) {
56
- return null;
57
- }
58
- current = parent;
55
+ current = nextAncestorDirectory(current);
59
56
  }
57
+ return null;
60
58
  }
61
59
  function packageInfoFromEntrypoint(entrypoint) {
62
60
  const normalizedEntrypoint = path.resolve(entrypoint);
@@ -82,7 +80,7 @@ export function resolveProjectLocalMcp(cwd) {
82
80
  }
83
81
  const projectRequire = createRequire(projectPackageJson);
84
82
  const resolved = projectRequire.resolve(PACKAGE_NAME);
85
- const entrypoint = realpathSync.native?.(resolved) ?? realpathSync(resolved);
83
+ const entrypoint = fs.realpathSync.native?.(resolved) ?? fs.realpathSync(resolved);
86
84
  return packageInfoFromEntrypoint(entrypoint);
87
85
  }
88
86
  catch (error) {
@@ -2,6 +2,13 @@ import path from "node:path";
2
2
  import { PrologProcess, resolveKbPlPath } from "kibi-runtime";
3
3
  import { escapeAtomContent } from "kibi-runtime";
4
4
  import { getCoreModulePathOverride, getKbPlPathOverride } from "../env.js";
5
+ export function parseMaybeDoubleEncodedJson(rawJson) {
6
+ let parsed = JSON.parse(rawJson);
7
+ if (typeof parsed === "string") {
8
+ parsed = JSON.parse(parsed);
9
+ }
10
+ return parsed;
11
+ }
5
12
  function isPrologProcess(value) {
6
13
  return (value instanceof PrologProcess ||
7
14
  (value !== null &&
@@ -60,11 +67,7 @@ export async function runJsonModuleQuery(prolog, fileName, goal, errorLabel) {
60
67
  if (!rawJson) {
61
68
  throw new Error(`${errorLabel} query returned no JsonString binding`);
62
69
  }
63
- let parsed = JSON.parse(rawJson);
64
- if (typeof parsed === "string") {
65
- parsed = JSON.parse(parsed);
66
- }
67
- return parsed;
70
+ return parseMaybeDoubleEncodedJson(rawJson);
68
71
  }
69
72
  async function runInteractiveModuleQuery(prolog, modulePath, goal, errorLabel) {
70
73
  const loadResult = await prolog.query(`use_module('${modulePath}')`);
@@ -38,7 +38,7 @@ const MCP_TOOL_ORDER = [
38
38
  "kb_plan_bootstrap",
39
39
  "kb_compile_intent",
40
40
  "kb_apply_plan",
41
- "kb_ingest_verification",
41
+ "kb_ingest_proof",
42
42
  ];
43
43
  // implements REQ-002
44
44
  const TOOL_ANNOTATIONS = {
@@ -134,8 +134,8 @@ const TOOL_ANNOTATIONS = {
134
134
  idempotentHint: false,
135
135
  openWorldHint: false,
136
136
  },
137
- kb_ingest_verification: {
138
- title: "Ingest contracted verification evidence",
137
+ kb_ingest_proof: {
138
+ title: "Ingest proof-run evidence",
139
139
  readOnlyHint: false,
140
140
  destructiveHint: false,
141
141
  idempotentHint: false,
package/dist/workspace.js CHANGED
@@ -68,19 +68,20 @@ function readFirstEnv(keys) {
68
68
  }
69
69
  return null;
70
70
  }
71
+ export function nextAncestorDirectory(current) {
72
+ const parent = path.dirname(current);
73
+ return parent === current ? undefined : parent;
74
+ }
71
75
  function findUpwards(startDir, marker) {
72
76
  let current = path.resolve(startDir);
73
- while (true) {
77
+ while (current !== undefined) {
74
78
  const candidate = path.join(current, marker);
75
79
  if (fs.existsSync(candidate)) {
76
80
  return current;
77
81
  }
78
- const parent = path.dirname(current);
79
- if (parent === current) {
80
- return null;
81
- }
82
- current = parent;
82
+ current = nextAncestorDirectory(current);
83
83
  }
84
+ return null;
84
85
  }
85
86
  function isBranchPath(p) {
86
87
  const parent = path.basename(path.dirname(p));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.30.0",
6
6
  "ajv": "^8.18.0",
@@ -9,8 +9,8 @@
9
9
  "fast-glob": "^3.2.12",
10
10
  "gray-matter": "^4.0.3",
11
11
  "js-yaml": "^4.1.0",
12
- "kibi-runtime": "^1.0.1",
13
- "kibi-core": "^0.11.0",
12
+ "kibi-runtime": "^2.0.0",
13
+ "kibi-core": "^0.12.0",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"
@@ -24,7 +24,8 @@
24
24
  "scripts": {
25
25
  "test": "bun test",
26
26
  "dev": "bun run bin/kibi-mcp",
27
- "build": "tsc -p tsconfig.json",
27
+ "clean": "rm -rf dist",
28
+ "build": "npm run clean && tsc -p tsconfig.json",
28
29
  "typecheck": "tsc -p tsconfig.json --noEmit",
29
30
  "prepack": "npm run build && node ../../scripts/verify-package-contract.mjs kibi-mcp"
30
31
  },