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.
@@ -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) => {
@@ -26,6 +27,9 @@ export function registerConfiguredTools(server, runtime, registerTool) {
26
27
  name,
27
28
  effects: publicSpec.effects,
28
29
  requiresProlog: publicSpec.requiresProlog,
30
+ ...(publicSpec.agentVisibleStructuredData === true
31
+ ? { agentVisibleStructuredData: true }
32
+ : {}),
29
33
  execute: (args, context) => execute(context, args),
30
34
  };
31
35
  registerTool(server, name, definition.description, definition.inputSchema, async () => undefined, runtime, spec, definition.annotations, definition.outputSchema);
@@ -80,7 +84,7 @@ export function registerConfiguredTools(server, runtime, registerTool) {
80
84
  });
81
85
  register({
82
86
  name: "kb_semantic_advisor",
83
- execute: async (_context, args) => runtime.handleKbSemanticAdvisor(args),
87
+ execute: async (context, args) => runtime.handleKbSemanticAdvisor(args, context),
84
88
  });
85
89
  register({
86
90
  name: "kb_upsert",
@@ -96,15 +100,24 @@ export function registerConfiguredTools(server, runtime, registerTool) {
96
100
  });
97
101
  register({
98
102
  name: "kb_check",
99
- execute: async (context, args) => runtime.handleKbCheck(prologFor(context), args),
103
+ execute: async (context, args) => {
104
+ const { async: asyncMode, ...checkArgs } = args;
105
+ if (asyncMode !== true) {
106
+ return runtime.handleKbCheck(prologFor(context), args);
107
+ }
108
+ // Long-KB full checks can exceed the tool timeout. Detach into a job
109
+ // and return the kibi.job.v1 receipt immediately; the agent polls
110
+ // kb_job_status for the terminal state and full result.
111
+ return startJob("kb_check", () => runtime.handleKbCheck(prologFor(context), checkArgs));
112
+ },
100
113
  });
101
114
  register({
102
115
  name: "kb_model_requirement",
103
- execute: async (context, args) => runtime.handleKbModelRequirement(prologFor(context), args),
116
+ execute: async (context, args) => runtime.handleKbModelRequirement(prologFor(context), args, withSessionProlog(context)),
104
117
  });
105
118
  register({
106
119
  name: "kb_suggest_predicates",
107
- execute: async (context, args) => runtime.handleKbSuggestPredicates(prologFor(context), args),
120
+ execute: async (context, args) => runtime.handleKbSuggestPredicates(prologFor(context), args, withSessionProlog(context)),
108
121
  });
109
122
  register({
110
123
  name: "kb_plan_bootstrap",
@@ -123,9 +136,9 @@ export function registerConfiguredTools(server, runtime, registerTool) {
123
136
  : executeApplyPlan(args, context),
124
137
  });
125
138
  register({
126
- name: "kb_ingest_verification",
127
- execute: async (context, args) => runtime.handleKbIngestVerification
128
- ? runtime.handleKbIngestVerification(args, context)
129
- : executeIngestVerification(args, context),
139
+ name: "kb_ingest_proof",
140
+ execute: async (context, args) => runtime.handleKbIngestProof
141
+ ? runtime.handleKbIngestProof(args, context)
142
+ : executeIngestProof(args, context),
130
143
  });
131
144
  }
@@ -49,10 +49,15 @@ function adaptProlog(prolog) {
49
49
  if (existing) {
50
50
  return existing;
51
51
  }
52
+ const engine = prolog;
53
+ const queryEntities = engine.queryEntities;
54
+ const searchEntities = engine.searchEntities;
55
+ const storageStatus = engine.storageStatus;
56
+ const queryStatusJson = engine.queryStatusJson;
52
57
  let lastResult = null;
53
58
  const port = {
54
- query: async (goal) => {
55
- lastResult = await prolog.query(goal);
59
+ query: async (goal, signal) => {
60
+ lastResult = await engine.query(goal, signal);
56
61
  return lastResult;
57
62
  },
58
63
  nextSolution: async () => {
@@ -60,39 +65,41 @@ function adaptProlog(prolog) {
60
65
  lastResult = null;
61
66
  return result;
62
67
  },
63
- save: () => prolog.query("kb_save"),
64
- ...(typeof prolog.queryEntities ===
65
- "function"
68
+ save: (signal) => engine.query("kb_save", signal),
69
+ ...(typeof queryEntities === "function"
66
70
  ? {
67
- queryEntities: (input) => prolog.queryEntities(input),
71
+ // The captured method uses .call(engine, ...) below to preserve the
72
+ // EngineClient receiver (unbound queryStatusJson breaks this.command).
73
+ queryEntities: (input, signal) => queryEntities.call(engine, input, signal),
68
74
  }
69
75
  : {}),
70
- ...(typeof prolog.searchEntities ===
71
- "function"
76
+ ...(typeof searchEntities === "function"
72
77
  ? {
73
- searchEntities: (input) => prolog.searchEntities(input),
78
+ searchEntities: (input, signal) => searchEntities.call(engine, input, signal),
74
79
  }
75
80
  : {}),
76
- ...(typeof prolog.storageStatus ===
77
- "function"
81
+ ...(typeof storageStatus === "function"
78
82
  ? {
79
- storageStatus: () => prolog.storageStatus(),
83
+ storageStatus: () => storageStatus.call(engine),
80
84
  }
81
85
  : {}),
82
- ...(typeof prolog.queryStatusJson ===
83
- "function"
86
+ ...(typeof queryStatusJson === "function"
84
87
  ? {
85
- queryStatusJson: () => prolog.queryStatusJson(),
88
+ queryStatusJson: (signal) => queryStatusJson.call(engine, signal),
86
89
  }
87
90
  : {}),
88
91
  };
89
92
  prologPorts.set(prolog, port);
90
93
  return port;
91
94
  }
95
+ /** Test seam: adapt a session engine so AbortSignal reaches EngineClient/PrologPort methods. */
96
+ export function _adaptPrologForTests(prolog) {
97
+ return adaptProlog(prolog);
98
+ }
92
99
  const operationRuntime = createMcpRuntime({
93
100
  workspaceRoot: resolveWorkspaceRoot(),
94
- activeBranchName: async () => (await getSessionModule()).activeBranchName,
95
- attachedBranchKbPath: async () => (await getSessionModule()).attachedBranchKbPath,
101
+ activeBranchName: async () => (await getSessionModule()).getActiveBranchName(),
102
+ attachedBranchKbPath: async () => (await getSessionModule()).getAttachedBranchKbPath(),
96
103
  ensureProlog: async () => (await getSessionModule()).ensureProlog(),
97
104
  adaptProlog,
98
105
  fs: nodeFilesystem,
@@ -100,7 +107,7 @@ const operationRuntime = createMcpRuntime({
100
107
  net: { fetch: (input, init) => globalThis.fetch(input, init) },
101
108
  refreshAttachedBranchStamp: async () => {
102
109
  const session = await getSessionModule();
103
- const kbPath = session.attachedBranchKbPath;
110
+ const kbPath = session.getAttachedBranchKbPath();
104
111
  if (kbPath) {
105
112
  try {
106
113
  session.updateAttachedBranchStamp(await readBranchKbStamp(kbPath));
@@ -124,12 +131,12 @@ export const DEFAULT_TOOLS_RUNTIME = {
124
131
  // runtime interface with looser Record<string, unknown> inputSchema. The cast is safe
125
132
  // because the tool definitions are statically authored and validated at startup.
126
133
  tools: TOOLS,
127
- activeBranchName: async () => (await getSessionModule()).activeBranchName,
134
+ activeBranchName: async () => (await getSessionModule()).getActiveBranchName(),
128
135
  ensureProlog: async () => (await getSessionModule()).ensureProlog(),
129
136
  resetProlog: async (reason) => (await getSessionModule()).resetProlog(reason),
130
137
  inFlightRequests: async () => (await getSessionModule()).inFlightRequests,
131
- isShuttingDown: async () => (await getSessionModule()).isShuttingDown,
132
- prologProcess: async () => (await getSessionModule()).prologProcess,
138
+ isShuttingDown: async () => (await getSessionModule()).getIsShuttingDown(),
139
+ prologProcess: async () => (await getSessionModule()).getPrologProcess(),
133
140
  operationRuntime,
134
141
  handleKbCheck,
135
142
  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";
@@ -27,10 +28,32 @@ export { jsonSchemaToZod } from "./json-schema-to-zod.js";
27
28
  export { _resetSessionModulePromise, _setToolsServerDepsForTests };
28
29
  const DEFAULT_TOOL_TIMEOUT_MS = 90_000;
29
30
  const TOOL_TIMEOUT_ENV = "KIBI_MCP_TOOL_TIMEOUT_MS";
30
- // Give cancellation and the Prolog reset a short bounded grace period, but do
31
- // not hold the MCP request open for the full five-second shutdown budget when
32
- // a handler never observes AbortSignal.
31
+ // Give cancellation a short bounded grace period, but do not hold the MCP
32
+ // request open for the full five-second shutdown budget when a handler never
33
+ // observes AbortSignal.
33
34
  const TOOL_TIMEOUT_GRACE_MS = 100;
35
+ // Mutations that ignore AbortSignal may leave the journal indeterminate. Only
36
+ // then tear down the shared session engine — never on ordinary read timeouts,
37
+ // which would reject sibling tools with "Kibi engine connection closed".
38
+ const TOOL_TIMEOUT_WEDGE_MS = 1_000;
39
+ function isMutationEffect(effects) {
40
+ return (effects?.some((effect) => effect === "kb-write" || effect === "workspace-write") ?? false);
41
+ }
42
+ /** Embed envelope data in content text for opted-in tools (hosts that hide structuredContent).
43
+ * Preserves non-text content parts; only augments/replaces text parts. */
44
+ function withAgentVisibleText(content, data) {
45
+ const textParts = content.filter((part) => part.type === "text" && typeof part.text === "string");
46
+ const nonTextParts = content.filter((part) => part.type !== "text");
47
+ const summary = textParts
48
+ .map((part) => part.text)
49
+ .join(" ")
50
+ .replace(/\s+/g, " ")
51
+ .trim();
52
+ const payload = JSON.stringify(data);
53
+ // Prefer JSON-first so first-line-only host summaries still include machine data.
54
+ const text = summary.length > 0 ? `${payload}\n${summary}` : payload;
55
+ return [{ type: "text", text }, ...nonTextParts];
56
+ }
34
57
  // implements REQ-008
35
58
  function debugLog(...args) {
36
59
  if (isMcpDebugEnabled()) {
@@ -63,27 +86,28 @@ async function withToolTimeout(toolName, operation, onTimeout) {
63
86
  const timeoutMs = getToolTimeoutMs();
64
87
  let timeout;
65
88
  try {
66
- return await Promise.race([
67
- operation,
68
- new Promise((_, reject) => {
69
- timeout = setTimeout(() => {
70
- const error = createToolTimeoutError(toolName, timeoutMs);
71
- // Do not report the timeout until cancellation/reset has completed
72
- // and the original request has reached a terminal state. This is
73
- // especially important for source-first mutations: a caller must
74
- // never retry while the authoritative journal is still deciding its
75
- // outcome.
76
- void onTimeout(error, timeoutMs)
77
- .then(async () => {
78
- await Promise.race([
79
- operation.then(() => undefined, () => undefined),
80
- new Promise((resolve) => setTimeout(resolve, TOOL_TIMEOUT_GRACE_MS)),
81
- ]);
82
- })
83
- .finally(() => reject(error));
84
- }, timeoutMs);
85
- }),
86
- ]);
89
+ const timeoutPromise = new Promise((_, reject) => {
90
+ timeout = setTimeout(() => {
91
+ const error = createToolTimeoutError(toolName, timeoutMs);
92
+ // Do not report the timeout until cancellation/reset has completed
93
+ // and the original request has reached a terminal state. This is
94
+ // especially important for source-first mutations: a caller must
95
+ // never retry while the authoritative journal is still deciding its
96
+ // outcome.
97
+ void onTimeout(error, timeoutMs)
98
+ .then(async () => {
99
+ await Promise.race([
100
+ operation.then(() => undefined, () => undefined),
101
+ new Promise((resolve) => setTimeout(resolve, TOOL_TIMEOUT_GRACE_MS)),
102
+ ]);
103
+ })
104
+ .finally(() => reject(error));
105
+ }, timeoutMs);
106
+ });
107
+ // When abort settles `operation` before this timeout promise rejects,
108
+ // the late reject must not become an unhandled rejection.
109
+ timeoutPromise.catch(() => undefined);
110
+ return await Promise.race([operation, timeoutPromise]);
87
111
  }
88
112
  finally {
89
113
  if (timeout) {
@@ -138,15 +162,30 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
138
162
  try {
139
163
  // Execute handler
140
164
  const result = await withToolTimeout(name, handlerPromise, async (error) => {
141
- resetAttempted = true;
142
165
  controller.abort(error);
166
+ // Read/discovery timeouts must cancel in-flight RPCs without
167
+ // terminating the shared session engine. A global resetProlog here
168
+ // rejects sibling tools with "Kibi engine connection closed".
169
+ if (!isMutationEffect(operationSpec.effects)) {
170
+ return;
171
+ }
172
+ const settled = await Promise.race([
173
+ handlerPromise.then(() => true, () => true),
174
+ new Promise((resolve) => setTimeout(() => resolve(false), TOOL_TIMEOUT_WEDGE_MS)),
175
+ ]);
176
+ if (settled) {
177
+ return;
178
+ }
179
+ resetAttempted = true;
143
180
  try {
144
181
  await runtime.resetProlog(`tool timeout: ${name}`);
145
182
  resetSucceeded = true;
146
183
  }
147
- catch (error) {
184
+ catch (resetFailure) {
148
185
  resetError =
149
- error instanceof Error ? error.message : String(error);
186
+ resetFailure instanceof Error
187
+ ? resetFailure.message
188
+ : String(resetFailure);
150
189
  }
151
190
  });
152
191
  const data = operationData(result);
@@ -168,8 +207,13 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
168
207
  typeof result === "object" &&
169
208
  !Array.isArray(result) &&
170
209
  "content" in result) {
210
+ const withContent = result;
211
+ const content = operationSpec.agentVisibleStructuredData === true
212
+ ? withAgentVisibleText(withContent.content, data)
213
+ : withContent.content;
171
214
  return {
172
215
  ...result,
216
+ content,
173
217
  structuredContent: envelope,
174
218
  };
175
219
  }
@@ -194,7 +238,7 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
194
238
  });
195
239
  }
196
240
  if (isToolTimeoutError(error) &&
197
- operationSpec.effects.some((effect) => effect === "kb-write" || effect === "workspace-write")) {
241
+ isMutationEffect(operationSpec.effects)) {
198
242
  const recoveryActions = [
199
243
  {
200
244
  operation: "kb_status",
@@ -263,4 +307,60 @@ export function registerAllTools(server,
263
307
  // INTENTIONAL: same generic bridge cast as addTool — see comment there.
264
308
  runtime = DEFAULT_TOOLS_RUNTIME) {
265
309
  registerConfiguredTools(server, runtime, addTool);
310
+ registerJobStatusTool(server);
311
+ }
312
+ /**
313
+ * kb_job_status polls background jobs started by long-running operations
314
+ * (currently `kb_check` with `async: true`). This is an MCP-server-native
315
+ * companion to the job receipts, not part of the canonical CLI operation
316
+ * catalog: jobs live in the server process and have no CLI counterpart.
317
+ */
318
+ function registerJobStatusTool(server) {
319
+ 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).", {
320
+ type: "object",
321
+ properties: {
322
+ jobId: {
323
+ type: "string",
324
+ description: "The jobId from the kibi.job.v1 receipt.",
325
+ },
326
+ },
327
+ required: ["jobId"],
328
+ }, async (args) => {
329
+ const jobId = typeof args.jobId === "string" ? args.jobId : "";
330
+ const job = jobId === "" ? undefined : getJob(jobId);
331
+ if (job === undefined) {
332
+ return {
333
+ content: [
334
+ {
335
+ type: "text",
336
+ text: JSON.stringify({
337
+ kibiProtocol: 1,
338
+ jobVersion: "kibi.job.v1",
339
+ jobId,
340
+ status: "unknown",
341
+ error: "No such job. Jobs are process-local and dropped on server restart; check the jobId or re-run the operation with async:true.",
342
+ }),
343
+ },
344
+ ],
345
+ structuredContent: {
346
+ kibiProtocol: 1,
347
+ jobVersion: "kibi.job.v1",
348
+ jobId,
349
+ status: "unknown",
350
+ error: "No such job. Jobs are process-local and dropped on server restart; check the jobId or re-run the operation with async:true.",
351
+ },
352
+ };
353
+ }
354
+ const snapshot = jobSnapshot(job);
355
+ return {
356
+ content: [{ type: "text", text: JSON.stringify(snapshot) }],
357
+ structuredContent: snapshot,
358
+ };
359
+ }, undefined, undefined, {
360
+ title: "Poll a Kibi background job",
361
+ readOnlyHint: true,
362
+ destructiveHint: false,
363
+ idempotentHint: true,
364
+ openWorldHint: false,
365
+ });
266
366
  }
@@ -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) {
@@ -22,7 +22,7 @@ export function hasImpactOptions(args) {
22
22
  args.includeWorkingTreeDiff ||
23
23
  (args.sourceFiles && args.sourceFiles.length > 0));
24
24
  }
25
- export function analyzeKbCheckImpact(workspaceRoot, args) {
25
+ export async function analyzeKbCheckImpact(workspaceRoot, args) {
26
26
  if (!hasImpactOptions(args)) {
27
27
  return undefined;
28
28
  }
@@ -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}')`);
@@ -1,12 +1,21 @@
1
1
  import { resolveWorkspaceRoot } from "../workspace.js";
2
2
  export function createDiscoveryContext(prolog, baseContext) {
3
3
  // implements REQ-kibi-operation-interface-parity
4
+ // Keep this adapter aligned with tools-runtime adaptProlog: indexed discovery
5
+ // and typed status must reach EngineClient command frames (not query-only
6
+ // fallbacks), and AbortSignal must forward for MCP tool cancellation.
7
+ const engine = prolog;
8
+ const queryEntities = engine.queryEntities;
9
+ const searchEntities = engine.searchEntities;
10
+ const storageStatus = engine.storageStatus;
11
+ const queryStatusJson = engine.queryStatusJson;
4
12
  let lastResult = null;
5
13
  const mode = prolog
6
14
  .useOneShotMode;
15
+ const signal = baseContext?.signal ?? new AbortController().signal;
7
16
  const port = {
8
- query: async (goal) => {
9
- lastResult = await prolog.query(goal);
17
+ query: async (goal, querySignal) => {
18
+ lastResult = await engine.query(goal, querySignal ?? signal);
10
19
  return lastResult;
11
20
  },
12
21
  oneShotMode: mode === undefined
@@ -17,18 +26,32 @@ export function createDiscoveryContext(prolog, baseContext) {
17
26
  lastResult = null;
18
27
  return result;
19
28
  },
20
- save: () => prolog.query("kb_save"),
21
- ...(typeof prolog.queryStatusJson ===
22
- "function"
29
+ save: (saveSignal) => engine.query("kb_save", saveSignal ?? signal),
30
+ ...(typeof queryEntities === "function"
23
31
  ? {
24
- queryStatusJson: () => prolog.queryStatusJson(),
32
+ queryEntities: (input, querySignal) => queryEntities.call(engine, input, querySignal ?? signal),
33
+ }
34
+ : {}),
35
+ ...(typeof searchEntities === "function"
36
+ ? {
37
+ searchEntities: (input, querySignal) => searchEntities.call(engine, input, querySignal ?? signal),
38
+ }
39
+ : {}),
40
+ ...(typeof storageStatus === "function"
41
+ ? {
42
+ storageStatus: () => storageStatus.call(engine),
43
+ }
44
+ : {}),
45
+ ...(typeof queryStatusJson === "function"
46
+ ? {
47
+ queryStatusJson: (statusSignal) => queryStatusJson.call(engine, statusSignal ?? signal),
25
48
  }
26
49
  : {}),
27
50
  };
28
51
  return {
29
52
  ...(baseContext ?? {}),
30
53
  workspaceRoot: baseContext?.workspaceRoot ?? resolveWorkspaceRoot(),
31
- signal: baseContext?.signal ?? new AbortController().signal,
54
+ signal,
32
55
  clock: baseContext?.clock ?? (() => new Date()),
33
56
  prolog: port,
34
57
  };
@@ -2,11 +2,11 @@ import { modelRequirementSpec } from "kibi-runtime";
2
2
  import { estimateNormativeSignalConfidence, extractRequirementClaim, getWorkspaceMigrationWarning, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, } from "kibi-runtime";
3
3
  import { resolveWorkspaceRoot } from "../workspace.js";
4
4
  export { estimateNormativeSignalConfidence, extractRequirementClaim, getWorkspaceMigrationWarning, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, };
5
- export async function handleKbModelRequirement(_prolog, args) {
6
- const context = {
5
+ export async function handleKbModelRequirement(_prolog, args, context) {
6
+ const resolved = context ?? {
7
7
  workspaceRoot: resolveWorkspaceRoot(),
8
8
  signal: new AbortController().signal,
9
9
  clock: () => new Date(),
10
10
  };
11
- return modelRequirementSpec.execute(args, context);
11
+ return modelRequirementSpec.execute(args, resolved);
12
12
  }
@@ -1,4 +1,4 @@
1
1
  import { executeSemanticAdvisor } from "kibi-runtime";
2
- export async function handleKbSemanticAdvisor(args) {
3
- return executeSemanticAdvisor(args);
2
+ export async function handleKbSemanticAdvisor(args, context) {
3
+ return executeSemanticAdvisor(args, context);
4
4
  }
@@ -1,13 +1,14 @@
1
1
  import { suggestPredicatesSpec } from "kibi-runtime";
2
2
  import { resolveWorkspaceRoot } from "../workspace.js";
3
3
  import { createDiscoveryContext } from "./discovery-adapter.js";
4
- export async function handleKbSuggestPredicates(prolog, args) {
5
- const context = prolog === null
6
- ? {
7
- workspaceRoot: resolveWorkspaceRoot(),
8
- signal: new AbortController().signal,
9
- clock: () => new Date(),
10
- }
11
- : createDiscoveryContext(prolog);
12
- return suggestPredicatesSpec.execute(args, context);
4
+ export async function handleKbSuggestPredicates(prolog, args, context) {
5
+ const resolved = context ??
6
+ (prolog === null
7
+ ? {
8
+ workspaceRoot: resolveWorkspaceRoot(),
9
+ signal: new AbortController().signal,
10
+ clock: () => new Date(),
11
+ }
12
+ : createDiscoveryContext(prolog));
13
+ return suggestPredicatesSpec.execute(args, resolved);
13
14
  }
@@ -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,