kibi-mcp 0.7.0 → 0.8.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/bin/kibi-mcp +39 -11
- package/dist/server/docs.js +31 -45
- package/dist/server/tools.js +6 -0
- package/dist/tools/autopilot-candidates.js +229 -0
- package/dist/tools/autopilot-discovery.js +243 -0
- package/dist/tools/autopilot-generate.js +271 -0
- package/dist/tools-config.js +36 -0
- package/package.json +7 -4
package/bin/kibi-mcp
CHANGED
|
@@ -16,9 +16,36 @@
|
|
|
16
16
|
You should have received a copy of the GNU Affero General Public License
|
|
17
17
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
const args = globalThis.Bun?.argv?.slice(2) ?? process.argv.slice(2);
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
function printHelp() {
|
|
22
|
+
process.stdout.write(`Usage: kibi-mcp [options]
|
|
23
|
+
|
|
24
|
+
Starts the Kibi MCP server over stdio.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--diagnostic-mode enable diagnostic logging to .kb/usage.log
|
|
28
|
+
-h, --help show help
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
33
|
+
printHelp();
|
|
34
|
+
process.exitCode = 0;
|
|
35
|
+
} else {
|
|
36
|
+
if (args.includes("--diagnostic-mode") && !process.argv.includes("--diagnostic-mode")) {
|
|
37
|
+
process.argv.push("--diagnostic-mode");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (args.includes("--diagnostic-mode")) {
|
|
41
|
+
globalThis.__KIBI_MCP_DIAGNOSTIC_MODE = true;
|
|
42
|
+
process.env.KIBI_WORKSPACE = process.cwd();
|
|
43
|
+
process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { startServer } = await import("../dist/server.js");
|
|
47
|
+
|
|
48
|
+
if (process.env.KIBI_MCP_DEBUG) {
|
|
22
49
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
23
50
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
24
51
|
|
|
@@ -36,24 +63,25 @@ if (process.env.KIBI_MCP_DEBUG) {
|
|
|
36
63
|
originalStderrWrite(`[KIBI-MCP-IN] ${str}\n`);
|
|
37
64
|
}
|
|
38
65
|
});
|
|
39
|
-
}
|
|
66
|
+
}
|
|
40
67
|
|
|
41
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
68
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
42
69
|
console.error('[KIBI-MCP] Unhandled rejection at promise:', promise);
|
|
43
70
|
console.error('[KIBI-MCP] Reason:', reason);
|
|
44
71
|
if (reason instanceof Error) {
|
|
45
72
|
console.error('[KIBI-MCP] Stack:', reason.stack);
|
|
46
73
|
}
|
|
47
74
|
process.exit(1);
|
|
48
|
-
});
|
|
75
|
+
});
|
|
49
76
|
|
|
50
|
-
process.on('uncaughtException', (error) => {
|
|
77
|
+
process.on('uncaughtException', (error) => {
|
|
51
78
|
console.error('[KIBI-MCP] Uncaught exception:', error.message);
|
|
52
79
|
console.error('[KIBI-MCP] Stack:', error.stack);
|
|
53
80
|
process.exit(1);
|
|
54
|
-
});
|
|
81
|
+
});
|
|
55
82
|
|
|
56
|
-
startServer().catch((error) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
83
|
+
startServer().catch((error) => {
|
|
84
|
+
console.error("Fatal error:", error);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
87
|
+
}
|
package/dist/server/docs.js
CHANGED
|
@@ -42,63 +42,49 @@ export const PROMPTS = [
|
|
|
42
42
|
// implements REQ-002, REQ-013, REQ-mcp-search-discovery
|
|
43
43
|
{
|
|
44
44
|
name: "init-kibi",
|
|
45
|
-
description: "
|
|
45
|
+
description: "Activation workflow to populate a new or empty Kibi KB from an existing repository.",
|
|
46
46
|
text: [
|
|
47
|
-
"# Kibi
|
|
47
|
+
"# Kibi Activation Workflow",
|
|
48
48
|
"",
|
|
49
|
-
"Use this workflow to
|
|
49
|
+
"Use this workflow to populate a Kibi knowledge base when it is new or empty.",
|
|
50
50
|
"",
|
|
51
|
-
"##
|
|
52
|
-
"1. Scan project structure to identify:",
|
|
53
|
-
" - Requirements (docs/requirements/, README, specs)",
|
|
54
|
-
" - Tests (unit, integration, e2e)",
|
|
55
|
-
" - Architecture decisions (docs/adr/, ARCHITECTURE.md)",
|
|
56
|
-
" - Feature flags (environment files, config)",
|
|
57
|
-
" - Events (domain events, pub/sub topics)",
|
|
58
|
-
" - Core symbols (key functions, classes, modules)",
|
|
51
|
+
"## Step 1: Generate Candidates (read-only)",
|
|
59
52
|
"",
|
|
60
|
-
"
|
|
61
|
-
"1. Identify atomic domain facts (invariants, constraints, properties)",
|
|
62
|
-
"2. Create reusable fact entities with `kb_upsert` using type 'fact'",
|
|
63
|
-
"3. Use consistent IDs: FACT-XXX with descriptive titles",
|
|
53
|
+
"Call `kb_autopilot_generate` to scan the repository and produce candidate entities.",
|
|
64
54
|
"",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"5. Reuse fact IDs across related requirements for contradiction detection",
|
|
55
|
+
"This tool is **read-only** — it never writes to the KB. It returns:",
|
|
56
|
+
"- `activationState`: the current KB state (e.g. `root_uninitialized`, `root_partial`)",
|
|
57
|
+
"- `candidates[]`: proposed entities with confidence scores and evidence",
|
|
58
|
+
"- `suppressedCandidates[]`: candidates suppressed due to duplicates, existing entities, or shadowed by typed sources",
|
|
59
|
+
"- `discoverySummary` / `payoffSummary`: context for agent review",
|
|
71
60
|
"",
|
|
72
|
-
"##
|
|
73
|
-
"1. Map existing tests to requirements they verify",
|
|
74
|
-
"2. Create test entities with `kb_upsert` using type 'test'",
|
|
75
|
-
"3. Link tests to requirements using `verified_by` relationship",
|
|
61
|
+
"## Step 2: Review Candidates",
|
|
76
62
|
"",
|
|
77
|
-
"
|
|
78
|
-
"1. Extract ADRs from docs/adr/ or decision records",
|
|
79
|
-
"2. Create adr entities with `kb_upsert` using type 'adr'",
|
|
80
|
-
"3. Link ADRs to symbols they constrain using `constrained_by`",
|
|
63
|
+
"Inspect `activationState`. If `applyBlocked` is true, stop — the KB cannot accept writes.",
|
|
81
64
|
"",
|
|
82
|
-
"
|
|
83
|
-
"1
|
|
84
|
-
"
|
|
85
|
-
"3. Link symbols that publish/consume events using `publishes`/`consumes`",
|
|
65
|
+
"For each candidate, evaluate:",
|
|
66
|
+
"- **confidence** (0–1): prefer high-confidence entities first",
|
|
67
|
+
"- **evidence**: verify the source reference is real before applying",
|
|
86
68
|
"",
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
69
|
+
"Discard or edit candidates that look speculative. The agent decides what to apply — the generator never writes.", "",
|
|
70
|
+
"## Step 3: Apply Approved Candidates", "",
|
|
71
|
+
"Apply approved candidates by executing each candidate.applyPlan sequentially:",
|
|
72
|
+
"1. For each approved candidate, run its `candidate.applyPlan` steps in ascending phase order and keep the candidate sequence deterministic",
|
|
73
|
+
"2. Execute each step with `kb_upsert` using the step's provided args, and confirm success before moving to the next step",
|
|
74
|
+
"3. After each batch, call `kb_check` with targeted rules (`required-fields`, `no-dangling-refs`) to catch issues early",
|
|
91
75
|
"",
|
|
92
|
-
"##
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
76
|
+
"## Step 4: Payoff Verification",
|
|
77
|
+
"",
|
|
78
|
+
"After all approved candidates are applied, verify the result:",
|
|
79
|
+
"- `kb_check` with all rules — must return zero violations",
|
|
80
|
+
"- `kb_find_gaps` with `{ type: 'req', missingRelationships: ['specified_by', 'verified_by'] }` — identify under-linked requirements",
|
|
81
|
+
"- `kb_coverage` with `{ by: 'req' }` — confirm traceability coverage", "",
|
|
82
|
+
"## Doc Hygiene",
|
|
96
83
|
"",
|
|
97
|
-
"## Best Practices",
|
|
98
|
-
"- Start with high-value entities first (critical requirements, security constraints)",
|
|
99
|
-
"- Use incremental batches to avoid overwhelming the KB",
|
|
100
84
|
"- Always call `kb_query` before creating to avoid duplicate entities",
|
|
101
|
-
"- Run `kb_check` after each batch
|
|
85
|
+
"- Run `kb_check` after each batch, not just at the end",
|
|
86
|
+
"- All writes go through `kb_upsert` — do not invoke CLI commands directly",
|
|
87
|
+
"- `kb_autopilot_generate` is read-only; only `kb_upsert` mutates the KB",
|
|
102
88
|
].join("\n"),
|
|
103
89
|
},
|
|
104
90
|
{
|
package/dist/server/tools.js
CHANGED
|
@@ -11,6 +11,7 @@ import { handleKbQuery } from "../tools/query.js";
|
|
|
11
11
|
import { handleKbSearch } from "../tools/search.js";
|
|
12
12
|
import { handleKbStatus } from "../tools/status.js";
|
|
13
13
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
14
|
+
import { handleKbAutopilotGenerate, } from "../tools/autopilot-generate.js";
|
|
14
15
|
const defaultToolsServerDeps = {
|
|
15
16
|
getSessionModule: () => import("./session.js"),
|
|
16
17
|
};
|
|
@@ -57,6 +58,7 @@ const DEFAULT_TOOLS_RUNTIME = {
|
|
|
57
58
|
handleKbSearch,
|
|
58
59
|
handleKbStatus,
|
|
59
60
|
handleKbUpsert,
|
|
61
|
+
handleKbAutopilotGenerate,
|
|
60
62
|
};
|
|
61
63
|
// implements REQ-008
|
|
62
64
|
function debugLog(...args) {
|
|
@@ -317,4 +319,8 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
317
319
|
const prolog = await runtime.ensureProlog();
|
|
318
320
|
return runtime.handleKbCheck(prolog, args);
|
|
319
321
|
}, runtime);
|
|
322
|
+
addTool(server, "kb_autopilot_generate", toolDef("kb_autopilot_generate").description, toolDef("kb_autopilot_generate").inputSchema, async (args) => {
|
|
323
|
+
const prolog = await runtime.ensureProlog();
|
|
324
|
+
return runtime.handleKbAutopilotGenerate(prolog, args);
|
|
325
|
+
}, runtime);
|
|
320
326
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Kibi — autopilot candidate builders
|
|
2
|
+
// Implements candidate assembly from public CLI extractors
|
|
3
|
+
import { extractFromManifest } from "kibi-cli/extractors/manifest";
|
|
4
|
+
import { extractFromMarkdown } from "kibi-cli/extractors/markdown";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
function resolveCandidatePaths(filePath, workspaceRoot) {
|
|
8
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
9
|
+
? filePath
|
|
10
|
+
: path.resolve(workspaceRoot, filePath);
|
|
11
|
+
const relativePath = (path.relative(workspaceRoot, absolutePath) || filePath)
|
|
12
|
+
.split(path.sep)
|
|
13
|
+
.join("/");
|
|
14
|
+
return { absolutePath, relativePath };
|
|
15
|
+
}
|
|
16
|
+
function isIgnoredGenericMarkdownPath(relativePath) {
|
|
17
|
+
const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
18
|
+
return /(^|\/)(documentation|\.kb|\.git|node_modules|vendor|vendors|third_party|third-party|dist|coverage)(\/|$)/.test(normalized);
|
|
19
|
+
}
|
|
20
|
+
function buildUpsertFromExtraction(er, typeOverride) {
|
|
21
|
+
const ent = er.entity;
|
|
22
|
+
const type = typeOverride ?? String(ent.type ?? "");
|
|
23
|
+
const id = String(ent.id ?? "");
|
|
24
|
+
// Copy properties except `type` to avoid delete
|
|
25
|
+
const properties = {};
|
|
26
|
+
for (const [k, v] of Object.entries(ent)) {
|
|
27
|
+
if (k === "type")
|
|
28
|
+
continue;
|
|
29
|
+
properties[k] = v;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
type,
|
|
33
|
+
id,
|
|
34
|
+
properties,
|
|
35
|
+
relationships: er.relationships ?? [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// implements REQ-mcp-init-kibi-autopilot-v1
|
|
39
|
+
export function buildTypedMarkdownCandidates(discoveryResult, existingEntities) {
|
|
40
|
+
const candidates = [];
|
|
41
|
+
const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
|
|
42
|
+
for (const filePath of discoveryResult.markdownFiles || []) {
|
|
43
|
+
try {
|
|
44
|
+
const extraction = extractFromMarkdown(filePath);
|
|
45
|
+
const { entity, relationships } = extraction;
|
|
46
|
+
if (existingEntities.ids.has(entity.id))
|
|
47
|
+
continue;
|
|
48
|
+
const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
|
|
49
|
+
const candidateId = `md:${relativePath}:${entity.id}`;
|
|
50
|
+
const upsert = buildUpsertFromExtraction({ entity, relationships });
|
|
51
|
+
candidates.push({
|
|
52
|
+
candidateId,
|
|
53
|
+
entityType: entity.type,
|
|
54
|
+
title: entity.title,
|
|
55
|
+
sourceKind: "typed_markdown",
|
|
56
|
+
sourcePath: absolutePath,
|
|
57
|
+
confidence: 1.0,
|
|
58
|
+
confidenceBand: "high",
|
|
59
|
+
evidence: [
|
|
60
|
+
`extracted_from_markdown:${relativePath}`,
|
|
61
|
+
`entity_id:${entity.id}`,
|
|
62
|
+
],
|
|
63
|
+
relationships: relationships || [],
|
|
64
|
+
applyPlan: [upsert],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// skip files that fail extraction
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return candidates;
|
|
72
|
+
}
|
|
73
|
+
// implements REQ-mcp-init-kibi-autopilot-v1
|
|
74
|
+
export function buildSymbolManifestCandidates(discoveryResult, existingEntities) {
|
|
75
|
+
const candidates = [];
|
|
76
|
+
const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
|
|
77
|
+
for (const filePath of discoveryResult.manifestFiles || []) {
|
|
78
|
+
try {
|
|
79
|
+
const results = extractFromManifest(filePath);
|
|
80
|
+
for (const res of results) {
|
|
81
|
+
const entity = res.entity;
|
|
82
|
+
const relationships = res.relationships || [];
|
|
83
|
+
if (existingEntities.ids.has(entity.id))
|
|
84
|
+
continue;
|
|
85
|
+
const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
|
|
86
|
+
const candidateId = `mf:${relativePath}:${entity.id}`;
|
|
87
|
+
const upsert = buildUpsertFromExtraction({ entity, relationships });
|
|
88
|
+
candidates.push({
|
|
89
|
+
candidateId,
|
|
90
|
+
entityType: entity.type,
|
|
91
|
+
title: entity.title,
|
|
92
|
+
sourceKind: "symbol_manifest",
|
|
93
|
+
sourcePath: absolutePath,
|
|
94
|
+
confidence: 0.98,
|
|
95
|
+
confidenceBand: "high",
|
|
96
|
+
evidence: [
|
|
97
|
+
`extracted_from_manifest:${relativePath}`,
|
|
98
|
+
`entity_id:${entity.id}`,
|
|
99
|
+
],
|
|
100
|
+
relationships,
|
|
101
|
+
applyPlan: [upsert],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// skip manifest parse errors
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return candidates;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Conservative generic markdown candidate builder.
|
|
113
|
+
* Scans a small, safe set of top-level markdown files and emits only
|
|
114
|
+
* ADR/REQ/FACT candidates when clear heading heuristics match.
|
|
115
|
+
*
|
|
116
|
+
* discoveryResult.markdownFiles is expected to be a list of file paths
|
|
117
|
+
* (absolute or relative). Files under documentation/**, .kb/**, .git/**,
|
|
118
|
+
* node_modules/**, vendor/**, third_party/** are ignored to avoid vendored
|
|
119
|
+
* trees and double-counting typed Kibi docs.
|
|
120
|
+
*/
|
|
121
|
+
// implements REQ-mcp-init-kibi-autopilot-v1
|
|
122
|
+
export function buildGenericMarkdownCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
|
|
123
|
+
const candidates = [];
|
|
124
|
+
const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
|
|
125
|
+
const files = discoveryResult.markdownFiles ?? [];
|
|
126
|
+
for (const rawPath of files) {
|
|
127
|
+
try {
|
|
128
|
+
const filePath = String(rawPath);
|
|
129
|
+
const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
|
|
130
|
+
if (isIgnoredGenericMarkdownPath(relativePath))
|
|
131
|
+
continue;
|
|
132
|
+
const base = path.basename(relativePath).toLowerCase();
|
|
133
|
+
const inDocsDir = /(^|\/)docs\//.test(relativePath);
|
|
134
|
+
// Only scan README.md, ARCHITECTURE.md or files under docs/**
|
|
135
|
+
if (!(base === "readme.md" || base === "architecture.md" || inDocsDir)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!fs.existsSync(absolutePath))
|
|
139
|
+
continue;
|
|
140
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
141
|
+
const lines = content.split(/\r?\n/);
|
|
142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
143
|
+
const line = lines[i];
|
|
144
|
+
if (line === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
const headingMatch = line.match(/^\s*#+\s*(.+)$/);
|
|
147
|
+
if (!headingMatch)
|
|
148
|
+
continue;
|
|
149
|
+
const headingRaw = headingMatch[1];
|
|
150
|
+
if (!headingRaw)
|
|
151
|
+
continue;
|
|
152
|
+
const heading = headingRaw.trim();
|
|
153
|
+
const headingLower = heading.toLowerCase();
|
|
154
|
+
let type = null;
|
|
155
|
+
let confidence = 0;
|
|
156
|
+
// ADR heuristic: headings that mention ADR or Architectural Decision
|
|
157
|
+
if (/\badr\b/i.test(heading) || /architectur.*decision/i.test(heading)) {
|
|
158
|
+
type = "adr";
|
|
159
|
+
confidence = 0.9;
|
|
160
|
+
}
|
|
161
|
+
// Requirements heuristic: explicit Requirements heading
|
|
162
|
+
if (!type && /\brequirements?\b/i.test(heading)) {
|
|
163
|
+
type = "req";
|
|
164
|
+
confidence = 0.85;
|
|
165
|
+
}
|
|
166
|
+
// Fact/Observation heuristic
|
|
167
|
+
if (!type && /\b(observations?|facts?|notes?)\b/i.test(heading)) {
|
|
168
|
+
type = "fact";
|
|
169
|
+
confidence = 0.8;
|
|
170
|
+
}
|
|
171
|
+
if (!type)
|
|
172
|
+
continue;
|
|
173
|
+
// Suppress below-threshold candidates early
|
|
174
|
+
if (confidence < minConfidence)
|
|
175
|
+
continue;
|
|
176
|
+
// Build a safe ID for the candidate. Use a repo-relative slug so it's
|
|
177
|
+
// deterministic across runs and comparable to typed candidates.
|
|
178
|
+
const slug = heading
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
181
|
+
.replace(/(^-|-$)/g, "")
|
|
182
|
+
.slice(0, 60);
|
|
183
|
+
const idPrefix = type === "adr" ? "ADR" : type === "req" ? "REQ" : "FACT";
|
|
184
|
+
const genId = `${idPrefix}-GEN-${slug || path.basename(relativePath).replace(/\.[^.]+$/, "")}`.toUpperCase();
|
|
185
|
+
if (existingEntities.ids.has(genId))
|
|
186
|
+
continue;
|
|
187
|
+
// Entity record for applyPlan; keep properties minimal and safe.
|
|
188
|
+
const entity = {
|
|
189
|
+
type,
|
|
190
|
+
id: genId,
|
|
191
|
+
title: heading,
|
|
192
|
+
status: type === "fact" ? "active" : type === "adr" ? "proposed" : "open",
|
|
193
|
+
source: `autopilot:generic:${relativePath}`,
|
|
194
|
+
text_ref: `${relativePath}#L${i + 1}`,
|
|
195
|
+
};
|
|
196
|
+
if (type === "fact") {
|
|
197
|
+
// Generic facts are observations by policy
|
|
198
|
+
entity.fact_kind = "observation";
|
|
199
|
+
}
|
|
200
|
+
const upsert = buildUpsertFromExtraction({
|
|
201
|
+
entity: entity,
|
|
202
|
+
relationships: [],
|
|
203
|
+
}, type);
|
|
204
|
+
const candidateId = `gen:${relativePath}:${type}:${slug}`;
|
|
205
|
+
const confidenceBand = confidence >= 0.95 ? "high" : "medium";
|
|
206
|
+
candidates.push({
|
|
207
|
+
candidateId,
|
|
208
|
+
entityType: type,
|
|
209
|
+
title: heading,
|
|
210
|
+
sourceKind: "generic_markdown",
|
|
211
|
+
sourcePath: absolutePath,
|
|
212
|
+
confidence,
|
|
213
|
+
confidenceBand,
|
|
214
|
+
evidence: [`generic_heading:${relativePath}#L${i + 1}`],
|
|
215
|
+
relationships: [],
|
|
216
|
+
applyPlan: [upsert],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
// skip unreadable files silently
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return candidates;
|
|
225
|
+
}
|
|
226
|
+
export default {
|
|
227
|
+
buildTypedMarkdownCandidates,
|
|
228
|
+
buildSymbolManifestCandidates,
|
|
229
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Autopilot discovery helpers
|
|
3
|
+
*
|
|
4
|
+
* Provides lightweight workspace activation classification and source discovery
|
|
5
|
+
* used by the `kb_autopilot_generate` workflow.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { runJsonModuleQuery } from "./core-module.js";
|
|
10
|
+
// Minimal copy of the opencode defaults used by other packages. Keep in sync
|
|
11
|
+
// with packages/opencode/src/file-filter.ts DEFAULT_SYNC_PATHS.
|
|
12
|
+
const DEFAULT_SYNC_PATHS = {
|
|
13
|
+
requirements: "documentation/requirements/**/*.md",
|
|
14
|
+
scenarios: "documentation/scenarios/**/*.md",
|
|
15
|
+
tests: "documentation/tests/**/*.md",
|
|
16
|
+
adr: "documentation/adr/**/*.md",
|
|
17
|
+
flags: "documentation/flags/**/*.md",
|
|
18
|
+
events: "documentation/events/**/*.md",
|
|
19
|
+
facts: "documentation/facts/**/*.md",
|
|
20
|
+
symbols: "documentation/symbols.yaml",
|
|
21
|
+
};
|
|
22
|
+
function findVendoredTrees(cwd) {
|
|
23
|
+
const results = [];
|
|
24
|
+
const vendoredMarkers = [
|
|
25
|
+
["kibi", "opencode.json"],
|
|
26
|
+
["kibi", "package.json"],
|
|
27
|
+
["kibi", "packages", "mcp"],
|
|
28
|
+
["kibi", "documentation"],
|
|
29
|
+
];
|
|
30
|
+
for (const marker of vendoredMarkers) {
|
|
31
|
+
const markerPath = path.join(cwd, ...marker);
|
|
32
|
+
if (fs.existsSync(markerPath)) {
|
|
33
|
+
results.push(marker.join("/"));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const nodeModules = path.join(cwd, "node_modules");
|
|
37
|
+
if (fs.existsSync(nodeModules)) {
|
|
38
|
+
try {
|
|
39
|
+
for (const entry of fs.readdirSync(nodeModules)) {
|
|
40
|
+
if (entry === "kibi" || entry.startsWith("kibi-")) {
|
|
41
|
+
results.push(`node_modules/${entry}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Array.from(new Set(results));
|
|
50
|
+
}
|
|
51
|
+
function rootKbConfigExists(cwd) {
|
|
52
|
+
return fs.existsSync(path.join(cwd, ".kb", "config.json"));
|
|
53
|
+
}
|
|
54
|
+
function readRootConfig(cwd) {
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(path.join(cwd, ".kb", "config.json"), "utf8");
|
|
57
|
+
return JSON.parse(raw) || null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function stripToRoot(p) {
|
|
64
|
+
const segments = p.split("/");
|
|
65
|
+
const rootSegments = [];
|
|
66
|
+
for (const seg of segments) {
|
|
67
|
+
if (seg.includes("*") || seg.includes("?") || seg.includes("["))
|
|
68
|
+
break;
|
|
69
|
+
rootSegments.push(seg);
|
|
70
|
+
}
|
|
71
|
+
const result = rootSegments.join("/");
|
|
72
|
+
return result || ".";
|
|
73
|
+
}
|
|
74
|
+
function normalizePattern(p) {
|
|
75
|
+
if (!p)
|
|
76
|
+
return null;
|
|
77
|
+
if (p.includes("*"))
|
|
78
|
+
return p;
|
|
79
|
+
if (p.endsWith(".yaml") || p.endsWith(".yml") || path.extname(p))
|
|
80
|
+
return p;
|
|
81
|
+
return `${p.replace(/\/+$/, "")}/**/*.md`;
|
|
82
|
+
}
|
|
83
|
+
function rootTargetsAllResolve(cwd) {
|
|
84
|
+
const config = readRootConfig(cwd) || {};
|
|
85
|
+
const paths = config.paths ?? {};
|
|
86
|
+
const keys = [
|
|
87
|
+
"requirements",
|
|
88
|
+
"scenarios",
|
|
89
|
+
"tests",
|
|
90
|
+
"adr",
|
|
91
|
+
"flags",
|
|
92
|
+
"events",
|
|
93
|
+
"facts",
|
|
94
|
+
"symbols",
|
|
95
|
+
];
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const raw = paths[key] ?? DEFAULT_SYNC_PATHS[key];
|
|
98
|
+
if (!raw)
|
|
99
|
+
return false;
|
|
100
|
+
const normalized = raw.replace(/\/+$|\s+$/g, "");
|
|
101
|
+
const isFile = normalized.endsWith(".yaml") || normalized.endsWith(".yml");
|
|
102
|
+
if (isFile) {
|
|
103
|
+
if (!fs.existsSync(path.resolve(cwd, normalized)))
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const root = stripToRoot(normalized);
|
|
108
|
+
if (!fs.existsSync(path.resolve(cwd, root)))
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/** Classify activation readiness for autopilot. */
|
|
115
|
+
// implements REQ-mcp-init-kibi-autopilot-v1
|
|
116
|
+
export async function classifyActivationState(workspaceRoot, prolog) {
|
|
117
|
+
const hasRootConfig = rootKbConfigExists(workspaceRoot);
|
|
118
|
+
const vendored = findVendoredTrees(workspaceRoot);
|
|
119
|
+
if (!hasRootConfig && vendored.length > 0) {
|
|
120
|
+
return "vendored_only";
|
|
121
|
+
}
|
|
122
|
+
if (!hasRootConfig) {
|
|
123
|
+
return "root_uninitialized";
|
|
124
|
+
}
|
|
125
|
+
// Root config exists → check targets
|
|
126
|
+
const allResolve = rootTargetsAllResolve(workspaceRoot);
|
|
127
|
+
if (!allResolve)
|
|
128
|
+
return "root_partial";
|
|
129
|
+
// Config exists and targets resolve — consult KB counts via Prolog
|
|
130
|
+
try {
|
|
131
|
+
const payload = await runJsonModuleQuery(prolog, "discovery.pl", "discovery:coverage_report_json(type, [], true, false, 100, 0, JsonString)", "Autopilot activation counts");
|
|
132
|
+
const rows = payload?.rows ?? [];
|
|
133
|
+
const counts = {};
|
|
134
|
+
for (const r of rows)
|
|
135
|
+
counts[r.type ?? r.id] = Number(r.count || 0);
|
|
136
|
+
const reqCount = counts.req ?? 0;
|
|
137
|
+
const nonSymbolTypes = [
|
|
138
|
+
"req",
|
|
139
|
+
"scenario",
|
|
140
|
+
"test",
|
|
141
|
+
"adr",
|
|
142
|
+
"flag",
|
|
143
|
+
"event",
|
|
144
|
+
"fact",
|
|
145
|
+
];
|
|
146
|
+
const nonSymbolTotal = nonSymbolTypes.reduce((s, t) => s + (counts[t] ?? 0), 0);
|
|
147
|
+
const scenarioTestAdrFact = (counts.scenario ?? 0) +
|
|
148
|
+
(counts.test ?? 0) +
|
|
149
|
+
(counts.adr ?? 0) +
|
|
150
|
+
(counts.fact ?? 0);
|
|
151
|
+
if (reqCount >= 1 && nonSymbolTotal >= 5 && scenarioTestAdrFact >= 1) {
|
|
152
|
+
return "root_active_seeded";
|
|
153
|
+
}
|
|
154
|
+
return "root_active_thin";
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// If Prolog is unavailable or the query fails, conservatively treat as thin
|
|
158
|
+
return "root_active_thin";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Recursively collect markdown files under `dir`, excluding known ignore dirs.
|
|
162
|
+
function collectMarkdownFiles(dir, workspaceRoot, vendoredRoots) {
|
|
163
|
+
const results = [];
|
|
164
|
+
if (!fs.existsSync(dir))
|
|
165
|
+
return results;
|
|
166
|
+
const stat = fs.statSync(dir);
|
|
167
|
+
if (!stat.isDirectory())
|
|
168
|
+
return results;
|
|
169
|
+
const entries = fs.readdirSync(dir);
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const full = path.join(dir, entry);
|
|
172
|
+
// Skip ignores
|
|
173
|
+
if (entry === ".git" || entry === "node_modules" || entry === ".kb")
|
|
174
|
+
continue;
|
|
175
|
+
// Skip vendored roots
|
|
176
|
+
const rel = path.relative(workspaceRoot, full).split(path.sep).join("/");
|
|
177
|
+
if (vendoredRoots.some((v) => rel === v || rel.startsWith(`${v}/`)))
|
|
178
|
+
continue;
|
|
179
|
+
const st = fs.statSync(full);
|
|
180
|
+
if (st.isDirectory()) {
|
|
181
|
+
results.push(...collectMarkdownFiles(full, workspaceRoot, vendoredRoots));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (st.isFile() && entry.endsWith(".md")) {
|
|
185
|
+
results.push(path.relative(workspaceRoot, full).split(path.sep).join("/"));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
/** Discover eligible source inputs for autopilot. */
|
|
191
|
+
// implements REQ-mcp-init-kibi-autopilot-v1
|
|
192
|
+
export function discoverSources(workspaceRoot, activationState) {
|
|
193
|
+
const vendored = findVendoredTrees(workspaceRoot);
|
|
194
|
+
if (activationState === "vendored_only") {
|
|
195
|
+
return { candidates: [], summary: { activationState, vendored } };
|
|
196
|
+
}
|
|
197
|
+
const config = readRootConfig(workspaceRoot) || {};
|
|
198
|
+
const paths = config.paths ??
|
|
199
|
+
DEFAULT_SYNC_PATHS;
|
|
200
|
+
const candidates = new Set();
|
|
201
|
+
// First: configured KB paths (include documentation/* if configured)
|
|
202
|
+
for (const key of Object.keys(DEFAULT_SYNC_PATHS)) {
|
|
203
|
+
const raw = paths[key];
|
|
204
|
+
if (!raw)
|
|
205
|
+
continue;
|
|
206
|
+
const normalized = raw.replace(/\s+$/, "");
|
|
207
|
+
if (normalized.endsWith(".yaml") || normalized.endsWith(".yml")) {
|
|
208
|
+
const abs = path.resolve(workspaceRoot, normalized);
|
|
209
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
210
|
+
candidates.add(path.relative(workspaceRoot, abs).split(path.sep).join("/"));
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const pat = normalizePattern(normalized) ?? normalized;
|
|
215
|
+
const root = stripToRoot(pat);
|
|
216
|
+
const absRoot = path.resolve(workspaceRoot, root);
|
|
217
|
+
if (fs.existsSync(absRoot) && fs.statSync(absRoot).isDirectory()) {
|
|
218
|
+
for (const f of collectMarkdownFiles(absRoot, workspaceRoot, vendored)) {
|
|
219
|
+
candidates.add(f);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Generic markdown candidates (top-level), but exclude documentation/** which
|
|
224
|
+
// is treated above via configured paths.
|
|
225
|
+
for (const file of ["README.md", "ARCHITECTURE.md"]) {
|
|
226
|
+
const abs = path.resolve(workspaceRoot, file);
|
|
227
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
228
|
+
const rel = path.relative(workspaceRoot, abs).split(path.sep).join("/");
|
|
229
|
+
if (!rel.startsWith("documentation/"))
|
|
230
|
+
candidates.add(rel);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const docsRoot = path.resolve(workspaceRoot, "docs");
|
|
234
|
+
if (fs.existsSync(docsRoot) && fs.statSync(docsRoot).isDirectory()) {
|
|
235
|
+
for (const f of collectMarkdownFiles(docsRoot, workspaceRoot, vendored)) {
|
|
236
|
+
candidates.add(f);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
candidates: Array.from(candidates).sort(),
|
|
241
|
+
summary: { activationState, reason: "discovered sources", vendored },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildTypedMarkdownCandidates, buildSymbolManifestCandidates, } from "./autopilot-candidates.js";
|
|
3
|
+
import { classifyActivationState, discoverSources as discoverActivationSources, } from "./autopilot-discovery.js";
|
|
4
|
+
import { loadEntities } from "./entity-query.js";
|
|
5
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
6
|
+
function extractTextRefFromApplyPlan(applyPlan) {
|
|
7
|
+
if (!Array.isArray(applyPlan) || applyPlan.length === 0)
|
|
8
|
+
return "";
|
|
9
|
+
const first = applyPlan[0];
|
|
10
|
+
if (!first || typeof first !== "object")
|
|
11
|
+
return "";
|
|
12
|
+
const firstRecord = first;
|
|
13
|
+
const properties = firstRecord.properties;
|
|
14
|
+
if (!properties || typeof properties !== "object")
|
|
15
|
+
return "";
|
|
16
|
+
const propsRecord = properties;
|
|
17
|
+
const textRef = propsRecord.text_ref;
|
|
18
|
+
return typeof textRef === "string" ? textRef : "";
|
|
19
|
+
}
|
|
20
|
+
function toSuppressedCandidate(reason, candidate) {
|
|
21
|
+
return {
|
|
22
|
+
candidateId: String(candidate.candidateId ?? ""),
|
|
23
|
+
reason,
|
|
24
|
+
sourcePath: String(candidate.sourcePath ?? ""),
|
|
25
|
+
entityType: String(candidate.entityType ?? ""),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function activationReasonFor(state) {
|
|
29
|
+
switch (state) {
|
|
30
|
+
case "vendored_only":
|
|
31
|
+
return "Workspace appears to contain vendored Kibi sources only; no local candidates generated.";
|
|
32
|
+
case "root_partial":
|
|
33
|
+
return "Workspace root is partially configured; discovery completed using available sources.";
|
|
34
|
+
case "root_active_seeded":
|
|
35
|
+
return "KB attached and discovery completed for a seeded workspace.";
|
|
36
|
+
case "root_active_thin":
|
|
37
|
+
return "KB attached and discovery completed for a thin workspace.";
|
|
38
|
+
default:
|
|
39
|
+
return "Workspace root is not fully initialized; discovery completed using the resolved workspace root.";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function splitDiscoveredSources(workspaceRoot, candidates) {
|
|
43
|
+
const markdownFiles = [];
|
|
44
|
+
const manifestFiles = [];
|
|
45
|
+
for (const relativePath of candidates) {
|
|
46
|
+
const absolutePath = path.resolve(workspaceRoot, relativePath);
|
|
47
|
+
if (/symbols\.ya?ml$/i.test(relativePath)) {
|
|
48
|
+
manifestFiles.push(absolutePath);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (/\.md$/i.test(relativePath)) {
|
|
52
|
+
markdownFiles.push(absolutePath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { markdownFiles, manifestFiles };
|
|
56
|
+
}
|
|
57
|
+
export async function handleKbAutopilotGenerate(// implements REQ-mcp-init-kibi-autopilot-v1
|
|
58
|
+
_prolog, args) {
|
|
59
|
+
const { includeGenericMarkdown = true, minConfidence = 0.8, maxCandidates = 50, entityTypes, } = args;
|
|
60
|
+
// Minimal discovery + candidate assembly implementation
|
|
61
|
+
const prolog = _prolog;
|
|
62
|
+
// Gather existing entity ids to suppress duplicates
|
|
63
|
+
let existingIds = new Set();
|
|
64
|
+
try {
|
|
65
|
+
const entities = await loadEntities(prolog, {});
|
|
66
|
+
for (const e of entities) {
|
|
67
|
+
const id = String(e.id ?? "");
|
|
68
|
+
if (id)
|
|
69
|
+
existingIds.add(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// If we can't list entities, proceed with empty set
|
|
74
|
+
existingIds = new Set();
|
|
75
|
+
}
|
|
76
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
77
|
+
const activationState = await classifyActivationState(workspaceRoot, prolog);
|
|
78
|
+
const activationDiscovery = discoverActivationSources(workspaceRoot, activationState);
|
|
79
|
+
const discovery = splitDiscoveredSources(workspaceRoot, activationDiscovery.candidates);
|
|
80
|
+
const allowGeneration = activationState === "root_uninitialized" || activationState === "root_partial";
|
|
81
|
+
if (!allowGeneration) {
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: "Autopilot generated 0 candidate(s).",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
structuredContent: {
|
|
90
|
+
activationState,
|
|
91
|
+
activationReason: activationReasonFor(activationState),
|
|
92
|
+
applyBlocked: true,
|
|
93
|
+
discoverySummary: {
|
|
94
|
+
markdownFiles: discovery.markdownFiles.length,
|
|
95
|
+
manifestFiles: discovery.manifestFiles.length,
|
|
96
|
+
vendored: activationDiscovery.summary.vendored ?? [],
|
|
97
|
+
},
|
|
98
|
+
candidates: [],
|
|
99
|
+
suppressedCandidates: [],
|
|
100
|
+
payoffSummary: {
|
|
101
|
+
current: {},
|
|
102
|
+
projectedIfAllApplied: {},
|
|
103
|
+
delta: {},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const typedMarkdownCandidates = buildTypedMarkdownCandidates(discovery, {
|
|
109
|
+
ids: existingIds,
|
|
110
|
+
workspaceRoot,
|
|
111
|
+
});
|
|
112
|
+
const manifestCandidates = buildSymbolManifestCandidates(discovery, {
|
|
113
|
+
ids: existingIds,
|
|
114
|
+
workspaceRoot,
|
|
115
|
+
});
|
|
116
|
+
// Lazy import to avoid circulars if any
|
|
117
|
+
// buildGenericMarkdownCandidates is added in autopilot-candidates
|
|
118
|
+
let genericCandidates = [];
|
|
119
|
+
if (includeGenericMarkdown) {
|
|
120
|
+
try {
|
|
121
|
+
// Import from same module file
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
123
|
+
const ac = await import("./autopilot-candidates.js");
|
|
124
|
+
if (typeof ac.buildGenericMarkdownCandidates === "function") {
|
|
125
|
+
genericCandidates = ac.buildGenericMarkdownCandidates(discovery, {
|
|
126
|
+
ids: existingIds,
|
|
127
|
+
workspaceRoot,
|
|
128
|
+
}, minConfidence);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// ignore import failures and proceed with typed candidates only
|
|
133
|
+
genericCandidates = [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Merge and filter candidates by requested entityTypes and minConfidence
|
|
137
|
+
let allCandidates = [...typedMarkdownCandidates, ...manifestCandidates, ...genericCandidates];
|
|
138
|
+
if (entityTypes && entityTypes.length > 0) {
|
|
139
|
+
const allowed = new Set(entityTypes);
|
|
140
|
+
allCandidates = allCandidates.filter((c) => allowed.has(c.entityType));
|
|
141
|
+
}
|
|
142
|
+
allCandidates = allCandidates.filter((c) => c.confidence >= minConfidence);
|
|
143
|
+
// Limit and deterministic sort (confidence desc, sourcePath asc)
|
|
144
|
+
allCandidates.sort((a, b) => {
|
|
145
|
+
if (b.confidence !== a.confidence)
|
|
146
|
+
return b.confidence - a.confidence;
|
|
147
|
+
if (a.sourcePath < b.sourcePath)
|
|
148
|
+
return -1;
|
|
149
|
+
if (a.sourcePath > b.sourcePath)
|
|
150
|
+
return 1;
|
|
151
|
+
return 0;
|
|
152
|
+
});
|
|
153
|
+
allCandidates = allCandidates.slice(0, maxCandidates);
|
|
154
|
+
// Dedupe logic
|
|
155
|
+
const seenByKey = new Map();
|
|
156
|
+
const suppressed = [];
|
|
157
|
+
// Helpers
|
|
158
|
+
function normalizeTitle(entityType, title) {
|
|
159
|
+
return `${entityType}::${String(title).trim().toLowerCase().replace(/\s+/g, " ")}`;
|
|
160
|
+
}
|
|
161
|
+
const typedTitleKeys = new Set(typedMarkdownCandidates.map((candidate) => normalizeTitle(String(candidate.entityType || ""), String(candidate.title || ""))));
|
|
162
|
+
for (const c of allCandidates) {
|
|
163
|
+
const record = { ...c };
|
|
164
|
+
const entityType = String(c.entityType || "");
|
|
165
|
+
const title = String(c.title || "");
|
|
166
|
+
const sourceKind = String(c.sourceKind || "");
|
|
167
|
+
const sourcePath = String(c.sourcePath || "");
|
|
168
|
+
const textRef = extractTextRefFromApplyPlan(c.applyPlan);
|
|
169
|
+
const titleKey = normalizeTitle(entityType, title);
|
|
170
|
+
// entity_exists: exact entity ID present in KB
|
|
171
|
+
const upsert = Array.isArray(c.applyPlan) ? c.applyPlan[0] : null;
|
|
172
|
+
let upsertId = "";
|
|
173
|
+
if (upsert && typeof upsert === "object") {
|
|
174
|
+
const upsertRecord = upsert;
|
|
175
|
+
const directId = upsertRecord.id;
|
|
176
|
+
if (typeof directId === "string" && directId.length > 0) {
|
|
177
|
+
upsertId = directId;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const properties = upsertRecord.properties;
|
|
181
|
+
if (properties && typeof properties === "object") {
|
|
182
|
+
const nestedId = properties.id;
|
|
183
|
+
if (typeof nestedId === "string" && nestedId.length > 0) {
|
|
184
|
+
upsertId = nestedId;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (existingIds.has(upsertId)) {
|
|
190
|
+
suppressed.push(toSuppressedCandidate("entity_exists", record));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (sourceKind === "generic_markdown" && typedTitleKeys.has(titleKey)) {
|
|
194
|
+
suppressed.push(toSuppressedCandidate("shadowed_by_typed_source", record));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// duplicate_title: same entityType + normalized title
|
|
198
|
+
const existing = seenByKey.get(titleKey);
|
|
199
|
+
if (existing) {
|
|
200
|
+
// keep the higher confidence one
|
|
201
|
+
const existingConf = Number(existing.confidence ?? 0);
|
|
202
|
+
const thisConf = Number(c.confidence ?? 0);
|
|
203
|
+
if (thisConf > existingConf) {
|
|
204
|
+
// move existing to suppressed
|
|
205
|
+
suppressed.push(toSuppressedCandidate("duplicate_title", existing));
|
|
206
|
+
seenByKey.set(titleKey, record);
|
|
207
|
+
}
|
|
208
|
+
else if (thisConf < existingConf) {
|
|
209
|
+
suppressed.push(toSuppressedCandidate("duplicate_title", record));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// tie-break by lexicographically smallest sourcePath:textRef
|
|
213
|
+
const existingRef = `${String(existing.sourcePath ?? "")}::${extractTextRefFromApplyPlan(existing.applyPlan)}`;
|
|
214
|
+
const thisRef = `${sourcePath}::${textRef}`;
|
|
215
|
+
if (thisRef < existingRef) {
|
|
216
|
+
suppressed.push(toSuppressedCandidate("duplicate_title", existing));
|
|
217
|
+
seenByKey.set(titleKey, record);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
suppressed.push(toSuppressedCandidate("duplicate_title", record));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
seenByKey.set(titleKey, record);
|
|
226
|
+
}
|
|
227
|
+
const candidateRecords = Array.from(seenByKey.values());
|
|
228
|
+
return {
|
|
229
|
+
content: [
|
|
230
|
+
{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: `Autopilot generated ${allCandidates.length} candidate(s).`,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
structuredContent: {
|
|
236
|
+
activationState,
|
|
237
|
+
activationReason: activationReasonFor(activationState),
|
|
238
|
+
applyBlocked: activationState === "root_partial",
|
|
239
|
+
discoverySummary: {
|
|
240
|
+
markdownFiles: discovery.markdownFiles.length,
|
|
241
|
+
manifestFiles: discovery.manifestFiles.length,
|
|
242
|
+
vendored: activationDiscovery.summary.vendored ?? [],
|
|
243
|
+
},
|
|
244
|
+
candidates: candidateRecords,
|
|
245
|
+
suppressedCandidates: suppressed,
|
|
246
|
+
payoffSummary: (() => {
|
|
247
|
+
// current counts by type
|
|
248
|
+
const current = {};
|
|
249
|
+
try {
|
|
250
|
+
// compute from existingIds via loadEntities would be expensive; fall back to empty
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
// noop
|
|
254
|
+
}
|
|
255
|
+
// projected if all applied
|
|
256
|
+
const projected = { ...current };
|
|
257
|
+
for (const r of candidateRecords) {
|
|
258
|
+
const t = String(r.entityType || "unknown");
|
|
259
|
+
projected[t] = (projected[t] || 0) + 1;
|
|
260
|
+
}
|
|
261
|
+
const delta = {};
|
|
262
|
+
for (const k of Object.keys(projected)) {
|
|
263
|
+
const projectedValue = projected[k] ?? 0;
|
|
264
|
+
const currentValue = current[k] ?? 0;
|
|
265
|
+
delta[k] = projectedValue - currentValue;
|
|
266
|
+
}
|
|
267
|
+
return { current, projectedIfAllApplied: projected, delta };
|
|
268
|
+
})(),
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
package/dist/tools-config.js
CHANGED
|
@@ -384,6 +384,42 @@ const BASE_TOOLS = [
|
|
|
384
384
|
},
|
|
385
385
|
},
|
|
386
386
|
},
|
|
387
|
+
{
|
|
388
|
+
name: "kb_autopilot_generate",
|
|
389
|
+
description: "Generate autopilot candidate batches for KB population. Read-only analysis that returns activation state, candidate entities with evidence, payoff summary, and exact applyPlan payloads for later kb_upsert calls. No mutation side effects.",
|
|
390
|
+
inputSchema: {
|
|
391
|
+
type: "object",
|
|
392
|
+
properties: {
|
|
393
|
+
includeGenericMarkdown: {
|
|
394
|
+
type: "boolean",
|
|
395
|
+
default: true,
|
|
396
|
+
description: "Whether to include generic markdown file content as candidate facts. Default: true.",
|
|
397
|
+
},
|
|
398
|
+
minConfidence: {
|
|
399
|
+
type: "number",
|
|
400
|
+
default: 0.8,
|
|
401
|
+
minimum: 0.6,
|
|
402
|
+
maximum: 0.95,
|
|
403
|
+
description: "Minimum confidence threshold for candidates. Clamped to [0.60, 0.95]. Default: 0.80.",
|
|
404
|
+
},
|
|
405
|
+
maxCandidates: {
|
|
406
|
+
type: "integer",
|
|
407
|
+
default: 50,
|
|
408
|
+
minimum: 1,
|
|
409
|
+
maximum: 200,
|
|
410
|
+
description: "Maximum number of candidates to return. Clamped to [1, 200]. Default: 50.",
|
|
411
|
+
},
|
|
412
|
+
entityTypes: {
|
|
413
|
+
type: "array",
|
|
414
|
+
items: {
|
|
415
|
+
type: "string",
|
|
416
|
+
enum: ["req", "scenario", "test", "adr", "fact", "symbol"],
|
|
417
|
+
},
|
|
418
|
+
description: "Optional filter to limit candidate generation to specific entity types.",
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
}
|
|
387
423
|
];
|
|
388
424
|
/**
|
|
389
425
|
* Inject _diagnostic_telemetry schema into tool inputs when diagnostic mode is enabled.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
6
6
|
"ajv": "^8.18.0",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"fast-glob": "^3.2.12",
|
|
10
10
|
"gray-matter": "^4.0.3",
|
|
11
11
|
"js-yaml": "^4.1.0",
|
|
12
|
-
"kibi-cli": "^0.6.
|
|
13
|
-
"kibi-core": "^0.5.
|
|
12
|
+
"kibi-cli": "^0.6.2",
|
|
13
|
+
"kibi-core": "^0.5.1",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|
|
16
16
|
"zod": "^4.3.6"
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"build": "tsc -p tsconfig.json",
|
|
28
28
|
"prepack": "npm run build"
|
|
29
29
|
},
|
|
30
|
-
"files": [
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"bin"
|
|
33
|
+
],
|
|
31
34
|
"engines": {
|
|
32
35
|
"node": ">=18",
|
|
33
36
|
"bun": ">=1.0"
|