kibi-mcp 0.8.0 → 0.10.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/server/docs.js +33 -5
- package/dist/server/tools.js +6 -0
- package/dist/tools/briefing-generate.js +529 -0
- package/dist/tools-config.js +25 -1
- package/package.json +1 -1
package/dist/server/docs.js
CHANGED
|
@@ -35,7 +35,8 @@ function renderToolsDoc() {
|
|
|
35
35
|
: "none";
|
|
36
36
|
}
|
|
37
37
|
lines.push("");
|
|
38
|
-
lines.push("Modeling note:
|
|
38
|
+
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).");
|
|
39
|
+
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.");
|
|
39
40
|
return lines.join("\n");
|
|
40
41
|
}
|
|
41
42
|
export const PROMPTS = [
|
|
@@ -87,6 +88,32 @@ export const PROMPTS = [
|
|
|
87
88
|
"- `kb_autopilot_generate` is read-only; only `kb_upsert` mutates the KB",
|
|
88
89
|
].join("\n"),
|
|
89
90
|
},
|
|
91
|
+
{
|
|
92
|
+
name: "brief-kibi",
|
|
93
|
+
description: "Start-task workflow for generating a citation-backed Kibi briefing before risky work.",
|
|
94
|
+
text: [
|
|
95
|
+
"# Kibi Briefing Workflow",
|
|
96
|
+
"",
|
|
97
|
+
"Use this workflow at the start of a task when you need a deterministic, citation-backed Kibi briefing.",
|
|
98
|
+
"",
|
|
99
|
+
"## Step 1: Generate the briefing",
|
|
100
|
+
"",
|
|
101
|
+
"Call `kb_briefing_generate` with any relevant `taskText`, `sourceFiles`, and `seedIds`.",
|
|
102
|
+
"",
|
|
103
|
+
"This tool is read-only. It returns `briefingState`, `activationState`, `activationReason`, `freshness`, `confidence`, `tldr`, `promptBlock`, `entities`, `constraints`, `regressionRisks`, `missingEvidence`, and `citations`.",
|
|
104
|
+
"",
|
|
105
|
+
"## Step 2: Inspect readiness",
|
|
106
|
+
"",
|
|
107
|
+
"Inspect `briefingState` before acting.",
|
|
108
|
+
"- If `briefingState` is `ready`, continue using only cited output from the briefing.",
|
|
109
|
+
"- If `briefingState` is `no_briefing`, stop and proceed without inventing briefing claims.",
|
|
110
|
+
"",
|
|
111
|
+
"## Step 3: Use the cited output",
|
|
112
|
+
"",
|
|
113
|
+
"Use `constraints`, `regressionRisks`, `missingEvidence`, and `promptBlock` only when their claims are backed by the returned `citations` and cited `entities`.",
|
|
114
|
+
"Do not add uncited assertions, and do not treat omitted topics as verified.",
|
|
115
|
+
].join("\n"),
|
|
116
|
+
},
|
|
90
117
|
{
|
|
91
118
|
name: "kibi_overview",
|
|
92
119
|
description: "High-level model for using kibi-mcp safely and effectively.",
|
|
@@ -107,14 +134,14 @@ export const PROMPTS = [
|
|
|
107
134
|
"- `kb_check`: Validate KB integrity against configurable rules",
|
|
108
135
|
"",
|
|
109
136
|
"Core modeling principles:",
|
|
137
|
+
"- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
|
|
110
138
|
"- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
|
|
111
|
-
"-
|
|
139
|
+
"- Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; observation and meta facts are non-blocking notes.",
|
|
112
140
|
"- Use `kb_search` first for discovery, then `kb_query` for exact follow-up before any mutation.",
|
|
113
141
|
"- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
|
|
114
142
|
"- Run `kb_check` after meaningful mutations to catch integrity issues early.",
|
|
115
143
|
"- Prefer explicit IDs and enum values to avoid invalid parameters.",
|
|
116
|
-
"-
|
|
117
|
-
"- Model requirements by first creating/reusing fact entities, then express req semantics with `constrains` + `requires_property` relationships (create-before-link).",
|
|
144
|
+
"- Model requirements by first creating/reusing fact entities (create-before-link).",
|
|
118
145
|
"- flag gates runtime/config behavior; use `fact` with `fact_kind: observation` or `meta` for bug and workaround notes.",
|
|
119
146
|
].join("\n"),
|
|
120
147
|
},
|
|
@@ -197,7 +224,8 @@ function registerDocResources() {
|
|
|
197
224
|
"4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
|
|
198
225
|
'5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
|
|
199
226
|
"",
|
|
200
|
-
"Note: Create or reuse `fact` entities first, then create `req` entities and link with `constrains` and `requires_property` (create-before-link).
|
|
227
|
+
"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).",
|
|
228
|
+
"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.",
|
|
201
229
|
"",
|
|
202
230
|
"## Find missing coverage",
|
|
203
231
|
'1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
|
package/dist/server/tools.js
CHANGED
|
@@ -12,6 +12,7 @@ import { handleKbSearch } from "../tools/search.js";
|
|
|
12
12
|
import { handleKbStatus } from "../tools/status.js";
|
|
13
13
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
14
14
|
import { handleKbAutopilotGenerate, } from "../tools/autopilot-generate.js";
|
|
15
|
+
import { handleKbBriefingGenerate, } from "../tools/briefing-generate.js";
|
|
15
16
|
const defaultToolsServerDeps = {
|
|
16
17
|
getSessionModule: () => import("./session.js"),
|
|
17
18
|
};
|
|
@@ -59,6 +60,7 @@ const DEFAULT_TOOLS_RUNTIME = {
|
|
|
59
60
|
handleKbStatus,
|
|
60
61
|
handleKbUpsert,
|
|
61
62
|
handleKbAutopilotGenerate,
|
|
63
|
+
handleKbBriefingGenerate,
|
|
62
64
|
};
|
|
63
65
|
// implements REQ-008
|
|
64
66
|
function debugLog(...args) {
|
|
@@ -323,4 +325,8 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
323
325
|
const prolog = await runtime.ensureProlog();
|
|
324
326
|
return runtime.handleKbAutopilotGenerate(prolog, args);
|
|
325
327
|
}, runtime);
|
|
328
|
+
addTool(server, "kb_briefing_generate", toolDef("kb_briefing_generate").description, toolDef("kb_briefing_generate").inputSchema, async (args) => {
|
|
329
|
+
const prolog = await runtime.ensureProlog();
|
|
330
|
+
return runtime.handleKbBriefingGenerate(prolog, args);
|
|
331
|
+
}, runtime);
|
|
326
332
|
}
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { rankEntities } from "kibi-cli/search-ranking";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { classifyActivationState, } from "./autopilot-discovery.js";
|
|
5
|
+
import { runJsonModuleQuery, toPrologList } from "./core-module.js";
|
|
6
|
+
import { loadEntities } from "./entity-query.js";
|
|
7
|
+
import { handleKbStatus } from "./status.js";
|
|
8
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
9
|
+
const ALLOWED_TYPES = [
|
|
10
|
+
"req",
|
|
11
|
+
"adr",
|
|
12
|
+
"scenario",
|
|
13
|
+
"test",
|
|
14
|
+
"fact",
|
|
15
|
+
"flag",
|
|
16
|
+
"symbol",
|
|
17
|
+
];
|
|
18
|
+
const TYPE_PRIORITY = {
|
|
19
|
+
req: 7,
|
|
20
|
+
adr: 6,
|
|
21
|
+
scenario: 5,
|
|
22
|
+
test: 4,
|
|
23
|
+
fact: 3,
|
|
24
|
+
flag: 2,
|
|
25
|
+
symbol: 1,
|
|
26
|
+
};
|
|
27
|
+
const GRAPH_RELATIONSHIPS = [
|
|
28
|
+
"implements",
|
|
29
|
+
"covered_by",
|
|
30
|
+
"specified_by",
|
|
31
|
+
"verified_by",
|
|
32
|
+
"constrained_by",
|
|
33
|
+
"constrains",
|
|
34
|
+
"requires_property",
|
|
35
|
+
"guards",
|
|
36
|
+
"relates_to",
|
|
37
|
+
];
|
|
38
|
+
function activationReasonFor(state) {
|
|
39
|
+
switch (state) {
|
|
40
|
+
case "vendored_only":
|
|
41
|
+
return "Workspace appears to contain vendored Kibi sources only; briefing generation is disabled.";
|
|
42
|
+
case "root_partial":
|
|
43
|
+
return "Workspace root is partially configured; briefing generation is disabled until Kibi inputs fully resolve.";
|
|
44
|
+
case "root_active_seeded":
|
|
45
|
+
return "KB attached and ready for citation-backed briefing generation in a seeded workspace.";
|
|
46
|
+
case "root_active_thin":
|
|
47
|
+
return "KB attached but thin; briefing generation may lack enough evidence.";
|
|
48
|
+
default:
|
|
49
|
+
return "Workspace root is not fully initialized; briefing generation is disabled until Kibi is attached.";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function inferTextOnlyActivationState(workspaceRoot) {
|
|
53
|
+
try {
|
|
54
|
+
const vendoredMarkers = [
|
|
55
|
+
["kibi", "opencode.json"],
|
|
56
|
+
["kibi", "package.json"],
|
|
57
|
+
["kibi", "packages", "mcp"],
|
|
58
|
+
["kibi", "documentation"],
|
|
59
|
+
];
|
|
60
|
+
const hasVendoredTree = vendoredMarkers.some((segments) => pathExists(path.join(workspaceRoot, ...segments)));
|
|
61
|
+
const hasRootConfig = pathExists(path.join(workspaceRoot, ".kb", "config.json"));
|
|
62
|
+
if (!hasRootConfig && hasVendoredTree) {
|
|
63
|
+
return "vendored_only";
|
|
64
|
+
}
|
|
65
|
+
if (!hasRootConfig) {
|
|
66
|
+
return "root_uninitialized";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Fall through to the conservative thin state below.
|
|
71
|
+
}
|
|
72
|
+
return "root_active_thin";
|
|
73
|
+
}
|
|
74
|
+
function pathExists(candidatePath) {
|
|
75
|
+
try {
|
|
76
|
+
return fs.existsSync(candidatePath);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function unknownFreshness() {
|
|
83
|
+
return {
|
|
84
|
+
state: "unknown",
|
|
85
|
+
syncState: "unknown",
|
|
86
|
+
dirty: false,
|
|
87
|
+
syncedAt: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function normalizeTaskText(taskText) {
|
|
91
|
+
return (taskText ?? "").trim();
|
|
92
|
+
}
|
|
93
|
+
function normalizeSeedIds(seedIds) {
|
|
94
|
+
const normalized = [];
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
for (const seedId of seedIds ?? []) {
|
|
97
|
+
const trimmed = String(seedId ?? "").trim();
|
|
98
|
+
if (!trimmed || seen.has(trimmed))
|
|
99
|
+
continue;
|
|
100
|
+
seen.add(trimmed);
|
|
101
|
+
normalized.push(trimmed);
|
|
102
|
+
}
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
function normalizeSourceFiles(workspaceRoot, sourceFiles) {
|
|
106
|
+
const normalized = [];
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
const normalizedRoot = path.resolve(workspaceRoot);
|
|
109
|
+
for (const sourceFile of sourceFiles ?? []) {
|
|
110
|
+
const trimmed = String(sourceFile ?? "").trim();
|
|
111
|
+
if (!trimmed)
|
|
112
|
+
continue;
|
|
113
|
+
const candidateAbsolute = path.resolve(path.isAbsolute(trimmed)
|
|
114
|
+
? trimmed
|
|
115
|
+
: path.join(normalizedRoot, trimmed));
|
|
116
|
+
const relative = path.relative(normalizedRoot, candidateAbsolute);
|
|
117
|
+
const repoRelative = !relative.startsWith("..") && !path.isAbsolute(relative)
|
|
118
|
+
? relative
|
|
119
|
+
: trimmed;
|
|
120
|
+
const normalizedPath = repoRelative
|
|
121
|
+
.split(path.sep)
|
|
122
|
+
.join("/")
|
|
123
|
+
.replace(/^\.\//, "")
|
|
124
|
+
.replace(/^\//, "");
|
|
125
|
+
if (!normalizedPath || seen.has(normalizedPath))
|
|
126
|
+
continue;
|
|
127
|
+
seen.add(normalizedPath);
|
|
128
|
+
normalized.push(normalizedPath);
|
|
129
|
+
}
|
|
130
|
+
return normalized;
|
|
131
|
+
}
|
|
132
|
+
function isAllowedType(type) {
|
|
133
|
+
return ALLOWED_TYPES.includes(type);
|
|
134
|
+
}
|
|
135
|
+
function stripOuterSingleQuotes(value) {
|
|
136
|
+
return value.startsWith("'") && value.endsWith("'") && value.length >= 2
|
|
137
|
+
? value.slice(1, -1)
|
|
138
|
+
: value;
|
|
139
|
+
}
|
|
140
|
+
function candidateKey(entity) {
|
|
141
|
+
return `${String(entity.type ?? "")}::${String(entity.id ?? "")}`;
|
|
142
|
+
}
|
|
143
|
+
function normalizeEntity(entity) {
|
|
144
|
+
const type = stripOuterSingleQuotes(String(entity.type ?? "").trim());
|
|
145
|
+
if (!isAllowedType(type))
|
|
146
|
+
return null;
|
|
147
|
+
return {
|
|
148
|
+
...entity,
|
|
149
|
+
id: String(entity.id ?? "").trim(),
|
|
150
|
+
type,
|
|
151
|
+
title: String(entity.title ?? "").trim(),
|
|
152
|
+
status: String(entity.status ?? "").trim(),
|
|
153
|
+
source: entity.source ? String(entity.source).trim().split(path.sep).join("/") : undefined,
|
|
154
|
+
textRef: entity.textRef
|
|
155
|
+
? String(entity.textRef).trim()
|
|
156
|
+
: entity.text_ref
|
|
157
|
+
? String(entity.text_ref).trim()
|
|
158
|
+
: undefined,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function addCandidate(candidates, entity, scoreDelta, reason) {
|
|
162
|
+
const normalizedEntity = normalizeEntity(entity);
|
|
163
|
+
if (!normalizedEntity)
|
|
164
|
+
return;
|
|
165
|
+
const key = candidateKey(normalizedEntity);
|
|
166
|
+
const existing = candidates.get(key);
|
|
167
|
+
if (existing) {
|
|
168
|
+
existing.score += scoreDelta;
|
|
169
|
+
if (!existing.reasons.includes(reason)) {
|
|
170
|
+
existing.reasons.push(reason);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
candidates.set(key, {
|
|
175
|
+
entity: normalizedEntity,
|
|
176
|
+
score: scoreDelta,
|
|
177
|
+
reasons: [reason],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function toFreshness(statusPayload) {
|
|
181
|
+
const syncState = String(statusPayload.syncState ?? "unknown");
|
|
182
|
+
if (statusPayload.dirty || syncState === "stale") {
|
|
183
|
+
return {
|
|
184
|
+
state: "stale",
|
|
185
|
+
syncState,
|
|
186
|
+
dirty: Boolean(statusPayload.dirty),
|
|
187
|
+
syncedAt: statusPayload.syncedAt ?? null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (syncState === "fresh") {
|
|
191
|
+
return {
|
|
192
|
+
state: "fresh",
|
|
193
|
+
syncState,
|
|
194
|
+
dirty: Boolean(statusPayload.dirty),
|
|
195
|
+
syncedAt: statusPayload.syncedAt ?? null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
state: "unknown",
|
|
200
|
+
syncState,
|
|
201
|
+
dirty: Boolean(statusPayload.dirty),
|
|
202
|
+
syncedAt: statusPayload.syncedAt ?? null,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function summarizeCandidateReason(reasons) {
|
|
206
|
+
return reasons.join(", ");
|
|
207
|
+
}
|
|
208
|
+
function sortedEntities(candidates) {
|
|
209
|
+
return Array.from(candidates.values())
|
|
210
|
+
.map(({ entity, score, reasons }) => {
|
|
211
|
+
const type = String(entity.type);
|
|
212
|
+
const typeBonus = TYPE_PRIORITY[type] ?? 0;
|
|
213
|
+
return {
|
|
214
|
+
id: String(entity.id ?? ""),
|
|
215
|
+
type,
|
|
216
|
+
title: String(entity.title ?? ""),
|
|
217
|
+
status: String(entity.status ?? ""),
|
|
218
|
+
...(entity.source ? { source: String(entity.source) } : {}),
|
|
219
|
+
...(entity.textRef ? { textRef: String(entity.textRef) } : {}),
|
|
220
|
+
score: score + typeBonus,
|
|
221
|
+
reason: summarizeCandidateReason(reasons),
|
|
222
|
+
};
|
|
223
|
+
})
|
|
224
|
+
.sort((left, right) => {
|
|
225
|
+
if (right.score !== left.score) {
|
|
226
|
+
return right.score - left.score;
|
|
227
|
+
}
|
|
228
|
+
const leftPriority = TYPE_PRIORITY[left.type] ?? 0;
|
|
229
|
+
const rightPriority = TYPE_PRIORITY[right.type] ?? 0;
|
|
230
|
+
if (rightPriority !== leftPriority) {
|
|
231
|
+
return rightPriority - leftPriority;
|
|
232
|
+
}
|
|
233
|
+
return left.id.localeCompare(right.id);
|
|
234
|
+
})
|
|
235
|
+
.slice(0, 8);
|
|
236
|
+
}
|
|
237
|
+
function selectCitationIds(entities, predicate) {
|
|
238
|
+
return entities.filter(predicate).map((entity) => entity.id);
|
|
239
|
+
}
|
|
240
|
+
function asSearchableText(entity) {
|
|
241
|
+
return `${entity.title} ${entity.source ?? ""} ${entity.textRef ?? ""}`.toLowerCase();
|
|
242
|
+
}
|
|
243
|
+
function buildConstraints(entities) {
|
|
244
|
+
const statements = [];
|
|
245
|
+
const adrCitationIds = selectCitationIds(entities, (entity) => entity.type === "adr" &&
|
|
246
|
+
asSearchableText(entity).includes("read-only") &&
|
|
247
|
+
asSearchableText(entity).includes("mcp"));
|
|
248
|
+
if (adrCitationIds.length > 0) {
|
|
249
|
+
statements.push({
|
|
250
|
+
statement: "Keep the briefing generator read-only and MCP-owned.",
|
|
251
|
+
citationIds: adrCitationIds,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const deterministicCitationIds = selectCitationIds(entities, (entity) => {
|
|
255
|
+
if (entity.type !== "req" && entity.type !== "test")
|
|
256
|
+
return false;
|
|
257
|
+
const searchable = asSearchableText(entity);
|
|
258
|
+
return searchable.includes("deterministic") || searchable.includes("citation");
|
|
259
|
+
});
|
|
260
|
+
if (deterministicCitationIds.length > 0) {
|
|
261
|
+
statements.push({
|
|
262
|
+
statement: "Return deterministic, citation-backed start-task output.",
|
|
263
|
+
citationIds: deterministicCitationIds,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return statements;
|
|
267
|
+
}
|
|
268
|
+
function buildRegressionRisks(entities) {
|
|
269
|
+
const statements = [];
|
|
270
|
+
const orderingCitationIds = selectCitationIds(entities, (entity) => entity.type === "test" &&
|
|
271
|
+
(asSearchableText(entity).includes("deterministic") ||
|
|
272
|
+
asSearchableText(entity).includes("briefing output")));
|
|
273
|
+
if (orderingCitationIds.length > 0) {
|
|
274
|
+
statements.push({
|
|
275
|
+
statement: "Do not let repeated calls change entity, citation, or prompt ordering.",
|
|
276
|
+
citationIds: orderingCitationIds,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const budgetCitationIds = selectCitationIds(entities, (entity) => entity.type === "fact" &&
|
|
280
|
+
(asSearchableText(entity).includes("prompt") ||
|
|
281
|
+
asSearchableText(entity).includes("budget")));
|
|
282
|
+
if (budgetCitationIds.length > 0) {
|
|
283
|
+
statements.push({
|
|
284
|
+
statement: "Do not exceed the OpenCode prompt budget.",
|
|
285
|
+
citationIds: budgetCitationIds,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return statements;
|
|
289
|
+
}
|
|
290
|
+
function buildMissingEvidence(_entities) {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
function bulletForEntity(entity) {
|
|
294
|
+
const searchable = asSearchableText(entity);
|
|
295
|
+
if (entity.type === "req" &&
|
|
296
|
+
searchable.includes("deterministic") &&
|
|
297
|
+
searchable.includes("citation")) {
|
|
298
|
+
return `- ${entity.id}: Keep start-task briefings deterministic and citation-backed.`;
|
|
299
|
+
}
|
|
300
|
+
if (entity.type === "adr" &&
|
|
301
|
+
searchable.includes("read-only") &&
|
|
302
|
+
searchable.includes("mcp")) {
|
|
303
|
+
return `- ${entity.id}: Keep the MCP tool read-only; do not repair or mutate the workspace.`;
|
|
304
|
+
}
|
|
305
|
+
if (entity.type === "test" &&
|
|
306
|
+
(searchable.includes("deterministic") || searchable.includes("briefing output"))) {
|
|
307
|
+
return `- ${entity.id}: Repeated calls must preserve entity, citation, and prompt ordering.`;
|
|
308
|
+
}
|
|
309
|
+
if (entity.type === "fact" &&
|
|
310
|
+
(searchable.includes("prompt") || searchable.includes("budget"))) {
|
|
311
|
+
return `- ${entity.id}: Keep the prompt block within 120 words and 5 bullets.`;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
function buildPromptBlock(entities) {
|
|
316
|
+
const bullets = entities
|
|
317
|
+
.map((entity) => bulletForEntity(entity))
|
|
318
|
+
.filter((bullet) => bullet !== null)
|
|
319
|
+
.slice(0, 5);
|
|
320
|
+
const promptBlock = bullets.join("\n");
|
|
321
|
+
const words = promptBlock.split(/\s+/).filter(Boolean);
|
|
322
|
+
if (bullets.length > 5 || words.length > 120) {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
return promptBlock;
|
|
326
|
+
}
|
|
327
|
+
function buildCitations(entities) {
|
|
328
|
+
return entities.map((entity) => ({
|
|
329
|
+
id: entity.id,
|
|
330
|
+
type: entity.type,
|
|
331
|
+
title: entity.title,
|
|
332
|
+
...(entity.source ? { source: entity.source } : {}),
|
|
333
|
+
...(entity.textRef ? { textRef: entity.textRef } : {}),
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
function roundScore(score) {
|
|
337
|
+
return Math.max(0, Math.min(1, Math.round(score * 100) / 100));
|
|
338
|
+
}
|
|
339
|
+
function buildConfidence(activationState, freshness, entities, missingEvidence, promptBlock) {
|
|
340
|
+
const reasons = [];
|
|
341
|
+
let score = entities.length > 0 ? 0.82 : 0.35;
|
|
342
|
+
if (activationState === "root_active_seeded") {
|
|
343
|
+
score += 0.13;
|
|
344
|
+
reasons.push("Seeded workspace provides broad KB evidence.");
|
|
345
|
+
}
|
|
346
|
+
else if (activationState === "root_active_thin") {
|
|
347
|
+
score -= 0.2;
|
|
348
|
+
reasons.push("Thin workspace reduces available evidence.");
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
score -= 0.4;
|
|
352
|
+
reasons.push("Workspace posture does not support reliable briefing generation.");
|
|
353
|
+
}
|
|
354
|
+
if (freshness.state !== "fresh") {
|
|
355
|
+
score -= 0.25;
|
|
356
|
+
reasons.push("KB freshness is not clean enough for a ready briefing.");
|
|
357
|
+
}
|
|
358
|
+
if (freshness.dirty) {
|
|
359
|
+
score -= 0.1;
|
|
360
|
+
reasons.push("Workspace is dirty, so citations may be stale.");
|
|
361
|
+
}
|
|
362
|
+
if (missingEvidence.length > 0) {
|
|
363
|
+
score -= 0.15;
|
|
364
|
+
reasons.push("Some briefing claims are missing supporting evidence.");
|
|
365
|
+
}
|
|
366
|
+
if (!promptBlock) {
|
|
367
|
+
score -= 0.1;
|
|
368
|
+
reasons.push("Prompt block could not be assembled within the prompt budget.");
|
|
369
|
+
}
|
|
370
|
+
const rounded = roundScore(score);
|
|
371
|
+
return {
|
|
372
|
+
score: rounded,
|
|
373
|
+
level: rounded >= 0.8 ? "high" : rounded >= 0.55 ? "medium" : "low",
|
|
374
|
+
reasons,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async function expandGraphNeighbors(prolog, seedIds) {
|
|
378
|
+
if (seedIds.length === 0) {
|
|
379
|
+
return new Map();
|
|
380
|
+
}
|
|
381
|
+
const payload = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:graph_expand_json(${toPrologList(seedIds)}, ${toPrologList(GRAPH_RELATIONSHIPS)}, 'both', 1, [], 200, 500, JsonString)`, "Briefing graph expansion");
|
|
382
|
+
const seedSet = new Set(seedIds);
|
|
383
|
+
const connected = new Set();
|
|
384
|
+
for (const edge of payload.edges ?? []) {
|
|
385
|
+
const from = String(edge.from ?? "");
|
|
386
|
+
const to = String(edge.to ?? "");
|
|
387
|
+
if (seedSet.has(from) && to)
|
|
388
|
+
connected.add(to);
|
|
389
|
+
if (seedSet.has(to) && from)
|
|
390
|
+
connected.add(from);
|
|
391
|
+
}
|
|
392
|
+
const neighbors = new Map();
|
|
393
|
+
for (const node of payload.nodes ?? []) {
|
|
394
|
+
const normalized = normalizeEntity(node);
|
|
395
|
+
if (!normalized)
|
|
396
|
+
continue;
|
|
397
|
+
const nodeId = String(normalized.id ?? "");
|
|
398
|
+
if (seedSet.has(nodeId) || !connected.has(nodeId))
|
|
399
|
+
continue;
|
|
400
|
+
neighbors.set(nodeId, normalized);
|
|
401
|
+
}
|
|
402
|
+
return neighbors;
|
|
403
|
+
}
|
|
404
|
+
async function loadByIds(prolog, ids) {
|
|
405
|
+
const groups = await Promise.all(ids.map((id) => loadEntities(prolog, { id })));
|
|
406
|
+
return groups.flat();
|
|
407
|
+
}
|
|
408
|
+
async function loadBySourceFiles(prolog, sourceFiles) {
|
|
409
|
+
const groups = await Promise.all(sourceFiles.map((sourceFile) => loadEntities(prolog, { sourceFile })));
|
|
410
|
+
return groups.flat();
|
|
411
|
+
}
|
|
412
|
+
function buildTldr(briefingState, entities) {
|
|
413
|
+
if (briefingState === "ready") {
|
|
414
|
+
const citedIds = entities.slice(0, 4).map((entity) => entity.id).join(", ");
|
|
415
|
+
return `Ready briefing assembled from ${entities.length} cited entities: ${citedIds}.`;
|
|
416
|
+
}
|
|
417
|
+
return "No reliable briefing is available from the current workspace posture and freshness state.";
|
|
418
|
+
}
|
|
419
|
+
export async function handleKbBriefingGenerate(// implements REQ-mcp-kibi-briefing-v1
|
|
420
|
+
prolog, args) {
|
|
421
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
422
|
+
const taskText = normalizeTaskText(args.taskText);
|
|
423
|
+
const sourceFiles = normalizeSourceFiles(workspaceRoot, args.sourceFiles);
|
|
424
|
+
const seedIds = normalizeSeedIds(args.seedIds);
|
|
425
|
+
if (!taskText && sourceFiles.length === 0 && seedIds.length === 0) {
|
|
426
|
+
throw new Error("Briefing generation failed: at least one of taskText, sourceFiles, or seedIds must be provided");
|
|
427
|
+
}
|
|
428
|
+
const useTextOnlyFastPath = taskText.length > 0 && sourceFiles.length === 0 && seedIds.length === 0;
|
|
429
|
+
const activationState = useTextOnlyFastPath
|
|
430
|
+
? inferTextOnlyActivationState(workspaceRoot)
|
|
431
|
+
: await classifyActivationState(workspaceRoot, prolog);
|
|
432
|
+
const activationReason = activationReasonFor(activationState);
|
|
433
|
+
const freshness = useTextOnlyFastPath
|
|
434
|
+
? unknownFreshness()
|
|
435
|
+
: toFreshness((await handleKbStatus(prolog, {})).structuredContent);
|
|
436
|
+
if (activationState === "root_uninitialized" ||
|
|
437
|
+
activationState === "root_partial" ||
|
|
438
|
+
activationState === "vendored_only" ||
|
|
439
|
+
freshness.state === "stale") {
|
|
440
|
+
const confidence = buildConfidence(activationState, freshness, [], [], "");
|
|
441
|
+
return {
|
|
442
|
+
content: [{ type: "text", text: "No briefing is available." }],
|
|
443
|
+
structuredContent: {
|
|
444
|
+
briefingState: "no_briefing",
|
|
445
|
+
activationState,
|
|
446
|
+
activationReason,
|
|
447
|
+
freshness,
|
|
448
|
+
confidence,
|
|
449
|
+
tldr: buildTldr("no_briefing", []),
|
|
450
|
+
promptBlock: "",
|
|
451
|
+
entities: [],
|
|
452
|
+
constraints: [],
|
|
453
|
+
regressionRisks: [],
|
|
454
|
+
missingEvidence: [],
|
|
455
|
+
citations: [],
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const candidates = new Map();
|
|
460
|
+
for (const entity of await loadByIds(prolog, seedIds)) {
|
|
461
|
+
addCandidate(candidates, entity, 100, "seed hit");
|
|
462
|
+
}
|
|
463
|
+
for (const entity of await loadBySourceFiles(prolog, sourceFiles)) {
|
|
464
|
+
addCandidate(candidates, entity, 90, "source-file hit");
|
|
465
|
+
}
|
|
466
|
+
const rankedIds = [];
|
|
467
|
+
if (taskText) {
|
|
468
|
+
const allEntities = (await loadEntities(prolog, {})).filter((entity) => isAllowedType(String(entity.type ?? "")));
|
|
469
|
+
const matches = await rankEntities(allEntities, taskText, workspaceRoot);
|
|
470
|
+
matches.forEach((match, index) => {
|
|
471
|
+
rankedIds.push(String(match.entity.id ?? ""));
|
|
472
|
+
addCandidate(candidates, match.entity, 70 - index, `text-search hit (#${index + 1})`);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const graphSeeds = Array.from(new Set([
|
|
476
|
+
...seedIds,
|
|
477
|
+
...sourceFiles.flatMap(() => []),
|
|
478
|
+
...Array.from(candidates.values()).map((candidate) => String(candidate.entity.id ?? "")),
|
|
479
|
+
...rankedIds,
|
|
480
|
+
].filter(Boolean)));
|
|
481
|
+
const graphNeighbors = await expandGraphNeighbors(prolog, graphSeeds);
|
|
482
|
+
for (const neighbor of graphNeighbors.values()) {
|
|
483
|
+
addCandidate(candidates, neighbor, 40, "graph neighbor");
|
|
484
|
+
}
|
|
485
|
+
const entities = sortedEntities(candidates);
|
|
486
|
+
const constraints = buildConstraints(entities);
|
|
487
|
+
const regressionRisks = buildRegressionRisks(entities);
|
|
488
|
+
const missingEvidence = buildMissingEvidence(entities);
|
|
489
|
+
const promptBlock = buildPromptBlock(entities);
|
|
490
|
+
const citations = buildCitations(entities);
|
|
491
|
+
const confidence = buildConfidence(activationState, freshness, entities, missingEvidence, promptBlock);
|
|
492
|
+
const briefingState = confidence.score >= 0.55 ? "ready" : "no_briefing";
|
|
493
|
+
if (briefingState === "no_briefing") {
|
|
494
|
+
return {
|
|
495
|
+
content: [{ type: "text", text: "No briefing is available." }],
|
|
496
|
+
structuredContent: {
|
|
497
|
+
briefingState,
|
|
498
|
+
activationState,
|
|
499
|
+
activationReason,
|
|
500
|
+
freshness,
|
|
501
|
+
confidence,
|
|
502
|
+
tldr: buildTldr("no_briefing", []),
|
|
503
|
+
promptBlock: "",
|
|
504
|
+
entities: [],
|
|
505
|
+
constraints: [],
|
|
506
|
+
regressionRisks: [],
|
|
507
|
+
missingEvidence: [],
|
|
508
|
+
citations: [],
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
content: [{ type: "text", text: `Briefing ready with ${entities.length} cited entities.` }],
|
|
514
|
+
structuredContent: {
|
|
515
|
+
briefingState,
|
|
516
|
+
activationState,
|
|
517
|
+
activationReason,
|
|
518
|
+
freshness,
|
|
519
|
+
confidence,
|
|
520
|
+
tldr: buildTldr(briefingState, entities),
|
|
521
|
+
promptBlock,
|
|
522
|
+
entities,
|
|
523
|
+
constraints,
|
|
524
|
+
regressionRisks,
|
|
525
|
+
missingEvidence,
|
|
526
|
+
citations,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
package/dist/tools-config.js
CHANGED
|
@@ -377,9 +377,10 @@ const BASE_TOOLS = [
|
|
|
377
377
|
"deprecated-adr-no-successor",
|
|
378
378
|
"domain-contradictions",
|
|
379
379
|
"strict-fact-shape",
|
|
380
|
+
"strict-req-fact-pairing",
|
|
380
381
|
],
|
|
381
382
|
},
|
|
382
|
-
description: "Optional rule subset. Allowed: must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, no-cycles, required-fields, deprecated-adr-no-successor, domain-contradictions, strict-fact-shape. If omitted, server runs all.",
|
|
383
|
+
description: "Optional rule subset. Allowed: must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, no-cycles, required-fields, deprecated-adr-no-successor, domain-contradictions, strict-fact-shape, strict-req-fact-pairing. If omitted, server runs all.",
|
|
383
384
|
},
|
|
384
385
|
},
|
|
385
386
|
},
|
|
@@ -419,6 +420,29 @@ const BASE_TOOLS = [
|
|
|
419
420
|
},
|
|
420
421
|
},
|
|
421
422
|
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "kb_briefing_generate",
|
|
426
|
+
description: "Generate a deterministic, read-only, start-task briefing from task text, source files, and seed IDs. No mutation side effects.",
|
|
427
|
+
inputSchema: {
|
|
428
|
+
type: "object",
|
|
429
|
+
properties: {
|
|
430
|
+
taskText: {
|
|
431
|
+
type: "string",
|
|
432
|
+
description: "Optional task description used to rank relevant cited entities for the briefing.",
|
|
433
|
+
},
|
|
434
|
+
sourceFiles: {
|
|
435
|
+
type: "array",
|
|
436
|
+
items: { type: "string" },
|
|
437
|
+
description: "Optional source-file paths used to gather cited entities for the briefing.",
|
|
438
|
+
},
|
|
439
|
+
seedIds: {
|
|
440
|
+
type: "array",
|
|
441
|
+
items: { type: "string" },
|
|
442
|
+
description: "Optional seed entity IDs used to anchor the briefing graph expansion.",
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
422
446
|
}
|
|
423
447
|
];
|
|
424
448
|
/**
|