kibi-mcp 0.10.0 → 0.11.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.
@@ -45,47 +45,46 @@ export const PROMPTS = [
45
45
  name: "init-kibi",
46
46
  description: "Activation workflow to populate a new or empty Kibi KB from an existing repository.",
47
47
  text: [
48
- "# Kibi Activation Workflow",
48
+ "# Kibi Interactive Activation Workflow",
49
49
  "",
50
- "Use this workflow to populate a Kibi knowledge base when it is new or empty.",
50
+ "Use this workflow to onboard a new or empty repository into Kibi through interactive discovery.",
51
51
  "",
52
- "## Step 1: Generate Candidates (read-only)",
52
+ "## Step 1: Gather Declared Context",
53
53
  "",
54
- "Call `kb_autopilot_generate` to scan the repository and produce candidate entities.",
54
+ "The agent must ask at most 4 bounded questions to gather declared intent from the user:",
55
+ "1. **Project Summary**: What is the core purpose of this project?",
56
+ "2. **Source of Truth**: Where is the primary documentation (canonical requirements, ADRs)?",
57
+ "3. **Priority Root**: In a monorepo, which package should be prioritized?",
58
+ "4. **Verification Anchors**: Where are the primary tests or verification configs located?",
55
59
  "",
56
- "This tool is **read-only** — it never writes to the KB. It returns:",
57
- "- `activationState`: the current KB state (e.g. `root_uninitialized`, `root_partial`)",
58
- "- `candidates[]`: proposed entities with confidence scores and evidence",
59
- "- `suppressedCandidates[]`: candidates suppressed due to duplicates, existing entities, or shadowed by typed sources",
60
- "- `discoverySummary` / `payoffSummary`: context for agent review",
60
+ "## Step 2: Synthesize Candidates (read-only)",
61
61
  "",
62
- "## Step 2: Review Candidates",
62
+ "Call `kb_autopilot_generate` with the gathered context to synthesize candidate entities.",
63
63
  "",
64
- "Inspect `activationState`. If `applyBlocked` is true, stop the KB cannot accept writes.",
64
+ "This tool is **read-only**. It returns additive `structuredContent` with:",
65
+ "- `promptBlock`: preview text for the user-facing approval prompt",
66
+ "- `recommendedActions`: agent-facing next steps, including any REQ/SCEN/TEST authoring routed for manual handling",
67
+ "- `declaredContext`: the user-provided bootstrap context",
68
+ "- `confidence`: confidence summary for the generated output",
69
+ "- `bootstrapMode`: current KB state (e.g., `root_uninitialized`)",
70
+ "- `candidates`: synthesized entities grounded in declared context and source evidence",
71
+ "- `discoverySummary`: source-backed discovery notes",
65
72
  "",
66
- "For each candidate, evaluate:",
67
- "- **confidence** (0–1): prefer high-confidence entities first",
68
- "- **evidence**: verify the source reference is real before applying",
73
+ "## Step 3: Preview and Approval",
69
74
  "",
70
- "Discard or edit candidates that look speculative. The agent decides what to apply the generator never writes.", "",
71
- "## Step 3: Apply Approved Candidates", "",
72
- "Apply approved candidates by executing each candidate.applyPlan sequentially:",
73
- "1. For each approved candidate, run its `candidate.applyPlan` steps in ascending phase order and keep the candidate sequence deterministic",
74
- "2. Execute each step with `kb_upsert` using the step's provided args, and confirm success before moving to the next step",
75
- "3. After each batch, call `kb_check` with targeted rules (`required-fields`, `no-dangling-refs`) to catch issues early",
75
+ "Present the `promptBlock` and a summary of `candidates` to the user. **Wait for explicit approval** before proceeding to writes.",
76
76
  "",
77
- "## Step 4: Payoff Verification",
77
+ "## Step 4: Apply Approved Candidates",
78
78
  "",
79
- "After all approved candidates are applied, verify the result:",
80
- "- `kb_check` with all rules must return zero violations",
81
- "- `kb_find_gaps` with `{ type: 'req', missingRelationships: ['specified_by', 'verified_by'] }` — identify under-linked requirements",
82
- "- `kb_coverage` with `{ by: 'req' }` confirm traceability coverage", "",
83
- "## Doc Hygiene",
79
+ "Apply approved candidates sequentially using `kb_upsert`.",
80
+ "1. Execute each approved candidate's `applyPlan` in ascending phase order.",
81
+ "2. Confirm success of each `kb_upsert` before moving to the next.",
82
+ "3. Run `kb_check` after the batch to verify KB integrity.",
84
83
  "",
85
- "- Always call `kb_query` before creating to avoid duplicate entities",
86
- "- Run `kb_check` after each batch, not just at the end",
87
- "- All writes go through `kb_upsert` do not invoke CLI commands directly",
88
- "- `kb_autopilot_generate` is read-only; only `kb_upsert` mutates the KB",
84
+ "## Rules",
85
+ "- Never apply changes without a user-facing preview and approval.",
86
+ "- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
87
+ "- Guidance must stay MCP-only; do not suggest `kibi` CLI commands.",
89
88
  ].join("\n"),
90
89
  },
91
90
  {
@@ -4,6 +4,56 @@ import { extractFromManifest } from "kibi-cli/extractors/manifest";
4
4
  import { extractFromMarkdown } from "kibi-cli/extractors/markdown";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs";
7
+ function slugify(value, maxLength = 80) {
8
+ return value
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9]+/g, "-")
11
+ .replace(/(^-|-$)/g, "")
12
+ .slice(0, maxLength);
13
+ }
14
+ function sortUniquePaths(paths) {
15
+ return Array.from(new Set(paths)).sort();
16
+ }
17
+ function getEvidenceFilePaths(discoveryResult, kind) {
18
+ return sortUniquePaths((discoveryResult.evidence ?? [])
19
+ .filter((item) => item.kind === kind)
20
+ .map((item) => item.absolutePath ?? "")
21
+ .filter((item) => Boolean(item)));
22
+ }
23
+ function getTypedMarkdownFiles(discoveryResult) {
24
+ const evidenceFiles = getEvidenceFilePaths(discoveryResult, "typed_markdown");
25
+ if (evidenceFiles.length > 0)
26
+ return evidenceFiles;
27
+ return discoveryResult.markdownFiles ?? [];
28
+ }
29
+ function getManifestFiles(discoveryResult) {
30
+ const evidenceFiles = getEvidenceFilePaths(discoveryResult, "symbol_manifest");
31
+ if (evidenceFiles.length > 0)
32
+ return evidenceFiles;
33
+ return discoveryResult.manifestFiles ?? [];
34
+ }
35
+ function getGenericMarkdownFiles(discoveryResult) {
36
+ const evidenceFiles = getEvidenceFilePaths(discoveryResult, "generic_markdown");
37
+ if (evidenceFiles.length > 0)
38
+ return evidenceFiles;
39
+ return discoveryResult.markdownFiles ?? [];
40
+ }
41
+ function hasGenericMarkdownEvidence(discoveryResult) {
42
+ return (discoveryResult.evidence ?? []).some((item) => item.kind === "generic_markdown");
43
+ }
44
+ function getFactEvidence(discoveryResult) {
45
+ return (discoveryResult.evidence ?? []).filter((item) => item.kind === "repo_metadata" ||
46
+ item.kind === "repo_layout" ||
47
+ item.kind === "test_topology" ||
48
+ item.kind === "source_symbols");
49
+ }
50
+ function toConfidenceBand(confidence) {
51
+ if (confidence >= 0.9)
52
+ return "high";
53
+ if (confidence >= 0.8)
54
+ return "medium";
55
+ return "low";
56
+ }
7
57
  function resolveCandidatePaths(filePath, workspaceRoot) {
8
58
  const absolutePath = path.isAbsolute(filePath)
9
59
  ? filePath
@@ -17,6 +67,20 @@ function isIgnoredGenericMarkdownPath(relativePath) {
17
67
  const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
18
68
  return /(^|\/)(documentation|\.kb|\.git|node_modules|vendor|vendors|third_party|third-party|dist|coverage)(\/|$)/.test(normalized);
19
69
  }
70
+ function shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown) {
71
+ const base = path.basename(relativePath).toLowerCase();
72
+ const inDocsDir = /(^|\/)docs\//.test(relativePath);
73
+ if (providerScopedMarkdown)
74
+ return true;
75
+ return base === "readme.md" || base === "architecture.md" || inDocsDir;
76
+ }
77
+ function pushSignal(signals, signal, seen) {
78
+ const key = `${signal.kind}::${signal.sourcePath}::${signal.title}`;
79
+ if (seen.has(key))
80
+ return;
81
+ seen.add(key);
82
+ signals.push(signal);
83
+ }
20
84
  function buildUpsertFromExtraction(er, typeOverride) {
21
85
  const ent = er.entity;
22
86
  const type = typeOverride ?? String(ent.type ?? "");
@@ -39,7 +103,7 @@ function buildUpsertFromExtraction(er, typeOverride) {
39
103
  export function buildTypedMarkdownCandidates(discoveryResult, existingEntities) {
40
104
  const candidates = [];
41
105
  const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
42
- for (const filePath of discoveryResult.markdownFiles || []) {
106
+ for (const filePath of getTypedMarkdownFiles(discoveryResult)) {
43
107
  try {
44
108
  const extraction = extractFromMarkdown(filePath);
45
109
  const { entity, relationships } = extraction;
@@ -74,7 +138,7 @@ export function buildTypedMarkdownCandidates(discoveryResult, existingEntities)
74
138
  export function buildSymbolManifestCandidates(discoveryResult, existingEntities) {
75
139
  const candidates = [];
76
140
  const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
77
- for (const filePath of discoveryResult.manifestFiles || []) {
141
+ for (const filePath of getManifestFiles(discoveryResult)) {
78
142
  try {
79
143
  const results = extractFromManifest(filePath);
80
144
  for (const res of results) {
@@ -111,7 +175,7 @@ export function buildSymbolManifestCandidates(discoveryResult, existingEntities)
111
175
  /**
112
176
  * Conservative generic markdown candidate builder.
113
177
  * Scans a small, safe set of top-level markdown files and emits only
114
- * ADR/REQ/FACT candidates when clear heading heuristics match.
178
+ * ADR/FACT candidates when clear heading heuristics match.
115
179
  *
116
180
  * discoveryResult.markdownFiles is expected to be a list of file paths
117
181
  * (absolute or relative). Files under documentation/**, .kb/**, .git/**,
@@ -122,17 +186,17 @@ export function buildSymbolManifestCandidates(discoveryResult, existingEntities)
122
186
  export function buildGenericMarkdownCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
123
187
  const candidates = [];
124
188
  const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
125
- const files = discoveryResult.markdownFiles ?? [];
189
+ const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
190
+ const files = getGenericMarkdownFiles(discoveryResult);
126
191
  for (const rawPath of files) {
127
192
  try {
128
193
  const filePath = String(rawPath);
129
194
  const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
130
195
  if (isIgnoredGenericMarkdownPath(relativePath))
131
196
  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)) {
197
+ // Legacy path-only discovery was conservative. Provider-scoped discovery
198
+ // already filters eligible generic docs, so allow broader repo markdown there.
199
+ if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown)) {
136
200
  continue;
137
201
  }
138
202
  if (!fs.existsSync(absolutePath))
@@ -158,11 +222,6 @@ export function buildGenericMarkdownCandidates(discoveryResult, existingEntities
158
222
  type = "adr";
159
223
  confidence = 0.9;
160
224
  }
161
- // Requirements heuristic: explicit Requirements heading
162
- if (!type && /\brequirements?\b/i.test(heading)) {
163
- type = "req";
164
- confidence = 0.85;
165
- }
166
225
  // Fact/Observation heuristic
167
226
  if (!type && /\b(observations?|facts?|notes?)\b/i.test(heading)) {
168
227
  type = "fact";
@@ -180,7 +239,7 @@ export function buildGenericMarkdownCandidates(discoveryResult, existingEntities
180
239
  .replace(/[^a-z0-9]+/g, "-")
181
240
  .replace(/(^-|-$)/g, "")
182
241
  .slice(0, 60);
183
- const idPrefix = type === "adr" ? "ADR" : type === "req" ? "REQ" : "FACT";
242
+ const idPrefix = type === "adr" ? "ADR" : "FACT";
184
243
  const genId = `${idPrefix}-GEN-${slug || path.basename(relativePath).replace(/\.[^.]+$/, "")}`.toUpperCase();
185
244
  if (existingEntities.ids.has(genId))
186
245
  continue;
@@ -223,7 +282,154 @@ export function buildGenericMarkdownCandidates(discoveryResult, existingEntities
223
282
  }
224
283
  return candidates;
225
284
  }
285
+ // implements REQ-mcp-init-kibi-autopilot-v1
286
+ export function collectSourceOnlyAuthoringSignals(discoveryResult, existingEntities, minConfidence = 0.8) {
287
+ const signals = [];
288
+ const seen = new Set();
289
+ const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
290
+ const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
291
+ for (const rawPath of getGenericMarkdownFiles(discoveryResult)) {
292
+ try {
293
+ const filePath = String(rawPath);
294
+ const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
295
+ if (isIgnoredGenericMarkdownPath(relativePath))
296
+ continue;
297
+ if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown))
298
+ continue;
299
+ if (!fs.existsSync(absolutePath))
300
+ continue;
301
+ const content = fs.readFileSync(absolutePath, "utf8");
302
+ const lines = content.split(/\r?\n/);
303
+ for (let i = 0; i < lines.length; i++) {
304
+ const line = lines[i];
305
+ if (line === undefined)
306
+ continue;
307
+ const headingMatch = line.match(/^\s*#+\s*(.+)$/);
308
+ if (!headingMatch)
309
+ continue;
310
+ const headingRaw = headingMatch[1];
311
+ if (!headingRaw)
312
+ continue;
313
+ const heading = headingRaw.trim();
314
+ const textRef = `${relativePath}#L${i + 1}`;
315
+ if (/\brequirements?\b/i.test(heading) && 0.84 >= minConfidence) {
316
+ pushSignal(signals, {
317
+ kind: "req",
318
+ title: `Author requirements from ${heading}`,
319
+ sourcePath: absolutePath,
320
+ confidence: 0.84,
321
+ evidence: [`generic_heading:${textRef}`],
322
+ }, seen);
323
+ }
324
+ if (/\bscenarios?\b/i.test(heading) && 0.83 >= minConfidence) {
325
+ pushSignal(signals, {
326
+ kind: "scenario",
327
+ title: `Author scenarios from ${heading}`,
328
+ sourcePath: absolutePath,
329
+ confidence: 0.83,
330
+ evidence: [`generic_heading:${textRef}`],
331
+ }, seen);
332
+ }
333
+ if (/\b(tests?|verification)\b/i.test(heading) && 0.82 >= minConfidence) {
334
+ pushSignal(signals, {
335
+ kind: "test",
336
+ title: `Author tests from ${heading}`,
337
+ sourcePath: absolutePath,
338
+ confidence: 0.82,
339
+ evidence: [`generic_heading:${textRef}`],
340
+ }, seen);
341
+ }
342
+ }
343
+ }
344
+ catch {
345
+ // ignore unreadable files when deriving authoring signals
346
+ }
347
+ }
348
+ for (const item of discoveryResult.evidence ?? []) {
349
+ const confidence = typeof item.data.confidence === "number" ? item.data.confidence : 0;
350
+ if (item.kind === "test_topology" && confidence >= minConfidence) {
351
+ const sourcePath = item.absolutePath ?? path.resolve(workspaceRoot, item.relativePath ?? item.label);
352
+ const relativePath = item.relativePath ?? item.label;
353
+ pushSignal(signals, {
354
+ kind: "test",
355
+ title: `Author TEST coverage for ${relativePath}`,
356
+ sourcePath,
357
+ confidence,
358
+ evidence: Array.isArray(item.data.evidence)
359
+ ? item.data.evidence.filter((value) => typeof value === "string")
360
+ : [`test_topology:${relativePath}`],
361
+ }, seen);
362
+ }
363
+ }
364
+ return signals.sort((left, right) => {
365
+ if (right.confidence !== left.confidence)
366
+ return right.confidence - left.confidence;
367
+ if (left.kind !== right.kind)
368
+ return left.kind.localeCompare(right.kind);
369
+ return left.sourcePath.localeCompare(right.sourcePath);
370
+ });
371
+ }
372
+ // implements REQ-mcp-init-kibi-autopilot-v1
373
+ export function buildProviderEvidenceCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
374
+ const candidates = [];
375
+ const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
376
+ for (const item of getFactEvidence(discoveryResult)) {
377
+ const relativePath = item.relativePath ?? item.label;
378
+ const absolutePath = item.absolutePath ?? path.resolve(workspaceRoot, relativePath);
379
+ const confidence = typeof item.data.confidence === "number" ? item.data.confidence : 0.8;
380
+ if (confidence < minConfidence)
381
+ continue;
382
+ const factKind = typeof item.data.factKind === "string" && item.data.factKind.length > 0
383
+ ? item.data.factKind
384
+ : item.kind === "repo_metadata"
385
+ ? "meta"
386
+ : "observation";
387
+ const title = typeof item.data.title === "string" && item.data.title.length > 0
388
+ ? item.data.title
389
+ : `Autopilot evidence from ${relativePath}`;
390
+ const slugSource = `${item.kind}-${relativePath}`;
391
+ const generatedId = `FACT-GEN-${slugify(slugSource, 64) || "evidence"}`.toUpperCase();
392
+ if (existingEntities.ids.has(generatedId))
393
+ continue;
394
+ const textRef = relativePath.includes("#") ? relativePath : `${relativePath}`;
395
+ const evidence = Array.isArray(item.data.evidence)
396
+ ? item.data.evidence.filter((value) => typeof value === "string")
397
+ : [];
398
+ candidates.push({
399
+ candidateId: `prov:${item.kind}:${slugify(relativePath, 96) || "evidence"}`,
400
+ entityType: "fact",
401
+ title,
402
+ sourceKind: item.kind,
403
+ sourcePath: absolutePath,
404
+ confidence,
405
+ confidenceBand: toConfidenceBand(confidence),
406
+ evidence: evidence.length > 0
407
+ ? evidence
408
+ : [`provider:${item.provider}`, `${item.kind}:${relativePath}`],
409
+ relationships: [],
410
+ applyPlan: [
411
+ {
412
+ type: "fact",
413
+ id: generatedId,
414
+ properties: {
415
+ id: generatedId,
416
+ title,
417
+ status: "active",
418
+ fact_kind: factKind,
419
+ source: `autopilot:${item.provider}:${relativePath}`,
420
+ text_ref: textRef,
421
+ },
422
+ relationships: [],
423
+ },
424
+ ],
425
+ });
426
+ }
427
+ return candidates;
428
+ }
226
429
  export default {
227
430
  buildTypedMarkdownCandidates,
228
431
  buildSymbolManifestCandidates,
432
+ buildGenericMarkdownCandidates,
433
+ collectSourceOnlyAuthoringSignals,
434
+ buildProviderEvidenceCandidates,
229
435
  };