kibi-mcp 0.9.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.
@@ -6,7 +6,33 @@
6
6
  */
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
+ import fg from "fast-glob";
10
+ import * as cliSymbolCoordinator from "kibi-cli/extractors/symbols-coordinator";
9
11
  import { runJsonModuleQuery } from "./core-module.js";
12
+ // implements REQ-001
13
+ export const AUTOPILOT_PROVIDER_ORDER = [
14
+ "typed_kibi_docs",
15
+ "generic_repo_docs",
16
+ "repo_metadata",
17
+ "repo_layout",
18
+ "test_topology",
19
+ "source_symbols",
20
+ ];
21
+ const IGNORED_DIRECTORY_NAMES = new Set([
22
+ ".git",
23
+ ".kb",
24
+ ".venv",
25
+ "build",
26
+ "coverage",
27
+ "dist",
28
+ "node_modules",
29
+ "target",
30
+ "third-party",
31
+ "third_party",
32
+ "vendor",
33
+ "vendors",
34
+ "venv",
35
+ ]);
10
36
  // Minimal copy of the opencode defaults used by other packages. Keep in sync
11
37
  // with packages/opencode/src/file-filter.ts DEFAULT_SYNC_PATHS.
12
38
  const DEFAULT_SYNC_PATHS = {
@@ -19,8 +45,51 @@ const DEFAULT_SYNC_PATHS = {
19
45
  facts: "documentation/facts/**/*.md",
20
46
  symbols: "documentation/symbols.yaml",
21
47
  };
48
+ const SOURCE_LANGUAGE_EXTENSIONS = {
49
+ ".ts": "typescript",
50
+ ".tsx": "typescript",
51
+ ".mts": "typescript",
52
+ ".cts": "typescript",
53
+ ".js": "javascript",
54
+ ".jsx": "javascript",
55
+ ".mjs": "javascript",
56
+ ".cjs": "javascript",
57
+ ".py": "python",
58
+ ".rb": "ruby",
59
+ ".go": "go",
60
+ ".rs": "rust",
61
+ ".java": "java",
62
+ ".kt": "kotlin",
63
+ ".swift": "swift",
64
+ ".php": "php",
65
+ ".c": "c",
66
+ ".cc": "cpp",
67
+ ".cpp": "cpp",
68
+ ".h": "c",
69
+ ".hpp": "cpp",
70
+ };
71
+ const PROJECT_SIGNAL_FILES = [
72
+ "README.md",
73
+ "README.mdx",
74
+ "package.json",
75
+ "tsconfig.json",
76
+ "pyproject.toml",
77
+ "Cargo.toml",
78
+ "go.mod",
79
+ ];
80
+ const PROJECT_SIGNAL_DIRS = [
81
+ "src",
82
+ "app",
83
+ "apps",
84
+ "packages",
85
+ "tests",
86
+ "test",
87
+ "docs",
88
+ "documentation",
89
+ "scripts",
90
+ ];
22
91
  function findVendoredTrees(cwd) {
23
- const results = [];
92
+ const results = new Set();
24
93
  const vendoredMarkers = [
25
94
  ["kibi", "opencode.json"],
26
95
  ["kibi", "package.json"],
@@ -30,7 +99,7 @@ function findVendoredTrees(cwd) {
30
99
  for (const marker of vendoredMarkers) {
31
100
  const markerPath = path.join(cwd, ...marker);
32
101
  if (fs.existsSync(markerPath)) {
33
- results.push(marker.join("/"));
102
+ results.add(marker[0] ?? "kibi");
34
103
  }
35
104
  }
36
105
  const nodeModules = path.join(cwd, "node_modules");
@@ -38,7 +107,7 @@ function findVendoredTrees(cwd) {
38
107
  try {
39
108
  for (const entry of fs.readdirSync(nodeModules)) {
40
109
  if (entry === "kibi" || entry.startsWith("kibi-")) {
41
- results.push(`node_modules/${entry}`);
110
+ results.add(`node_modules/${entry}`);
42
111
  }
43
112
  }
44
113
  }
@@ -46,11 +115,30 @@ function findVendoredTrees(cwd) {
46
115
  // ignore
47
116
  }
48
117
  }
49
- return Array.from(new Set(results));
118
+ return Array.from(results).sort();
50
119
  }
51
120
  function rootKbConfigExists(cwd) {
52
121
  return fs.existsSync(path.join(cwd, ".kb", "config.json"));
53
122
  }
123
+ function hasWorkspaceProjectSignals(cwd, vendoredRoots) {
124
+ const vendoredTopLevel = new Set(vendoredRoots
125
+ .map((item) => item.split("/")[0])
126
+ .filter((item) => Boolean(item)));
127
+ for (const fileName of PROJECT_SIGNAL_FILES) {
128
+ if (fs.existsSync(path.join(cwd, fileName))) {
129
+ return true;
130
+ }
131
+ }
132
+ for (const dirName of PROJECT_SIGNAL_DIRS) {
133
+ if (vendoredTopLevel.has(dirName))
134
+ continue;
135
+ const candidate = path.join(cwd, dirName);
136
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
137
+ return true;
138
+ }
139
+ }
140
+ return false;
141
+ }
54
142
  function readRootConfig(cwd) {
55
143
  try {
56
144
  const raw = fs.readFileSync(path.join(cwd, ".kb", "config.json"), "utf8");
@@ -80,6 +168,575 @@ function normalizePattern(p) {
80
168
  return p;
81
169
  return `${p.replace(/\/+$/, "")}/**/*.md`;
82
170
  }
171
+ function buildSourceSummary(activation, vendored) {
172
+ return {
173
+ activationState: activation.activationState,
174
+ activationMode: activation.activationMode,
175
+ applyBlocked: activation.applyBlocked,
176
+ reason: activation.reason,
177
+ ...(activation.handoffMessage
178
+ ? { handoffMessage: activation.handoffMessage }
179
+ : {}),
180
+ ...(vendored.length > 0 ? { vendored } : {}),
181
+ };
182
+ }
183
+ function createEmptyProviderCounts() {
184
+ return Object.fromEntries(AUTOPILOT_PROVIDER_ORDER.map((provider) => [provider, 0]));
185
+ }
186
+ function sortUnique(values) {
187
+ return Array.from(new Set(values)).filter(Boolean).sort();
188
+ }
189
+ function toRelativePosixPath(workspaceRoot, targetPath) {
190
+ return path.relative(workspaceRoot, targetPath).split(path.sep).join("/");
191
+ }
192
+ function normalizeDiscoveryPaths(cwd) {
193
+ const config = readRootConfig(cwd) || {};
194
+ const configured = config.paths ?? {};
195
+ const readPath = (key) => {
196
+ const configuredValue = configured[key];
197
+ if (typeof configuredValue === "string" && configuredValue.length > 0) {
198
+ return configuredValue;
199
+ }
200
+ const fallbackValue = DEFAULT_SYNC_PATHS[key];
201
+ return typeof fallbackValue === "string" ? fallbackValue : "";
202
+ };
203
+ return {
204
+ requirements: readPath("requirements"),
205
+ scenarios: readPath("scenarios"),
206
+ tests: readPath("tests"),
207
+ adr: readPath("adr"),
208
+ flags: readPath("flags"),
209
+ events: readPath("events"),
210
+ facts: readPath("facts"),
211
+ symbols: readPath("symbols"),
212
+ };
213
+ }
214
+ function buildIgnoredGlobs(vendoredRoots) {
215
+ const ignored = new Set();
216
+ for (const dirName of IGNORED_DIRECTORY_NAMES) {
217
+ ignored.add(`**/${dirName}`);
218
+ ignored.add(`**/${dirName}/**`);
219
+ }
220
+ for (const vendoredRoot of vendoredRoots) {
221
+ const normalized = vendoredRoot.replace(/\\/g, "/").replace(/^\.\//, "");
222
+ if (!normalized)
223
+ continue;
224
+ ignored.add(normalized);
225
+ ignored.add(`${normalized}/**`);
226
+ ignored.add(`**/${normalized}`);
227
+ ignored.add(`**/${normalized}/**`);
228
+ }
229
+ return Array.from(ignored);
230
+ }
231
+ function detectLanguagesFromPaths(paths) {
232
+ const detected = new Set();
233
+ for (const filePath of paths) {
234
+ const language = SOURCE_LANGUAGE_EXTENSIONS[path.extname(filePath).toLowerCase()];
235
+ if (language) {
236
+ detected.add(language);
237
+ }
238
+ }
239
+ return Array.from(detected);
240
+ }
241
+ function createFileEvidence(provider, kind, workspaceRoot, absolutePath, data = {}) {
242
+ const relativePath = toRelativePosixPath(workspaceRoot, absolutePath);
243
+ return {
244
+ provider,
245
+ kind,
246
+ label: relativePath,
247
+ relativePath,
248
+ absolutePath,
249
+ data,
250
+ };
251
+ }
252
+ function runTypedKibiDocsProvider(workspaceRoot) {
253
+ const discoveryPaths = normalizeDiscoveryPaths(workspaceRoot);
254
+ const markdownPatterns = [
255
+ normalizePattern(discoveryPaths.requirements),
256
+ normalizePattern(discoveryPaths.scenarios),
257
+ normalizePattern(discoveryPaths.tests),
258
+ normalizePattern(discoveryPaths.adr),
259
+ normalizePattern(discoveryPaths.flags),
260
+ normalizePattern(discoveryPaths.events),
261
+ normalizePattern(discoveryPaths.facts),
262
+ ].filter((pattern) => Boolean(pattern));
263
+ const markdownFiles = fg.sync(markdownPatterns, {
264
+ cwd: workspaceRoot,
265
+ absolute: true,
266
+ onlyFiles: true,
267
+ unique: true,
268
+ suppressErrors: true,
269
+ });
270
+ const manifestFiles = discoveryPaths.symbols
271
+ ? fg.sync(discoveryPaths.symbols, {
272
+ cwd: workspaceRoot,
273
+ absolute: true,
274
+ onlyFiles: true,
275
+ unique: true,
276
+ suppressErrors: true,
277
+ })
278
+ : [];
279
+ const evidence = [
280
+ ...sortUnique(markdownFiles).map((absolutePath) => createFileEvidence("typed_kibi_docs", "typed_markdown", workspaceRoot, absolutePath)),
281
+ ...sortUnique(manifestFiles).map((absolutePath) => createFileEvidence("typed_kibi_docs", "symbol_manifest", workspaceRoot, absolutePath)),
282
+ ];
283
+ return {
284
+ provider: "typed_kibi_docs",
285
+ evidence,
286
+ };
287
+ }
288
+ function runGenericRepoDocsProvider(workspaceRoot, vendoredRoots, typedFilePaths) {
289
+ const markdownFiles = fg.sync("**/*.md", {
290
+ cwd: workspaceRoot,
291
+ absolute: true,
292
+ onlyFiles: true,
293
+ unique: true,
294
+ suppressErrors: true,
295
+ ignore: buildIgnoredGlobs(vendoredRoots),
296
+ });
297
+ const evidence = sortUnique(markdownFiles)
298
+ .map((absolutePath) => createFileEvidence("generic_repo_docs", "generic_markdown", workspaceRoot, absolutePath))
299
+ .filter((item) => !typedFilePaths.has(item.relativePath ?? ""));
300
+ return {
301
+ provider: "generic_repo_docs",
302
+ evidence,
303
+ };
304
+ }
305
+ function detectLanguagesFromPackageJson(packageJson) {
306
+ const detected = new Set();
307
+ const scripts = packageJson.scripts;
308
+ const bin = packageJson.bin;
309
+ if (typeof scripts === "object" && scripts) {
310
+ for (const value of Object.values(scripts)) {
311
+ if (typeof value === "string" && /\.(cts|mts|ts|tsx)\b|\b(tsx|ts-node)\b/i.test(value)) {
312
+ detected.add("typescript");
313
+ }
314
+ if (typeof value === "string" && /\.(cjs|mjs|js|jsx)\b/i.test(value)) {
315
+ detected.add("javascript");
316
+ }
317
+ }
318
+ }
319
+ if (typeof bin === "string" && /\.(cts|mts|ts|tsx)\b/i.test(bin)) {
320
+ detected.add("typescript");
321
+ }
322
+ if (typeof bin === "object" && bin) {
323
+ for (const value of Object.values(bin)) {
324
+ if (typeof value === "string" && /\.(cts|mts|ts|tsx)\b/i.test(value)) {
325
+ detected.add("typescript");
326
+ }
327
+ }
328
+ }
329
+ return Array.from(detected);
330
+ }
331
+ function runRepoMetadataProvider(workspaceRoot) {
332
+ const patterns = [
333
+ "package.json",
334
+ "opencode.json",
335
+ "tsconfig.json",
336
+ "tsconfig.*.json",
337
+ "bun.lock",
338
+ "bun.lockb",
339
+ "bunfig.toml",
340
+ "pnpm-workspace.yaml",
341
+ "pnpm-lock.yaml",
342
+ "package-lock.json",
343
+ "yarn.lock",
344
+ "Cargo.toml",
345
+ "go.mod",
346
+ "pyproject.toml",
347
+ "requirements*.txt",
348
+ ];
349
+ const metadataFiles = fg.sync(patterns, {
350
+ cwd: workspaceRoot,
351
+ absolute: true,
352
+ onlyFiles: true,
353
+ unique: true,
354
+ suppressErrors: true,
355
+ });
356
+ const detectedLanguages = new Set();
357
+ const scanWarnings = [];
358
+ const evidence = [];
359
+ for (const absolutePath of sortUnique(metadataFiles)) {
360
+ const relativePath = toRelativePosixPath(workspaceRoot, absolutePath);
361
+ const basename = path.basename(relativePath);
362
+ const data = {
363
+ title: `Repository metadata: ${basename}`,
364
+ factKind: "meta",
365
+ confidence: basename.startsWith("tsconfig") ? 0.9 : 0.86,
366
+ evidence: [`repo_metadata:${relativePath}`],
367
+ };
368
+ if (basename.startsWith("tsconfig")) {
369
+ detectedLanguages.add("typescript");
370
+ }
371
+ if (basename === "Cargo.toml") {
372
+ detectedLanguages.add("rust");
373
+ }
374
+ if (basename === "go.mod") {
375
+ detectedLanguages.add("go");
376
+ }
377
+ if (basename === "pyproject.toml") {
378
+ detectedLanguages.add("python");
379
+ }
380
+ if (basename === "package.json") {
381
+ try {
382
+ const parsed = JSON.parse(fs.readFileSync(absolutePath, "utf8"));
383
+ for (const language of detectLanguagesFromPackageJson(parsed)) {
384
+ detectedLanguages.add(language);
385
+ }
386
+ if (typeof parsed.packageManager === "string") {
387
+ data.packageManager = parsed.packageManager;
388
+ }
389
+ }
390
+ catch (error) {
391
+ scanWarnings.push(`repo_metadata:failed_to_parse:${relativePath}`);
392
+ }
393
+ }
394
+ evidence.push(createFileEvidence("repo_metadata", "repo_metadata", workspaceRoot, absolutePath, data));
395
+ }
396
+ return {
397
+ provider: "repo_metadata",
398
+ evidence,
399
+ detectedLanguages: Array.from(detectedLanguages),
400
+ scanWarnings,
401
+ };
402
+ }
403
+ function runRepoLayoutProvider(workspaceRoot, vendoredRoots) {
404
+ const layoutRoots = ["src", "app", "apps", "packages", "tests", "test", "docs", "scripts"];
405
+ const evidence = [];
406
+ for (const relativePath of layoutRoots) {
407
+ const absolutePath = path.join(workspaceRoot, relativePath);
408
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) {
409
+ continue;
410
+ }
411
+ evidence.push({
412
+ provider: "repo_layout",
413
+ kind: "repo_layout",
414
+ label: relativePath,
415
+ relativePath,
416
+ absolutePath,
417
+ data: {
418
+ title: `Repository layout: ${relativePath} directory`,
419
+ factKind: "observation",
420
+ confidence: 0.84,
421
+ evidence: [`repo_layout:${relativePath}`],
422
+ },
423
+ });
424
+ }
425
+ const codeFiles = fg.sync([
426
+ "src/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
427
+ "app/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
428
+ "apps/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
429
+ "packages/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
430
+ "tests/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
431
+ "test/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
432
+ ], {
433
+ cwd: workspaceRoot,
434
+ absolute: true,
435
+ onlyFiles: true,
436
+ unique: true,
437
+ suppressErrors: true,
438
+ ignore: buildIgnoredGlobs(vendoredRoots),
439
+ });
440
+ return {
441
+ provider: "repo_layout",
442
+ evidence,
443
+ detectedLanguages: detectLanguagesFromPaths(codeFiles),
444
+ };
445
+ }
446
+ function detectTestFrameworksFromContent(content) {
447
+ const frameworks = new Set();
448
+ if (/\bbun:test\b/.test(content))
449
+ frameworks.add("bun:test");
450
+ if (/\bvitest\b/.test(content))
451
+ frameworks.add("vitest");
452
+ if (/\bnode:test\b/.test(content))
453
+ frameworks.add("node:test");
454
+ if (/\bmocha\b/.test(content))
455
+ frameworks.add("mocha");
456
+ if (/\bjest\b|@jest\/globals/.test(content))
457
+ frameworks.add("jest");
458
+ return Array.from(frameworks);
459
+ }
460
+ function runTestTopologyProvider(workspaceRoot, vendoredRoots) {
461
+ const testFiles = fg.sync([
462
+ "**/*.test.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
463
+ "**/*.spec.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
464
+ "**/__tests__/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
465
+ ], {
466
+ cwd: workspaceRoot,
467
+ absolute: true,
468
+ onlyFiles: true,
469
+ unique: true,
470
+ suppressErrors: true,
471
+ ignore: buildIgnoredGlobs(vendoredRoots),
472
+ });
473
+ const detectedFrameworks = new Set();
474
+ const detectedLanguages = new Set();
475
+ const scanWarnings = [];
476
+ const evidence = [];
477
+ for (const absolutePath of sortUnique(testFiles)) {
478
+ const relativePath = toRelativePosixPath(workspaceRoot, absolutePath);
479
+ const frameworks = (() => {
480
+ try {
481
+ return detectTestFrameworksFromContent(fs.readFileSync(absolutePath, "utf8"));
482
+ }
483
+ catch (error) {
484
+ scanWarnings.push(`test_topology:failed_to_read:${relativePath}`);
485
+ return [];
486
+ }
487
+ })();
488
+ for (const framework of frameworks) {
489
+ detectedFrameworks.add(framework);
490
+ }
491
+ for (const language of detectLanguagesFromPaths([absolutePath])) {
492
+ detectedLanguages.add(language);
493
+ }
494
+ evidence.push(createFileEvidence("test_topology", "test_topology", workspaceRoot, absolutePath, {
495
+ title: frameworks.length > 0
496
+ ? `Test topology: ${frameworks.join(", ")} in ${relativePath}`
497
+ : `Test topology: ${relativePath}`,
498
+ factKind: "observation",
499
+ confidence: frameworks.length > 0 ? 0.92 : 0.85,
500
+ evidence: [
501
+ `test_topology:${relativePath}`,
502
+ ...frameworks.map((framework) => `framework:${framework}`),
503
+ ],
504
+ frameworks,
505
+ }));
506
+ }
507
+ return {
508
+ provider: "test_topology",
509
+ evidence,
510
+ detectedLanguages: Array.from(detectedLanguages),
511
+ detectedTestFrameworks: Array.from(detectedFrameworks),
512
+ scanWarnings,
513
+ };
514
+ }
515
+ function runSourceSymbolsProvider(workspaceRoot, vendoredRoots) {
516
+ const analyzeSourceText = cliSymbolCoordinator.analyzeSourceText;
517
+ const sourceFiles = fg.sync([
518
+ "src/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
519
+ "app/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
520
+ "apps/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
521
+ "packages/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,py,rb,go,rs,java,kt,swift,php,c,cc,cpp,h,hpp}",
522
+ ], {
523
+ cwd: workspaceRoot,
524
+ absolute: true,
525
+ onlyFiles: true,
526
+ unique: true,
527
+ suppressErrors: true,
528
+ ignore: buildIgnoredGlobs(vendoredRoots),
529
+ });
530
+ const evidence = [];
531
+ const detectedLanguages = new Set();
532
+ const scanWarnings = [];
533
+ for (const absolutePath of sortUnique(sourceFiles)) {
534
+ const relativePath = toRelativePosixPath(workspaceRoot, absolutePath);
535
+ const language = SOURCE_LANGUAGE_EXTENSIONS[path.extname(absolutePath).toLowerCase()] ?? "unknown";
536
+ detectedLanguages.add(language);
537
+ try {
538
+ const content = fs.readFileSync(absolutePath, "utf8");
539
+ const analysis = analyzeSourceText
540
+ ? analyzeSourceText(relativePath, content)
541
+ : {
542
+ sourceFile: relativePath,
543
+ language,
544
+ providerId: null,
545
+ module: {
546
+ title: path.basename(relativePath, path.extname(relativePath)) || relativePath,
547
+ analysisMode: "fallback",
548
+ fallbackReason: "provider_unavailable",
549
+ },
550
+ symbols: [],
551
+ };
552
+ if (analysis.symbols.length > 0) {
553
+ evidence.push({
554
+ provider: "source_symbols",
555
+ kind: "source_symbols",
556
+ label: relativePath,
557
+ relativePath,
558
+ absolutePath,
559
+ data: {
560
+ title: `Source symbols: ${analysis.module.title}`,
561
+ factKind: "observation",
562
+ confidence: 0.9,
563
+ evidence: [
564
+ `source_symbols:${relativePath}`,
565
+ `language:${analysis.language}`,
566
+ `provider:${analysis.providerId ?? "fallback"}`,
567
+ ...analysis.symbols
568
+ .slice(0, 5)
569
+ .map((symbol) => `symbol:${symbol.kind}:${symbol.name}`),
570
+ ],
571
+ analysisMode: analysis.module.analysisMode,
572
+ providerId: analysis.providerId,
573
+ symbolCount: analysis.symbols.length,
574
+ },
575
+ });
576
+ continue;
577
+ }
578
+ evidence.push({
579
+ provider: "source_symbols",
580
+ kind: "source_symbols",
581
+ label: relativePath,
582
+ relativePath,
583
+ absolutePath,
584
+ data: {
585
+ title: `Source module: ${analysis.module.title}`,
586
+ factKind: "observation",
587
+ confidence: 0.82,
588
+ evidence: [
589
+ `source_symbols:${relativePath}`,
590
+ `language:${analysis.language}`,
591
+ `analysis_mode:${analysis.module.analysisMode}`,
592
+ ...(analysis.module.fallbackReason
593
+ ? [`fallback:${analysis.module.fallbackReason}`]
594
+ : []),
595
+ ],
596
+ analysisMode: analysis.module.analysisMode,
597
+ fallbackReason: analysis.module.fallbackReason,
598
+ providerId: analysis.providerId,
599
+ symbolCount: 0,
600
+ },
601
+ });
602
+ }
603
+ catch {
604
+ scanWarnings.push(`source_symbols:failed_to_analyze:${relativePath}`);
605
+ }
606
+ }
607
+ return {
608
+ provider: "source_symbols",
609
+ evidence,
610
+ detectedLanguages: Array.from(detectedLanguages),
611
+ scanWarnings,
612
+ };
613
+ }
614
+ function buildDiscoverySummary(activation, vendored, providerResults) {
615
+ const providerCounts = createEmptyProviderCounts();
616
+ const detectedLanguages = new Set();
617
+ const detectedTestFrameworks = new Set();
618
+ const scanWarnings = [];
619
+ let truncated = false;
620
+ for (const result of providerResults) {
621
+ providerCounts[result.provider] = result.evidence.length;
622
+ for (const language of result.detectedLanguages ?? []) {
623
+ detectedLanguages.add(language);
624
+ }
625
+ for (const framework of result.detectedTestFrameworks ?? []) {
626
+ detectedTestFrameworks.add(framework);
627
+ }
628
+ scanWarnings.push(...(result.scanWarnings ?? []));
629
+ truncated ||= Boolean(result.truncated);
630
+ }
631
+ return {
632
+ ...buildSourceSummary(activation, vendored),
633
+ providersRun: providerResults.map((result) => result.provider),
634
+ providerCounts,
635
+ detectedLanguages: Array.from(detectedLanguages).sort(),
636
+ detectedTestFrameworks: Array.from(detectedTestFrameworks).sort(),
637
+ excludedRoots: Array.from(IGNORED_DIRECTORY_NAMES).sort(),
638
+ truncated,
639
+ scanWarnings: sortUnique(scanWarnings),
640
+ };
641
+ }
642
+ // implements REQ-001
643
+ export function discoverProviderEvidence(workspaceRoot, activation) {
644
+ const vendored = findVendoredTrees(workspaceRoot);
645
+ if (!activation.allowCandidateGeneration) {
646
+ return {
647
+ evidence: [],
648
+ providerResults: [],
649
+ summary: {
650
+ ...buildSourceSummary(activation, vendored),
651
+ providersRun: [],
652
+ providerCounts: createEmptyProviderCounts(),
653
+ detectedLanguages: [],
654
+ detectedTestFrameworks: [],
655
+ excludedRoots: Array.from(IGNORED_DIRECTORY_NAMES).sort(),
656
+ truncated: false,
657
+ scanWarnings: [],
658
+ },
659
+ };
660
+ }
661
+ const typedKibiDocs = runTypedKibiDocsProvider(workspaceRoot);
662
+ const typedPaths = new Set(typedKibiDocs.evidence
663
+ .map((item) => item.relativePath)
664
+ .filter((item) => Boolean(item)));
665
+ const providerResults = [
666
+ typedKibiDocs,
667
+ runGenericRepoDocsProvider(workspaceRoot, vendored, typedPaths),
668
+ runRepoMetadataProvider(workspaceRoot),
669
+ runRepoLayoutProvider(workspaceRoot, vendored),
670
+ runTestTopologyProvider(workspaceRoot, vendored),
671
+ runSourceSymbolsProvider(workspaceRoot, vendored),
672
+ ];
673
+ const evidence = providerResults.flatMap((result) => result.evidence);
674
+ evidence.sort((left, right) => {
675
+ const providerCompare = AUTOPILOT_PROVIDER_ORDER.indexOf(left.provider) -
676
+ AUTOPILOT_PROVIDER_ORDER.indexOf(right.provider);
677
+ if (providerCompare !== 0)
678
+ return providerCompare;
679
+ const leftKey = left.relativePath ?? left.label;
680
+ const rightKey = right.relativePath ?? right.label;
681
+ return leftKey.localeCompare(rightKey);
682
+ });
683
+ return {
684
+ evidence,
685
+ providerResults,
686
+ summary: buildDiscoverySummary(activation, vendored, providerResults),
687
+ };
688
+ }
689
+ function toActivationPolicy(activationState) {
690
+ switch (activationState) {
691
+ case "root_partial":
692
+ return {
693
+ activationState,
694
+ activationMode: "repair_bootstrap",
695
+ applyBlocked: true,
696
+ allowCandidateGeneration: true,
697
+ reason: "Workspace root is only partially configured; run a repair bootstrap scan and keep apply blocked until the root is repaired.",
698
+ };
699
+ case "root_active_thin":
700
+ return {
701
+ activationState,
702
+ activationMode: "attached_thin_handoff",
703
+ applyBlocked: true,
704
+ allowCandidateGeneration: false,
705
+ reason: "Workspace already has an attached but thin KB; bootstrap synthesis is replaced by an explicit thin handoff.",
706
+ handoffMessage: "Attached thin KB detected. Review the sparse KB coverage and continue with a handoff instead of a bootstrap apply plan.",
707
+ };
708
+ case "root_active_seeded":
709
+ return {
710
+ activationState,
711
+ activationMode: "attached_seeded_handoff",
712
+ applyBlocked: true,
713
+ allowCandidateGeneration: false,
714
+ reason: "Workspace already has an attached seeded KB; bootstrap synthesis is replaced by an explicit seeded handoff.",
715
+ handoffMessage: "Attached seeded KB detected. Use the existing KB context instead of generating bootstrap candidates.",
716
+ };
717
+ case "vendored_only":
718
+ return {
719
+ activationState,
720
+ activationMode: "vendored_blocked",
721
+ applyBlocked: true,
722
+ allowCandidateGeneration: false,
723
+ reason: "Workspace appears to contain vendored Kibi sources only; bootstrap generation is blocked in this posture.",
724
+ handoffMessage: "Vendored Kibi posture detected. Move to the real project root before attempting bootstrap.",
725
+ };
726
+ case "root_uninitialized":
727
+ return {
728
+ activationState,
729
+ activationMode: "cold_start_bootstrap",
730
+ applyBlocked: false,
731
+ allowCandidateGeneration: true,
732
+ reason: "Workspace has no attached root KB yet; run a cold-start bootstrap scan across repository evidence.",
733
+ };
734
+ }
735
+ }
736
+ // implements REQ-mcp-init-kibi-autopilot-v1
737
+ export async function resolveActivationPolicy(workspaceRoot, prolog) {
738
+ return toActivationPolicy(await classifyActivationState(workspaceRoot, prolog));
739
+ }
83
740
  function rootTargetsAllResolve(cwd) {
84
741
  const config = readRootConfig(cwd) || {};
85
742
  const paths = config.paths ?? {};
@@ -116,7 +773,9 @@ function rootTargetsAllResolve(cwd) {
116
773
  export async function classifyActivationState(workspaceRoot, prolog) {
117
774
  const hasRootConfig = rootKbConfigExists(workspaceRoot);
118
775
  const vendored = findVendoredTrees(workspaceRoot);
119
- if (!hasRootConfig && vendored.length > 0) {
776
+ if (!hasRootConfig &&
777
+ vendored.length > 0 &&
778
+ !hasWorkspaceProjectSignals(workspaceRoot, vendored)) {
120
779
  return "vendored_only";
121
780
  }
122
781
  if (!hasRootConfig) {
@@ -166,11 +825,11 @@ function collectMarkdownFiles(dir, workspaceRoot, vendoredRoots) {
166
825
  const stat = fs.statSync(dir);
167
826
  if (!stat.isDirectory())
168
827
  return results;
169
- const entries = fs.readdirSync(dir);
828
+ const entries = fs.readdirSync(dir).sort();
170
829
  for (const entry of entries) {
171
830
  const full = path.join(dir, entry);
172
831
  // Skip ignores
173
- if (entry === ".git" || entry === "node_modules" || entry === ".kb")
832
+ if (IGNORED_DIRECTORY_NAMES.has(entry.toLowerCase()))
174
833
  continue;
175
834
  // Skip vendored roots
176
835
  const rel = path.relative(workspaceRoot, full).split(path.sep).join("/");
@@ -189,55 +848,22 @@ function collectMarkdownFiles(dir, workspaceRoot, vendoredRoots) {
189
848
  }
190
849
  /** Discover eligible source inputs for autopilot. */
191
850
  // 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;
851
+ export function discoverSources(workspaceRoot, activation) {
852
+ const discovery = discoverProviderEvidence(workspaceRoot, activation);
200
853
  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("/"));
854
+ for (const item of discovery.evidence) {
855
+ if (item.kind === "typed_markdown" ||
856
+ item.kind === "symbol_manifest" ||
857
+ item.kind === "generic_markdown" ||
858
+ item.kind === "source_symbols") {
859
+ const relativePath = item.relativePath;
860
+ if (relativePath) {
861
+ candidates.add(relativePath);
211
862
  }
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
863
  }
238
864
  }
239
865
  return {
240
866
  candidates: Array.from(candidates).sort(),
241
- summary: { activationState, reason: "discovered sources", vendored },
867
+ summary: discovery.summary,
242
868
  };
243
869
  }