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.
- package/dist/diagnostics.js +8 -8
- package/dist/env.js +21 -30
- package/dist/runtime/mcp-runtime.js +25 -3
- package/dist/server/jobs.js +107 -0
- package/dist/server/json-schema-to-zod.js +31 -10
- package/dist/server/session.js +104 -82
- package/dist/server/tool-registration.js +22 -9
- package/dist/server/tools-runtime.js +28 -21
- package/dist/server/tools.js +128 -28
- package/dist/startup-resolution.js +15 -17
- package/dist/tools/check-impact.js +1 -1
- package/dist/tools/core-module.js +8 -5
- package/dist/tools/discovery-adapter.js +30 -7
- package/dist/tools/model-requirement.js +3 -3
- package/dist/tools/semantic-advisor.js +2 -2
- package/dist/tools/suggest-predicates.js +10 -9
- package/dist/tools-config.js +3 -3
- package/dist/workspace.js +10 -35
- package/package.json +6 -9
package/dist/diagnostics.js
CHANGED
|
@@ -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
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
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?.
|
|
218
|
-
fields.
|
|
217
|
+
if (typeof metadata?.proofSnapshot === "string") {
|
|
218
|
+
fields.coverage_proof_snapshot = metadata.proofSnapshot;
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
function appendPredicateSuggestionFields(fields, structuredContent) {
|
package/dist/env.js
CHANGED
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import fs from "node:fs";
|
|
19
|
-
import { resolveEnvFilePath,
|
|
20
|
-
const DEFAULT_ENV_FILE = ".env";
|
|
19
|
+
import { KIBI_PROJECT_ENV_FILE, bootstrapKibiEnvironment, inspectSecretSource, parseEnvContent, resolveEnvFilePath, resolveKibiProjectEnvPath, resolveKibiWorkspaceRoot, } from "kibi-runtime";
|
|
21
20
|
function getEnvValue(key) {
|
|
22
21
|
const value = process.env[key];
|
|
23
22
|
return typeof value === "string" ? value : undefined;
|
|
@@ -26,9 +25,10 @@ function getTrimmedEnvValue(key) {
|
|
|
26
25
|
const value = getEnvValue(key)?.trim();
|
|
27
26
|
return value ? value : undefined;
|
|
28
27
|
}
|
|
28
|
+
/** Preferred project env file name (`.env.kibi` or `KIBI_ENV_FILE` override). */
|
|
29
29
|
export function getEnvFileName() {
|
|
30
30
|
// implements REQ-002
|
|
31
|
-
return getTrimmedEnvValue("KIBI_ENV_FILE") ??
|
|
31
|
+
return getTrimmedEnvValue("KIBI_ENV_FILE") ?? KIBI_PROJECT_ENV_FILE;
|
|
32
32
|
}
|
|
33
33
|
export function isMcpDebugEnabled() {
|
|
34
34
|
// implements REQ-002
|
|
@@ -51,12 +51,26 @@ export function getCoreModulePathOverride(fileName) {
|
|
|
51
51
|
const envKey = `KIBI_${fileName.replace(/\W/g, "_").toUpperCase()}_PATH`;
|
|
52
52
|
return getEnvValue(envKey);
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Harness-independent bootstrap: process → project `.env.kibi`/`KIBI_ENV_FILE`
|
|
56
|
+
* → user `~/.config/kibi/env`, plus legacy `.env` gap-fill.
|
|
57
|
+
* Prefer calling from `startServer()` only.
|
|
58
|
+
*/
|
|
54
59
|
export function loadDefaultEnvFile() {
|
|
55
60
|
// implements REQ-002
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
61
|
+
const result = bootstrapKibiEnvironment();
|
|
62
|
+
const keysLoaded = [
|
|
63
|
+
...result.keysLoadedFromUser,
|
|
64
|
+
...result.keysLoadedFromProject,
|
|
65
|
+
...result.keysLoadedFromLegacy,
|
|
66
|
+
];
|
|
67
|
+
return {
|
|
68
|
+
loaded: keysLoaded.length > 0,
|
|
69
|
+
envFilePath: result.projectEnvPath,
|
|
70
|
+
keysLoaded,
|
|
71
|
+
};
|
|
59
72
|
}
|
|
73
|
+
/** @deprecated Prefer bootstrapKibiEnvironment; kept for targeted file tests. */
|
|
60
74
|
export function loadEnvFile(options) {
|
|
61
75
|
const { envFileName, workspaceRoot } = options;
|
|
62
76
|
const envFilePath = resolveEnvFilePath(envFileName, workspaceRoot);
|
|
@@ -80,27 +94,4 @@ export function loadEnvFile(options) {
|
|
|
80
94
|
return { loaded: false, envFilePath, keysLoaded };
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
|
-
|
|
84
|
-
const lines = content.split(/\r?\n/);
|
|
85
|
-
const entries = [];
|
|
86
|
-
for (const rawLine of lines) {
|
|
87
|
-
const line = rawLine.trim();
|
|
88
|
-
if (!line || line.startsWith("#")) {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
const eqIndex = line.indexOf("=");
|
|
92
|
-
if (eqIndex <= 0) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const key = line.substring(0, eqIndex).trim();
|
|
96
|
-
let value = line.substring(eqIndex + 1).trim();
|
|
97
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
98
|
-
value = value.slice(1, -1);
|
|
99
|
-
}
|
|
100
|
-
else if (value.startsWith("'") && value.endsWith("'")) {
|
|
101
|
-
value = value.slice(1, -1);
|
|
102
|
-
}
|
|
103
|
-
entries.push({ key, value });
|
|
104
|
-
}
|
|
105
|
-
return entries;
|
|
106
|
-
}
|
|
97
|
+
export { resolveEnvFilePath, resolveKibiProjectEnvPath, resolveKibiWorkspaceRoot, inspectSecretSource, };
|
|
@@ -1,16 +1,38 @@
|
|
|
1
|
-
import { resolveBranchAttachment } from "kibi-runtime";
|
|
1
|
+
import { CapabilityRegistryCache, ensureCapabilityRegistry, resolveBranchAttachment, } from "kibi-runtime";
|
|
2
|
+
export function attachedContextWithProlog(withAttachment, prolog) {
|
|
3
|
+
return { ...withAttachment, prolog };
|
|
4
|
+
}
|
|
5
|
+
function resolvePluginsOption(plugins) {
|
|
6
|
+
if (plugins === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (typeof plugins === "function")
|
|
9
|
+
return plugins;
|
|
10
|
+
return async () => plugins;
|
|
11
|
+
}
|
|
2
12
|
// implements REQ-kibi-operation-interface-parity
|
|
3
13
|
export function createMcpRuntime(session) {
|
|
4
14
|
const sessionPrologs = new WeakMap();
|
|
15
|
+
// Per-runtime cache — not a process-global singleton.
|
|
16
|
+
const pluginCache = new CapabilityRegistryCache();
|
|
5
17
|
return {
|
|
6
18
|
open: async (spec, options = {}) => {
|
|
7
19
|
const fs = options.fs ?? session.fs;
|
|
8
20
|
const git = options.git ?? session.git;
|
|
9
21
|
const net = options.net ?? session.net;
|
|
22
|
+
const pluginsOption = options.plugins ?? session.plugins;
|
|
23
|
+
const injectedPlugins = resolvePluginsOption(pluginsOption);
|
|
24
|
+
const workspaceRoot = options.workspaceRoot ?? session.workspaceRoot;
|
|
25
|
+
const ensurePlugins = async () => {
|
|
26
|
+
if (injectedPlugins)
|
|
27
|
+
return injectedPlugins();
|
|
28
|
+
return ensureCapabilityRegistry(workspaceRoot, { cache: pluginCache });
|
|
29
|
+
};
|
|
10
30
|
const context = {
|
|
11
|
-
workspaceRoot
|
|
31
|
+
workspaceRoot,
|
|
12
32
|
signal: options.signal ?? session.signal ?? new AbortController().signal,
|
|
13
33
|
clock: options.clock ?? session.clock ?? (() => new Date()),
|
|
34
|
+
ensurePlugins,
|
|
35
|
+
...(pluginsOption !== undefined ? { plugins: pluginsOption } : {}),
|
|
14
36
|
...(fs ? { fs } : {}),
|
|
15
37
|
...(fs ? { sourceFirst: true } : {}),
|
|
16
38
|
...(git ? { git } : {}),
|
|
@@ -51,7 +73,7 @@ export function createMcpRuntime(session) {
|
|
|
51
73
|
throw new Error(`Legacy branch attachment requires explicit migration before '${spec.name}'; run kibi branch migrate --from ${attachment.kbBranch} --to ${attachment.gitBranch} --apply`);
|
|
52
74
|
}
|
|
53
75
|
if (options.prolog) {
|
|
54
|
-
return
|
|
76
|
+
return attachedContextWithProlog(withAttachment, options.prolog);
|
|
55
77
|
}
|
|
56
78
|
const sessionProlog = await session.ensureProlog();
|
|
57
79
|
const operationContext = {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import process from "node:process";
|
|
19
|
+
const MAX_FINISHED_JOBS = 32;
|
|
20
|
+
const jobs = new Map();
|
|
21
|
+
let jobCounter = 0;
|
|
22
|
+
function nextJobId(tool) {
|
|
23
|
+
jobCounter += 1;
|
|
24
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
25
|
+
return `job-${tool}-${jobCounter.toString(36)}-${random}`;
|
|
26
|
+
}
|
|
27
|
+
function evictOldestFinished() {
|
|
28
|
+
for (const [id, record] of jobs) {
|
|
29
|
+
if (jobs.size <= MAX_FINISHED_JOBS)
|
|
30
|
+
break;
|
|
31
|
+
if (record.status !== "running") {
|
|
32
|
+
jobs.delete(id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Start a background job for `tool`. The factory's promise runs detached;
|
|
38
|
+
* callers return the `kibi.job.v1` receipt immediately so the MCP request is
|
|
39
|
+
* never held open for the duration of the operation.
|
|
40
|
+
*/
|
|
41
|
+
// implements REQ-002
|
|
42
|
+
export function startJob(tool, factory) {
|
|
43
|
+
const jobId = nextJobId(tool);
|
|
44
|
+
const record = {
|
|
45
|
+
jobId,
|
|
46
|
+
tool,
|
|
47
|
+
startedAt: new Date().toISOString(),
|
|
48
|
+
status: "running",
|
|
49
|
+
};
|
|
50
|
+
jobs.set(jobId, record);
|
|
51
|
+
void (async () => {
|
|
52
|
+
try {
|
|
53
|
+
const result = await factory();
|
|
54
|
+
record.status = "succeeded";
|
|
55
|
+
record.finishedAt = new Date().toISOString();
|
|
56
|
+
record.result =
|
|
57
|
+
result !== null && typeof result === "object"
|
|
58
|
+
? result
|
|
59
|
+
: { value: result };
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
record.status = "failed";
|
|
63
|
+
record.finishedAt = new Date().toISOString();
|
|
64
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
evictOldestFinished();
|
|
68
|
+
}
|
|
69
|
+
})().catch((error) => {
|
|
70
|
+
// Defensive: the internal bookkeeping above must never crash the server
|
|
71
|
+
// process via an unhandled rejection.
|
|
72
|
+
process.stderr.write(`[kibi-mcp] job bookkeeping error for ${jobId}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
kibiProtocol: 1,
|
|
76
|
+
jobVersion: "kibi.job.v1",
|
|
77
|
+
jobId,
|
|
78
|
+
tool,
|
|
79
|
+
status: "running",
|
|
80
|
+
pollWith: "kb_job_status",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// implements REQ-002
|
|
84
|
+
export function getJob(jobId) {
|
|
85
|
+
return jobs.get(jobId);
|
|
86
|
+
}
|
|
87
|
+
/** Public JSON shape for kb_job_status responses. */
|
|
88
|
+
// implements REQ-002
|
|
89
|
+
export function jobSnapshot(job) {
|
|
90
|
+
return {
|
|
91
|
+
kibiProtocol: 1,
|
|
92
|
+
jobVersion: "kibi.job.v1",
|
|
93
|
+
jobId: job.jobId,
|
|
94
|
+
tool: job.tool,
|
|
95
|
+
status: job.status,
|
|
96
|
+
startedAt: job.startedAt,
|
|
97
|
+
...(job.finishedAt === undefined ? {} : { finishedAt: job.finishedAt }),
|
|
98
|
+
...(job.result === undefined ? {} : { result: job.result }),
|
|
99
|
+
...(job.error === undefined ? {} : { error: job.error }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Test seam: drop all job state. */
|
|
103
|
+
// implements REQ-002
|
|
104
|
+
export function resetJobsForTests() {
|
|
105
|
+
jobs.clear();
|
|
106
|
+
jobCounter = 0;
|
|
107
|
+
}
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
export function firstDefined(items) {
|
|
3
|
+
return items[0];
|
|
4
|
+
}
|
|
5
|
+
export function describedAnySchema(description) {
|
|
6
|
+
const anySchema = z.any();
|
|
7
|
+
return description ? anySchema.describe(description) : anySchema;
|
|
8
|
+
}
|
|
9
|
+
export function singleLiteralOrAny(literalSchemas, description) {
|
|
10
|
+
const single = firstDefined(literalSchemas);
|
|
11
|
+
if (!single) {
|
|
12
|
+
return describedAnySchema(description);
|
|
13
|
+
}
|
|
14
|
+
return description ? single.describe(description) : single;
|
|
15
|
+
}
|
|
2
16
|
function hasRequiredProperties(value, schema) {
|
|
3
17
|
if (schema === null || typeof schema !== "object")
|
|
4
18
|
return false;
|
|
@@ -64,11 +78,7 @@ export function jsonSchemaToZod(schema) {
|
|
|
64
78
|
}
|
|
65
79
|
const literalSchemas = literals.map((value) => z.literal(value));
|
|
66
80
|
if (literalSchemas.length === 1) {
|
|
67
|
-
|
|
68
|
-
if (!single) {
|
|
69
|
-
return description ? z.any().describe(description) : z.any();
|
|
70
|
-
}
|
|
71
|
-
return description ? single.describe(description) : single;
|
|
81
|
+
return singleLiteralOrAny(literalSchemas, description);
|
|
72
82
|
}
|
|
73
83
|
const union = z.union(literalSchemas);
|
|
74
84
|
return description ? union.describe(description) : union;
|
|
@@ -78,6 +88,20 @@ export function jsonSchemaToZod(schema) {
|
|
|
78
88
|
: typeof obj.type === "string"
|
|
79
89
|
? [obj.type]
|
|
80
90
|
: [];
|
|
91
|
+
// JSON Schema anyOf unions become real Zod unions so MCP output
|
|
92
|
+
// validation enforces the declared alternatives instead of silently
|
|
93
|
+
// degrading to z.any() — e.g. kb_check's synchronous payload versus its
|
|
94
|
+
// kibi.job.v1 async receipt. oneOf is intentionally left degrading: its
|
|
95
|
+
// required-guard branches carry no properties and would loosen input
|
|
96
|
+
// validation instead of tightening it.
|
|
97
|
+
const anyOfVariants = Array.isArray(obj.anyOf) ? obj.anyOf : [];
|
|
98
|
+
if (anyOfVariants.length > 0) {
|
|
99
|
+
const variants = anyOfVariants.map((variant) => jsonSchemaToZod(variant));
|
|
100
|
+
const union = z.union(variants);
|
|
101
|
+
return typeof obj.description === "string"
|
|
102
|
+
? union.describe(obj.description)
|
|
103
|
+
: union;
|
|
104
|
+
}
|
|
81
105
|
// JSON Schema nullable fields are represented as a type union in the
|
|
82
106
|
// catalog. Build a real Zod union so MCP publishes and validates the same
|
|
83
107
|
// nullability instead of silently degrading the field to z.any().
|
|
@@ -195,10 +219,7 @@ export function jsonSchemaToZod(schema) {
|
|
|
195
219
|
const description = typeof obj.description === "string" ? obj.description : undefined;
|
|
196
220
|
return description ? n.describe(description) : n;
|
|
197
221
|
}
|
|
198
|
-
default:
|
|
199
|
-
|
|
200
|
-
const description = typeof obj.description === "string" ? obj.description : undefined;
|
|
201
|
-
return description ? anySchema.describe(description) : anySchema;
|
|
202
|
-
}
|
|
222
|
+
default:
|
|
223
|
+
return describedAnySchema(typeof obj.description === "string" ? obj.description : undefined);
|
|
203
224
|
}
|
|
204
225
|
}
|
package/dist/server/session.js
CHANGED
|
@@ -37,35 +37,53 @@ const defaultSessionDeps = {
|
|
|
37
37
|
resolveWorkspaceRoot,
|
|
38
38
|
};
|
|
39
39
|
let sessionDeps = { ...defaultSessionDeps };
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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;
|