kibi-mcp 0.15.1 → 0.16.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 +52 -1
- package/dist/server/docs.js +17 -14
- package/dist/server/session.js +37 -0
- package/dist/server/tools.js +84 -3
- package/dist/tools/autopilot-discovery.js +1 -1
- package/dist/tools/autopilot-generate.js +53 -18
- package/dist/tools/suggest-predicates.js +554 -0
- package/dist/tools/upsert.js +18 -5
- package/dist/tools-config.js +124 -4
- package/package.json +3 -6
package/dist/diagnostics.js
CHANGED
|
@@ -37,7 +37,7 @@ let diagnosticUsageLogPath = null;
|
|
|
37
37
|
export function initializeDiagnosticMode(enabled = DIAGNOSTIC_MODE_ENABLED) {
|
|
38
38
|
diagnosticUsageLogPath = null;
|
|
39
39
|
if (!enabled) {
|
|
40
|
-
|
|
40
|
+
process.env.KIBI_MCP_DIAGNOSTIC_MODE = "0";
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
@@ -89,6 +89,57 @@ export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
|
|
|
89
89
|
},
|
|
90
90
|
};
|
|
91
91
|
// implements REQ-002
|
|
92
|
+
export function classifyDiagnosticError(error) {
|
|
93
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
94
|
+
const message = err.message;
|
|
95
|
+
const lower = message.toLowerCase();
|
|
96
|
+
if (lower.includes("stale_snapshot")) {
|
|
97
|
+
return buildErrorFields(err, "stale_snapshot", "persistence", "KB snapshot is stale; refresh/retry after the latest KB state is attached.");
|
|
98
|
+
}
|
|
99
|
+
if (lower.includes("unknown option") && lower.includes("h for help")) {
|
|
100
|
+
return buildErrorFields(err, "prolog_unknown_option", "prolog_runtime", "Prolog rejected startup/module/query options; inspect MCP package wiring and Prolog invocation.");
|
|
101
|
+
}
|
|
102
|
+
if (lower.includes("prolog process not started")) {
|
|
103
|
+
return buildErrorFields(err, "prolog_process_not_started", "prolog_lifecycle", "Prolog process is unavailable; restart the MCP server before retrying.");
|
|
104
|
+
}
|
|
105
|
+
if (lower.includes("resetting prolog worker") ||
|
|
106
|
+
lower.includes("prolog worker reset")) {
|
|
107
|
+
return buildErrorFields(err, "prolog_worker_reset", "prolog_lifecycle", "Prolog worker was reset so the next MCP call can start from a fresh worker.");
|
|
108
|
+
}
|
|
109
|
+
if (/timed out after \d+ms/i.test(message) ||
|
|
110
|
+
lower.includes("tool timeout")) {
|
|
111
|
+
return buildErrorFields(err, "tool_timeout", "tool_timeout", "MCP tool execution exceeded its bounded timeout.");
|
|
112
|
+
}
|
|
113
|
+
if (lower.includes("coarsely while granular symbols are available")) {
|
|
114
|
+
return buildErrorFields(err, "coarse_symbol_linkage", "validation", "Symbol traceability targeted a coarse file/module while narrower exported symbols exist.");
|
|
115
|
+
}
|
|
116
|
+
if (message.startsWith("Entity validation failed:")) {
|
|
117
|
+
return buildErrorFields(err, "entity_validation_failed", "validation", "Entity payload failed schema validation.");
|
|
118
|
+
}
|
|
119
|
+
if (message.startsWith("Relationship validation failed")) {
|
|
120
|
+
return buildErrorFields(err, "relationship_validation_failed", "validation", "Relationship payload failed schema validation.");
|
|
121
|
+
}
|
|
122
|
+
if (message.startsWith("Relationship source must match the upserted entity")) {
|
|
123
|
+
return buildErrorFields(err, "relationship_source_mismatch", "validation", "Relationship source did not match the entity being upserted.");
|
|
124
|
+
}
|
|
125
|
+
if (lower.includes("module load failed")) {
|
|
126
|
+
return buildErrorFields(err, "prolog_module_load_failed", "prolog_runtime", "Prolog failed to load an execution module.");
|
|
127
|
+
}
|
|
128
|
+
if (lower.includes("query failed")) {
|
|
129
|
+
return buildErrorFields(err, "prolog_query_failed", "prolog_runtime", "Prolog query execution failed.");
|
|
130
|
+
}
|
|
131
|
+
return buildErrorFields(err, "handler_error", "handler", "Unhandled MCP handler error.");
|
|
132
|
+
}
|
|
133
|
+
function buildErrorFields(error, category, stage, summary) {
|
|
134
|
+
return {
|
|
135
|
+
error_name: error.name,
|
|
136
|
+
error_message: error.message,
|
|
137
|
+
error_category: category,
|
|
138
|
+
error_stage: stage,
|
|
139
|
+
error_summary: summary,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// implements REQ-002
|
|
92
143
|
export function deriveDiagnosticFields(toolName, args, telemetry, result) {
|
|
93
144
|
const fields = {
|
|
94
145
|
telemetry_status: telemetry ? "provided" : "missing",
|
package/dist/server/docs.js
CHANGED
|
@@ -38,6 +38,7 @@ function renderToolsDoc() {
|
|
|
38
38
|
lines.push("");
|
|
39
39
|
lines.push("Modeling note: Kibi has eight core entity types grouped into common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).");
|
|
40
40
|
lines.push("Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; use `flag` for runtime/config gates and `fact_kind: observation` or `meta` for bug/workaround notes.");
|
|
41
|
+
lines.push("Predicate flow: before writing ontology prose, call `kb_suggest_predicates`; apply a suggested `fact_kind: predicate` via `requires_predicate`, or record the returned `review:ontology-gap` observation when no predicate fits.");
|
|
41
42
|
return lines.join("\n");
|
|
42
43
|
}
|
|
43
44
|
export const PROMPTS = [
|
|
@@ -48,7 +49,7 @@ export const PROMPTS = [
|
|
|
48
49
|
text: [
|
|
49
50
|
"# Kibi Interactive Activation Workflow",
|
|
50
51
|
"",
|
|
51
|
-
"Use this workflow to onboard a new or empty repository into Kibi through interactive discovery.",
|
|
52
|
+
"Use this post-hoc workflow to onboard a new or empty repository into Kibi through interactive discovery.",
|
|
52
53
|
"",
|
|
53
54
|
"## Step 1: Gather Declared Context",
|
|
54
55
|
"",
|
|
@@ -63,27 +64,28 @@ export const PROMPTS = [
|
|
|
63
64
|
"Call `kb_autopilot_generate` with the gathered context to synthesize candidate entities.",
|
|
64
65
|
"",
|
|
65
66
|
"This tool is **read-only**. It returns additive `structuredContent` with:",
|
|
66
|
-
"- `promptBlock`: review text that
|
|
67
|
+
"- `promptBlock`: review text that must be surfaced before writes",
|
|
67
68
|
"- `recommendedActions`: agent-facing next steps, including any REQ/SCEN/TEST authoring routed for manual handling",
|
|
68
69
|
"- `declaredContext`: the user-provided bootstrap context",
|
|
69
70
|
"- `confidence`: confidence summary for the generated output",
|
|
70
71
|
"- `bootstrapMode`: current KB state (e.g., `root_uninitialized`)",
|
|
71
72
|
"- `candidates`: synthesized entities grounded in declared context and source evidence",
|
|
73
|
+
"- `applyPlan`: exact sequential `kb_upsert` payloads for approved candidates",
|
|
72
74
|
"- `discoverySummary`: source-backed discovery notes",
|
|
73
75
|
"",
|
|
74
|
-
"## Step 3:
|
|
76
|
+
"## Step 3: Preview and Approval",
|
|
75
77
|
"",
|
|
76
|
-
"Surface the `promptBlock
|
|
78
|
+
"Surface the `promptBlock`, a summary of `candidates`, and the exact `structuredContent.applyPlan` payloads. Wait for explicit user approval before proceeding to writes.",
|
|
77
79
|
"",
|
|
78
80
|
"## Step 4: Apply Candidates",
|
|
79
81
|
"",
|
|
80
82
|
"Apply candidates sequentially using `kb_upsert`.",
|
|
81
|
-
"1. Execute
|
|
83
|
+
"1. Execute `structuredContent.applyPlan` sequentially in listed order.",
|
|
82
84
|
"2. Confirm success of each `kb_upsert` before moving to the next.",
|
|
83
85
|
"3. Run `kb_check` after the batch to verify KB integrity.",
|
|
84
86
|
"",
|
|
85
87
|
"## Rules",
|
|
86
|
-
"-
|
|
88
|
+
"- Never apply bootstrap writes without user-facing preview and explicit approval.",
|
|
87
89
|
"- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
|
|
88
90
|
"- Guidance must stay MCP-only; do not suggest `kibi` CLI commands.",
|
|
89
91
|
].join("\n"),
|
|
@@ -110,7 +112,7 @@ export const PROMPTS = [
|
|
|
110
112
|
"Core modeling principles:",
|
|
111
113
|
"- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
|
|
112
114
|
"- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
|
|
113
|
-
"- High-confidence
|
|
115
|
+
"- High-confidence `kb_model_requirement` output is deterministic; `/init-kibi` bootstrap writes still require preview and explicit approval.",
|
|
114
116
|
"- Low-confidence claims (< 0.7) are downgraded to `observation` facts to prevent false-positive contradictions.",
|
|
115
117
|
"- Only strict domain facts participate in contradiction inference; observation and meta facts are non-blocking notes.",
|
|
116
118
|
"- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
|
|
@@ -134,8 +136,9 @@ export const PROMPTS = [
|
|
|
134
136
|
"3. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
|
|
135
137
|
"4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
|
|
136
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`).",
|
|
137
|
-
"
|
|
138
|
-
"
|
|
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
|
+
"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.",
|
|
139
142
|
"",
|
|
140
143
|
"If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
|
|
141
144
|
].join("\n"),
|
|
@@ -202,16 +205,16 @@ function registerDocResources() {
|
|
|
202
205
|
"4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
|
|
203
206
|
'5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
|
|
204
207
|
"",
|
|
208
|
+
"## Model requirements as ontology predicates",
|
|
209
|
+
'1. Spell out the requirement prose and call `kb_suggest_predicates` with `{ "text": "...", "requirementId": "REQ-..." }`',
|
|
210
|
+
"2. If candidates are returned, apply the top or user-selected `structuredContent.applyPlan` to create `fact_kind: predicate`, then attach `structuredContent.relationshipPlan` with `requires_predicate` while preserving existing req metadata",
|
|
211
|
+
"3. If no candidate fits, apply or review the returned `review:ontology-gap` observation instead of silently writing prose",
|
|
212
|
+
"",
|
|
205
213
|
"Note: Kibi has eight core entity types. Create or reuse `fact` entities first, then create `req` entities and link with `constrains` and `requires_property` (create-before-link).",
|
|
206
214
|
"Only strict domain facts are contradiction-safe. Use `flag` for runtime/config gates; use `fact` with `fact_kind: observation` or `meta` for bug/workaround notes.",
|
|
207
215
|
"",
|
|
208
216
|
"## Find missing coverage",
|
|
209
217
|
'1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
|
|
210
|
-
"",
|
|
211
|
-
"## Find missing coverage",
|
|
212
|
-
"",
|
|
213
|
-
"## Find missing coverage",
|
|
214
|
-
'1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
|
|
215
218
|
'2. `kb_coverage` with `{ "by": "req", "includePassing": false }` to review evaluated coverage rows',
|
|
216
219
|
'3. `kb_graph` with `{ "seedIds": ["REQ-001"], "direction": "both", "depth": 2 }` to inspect neighboring entities',
|
|
217
220
|
"",
|
package/dist/server/session.js
CHANGED
|
@@ -38,6 +38,7 @@ export let prologProcess = null;
|
|
|
38
38
|
let isInitialized = false;
|
|
39
39
|
export let activeBranchName = "develop";
|
|
40
40
|
let ensurePrologTail = Promise.resolve();
|
|
41
|
+
let prologResetGeneration = 0;
|
|
41
42
|
export let isShuttingDown = false;
|
|
42
43
|
let shutdownTimeout = null;
|
|
43
44
|
export const inFlightRequests = new Map();
|
|
@@ -47,6 +48,7 @@ export function resetSessionStateForTests() {
|
|
|
47
48
|
isInitialized = false;
|
|
48
49
|
activeBranchName = "develop";
|
|
49
50
|
ensurePrologTail = Promise.resolve();
|
|
51
|
+
prologResetGeneration = 0;
|
|
50
52
|
isShuttingDown = false;
|
|
51
53
|
inFlightRequests.clear();
|
|
52
54
|
if (shutdownTimeout) {
|
|
@@ -138,8 +140,38 @@ export async function initiateGracefulShutdown(exitCode = 0) {
|
|
|
138
140
|
process.exit(exitCode);
|
|
139
141
|
}
|
|
140
142
|
// implements REQ-008
|
|
143
|
+
export async function resetProlog(reason) {
|
|
144
|
+
debugLog(`[KIBI-MCP] Resetting Prolog worker: ${reason}`);
|
|
145
|
+
prologResetGeneration += 1;
|
|
146
|
+
const current = prologProcess;
|
|
147
|
+
prologProcess = null;
|
|
148
|
+
isInitialized = false;
|
|
149
|
+
if (current) {
|
|
150
|
+
try {
|
|
151
|
+
await current.terminate();
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error("[KIBI-MCP] Error resetting Prolog worker:", error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// implements REQ-008
|
|
141
159
|
async function ensurePrologUnsafe() {
|
|
160
|
+
const generationAtStart = prologResetGeneration;
|
|
142
161
|
const workspaceRoot = sessionDeps.resolveWorkspaceRoot();
|
|
162
|
+
const assertGeneration = async () => {
|
|
163
|
+
if (generationAtStart !== prologResetGeneration) {
|
|
164
|
+
const current = prologProcess;
|
|
165
|
+
prologProcess = null;
|
|
166
|
+
isInitialized = false;
|
|
167
|
+
if (current) {
|
|
168
|
+
await current.terminate().catch((error) => {
|
|
169
|
+
console.error("[KIBI-MCP] Error terminating stale Prolog after reset generation change:", error);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
throw new Error("Prolog worker reset while initialization was in progress");
|
|
173
|
+
}
|
|
174
|
+
};
|
|
143
175
|
// Determine target branch: respect KIBI_BRANCH override or resolve from git
|
|
144
176
|
const envBranch = getBranchOverride();
|
|
145
177
|
let targetBranch;
|
|
@@ -170,10 +202,12 @@ async function ensurePrologUnsafe() {
|
|
|
170
202
|
debugLog(`[KIBI-MCP] Branch changed: ${activeBranchName} -> ${targetBranch}`);
|
|
171
203
|
// Persist and detach from old KB
|
|
172
204
|
const saveResult = await prologProcess.query("kb_save");
|
|
205
|
+
await assertGeneration();
|
|
173
206
|
if (!saveResult.success) {
|
|
174
207
|
throw new Error(`Failed to save old KB before detach: ${saveResult.error || "Unknown error"}`);
|
|
175
208
|
}
|
|
176
209
|
const detachResult = await prologProcess.query("kb_detach");
|
|
210
|
+
await assertGeneration();
|
|
177
211
|
if (!detachResult.success) {
|
|
178
212
|
debugLog(`[KIBI-MCP] Warning: failed to detach from old KB: ${detachResult.error || "Unknown error"}`);
|
|
179
213
|
// Continue anyway - we'll try to attach to the new KB
|
|
@@ -183,6 +217,7 @@ async function ensurePrologUnsafe() {
|
|
|
183
217
|
const newKbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
184
218
|
// Attach to new branch KB
|
|
185
219
|
const attachResult = await prologProcess.query(`kb_attach('${newKbPath}')`);
|
|
220
|
+
await assertGeneration();
|
|
186
221
|
if (!attachResult.success) {
|
|
187
222
|
throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
|
|
188
223
|
}
|
|
@@ -195,6 +230,7 @@ async function ensurePrologUnsafe() {
|
|
|
195
230
|
debugLog("[KIBI-MCP] Initializing Prolog process...");
|
|
196
231
|
prologProcess = new sessionDeps.PrologProcess({ timeout: 120000 });
|
|
197
232
|
await prologProcess.start();
|
|
233
|
+
await assertGeneration();
|
|
198
234
|
// Startup debug: resolve which kibi-cli is being used and its version (best-effort).
|
|
199
235
|
// Gate all output under KIBI_MCP_DEBUG and write only to stderr via debugLog.
|
|
200
236
|
if (isMcpDebugEnabled()) {
|
|
@@ -235,6 +271,7 @@ async function ensurePrologUnsafe() {
|
|
|
235
271
|
ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
236
272
|
const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
237
273
|
const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
|
|
274
|
+
await assertGeneration();
|
|
238
275
|
if (!attachResult.success) {
|
|
239
276
|
throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
|
|
240
277
|
}
|
package/dist/server/tools.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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";
|
|
1
19
|
import { z } from "zod";
|
|
2
|
-
import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
|
|
20
|
+
import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, classifyDiagnosticError, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
|
|
3
21
|
import { isMcpDebugEnabled } from "../env.js";
|
|
4
22
|
import { TOOLS } from "../tools-config.js";
|
|
5
23
|
import { handleKbAutopilotGenerate, } from "../tools/autopilot-generate.js";
|
|
@@ -13,7 +31,10 @@ import { handleKbQuery } from "../tools/query.js";
|
|
|
13
31
|
import { handleKbSearch } from "../tools/search.js";
|
|
14
32
|
import { handleKbSkillsList, handleKbSkillsLoad, handleKbSkillsRead, } from "../tools/skills.js";
|
|
15
33
|
import { handleKbStatus } from "../tools/status.js";
|
|
34
|
+
import { handleKbSuggestPredicates, } from "../tools/suggest-predicates.js";
|
|
16
35
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
36
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 90_000;
|
|
37
|
+
const TOOL_TIMEOUT_ENV = "KIBI_MCP_TOOL_TIMEOUT_MS";
|
|
17
38
|
const defaultToolsServerDeps = {
|
|
18
39
|
getSessionModule: () => import("./session.js"),
|
|
19
40
|
};
|
|
@@ -40,6 +61,7 @@ async function getSessionModule() {
|
|
|
40
61
|
const DEFAULT_TOOLS_RUNTIME = {
|
|
41
62
|
diagnosticModeEnabled: () => DIAGNOSTIC_MODE_ENABLED,
|
|
42
63
|
appendUsageLogLine,
|
|
64
|
+
classifyDiagnosticError,
|
|
43
65
|
deriveDiagnosticFields,
|
|
44
66
|
extractToolCallPayload,
|
|
45
67
|
// INTENTIONAL: TOOLS is imported as a Zod-inferred schema type; ToolConfig is the
|
|
@@ -48,6 +70,7 @@ const DEFAULT_TOOLS_RUNTIME = {
|
|
|
48
70
|
tools: TOOLS,
|
|
49
71
|
activeBranchName: async () => (await getSessionModule()).activeBranchName,
|
|
50
72
|
ensureProlog: async () => (await getSessionModule()).ensureProlog(),
|
|
73
|
+
resetProlog: async (reason) => (await getSessionModule()).resetProlog(reason),
|
|
51
74
|
inFlightRequests: async () => (await getSessionModule()).inFlightRequests,
|
|
52
75
|
isShuttingDown: async () => (await getSessionModule()).isShuttingDown,
|
|
53
76
|
prologProcess: async () => (await getSessionModule()).prologProcess,
|
|
@@ -64,6 +87,7 @@ const DEFAULT_TOOLS_RUNTIME = {
|
|
|
64
87
|
handleKbSkillsRead,
|
|
65
88
|
handleKbUpsert,
|
|
66
89
|
handleKbModelRequirement,
|
|
90
|
+
handleKbSuggestPredicates,
|
|
67
91
|
handleKbAutopilotGenerate,
|
|
68
92
|
};
|
|
69
93
|
// implements REQ-008
|
|
@@ -73,6 +97,43 @@ function debugLog(...args) {
|
|
|
73
97
|
}
|
|
74
98
|
}
|
|
75
99
|
// implements REQ-002
|
|
100
|
+
function getToolTimeoutMs() {
|
|
101
|
+
const raw = process.env[TOOL_TIMEOUT_ENV]?.trim();
|
|
102
|
+
if (!raw) {
|
|
103
|
+
return DEFAULT_TOOL_TIMEOUT_MS;
|
|
104
|
+
}
|
|
105
|
+
const parsed = Number(raw);
|
|
106
|
+
return Number.isSafeInteger(parsed) && parsed > 0
|
|
107
|
+
? parsed
|
|
108
|
+
: DEFAULT_TOOL_TIMEOUT_MS;
|
|
109
|
+
}
|
|
110
|
+
// implements REQ-002
|
|
111
|
+
function createToolTimeoutError(toolName, timeoutMs) {
|
|
112
|
+
return new Error(`Tool ${toolName} timed out after ${timeoutMs}ms`);
|
|
113
|
+
}
|
|
114
|
+
// implements REQ-002
|
|
115
|
+
async function withToolTimeout(toolName, operation, onTimeout) {
|
|
116
|
+
const timeoutMs = getToolTimeoutMs();
|
|
117
|
+
let timeout;
|
|
118
|
+
try {
|
|
119
|
+
return await Promise.race([
|
|
120
|
+
operation,
|
|
121
|
+
new Promise((_, reject) => {
|
|
122
|
+
timeout = setTimeout(() => {
|
|
123
|
+
const error = createToolTimeoutError(toolName, timeoutMs);
|
|
124
|
+
reject(error);
|
|
125
|
+
void onTimeout(error, timeoutMs);
|
|
126
|
+
}, timeoutMs);
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (timeout) {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// implements REQ-002
|
|
76
137
|
export function jsonSchemaToZod(schema) {
|
|
77
138
|
if (!schema || typeof schema !== "object") {
|
|
78
139
|
return z.any();
|
|
@@ -209,9 +270,21 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
209
270
|
const trackedRequests = await runtime.inFlightRequests();
|
|
210
271
|
const handlerPromise = handler(businessArgs);
|
|
211
272
|
trackedRequests.set(requestId, handlerPromise);
|
|
273
|
+
let resetAttempted = false;
|
|
274
|
+
let resetSucceeded = false;
|
|
275
|
+
let resetError = null;
|
|
212
276
|
try {
|
|
213
277
|
// Execute handler
|
|
214
|
-
const result = await handlerPromise
|
|
278
|
+
const result = await withToolTimeout(name, handlerPromise, async () => {
|
|
279
|
+
resetAttempted = true;
|
|
280
|
+
try {
|
|
281
|
+
await runtime.resetProlog(`tool timeout: ${name}`);
|
|
282
|
+
resetSucceeded = true;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
resetError = error instanceof Error ? error.message : String(error);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
215
288
|
// Log usage in diagnostic mode
|
|
216
289
|
if (diagnosticModeEnabled) {
|
|
217
290
|
const finishedAt = new Date();
|
|
@@ -240,6 +313,7 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
240
313
|
if (diagnosticModeEnabled) {
|
|
241
314
|
const finishedAt = new Date();
|
|
242
315
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
316
|
+
const diagnosticErrorFields = runtime.classifyDiagnosticError(err);
|
|
243
317
|
const processHandle = await runtime.prologProcess();
|
|
244
318
|
const branchName = await runtime.activeBranchName();
|
|
245
319
|
runtime.appendUsageLogLine({
|
|
@@ -254,7 +328,10 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
254
328
|
duration_ms: finishedAt.getTime() - startedAt.getTime(),
|
|
255
329
|
prolog_pid: processHandle?.getPid() ?? null,
|
|
256
330
|
active_branch: branchName,
|
|
257
|
-
|
|
331
|
+
reset_attempted: resetAttempted,
|
|
332
|
+
reset_succeeded: resetSucceeded,
|
|
333
|
+
reset_error: resetError,
|
|
334
|
+
...diagnosticErrorFields,
|
|
258
335
|
});
|
|
259
336
|
}
|
|
260
337
|
throw error;
|
|
@@ -332,6 +409,10 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
332
409
|
const prolog = await runtime.ensureProlog();
|
|
333
410
|
return runtime.handleKbModelRequirement(prolog, args);
|
|
334
411
|
}, runtime);
|
|
412
|
+
addTool(server, "kb_suggest_predicates", toolDef("kb_suggest_predicates").description, toolDef("kb_suggest_predicates").inputSchema, async (args) => {
|
|
413
|
+
const prolog = await runtime.ensureProlog();
|
|
414
|
+
return runtime.handleKbSuggestPredicates(prolog, args);
|
|
415
|
+
}, runtime);
|
|
335
416
|
addTool(server, "kb_autopilot_generate", toolDef("kb_autopilot_generate").description, toolDef("kb_autopilot_generate").inputSchema, async (args) => {
|
|
336
417
|
const prolog = await runtime.ensureProlog();
|
|
337
418
|
return runtime.handleKbAutopilotGenerate(prolog, args);
|
|
@@ -847,7 +847,7 @@ export async function classifyActivationState(workspaceRoot, prolog) {
|
|
|
847
847
|
}
|
|
848
848
|
}
|
|
849
849
|
// Recursively collect markdown files under `dir`, excluding known ignore dirs.
|
|
850
|
-
function collectMarkdownFiles(dir, workspaceRoot, vendoredRoots) {
|
|
850
|
+
export function collectMarkdownFiles(dir, workspaceRoot, vendoredRoots) {
|
|
851
851
|
const results = [];
|
|
852
852
|
if (!fs.existsSync(dir))
|
|
853
853
|
return results;
|
|
@@ -6,6 +6,25 @@ import { buildGenericMarkdownCandidates, buildNormativeRequirementCandidates, bu
|
|
|
6
6
|
import { discoverProviderEvidence, resolveActivationPolicy, } from "./autopilot-discovery.js";
|
|
7
7
|
import { loadEntities } from "./entity-query.js";
|
|
8
8
|
import { getWorkspaceMigrationWarning } from "./model-requirement.js";
|
|
9
|
+
const defaultAutopilotGenerateDeps = {
|
|
10
|
+
buildGenericMarkdownCandidates,
|
|
11
|
+
buildNormativeRequirementCandidates,
|
|
12
|
+
buildProviderEvidenceCandidates,
|
|
13
|
+
buildSymbolManifestCandidates,
|
|
14
|
+
buildTypedMarkdownCandidates,
|
|
15
|
+
collectSourceOnlyAuthoringSignals,
|
|
16
|
+
discoverProviderEvidence,
|
|
17
|
+
getWorkspaceMigrationWarning,
|
|
18
|
+
loadEntities,
|
|
19
|
+
resolveActivationPolicy,
|
|
20
|
+
};
|
|
21
|
+
let autopilotGenerateDeps = defaultAutopilotGenerateDeps;
|
|
22
|
+
export function _setAutopilotGenerateDepsForTests(deps) {
|
|
23
|
+
autopilotGenerateDeps = { ...defaultAutopilotGenerateDeps, ...deps };
|
|
24
|
+
}
|
|
25
|
+
export function _resetAutopilotGenerateDepsForTests() {
|
|
26
|
+
autopilotGenerateDeps = defaultAutopilotGenerateDeps;
|
|
27
|
+
}
|
|
9
28
|
function clamp(value, min, max) {
|
|
10
29
|
return Math.max(min, Math.min(max, value));
|
|
11
30
|
}
|
|
@@ -60,6 +79,13 @@ function countCandidatesByType(candidateRecords) {
|
|
|
60
79
|
}
|
|
61
80
|
return counts;
|
|
62
81
|
}
|
|
82
|
+
function buildApplyPlan(candidateRecords) {
|
|
83
|
+
return candidateRecords.flatMap((candidate) => {
|
|
84
|
+
if (!Array.isArray(candidate.applyPlan))
|
|
85
|
+
return [];
|
|
86
|
+
return candidate.applyPlan.filter((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
63
89
|
function formatCandidateTypeCounts(candidateRecords) {
|
|
64
90
|
const counts = countCandidatesByType(candidateRecords);
|
|
65
91
|
return Object.keys(counts)
|
|
@@ -370,7 +396,7 @@ _prolog, args) {
|
|
|
370
396
|
// Gather existing entity ids to suppress duplicates
|
|
371
397
|
let existingIds = new Set();
|
|
372
398
|
try {
|
|
373
|
-
const entities = await loadEntities(prolog, {});
|
|
399
|
+
const entities = await autopilotGenerateDeps.loadEntities(prolog, {});
|
|
374
400
|
for (const e of entities) {
|
|
375
401
|
const id = String(e.id ?? "");
|
|
376
402
|
if (id)
|
|
@@ -382,10 +408,10 @@ _prolog, args) {
|
|
|
382
408
|
existingIds = new Set();
|
|
383
409
|
}
|
|
384
410
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
385
|
-
const activation = await resolveActivationPolicy(workspaceRoot, prolog);
|
|
411
|
+
const activation = await autopilotGenerateDeps.resolveActivationPolicy(workspaceRoot, prolog);
|
|
386
412
|
const activationState = activation.activationState;
|
|
387
|
-
const activationDiscovery = discoverProviderEvidence(workspaceRoot, activation);
|
|
388
|
-
const migrationWarning = await getWorkspaceMigrationWarning(workspaceRoot);
|
|
413
|
+
const activationDiscovery = autopilotGenerateDeps.discoverProviderEvidence(workspaceRoot, activation);
|
|
414
|
+
const migrationWarning = await autopilotGenerateDeps.getWorkspaceMigrationWarning(workspaceRoot);
|
|
389
415
|
const declaredContext = normalizeBootstrapContext(bootstrapContext);
|
|
390
416
|
const discoveredCandidatePaths = activationDiscovery.evidence.reduce((acc, item) => {
|
|
391
417
|
const relativePath = item.relativePath;
|
|
@@ -407,7 +433,7 @@ _prolog, args) {
|
|
|
407
433
|
markdownFiles: [],
|
|
408
434
|
evidence: candidateDiscovery.evidence.filter((item) => item.kind !== "generic_markdown"),
|
|
409
435
|
};
|
|
410
|
-
let sourceOnlySignals = collectSourceOnlyAuthoringSignals(guidanceDiscovery, {
|
|
436
|
+
let sourceOnlySignals = autopilotGenerateDeps.collectSourceOnlyAuthoringSignals(guidanceDiscovery, {
|
|
411
437
|
ids: existingIds,
|
|
412
438
|
workspaceRoot,
|
|
413
439
|
}, normalizedMinConfidence);
|
|
@@ -428,28 +454,31 @@ _prolog, args) {
|
|
|
428
454
|
return `${entityType}::${String(title).trim().toLowerCase().replace(/\s+/g, " ")}`;
|
|
429
455
|
}
|
|
430
456
|
if (activation.allowCandidateGeneration) {
|
|
431
|
-
typedMarkdownCandidates =
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
457
|
+
typedMarkdownCandidates =
|
|
458
|
+
autopilotGenerateDeps.buildTypedMarkdownCandidates(candidateDiscovery, {
|
|
459
|
+
ids: existingIds,
|
|
460
|
+
workspaceRoot,
|
|
461
|
+
});
|
|
462
|
+
manifestCandidates = autopilotGenerateDeps.buildSymbolManifestCandidates(candidateDiscovery, {
|
|
436
463
|
ids: existingIds,
|
|
437
464
|
workspaceRoot,
|
|
438
465
|
});
|
|
439
466
|
if (includeGenericMarkdown) {
|
|
440
|
-
genericCandidates = buildGenericMarkdownCandidates(candidateDiscovery, {
|
|
467
|
+
genericCandidates = autopilotGenerateDeps.buildGenericMarkdownCandidates(candidateDiscovery, {
|
|
441
468
|
ids: existingIds,
|
|
442
469
|
workspaceRoot,
|
|
443
470
|
}, normalizedMinConfidence);
|
|
444
|
-
normativeRequirementCandidates =
|
|
471
|
+
normativeRequirementCandidates =
|
|
472
|
+
autopilotGenerateDeps.buildNormativeRequirementCandidates(candidateDiscovery, {
|
|
473
|
+
ids: existingIds,
|
|
474
|
+
workspaceRoot,
|
|
475
|
+
}, normalizedMinConfidence);
|
|
476
|
+
}
|
|
477
|
+
providerEvidenceCandidates =
|
|
478
|
+
autopilotGenerateDeps.buildProviderEvidenceCandidates(candidateDiscovery, {
|
|
445
479
|
ids: existingIds,
|
|
446
480
|
workspaceRoot,
|
|
447
481
|
}, normalizedMinConfidence);
|
|
448
|
-
}
|
|
449
|
-
providerEvidenceCandidates = buildProviderEvidenceCandidates(candidateDiscovery, {
|
|
450
|
-
ids: existingIds,
|
|
451
|
-
workspaceRoot,
|
|
452
|
-
}, normalizedMinConfidence);
|
|
453
482
|
allCandidates = [
|
|
454
483
|
...typedMarkdownCandidates,
|
|
455
484
|
...manifestCandidates,
|
|
@@ -573,6 +602,7 @@ _prolog, args) {
|
|
|
573
602
|
// best-effort only; ignore failures here so generation can continue
|
|
574
603
|
}
|
|
575
604
|
const candidateRecords = Array.from(seenByKey.values());
|
|
605
|
+
const applyPlan = buildApplyPlan(candidateRecords);
|
|
576
606
|
const payoffSummary = buildPayoffSummary(candidateRecords);
|
|
577
607
|
const promptBlock = buildPromptBlock(workspaceRoot, activationState, activation.activationMode, activation.reason, activation.applyBlocked, declaredContext, candidateRecords, sourceOnlySignals, activationDiscovery.summary.scanWarnings);
|
|
578
608
|
const confidence = buildConfidence(activation.activationMode, activation.applyBlocked, declaredContext, candidateRecords, sourceOnlySignals, promptBlock);
|
|
@@ -585,6 +615,9 @@ _prolog, args) {
|
|
|
585
615
|
const effectiveTldr = confidence.level === "low" && !activation.applyBlocked
|
|
586
616
|
? `Low-confidence bootstrap (${confidence.score}): review diagnostics before proceeding. ${tldr}`
|
|
587
617
|
: tldr;
|
|
618
|
+
const effectiveText = applyPlan.length > 0
|
|
619
|
+
? `${effectiveTldr} Review structuredContent.applyPlan for exact sequential kb_upsert payloads before requesting approval.`
|
|
620
|
+
: effectiveTldr;
|
|
588
621
|
const structuredContent = {
|
|
589
622
|
activationState,
|
|
590
623
|
activationMode: activation.activationMode,
|
|
@@ -602,6 +635,7 @@ _prolog, args) {
|
|
|
602
635
|
declaredContext,
|
|
603
636
|
discoverySummary: activationDiscovery.summary,
|
|
604
637
|
candidates: candidateRecords,
|
|
638
|
+
applyPlan,
|
|
605
639
|
suppressedCandidates: suppressed,
|
|
606
640
|
payoffSummary,
|
|
607
641
|
};
|
|
@@ -609,12 +643,13 @@ _prolog, args) {
|
|
|
609
643
|
content: [
|
|
610
644
|
{
|
|
611
645
|
type: "text",
|
|
612
|
-
text:
|
|
646
|
+
text: effectiveText,
|
|
613
647
|
},
|
|
614
648
|
],
|
|
615
649
|
structuredContent,
|
|
616
650
|
migrationWarning,
|
|
617
651
|
candidates: candidateRecords,
|
|
652
|
+
applyPlan,
|
|
618
653
|
suppressedCandidates: suppressed,
|
|
619
654
|
payoffSummary,
|
|
620
655
|
};
|