kibi-runtime 1.0.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/engine-daemon.js +2402 -0
- package/dist/engine.d.ts +65 -0
- package/dist/engine.js +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +279965 -0
- package/dist/skill-operations.d.ts +69 -0
- package/dist/skill-operations.js +105 -0
- package/dist/skill-system/errors.d.ts +18 -0
- package/dist/skill-system/errors.js +36 -0
- package/dist/skill-system/loader.d.ts +8 -0
- package/dist/skill-system/loader.js +81 -0
- package/dist/skill-system/paths.d.ts +5 -0
- package/dist/skill-system/paths.js +25 -0
- package/dist/skill-system/types.d.ts +14 -0
- package/dist/skill-system/types.js +1 -0
- package/dist/skill-system/validation.d.ts +9 -0
- package/dist/skill-system/validation.js +119 -0
- package/dist/skills/kibi-bootstrap/SKILL.md +58 -0
- package/dist/skills/kibi-bootstrap/resources/bootstrap.md +11 -0
- package/dist/skills/kibi-bootstrap/resources/branch-lifecycle.md +7 -0
- package/dist/skills/kibi-bootstrap/resources/operation-access.md +5 -0
- package/dist/skills/kibi-bootstrap/resources/source-authoring.md +8 -0
- package/dist/skills/kibi-freshness/SKILL.md +74 -0
- package/dist/skills/kibi-freshness/resources/branch-lifecycle.md +7 -0
- package/dist/skills/kibi-freshness/resources/operation-access.md +6 -0
- package/dist/skills/kibi-freshness/resources/source-authoring.md +7 -0
- package/dist/skills/kibi-traceability/SKILL.md +60 -0
- package/dist/skills/kibi-traceability/resources/branch-lifecycle.md +7 -0
- package/dist/skills/kibi-traceability/resources/operation-access.md +5 -0
- package/dist/skills/kibi-traceability/resources/source-authoring.md +6 -0
- package/dist/skills/kibi-traceability/resources/traceability.md +8 -0
- package/dist/skills/kibi-usage/SKILL.md +152 -0
- package/dist/skills/kibi-usage/resources/branch-lifecycle.md +31 -0
- package/dist/skills/kibi-usage/resources/fact-lanes.md +235 -0
- package/dist/skills/kibi-usage/resources/kb-improvement.md +38 -0
- package/dist/skills/kibi-usage/resources/logic-ir.md +43 -0
- package/dist/skills/kibi-usage/resources/operation-access.md +42 -0
- package/dist/skills/kibi-usage/resources/relationship-directions.md +119 -0
- package/dist/skills/kibi-usage/resources/source-authoring.md +31 -0
- package/dist/skills/kibi-usage/resources/ui-requirements.md +96 -0
- package/dist/skills/kibi-usage/resources/workflows.md +150 -0
- package/dist/skills.d.ts +3 -0
- package/dist/skills.js +2 -0
- package/package.json +37 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { OperationResult } from "kibi-cli/operations";
|
|
2
|
+
import type { OperationContext } from "kibi-cli/operations/runtime-types";
|
|
3
|
+
import type { SkillManifest } from "./skill-system/types.js";
|
|
4
|
+
declare function executeSkillsList(_input: Readonly<Record<string, unknown>>, _context: OperationContext): Promise<OperationResult<{
|
|
5
|
+
readonly skills: readonly SkillManifest[];
|
|
6
|
+
}>>;
|
|
7
|
+
declare function executeSkillsLoad(input: Readonly<Record<string, unknown>>, _context: OperationContext): Promise<OperationResult<{
|
|
8
|
+
readonly metadata: SkillManifest;
|
|
9
|
+
readonly body: string;
|
|
10
|
+
readonly resources: readonly string[];
|
|
11
|
+
readonly contentHash: string;
|
|
12
|
+
readonly sourceType: "bundled";
|
|
13
|
+
}>>;
|
|
14
|
+
declare function executeSkillsRead(input: Readonly<Record<string, unknown>>, _context: OperationContext): Promise<OperationResult<{
|
|
15
|
+
readonly content: string;
|
|
16
|
+
}>>;
|
|
17
|
+
export declare const skillsListSpec: {
|
|
18
|
+
readonly name: "kb_skills_list";
|
|
19
|
+
readonly cliName: "skills list";
|
|
20
|
+
readonly description: "List bundled Kibi agent skills available for progressive disclosure. Read-only; does not mutate the KB or require Prolog.";
|
|
21
|
+
readonly businessInputSchema: {
|
|
22
|
+
readonly type: "object";
|
|
23
|
+
readonly properties: {};
|
|
24
|
+
};
|
|
25
|
+
readonly requiresProlog: false;
|
|
26
|
+
readonly effects: readonly ["local-read"];
|
|
27
|
+
readonly execute: typeof executeSkillsList;
|
|
28
|
+
};
|
|
29
|
+
export declare const skillsLoadSpec: {
|
|
30
|
+
readonly name: "kb_skills_load";
|
|
31
|
+
readonly cliName: "skills load";
|
|
32
|
+
readonly description: "Load a bundled Kibi agent skill by ID, returning its manifest metadata, Markdown body, declared resources, content hash, and source type. Read-only; does not execute scripts or require Prolog.";
|
|
33
|
+
readonly businessInputSchema: {
|
|
34
|
+
readonly type: "object";
|
|
35
|
+
readonly required: readonly ["id"];
|
|
36
|
+
readonly properties: {
|
|
37
|
+
readonly id: {
|
|
38
|
+
readonly type: "string";
|
|
39
|
+
readonly description: "Bundled skill ID to load. Example: 'kibi-usage'.";
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
readonly requiresProlog: false;
|
|
44
|
+
readonly effects: readonly ["local-read"];
|
|
45
|
+
readonly execute: typeof executeSkillsLoad;
|
|
46
|
+
};
|
|
47
|
+
export declare const skillsReadSpec: {
|
|
48
|
+
readonly name: "kb_skills_read";
|
|
49
|
+
readonly cliName: "skills read";
|
|
50
|
+
readonly description: "Read a declared resource from a bundled Kibi agent skill. Resource paths are restricted to the skill manifest; arbitrary file paths are not exposed. Read-only; does not require Prolog.";
|
|
51
|
+
readonly businessInputSchema: {
|
|
52
|
+
readonly type: "object";
|
|
53
|
+
readonly required: readonly ["id", "resource"];
|
|
54
|
+
readonly properties: {
|
|
55
|
+
readonly id: {
|
|
56
|
+
readonly type: "string";
|
|
57
|
+
readonly description: "Bundled skill ID. Example: 'kibi-usage'.";
|
|
58
|
+
};
|
|
59
|
+
readonly resource: {
|
|
60
|
+
readonly type: "string";
|
|
61
|
+
readonly description: "Manifest-declared resource path to read. Example: 'resources/workflows.md'.";
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
readonly requiresProlog: false;
|
|
66
|
+
readonly effects: readonly ["local-read"];
|
|
67
|
+
readonly execute: typeof executeSkillsRead;
|
|
68
|
+
};
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { listBundledSkills, loadBundledSkill, readBundledSkillResource, } from "./skills.js";
|
|
3
|
+
function assertNonEmptyString(value, field) {
|
|
4
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
5
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
async function executeSkillsList(_input, _context) {
|
|
9
|
+
const skills = listBundledSkills();
|
|
10
|
+
const ids = skills.map((skill) => skill.id).join(", ") || "none";
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{ type: "text", text: `Found ${skills.length} bundled skills: ${ids}` },
|
|
14
|
+
],
|
|
15
|
+
structuredContent: { skills },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function executeSkillsLoad(input, _context) {
|
|
19
|
+
const id = typeof input.id === "string" ? input.id : "";
|
|
20
|
+
assertNonEmptyString(id, "id");
|
|
21
|
+
const bundle = loadBundledSkill(id);
|
|
22
|
+
const resources = bundle.manifest.resources ?? [];
|
|
23
|
+
const contentHash = createHash("sha256")
|
|
24
|
+
.update(bundle.body, "utf8")
|
|
25
|
+
.digest("hex");
|
|
26
|
+
const resourceList = resources.length === 0 ? "none" : resources.join(", ");
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: `Loaded bundled skill ${bundle.manifest.id} with ${resources.length} resources: ${resourceList}`,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
structuredContent: {
|
|
35
|
+
metadata: bundle.manifest,
|
|
36
|
+
body: bundle.body,
|
|
37
|
+
resources,
|
|
38
|
+
contentHash,
|
|
39
|
+
sourceType: "bundled",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function executeSkillsRead(input, _context) {
|
|
44
|
+
const id = typeof input.id === "string" ? input.id : "";
|
|
45
|
+
const resource = typeof input.resource === "string" ? input.resource : "";
|
|
46
|
+
assertNonEmptyString(id, "id");
|
|
47
|
+
assertNonEmptyString(resource, "resource");
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{ type: "text", text: `Read bundled skill resource ${id}/${resource}` },
|
|
51
|
+
],
|
|
52
|
+
structuredContent: {
|
|
53
|
+
content: readBundledSkillResource(id, resource),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export const skillsListSpec = {
|
|
58
|
+
name: "kb_skills_list",
|
|
59
|
+
cliName: "skills list",
|
|
60
|
+
description: "List bundled Kibi agent skills available for progressive disclosure. Read-only; does not mutate the KB or require Prolog.",
|
|
61
|
+
businessInputSchema: { type: "object", properties: {} },
|
|
62
|
+
requiresProlog: false,
|
|
63
|
+
effects: ["local-read"],
|
|
64
|
+
execute: executeSkillsList,
|
|
65
|
+
};
|
|
66
|
+
export const skillsLoadSpec = {
|
|
67
|
+
name: "kb_skills_load",
|
|
68
|
+
cliName: "skills load",
|
|
69
|
+
description: "Load a bundled Kibi agent skill by ID, returning its manifest metadata, Markdown body, declared resources, content hash, and source type. Read-only; does not execute scripts or require Prolog.",
|
|
70
|
+
businessInputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
required: ["id"],
|
|
73
|
+
properties: {
|
|
74
|
+
id: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Bundled skill ID to load. Example: 'kibi-usage'.",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
requiresProlog: false,
|
|
81
|
+
effects: ["local-read"],
|
|
82
|
+
execute: executeSkillsLoad,
|
|
83
|
+
};
|
|
84
|
+
export const skillsReadSpec = {
|
|
85
|
+
name: "kb_skills_read",
|
|
86
|
+
cliName: "skills read",
|
|
87
|
+
description: "Read a declared resource from a bundled Kibi agent skill. Resource paths are restricted to the skill manifest; arbitrary file paths are not exposed. Read-only; does not require Prolog.",
|
|
88
|
+
businessInputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
required: ["id", "resource"],
|
|
91
|
+
properties: {
|
|
92
|
+
id: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Bundled skill ID. Example: 'kibi-usage'.",
|
|
95
|
+
},
|
|
96
|
+
resource: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Manifest-declared resource path to read. Example: 'resources/workflows.md'.",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
requiresProlog: false,
|
|
103
|
+
effects: ["local-read"],
|
|
104
|
+
execute: executeSkillsRead,
|
|
105
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class SkillNotFoundError extends Error {
|
|
2
|
+
constructor(id: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class SkillResourceNotFoundError extends Error {
|
|
5
|
+
constructor(id: string, resourcePath: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class SkillResourceOutOfBoundsError extends Error {
|
|
8
|
+
constructor(id: string, resourcePath: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class SkillValidationError extends Error {
|
|
11
|
+
readonly field: string;
|
|
12
|
+
constructor(field: string, message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class SkillOversizeError extends Error {
|
|
15
|
+
readonly maxBytes: number;
|
|
16
|
+
readonly actualBytes: number;
|
|
17
|
+
constructor(pathLike: string, maxBytes: number, actualBytes: number);
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class SkillNotFoundError extends Error {
|
|
2
|
+
constructor(id) {
|
|
3
|
+
super(`Skill not found: ${id}`);
|
|
4
|
+
this.name = "SkillNotFoundError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class SkillResourceNotFoundError extends Error {
|
|
8
|
+
constructor(id, resourcePath) {
|
|
9
|
+
super(`Skill resource not found: ${id}/${resourcePath}`);
|
|
10
|
+
this.name = "SkillResourceNotFoundError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class SkillResourceOutOfBoundsError extends Error {
|
|
14
|
+
constructor(id, resourcePath) {
|
|
15
|
+
super(`Skill resource escapes bundle root: ${id}/${resourcePath}`);
|
|
16
|
+
this.name = "SkillResourceOutOfBoundsError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class SkillValidationError extends Error {
|
|
20
|
+
field;
|
|
21
|
+
constructor(field, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "SkillValidationError";
|
|
24
|
+
this.field = field;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class SkillOversizeError extends Error {
|
|
28
|
+
maxBytes;
|
|
29
|
+
actualBytes;
|
|
30
|
+
constructor(pathLike, maxBytes, actualBytes) {
|
|
31
|
+
super(`Skill file exceeds ${maxBytes} bytes: ${pathLike} (${actualBytes} bytes)`);
|
|
32
|
+
this.name = "SkillOversizeError";
|
|
33
|
+
this.maxBytes = maxBytes;
|
|
34
|
+
this.actualBytes = actualBytes;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SkillBundle, SkillManifest } from "./types.js";
|
|
2
|
+
export declare function setBundledSkillsDir(dir: string): void;
|
|
3
|
+
export declare function resetBundledSkillsDir(): void;
|
|
4
|
+
export declare function listBundledSkills(): SkillManifest[];
|
|
5
|
+
export declare function loadBundledSkill(id: string): SkillBundle;
|
|
6
|
+
export declare function loadBundledSkillFrom(skillsDir: string, id: string): SkillBundle;
|
|
7
|
+
export declare function readBundledSkillResource(id: string, resourcePath: string): string;
|
|
8
|
+
export declare function readBundledSkillResourceFrom(skillsDir: string, id: string, resourcePath: string): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { SkillNotFoundError, SkillResourceNotFoundError, SkillResourceOutOfBoundsError, } from "./errors.js";
|
|
5
|
+
import { SKILL_FILE_NAME, isPathOutOfBounds, isWithinRoot, normalizeResourcePath, } from "./paths.js";
|
|
6
|
+
import { RESOURCE_MAX_BYTES, assertMaxBytes, parseSkillBundle, } from "./validation.js";
|
|
7
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
// Source tests load this module from src/skill-system, while the published
|
|
9
|
+
// bundle inlines it into dist/index.js and copies assets to dist/skills.
|
|
10
|
+
const defaultBundledSkillsDir = existsSync(resolve(moduleDir, "skills"))
|
|
11
|
+
? resolve(moduleDir, "skills")
|
|
12
|
+
: resolve(moduleDir, "../skills");
|
|
13
|
+
let bundledSkillsDir = defaultBundledSkillsDir;
|
|
14
|
+
export function setBundledSkillsDir(dir) {
|
|
15
|
+
bundledSkillsDir = dir;
|
|
16
|
+
}
|
|
17
|
+
export function resetBundledSkillsDir() {
|
|
18
|
+
bundledSkillsDir = defaultBundledSkillsDir;
|
|
19
|
+
}
|
|
20
|
+
export function listBundledSkills() {
|
|
21
|
+
return listFrom(bundledSkillsDir);
|
|
22
|
+
}
|
|
23
|
+
export function loadBundledSkill(id) {
|
|
24
|
+
return loadBundledSkillFrom(bundledSkillsDir, id);
|
|
25
|
+
}
|
|
26
|
+
// implements REQ-reusable-skill-subsystem
|
|
27
|
+
// covered_by TEST-skill-cli-load-validate
|
|
28
|
+
export function loadBundledSkillFrom(skillsDir, id) {
|
|
29
|
+
const root = findRoot(resolve(skillsDir), id);
|
|
30
|
+
if (!root)
|
|
31
|
+
throw new SkillNotFoundError(id);
|
|
32
|
+
return parseSkillBundle(root);
|
|
33
|
+
}
|
|
34
|
+
export function readBundledSkillResource(id, resourcePath) {
|
|
35
|
+
return readBundle(loadBundledSkill(id), id, resourcePath);
|
|
36
|
+
}
|
|
37
|
+
// implements REQ-reusable-skill-subsystem
|
|
38
|
+
// covered_by TEST-skill-cli-load-validate
|
|
39
|
+
export function readBundledSkillResourceFrom(skillsDir, id, resourcePath) {
|
|
40
|
+
return readBundle(loadBundledSkillFrom(skillsDir, id), id, resourcePath);
|
|
41
|
+
}
|
|
42
|
+
function listFrom(dir) {
|
|
43
|
+
if (!existsSync(dir))
|
|
44
|
+
return [];
|
|
45
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
46
|
+
.filter((entry) => entry.isDirectory())
|
|
47
|
+
.map((entry) => join(dir, entry.name))
|
|
48
|
+
.filter((root) => existsSync(join(root, SKILL_FILE_NAME)))
|
|
49
|
+
.map((root) => parseSkillBundle(root).manifest)
|
|
50
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
51
|
+
}
|
|
52
|
+
function findRoot(dir, id) {
|
|
53
|
+
return listRoots(dir).find((root) => parseSkillBundle(root).manifest.id === id);
|
|
54
|
+
}
|
|
55
|
+
function listRoots(dir) {
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
return [];
|
|
58
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
59
|
+
.filter((entry) => entry.isDirectory())
|
|
60
|
+
.map((entry) => join(dir, entry.name))
|
|
61
|
+
.filter((root) => existsSync(join(root, SKILL_FILE_NAME)));
|
|
62
|
+
}
|
|
63
|
+
function readBundle(bundle, id, resourcePath) {
|
|
64
|
+
const resource = normalizeResourcePath(resourcePath);
|
|
65
|
+
if (!resource || isPathOutOfBounds(resourcePath))
|
|
66
|
+
throw new SkillResourceOutOfBoundsError(id, resourcePath);
|
|
67
|
+
if (!(bundle.manifest.resources ?? []).some((declared) => normalizeResourcePath(declared) === resource))
|
|
68
|
+
throw new SkillResourceNotFoundError(id, resourcePath);
|
|
69
|
+
const candidate = resolve(bundle.rootDir, resource);
|
|
70
|
+
try {
|
|
71
|
+
if (!isWithinRoot(realpathSync(bundle.rootDir), realpathSync(candidate)))
|
|
72
|
+
throw new SkillResourceOutOfBoundsError(id, resourcePath);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof SkillResourceOutOfBoundsError)
|
|
76
|
+
throw error;
|
|
77
|
+
throw new SkillResourceNotFoundError(id, resourcePath);
|
|
78
|
+
}
|
|
79
|
+
assertMaxBytes(candidate, RESOURCE_MAX_BYTES);
|
|
80
|
+
return readFileSync(candidate, "utf8");
|
|
81
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const SKILL_FILE_NAME = "SKILL.md";
|
|
2
|
+
export declare function normalizeResourcePath(pathLike: string): string;
|
|
3
|
+
export declare function isPathOutOfBounds(pathLike: string): boolean;
|
|
4
|
+
export declare function isWithinRoot(rootDir: string, candidatePath: string): boolean;
|
|
5
|
+
export declare function resolveSkillFilePath(pathLike: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
3
|
+
export const SKILL_FILE_NAME = "SKILL.md";
|
|
4
|
+
export function normalizeResourcePath(pathLike) {
|
|
5
|
+
return normalize(pathLike).replaceAll("\\", "/");
|
|
6
|
+
}
|
|
7
|
+
export function isPathOutOfBounds(pathLike) {
|
|
8
|
+
const normalized = normalizeResourcePath(pathLike);
|
|
9
|
+
return (isAbsolute(pathLike) ||
|
|
10
|
+
normalized === ".." ||
|
|
11
|
+
normalized.startsWith("../") ||
|
|
12
|
+
normalized.includes("/../"));
|
|
13
|
+
}
|
|
14
|
+
export function isWithinRoot(rootDir, candidatePath) {
|
|
15
|
+
const rel = relative(rootDir, candidatePath);
|
|
16
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
17
|
+
}
|
|
18
|
+
export function resolveSkillFilePath(pathLike) {
|
|
19
|
+
const resolved = resolve(pathLike);
|
|
20
|
+
if (existsSync(resolved) && statSync(resolved).isDirectory())
|
|
21
|
+
return join(resolved, SKILL_FILE_NAME);
|
|
22
|
+
return resolved.endsWith(SKILL_FILE_NAME)
|
|
23
|
+
? resolved
|
|
24
|
+
: join(resolved, SKILL_FILE_NAME);
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SkillManifest {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
version: string;
|
|
6
|
+
kibiCompatibility: string;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
resources?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface SkillBundle {
|
|
11
|
+
manifest: SkillManifest;
|
|
12
|
+
body: string;
|
|
13
|
+
rootDir: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SkillValidationError } from "./errors.js";
|
|
2
|
+
import type { SkillBundle } from "./types.js";
|
|
3
|
+
export declare const RESOURCE_MAX_BYTES: number;
|
|
4
|
+
export declare function assertMaxBytes(pathLike: string, maxBytes: number): void;
|
|
5
|
+
export declare function parseSkillBundle(rootDir: string): SkillBundle;
|
|
6
|
+
export declare function validateSkillBundle(pathLike: string): {
|
|
7
|
+
valid: boolean;
|
|
8
|
+
errors: SkillValidationError[];
|
|
9
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { SkillOversizeError, SkillValidationError } from "./errors.js";
|
|
5
|
+
import { SKILL_FILE_NAME, isPathOutOfBounds, isWithinRoot, normalizeResourcePath, resolveSkillFilePath, } from "./paths.js";
|
|
6
|
+
const SKILL_MARKDOWN_MAX_BYTES = 256 * 1024;
|
|
7
|
+
export const RESOURCE_MAX_BYTES = 128 * 1024;
|
|
8
|
+
export function assertMaxBytes(pathLike, maxBytes) {
|
|
9
|
+
const size = statSync(pathLike).size;
|
|
10
|
+
if (size > maxBytes)
|
|
11
|
+
throw new SkillOversizeError(pathLike, maxBytes, size);
|
|
12
|
+
}
|
|
13
|
+
export function parseSkillBundle(rootDir) {
|
|
14
|
+
const path = resolve(rootDir, SKILL_FILE_NAME);
|
|
15
|
+
assertMaxBytes(path, SKILL_MARKDOWN_MAX_BYTES);
|
|
16
|
+
const parsed = matter(readFileSync(path, "utf8"));
|
|
17
|
+
const [error] = validateManifestData(parsed.data);
|
|
18
|
+
if (error)
|
|
19
|
+
throw error;
|
|
20
|
+
return {
|
|
21
|
+
manifest: coerceManifest(parsed.data),
|
|
22
|
+
body: parsed.content,
|
|
23
|
+
rootDir: resolve(rootDir),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function validateSkillBundle(pathLike) {
|
|
27
|
+
const path = resolveSkillFilePath(pathLike);
|
|
28
|
+
const errors = [];
|
|
29
|
+
if (!existsSync(path))
|
|
30
|
+
return {
|
|
31
|
+
valid: false,
|
|
32
|
+
errors: [
|
|
33
|
+
new SkillValidationError("SKILL.md", `Missing ${SKILL_FILE_NAME}`),
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
const parsed = matter(readFileSync(path, "utf8"));
|
|
37
|
+
errors.push(...validateManifestData(parsed.data));
|
|
38
|
+
if (errors.length === 0)
|
|
39
|
+
validateBundleContents(path, coerceManifest(parsed.data), errors);
|
|
40
|
+
return { valid: errors.length === 0, errors };
|
|
41
|
+
}
|
|
42
|
+
function validateBundleContents(path, manifest, errors) {
|
|
43
|
+
try {
|
|
44
|
+
assertMaxBytes(path, SKILL_MARKDOWN_MAX_BYTES);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
errors.push(new SkillValidationError("SKILL.md", error instanceof Error ? error.message : String(error)));
|
|
48
|
+
}
|
|
49
|
+
let root;
|
|
50
|
+
try {
|
|
51
|
+
root = realpathSync(dirname(path));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
errors.push(new SkillValidationError("SKILL.md", error instanceof Error ? error.message : String(error)));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const resource of manifest.resources ?? []) {
|
|
58
|
+
const resourcePath = resolve(dirname(path), resource);
|
|
59
|
+
try {
|
|
60
|
+
if (!existsSync(resourcePath)) {
|
|
61
|
+
errors.push(new SkillValidationError("resources", `Missing skill resource: ${resource}`));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const real = realpathSync(resourcePath);
|
|
65
|
+
if (!isWithinRoot(root, real)) {
|
|
66
|
+
errors.push(new SkillValidationError("resources", `Skill resource escapes bundle root: ${resource}`));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
assertMaxBytes(resourcePath, RESOURCE_MAX_BYTES);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
errors.push(new SkillValidationError("resources", error instanceof Error ? error.message : String(error)));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function validateManifestData(data) {
|
|
77
|
+
const errors = [];
|
|
78
|
+
for (const field of [
|
|
79
|
+
"id",
|
|
80
|
+
"name",
|
|
81
|
+
"description",
|
|
82
|
+
"version",
|
|
83
|
+
"kibiCompatibility",
|
|
84
|
+
])
|
|
85
|
+
if (typeof data[field] !== "string" || data[field].trim() === "")
|
|
86
|
+
errors.push(new SkillValidationError(field, `Missing required skill field: ${field}`));
|
|
87
|
+
if (typeof data.version === "string" &&
|
|
88
|
+
!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(data.version))
|
|
89
|
+
errors.push(new SkillValidationError("version", `Invalid skill version: ${data.version}`));
|
|
90
|
+
for (const [field, value] of [
|
|
91
|
+
["tags", data.tags],
|
|
92
|
+
["resources", data.resources],
|
|
93
|
+
]) {
|
|
94
|
+
if (value !== undefined && !isStringArray(value))
|
|
95
|
+
errors.push(new SkillValidationError(field, `Skill ${field} must be strings`));
|
|
96
|
+
}
|
|
97
|
+
if (isStringArray(data.resources))
|
|
98
|
+
for (const resource of data.resources)
|
|
99
|
+
if (!normalizeResourcePath(resource) || isPathOutOfBounds(resource))
|
|
100
|
+
errors.push(new SkillValidationError("resources", `Invalid skill resource: ${resource}`));
|
|
101
|
+
return errors;
|
|
102
|
+
}
|
|
103
|
+
function coerceManifest(data) {
|
|
104
|
+
const manifest = {
|
|
105
|
+
id: String(data.id),
|
|
106
|
+
name: String(data.name),
|
|
107
|
+
description: String(data.description),
|
|
108
|
+
version: String(data.version),
|
|
109
|
+
kibiCompatibility: String(data.kibiCompatibility),
|
|
110
|
+
};
|
|
111
|
+
if (isStringArray(data.tags))
|
|
112
|
+
manifest.tags = data.tags;
|
|
113
|
+
if (isStringArray(data.resources))
|
|
114
|
+
manifest.resources = data.resources.map(normalizeResourcePath);
|
|
115
|
+
return manifest;
|
|
116
|
+
}
|
|
117
|
+
function isStringArray(value) {
|
|
118
|
+
return (Array.isArray(value) && value.every((item) => typeof item === "string"));
|
|
119
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: kibi-bootstrap
|
|
3
|
+
name: kibi-bootstrap
|
|
4
|
+
description: Bootstrap Kibi from the current Git checkout with exact Git attachment, preview approval, source-first writes, and repair-safe completion.
|
|
5
|
+
version: 3.0.0
|
|
6
|
+
kibiCompatibility: ">=1.0.0"
|
|
7
|
+
tags:
|
|
8
|
+
- kibi
|
|
9
|
+
- bootstrap
|
|
10
|
+
- source-first
|
|
11
|
+
- agent-guidance
|
|
12
|
+
resources:
|
|
13
|
+
- resources/bootstrap.md
|
|
14
|
+
- resources/branch-lifecycle.md
|
|
15
|
+
- resources/source-authoring.md
|
|
16
|
+
- resources/operation-access.md
|
|
17
|
+
---
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
Seed branch-local Kibi knowledge for an attached but thin repository without
|
|
21
|
+
creating a parallel human-maintained ticket system. Ask at most four bounded
|
|
22
|
+
context questions only when the planner says repository evidence is
|
|
23
|
+
insufficient; agents own deterministic translation and humans resolve genuine
|
|
24
|
+
ambiguity. A seeded repository hands off to the normal Kibi workflow.
|
|
25
|
+
|
|
26
|
+
## Interface and preview
|
|
27
|
+
|
|
28
|
+
Use the visible approved MCP surface or the trusted project-local CLI as equal
|
|
29
|
+
peer interfaces. If neither is available, stop. Run `kb_plan_bootstrap` (or
|
|
30
|
+
`plan-bootstrap --input`) read-only. If it returns `needs_context`, ask only its
|
|
31
|
+
bounded questions (never more than four), then rerun the planner. Show the
|
|
32
|
+
complete returned `structuredContent.plan`, including its canonical hash, and
|
|
33
|
+
get explicit approval before any write. Pass that plan object unchanged to
|
|
34
|
+
`kb_apply_plan`; do not reconstruct it from preview fields.
|
|
35
|
+
|
|
36
|
+
For CLI JSON, use the trusted route with `--input` (for example
|
|
37
|
+
`printf '%s\n' '{}' | kibi status --input -`); MCP and CLI are semantic peers.
|
|
38
|
+
Read the branch-lifecycle and source-authoring resources before migration,
|
|
39
|
+
source writes, or repair actions.
|
|
40
|
+
|
|
41
|
+
## Apply and verify
|
|
42
|
+
|
|
43
|
+
Apply the approved `kibi.bootstrap-plan.v1` by calling `kb_apply_plan` with the
|
|
44
|
+
exact plan and approved hash. The operation owns dependency ordering,
|
|
45
|
+
source-first writes, sequential mutation, and recovery journaling. Direct
|
|
46
|
+
`kb_upsert` is forbidden for every kibi-bootstrap task, including after
|
|
47
|
+
approval; never replay plan actions manually. Use `kb_delete` only for an
|
|
48
|
+
approved hash-bound deletion plan; evolve requirements with `supersedes`.
|
|
49
|
+
Finish with `kb_check` and `kb_status`.
|
|
50
|
+
|
|
51
|
+
Consume the versioned `kibiProtocol: 1` result envelope: inspect `status`, `effects`,
|
|
52
|
+
`diagnostics`, and `nextActions`. On `committed_with_repairs`, execute required
|
|
53
|
+
repair actions and never retry the original mutation.
|
|
54
|
+
|
|
55
|
+
Kibi may author tracked Markdown/YAML/manifests and relationship shards
|
|
56
|
+
transactionally, but never Git-stages or commits them. Never read or edit
|
|
57
|
+
`.kb` directly. Missing exact branch stores are compiled from this checkout's
|
|
58
|
+
tracked sources by `kibi sync`; Kibi does not copy another branch's store.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Bootstrap resource
|
|
2
|
+
|
|
3
|
+
`kb_plan_bootstrap` is read-only and returns `kibi.bootstrap-plan.v1`. Review
|
|
4
|
+
candidate evidence, exact actions, dependencies, expected snapshots, source
|
|
5
|
+
hashes, bounded questions, diagnostics, and the canonical `planHash` before
|
|
6
|
+
approving. A preview does not authorize source edits. After approval, pass the
|
|
7
|
+
unchanged returned `structuredContent.plan` to `kb_apply_plan` once with its
|
|
8
|
+
approved hash; inspect its typed result
|
|
9
|
+
and follow `nextActions` if it returns `committed_with_repairs`. Direct
|
|
10
|
+
`kb_upsert` is forbidden for every bootstrap task; never manually replay the
|
|
11
|
+
plan through it.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Branch lifecycle
|
|
2
|
+
|
|
3
|
+
Use the exact `KIBI_BRANCH` value or Git symbolic HEAD. Hashed stores carry a
|
|
4
|
+
matching `branch.json`; missing stores are compiled from tracked sources by
|
|
5
|
+
`kibi sync`. Legacy literal stores require explicit migration. Kibi never
|
|
6
|
+
copies stores between branches or resolves Git conflicts. Read the preview,
|
|
7
|
+
approval hash, quarantine, restore, and purge details before lifecycle work.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Source authoring
|
|
2
|
+
|
|
3
|
+
Tracked documents, manifests, and relationship shards are authoritative.
|
|
4
|
+
Kibi may write them transactionally but does not stage or commit them. Use
|
|
5
|
+
`document.path` for ambiguous new targets, preserve existing bodies when the
|
|
6
|
+
body is omitted, and use hash-bound deletion plans. On
|
|
7
|
+
`committed_with_repairs`, follow typed `nextActions` and never retry the
|
|
8
|
+
original mutation.
|