kibi-mcp 0.17.5 → 0.19.2

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.
Files changed (43) hide show
  1. package/bin/kibi-mcp +1 -1
  2. package/dist/diagnostics-helpers.js +22 -0
  3. package/dist/diagnostics.js +113 -4
  4. package/dist/runtime/mcp-runtime.js +43 -0
  5. package/dist/server/docs.js +11 -10
  6. package/dist/server/json-schema-to-zod.js +73 -2
  7. package/dist/server/session.js +33 -15
  8. package/dist/server/tool-registration.js +35 -59
  9. package/dist/server/tools-runtime.js +52 -2
  10. package/dist/server/tools.js +19 -5
  11. package/dist/server/transport.js +9 -5
  12. package/dist/tools/autopilot-generate.js +37 -646
  13. package/dist/tools/autopilot-test-compat.js +88 -0
  14. package/dist/tools/autopilot-test-parsers.js +120 -0
  15. package/dist/tools/check-config.js +67 -0
  16. package/dist/tools/check-format.js +92 -0
  17. package/dist/tools/check-impact.js +45 -0
  18. package/dist/tools/check-prolog.js +51 -0
  19. package/dist/tools/check.js +13 -190
  20. package/dist/tools/coverage.js +11 -25
  21. package/dist/tools/delete.js +5 -112
  22. package/dist/tools/discovery-adapter.js +23 -0
  23. package/dist/tools/entity-query.js +1 -111
  24. package/dist/tools/find-gaps.js +11 -26
  25. package/dist/tools/graph.js +11 -24
  26. package/dist/tools/model-requirement.js +8 -347
  27. package/dist/tools/mutation-context.js +28 -0
  28. package/dist/tools/query-plan-safety.js +58 -0
  29. package/dist/tools/query.js +5 -50
  30. package/dist/tools/relationship-validation.js +11 -0
  31. package/dist/tools/search.js +4 -37
  32. package/dist/tools/semantic-advisor.js +2 -40
  33. package/dist/tools/skills.js +25 -47
  34. package/dist/tools/sparql.js +12 -93
  35. package/dist/tools/status.js +5 -19
  36. package/dist/tools/suggest-predicates.js +11 -2448
  37. package/dist/tools/upsert.js +21 -794
  38. package/dist/tools/validate-upsert.js +16 -40
  39. package/dist/tools-config.js +75 -764
  40. package/package.json +4 -3
  41. package/dist/semantic-advisor/analyze-prose.js +0 -1367
  42. package/dist/semantic-advisor/prose-coverage-evaluator.js +0 -66
  43. /package/dist/{semantic-advisor/types.js → tools/check-types.js} +0 -0
package/bin/kibi-mcp CHANGED
@@ -98,7 +98,7 @@ if (args.includes("-h") || args.includes("--help")) {
98
98
 
99
99
  if (args.includes("--diagnostic-mode")) {
100
100
  globalThis.__KIBI_MCP_DIAGNOSTIC_MODE = true;
101
- process.env.KIBI_WORKSPACE = process.cwd();
101
+ process.env.KIBI_WORKSPACE ??= process.cwd();
102
102
  process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
103
103
  }
104
104
 
@@ -70,12 +70,32 @@ export function classifyDiagnosticError(error) {
70
70
  return buildErrorFields(err, "relationship_validation_failed", "validation", "Relationship payload failed schema validation.");
71
71
  if (err.message.startsWith("Relationship source must match the upserted entity"))
72
72
  return buildErrorFields(err, "relationship_source_mismatch", "validation", "Relationship source did not match the entity being upserted.");
73
+ if (lower.includes("contradiction detected for requirement")) {
74
+ const fields = buildErrorFields(err, "semantic_contradiction", "validation", "Requirement prose/facts contradict existing contradiction-ready requirements.");
75
+ return {
76
+ ...fields,
77
+ semantic_outcome: "conflict-blocked",
78
+ ...semanticContradictionIds(err.message),
79
+ };
80
+ }
73
81
  if (lower.includes("module load failed"))
74
82
  return buildErrorFields(err, "prolog_module_load_failed", "prolog_runtime", "Prolog failed to load an execution module.");
75
83
  if (lower.includes("query failed"))
76
84
  return buildErrorFields(err, "prolog_query_failed", "prolog_runtime", "Prolog query execution failed.");
77
85
  return buildErrorFields(err, "handler_error", "handler", "Unhandled MCP handler error.");
78
86
  }
87
+ function semanticContradictionIds(message) {
88
+ const checked = message.match(/Contradiction detected for requirement\s+([^:\s]+)/i);
89
+ const conflicts = [
90
+ ...message.matchAll(/Conflicts with\s+([^:\s]+)/gi),
91
+ ].flatMap((match) => (match[1] ? [match[1]] : []));
92
+ return {
93
+ ...(checked?.[1] ? { semantic_checked_req_id: checked[1] } : {}),
94
+ ...(conflicts.length > 0
95
+ ? { semantic_conflicting_req_ids: conflicts }
96
+ : {}),
97
+ };
98
+ }
79
99
  export function deriveDiagnosticHints(input) {
80
100
  const error = input.error instanceof Error ? input.error : new Error(String(input.error));
81
101
  const message = error.message.toLowerCase();
@@ -89,6 +109,8 @@ export function deriveDiagnosticHints(input) {
89
109
  return `tool_timeout ${mappedStage}: retry the same request after reducing payload size or scope.`;
90
110
  if (error_category === "relationship_source_mismatch")
91
111
  return `relationship_source_mismatch ${mappedStage}: align the relationship source with the upserted entity id.`;
112
+ if (error_category === "semantic_contradiction")
113
+ return `semantic_contradiction ${mappedStage}: add a supersedes relationship to the conflicting requirement, deprecate the older requirement, or align the modeled facts.`;
92
114
  if (error_category === "prolog_process_not_started")
93
115
  return `prolog_process_not_started ${mappedStage}: restart the MCP server before retrying the tool call.`;
94
116
  if (error_category === "prolog_query_failed")
@@ -58,6 +58,88 @@ export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
58
58
  },
59
59
  },
60
60
  };
61
+ function isRecord(value) {
62
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
63
+ }
64
+ function structuredContentFrom(result) {
65
+ if (!isRecord(result))
66
+ return undefined;
67
+ const structuredContent = result.structuredContent;
68
+ return isRecord(structuredContent) ? structuredContent : undefined;
69
+ }
70
+ function stringArray(value) {
71
+ if (!Array.isArray(value))
72
+ return [];
73
+ return value.filter((item) => typeof item === "string");
74
+ }
75
+ function suggestionKindsFrom(value) {
76
+ if (!Array.isArray(value))
77
+ return [];
78
+ return value.flatMap((item) => {
79
+ if (!isRecord(item) || typeof item.kind !== "string")
80
+ return [];
81
+ return [item.kind];
82
+ });
83
+ }
84
+ function appendSemanticAdvisorFields(fields, receipt) {
85
+ if (!isRecord(receipt))
86
+ return;
87
+ const readiness = receipt.logic_readiness;
88
+ const lane = receipt.candidate_lane;
89
+ const suggestionKinds = suggestionKindsFrom(receipt.suggestions);
90
+ if (typeof readiness === "string") {
91
+ fields.semantic_logic_readiness = readiness;
92
+ }
93
+ if (typeof lane === "string") {
94
+ fields.semantic_candidate_lane = lane;
95
+ }
96
+ fields.semantic_suggestion_kinds = suggestionKinds;
97
+ fields.semantic_suggestion_count = suggestionKinds.length;
98
+ fields.semantic_next_tools = stringArray(receipt.suggested_next_tools);
99
+ }
100
+ function appendPredicateSuggestionFields(fields, structuredContent) {
101
+ const candidates = Array.isArray(structuredContent.candidates)
102
+ ? structuredContent.candidates
103
+ : [];
104
+ const topCandidate = candidates.find(isRecord);
105
+ fields.predicate_candidate_count = candidates.length;
106
+ if (topCandidate) {
107
+ if (typeof topCandidate.predicate_name === "string") {
108
+ fields.predicate_top_name = topCandidate.predicate_name;
109
+ }
110
+ if (typeof topCandidate.score === "number") {
111
+ fields.predicate_top_score = topCandidate.score;
112
+ }
113
+ }
114
+ if (typeof structuredContent.recommendedAction === "string") {
115
+ fields.predicate_recommended_action = structuredContent.recommendedAction;
116
+ }
117
+ fields.predicate_relationship_plan = isRecord(structuredContent.relationshipPlan);
118
+ }
119
+ function appendContradictionCheckFields(fields, contradictionCheck) {
120
+ if (!isRecord(contradictionCheck))
121
+ return;
122
+ const outcome = contradictionCheck.outcome;
123
+ if (typeof outcome === "string") {
124
+ fields.semantic_contradiction_outcome = outcome;
125
+ }
126
+ const checkedReqId = contradictionCheck.checked_req_id;
127
+ if (typeof checkedReqId === "string") {
128
+ fields.semantic_checked_req_id = checkedReqId;
129
+ }
130
+ const strictReadiness = contradictionCheck.strict_readiness;
131
+ if (typeof strictReadiness === "string") {
132
+ fields.semantic_strict_readiness = strictReadiness;
133
+ }
134
+ const subjectKey = contradictionCheck.subject_key;
135
+ if (typeof subjectKey === "string") {
136
+ fields.semantic_subject_key = subjectKey;
137
+ }
138
+ const propertyKey = contradictionCheck.property_key;
139
+ if (typeof propertyKey === "string") {
140
+ fields.semantic_property_key = propertyKey;
141
+ }
142
+ }
61
143
  export function deriveDiagnosticFields(toolName, args, telemetry, result) {
62
144
  const fields = {
63
145
  telemetry_status: telemetry ? "provided" : "missing",
@@ -67,10 +149,7 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
67
149
  fields.telemetry_confidence_score = telemetry.confidence_score ?? null;
68
150
  fields.telemetry_attempt_number = telemetry.attempt_number ?? null;
69
151
  }
70
- const structuredContent = result && typeof result === "object" && "structuredContent" in result
71
- ? result
72
- .structuredContent
73
- : undefined;
152
+ const structuredContent = structuredContentFrom(result);
74
153
  if (toolName === "kb_query" || toolName === "kb_search") {
75
154
  const resultCount = Number(structuredContent?.count ?? 0);
76
155
  fields.result_count = resultCount;
@@ -85,6 +164,36 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
85
164
  fields.result_summary =
86
165
  violationCount === 0 ? "0 violations" : `${violationCount} violations`;
87
166
  }
167
+ if (toolName === "kb_semantic_advisor" && structuredContent) {
168
+ appendSemanticAdvisorFields(fields, structuredContent.receipt);
169
+ const readiness = fields.semantic_logic_readiness;
170
+ const lane = fields.semantic_candidate_lane;
171
+ if (typeof readiness === "string" && typeof lane === "string") {
172
+ fields.result_summary = `semantic advisor ${readiness} via ${lane}`;
173
+ }
174
+ }
175
+ if (toolName === "kb_suggest_predicates" && structuredContent) {
176
+ appendPredicateSuggestionFields(fields, structuredContent);
177
+ const count = Number(fields.predicate_candidate_count ?? 0);
178
+ const topName = fields.predicate_top_name;
179
+ fields.result_summary =
180
+ typeof topName === "string"
181
+ ? `${count} predicate candidates; top=${topName}`
182
+ : `${count} predicate candidates`;
183
+ }
184
+ if (toolName === "kb_upsert" && structuredContent) {
185
+ const created = Number(structuredContent.created ?? 0);
186
+ const updated = Number(structuredContent.updated ?? 0);
187
+ fields.upsert_created = created;
188
+ fields.upsert_updated = updated;
189
+ fields.upsert_relationships_created = Number(structuredContent.relationships_created ?? 0);
190
+ appendContradictionCheckFields(fields, structuredContent.contradictionCheck);
191
+ appendSemanticAdvisorFields(fields, structuredContent.semanticAdvisor);
192
+ const readiness = fields.semantic_logic_readiness;
193
+ if (typeof readiness === "string") {
194
+ fields.result_summary = `upsert ${created > 0 ? "created" : "updated"}; semantic ${readiness}`;
195
+ }
196
+ }
88
197
  if (!fields.result_summary) {
89
198
  fields.result_summary = `${toolName} completed`;
90
199
  }
@@ -0,0 +1,43 @@
1
+ // implements REQ-kibi-operation-interface-parity
2
+ export function createMcpRuntime(session) {
3
+ const sessionPrologs = new WeakMap();
4
+ return {
5
+ open: async (spec, options = {}) => {
6
+ const fs = options.fs ?? session.fs;
7
+ const git = options.git ?? session.git;
8
+ const net = options.net ?? session.net;
9
+ const context = {
10
+ workspaceRoot: options.workspaceRoot ?? session.workspaceRoot,
11
+ signal: options.signal ?? session.signal ?? new AbortController().signal,
12
+ clock: options.clock ?? session.clock ?? (() => new Date()),
13
+ ...(fs ? { fs } : {}),
14
+ ...(git ? { git } : {}),
15
+ ...(net ? { net } : {}),
16
+ };
17
+ if (!spec.requiresProlog) {
18
+ return context;
19
+ }
20
+ if (options.prolog) {
21
+ return { ...context, prolog: options.prolog };
22
+ }
23
+ const sessionProlog = await session.ensureProlog();
24
+ const operationContext = {
25
+ ...context,
26
+ prolog: session.adaptProlog(sessionProlog),
27
+ };
28
+ sessionPrologs.set(operationContext, sessionProlog);
29
+ return operationContext;
30
+ },
31
+ afterSuccess: async (spec) => {
32
+ if (spec.effects.includes("kb-write") &&
33
+ (await session.attachedBranchKbPath()) !== null) {
34
+ await session.refreshAttachedBranchStamp();
35
+ }
36
+ },
37
+ close: async (context, outcome) => {
38
+ await session.requestCleanup?.(context, outcome);
39
+ sessionPrologs.delete(context);
40
+ },
41
+ sessionProlog: (context) => sessionPrologs.get(context),
42
+ };
43
+ }
@@ -16,9 +16,6 @@
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
18
  import { TOOLS } from "../tools-config.js";
19
- // INTENTIONAL: TOOLS is imported as a Zod-inferred schema type; ToolConfig is the runtime
20
- // interface with looser Record<string, unknown> inputSchema. The cast is safe because the
21
- // tool definitions are statically authored and validated at startup.
22
19
  const ACTIVE_TOOLS = TOOLS;
23
20
  function renderToolsDoc() {
24
21
  const lines = [
@@ -87,7 +84,10 @@ export const PROMPTS = [
87
84
  "## Rules",
88
85
  "- Never apply bootstrap writes without user-facing preview and explicit approval.",
89
86
  "- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
90
- "- Guidance must stay MCP-only; do not suggest `kibi` CLI commands.",
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|->`.",
89
+ "- 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
+ "- Do not read or edit `.kb/` files directly. Query before mutate. Run `kb_upsert` sequentially. Run `kb_check` before completion.",
91
91
  ].join("\n"),
92
92
  },
93
93
  {
@@ -107,7 +107,7 @@ export const PROMPTS = [
107
107
  "- `kb_graph`: Bounded graph traversal from seed IDs",
108
108
  "- `kb_upsert`: Create or update entities and their relationships",
109
109
  "- `kb_delete`: Remove entities by ID (with dependency safety checks)",
110
- "- `kb_check`: Validate KB integrity against configurable rules",
110
+ "- `kb_check`: Validate KB integrity against configurable rules; omit `rules` for final full validation plus full-KB `qualityDiagnostics[]` audit review",
111
111
  "",
112
112
  "Core modeling principles:",
113
113
  "- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
@@ -118,7 +118,7 @@ export const PROMPTS = [
118
118
  "- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
119
119
  "- Use `kb_search` first for discovery, then `kb_query` for exact follow-up before any mutation.",
120
120
  "- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
121
- "- Run `kb_check` after meaningful mutations to catch integrity issues early.",
121
+ "- Run `kb_check` with explicit `rules` during iteration for scoped feedback; run an unfiltered `kb_check` before completion to include the full-KB `qualityDiagnostics[]` audit scan.",
122
122
  "- Prefer explicit IDs and enum values to avoid invalid parameters.",
123
123
  "- Model requirements by first creating/reusing fact entities (create-before-link).",
124
124
  "- flag gates runtime/config behavior; use `fact` with `fact_kind: observation` or `meta` for bug and workaround notes.",
@@ -138,7 +138,8 @@ export const PROMPTS = [
138
138
  "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
139
  "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
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.",
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.",
142
143
  "",
143
144
  "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
144
145
  ].join("\n"),
@@ -203,7 +204,7 @@ function registerDocResources() {
203
204
  "2. `kb_upsert` for the fact entity first (create-before-link)",
204
205
  "3. `kb_upsert` for the req entity and include `relationships` with `constrains` and `requires_property`",
205
206
  "4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
206
- '5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
207
+ '5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation; supplying `rules` skips the full-KB advisory scan',
207
208
  "",
208
209
  "## Model requirements as ontology predicates",
209
210
  '1. Spell out the requirement prose and call `kb_suggest_predicates` with `{ "text": "...", "requirementId": "REQ-..." }`',
@@ -222,9 +223,9 @@ function registerDocResources() {
222
223
  '1. `kb_query` with `{ "type": "test" }` to check for existing test IDs',
223
224
  '2. `kb_query` with `{ "id": "REQ-XXX" }` to verify the requirement exists',
224
225
  "3. `kb_upsert` with entity payload and `relationships` containing `verified_by`",
225
- '4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
226
+ '4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation; use an unfiltered `kb_check` before completion for full-KB `qualityDiagnostics[]`',
226
227
  "",
227
- "Note: Always use query-first pattern. Specify only needed rules in kb_check for faster validation.",
228
+ "Note: Always use query-first pattern. Specify only needed rules in kb_check for faster iteration; omit rules for the final full validation and full-KB quality diagnostics review.",
228
229
  ].join("\n");
229
230
  return [
230
231
  {
@@ -1,4 +1,40 @@
1
1
  import { z } from "zod";
2
+ function hasRequiredProperties(value, schema) {
3
+ if (schema === null || typeof schema !== "object")
4
+ return false;
5
+ const condition = schema;
6
+ const required = Array.isArray(condition.required)
7
+ ? condition.required.filter((key) => typeof key === "string" && key.length > 0)
8
+ : [];
9
+ if (required.length > 0 &&
10
+ !required.every((key) => Object.hasOwn(value, key))) {
11
+ return false;
12
+ }
13
+ if (Array.isArray(condition.anyOf) &&
14
+ !condition.anyOf.some((entry) => hasRequiredProperties(value, entry))) {
15
+ return false;
16
+ }
17
+ if (Array.isArray(condition.allOf) &&
18
+ !condition.allOf.every((entry) => hasRequiredProperties(value, entry))) {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ function conditionalRequiredKeys(value, condition) {
24
+ if (condition === null || typeof condition !== "object")
25
+ return [];
26
+ const rule = condition;
27
+ if (!("if" in rule) || !("then" in rule))
28
+ return [];
29
+ if (!hasRequiredProperties(value, rule.if))
30
+ return [];
31
+ if (rule.then === null || typeof rule.then !== "object")
32
+ return [];
33
+ const required = rule.then.required;
34
+ return Array.isArray(required)
35
+ ? required.filter((key) => typeof key === "string" && key.length > 0)
36
+ : [];
37
+ }
2
38
  // implements REQ-002
3
39
  export function jsonSchemaToZod(schema) {
4
40
  if (!schema || typeof schema !== "object") {
@@ -43,7 +79,29 @@ export function jsonSchemaToZod(schema) {
43
79
  ? z.object(shape)
44
80
  : z.looseObject(shape);
45
81
  const description = typeof obj.description === "string" ? obj.description : undefined;
46
- return description ? objectSchema.describe(description) : objectSchema;
82
+ let result = description
83
+ ? objectSchema.describe(description)
84
+ : objectSchema;
85
+ if (Array.isArray(obj.allOf)) {
86
+ result = result
87
+ .superRefine((value, context) => {
88
+ if (value === null || typeof value !== "object")
89
+ return;
90
+ for (const condition of obj.allOf) {
91
+ for (const key of conditionalRequiredKeys(value, condition)) {
92
+ if (!Object.hasOwn(value, key)) {
93
+ context.addIssue({
94
+ code: "custom",
95
+ path: [key],
96
+ message: `Required by conditional JSON Schema rule: ${key}`,
97
+ });
98
+ }
99
+ }
100
+ }
101
+ })
102
+ .meta({ allOf: obj.allOf });
103
+ }
104
+ return result;
47
105
  }
48
106
  case "array": {
49
107
  const itemSchema = jsonSchemaToZod(obj.items);
@@ -55,7 +113,17 @@ export function jsonSchemaToZod(schema) {
55
113
  if (typeof obj.maxItems === "number") {
56
114
  arraySchema = arraySchema.max(obj.maxItems);
57
115
  }
58
- return description ? arraySchema.describe(description) : arraySchema;
116
+ let result = description
117
+ ? arraySchema.describe(description)
118
+ : arraySchema;
119
+ if (obj.uniqueItems === true) {
120
+ result = result
121
+ .refine((values) => Array.isArray(values) &&
122
+ new Set(values.map((value) => JSON.stringify(value))).size ===
123
+ values.length, { message: "Array items must be unique" })
124
+ .meta({ uniqueItems: true });
125
+ }
126
+ return result;
59
127
  }
60
128
  case "string": {
61
129
  let s = z.string();
@@ -66,6 +134,9 @@ export function jsonSchemaToZod(schema) {
66
134
  if (typeof obj.maxLength === "number") {
67
135
  s = s.max(obj.maxLength);
68
136
  }
137
+ if (typeof obj.pattern === "string") {
138
+ s = s.regex(new RegExp(obj.pattern));
139
+ }
69
140
  return description ? s.describe(description) : s;
70
141
  }
71
142
  case "number": {
@@ -42,6 +42,8 @@ let ensurePrologTail = Promise.resolve();
42
42
  let prologResetGeneration = 0;
43
43
  export let attachedBranchKbPath = null;
44
44
  let attachedBranchStamp = null;
45
+ // implements REQ-mcp-kb-freshness
46
+ // covered_by TEST-mcp-kb-freshness
45
47
  export function updateAttachedBranchStamp(stamp) {
46
48
  attachedBranchStamp = stamp;
47
49
  }
@@ -85,7 +87,7 @@ export function ensureBranchKbExists(workspaceRoot, branch) {
85
87
  }
86
88
  const branchPath = sessionDeps.resolveKbPath(workspaceRoot, branch);
87
89
  if (sessionDeps.fs.existsSync(branchPath)) {
88
- return;
90
+ return false;
89
91
  }
90
92
  // Try to copy from the previously active branch if available
91
93
  const previousBranch = activeBranchName;
@@ -96,11 +98,12 @@ export function ensureBranchKbExists(workspaceRoot, branch) {
96
98
  // Copy from previous branch for continuity
97
99
  sessionDeps.copyCleanSnapshot(previousBranchPath, branchPath);
98
100
  debugLog(`[KIBI-MCP] Created branch KB for '${branch}' from '${previousBranch}'`);
99
- return;
101
+ return false;
100
102
  }
101
103
  // No previous branch available - create empty KB
102
104
  sessionDeps.fs.mkdirSync(branchPath, { recursive: true });
103
105
  debugLog(`[KIBI-MCP] Created empty branch KB for '${branch}'`);
106
+ return true;
104
107
  }
105
108
  export async function initiateGracefulShutdown(exitCode = 0) {
106
109
  if (isShuttingDown) {
@@ -108,6 +111,18 @@ export async function initiateGracefulShutdown(exitCode = 0) {
108
111
  }
109
112
  isShuttingDown = true;
110
113
  debugLog(`[KIBI-MCP] Initiating graceful shutdown (exit code: ${exitCode})`);
114
+ const currentProlog = prologProcess;
115
+ prologProcess = null;
116
+ if (currentProlog?.isRunning()) {
117
+ debugLog("[KIBI-MCP] Cancelling active Prolog work...");
118
+ try {
119
+ await currentProlog.terminate();
120
+ debugLog("[KIBI-MCP] Prolog process terminated");
121
+ }
122
+ catch (error) {
123
+ console.error("[KIBI-MCP] Error terminating Prolog:", error);
124
+ }
125
+ }
111
126
  // Wait for in-flight requests
112
127
  if (inFlightRequests.size > 0) {
113
128
  debugLog(`[KIBI-MCP] Waiting for ${inFlightRequests.size} in-flight requests to complete...`);
@@ -133,17 +148,6 @@ export async function initiateGracefulShutdown(exitCode = 0) {
133
148
  }
134
149
  }
135
150
  }
136
- // Cleanup Prolog process
137
- if (prologProcess?.isRunning()) {
138
- debugLog("[KIBI-MCP] Terminating Prolog process...");
139
- try {
140
- await prologProcess.terminate();
141
- debugLog("[KIBI-MCP] Prolog process terminated");
142
- }
143
- catch (error) {
144
- console.error("[KIBI-MCP] Error terminating Prolog:", error);
145
- }
146
- }
147
151
  // Exit
148
152
  process.exit(exitCode);
149
153
  }
@@ -271,13 +275,20 @@ async function ensurePrologUnsafe() {
271
275
  // Continue anyway - we'll try to attach to the new KB
272
276
  }
273
277
  // Ensure new branch KB exists
274
- ensureBranchKbExists(workspaceRoot, targetBranch);
278
+ const createdEmptyBranch = ensureBranchKbExists(workspaceRoot, targetBranch);
275
279
  // Attach to new branch KB
276
280
  const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
277
281
  await assertGeneration();
278
282
  if (!attachResult.success) {
279
283
  throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
280
284
  }
285
+ if (createdEmptyBranch) {
286
+ const initialSaveResult = await prologProcess.query("kb_save");
287
+ await assertGeneration();
288
+ if (!initialSaveResult.success) {
289
+ throw new Error(`Failed to initialize empty branch KB: ${initialSaveResult.error || "Unknown error"}`);
290
+ }
291
+ }
281
292
  activeBranchName = targetBranch;
282
293
  attachedBranchKbPath = kbPath;
283
294
  attachedBranchStamp = await readBranchKbStamp(kbPath);
@@ -327,13 +338,20 @@ async function ensurePrologUnsafe() {
327
338
  debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${envBranch || "not set"}`);
328
339
  debugLog(`[KIBI-MCP] Resolved branch: ${targetBranch}`);
329
340
  activeBranchName = targetBranch;
330
- ensureBranchKbExists(workspaceRoot, targetBranch);
341
+ const createdEmptyBranch = ensureBranchKbExists(workspaceRoot, targetBranch);
331
342
  const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
332
343
  const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
333
344
  await assertGeneration();
334
345
  if (!attachResult.success) {
335
346
  throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
336
347
  }
348
+ if (createdEmptyBranch) {
349
+ const initialSaveResult = await prologProcess.query("kb_save");
350
+ await assertGeneration();
351
+ if (!initialSaveResult.success) {
352
+ throw new Error(`Failed to initialize empty branch KB: ${initialSaveResult.error || "Unknown error"}`);
353
+ }
354
+ }
337
355
  attachedBranchKbPath = kbPath;
338
356
  attachedBranchStamp = await readBranchKbStamp(kbPath);
339
357
  isInitialized = true;