Skills no longer come from the user's ~/.agents library - most users have none, and the CEO branch that compiled the entire catalogue into its prompt overflowed the model context (1.1MB) and failed every CEO run. - Bundle "built-in skills/" with the app (extraResources) and stage it at startup into <profile>/builtin-skills as one dir-per-skill copy - both bundle shapes (SKILL.md dirs and flat category .md files) normalize into the same importable layout - Import the staged catalogue per org at startup and inside onboarding, so the CEO's first run already carries it - Forced house style: resolveRuntimeSnapshot unions the humanizer skill into every agent's runtime (CEO included) as a resolution rule - no binding row to forget, delete, or bypass; revoke_skill refuses it - CEO staffs hires with skills: hire_agent accepts a skills array, grant_skill/revoke_skill adjust later, list_skills now shows descriptions from skill frontmatter - Kill the c_suite compile-all branch; everyone resolves bindings-only, capped at 200k chars with omissions named in the prompt - Remove the Skills settings page and its import/sync/test IPC surface; drop ~/.agents discovery and the AGENTCOMPANY_NEW_SKILLS_ROOT seam in favor of AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR - Tests: staging/import/forced-resolution/cap units, real-bundle CI check, e2e asserts the tab is gone and the catalogue self-imports
82 lines
3.3 KiB
TypeScript
82 lines
3.3 KiB
TypeScript
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { RISK_LEVEL, type RiskLevel } from "../../shared/domain";
|
|
import { assertSafeSkillPath } from "./skill-artifacts";
|
|
|
|
export interface SkillManifest {
|
|
name: string;
|
|
version: string;
|
|
entrypoint: string;
|
|
capabilities: string[];
|
|
requiredTools: string[];
|
|
compatibleModels: string[];
|
|
riskLevel: RiskLevel;
|
|
tests: string[];
|
|
inputs: unknown;
|
|
outputs: unknown;
|
|
/** One-line summary shown when agents browse the library. */
|
|
description?: string;
|
|
}
|
|
|
|
export function loadSkillManifest(directoryPath: string, requestedVersion?: string): SkillManifest {
|
|
const manifestPath = join(directoryPath, "skill.manifest.json");
|
|
if (!existsSync(manifestPath)) throw new Error("skill.manifest.json is required.");
|
|
const raw = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>;
|
|
const manifest = normalizeManifest(raw);
|
|
if (requestedVersion !== undefined && manifest.version !== requestedVersion) {
|
|
throw new Error("Skill manifest version must match the requested version.");
|
|
}
|
|
verifyDeclaredFile(directoryPath, manifest.entrypoint);
|
|
for (const testPath of manifest.tests) verifyDeclaredFile(directoryPath, testPath);
|
|
return manifest;
|
|
}
|
|
|
|
function normalizeManifest(raw: Record<string, unknown>): SkillManifest {
|
|
const manifest: SkillManifest = {
|
|
name: readString(raw.name, "name"),
|
|
version: readSemver(raw.version),
|
|
entrypoint: assertSafeSkillPath(readString(raw.entrypoint, "entrypoint")),
|
|
capabilities: readStringArray(raw.capabilities, "capabilities"),
|
|
requiredTools: readStringArray(raw.required_tools ?? raw.requiredTools, "required_tools"),
|
|
compatibleModels: readStringArray(raw.compatible_models ?? raw.compatibleModels, "compatible_models"),
|
|
riskLevel: readRisk(raw.risk_level ?? raw.riskLevel),
|
|
tests: readStringArray(raw.tests, "tests").map(assertSafeSkillPath),
|
|
inputs: raw.inputs ?? {},
|
|
outputs: raw.outputs ?? {},
|
|
};
|
|
if (typeof raw.description === "string" && raw.description.trim()) {
|
|
manifest.description = raw.description.trim();
|
|
}
|
|
return manifest;
|
|
}
|
|
|
|
function readString(value: unknown, field: string): string {
|
|
if (typeof value !== "string" || !value.trim()) throw new Error(`Skill manifest field ${field} is required.`);
|
|
return value.trim();
|
|
}
|
|
|
|
function readSemver(value: unknown): string {
|
|
const version = readString(value, "version");
|
|
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
|
|
throw new Error("Skill manifest version must be valid semver.");
|
|
}
|
|
return version;
|
|
}
|
|
|
|
function readStringArray(value: unknown, field: string): string[] {
|
|
if (!Array.isArray(value)) throw new Error(`Skill manifest field ${field} must be an array.`);
|
|
return value.map((item) => readString(item, field));
|
|
}
|
|
|
|
function readRisk(value: unknown): RiskLevel {
|
|
if (value === RISK_LEVEL.LOW || value === RISK_LEVEL.MEDIUM || value === RISK_LEVEL.HIGH) return value;
|
|
throw new Error("Skill manifest risk_level must be low, medium, or high.");
|
|
}
|
|
|
|
function verifyDeclaredFile(directoryPath: string, relativePath: string): void {
|
|
const path = join(directoryPath, ...relativePath.split("/"));
|
|
if (!existsSync(path) || !lstatSync(path).isFile()) {
|
|
throw new Error(`Skill manifest file was not found: ${relativePath}`);
|
|
}
|
|
}
|