kibi-cli 0.10.1 → 0.11.1
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/cli.d.ts.map +1 -1
- package/dist/cli.js +45 -0
- package/dist/commands/skills.d.ts +14 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +86 -0
- package/dist/commands/usage-metrics.d.ts +8 -0
- package/dist/commands/usage-metrics.d.ts.map +1 -0
- package/dist/commands/usage-metrics.js +323 -0
- package/dist/public/skills/kibi-usage/SKILL.md +125 -0
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +45 -0
- package/dist/public/skills/kibi-usage/resources/relationship-directions.md +89 -0
- package/dist/public/skills/kibi-usage/resources/workflows.md +35 -0
- package/dist/public/skills.d.ts +42 -0
- package/dist/public/skills.d.ts.map +1 -0
- package/dist/public/skills.js +262 -0
- package/package.json +6 -2
- package/src/public/skills/kibi-usage/SKILL.md +125 -0
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +45 -0
- package/src/public/skills/kibi-usage/resources/relationship-directions.md +89 -0
- package/src/public/skills/kibi-usage/resources/workflows.md +35 -0
- package/src/public/skills.ts +359 -0
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA8CA,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
package/dist/cli.js
CHANGED
|
@@ -28,8 +28,10 @@ import { initCommand } from "./commands/init.js";
|
|
|
28
28
|
import { migrateCommand } from "./commands/migrate.js";
|
|
29
29
|
import { queryCommand } from "./commands/query.js";
|
|
30
30
|
import { searchCommand } from "./commands/search.js";
|
|
31
|
+
import { skillsListCommand, skillsLoadCommand, skillsReadCommand, skillsValidateCommand, } from "./commands/skills.js";
|
|
31
32
|
import { statusCommand } from "./commands/status.js";
|
|
32
33
|
import { syncCommand } from "./commands/sync.js";
|
|
34
|
+
import { usageMetricsCommand } from "./commands/usage-metrics.js";
|
|
33
35
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
34
36
|
const VERSION = packageJson.version ?? "0.1.0";
|
|
35
37
|
// implements REQ-003
|
|
@@ -167,6 +169,49 @@ program
|
|
|
167
169
|
.command("doctor")
|
|
168
170
|
.description("Diagnose KB setup and configuration")
|
|
169
171
|
.action(withExitCode(async () => doctorCommand()));
|
|
172
|
+
program
|
|
173
|
+
.command("usage-metrics")
|
|
174
|
+
.description("Report usage and quality metrics from .kb/usage.log")
|
|
175
|
+
.option("--format <format>", "Output format: json|table", "table")
|
|
176
|
+
.option("--limit <n>", "Limit top zero-result source files", "10")
|
|
177
|
+
.action(withExitCode(async (options) => {
|
|
178
|
+
return usageMetricsCommand(options);
|
|
179
|
+
}));
|
|
180
|
+
const skillsProgram = program
|
|
181
|
+
.command("skills")
|
|
182
|
+
.description("Manage bundled markdown skills");
|
|
183
|
+
skillsProgram
|
|
184
|
+
.command("list")
|
|
185
|
+
.description("List bundled markdown skills")
|
|
186
|
+
.option("--format <format>", "Output format: json|table", "table")
|
|
187
|
+
.action(withExitCode(async (options) => {
|
|
188
|
+
return skillsListCommand(options);
|
|
189
|
+
}));
|
|
190
|
+
skillsProgram
|
|
191
|
+
.command("load")
|
|
192
|
+
.description("Load a bundled markdown skill")
|
|
193
|
+
.argument("<id>", "Bundled skill ID")
|
|
194
|
+
.option("--format <format>", "Output format: json|markdown", "markdown")
|
|
195
|
+
.action(withExitCode(async (id, options) => {
|
|
196
|
+
return skillsLoadCommand(id, options);
|
|
197
|
+
}));
|
|
198
|
+
skillsProgram
|
|
199
|
+
.command("read")
|
|
200
|
+
.description("Read a declared bundled skill resource")
|
|
201
|
+
.argument("<id>", "Bundled skill ID")
|
|
202
|
+
.argument("<resource>", "Declared resource path")
|
|
203
|
+
.option("--format <format>", "Output format: text|json", "text")
|
|
204
|
+
.action(withExitCode(async (id, resource, options) => {
|
|
205
|
+
return skillsReadCommand(id, resource, options);
|
|
206
|
+
}));
|
|
207
|
+
skillsProgram
|
|
208
|
+
.command("validate")
|
|
209
|
+
.description("Validate a bundled markdown skill path")
|
|
210
|
+
.argument("<path>", "Skill bundle directory or SKILL.md path")
|
|
211
|
+
.option("--format <format>", "Output format: json|table", "table")
|
|
212
|
+
.action(withExitCode(async (pathLike, options) => {
|
|
213
|
+
return skillsValidateCommand(pathLike, options);
|
|
214
|
+
}));
|
|
170
215
|
program
|
|
171
216
|
.command("branch")
|
|
172
217
|
.description("Manage branch KBs")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CommandResult } from "../cli.js";
|
|
2
|
+
interface FormatOptions<TFormat extends string> {
|
|
3
|
+
format?: TFormat;
|
|
4
|
+
}
|
|
5
|
+
type ListFormat = "json" | "table";
|
|
6
|
+
type LoadFormat = "json" | "markdown";
|
|
7
|
+
type ReadFormat = "text" | "json";
|
|
8
|
+
type ValidateFormat = "json" | "table";
|
|
9
|
+
export declare function skillsListCommand(options: FormatOptions<ListFormat>): Promise<CommandResult | undefined>;
|
|
10
|
+
export declare function skillsLoadCommand(id: string, options: FormatOptions<LoadFormat>): Promise<CommandResult | undefined>;
|
|
11
|
+
export declare function skillsReadCommand(id: string, resource: string, options: FormatOptions<ReadFormat>): Promise<CommandResult | undefined>;
|
|
12
|
+
export declare function skillsValidateCommand(pathLike: string, options: FormatOptions<ValidateFormat>): Promise<CommandResult | undefined>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAU/C,UAAU,aAAa,CAAC,OAAO,SAAS,MAAM;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,KAAK,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;AACnC,KAAK,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC;AACtC,KAAK,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAClC,KAAK,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAGvC,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAUpC;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAgBpC;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAUpC;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,CAAC,cAAc,CAAC,GACrC,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAepC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
import { listBundledSkills, loadBundledSkill, readBundledSkillResource, validateSkillBundle, } from "kibi-cli/skills";
|
|
4
|
+
// implements REQ-003
|
|
5
|
+
export async function skillsListCommand(options) {
|
|
6
|
+
return handleSkillCommand(() => {
|
|
7
|
+
const skills = listBundledSkills();
|
|
8
|
+
if (options.format === "json") {
|
|
9
|
+
console.log(JSON.stringify(skills, null, 2));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
console.log(renderSkillsTable(skills));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function skillsLoadCommand(id, options) {
|
|
16
|
+
return handleSkillCommand(() => {
|
|
17
|
+
const bundle = loadBundledSkill(id);
|
|
18
|
+
if (options.format === "json") {
|
|
19
|
+
console.log(JSON.stringify({
|
|
20
|
+
metadata: bundle.manifest,
|
|
21
|
+
body: bundle.body,
|
|
22
|
+
resources: bundle.manifest.resources ?? [],
|
|
23
|
+
contentHash: createHash("sha256").update(bundle.body).digest("hex"),
|
|
24
|
+
sourceType: "bundled",
|
|
25
|
+
}, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
process.stdout.write(bundle.body.endsWith("\n") ? bundle.body : `${bundle.body}\n`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function skillsReadCommand(id, resource, options) {
|
|
32
|
+
return handleSkillCommand(() => {
|
|
33
|
+
const contents = readBundledSkillResource(id, resource);
|
|
34
|
+
if (options.format === "json") {
|
|
35
|
+
console.log(JSON.stringify({ id, resource, contents }, null, 2));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
process.stdout.write(contents.endsWith("\n") ? contents : `${contents}\n`);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export async function skillsValidateCommand(pathLike, options) {
|
|
42
|
+
return handleSkillCommand(() => {
|
|
43
|
+
const result = validateSkillBundle(pathLike);
|
|
44
|
+
const printable = {
|
|
45
|
+
valid: result.valid,
|
|
46
|
+
errors: result.errors.map(formatValidationError),
|
|
47
|
+
};
|
|
48
|
+
if (options.format === "json") {
|
|
49
|
+
console.log(JSON.stringify(printable, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.log(renderValidationTable(printable));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function handleSkillCommand(action) {
|
|
56
|
+
try {
|
|
57
|
+
action();
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
console.error(message);
|
|
63
|
+
return { exitCode: 1 };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function renderSkillsTable(skills) {
|
|
67
|
+
const table = new Table({ head: ["ID", "Name", "Version", "Description"] });
|
|
68
|
+
for (const skill of skills) {
|
|
69
|
+
table.push([skill.id, skill.name, skill.version, skill.description]);
|
|
70
|
+
}
|
|
71
|
+
return table.toString();
|
|
72
|
+
}
|
|
73
|
+
function formatValidationError(error) {
|
|
74
|
+
return { field: error.field, message: error.message };
|
|
75
|
+
}
|
|
76
|
+
function renderValidationTable(result) {
|
|
77
|
+
const table = new Table({ head: ["Valid", "Field", "Message"] });
|
|
78
|
+
if (result.errors.length === 0) {
|
|
79
|
+
table.push([String(result.valid), "", ""]);
|
|
80
|
+
return table.toString();
|
|
81
|
+
}
|
|
82
|
+
for (const error of result.errors) {
|
|
83
|
+
table.push([String(result.valid), error.field, error.message]);
|
|
84
|
+
}
|
|
85
|
+
return table.toString();
|
|
86
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CommandResult } from "../cli.js";
|
|
2
|
+
interface UsageMetricsOptions {
|
|
3
|
+
format?: "json" | "table";
|
|
4
|
+
limit?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function usageMetricsCommand(options: UsageMetricsOptions): Promise<CommandResult | undefined>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=usage-metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-metrics.d.ts","sourceRoot":"","sources":["../../src/commands/usage-metrics.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,UAAU,mBAAmB;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAuBpC"}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import Table from "cli-table3";
|
|
4
|
+
// implements REQ-003
|
|
5
|
+
export async function usageMetricsCommand(options) {
|
|
6
|
+
const limit = Number.parseInt(options.limit || "10", 10);
|
|
7
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
8
|
+
console.error("Error: --limit must be a positive integer");
|
|
9
|
+
return { exitCode: 1 };
|
|
10
|
+
}
|
|
11
|
+
const usageLogPath = path.join(process.cwd(), ".kb", "usage.log");
|
|
12
|
+
if (!existsSync(usageLogPath)) {
|
|
13
|
+
console.error(`Error: usage log not found at ${usageLogPath}`);
|
|
14
|
+
return { exitCode: 1 };
|
|
15
|
+
}
|
|
16
|
+
const rows = parseUsageLog(readFileSync(usageLogPath, "utf8"));
|
|
17
|
+
const report = buildUsageMetricsReport(rows, limit);
|
|
18
|
+
if (options.format === "json") {
|
|
19
|
+
console.log(JSON.stringify(report, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(renderUsageMetricsReport(report));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
function parseUsageLog(contents) {
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (const [index, line] of contents.split(/\r?\n/).entries()) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(trimmed);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
throw new Error(`Failed to parse .kb/usage.log line ${index + 1}: ${message}`);
|
|
39
|
+
}
|
|
40
|
+
if (!parsed || typeof parsed !== "object") {
|
|
41
|
+
throw new Error(`Failed to parse .kb/usage.log line ${index + 1}: expected object`);
|
|
42
|
+
}
|
|
43
|
+
rows.push(parsed);
|
|
44
|
+
}
|
|
45
|
+
return rows;
|
|
46
|
+
}
|
|
47
|
+
function buildUsageMetricsReport(rows, limit) {
|
|
48
|
+
const timestamps = rows
|
|
49
|
+
.map((row) => row.timestamp)
|
|
50
|
+
.filter((value) => typeof value === "string")
|
|
51
|
+
.sort((left, right) => left.localeCompare(right));
|
|
52
|
+
const toolCounts = new Map();
|
|
53
|
+
const branchCounts = new Map();
|
|
54
|
+
const zeroResultToolCounts = new Map();
|
|
55
|
+
const zeroResultSourceFileCounts = new Map();
|
|
56
|
+
const upsertErrorCategories = new Map();
|
|
57
|
+
const violationTrend = [];
|
|
58
|
+
let successCount = 0;
|
|
59
|
+
let errorCount = 0;
|
|
60
|
+
let telemetryCompleteCount = 0;
|
|
61
|
+
let telemetryMissingCount = 0;
|
|
62
|
+
let zeroResultCount = 0;
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
increment(toolCounts, normalizeKey(row.tool, "unknown"));
|
|
65
|
+
increment(branchCounts, normalizeKey(row.active_branch || row.branch, "unknown"));
|
|
66
|
+
const outcome = getOutcome(row);
|
|
67
|
+
if (outcome === "success") {
|
|
68
|
+
successCount += 1;
|
|
69
|
+
}
|
|
70
|
+
if (outcome === "error") {
|
|
71
|
+
errorCount += 1;
|
|
72
|
+
}
|
|
73
|
+
if (hasCompleteTelemetry(row)) {
|
|
74
|
+
telemetryCompleteCount += 1;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
telemetryMissingCount += 1;
|
|
78
|
+
}
|
|
79
|
+
if (isZeroResult(row)) {
|
|
80
|
+
zeroResultCount += 1;
|
|
81
|
+
increment(zeroResultToolCounts, normalizeKey(row.tool, "unknown"));
|
|
82
|
+
const sourceFile = getSourceFile(row);
|
|
83
|
+
if (sourceFile) {
|
|
84
|
+
increment(zeroResultSourceFileCounts, sourceFile);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (row.tool === "kb_check" &&
|
|
88
|
+
typeof row.violation_count === "number" &&
|
|
89
|
+
typeof row.timestamp === "string") {
|
|
90
|
+
violationTrend.push({
|
|
91
|
+
timestamp: row.timestamp,
|
|
92
|
+
violationCount: row.violation_count,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (row.tool === "kb_upsert" && outcome === "error") {
|
|
96
|
+
increment(upsertErrorCategories, categorizeUpsertError(row.error_message ?? row.error));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
violationTrend.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
|
|
100
|
+
return {
|
|
101
|
+
rowCount: rows.length,
|
|
102
|
+
dateRange: {
|
|
103
|
+
first: timestamps[0] ?? null,
|
|
104
|
+
last: timestamps.at(-1) ?? null,
|
|
105
|
+
},
|
|
106
|
+
toolCounts: mapToSortedObject(toolCounts),
|
|
107
|
+
branchCounts: mapToSortedObject(branchCounts),
|
|
108
|
+
outcomeCounts: {
|
|
109
|
+
success: successCount,
|
|
110
|
+
error: errorCount,
|
|
111
|
+
},
|
|
112
|
+
telemetry: {
|
|
113
|
+
completeCount: telemetryCompleteCount,
|
|
114
|
+
missingCount: telemetryMissingCount,
|
|
115
|
+
completenessRate: rows.length === 0 ? 0 : telemetryCompleteCount / rows.length,
|
|
116
|
+
},
|
|
117
|
+
zeroResults: {
|
|
118
|
+
count: zeroResultCount,
|
|
119
|
+
rate: rows.length === 0 ? 0 : zeroResultCount / rows.length,
|
|
120
|
+
byTool: mapToSortedObject(zeroResultToolCounts),
|
|
121
|
+
topSourceFiles: sortCountEntries(zeroResultSourceFileCounts)
|
|
122
|
+
.slice(0, limit)
|
|
123
|
+
.map(([sourceFile, count]) => ({ sourceFile, count })),
|
|
124
|
+
},
|
|
125
|
+
kbCheck: {
|
|
126
|
+
violationTrend,
|
|
127
|
+
},
|
|
128
|
+
upsertErrors: {
|
|
129
|
+
categories: mapToSortedObject(upsertErrorCategories),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function hasCompleteTelemetry(row) {
|
|
134
|
+
if (row.telemetry_status === "provided") {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
if (row.telemetry_status === "missing") {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return row.telemetry !== null && row.telemetry !== undefined;
|
|
141
|
+
}
|
|
142
|
+
function getOutcome(row) {
|
|
143
|
+
if (row.status === "success" || row.status === "error") {
|
|
144
|
+
return row.status;
|
|
145
|
+
}
|
|
146
|
+
if (row.success === true) {
|
|
147
|
+
return "success";
|
|
148
|
+
}
|
|
149
|
+
if (row.success === false) {
|
|
150
|
+
return "error";
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function isZeroResult(row) {
|
|
155
|
+
if (row.zero_results === true) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (row.zero_results === false) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (row.result_count === 0) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return row.result_summary === "0 results";
|
|
165
|
+
}
|
|
166
|
+
function getSourceFile(row) {
|
|
167
|
+
if (typeof row.sourceFile === "string" && row.sourceFile.trim()) {
|
|
168
|
+
return row.sourceFile;
|
|
169
|
+
}
|
|
170
|
+
if (row.args &&
|
|
171
|
+
typeof row.args.sourceFile === "string" &&
|
|
172
|
+
row.args.sourceFile.trim()) {
|
|
173
|
+
return row.args.sourceFile;
|
|
174
|
+
}
|
|
175
|
+
if (row.business_args &&
|
|
176
|
+
typeof row.business_args.sourceFile === "string" &&
|
|
177
|
+
row.business_args.sourceFile.trim()) {
|
|
178
|
+
return row.business_args.sourceFile;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function categorizeUpsertError(errorMessage) {
|
|
183
|
+
if (!errorMessage) {
|
|
184
|
+
return "Unknown error";
|
|
185
|
+
}
|
|
186
|
+
if (errorMessage.startsWith("Entity validation failed:")) {
|
|
187
|
+
return "Entity validation failed";
|
|
188
|
+
}
|
|
189
|
+
if (errorMessage.startsWith("Relationship source must match the upserted entity")) {
|
|
190
|
+
return "Relationship source must match the upserted entity";
|
|
191
|
+
}
|
|
192
|
+
const semicolonIndex = errorMessage.indexOf(";");
|
|
193
|
+
if (semicolonIndex > 0) {
|
|
194
|
+
return errorMessage.slice(0, semicolonIndex).trim();
|
|
195
|
+
}
|
|
196
|
+
const colonIndex = errorMessage.indexOf(":");
|
|
197
|
+
if (colonIndex > 0) {
|
|
198
|
+
return errorMessage.slice(0, colonIndex).trim();
|
|
199
|
+
}
|
|
200
|
+
return errorMessage.trim() || "Unknown error";
|
|
201
|
+
}
|
|
202
|
+
function increment(counts, key) {
|
|
203
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
204
|
+
}
|
|
205
|
+
function normalizeKey(value, fallback) {
|
|
206
|
+
return typeof value === "string" && value.trim() ? value : fallback;
|
|
207
|
+
}
|
|
208
|
+
function sortCountEntries(counts) {
|
|
209
|
+
return [...counts.entries()].sort(([leftKey, leftCount], [rightKey, rightCount]) => rightCount - leftCount || leftKey.localeCompare(rightKey));
|
|
210
|
+
}
|
|
211
|
+
function mapToSortedObject(counts) {
|
|
212
|
+
return Object.fromEntries(sortCountEntries(counts));
|
|
213
|
+
}
|
|
214
|
+
function renderUsageMetricsReport(report) {
|
|
215
|
+
const sections = [
|
|
216
|
+
renderSummaryTable(report),
|
|
217
|
+
renderCountsTable("Tool Counts", "Tool", report.toolCounts),
|
|
218
|
+
renderCountsTable("Branch Counts", "Branch", report.branchCounts),
|
|
219
|
+
renderCountsTable("Outcome Counts", "Outcome", report.outcomeCounts),
|
|
220
|
+
renderTelemetryTable(report),
|
|
221
|
+
renderZeroResultsTable(report),
|
|
222
|
+
renderViolationTrendTable(report),
|
|
223
|
+
renderCountsTable("Upsert Error Categories", "Category", report.upsertErrors.categories),
|
|
224
|
+
].filter(Boolean);
|
|
225
|
+
return sections.join("\n\n");
|
|
226
|
+
}
|
|
227
|
+
function renderSummaryTable(report) {
|
|
228
|
+
const table = new Table({
|
|
229
|
+
head: ["Field", "Value"],
|
|
230
|
+
colWidths: [24, 56],
|
|
231
|
+
wordWrap: true,
|
|
232
|
+
});
|
|
233
|
+
table.push(["Row Count", String(report.rowCount)], ["First Timestamp", report.dateRange.first ?? "-"], ["Last Timestamp", report.dateRange.last ?? "-"]);
|
|
234
|
+
return table.toString();
|
|
235
|
+
}
|
|
236
|
+
function renderCountsTable(title, label, counts) {
|
|
237
|
+
const entries = Object.entries(counts);
|
|
238
|
+
const table = new Table({
|
|
239
|
+
head: [label, "Count"],
|
|
240
|
+
colWidths: [48, 12],
|
|
241
|
+
wordWrap: true,
|
|
242
|
+
});
|
|
243
|
+
if (entries.length === 0) {
|
|
244
|
+
table.push(["-", "0"]);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
for (const [key, count] of entries) {
|
|
248
|
+
table.push([key, String(count)]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return `${title}\n${table.toString()}`;
|
|
252
|
+
}
|
|
253
|
+
function renderTelemetryTable(report) {
|
|
254
|
+
const table = new Table({
|
|
255
|
+
head: ["Metric", "Value"],
|
|
256
|
+
colWidths: [32, 28],
|
|
257
|
+
wordWrap: true,
|
|
258
|
+
});
|
|
259
|
+
table.push(["Complete", String(report.telemetry.completeCount)], ["Missing", String(report.telemetry.missingCount)], ["Completeness Rate", formatRate(report.telemetry.completenessRate)]);
|
|
260
|
+
return `Telemetry\n${table.toString()}`;
|
|
261
|
+
}
|
|
262
|
+
function renderZeroResultsTable(report) {
|
|
263
|
+
const summary = new Table({
|
|
264
|
+
head: ["Metric", "Value"],
|
|
265
|
+
colWidths: [32, 28],
|
|
266
|
+
wordWrap: true,
|
|
267
|
+
});
|
|
268
|
+
summary.push(["Count", String(report.zeroResults.count)], ["Rate", formatRate(report.zeroResults.rate)]);
|
|
269
|
+
const byTool = new Table({
|
|
270
|
+
head: ["Tool", "Count"],
|
|
271
|
+
colWidths: [48, 12],
|
|
272
|
+
wordWrap: true,
|
|
273
|
+
});
|
|
274
|
+
const byToolEntries = Object.entries(report.zeroResults.byTool);
|
|
275
|
+
if (byToolEntries.length === 0) {
|
|
276
|
+
byTool.push(["-", "0"]);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
for (const [tool, count] of byToolEntries) {
|
|
280
|
+
byTool.push([tool, String(count)]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const sourceFiles = new Table({
|
|
284
|
+
head: ["Source File", "Zero Results"],
|
|
285
|
+
colWidths: [48, 14],
|
|
286
|
+
wordWrap: true,
|
|
287
|
+
});
|
|
288
|
+
if (report.zeroResults.topSourceFiles.length === 0) {
|
|
289
|
+
sourceFiles.push(["-", "0"]);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
for (const entry of report.zeroResults.topSourceFiles) {
|
|
293
|
+
sourceFiles.push([entry.sourceFile, String(entry.count)]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return [
|
|
297
|
+
"Zero Results",
|
|
298
|
+
summary.toString(),
|
|
299
|
+
"Zero-Result Counts By Tool",
|
|
300
|
+
byTool.toString(),
|
|
301
|
+
"Zero-Result Source Files",
|
|
302
|
+
sourceFiles.toString(),
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
function renderViolationTrendTable(report) {
|
|
306
|
+
const table = new Table({
|
|
307
|
+
head: ["Timestamp", "Violations"],
|
|
308
|
+
colWidths: [32, 12],
|
|
309
|
+
wordWrap: true,
|
|
310
|
+
});
|
|
311
|
+
if (report.kbCheck.violationTrend.length === 0) {
|
|
312
|
+
table.push(["-", "0"]);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
for (const entry of report.kbCheck.violationTrend) {
|
|
316
|
+
table.push([entry.timestamp, String(entry.violationCount)]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return `KB Check Violation Trend\n${table.toString()}`;
|
|
320
|
+
}
|
|
321
|
+
function formatRate(value) {
|
|
322
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
323
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: kibi-usage
|
|
3
|
+
name: Kibi Usage
|
|
4
|
+
description: Guides agents to use Kibi MCP, facts, relationships, and validation correctly
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
kibiCompatibility: ">=0.11.0"
|
|
7
|
+
tags:
|
|
8
|
+
- kibi
|
|
9
|
+
- mcp
|
|
10
|
+
- knowledge-base
|
|
11
|
+
- traceability
|
|
12
|
+
- agent-guidance
|
|
13
|
+
resources:
|
|
14
|
+
- resources/relationship-directions.md
|
|
15
|
+
- resources/fact-lanes.md
|
|
16
|
+
- resources/workflows.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Kibi Usage
|
|
20
|
+
|
|
21
|
+
Consult this skill before any Kibi knowledge base operation, on first interaction with a Kibi-enabled repo, after detecting stale or dirty KB status, and before performing mutations.
|
|
22
|
+
|
|
23
|
+
## MCP-Only Rules
|
|
24
|
+
|
|
25
|
+
Interact with the knowledge base exclusively through MCP tools. Do not read or edit files inside `.kb/` directly. Do not run any `kibi` CLI commands from the agent session. The MCP surface is the only sanctioned interface for agents.
|
|
26
|
+
|
|
27
|
+
## Discovery-First Workflow
|
|
28
|
+
|
|
29
|
+
Always discover before you mutate. Start with `kb_search` for exploratory discovery across metadata and markdown body text. Split broad queries into 1-3 focused probes. Review top hits for relevance before concluding the KB lacks knowledge.
|
|
30
|
+
|
|
31
|
+
Follow up with `kb_query` for exact lookups by `id`, `type`, `tags`, or `sourceFile`. Call `kb_status` to inspect branch attachment and freshness when stale context would affect decisions. Only after discovery and confirmation should you mutate.
|
|
32
|
+
|
|
33
|
+
## Relationship Directions
|
|
34
|
+
|
|
35
|
+
Relationship direction is fixed and semantic. Getting it wrong breaks traceability queries and validation.
|
|
36
|
+
|
|
37
|
+
| Relationship | Direction | Meaning |
|
|
38
|
+
|-------------|-----------|---------|
|
|
39
|
+
| `implements` | symbol -> req | Symbol owns or implements requirement behavior |
|
|
40
|
+
| `specified_by` | req -> scenario | Requirement is specified by a scenario |
|
|
41
|
+
| `verified_by` | req/scenario -> test | Requirement or scenario is verified by a test |
|
|
42
|
+
| `validates` | test -> req/scenario | Test validates a requirement or scenario |
|
|
43
|
+
| `executable_for` | symbol -> test | Symbol is executable test code for a test entity |
|
|
44
|
+
| `constrains` | req -> fact(subject) | Requirement constrains a domain fact |
|
|
45
|
+
| `requires_property` | req -> fact(property_value) | Requirement requires a property value |
|
|
46
|
+
| `supersedes` | old-req -> new-req | Old requirement is replaced by new requirement |
|
|
47
|
+
| `covered_by` | symbol -> test | Production symbol has coverage evidence from a test |
|
|
48
|
+
|
|
49
|
+
See `resources/relationship-directions.md` for detailed payload examples.
|
|
50
|
+
|
|
51
|
+
## Strict Fact Lane
|
|
52
|
+
|
|
53
|
+
Normative requirements that must participate in contradiction blocking use the strict fact lane. Create a `fact_kind: subject` fact and link it from the requirement via `constrains`. Create a `fact_kind: property_value` fact and link it via `requires_property`.
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
# Fact entity
|
|
57
|
+
id: FACT-USER-ROLE
|
|
58
|
+
title: User Role Assignment
|
|
59
|
+
status: active
|
|
60
|
+
fact_kind: subject
|
|
61
|
+
subject_key: user.role_assignment
|
|
62
|
+
|
|
63
|
+
# Requirement entity
|
|
64
|
+
id: REQ-019
|
|
65
|
+
title: Users can have up to 3 roles
|
|
66
|
+
status: open
|
|
67
|
+
relationships:
|
|
68
|
+
- type: constrains
|
|
69
|
+
from: REQ-019
|
|
70
|
+
to: FACT-USER-ROLE
|
|
71
|
+
- type: requires_property
|
|
72
|
+
from: REQ-019
|
|
73
|
+
to: FACT-LIMIT-3
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See `resources/fact-lanes.md` for the full strict vs observation lane comparison.
|
|
77
|
+
|
|
78
|
+
## Fact vs Flag
|
|
79
|
+
|
|
80
|
+
Use `flag` for runtime or config gates only. Feature flags, kill-switches, and deferred capabilities are valid `flag` entities.
|
|
81
|
+
|
|
82
|
+
Bugs, incidents, and workarounds belong in `fact` entities with `fact_kind: observation` or `meta`. These fact kinds are excluded from contradiction inference, making them appropriate for non-blocking evidence.
|
|
83
|
+
|
|
84
|
+
Anti-example: do not create a `flag` named `BUG-123` to track a defect. Create a `fact` with `fact_kind: observation` instead.
|
|
85
|
+
|
|
86
|
+
## Create-Before-Link
|
|
87
|
+
|
|
88
|
+
Always confirm or create endpoint entities before linking them. Query target IDs with `kb_query` first. If an endpoint does not exist, create it with `kb_upsert` before creating the relationship. Creating relationships to non-existent entities produces dangling references that `kb_check` will flag.
|
|
89
|
+
|
|
90
|
+
## Sequential Upserts
|
|
91
|
+
|
|
92
|
+
Never fire `kb_upsert` calls in parallel. Execute them sequentially to avoid lock contention and ensure deterministic ordering. This is especially important when creating chains of related entities.
|
|
93
|
+
|
|
94
|
+
## Targeted and Final Checks
|
|
95
|
+
|
|
96
|
+
Run `kb_check` with specific rules during iteration for fast feedback. For example, use `rules: ["required-fields", "no-dangling-refs"]` after small changes. Run a full `kb_check` without rule filters before declaring work complete.
|
|
97
|
+
|
|
98
|
+
## Domain Contradictions and Evolution
|
|
99
|
+
|
|
100
|
+
The `domain-contradictions` rule detects conflicts between strict-lane facts linked to requirements. When a contradiction is found, the supported escape hatch is `supersedes`: create a new requirement that supersedes the old one, then link the new requirement to updated facts.
|
|
101
|
+
|
|
102
|
+
Use `kb_model_requirement` for automated strict-fact modeling. It generates the subject and property_value facts, links them via `constrains` and `requires_property`, and handles low-confidence downgrades to `observation` facts automatically.
|
|
103
|
+
|
|
104
|
+
## Stale or Dirty KB Handling
|
|
105
|
+
|
|
106
|
+
Call `kb_status` when you suspect the branch KB is stale or when switching context. Report freshness findings to the user rather than relying on outdated KB context. If `kb_status` indicates a schema migration is needed, ask the user or operator to handle it outside the agent session.
|
|
107
|
+
|
|
108
|
+
## Anti-Patterns and Remediation
|
|
109
|
+
|
|
110
|
+
| Anti-Pattern | Problem | Remediation |
|
|
111
|
+
|-------------|---------|-------------|
|
|
112
|
+
| Reversed relationship direction | Traceability queries break | Verify direction against the relationship table above |
|
|
113
|
+
| Bug-as-flag | `flag` misused for defect tracking | Use `fact` with `fact_kind: observation` or `meta` |
|
|
114
|
+
| Parallel upserts | Lock contention and nondeterminism | Execute `kb_upsert` calls sequentially |
|
|
115
|
+
| Embedded scenarios in reqs | Violates canonical traceability chain | Create separate `req`, `scen`, and `test` entities |
|
|
116
|
+
| Missing `kb_check` | Undetected dangling refs and violations | Run targeted checks during work, full check at completion |
|
|
117
|
+
| Tags as multi-ID lookup | Tags are metadata, not identifiers | Use `kb_query` with explicit `id` values |
|
|
118
|
+
| `relates_to` for strict modeling | Loses contradiction safety | Use `constrains` and `requires_property` instead |
|
|
119
|
+
|
|
120
|
+
Before/after for reversed direction:
|
|
121
|
+
|
|
122
|
+
- Wrong: `relationships: [{ type: "implements", from: "REQ-001", to: "SYM-001" }]`
|
|
123
|
+
- Right: `relationships: [{ type: "implements", from: "SYM-001", to: "REQ-001" }]`
|
|
124
|
+
|
|
125
|
+
See `resources/workflows.md` for the golden-path discovery to validation sequence.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Fact Lanes
|
|
2
|
+
|
|
3
|
+
## Strict Lane (Contradiction-Safe)
|
|
4
|
+
|
|
5
|
+
### fact_kind: subject
|
|
6
|
+
```yaml
|
|
7
|
+
id: FACT-USER-ROLE
|
|
8
|
+
title: User Role Assignment
|
|
9
|
+
status: active
|
|
10
|
+
fact_kind: subject
|
|
11
|
+
subject_key: user.role_assignment
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### fact_kind: property_value
|
|
15
|
+
```yaml
|
|
16
|
+
id: FACT-LIMIT-3
|
|
17
|
+
title: Maximum of Three Roles
|
|
18
|
+
status: active
|
|
19
|
+
fact_kind: property_value
|
|
20
|
+
subject_key: user.role_assignment
|
|
21
|
+
property_key: max_roles
|
|
22
|
+
operator: lte
|
|
23
|
+
value_type: int
|
|
24
|
+
value_int: 3
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Context Lane (Non-Blocking)
|
|
28
|
+
|
|
29
|
+
### fact_kind: observation
|
|
30
|
+
For bug records, incident notes, and observed behavior.
|
|
31
|
+
```yaml
|
|
32
|
+
id: FACT-BUG-123
|
|
33
|
+
title: Login fails on Safari 17
|
|
34
|
+
status: active
|
|
35
|
+
fact_kind: observation
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### fact_kind: meta
|
|
39
|
+
For governance notes, process commentary, and workaround documentation.
|
|
40
|
+
```yaml
|
|
41
|
+
id: FACT-WORKAROUND-456
|
|
42
|
+
title: Temporary cache bypass for v2 migration
|
|
43
|
+
status: active
|
|
44
|
+
fact_kind: meta
|
|
45
|
+
```
|