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.
- package/dist/server/docs.js +64 -50
- package/dist/server/tools.js +12 -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/briefing-generate.js +529 -0
- package/dist/tools-config.js +61 -1
- package/package.json +2 -2
package/dist/server/docs.js
CHANGED
|
@@ -35,70 +35,83 @@ function renderToolsDoc() {
|
|
|
35
35
|
: "none";
|
|
36
36
|
}
|
|
37
37
|
lines.push("");
|
|
38
|
-
lines.push("Modeling note:
|
|
38
|
+
lines.push("Modeling note: Kibi has eight core entity types grouped into common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).");
|
|
39
|
+
lines.push("Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; use `flag` for runtime/config gates and `fact_kind: observation` or `meta` for bug/workaround notes.");
|
|
39
40
|
return lines.join("\n");
|
|
40
41
|
}
|
|
41
42
|
export const PROMPTS = [
|
|
42
43
|
// implements REQ-002, REQ-013, REQ-mcp-search-discovery
|
|
43
44
|
{
|
|
44
45
|
name: "init-kibi",
|
|
45
|
-
description: "
|
|
46
|
+
description: "Activation workflow to populate a new or empty Kibi KB from an existing repository.",
|
|
46
47
|
text: [
|
|
47
|
-
"# Kibi
|
|
48
|
+
"# Kibi Activation Workflow",
|
|
48
49
|
"",
|
|
49
|
-
"Use this workflow to
|
|
50
|
+
"Use this workflow to populate a Kibi knowledge base when it is new or empty.",
|
|
50
51
|
"",
|
|
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)",
|
|
52
|
+
"## Step 1: Generate Candidates (read-only)",
|
|
59
53
|
"",
|
|
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",
|
|
54
|
+
"Call `kb_autopilot_generate` to scan the repository and produce candidate entities.",
|
|
64
55
|
"",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
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
|
-
"##
|
|
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
|
-
"
|
|
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
|
-
"
|
|
83
|
-
"1
|
|
84
|
-
"
|
|
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
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
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
|
-
"##
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
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
|
|
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
|
-
"-
|
|
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
|
-
"-
|
|
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).
|
|
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',
|
package/dist/server/tools.js
CHANGED
|
@@ -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
|
+
}
|