Ship skills built-in: bundle the catalogue, assign per hire, force humanizer
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
This commit is contained in:
1 parent
55c043f8b4
commit
86aa6fdb29
318 files changed
+77467
-520
No files matched your search
@@ -37,10 +37,12 @@ describe("agent instructions", () => {
|
||||
connectorNames: ["GitHub", "Slack"],
|
||||
language: "en",
|
||||
});
|
||||
expect(text).toContain("every connected service and every company skill");
|
||||
expect(text).toContain("every connected service");
|
||||
expect(text).toContain("grant_connector");
|
||||
expect(text).toContain("grant_skill");
|
||||
expect(text).toContain("grant before you assign");
|
||||
expect(text).toContain("staff every hire with the skills their role needs");
|
||||
expect(text).toContain("humanizer");
|
||||
expect(text).toContain("GitHub, Slack");
|
||||
expect(text).toContain("You run this company");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { stageBuiltinSkillsLibrary } from "../../src/main/services/builtin-skills";
|
||||
|
||||
/**
|
||||
* The real bundled catalogue must stage cleanly: every skill gets a unique
|
||||
* slug, the forced humanizer skill exists, and category files carry the
|
||||
* descriptions agents browse. A broken bundle should fail CI, not first
|
||||
* launch.
|
||||
*/
|
||||
describe("bundled skill catalogue", () => {
|
||||
test("the repo bundle stages with unique slugs and the forced humanizer", () => {
|
||||
const libraryRoot = mkdtempSync(join(tmpdir(), "ac-bundle-check-"));
|
||||
try {
|
||||
const staged = stageBuiltinSkillsLibrary(resolve(process.cwd(), "built-in skills"), libraryRoot);
|
||||
expect(staged.length).toBeGreaterThan(200);
|
||||
expect(staged.some((unit) => unit.slug === "humanizer")).toBe(true);
|
||||
const withDescription = staged.filter((unit) => unit.manifest.description).length;
|
||||
expect(withDescription / staged.length).toBeGreaterThan(0.9);
|
||||
} finally {
|
||||
rmSync(libraryRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { ChatService } from "../../src/main/services/chat-service";
|
||||
import { SkillService, MAX_COMPILED_PROMPT_CHARS } from "../../src/main/services/skill-service";
|
||||
import { importStagedSkills, parseFrontmatter, stageBuiltinSkillsLibrary } from "../../src/main/services/builtin-skills";
|
||||
import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools";
|
||||
|
||||
const HUMANIZER_MD = `---
|
||||
name: humanizer
|
||||
version: 2.1.1
|
||||
description: |
|
||||
Remove signs of AI-generated writing from text. Use when editing or reviewing
|
||||
text to make it sound more natural and human-written.
|
||||
allowed-tools:
|
||||
- Read
|
||||
---
|
||||
|
||||
# Humanizer
|
||||
|
||||
Write like a person: cut inflated symbolism and rule-of-three padding.
|
||||
`;
|
||||
|
||||
const ARCHITECT_MD = `---
|
||||
name: Backend Architect
|
||||
description: Senior backend architect specializing in scalable system design.
|
||||
color: blue
|
||||
emoji: B
|
||||
---
|
||||
|
||||
# Backend Architect Agent Personality
|
||||
|
||||
Design the systems that hold everything up.
|
||||
`;
|
||||
|
||||
interface Harness {
|
||||
connection: DatabaseConnection;
|
||||
organization: OrganizationService;
|
||||
skills: SkillService;
|
||||
orgId: string;
|
||||
sourceRoot: string;
|
||||
libraryRoot: string;
|
||||
}
|
||||
|
||||
function harness(): Harness {
|
||||
const stamp = randomUUID();
|
||||
const sourceRoot = join(tmpdir(), `ac-builtin-src-${stamp}`);
|
||||
const libraryRoot = join(tmpdir(), `ac-builtin-lib-${stamp}`);
|
||||
// Bundle fixture mirroring the real repo layout: a directory skill, a
|
||||
// category of flat skills, and non-skill folders that must be ignored.
|
||||
mkdirSync(join(sourceRoot, "humanizer"), { recursive: true });
|
||||
writeFileSync(join(sourceRoot, "humanizer", "SKILL.md"), HUMANIZER_MD, "utf8");
|
||||
writeFileSync(join(sourceRoot, "humanizer", "README.md"), "# About\n", "utf8");
|
||||
mkdirSync(join(sourceRoot, "engineering"), { recursive: true });
|
||||
writeFileSync(join(sourceRoot, "engineering", "engineering-backend-architect.md"), ARCHITECT_MD, "utf8");
|
||||
writeFileSync(join(sourceRoot, "engineering", "README.md"), "docs, not a skill\n", "utf8");
|
||||
mkdirSync(join(sourceRoot, "scripts"), { recursive: true });
|
||||
writeFileSync(join(sourceRoot, "scripts", "install.sh"), "#!/bin/sh\n", "utf8");
|
||||
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-builtin-${stamp}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const skills = new SkillService(connection.db, join(tmpdir(), `ac-builtin-art-${stamp}`));
|
||||
let orgId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" });
|
||||
});
|
||||
return { connection, organization, skills, orgId, sourceRoot, libraryRoot };
|
||||
}
|
||||
|
||||
describe("built-in skills", () => {
|
||||
test("stages both bundle shapes into the library and imports them once per org", () => {
|
||||
const h = harness();
|
||||
const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot);
|
||||
expect(staged.map((unit) => unit.slug).sort()).toEqual(["engineering-backend-architect", "humanizer"]);
|
||||
expect(existsSync(join(h.libraryRoot, "humanizer", "SKILL.md"))).toBe(true);
|
||||
expect(existsSync(join(h.libraryRoot, "engineering-backend-architect", "SKILL.md"))).toBe(true);
|
||||
const architect = staged.find((unit) => unit.slug === "engineering-backend-architect");
|
||||
expect(architect?.manifest.name).toBe("Backend Architect");
|
||||
expect(architect?.manifest.description).toContain("scalable system design");
|
||||
const humanizer = staged.find((unit) => unit.slug === "humanizer");
|
||||
expect(humanizer?.manifest.version).toBe("2.1.1");
|
||||
expect(humanizer?.manifest.description).toContain("AI-generated writing");
|
||||
|
||||
const first = h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, staged));
|
||||
expect(first).toMatchObject({ imported: 2, skipped: 0, failures: [] });
|
||||
const second = h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, staged));
|
||||
expect(second).toMatchObject({ imported: 0, skipped: 2 });
|
||||
const slugs = (h.connection.db.prepare("select slug from skill_definitions where org_id = ? order by slug").all(h.orgId) as Array<{ slug: string }>)
|
||||
.map((row) => row.slug);
|
||||
expect(slugs).toEqual(["engineering-backend-architect", "humanizer"]);
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("every agent's run carries humanizer; hire-time skills join it and oversized grants are named, not overflowed", () => {
|
||||
const h = harness();
|
||||
// A third, oversized flat skill to prove the compiled-prompt cap.
|
||||
writeFileSync(
|
||||
join(h.sourceRoot, "engineering", "engineering-epic-novelist.md"),
|
||||
`---\nname: Epic Novelist\ndescription: too big to pack\n---\n\n${"x".repeat(MAX_COMPILED_PROMPT_CHARS + 10)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot);
|
||||
h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, staged));
|
||||
|
||||
let ceoId = "";
|
||||
h.connection.transaction(() => {
|
||||
const providerId = (h.connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(h.orgId) as { id: string }).id;
|
||||
ceoId = h.organization.createAgent({
|
||||
orgId: h.orgId, name: "CEO", role: "CEO", department: "Executive",
|
||||
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null, roleLevel: "c_suite",
|
||||
});
|
||||
});
|
||||
|
||||
// The CEO hires with skills; the hire carries them plus forced humanizer.
|
||||
const providerId = (h.connection.db.prepare("select model_provider_id from agents where id = ?").get(ceoId) as { model_provider_id: string }).model_provider_id;
|
||||
const ceoCtx: AgentToolContext = {
|
||||
orgId: h.orgId,
|
||||
agent: { id: ceoId, modelProviderId: providerId, modelId: "gpt-5.4" },
|
||||
taskProjectId: null,
|
||||
taskGoalId: null,
|
||||
workspace: null,
|
||||
};
|
||||
const deps = { db: h.connection.db, organization: h.organization, chat: new ChatService(h.connection.db) };
|
||||
const hired = h.connection.transaction(() => dispatchAgentTool(deps, ceoCtx, {
|
||||
id: "call_1",
|
||||
name: "hire_agent",
|
||||
arguments: { name: "Dana Lee", role: "Engineer", department: "Engineering", skills: ["engineering-backend-architect", "engineering-epic-novelist", "no-such-skill"] },
|
||||
}));
|
||||
expect(hired).toContain("They carry: engineering-backend-architect, engineering-epic-novelist");
|
||||
expect(hired).toContain("Unknown skills skipped: no-such-skill");
|
||||
|
||||
const danaId = (h.connection.db.prepare("select id from agents where org_id = ? and name = 'Dana Lee'").get(h.orgId) as { id: string }).id;
|
||||
const danaSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana" }));
|
||||
const danaPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(danaSnapshot) as { compiled_prompt: string }).compiled_prompt;
|
||||
expect(danaPrompt.indexOf("# Skill: humanizer")).toBe(0);
|
||||
expect(danaPrompt).toContain("# Skill: Backend Architect");
|
||||
expect(danaPrompt).toContain("Skills omitted for size");
|
||||
expect(danaPrompt).toContain("Epic Novelist");
|
||||
expect(danaPrompt.length).toBeLessThan(MAX_COMPILED_PROMPT_CHARS + 5_000);
|
||||
|
||||
// The CEO carries humanizer too - forced by resolution, not by binding.
|
||||
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId)).toBe(true);
|
||||
const ceoSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: ceoId, runId: "run_ceo" }));
|
||||
const ceoPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(ceoSnapshot) as { compiled_prompt: string }).compiled_prompt;
|
||||
expect(ceoPrompt).toContain("# Skill: humanizer");
|
||||
expect(ceoPrompt).not.toContain("# Skill: Backend Architect");
|
||||
|
||||
// list_skills shows descriptions and marks the house-style skill.
|
||||
const listing = dispatchAgentTool(deps, ceoCtx, { id: "call_2", name: "list_skills", arguments: {} });
|
||||
expect(listing).toContain("humanizer (humanizer)");
|
||||
expect(listing).toContain("house style");
|
||||
expect(listing).toContain("Backend Architect (engineering-backend-architect) — Senior backend architect");
|
||||
h.connection.close();
|
||||
});
|
||||
|
||||
test("frontmatter parsing reads inline values and block scalars", () => {
|
||||
expect(parseFrontmatter(ARCHITECT_MD)).toMatchObject({
|
||||
name: "Backend Architect",
|
||||
description: "Senior backend architect specializing in scalable system design.",
|
||||
});
|
||||
expect(parseFrontmatter(HUMANIZER_MD)).toMatchObject({ name: "humanizer", version: "2.1.1" });
|
||||
expect(parseFrontmatter(HUMANIZER_MD).description).toContain("sound more natural and human-written");
|
||||
expect(parseFrontmatter("no frontmatter")).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { SkillService } from "../../src/main/services/skill-service";
|
||||
import { syncSkillsFromDisk } from "../../src/main/services/skill-discovery";
|
||||
|
||||
interface Harness {
|
||||
connection: DatabaseConnection;
|
||||
skills: SkillService;
|
||||
orgId: string;
|
||||
globalRoot: string;
|
||||
workspaceRoot: string;
|
||||
}
|
||||
|
||||
function harness(): Harness {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-skdisc-${randomUUID()}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const workspaceRoot = join(tmpdir(), `ac-skdisc-ws-${randomUUID()}`);
|
||||
mkdirSync(workspaceRoot, { recursive: true });
|
||||
let orgId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" });
|
||||
organization.createWorkspace({ orgId, name: "HQ", localPath: workspaceRoot, projectId: null });
|
||||
});
|
||||
const globalRoot = join(tmpdir(), `ac-skdisc-global-${randomUUID()}`);
|
||||
mkdirSync(globalRoot, { recursive: true });
|
||||
const artifactRoot = join(tmpdir(), `ac-skdisc-art-${randomUUID()}`);
|
||||
return { connection, skills: new SkillService(connection.db, artifactRoot), orgId, globalRoot, workspaceRoot };
|
||||
}
|
||||
|
||||
function writeManifestSkill(root: string, dirName: string, name: string): void {
|
||||
const dir = join(root, dirName);
|
||||
mkdirSync(join(dir, "checks"), { recursive: true });
|
||||
writeFileSync(join(dir, "SKILL.md"), `Guide for ${name}.\n`, "utf8");
|
||||
writeFileSync(join(dir, "checks", "acceptance.txt"), "accepted\n", "utf8");
|
||||
writeFileSync(join(dir, "skill.manifest.json"), JSON.stringify({
|
||||
name,
|
||||
version: "1.0.0",
|
||||
entrypoint: "SKILL.md",
|
||||
capabilities: ["review"],
|
||||
required_tools: ["git"],
|
||||
compatible_models: ["gpt-5.4"],
|
||||
risk_level: "low",
|
||||
tests: ["checks/acceptance.txt"],
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
}, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function writeMarkdownOnlySkill(root: string, dirName: string, frontmatterName: string | null): void {
|
||||
const dir = join(root, dirName);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const frontmatter = frontmatterName ? `---\nname: ${frontmatterName}\ndescription: does things\n---\n\n` : "";
|
||||
writeFileSync(join(dir, "SKILL.md"), `${frontmatter}# Steps\nDo the thing.\n`, "utf8");
|
||||
}
|
||||
|
||||
function skillNames(h: Harness): string[] {
|
||||
return (h.connection.db.prepare("select name from skill_definitions where org_id = ? order by name asc").all(h.orgId) as Array<{ name: string }>)
|
||||
.map((row) => row.name);
|
||||
}
|
||||
|
||||
describe("skill discovery", () => {
|
||||
test("imports skills from the global directory and every workspace", () => {
|
||||
const h = harness();
|
||||
writeManifestSkill(h.globalRoot, "release-gate", "Release Gate");
|
||||
writeMarkdownOnlySkill(join(h.workspaceRoot, ".agents", "skills"), "code-style", "Code Style");
|
||||
|
||||
const report = syncSkillsFromDisk(h.connection.db, h.skills, h.orgId, h.globalRoot);
|
||||
expect(report.imported).toBe(2);
|
||||
expect(report.failures).toEqual([]);
|
||||
expect(skillNames(h)).toEqual(["Code Style", "Release Gate"]);
|
||||
});
|
||||
|
||||
test("a SKILL.md without frontmatter falls back to the directory name", () => {
|
||||
const h = harness();
|
||||
writeMarkdownOnlySkill(h.globalRoot, "incident-runbook", null);
|
||||
syncSkillsFromDisk(h.connection.db, h.skills, h.orgId, h.globalRoot);
|
||||
expect(skillNames(h)).toEqual(["incident-runbook"]);
|
||||
});
|
||||
|
||||
test("re-syncing skips what the org already carries", () => {
|
||||
const h = harness();
|
||||
writeManifestSkill(h.globalRoot, "release-gate", "Release Gate");
|
||||
syncSkillsFromDisk(h.connection.db, h.skills, h.orgId, h.globalRoot);
|
||||
const second = syncSkillsFromDisk(h.connection.db, h.skills, h.orgId, h.globalRoot);
|
||||
expect(second.imported).toBe(0);
|
||||
expect(second.skipped).toBe(1);
|
||||
expect(skillNames(h)).toEqual(["Release Gate"]);
|
||||
});
|
||||
|
||||
test("one broken directory cannot block the rest", () => {
|
||||
const h = harness();
|
||||
writeManifestSkill(h.globalRoot, "good", "Good Skill");
|
||||
const broken = join(h.globalRoot, "broken");
|
||||
mkdirSync(broken, { recursive: true });
|
||||
writeFileSync(join(broken, "skill.manifest.json"), "{ not json", "utf8");
|
||||
writeFileSync(join(broken, "SKILL.md"), "x", "utf8");
|
||||
|
||||
const report = syncSkillsFromDisk(h.connection.db, h.skills, h.orgId, h.globalRoot);
|
||||
expect(report.imported).toBe(1);
|
||||
expect(report.failures).toHaveLength(1);
|
||||
expect(skillNames(h)).toEqual(["Good Skill"]);
|
||||
});
|
||||
});
|
||||
@@ -97,12 +97,23 @@ describe("skill grants", () => {
|
||||
expect(new SkillService(h.connection.db, "").hasRuntimeSkills(h.orgId, h.danaId)).toBe(false);
|
||||
});
|
||||
|
||||
test("the CEO carries every active skill by role, without bindings", () => {
|
||||
test("the humanizer house-style skill reaches every agent without bindings and cannot be revoked", () => {
|
||||
const h = harness();
|
||||
const skills = new SkillService(h.connection.db, "");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId)).toBe(false);
|
||||
// A non-forced skill grants nothing on its own - not even to the CEO.
|
||||
importSkill(h, "Release Gate", "release-gate");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId)).toBe(true);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId)).toBe(false);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId)).toBe(false);
|
||||
// Once the forced skill exists in the catalogue, everyone carries it.
|
||||
importSkill(h, "humanizer", "humanizer");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId)).toBe(true);
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId)).toBe(true);
|
||||
// House style is not revocable, and granting it is a no-op with feedback.
|
||||
const revoked = dispatchAgentTool(h.deps, h.ceoCtx, call("revoke_skill", { agent_name: "Dana", skill: "humanizer" }));
|
||||
expect(revoked).toContain("ERROR");
|
||||
expect(revoked).toContain("house style");
|
||||
const granted = dispatchAgentTool(h.deps, h.ceoCtx, call("grant_skill", { agent_name: "Dana", skill: "humanizer" }));
|
||||
expect(granted).toContain("automatically");
|
||||
expect(skills.hasRuntimeSkills(h.orgId, h.danaId)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -130,7 +130,7 @@ describe("task execution service", () => {
|
||||
expect(capturedPrompt).toContain("Workspace: Task Workspace");
|
||||
expect(capturedPrompt).toContain("Execution context:");
|
||||
expect(capturedPrompt).toContain("Secret scope:");
|
||||
expect(capturedPrompt).toContain("Skill runtime:");
|
||||
expect(capturedPrompt).toContain("Skills you carry this run");
|
||||
expect(capturedPrompt).toContain("Tool allowlist:");
|
||||
expect(state.auditEvents.some((event) => event.entityType === "task_run" && event.action === "completed")).toBe(true);
|
||||
rmSync(skillDir, { recursive: true, force: true });
|
||||
|
||||
Reference in new issue
Block a user