kibi-mcp 0.19.2 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@
7
7
  * the Free Software Foundation, either version 3 of the License, or
8
8
  * (at your option) any later version.
9
9
  */
10
+ import { createHash } from "node:crypto";
10
11
  import fs from "node:fs";
11
12
  import path from "node:path";
12
13
  import { resolveWorkspaceRoot } from "./workspace.js";
@@ -21,7 +22,14 @@ export function initializeDiagnosticMode(enabled = DIAGNOSTIC_MODE_ENABLED) {
21
22
  return;
22
23
  }
23
24
  const workspaceRoot = resolveWorkspaceRoot();
24
- diagnosticUsageLogPath = path.join(workspaceRoot, ".kb", "usage.log");
25
+ // Evaluators may run an independent read-only verification client in the
26
+ // same fixture workspace after the model. Allow that client to use a
27
+ // separate receipt so model-originated usage can be reconciled without
28
+ // accidentally treating verifier calls as model evidence. Normal Kibi
29
+ // operation remains rooted at `.kb/usage.log`.
30
+ diagnosticUsageLogPath =
31
+ process.env.KIBI_MCP_DIAGNOSTIC_USAGE_LOG_PATH ??
32
+ path.join(workspaceRoot, ".kb", "usage.log");
25
33
  process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
26
34
  }
27
35
  export function appendUsageLogLine(entry) {
@@ -56,6 +64,14 @@ export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
56
64
  type: "string",
57
65
  description: "If you had to split your task into multiple steps because this tool lacks a specific filtering or querying capability, describe what parameter is missing. Otherwise, leave empty.",
58
66
  },
67
+ session_id: {
68
+ type: "string",
69
+ description: "Optional opaque workflow-session identifier used to correlate advisor, preflight, mutation, coverage, and verification evidence. Do not put personal data in this field.",
70
+ },
71
+ actor_id: {
72
+ type: "string",
73
+ description: "Optional opaque actor identifier used to prevent evidence from a different actor being correlated with this call. Do not put personal data in this field.",
74
+ },
59
75
  },
60
76
  };
61
77
  function isRecord(value) {
@@ -96,6 +112,69 @@ function appendSemanticAdvisorFields(fields, receipt) {
96
112
  fields.semantic_suggestion_kinds = suggestionKinds;
97
113
  fields.semantic_suggestion_count = suggestionKinds.length;
98
114
  fields.semantic_next_tools = stringArray(receipt.suggested_next_tools);
115
+ const inventoryContract = receipt.inventory_contract;
116
+ if (isRecord(inventoryContract) &&
117
+ typeof inventoryContract.source_hash === "string") {
118
+ fields.semantic_source_hash = inventoryContract.source_hash;
119
+ }
120
+ }
121
+ function canonicalDiagnosticValue(value) {
122
+ if (Array.isArray(value))
123
+ return value.map(canonicalDiagnosticValue);
124
+ if (!isRecord(value))
125
+ return value;
126
+ return Object.fromEntries(Object.entries(value)
127
+ .filter(([key]) => key !== "_diagnostic_telemetry")
128
+ .sort(([left], [right]) => left.localeCompare(right))
129
+ .map(([key, child]) => [key, canonicalDiagnosticValue(child)]));
130
+ }
131
+ function mutationFingerprint(args) {
132
+ return createHash("sha256")
133
+ .update(JSON.stringify(canonicalDiagnosticValue(args)))
134
+ .digest("hex");
135
+ }
136
+ function appendMutationFields(fields, args) {
137
+ fields.mutation_fingerprint = mutationFingerprint(args);
138
+ const type = typeof args.type === "string" ? args.type : "unknown";
139
+ const id = typeof args.id === "string" ? args.id : "unknown";
140
+ fields.mutation_target = `${type}:${id}`;
141
+ }
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_snapshot_unavailable",
148
+ ]);
149
+ function appendCoverageFields(fields, args, structuredContent) {
150
+ const rows = Array.isArray(structuredContent.rows)
151
+ ? structuredContent.rows.filter(isRecord)
152
+ : [];
153
+ const summary = isRecord(structuredContent.summary)
154
+ ? structuredContent.summary
155
+ : {};
156
+ const gapCodes = rows.flatMap((row) => stringArray(row.proofGaps));
157
+ const uniqueGapCodes = [...new Set(gapCodes)].sort();
158
+ const repairPlan = isRecord(structuredContent.repairPlan)
159
+ ? structuredContent.repairPlan
160
+ : undefined;
161
+ const scope = isRecord(repairPlan?.scope) ? repairPlan.scope : undefined;
162
+ fields.coverage_by = typeof args.by === "string" ? args.by : "req";
163
+ fields.coverage_requirement_count = Number(summary.total ?? rows.length);
164
+ fields.coverage_proven_count = Number(summary.proofProven ?? 0);
165
+ fields.coverage_proof_missing_count = Number(summary.proofMissing ?? 0);
166
+ fields.coverage_proof_gap_count = gapCodes.length;
167
+ fields.coverage_gap_codes = uniqueGapCodes;
168
+ fields.coverage_receipt_gap_count = gapCodes.filter((gap) => RECEIPT_GAP_CODES.has(gap)).length;
169
+ if (typeof scope?.complete === "boolean") {
170
+ fields.coverage_scope_complete = scope.complete;
171
+ }
172
+ const metadata = isRecord(structuredContent.meta)
173
+ ? structuredContent.meta
174
+ : undefined;
175
+ if (typeof metadata?.verificationSnapshot === "string") {
176
+ fields.coverage_verification_snapshot = metadata.verificationSnapshot;
177
+ }
99
178
  }
100
179
  function appendPredicateSuggestionFields(fields, structuredContent) {
101
180
  const candidates = Array.isArray(structuredContent.candidates)
@@ -148,6 +227,12 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
148
227
  fields.telemetry_is_autonomous = telemetry.is_autonomous ?? null;
149
228
  fields.telemetry_confidence_score = telemetry.confidence_score ?? null;
150
229
  fields.telemetry_attempt_number = telemetry.attempt_number ?? null;
230
+ if (typeof telemetry.session_id === "string") {
231
+ fields.session_id = telemetry.session_id;
232
+ }
233
+ if (typeof telemetry.actor_id === "string") {
234
+ fields.actor_id = telemetry.actor_id;
235
+ }
151
236
  }
152
237
  const structuredContent = structuredContentFrom(result);
153
238
  if (toolName === "kb_query" || toolName === "kb_search") {
@@ -164,6 +249,10 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
164
249
  fields.result_summary =
165
250
  violationCount === 0 ? "0 violations" : `${violationCount} violations`;
166
251
  }
252
+ if (toolName === "kb_coverage" && structuredContent) {
253
+ appendCoverageFields(fields, args, structuredContent);
254
+ fields.result_summary = `${String(fields.coverage_proven_count)} proven; ${String(fields.coverage_proof_gap_count)} proof gaps`;
255
+ }
167
256
  if (toolName === "kb_semantic_advisor" && structuredContent) {
168
257
  appendSemanticAdvisorFields(fields, structuredContent.receipt);
169
258
  const readiness = fields.semantic_logic_readiness;
@@ -182,6 +271,7 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
182
271
  : `${count} predicate candidates`;
183
272
  }
184
273
  if (toolName === "kb_upsert" && structuredContent) {
274
+ appendMutationFields(fields, args);
185
275
  const created = Number(structuredContent.created ?? 0);
186
276
  const updated = Number(structuredContent.updated ?? 0);
187
277
  fields.upsert_created = created;
@@ -194,6 +284,15 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
194
284
  fields.result_summary = `upsert ${created > 0 ? "created" : "updated"}; semantic ${readiness}`;
195
285
  }
196
286
  }
287
+ if (toolName === "kb_validate_upsert" && structuredContent) {
288
+ appendMutationFields(fields, args);
289
+ fields.validation_valid = structuredContent.valid === true;
290
+ appendSemanticAdvisorFields(fields, structuredContent.semanticAdvisor);
291
+ fields.result_summary =
292
+ structuredContent.valid === true
293
+ ? "upsert payload valid"
294
+ : "upsert payload invalid";
295
+ }
197
296
  if (!fields.result_summary) {
198
297
  fields.result_summary = `${toolName} completed`;
199
298
  }
@@ -84,8 +84,9 @@ export const PROMPTS = [
84
84
  "## Rules",
85
85
  "- Never apply bootstrap writes without user-facing preview and explicit approval.",
86
86
  "- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
87
- "- Prefer MCP tools; CLI routes are available when MCP is unavailable.",
88
- "- If Kibi MCP tools are visible, use MCP. If MCP availability is unknown and a trusted local Kibi CLI is available, use its dedicated JSON routes with `--input <file|->`.",
87
+ "- MCP tools and the trusted project-local CLI are peer surfaces; choose by what is visible and approved here.",
88
+ "- If Kibi MCP tools are visible and approved, use MCP.",
89
+ "- Otherwise, in a trusted workspace, use the project-local CLI's dedicated JSON routes with `--input <file|->`.",
89
90
  "- If neither interface is available, Kibi operation is blocked; tell the operator to enable MCP or the trusted project-local CLI. Do not infer MCP availability from config file existence.",
90
91
  "- Do not read or edit `.kb/` files directly. Query before mutate. Run `kb_upsert` sequentially. Run `kb_check` before completion.",
91
92
  ].join("\n"),
@@ -103,7 +104,7 @@ export const PROMPTS = [
103
104
  "- `kb_query`: Exact lookup of entities by type, ID, tags, or source file",
104
105
  "- `kb_status`: Branch, snapshot, and freshness inspection",
105
106
  "- `kb_find_gaps`: Bulk missing/present relationship analysis",
106
- "- `kb_coverage`: Curated coverage reporting",
107
+ "- `kb_coverage`: Curated proof reporting plus deterministic read-only repair batches for requirements",
107
108
  "- `kb_graph`: Bounded graph traversal from seed IDs",
108
109
  "- `kb_upsert`: Create or update entities and their relationships",
109
110
  "- `kb_delete`: Remove entities by ID (with dependency safety checks)",
@@ -137,9 +138,11 @@ export const PROMPTS = [
137
138
  "4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
138
139
  "5. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property` (automated via `kb_model_requirement`).",
139
140
  "6. **Suggest predicates before prose**: For ontology-lane requirements, spell out the prose claim and call `kb_suggest_predicates` before writing `fact_kind: observation`. Apply the selected `fact_kind: predicate` applyPlan, then attach the returned `relationshipPlan` as `requires_predicate` while preserving existing req metadata; use the returned `review:ontology-gap` observation when no predicate fits.",
140
- "7. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
141
- "8. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need so scoped validation stays fast and skips the full-KB advisory scan.",
142
- "9. **Final check**: Run `kb_check` without `rules` before completion so hard `violations[]` and advisory full-KB `qualityDiagnostics[]` are both reviewed.",
141
+ "7. **Plan repairs**: Requirement `kb_coverage` returns `repairPlan`; require `repairPlan.scope.complete`, apply only a batch whose state is `ready`, and treat every batch as non-auto-applicable guidance.",
142
+ "8. **Mutate sequentially**: Query current endpoints, validate each payload, then call `kb_upsert` one at a time or `kb_delete` for explicit removals. Re-run `kb_coverage` after each batch rather than continuing from a stale plan.",
143
+ "9. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need so scoped validation stays fast and skips the full-KB advisory scan.",
144
+ "10. **Final check**: Run `kb_check` without `rules` before completion so hard `violations[]` and advisory full-KB `qualityDiagnostics[]`—including telemetry acceptance when usage evidence exists—are both reviewed.",
145
+ "11. **Telemetry gate**: When `.kb/usage.log` exists, run the CLI-only `kibi usage-metrics --format json --require-acceptance`; stale, partial, or insufficient `kibi.telemetry-acceptance.v1` evidence is not a pass.",
143
146
  "",
144
147
  "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
145
148
  ].join("\n"),
@@ -216,8 +219,10 @@ function registerDocResources() {
216
219
  "",
217
220
  "## Find missing coverage",
218
221
  '1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
219
- '2. `kb_coverage` with `{ "by": "req", "includePassing": false }` to review evaluated coverage rows',
220
- '3. `kb_graph` with `{ "seedIds": ["REQ-001"], "direction": "both", "depth": 2 }` to inspect neighboring entities',
222
+ '2. `kb_coverage` with `{ "by": "req", "includePassing": false }` to review proof rows and `repairPlan`; require `repairPlan.scope.complete` before treating it as a project-wide inventory',
223
+ "3. Apply only a `repairPlan` batch whose state is `ready`; batches are non-auto-applicable guidance, so query current endpoints, validate every write, and keep `kb_upsert` sequential",
224
+ "4. Re-run `kb_coverage` after each batch instead of continuing from a stale plan",
225
+ '5. `kb_graph` with `{ "seedIds": ["REQ-001"], "direction": "both", "depth": 2 }` to inspect neighboring entities',
221
226
  "",
222
227
  "## Add a requirement and link it to a test",
223
228
  '1. `kb_query` with `{ "type": "test" }` to check for existing test IDs',
@@ -41,6 +41,18 @@ export function jsonSchemaToZod(schema) {
41
41
  return z.any();
42
42
  }
43
43
  const obj = schema;
44
+ if (Object.hasOwn(obj, "const")) {
45
+ const description = typeof obj.description === "string" ? obj.description : undefined;
46
+ const value = obj.const;
47
+ if (typeof value === "string" ||
48
+ typeof value === "number" ||
49
+ typeof value === "boolean" ||
50
+ value === null) {
51
+ const literal = z.literal(value);
52
+ return description ? literal.describe(description) : literal;
53
+ }
54
+ return description ? z.any().describe(description) : z.any();
55
+ }
44
56
  if (Array.isArray(obj.enum) && obj.enum.length > 0) {
45
57
  const description = typeof obj.description === "string" ? obj.description : undefined;
46
58
  const literals = obj.enum.filter((value) => typeof value === "string" ||
@@ -39,7 +39,7 @@ export function registerConfiguredTools(server, runtime, registerTool) {
39
39
  });
40
40
  register({
41
41
  name: "kb_status",
42
- execute: async (context, args) => runtime.handleKbStatus(prologFor(context), args),
42
+ execute: async (context, args) => runtime.handleKbStatus(prologFor(context), args, context),
43
43
  });
44
44
  register({
45
45
  name: "kb_skills_list",
@@ -59,7 +59,7 @@ export function registerConfiguredTools(server, runtime, registerTool) {
59
59
  });
60
60
  register({
61
61
  name: "kb_coverage",
62
- execute: async (context, args) => runtime.handleKbCoverage(prologFor(context), args),
62
+ execute: async (context, args) => runtime.handleKbCoverage(prologFor(context), args, context),
63
63
  });
64
64
  register({
65
65
  name: "kb_graph",
@@ -1,4 +1,5 @@
1
1
  import { getSpec } from "kibi-cli/operations";
2
+ import { nodeGit } from "kibi-cli/operations/node-ports";
2
3
  import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, classifyDiagnosticError, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
3
4
  import { isMcpDebugEnabled } from "../env.js";
4
5
  import { createMcpRuntime } from "../runtime/mcp-runtime.js";
@@ -71,6 +72,7 @@ const operationRuntime = createMcpRuntime({
71
72
  attachedBranchKbPath: async () => (await getSessionModule()).attachedBranchKbPath,
72
73
  ensureProlog: async () => (await getSessionModule()).ensureProlog(),
73
74
  adaptProlog,
75
+ git: nodeGit,
74
76
  net: { fetch: (input, init) => globalThis.fetch(input, init) },
75
77
  refreshAttachedBranchStamp: async () => {
76
78
  const session = await getSessionModule();
@@ -44,6 +44,20 @@ function nearestPackageJson(startPath) {
44
44
  current = parent;
45
45
  }
46
46
  }
47
+ function nearestPackageJsonFromDirectory(startPath) {
48
+ let current = path.resolve(startPath);
49
+ while (true) {
50
+ const candidate = path.join(current, "package.json");
51
+ if (existsSync(candidate)) {
52
+ return candidate;
53
+ }
54
+ const parent = path.dirname(current);
55
+ if (parent === current) {
56
+ return null;
57
+ }
58
+ current = parent;
59
+ }
60
+ }
47
61
  function packageInfoFromEntrypoint(entrypoint) {
48
62
  const normalizedEntrypoint = path.resolve(entrypoint);
49
63
  const packageJsonPath = nearestPackageJson(normalizedEntrypoint);
@@ -62,7 +76,11 @@ export function readRunningPackageInfo(entrypointUrl) {
62
76
  }
63
77
  export function resolveProjectLocalMcp(cwd) {
64
78
  try {
65
- const projectRequire = createRequire(path.join(path.resolve(cwd), "package.json"));
79
+ const projectPackageJson = nearestPackageJsonFromDirectory(cwd);
80
+ if (!projectPackageJson) {
81
+ return null;
82
+ }
83
+ const projectRequire = createRequire(projectPackageJson);
66
84
  const resolved = projectRequire.resolve(PACKAGE_NAME);
67
85
  const entrypoint = realpathSync.native?.(resolved) ?? realpathSync(resolved);
68
86
  return packageInfoFromEntrypoint(entrypoint);
@@ -76,6 +94,7 @@ export function resolveProjectLocalMcp(cwd) {
76
94
  }
77
95
  }
78
96
  export function compareMcpResolution(running, projectLocal) {
97
+ // implements REQ-cursor-agent-plugin-standard-v1
79
98
  const diagnosticText = JSON.stringify({ running, projectLocal });
80
99
  const forbiddenVersionObserved = diagnosticText.includes(FORBIDDEN_VERSION);
81
100
  if (!projectLocal) {
@@ -92,16 +111,14 @@ export function compareMcpResolution(running, projectLocal) {
92
111
  forbiddenVersionObserved,
93
112
  };
94
113
  }
95
- if (path.resolve(running.packageRoot) !== path.resolve(projectLocal.packageRoot)) {
96
- return {
97
- stale: true,
98
- reason: `package root mismatch: running ${running.packageRoot}, project-local ${projectLocal.packageRoot}`,
99
- forbiddenVersionObserved,
100
- };
101
- }
114
+ // Same version: the running build is the version the project pins. Re-entering
115
+ // a same-version copy (e.g. a bun-store published install) abandons the local
116
+ // dev build for an identical-version store copy, which defeats local
117
+ // dogfooding and can silently drop unreleased local fixes. Only re-enter on a
118
+ // genuine version mismatch.
102
119
  return {
103
120
  stale: false,
104
- reason: "running kibi-mcp matches project-local kibi-mcp",
121
+ reason: "running kibi-mcp matches the project-local version",
105
122
  forbiddenVersionObserved,
106
123
  };
107
124
  }
@@ -30,6 +30,7 @@ export async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr) {
30
30
  description: v.description,
31
31
  ...(v.suggestion ? { suggestion: v.suggestion } : {}),
32
32
  ...(v.source ? { source: v.source } : {}),
33
+ ...(v.evidence ? { evidence: v.evidence } : {}),
33
34
  });
34
35
  }
35
36
  }
@@ -1,14 +1,6 @@
1
- import { executeCoverage } from "kibi-cli/operations";
1
+ import { executeCoverage, } from "kibi-cli/operations";
2
+ import { createDiscoveryContext } from "./discovery-adapter.js";
2
3
  // implements REQ-002, REQ-013
3
- export async function handleKbCoverage(prolog, args) {
4
- return executeCoverage({ ...args }, {
5
- workspaceRoot: process.cwd(),
6
- signal: new AbortController().signal,
7
- clock: () => new Date(),
8
- prolog: {
9
- query: (goal) => prolog.query(goal),
10
- nextSolution: async () => null,
11
- save: () => prolog.query("kb_save"),
12
- },
13
- });
4
+ export async function handleKbCoverage(prolog, args, context) {
5
+ return executeCoverage({ ...args }, createDiscoveryContext(prolog, context));
14
6
  }
@@ -1,5 +1,5 @@
1
1
  import { resolveWorkspaceRoot } from "../workspace.js";
2
- export function createDiscoveryContext(prolog) {
2
+ export function createDiscoveryContext(prolog, baseContext) {
3
3
  // implements REQ-kibi-operation-interface-parity
4
4
  let lastResult = null;
5
5
  const port = {
@@ -15,9 +15,10 @@ export function createDiscoveryContext(prolog) {
15
15
  save: () => prolog.query("kb_save"),
16
16
  };
17
17
  return {
18
- workspaceRoot: resolveWorkspaceRoot(),
19
- signal: new AbortController().signal,
20
- clock: () => new Date(),
18
+ ...(baseContext ?? {}),
19
+ workspaceRoot: baseContext?.workspaceRoot ?? resolveWorkspaceRoot(),
20
+ signal: baseContext?.signal ?? new AbortController().signal,
21
+ clock: baseContext?.clock ?? (() => new Date()),
21
22
  prolog: port,
22
23
  };
23
24
  }
@@ -1,6 +1,10 @@
1
1
  import { statusSpec } from "kibi-cli/operations";
2
2
  import { createDiscoveryContext } from "./discovery-adapter.js";
3
- export async function handleKbStatus(prolog, args) {
3
+ export async function handleKbStatus(prolog, args, context) {
4
4
  // implements REQ-kibi-operation-interface-parity
5
- return statusSpec.execute(args, createDiscoveryContext(prolog));
5
+ // Freshness is read-after-write sensitive: invalidate the PrologProcess query
6
+ // cache so kb_status always reflects the current workspace state rather than
7
+ // a stale earlier-in-session result.
8
+ prolog.invalidateCache();
9
+ return statusSpec.execute(args, createDiscoveryContext(prolog, context));
6
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.19.2",
3
+ "version": "0.21.1",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.26.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-cli": "^0.16.0",
13
- "kibi-core": "^0.7.1",
12
+ "kibi-cli": "^0.18.1",
13
+ "kibi-core": "^0.9.1",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"