kibi-mcp 0.7.1 → 0.9.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.
@@ -35,70 +35,83 @@ function renderToolsDoc() {
35
35
  : "none";
36
36
  }
37
37
  lines.push("");
38
- lines.push("Modeling note: Prefer query-first discovery; create `fact` entities before `req` entities and express semantics via `constrains` + `requires_property`.");
38
+ lines.push("Modeling note: Kibi has eight core entity types grouped into common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).");
39
+ lines.push("Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; use `flag` for runtime/config gates and `fact_kind: observation` or `meta` for bug/workaround notes.");
39
40
  return lines.join("\n");
40
41
  }
41
42
  export const PROMPTS = [
42
43
  // implements REQ-002, REQ-013, REQ-mcp-search-discovery
43
44
  {
44
45
  name: "init-kibi",
45
- description: "Bootstrap Kibi on an existing repository with zero entities.",
46
+ description: "Activation workflow to populate a new or empty Kibi KB from an existing repository.",
46
47
  text: [
47
- "# Kibi Initialization Workflow",
48
+ "# Kibi Activation Workflow",
48
49
  "",
49
- "Use this workflow to retroactively bootstrap Kibi on an existing repository with zero entities.",
50
+ "Use this workflow to populate a Kibi knowledge base when it is new or empty.",
50
51
  "",
51
- "## Phase 1: Discovery",
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)",
52
+ "## Step 1: Generate Candidates (read-only)",
59
53
  "",
60
- "## Phase 2: Fact Extraction",
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",
54
+ "Call `kb_autopilot_generate` to scan the repository and produce candidate entities.",
64
55
  "",
65
- "## Phase 3: Requirement Encoding",
66
- "1. Extract requirements from documentation",
67
- "2. For each requirement, determine which facts it constrains or requires",
68
- "3. Create req entities with `kb_upsert` using type 'req'",
69
- "4. Link reqs to facts using `constrains` and `requires_property` relationships",
70
- "5. Reuse fact IDs across related requirements for contradiction detection",
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",
71
61
  "",
72
- "## Phase 4: Test Linking",
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",
62
+ "## Step 2: Review Candidates",
76
63
  "",
77
- "## Phase 5: Architecture Documentation",
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`",
64
+ "Inspect `activationState`. If `applyBlocked` is true, stop — the KB cannot accept writes.",
81
65
  "",
82
- "## Phase 6: Event Catalog",
83
- "1. Identify domain/system events from code",
84
- "2. Create event entities with `kb_upsert` using type 'event'",
85
- "3. Link symbols that publish/consume events using `publishes`/`consumes`",
66
+ "For each candidate, evaluate:",
67
+ "- **confidence** (0–1): prefer high-confidence entities first",
68
+ "- **evidence**: verify the source reference is real before applying",
86
69
  "",
87
- "## Phase 7: Symbol Mapping",
88
- "1. Map key code symbols to requirements",
89
- "2. Create symbol entities with `kb_upsert` using type 'symbol'",
90
- "3. Link symbols to requirements using `implements`",
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",
91
76
  "",
92
- "## Phase 8: Validation",
93
- "1. Run `kb_check` with all rules to verify integrity",
94
- "2. Fix any dangling references or constraint violations",
95
- "3. Re-run validation until clean",
77
+ "## Step 4: Payoff Verification",
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",
96
84
  "",
97
- "## Best Practices",
98
- "- Start with high-value entities first (critical requirements, security constraints)",
99
- "- Use incremental batches to avoid overwhelming the KB",
100
85
  "- Always call `kb_query` before creating to avoid duplicate entities",
101
- "- Run `kb_check` after each batch of changes",
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",
89
+ ].join("\n"),
90
+ },
91
+ {
92
+ name: "brief-kibi",
93
+ description: "Start-task workflow for generating a citation-backed Kibi briefing before risky work.",
94
+ text: [
95
+ "# Kibi Briefing Workflow",
96
+ "",
97
+ "Use this workflow at the start of a task when you need a deterministic, citation-backed Kibi briefing.",
98
+ "",
99
+ "## Step 1: Generate the briefing",
100
+ "",
101
+ "Call `kb_briefing_generate` with any relevant `taskText`, `sourceFiles`, and `seedIds`.",
102
+ "",
103
+ "This tool is read-only. It returns `briefingState`, `activationState`, `activationReason`, `freshness`, `confidence`, `tldr`, `promptBlock`, `entities`, `constraints`, `regressionRisks`, `missingEvidence`, and `citations`.",
104
+ "",
105
+ "## Step 2: Inspect readiness",
106
+ "",
107
+ "Inspect `briefingState` before acting.",
108
+ "- If `briefingState` is `ready`, continue using only cited output from the briefing.",
109
+ "- If `briefingState` is `no_briefing`, stop and proceed without inventing briefing claims.",
110
+ "",
111
+ "## Step 3: Use the cited output",
112
+ "",
113
+ "Use `constraints`, `regressionRisks`, `missingEvidence`, and `promptBlock` only when their claims are backed by the returned `citations` and cited `entities`.",
114
+ "Do not add uncited assertions, and do not treat omitted topics as verified.",
102
115
  ].join("\n"),
103
116
  },
104
117
  {
@@ -121,14 +134,14 @@ export const PROMPTS = [
121
134
  "- `kb_check`: Validate KB integrity against configurable rules",
122
135
  "",
123
136
  "Core modeling principles:",
137
+ "- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
124
138
  "- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
125
- "- Reuse canonical fact IDs across requirements; shared constrained facts make contradictions detectable.",
139
+ "- Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; observation and meta facts are non-blocking notes.",
126
140
  "- Use `kb_search` first for discovery, then `kb_query` for exact follow-up before any mutation.",
127
141
  "- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
128
142
  "- Run `kb_check` after meaningful mutations to catch integrity issues early.",
129
143
  "- Prefer explicit IDs and enum values to avoid invalid parameters.",
130
- "- Assume every write can affect downstream traceability queries.",
131
- "- Model requirements by first creating/reusing fact entities, then express req semantics with `constrains` + `requires_property` relationships (create-before-link).",
144
+ "- Model requirements by first creating/reusing fact entities (create-before-link).",
132
145
  "- flag gates runtime/config behavior; use `fact` with `fact_kind: observation` or `meta` for bug and workaround notes.",
133
146
  ].join("\n"),
134
147
  },
@@ -211,7 +224,8 @@ function registerDocResources() {
211
224
  "4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
212
225
  '5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
213
226
  "",
214
- "Note: Create or reuse `fact` entities first, then create `req` entities and link with `constrains` and `requires_property` (create-before-link). Use `flag` for runtime/config gates; use `fact` with `fact_kind: observation` or `meta` for bug and workaround notes.",
227
+ "Note: Kibi has eight core entity types. Create or reuse `fact` entities first, then create `req` entities and link with `constrains` and `requires_property` (create-before-link).",
228
+ "Only strict domain facts are contradiction-safe. Use `flag` for runtime/config gates; use `fact` with `fact_kind: observation` or `meta` for bug/workaround notes.",
215
229
  "",
216
230
  "## Find missing coverage",
217
231
  '1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
@@ -11,6 +11,8 @@ 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";
15
+ import { handleKbBriefingGenerate, } from "../tools/briefing-generate.js";
14
16
  const defaultToolsServerDeps = {
15
17
  getSessionModule: () => import("./session.js"),
16
18
  };
@@ -57,6 +59,8 @@ const DEFAULT_TOOLS_RUNTIME = {
57
59
  handleKbSearch,
58
60
  handleKbStatus,
59
61
  handleKbUpsert,
62
+ handleKbAutopilotGenerate,
63
+ handleKbBriefingGenerate,
60
64
  };
61
65
  // implements REQ-008
62
66
  function debugLog(...args) {
@@ -317,4 +321,12 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
317
321
  const prolog = await runtime.ensureProlog();
318
322
  return runtime.handleKbCheck(prolog, args);
319
323
  }, runtime);
324
+ addTool(server, "kb_autopilot_generate", toolDef("kb_autopilot_generate").description, toolDef("kb_autopilot_generate").inputSchema, async (args) => {
325
+ const prolog = await runtime.ensureProlog();
326
+ return runtime.handleKbAutopilotGenerate(prolog, args);
327
+ }, runtime);
328
+ addTool(server, "kb_briefing_generate", toolDef("kb_briefing_generate").description, toolDef("kb_briefing_generate").inputSchema, async (args) => {
329
+ const prolog = await runtime.ensureProlog();
330
+ return runtime.handleKbBriefingGenerate(prolog, args);
331
+ }, runtime);
320
332
  }
@@ -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
+ }