kibi-cursor 0.2.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/.cursor-plugin/plugin.json +18 -0
- package/CHANGELOG.md +21 -0
- package/README.md +120 -0
- package/commands/init-kibi.md +21 -0
- package/dist/.cursor-plugin/plugin.json +18 -0
- package/dist/commands/init-kibi.md +21 -0
- package/dist/guidance.d.ts +2 -0
- package/dist/guidance.js +37 -0
- package/dist/hook-input.d.ts +12 -0
- package/dist/hook-input.js +75 -0
- package/dist/hook-runner.d.ts +13 -0
- package/dist/hook-runner.js +163 -0
- package/dist/hook-state.d.ts +18 -0
- package/dist/hook-state.js +250 -0
- package/dist/hooks/hooks.json +31 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/kb-mcp-tools.d.ts +1 -0
- package/dist/kb-mcp-tools.js +36 -0
- package/dist/mcp.json +8 -0
- package/dist/messages.d.ts +6 -0
- package/dist/messages.js +22 -0
- package/dist/path-policy.d.ts +7 -0
- package/dist/path-policy.js +146 -0
- package/dist/rules/kibi-traceability.mdc +14 -0
- package/dist/rules/kibi-workflow.mdc +19 -0
- package/dist/skills/init-kibi/SKILL.md +23 -0
- package/dist/skills/kibi-freshness/SKILL.md +22 -0
- package/dist/skills/kibi-traceability/SKILL.md +22 -0
- package/dist/skills/kibi-usage/SKILL.md +23 -0
- package/hooks/hooks.json +31 -0
- package/mcp.json +8 -0
- package/package.json +58 -0
- package/rules/kibi-traceability.mdc +14 -0
- package/rules/kibi-workflow.mdc +19 -0
- package/skills/init-kibi/SKILL.md +23 -0
- package/skills/kibi-freshness/SKILL.md +22 -0
- package/skills/kibi-traceability/SKILL.md +22 -0
- package/skills/kibi-usage/SKILL.md +23 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// implements REQ-cursor-kibi-plugin-v1
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const stateFileName = "hook-state.json";
|
|
7
|
+
const lockFileName = "hook-state.lock";
|
|
8
|
+
const maxDirtyPaths = 50;
|
|
9
|
+
const maxGuidedPaths = 100;
|
|
10
|
+
const lockRetryCount = 100;
|
|
11
|
+
const lockRetryDelayMs = 5;
|
|
12
|
+
const staleLockAgeMs = 30_000;
|
|
13
|
+
function statePath(pluginData) {
|
|
14
|
+
return path.join(pluginData, stateFileName);
|
|
15
|
+
}
|
|
16
|
+
function lockPath(pluginData) {
|
|
17
|
+
return path.join(pluginData, lockFileName);
|
|
18
|
+
}
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function normalizePath(dirtyPath) {
|
|
23
|
+
return dirtyPath.trim().replaceAll("\\", "/");
|
|
24
|
+
}
|
|
25
|
+
function sleepSync(milliseconds) {
|
|
26
|
+
const buffer = new SharedArrayBuffer(4);
|
|
27
|
+
const view = new Int32Array(buffer);
|
|
28
|
+
Atomics.wait(view, 0, 0, milliseconds);
|
|
29
|
+
}
|
|
30
|
+
function uniqueTempPath(pluginData) {
|
|
31
|
+
return path.join(pluginData, `${stateFileName}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
|
|
32
|
+
}
|
|
33
|
+
function mergeStringPaths(existingPaths, nextPaths) {
|
|
34
|
+
const merged = [...existingPaths, ...nextPaths]
|
|
35
|
+
.map(normalizePath)
|
|
36
|
+
.filter((dirtyPath) => dirtyPath.length > 0);
|
|
37
|
+
return [...new Set(merged)].slice(-maxGuidedPaths);
|
|
38
|
+
}
|
|
39
|
+
const maxKbMutationTools = 20;
|
|
40
|
+
function emptyHookState() {
|
|
41
|
+
return {
|
|
42
|
+
dirtyPaths: [],
|
|
43
|
+
guidedReadPaths: [],
|
|
44
|
+
guidedWritePaths: [],
|
|
45
|
+
kbMutationTools: [],
|
|
46
|
+
kbCheckRun: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function acquireLock(pluginData) {
|
|
50
|
+
const targetLockPath = lockPath(pluginData);
|
|
51
|
+
for (let attempt = 0; attempt < lockRetryCount; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
return fs.openSync(targetLockPath, "wx");
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof Error &&
|
|
57
|
+
"code" in error &&
|
|
58
|
+
error.code === "EEXIST") {
|
|
59
|
+
if (removeStaleLock(targetLockPath)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
sleepSync(lockRetryDelayMs);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function removeStaleLock(targetLockPath) {
|
|
71
|
+
let stats;
|
|
72
|
+
try {
|
|
73
|
+
stats = fs.statSync(targetLockPath);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (Date.now() - stats.mtimeMs < staleLockAgeMs) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
fs.unlinkSync(targetLockPath);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function releaseLock(pluginData, fileDescriptor) {
|
|
90
|
+
try {
|
|
91
|
+
fs.closeSync(fileDescriptor);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Ignore close failures; unlink still gives future hook runs a chance.
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
fs.unlinkSync(lockPath(pluginData));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// A missing lock file is safe: bounded retries prevent permanent blocking.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function coerceHookState(value) {
|
|
104
|
+
if (!isRecord(value)) {
|
|
105
|
+
return emptyHookState();
|
|
106
|
+
}
|
|
107
|
+
const dirtyPaths = Array.isArray(value.dirtyPaths)
|
|
108
|
+
? value.dirtyPaths
|
|
109
|
+
.filter((dirtyPath) => typeof dirtyPath === "string")
|
|
110
|
+
.map(normalizePath)
|
|
111
|
+
.filter((dirtyPath) => dirtyPath.length > 0)
|
|
112
|
+
: [];
|
|
113
|
+
const guidedReadPaths = Array.isArray(value.guidedReadPaths)
|
|
114
|
+
? value.guidedReadPaths
|
|
115
|
+
.filter((entry) => typeof entry === "string")
|
|
116
|
+
.map(normalizePath)
|
|
117
|
+
.filter((entry) => entry.length > 0)
|
|
118
|
+
: [];
|
|
119
|
+
const guidedWritePaths = Array.isArray(value.guidedWritePaths)
|
|
120
|
+
? value.guidedWritePaths
|
|
121
|
+
.filter((entry) => typeof entry === "string")
|
|
122
|
+
.map(normalizePath)
|
|
123
|
+
.filter((entry) => entry.length > 0)
|
|
124
|
+
: [];
|
|
125
|
+
const kbMutationTools = Array.isArray(value.kbMutationTools)
|
|
126
|
+
? value.kbMutationTools
|
|
127
|
+
.filter((entry) => typeof entry === "string")
|
|
128
|
+
.map((entry) => entry.trim())
|
|
129
|
+
.filter((entry) => entry.length > 0)
|
|
130
|
+
: [];
|
|
131
|
+
const kbCheckRun = value.kbCheckRun === true;
|
|
132
|
+
return {
|
|
133
|
+
dirtyPaths: [...new Set(dirtyPaths)].slice(-maxDirtyPaths),
|
|
134
|
+
guidedReadPaths: [...new Set(guidedReadPaths)].slice(-maxGuidedPaths),
|
|
135
|
+
guidedWritePaths: [...new Set(guidedWritePaths)].slice(-maxGuidedPaths),
|
|
136
|
+
kbMutationTools: [...new Set(kbMutationTools)].slice(-maxKbMutationTools),
|
|
137
|
+
kbCheckRun,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function resolveStateDir(pluginData, conversationId) {
|
|
141
|
+
if (pluginData) {
|
|
142
|
+
return pluginData;
|
|
143
|
+
}
|
|
144
|
+
if (!conversationId) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return path.join(os.tmpdir(), "kibi-cursor-hook-state", conversationId.replaceAll(/[^a-zA-Z0-9._-]/g, "_"));
|
|
148
|
+
}
|
|
149
|
+
export function loadHookState(stateDir) {
|
|
150
|
+
if (!stateDir) {
|
|
151
|
+
return emptyHookState();
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
return coerceHookState(JSON.parse(fs.readFileSync(statePath(stateDir), "utf8")));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return emptyHookState();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function saveHookState(stateDir, state) {
|
|
161
|
+
if (!stateDir) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
165
|
+
const boundedState = coerceHookState(state);
|
|
166
|
+
const tempPath = uniqueTempPath(stateDir);
|
|
167
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(boundedState)}\n`);
|
|
168
|
+
fs.renameSync(tempPath, statePath(stateDir));
|
|
169
|
+
}
|
|
170
|
+
export function updateHookState(stateDir, updater) {
|
|
171
|
+
const initialState = loadHookState(stateDir);
|
|
172
|
+
if (!stateDir) {
|
|
173
|
+
return updater(initialState);
|
|
174
|
+
}
|
|
175
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
176
|
+
const lockFileDescriptor = acquireLock(stateDir);
|
|
177
|
+
if (lockFileDescriptor === undefined) {
|
|
178
|
+
return updater(initialState);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const lockedState = loadHookState(stateDir);
|
|
182
|
+
const nextState = updater(lockedState);
|
|
183
|
+
saveHookState(stateDir, nextState);
|
|
184
|
+
return nextState;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return updater(initialState);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
releaseLock(stateDir, lockFileDescriptor);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export function addDirtyPaths(stateDir, dirtyPaths) {
|
|
194
|
+
return updateHookState(stateDir, (state) => ({
|
|
195
|
+
...state,
|
|
196
|
+
dirtyPaths: mergeStringPaths(state.dirtyPaths, dirtyPaths).slice(-maxDirtyPaths),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
export function rememberGuidedPath(stateDir, kind, guidedPath) {
|
|
200
|
+
return updateHookState(stateDir, (state) => {
|
|
201
|
+
const normalized = normalizePath(guidedPath);
|
|
202
|
+
if (normalized.length === 0) {
|
|
203
|
+
return state;
|
|
204
|
+
}
|
|
205
|
+
if (kind === "read") {
|
|
206
|
+
return {
|
|
207
|
+
...state,
|
|
208
|
+
guidedReadPaths: mergeStringPaths(state.guidedReadPaths, [normalized]),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
...state,
|
|
213
|
+
guidedWritePaths: mergeStringPaths(state.guidedWritePaths, [normalized]),
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
export function hasGuidedPath(state, kind, guidedPath) {
|
|
218
|
+
const normalized = normalizePath(guidedPath);
|
|
219
|
+
const bucket = kind === "read" ? state.guidedReadPaths : state.guidedWritePaths;
|
|
220
|
+
return bucket.includes(normalized);
|
|
221
|
+
}
|
|
222
|
+
export function recordKbMcpTool(stateDir, toolName) {
|
|
223
|
+
const normalized = toolName.trim();
|
|
224
|
+
if (normalized.length === 0) {
|
|
225
|
+
return loadHookState(stateDir);
|
|
226
|
+
}
|
|
227
|
+
return updateHookState(stateDir, (state) => {
|
|
228
|
+
if (normalized === "kb_check") {
|
|
229
|
+
return { ...state, kbCheckRun: true };
|
|
230
|
+
}
|
|
231
|
+
if (normalized === "kb_upsert" || normalized === "kb_delete") {
|
|
232
|
+
return {
|
|
233
|
+
...state,
|
|
234
|
+
kbMutationTools: mergeStringPaths(state.kbMutationTools, [normalized]).slice(-maxKbMutationTools),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return state;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
export function clearSessionHookState(stateDir) {
|
|
241
|
+
const clearedState = emptyHookState();
|
|
242
|
+
if (!stateDir) {
|
|
243
|
+
return clearedState;
|
|
244
|
+
}
|
|
245
|
+
return updateHookState(stateDir, () => clearedState);
|
|
246
|
+
}
|
|
247
|
+
/** @deprecated Use clearSessionHookState */
|
|
248
|
+
export function clearDirtyPaths(stateDir) {
|
|
249
|
+
return clearSessionHookState(stateDir);
|
|
250
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"sessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"command": "node dist/hook-runner.js"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"preToolUse": [
|
|
10
|
+
{
|
|
11
|
+
"command": "node dist/hook-runner.js"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"postToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"command": "node dist/hook-runner.js"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"beforeReadFile": [
|
|
20
|
+
{
|
|
21
|
+
"command": "node dist/hook-runner.js"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"stop": [
|
|
25
|
+
{
|
|
26
|
+
"command": "node dist/hook-runner.js",
|
|
27
|
+
"loop_limit": 1
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function extractKbMcpToolName(toolName: string | undefined, toolInput: unknown): string | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// implements REQ-cursor-kibi-plugin-v1
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function readString(record, keys) {
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const value = record[key];
|
|
8
|
+
if (typeof value === "string") {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
export function extractKbMcpToolName(toolName, toolInput) {
|
|
15
|
+
if (toolName) {
|
|
16
|
+
const normalized = toolName.trim();
|
|
17
|
+
if (normalized.startsWith("kb_")) {
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!isRecord(toolInput)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const directTool = readString(toolInput, ["toolName", "tool_name", "name"]);
|
|
25
|
+
if (directTool?.startsWith("kb_")) {
|
|
26
|
+
return directTool;
|
|
27
|
+
}
|
|
28
|
+
const nestedArgs = toolInput.arguments ?? toolInput.args;
|
|
29
|
+
if (isRecord(nestedArgs)) {
|
|
30
|
+
const nestedTool = readString(nestedArgs, ["toolName", "tool_name", "name"]);
|
|
31
|
+
if (nestedTool?.startsWith("kb_")) {
|
|
32
|
+
return nestedTool;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
package/dist/mcp.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HookState } from "./hook-state.js";
|
|
2
|
+
export declare const BOOTSTRAP_REMINDER = "Kibi is not initialized for this workspace. Use the Kibi MCP workflow to bootstrap project memory before relying on KB lookups; do not edit .kb/ files directly.";
|
|
3
|
+
export declare const DIRECT_KB_EDIT_WARNING = "Avoid direct edits to .kb/. Use Kibi MCP tools for KB discovery and mutations so project memory stays valid.";
|
|
4
|
+
export declare function stopFollowupMessage(state: HookState): string | undefined;
|
|
5
|
+
/** @deprecated Use stopFollowupMessage */
|
|
6
|
+
export declare function freshnessReminder(dirtyPaths: readonly string[]): string;
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isKbFreshnessRelevantPath } from "./path-policy.js";
|
|
2
|
+
export const BOOTSTRAP_REMINDER = "Kibi is not initialized for this workspace. Use the Kibi MCP workflow to bootstrap project memory before relying on KB lookups; do not edit .kb/ files directly.";
|
|
3
|
+
export const DIRECT_KB_EDIT_WARNING = "Avoid direct edits to .kb/. Use Kibi MCP tools for KB discovery and mutations so project memory stays valid.";
|
|
4
|
+
export function stopFollowupMessage(state) {
|
|
5
|
+
if (state.kbMutationTools.length > 0) {
|
|
6
|
+
const tools = [...new Set(state.kbMutationTools)];
|
|
7
|
+
return `Kibi KB updated (${tools.join(", ")}).`;
|
|
8
|
+
}
|
|
9
|
+
const freshnessPaths = state.dirtyPaths.filter(isKbFreshnessRelevantPath);
|
|
10
|
+
if (freshnessPaths.length === 0 || state.kbCheckRun) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const fileCount = freshnessPaths.length;
|
|
14
|
+
const noun = fileCount === 1 ? "file" : "files";
|
|
15
|
+
return `Kibi: sync or record no-impact after ${fileCount} edited ${noun}.`;
|
|
16
|
+
}
|
|
17
|
+
/** @deprecated Use stopFollowupMessage */
|
|
18
|
+
export function freshnessReminder(dirtyPaths) {
|
|
19
|
+
const fileCount = dirtyPaths.length;
|
|
20
|
+
const noun = fileCount === 1 ? "file" : "files";
|
|
21
|
+
return `Kibi: sync or record no-impact after ${fileCount} edited ${noun}.`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function extractExplicitPathFields(input: unknown): string[];
|
|
2
|
+
export declare function isDirectKbPath(candidate: string): boolean;
|
|
3
|
+
export declare function isMeaningfulTrackedPath(candidate: string): boolean;
|
|
4
|
+
/** Paths whose edits should trigger a KB freshness stop follow-up. */
|
|
5
|
+
export declare function isKbFreshnessRelevantPath(candidate: string): boolean;
|
|
6
|
+
export declare function isDocumentationTrackedPath(candidate: string): boolean;
|
|
7
|
+
export declare function toRepoRelativePath(candidate: string, cwd: string | undefined): string;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// implements REQ-cursor-kibi-plugin-v1
|
|
2
|
+
const explicitPathKeys = new Set([
|
|
3
|
+
"absolute_path",
|
|
4
|
+
"file",
|
|
5
|
+
"file_path",
|
|
6
|
+
"filepath",
|
|
7
|
+
"new_path",
|
|
8
|
+
"old_path",
|
|
9
|
+
"path",
|
|
10
|
+
"paths",
|
|
11
|
+
"relative_path",
|
|
12
|
+
"target_path",
|
|
13
|
+
]);
|
|
14
|
+
const sourceExtensions = new Set([
|
|
15
|
+
".c",
|
|
16
|
+
".cc",
|
|
17
|
+
".cpp",
|
|
18
|
+
".cs",
|
|
19
|
+
".css",
|
|
20
|
+
".go",
|
|
21
|
+
".h",
|
|
22
|
+
".hpp",
|
|
23
|
+
".html",
|
|
24
|
+
".java",
|
|
25
|
+
".js",
|
|
26
|
+
".jsx",
|
|
27
|
+
".kt",
|
|
28
|
+
".lua",
|
|
29
|
+
".mjs",
|
|
30
|
+
".mts",
|
|
31
|
+
".php",
|
|
32
|
+
".py",
|
|
33
|
+
".rb",
|
|
34
|
+
".rs",
|
|
35
|
+
".scala",
|
|
36
|
+
".sh",
|
|
37
|
+
".swift",
|
|
38
|
+
".ts",
|
|
39
|
+
".tsx",
|
|
40
|
+
".vue",
|
|
41
|
+
]);
|
|
42
|
+
const documentationExtensions = new Set([".md", ".mdx", ".rst", ".txt"]);
|
|
43
|
+
function isRecord(value) {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
function normalizePath(candidate) {
|
|
47
|
+
return candidate.trim().replaceAll("\\", "/");
|
|
48
|
+
}
|
|
49
|
+
function pathSegments(candidate) {
|
|
50
|
+
return normalizePath(candidate).split("/").filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
function collectPathValues(value, output) {
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
const normalized = normalizePath(value);
|
|
55
|
+
if (normalized.length > 0) {
|
|
56
|
+
output.push(normalized);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(value)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (const item of value) {
|
|
64
|
+
collectPathValues(item, output);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function visitExplicitPathFields(value, output) {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
for (const item of value) {
|
|
70
|
+
visitExplicitPathFields(item, output);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!isRecord(value)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const [key, child] of Object.entries(value)) {
|
|
78
|
+
if (explicitPathKeys.has(key.toLowerCase())) {
|
|
79
|
+
collectPathValues(child, output);
|
|
80
|
+
}
|
|
81
|
+
visitExplicitPathFields(child, output);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function extractExplicitPathFields(input) {
|
|
85
|
+
const paths = [];
|
|
86
|
+
visitExplicitPathFields(input, paths);
|
|
87
|
+
return [...new Set(paths)];
|
|
88
|
+
}
|
|
89
|
+
export function isDirectKbPath(candidate) {
|
|
90
|
+
return pathSegments(candidate).includes(".kb");
|
|
91
|
+
}
|
|
92
|
+
export function isMeaningfulTrackedPath(candidate) {
|
|
93
|
+
const normalized = normalizePath(candidate);
|
|
94
|
+
const segments = pathSegments(normalized);
|
|
95
|
+
if (segments.includes(".kb") || segments.includes("dist")) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const basename = segments.at(-1) ?? "";
|
|
99
|
+
const extension = basename.includes(".")
|
|
100
|
+
? `.${basename.split(".").at(-1) ?? ""}`
|
|
101
|
+
: "";
|
|
102
|
+
if (segments.includes("docs") || segments.includes("documentation")) {
|
|
103
|
+
return documentationExtensions.has(extension);
|
|
104
|
+
}
|
|
105
|
+
if (basename === "README.md") {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (segments.includes("src") ||
|
|
109
|
+
segments.includes("tests") ||
|
|
110
|
+
segments.includes("test")) {
|
|
111
|
+
return (sourceExtensions.has(extension) || documentationExtensions.has(extension));
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
/** Paths whose edits should trigger a KB freshness stop follow-up. */
|
|
116
|
+
export function isKbFreshnessRelevantPath(candidate) {
|
|
117
|
+
const segments = pathSegments(normalizePath(candidate));
|
|
118
|
+
if (segments.includes("documentation")) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return (segments[0] === "packages" &&
|
|
122
|
+
segments[1] === "core" &&
|
|
123
|
+
segments[2] === "src");
|
|
124
|
+
}
|
|
125
|
+
export function isDocumentationTrackedPath(candidate) {
|
|
126
|
+
const normalized = normalizePath(candidate);
|
|
127
|
+
const segments = pathSegments(normalized);
|
|
128
|
+
const basename = segments.at(-1) ?? "";
|
|
129
|
+
const extension = basename.includes(".")
|
|
130
|
+
? `.${basename.split(".").at(-1) ?? ""}`
|
|
131
|
+
: "";
|
|
132
|
+
return (segments.includes("docs") ||
|
|
133
|
+
segments.includes("documentation") ||
|
|
134
|
+
documentationExtensions.has(extension));
|
|
135
|
+
}
|
|
136
|
+
export function toRepoRelativePath(candidate, cwd) {
|
|
137
|
+
const normalized = normalizePath(candidate);
|
|
138
|
+
if (!cwd) {
|
|
139
|
+
return normalized;
|
|
140
|
+
}
|
|
141
|
+
const cwdPrefix = `${normalizePath(cwd)}/`;
|
|
142
|
+
if (normalized.startsWith(cwdPrefix)) {
|
|
143
|
+
return normalized.slice(cwdPrefix.length);
|
|
144
|
+
}
|
|
145
|
+
return normalized;
|
|
146
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Requirement traceability and symbol linking through Kibi
|
|
3
|
+
globs:
|
|
4
|
+
- "**/*.{ts,tsx,js,jsx,py,go,rs,java}"
|
|
5
|
+
alwaysApply: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Before changing production code, discover linked requirements with `kb_search` and `kb_query(sourceFile=...)`.
|
|
9
|
+
|
|
10
|
+
Preserve the canonical chain `REQ-* -> SCEN-* -> TEST-*` when modeling behavior.
|
|
11
|
+
|
|
12
|
+
Prefer symbol manifest + `executable_for` for test/e2e code. Inline `// implements REQ-xxx` remains acceptable for quick code-only changes.
|
|
13
|
+
|
|
14
|
+
Route durable domain facts to strict `fact` entities. Use `fact_kind: observation` or `meta` for bugs and workarounds, not `flag`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Kibi MCP discovery-first workflow and KB mutation discipline
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use Kibi via MCP tools only. Do not manually read or edit `.kb/` files.
|
|
7
|
+
|
|
8
|
+
Discovery workflow:
|
|
9
|
+
- Start with `kb_search` for broad discovery.
|
|
10
|
+
- Follow with `kb_query` for exact entities, filters, and source-linked lookups.
|
|
11
|
+
- Use `kb_status`, `kb_find_gaps`, `kb_coverage`, and `kb_graph` when reporting questions need them.
|
|
12
|
+
|
|
13
|
+
Mutation workflow:
|
|
14
|
+
- Query before mutate.
|
|
15
|
+
- Create relationship endpoints before linking.
|
|
16
|
+
- Use `kb_upsert` sequentially, then `kb_check`.
|
|
17
|
+
- Use `kb_delete` only for intentional removals with dependency awareness.
|
|
18
|
+
|
|
19
|
+
Completion must state KB freshness outcome: updated, no-impact with rationale, or deferred/failed.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: init-kibi
|
|
3
|
+
description: Bootstrap and model knowledge safely using approved Kibi MCP initialization and mutation workflow.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Set up or refresh Kibi knowledge for the current branch without manual filesystem edits.
|
|
9
|
+
|
|
10
|
+
## MCP workflow
|
|
11
|
+
|
|
12
|
+
- Run `kb_autopilot_generate` for suggested initial facts and claims.
|
|
13
|
+
- Show the generated preview and get explicit approval before writes.
|
|
14
|
+
- Use `kb_upsert` for reviewed entity creation or updates.
|
|
15
|
+
- If cleanup is needed, use `kb_delete` only after checking dependencies.
|
|
16
|
+
- Finish with `kb_check` to validate the branch snapshot.
|
|
17
|
+
|
|
18
|
+
## Guidance
|
|
19
|
+
|
|
20
|
+
- Keep mutation requests explicit and small.
|
|
21
|
+
- Use source-linked entities where possible so future `kb_query` lookups can verify context.
|
|
22
|
+
- Do not edit `.kb/**` files directly; all changes go through MCP calls.
|
|
23
|
+
- Ask at most four bounded context questions, and only when repository evidence is insufficient.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kibi-freshness
|
|
3
|
+
description: Check branch freshness and source linkage quality before decisions or risky writes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Help agents decide whether KB context is current enough to support safe changes.
|
|
9
|
+
|
|
10
|
+
## MCP workflow
|
|
11
|
+
|
|
12
|
+
- Use `kb_status` first to confirm branch attachment and freshness state.
|
|
13
|
+
- Use `kb_query` with `sourceFile`/text refs for source-linked discovery.
|
|
14
|
+
- Re-run `kb_search` after sync gaps are detected.
|
|
15
|
+
- If knowledge must change, update it with `kb_upsert` or `kb_delete` and finish with `kb_check`.
|
|
16
|
+
|
|
17
|
+
## Guidance
|
|
18
|
+
|
|
19
|
+
- Never proceed with broad changes when freshness is stale and unresolved.
|
|
20
|
+
- If entities look stale, prefer refreshing context via MCP flows and then re-query.
|
|
21
|
+
- Avoid direct `.kb` editing; use `kb_status` and other MCP checks instead.
|
|
22
|
+
- Completion must state one freshness outcome: KB updated, no KB impact with rationale, or deferred/failed.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kibi-traceability
|
|
3
|
+
description: Maintain requirement, scenario, and test traceability through Kibi MCP tools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Validate and strengthen traceability before and after code changes.
|
|
9
|
+
|
|
10
|
+
## MCP workflow
|
|
11
|
+
|
|
12
|
+
- Locate requirements with `kb_search` and inspect exact details with `kb_query`.
|
|
13
|
+
- Apply updates with `kb_upsert` for requirements, scenarios, tests, or facts.
|
|
14
|
+
- Validate constraints and consistency with `kb_check`.
|
|
15
|
+
|
|
16
|
+
## Guidance
|
|
17
|
+
|
|
18
|
+
- Prefer source-linked relationships so symbols and files can be traced back cleanly.
|
|
19
|
+
- Include `kb_query` and `kb_delete` only when explicit cleanups are required.
|
|
20
|
+
- Never directly edit `.kb/**`; all modeling and cleanup goes through MCP tools.
|
|
21
|
+
- Preserve the canonical chain `REQ-* -> SCEN-* -> TEST-*` when adding or changing requirements.
|
|
22
|
+
- Production symbols should implement requirements; test symbols should remain executable evidence for tests.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kibi-usage
|
|
3
|
+
description: Use Kibi MCP discovery tools to find relevant requirements and context before editing code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Help an agent discover existing knowledge in Kibi before making changes.
|
|
9
|
+
|
|
10
|
+
## MCP workflow
|
|
11
|
+
|
|
12
|
+
- Start with `kb_search` using domain keywords and requirement intent.
|
|
13
|
+
- Move to `kb_query` for exact entity retrieval by `id` or `type`.
|
|
14
|
+
- Use `kb_status` when context is stale or branch linkage is unclear.
|
|
15
|
+
- Use curated reports only when the question needs them: `kb_find_gaps`, `kb_coverage`, or `kb_graph`.
|
|
16
|
+
- Mutate only through `kb_upsert` or `kb_delete`, then validate with `kb_check`.
|
|
17
|
+
|
|
18
|
+
## Guidance
|
|
19
|
+
|
|
20
|
+
- Prefer source-linked lookups and traceability-aware retrieval via `kb_query` filters.
|
|
21
|
+
- Never open or patch files under `.kb/` directly.
|
|
22
|
+
- When unsure about scope, re-run `kb_search` with tighter terms, then validate with `kb_query`.
|
|
23
|
+
- Report the KB outcome before completion: updated, no-impact with rationale, or deferred/failed.
|