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:
ZacharyZhang-NY committed 2026-07-08 21:18:50 -04:00
1 parent 55c043f8b4
commit 86aa6fdb29
318 files changed
+77467 -520

No files matched your search

+14 -9
View File
@@ -28,7 +28,7 @@ import { AutomationService } from "../services/automation-service";
import { MeetingService } from "../services/meeting-service";
import { SprintService } from "../services/sprint-service";
import { ComposioConnectionService } from "../services/composio/composio-connection-service";
import { syncSkillsFromDisk } from "../services/skill-discovery";
import { importStagedSkills, stageBuiltinSkillsLibrary } from "../services/builtin-skills";
import { coalesce } from "./coalesce";
import { normalizeExternalBrowserUrl } from "./external-url";
@@ -36,6 +36,10 @@ export interface IpcDeps {
connection: DatabaseConnection;
mainWindow: BrowserWindow;
artifactRoot: string;
/** Bundled built-in skills source (repo folder in dev, resources when packaged). */
builtinSkillsSource: string;
/** Normalized on-disk skill library under the profile directory. */
builtinSkillsLibrary: string;
}
export function registerIpcHandlers(deps: IpcDeps): void {
@@ -94,9 +98,17 @@ export function registerIpcHandlers(deps: IpcDeps): void {
ipcMain.handle(IPC_CHANNELS.bootstrapOnboarding, (_event, input) => {
try {
let summary;
let summary: { orgId: string } | undefined;
// Staging happens before the transaction (file mirroring), the import
// inside it: the CEO's kickoff run can be claimed within seconds of
// commit and must already find the forced humanizer skill.
const staged = stageBuiltinSkillsLibrary(deps.builtinSkillsSource, deps.builtinSkillsLibrary);
deps.connection.transaction(() => {
summary = onboarding.bootstrap(input);
const report = importStagedSkills(skills, summary.orgId, staged);
if (report.failures.length > 0) {
console.warn(`[skills] onboarding import failures (${report.failures.length}): ${report.failures.slice(0, 3).join("; ")}`);
}
});
notifyDataChanged();
return ok(summary);
@@ -264,13 +276,6 @@ export function registerIpcHandlers(deps: IpcDeps): void {
return fail("provider_cancel_failed", error instanceof Error ? error.message : String(error));
}
});
ipcMain.handle(IPC_CHANNELS.importSkillDirectory, (_event, input) => mutate(() => skills.importDirectory(input)));
ipcMain.handle(IPC_CHANNELS.syncSkillsFromDisk, (_event, input) => mutate(() => {
syncSkillsFromDisk(deps.connection.db, skills, input.orgId);
return null;
}));
ipcMain.handle(IPC_CHANNELS.resolveAgentSkills, (_event, input) => mutate(() => skills.resolveRuntimeSnapshot(input)));
ipcMain.handle(IPC_CHANNELS.runSkillTests, (_event, input) => mutate(() => skills.runTests(input)));
ipcMain.handle(IPC_CHANNELS.createSocialAccount, (_event, input) => mutate(() => social.createAccount(input)));
ipcMain.handle(IPC_CHANNELS.createScheduledPost, (_event, input) => mutate(() => social.createScheduledPost(input)));
ipcMain.handle(IPC_CHANNELS.setScheduledPostState, (_event, input) => mutate(() => social.setScheduledPostState(input)));
+34 -14
View File
@@ -5,7 +5,7 @@ import type { DatabaseSync } from "node:sqlite";
import { DatabaseConnection } from "./database/connection";
import { createMainWindow } from "./app/create-window";
import { registerIpcHandlers } from "./app/ipc-handlers";
import { syncSkillsFromDisk } from "./services/skill-discovery";
import { builtinSkillsSourceOverride, importStagedSkills, stageBuiltinSkillsLibrary } from "./services/builtin-skills";
import { SkillService } from "./services/skill-service";
import { startProviderBackgroundWorker } from "./app/provider-background-worker";
import { recoverInterruptedWork } from "./app/startup-recovery";
@@ -35,12 +35,16 @@ if (!app.requestSingleInstanceLock()) {
language,
});
const builtinSkillsSource = resolveBuiltinSkillsSource();
const builtinSkillsLibrary = join(profileDir, "builtin-skills");
registerIpcHandlers({
connection,
mainWindow: win,
artifactRoot: join(profileDir, "artifacts"),
builtinSkillsSource,
builtinSkillsLibrary,
});
syncAllOrganizationSkills(connection.db, join(profileDir, "artifacts"));
syncAllOrganizationSkills(connection, join(profileDir, "artifacts"), builtinSkillsSource, builtinSkillsLibrary);
const stopProviderBackgroundWorker = startProviderBackgroundWorker({
db: connection.db,
mainWindow: win,
@@ -73,19 +77,35 @@ function storedLanguage(db: DatabaseSync): string {
}
/**
* Loads the on-disk skill library for every organization at startup. One
* organization's failure (a broken workspace path, say) must not block the
* rest — or the app.
* The bundled skill catalogue: the repo folder during development, the
* extraResources copy in a packaged build, or an explicit override for
* hermetic tests (same seam pattern as AGENTCOMPANY_NEW_PROFILE_DIR).
*/
function syncAllOrganizationSkills(db: DatabaseSync, artifactRoot: string): void {
const skills = new SkillService(db, artifactRoot);
const orgs = db.prepare("select id from organizations").all() as Array<{ id: string }>;
for (const org of orgs) {
try {
syncSkillsFromDisk(db, skills, org.id);
} catch {
// Broken directories are reported per entry inside the sync; anything
// beyond that is a per-org environment fault that must not stop startup.
function resolveBuiltinSkillsSource(): string {
const override = builtinSkillsSourceOverride();
if (override) return override;
if (app.isPackaged) return join(process.resourcesPath, "built-in-skills");
return join(app.getAppPath(), "built-in skills");
}
/**
* Mirrors the bundled skill catalogue into the profile library and imports it
* for every organization at startup. Per-skill failures are reported inside
* the sync; a broken bundle is loud in the log but must not stop the app.
*/
function syncAllOrganizationSkills(connection: DatabaseConnection, artifactRoot: string, sourceRoot: string, libraryRoot: string): void {
try {
const staged = stageBuiltinSkillsLibrary(sourceRoot, libraryRoot);
const skills = new SkillService(connection.db, artifactRoot);
const orgs = connection.db.prepare("select id from organizations").all() as Array<{ id: string }>;
for (const org of orgs) {
const report = connection.transaction(() => importStagedSkills(skills, org.id, staged));
if (report.imported > 0 || report.failures.length > 0) {
console.info(`[skills] org ${org.id}: imported ${report.imported}, skipped ${report.skipped}, failures ${report.failures.length}`);
for (const failure of report.failures.slice(0, 5)) console.warn(`[skills] ${failure}`);
}
}
} catch (error) {
console.error(`[skills] built-in skill sync failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
+68 -7
View File
@@ -1,15 +1,22 @@
import { requireString } from "./agent-tool-args";
import type { AgentToolContext, AgentToolDeps } from "./agent-tools";
import type { JsonRecord } from "./provider-http";
import { SkillService } from "./skill-service";
import type { SkillManifest } from "./skill-manifest";
import { FORCED_SKILL_SLUGS, SkillService } from "./skill-service";
/** Company skill library visibility for any agent: what exists, who holds it. */
export function listSkillsTool(deps: AgentToolDeps, ctx: AgentToolContext): string {
const skills = deps.db.prepare(
"select id, name, slug from skill_definitions where org_id = ? and status = 'active' order by slug asc",
).all(ctx.orgId) as Array<{ id: string; name: string; slug: string }>;
const skills = deps.db.prepare(`
select sd.id, sd.name, sd.slug, sr.manifest_json
from skill_definitions sd
left join skill_revisions sr on sr.id = (
select id from skill_revisions where skill_id = sd.id order by published_at desc, id desc limit 1
)
where sd.org_id = ? and sd.status = 'active'
order by sd.slug asc
`).all(ctx.orgId) as Array<{ id: string; name: string; slug: string; manifest_json: string | null }>;
if (skills.length === 0) {
return "The company has no skills yet. The board imports them on the Settings page.";
return "The company skill library is empty. Built-in skills load at startup; if this persists, the installation is broken.";
}
const bindings = deps.db.prepare(
"select skill_id, agent_id from skill_bindings where org_id = ? and enabled = 1",
@@ -18,14 +25,22 @@ export function listSkillsTool(deps: AgentToolDeps, ctx: AgentToolContext): stri
(deps.db.prepare("select id, name from agents where org_id = ?").all(ctx.orgId) as Array<{ id: string; name: string }>)
.map((row) => [row.id, row.name]),
);
const forced = new Set<string>(FORCED_SKILL_SLUGS);
const lines = skills.map((skill) => {
const summary = skillSummary(skill.manifest_json);
if (forced.has(skill.slug)) {
return `- ${skill.name} (${skill.slug})${summary}: house style — every agent carries it automatically`;
}
const holders = bindings
.filter((binding) => binding.skill_id === skill.id)
.map((binding) => agentNames.get(binding.agent_id))
.filter(Boolean);
return `- ${skill.name} (${skill.slug}): ${holders.length > 0 ? `granted to ${holders.join(", ")}` : "no grants yet"}`;
return `- ${skill.name} (${skill.slug})${summary}: ${holders.length > 0 ? `granted to ${holders.join(", ")}` : "no grants yet"}`;
});
return ["Company skills (the CEO carries all of them; teammates need a grant):", ...lines].join("\n");
return [
"Company skill library (grant by slug with grant_skill, or pass skills to hire_agent):",
...lines,
].join("\n");
}
/** CEO-only: hand a skill to a teammate, or take it back. */
@@ -37,6 +52,11 @@ export function grantSkillTool(deps: AgentToolDeps, ctx: AgentToolContext, args:
}
const agentName = requireString(args.agent_name, "agent_name");
const skillSlug = requireString(args.skill, "skill").trim().toLowerCase();
if ((FORCED_SKILL_SLUGS as readonly string[]).includes(skillSlug)) {
return mode === "grant"
? `${agentName} already carries ${skillSlug} automatically — it is company house style for every agent.`
: `ERROR: ${skillSlug} is company house style and cannot be revoked from anyone.`;
}
const target = deps.db.prepare(
"select id, name from agents where org_id = ? and lower(name) = lower(?) and lifecycle = 'active' limit 1",
).get(ctx.orgId, agentName) as { id: string; name: string } | undefined;
@@ -57,3 +77,44 @@ export function grantSkillTool(deps: AgentToolDeps, ctx: AgentToolContext, args:
skillService.unbind({ orgId: ctx.orgId, agentId: target.id, skillId: skill.id });
return `${target.name} no longer has the ${skill.name} skill.`;
}
/**
* Binds a set of library skills to a freshly hired agent. Unknown slugs are
* reported back to the caller (the hiring CEO) instead of failing the hire.
*/
export function bindSkillSlugs(deps: AgentToolDeps, orgId: string, agentId: string, slugs: string[]): { granted: string[]; unknown: string[] } {
const skillService = new SkillService(deps.db, "");
const granted: string[] = [];
const unknown: string[] = [];
for (const raw of slugs) {
const slug = raw.trim().toLowerCase();
if (!slug || (FORCED_SKILL_SLUGS as readonly string[]).includes(slug)) continue;
const row = deps.db.prepare(`
select sd.id as skill_id, sd.name, sr.id as revision_id
from skill_definitions sd
join skill_revisions sr on sr.id = (
select id from skill_revisions where skill_id = sd.id order by published_at desc, id desc limit 1
)
where sd.org_id = ? and sd.slug = ? and sd.status = 'active'
`).get(orgId, slug) as { skill_id: string; name: string; revision_id: string } | undefined;
if (!row) {
unknown.push(slug);
continue;
}
skillService.bind({ orgId, agentId, skillId: row.skill_id, revisionId: row.revision_id });
granted.push(slug);
}
return { granted, unknown };
}
function skillSummary(manifestJson: string | null): string {
if (!manifestJson) return "";
try {
const manifest = JSON.parse(manifestJson) as SkillManifest;
if (!manifest.description) return "";
const compact = manifest.description.replace(/\s+/g, " ").trim();
return compact ? ` — ${compact.length > 140 ? `${compact.slice(0, 137)}...` : compact}` : "";
} catch {
return "";
}
}
+12 -3
View File
@@ -6,7 +6,7 @@ import type { ChatService } from "./chat-service";
import { canonicalMentionHandle } from "../../shared/mention-handles";
import { readMessagesTool } from "./agent-read-messages";
import { grantConnectorTool, listConnectorsTool } from "./agent-connector-tools";
import { grantSkillTool, listSkillsTool } from "./agent-skill-tools";
import { bindSkillSlugs, grantSkillTool, listSkillsTool } from "./agent-skill-tools";
import { createDocumentTool, writeDeliverableTool } from "./agent-deliverable-tools";
import { describeSend } from "./agent-message-presence";
import { optionalString, optionalStringArray, requireString } from "./agent-tool-args";
@@ -40,7 +40,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [
{
name: "hire_agent",
description:
"Add one direct report to your team. During team building, call this 3-5 times to staff the roles you need before delegating work.",
"Add one direct report to your team. During team building, call this 3-5 times to staff the roles you need before delegating work. Staff every hire with the library skills their role needs (check list_skills; several are fine) — those skills become mandatory practice in the hire's runs.",
parameters: {
type: "object",
additionalProperties: false,
@@ -49,6 +49,11 @@ export const AGENT_TOOLS: ToolDefinition[] = [
role: { type: "string", description: "Their role/title, e.g. 'Growth Marketer'." },
department: { type: "string", enum: [...ORGANIZATION_DEPARTMENTS], description: "The company department the hire belongs to." },
model: { type: "string", description: "Optional model id; defaults to your own model." },
skills: {
type: "array",
items: { type: "string" },
description: "Skill slugs from list_skills this hire must work with, e.g. ['engineering-backend-architect']. Every agent also carries the humanizer house-style skill automatically.",
},
},
required: ["name", "role", "department"],
},
@@ -254,6 +259,7 @@ function hireAgent(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord)
const role = requireString(args.role, "role");
const department = requireString(args.department, "department");
const model = optionalString(args.model) ?? ctx.agent.modelId;
const requestedSkills = optionalStringArray(args.skills, "skills");
const agentId = deps.organization.createAgent({
orgId: ctx.orgId,
name,
@@ -268,7 +274,10 @@ function hireAgent(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord)
heartbeatEnabled: true,
heartbeatIntervalSec: HEARTBEAT_INTERVAL_SEC,
});
return `Hired ${name} as ${role} in ${department} (agent_id=${agentId}).`;
const { granted, unknown } = bindSkillSlugs(deps, ctx.orgId, agentId, requestedSkills);
const skillNote = granted.length > 0 ? ` They carry: ${granted.join(", ")} (plus the automatic humanizer house style).` : "";
const unknownNote = unknown.length > 0 ? ` Unknown skills skipped: ${unknown.join(", ")} — run list_skills and grant_skill to fix.` : "";
return `Hired ${name} as ${role} in ${department} (agent_id=${agentId}).${skillNote}${unknownNote}`;
}
function createTask(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord, options: AgentToolDispatchOptions): string {
+185
View File
@@ -0,0 +1,185 @@
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { RISK_LEVEL } from "../../shared/domain";
import { loadSkillManifest, type SkillManifest } from "./skill-manifest";
import type { SkillService } from "./skill-service";
/**
* The skill catalogue ships with the app: `built-in skills/` in the repo, and
* `resources/built-in-skills` in a packaged build. The user never manages it.
*
* The bundle holds two shapes:
* - a directory per skill with a SKILL.md (and optional skill.manifest.json),
* e.g. `humanizer/`;
* - category directories of flat one-file skills, e.g.
* `engineering/engineering-backend-architect.md`.
*
* Staging normalizes both into one library layout the import machinery
* understands — `<libraryRoot>/<slug>/SKILL.md` — a real copy on disk, never a
* reference back into the bundle. Import then publishes every staged skill the
* org does not carry yet into the immutable artifact store.
*/
export interface StagedBuiltinSkill {
slug: string;
directory: string;
manifest: SkillManifest;
}
export interface BuiltinSkillSyncReport {
imported: number;
skipped: number;
failures: string[];
}
/** Test seam, same pattern as AGENTCOMPANY_NEW_PROFILE_DIR. */
export function builtinSkillsSourceOverride(): string | undefined {
return process.env.AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR;
}
/**
* Mirrors the bundled catalogue into the normalized on-disk library and
* returns every staged skill. Idempotent: re-staging overwrites in place so
* an app update refreshes the library on next launch.
*/
export function stageBuiltinSkillsLibrary(sourceRoot: string, libraryRoot: string): StagedBuiltinSkill[] {
if (!existsSync(sourceRoot)) {
throw new Error(`Built-in skills bundle was not found: ${sourceRoot}`);
}
const staged: StagedBuiltinSkill[] = [];
const seen = new Set<string>();
for (const entry of readdirSync(sourceRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
if (!entry.isDirectory()) continue;
const sourceDir = join(sourceRoot, entry.name);
if (existsSync(join(sourceDir, "SKILL.md")) || existsSync(join(sourceDir, "skill.manifest.json"))) {
staged.push(stageDirectorySkill(sourceDir, entry.name, libraryRoot, seen));
continue;
}
staged.push(...stageCategorySkills(sourceDir, libraryRoot, seen));
}
return staged;
}
/** Publishes every staged skill the org does not already carry. */
export function importStagedSkills(skills: SkillService, orgId: string, staged: StagedBuiltinSkill[]): BuiltinSkillSyncReport {
const report: BuiltinSkillSyncReport = { imported: 0, skipped: 0, failures: [] };
for (const unit of staged) {
try {
if (skills.hasSkill(orgId, unit.slug)) {
report.skipped += 1;
continue;
}
skills.importManifest(orgId, unit.directory, unit.manifest, unit.slug);
report.imported += 1;
} catch (error) {
report.failures.push(`${unit.slug}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return report;
}
function stageDirectorySkill(sourceDir: string, dirName: string, libraryRoot: string, seen: Set<string>): StagedBuiltinSkill {
const slug = uniqueSlug(slugify(dirName), seen, sourceDir);
const target = join(libraryRoot, slug);
mkdirSync(target, { recursive: true });
cpSync(sourceDir, target, { recursive: true });
const manifest = existsSync(join(target, "skill.manifest.json"))
? loadSkillManifest(target)
: manifestFromSkillMarkdown(readFileSync(join(target, "SKILL.md"), "utf8"), dirName);
return { slug, directory: target, manifest };
}
function stageCategorySkills(categoryDir: string, libraryRoot: string, seen: Set<string>): StagedBuiltinSkill[] {
const staged: StagedBuiltinSkill[] = [];
for (const entry of readdirSync(categoryDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
if (entry.name.toLowerCase() === "readme.md") continue;
const body = readFileSync(join(categoryDir, entry.name), "utf8");
const frontmatter = parseFrontmatter(body);
// A category file without a name: header is documentation, not a skill.
if (!frontmatter.name) continue;
const stem = entry.name.replace(/\.md$/i, "");
const slug = uniqueSlug(slugify(stem), seen, join(categoryDir, entry.name));
const target = join(libraryRoot, slug);
mkdirSync(target, { recursive: true });
writeFileSync(join(target, "SKILL.md"), body, "utf8");
staged.push({
slug,
directory: target,
manifest: derivedManifest(frontmatter.name, frontmatter.version, frontmatter.description),
});
}
return staged;
}
function manifestFromSkillMarkdown(markdown: string, fallbackName: string): SkillManifest {
const frontmatter = parseFrontmatter(markdown);
return derivedManifest(frontmatter.name ?? fallbackName, frontmatter.version, frontmatter.description);
}
function derivedManifest(name: string, version: string | undefined, description: string | undefined): SkillManifest {
return {
name,
version: version && /^\d+\.\d+\.\d+$/.test(version) ? version : "1.0.0",
entrypoint: "SKILL.md",
capabilities: [],
requiredTools: [],
compatibleModels: [],
riskLevel: RISK_LEVEL.LOW,
tests: [],
inputs: {},
outputs: {},
...(description ? { description } : {}),
};
}
interface SkillFrontmatter {
name?: string;
version?: string;
description?: string;
}
/**
* Minimal YAML-frontmatter reader for the three fields skills declare. Handles
* inline values (`description: text`) and block scalars (`description: |`)
* because the bundled skills use both.
*/
export function parseFrontmatter(markdown: string): SkillFrontmatter {
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
if (!match) return {};
const lines = match[1].split(/\r?\n/);
const result: SkillFrontmatter = {};
for (let index = 0; index < lines.length; index += 1) {
const field = /^(name|version|description)\s*:\s*(.*)$/.exec(lines[index]);
if (!field) continue;
const key = field[1] as keyof SkillFrontmatter;
const inline = field[2].trim();
if (inline && !/^[|>][+-]?$/.test(inline)) {
result[key] = stripQuotes(inline);
continue;
}
const block: string[] = [];
while (index + 1 < lines.length && (/^\s+\S/.test(lines[index + 1]) || lines[index + 1].trim() === "")) {
block.push(lines[index + 1].trim());
index += 1;
}
const value = block.join(" ").replace(/\s+/g, " ").trim();
if (value) result[key] = value;
}
return result;
}
function stripQuotes(value: string): string {
return value.replace(/^["']|["']$/g, "");
}
function slugify(value: string): string {
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
function uniqueSlug(slug: string, seen: Set<string>, source: string): string {
if (!slug) throw new Error(`Built-in skill has no usable slug: ${source}`);
if (seen.has(slug)) throw new Error(`Built-in skill slug collision: ${slug} (${source})`);
seen.add(slug);
return slug;
}
-104
View File
@@ -1,104 +0,0 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { RISK_LEVEL } from "../../shared/domain";
import { loadSkillManifest, type SkillManifest } from "./skill-manifest";
import type { SkillService } from "./skill-service";
/**
* Skills live on disk the way developers keep them: one directory per skill
* under the user's global `~/.agents/skills` and under each workspace's
* `.agents/skills`. A directory qualifies with either a full
* skill.manifest.json or just a SKILL.md; for the latter the manifest is
* derived (name from frontmatter or the directory name, SKILL.md as the
* entrypoint). Paths are joined per platform — Windows, macOS, and Linux
* all resolve the same way.
*/
export interface SkillSyncReport {
imported: number;
skipped: number;
failures: string[];
}
export function defaultGlobalSkillsRoot(): string {
// Same isolation seam as AGENTCOMPANY_NEW_PROFILE_DIR: tests point this at
// a temporary directory so they never read the developer's real library.
return process.env.AGENTCOMPANY_NEW_SKILLS_ROOT ?? join(homedir(), ".agents", "skills");
}
export function discoverSkillDirectories(workspaceRoots: string[], globalRoot: string = defaultGlobalSkillsRoot()): string[] {
const roots = [globalRoot, ...workspaceRoots.map((root) => join(root, ".agents", "skills"))];
const found: string[] = [];
for (const root of roots) {
if (!existsSync(root)) continue;
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const dir = join(root, entry.name);
if (existsSync(join(dir, "skill.manifest.json")) || existsSync(join(dir, "SKILL.md"))) {
found.push(dir);
}
}
}
return found;
}
/** Full manifest when present; otherwise derived from SKILL.md. */
export function manifestForDirectory(directoryPath: string): SkillManifest {
if (existsSync(join(directoryPath, "skill.manifest.json"))) {
return loadSkillManifest(directoryPath);
}
const skillMd = readFileSync(join(directoryPath, "SKILL.md"), "utf8");
return {
name: frontmatterName(skillMd) ?? basename(directoryPath),
version: "1.0.0",
entrypoint: "SKILL.md",
capabilities: [],
requiredTools: [],
compatibleModels: [],
riskLevel: RISK_LEVEL.LOW,
tests: [],
inputs: {},
outputs: {},
};
}
/**
* Imports every discovered skill the org does not already carry. Individual
* bad directories are reported and skipped so one broken entry cannot block
* the rest of the library.
*/
export function syncSkillsFromDisk(
db: DatabaseSync,
skills: SkillService,
orgId: string,
globalRoot: string = defaultGlobalSkillsRoot(),
): SkillSyncReport {
const workspaceRoots = (db.prepare("select local_path from workspaces where org_id = ?").all(orgId) as Array<{ local_path: string }>)
.map((row) => row.local_path);
const report: SkillSyncReport = { imported: 0, skipped: 0, failures: [] };
for (const directory of discoverSkillDirectories(workspaceRoots, globalRoot)) {
try {
const manifest = manifestForDirectory(directory);
if (skills.hasSkill(orgId, manifest.name)) {
report.skipped += 1;
continue;
}
skills.importManifest(orgId, directory, manifest);
report.imported += 1;
} catch (error) {
report.failures.push(`${directory}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return report;
}
function frontmatterName(markdown: string): string | null {
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
if (!match) return null;
const nameLine = match[1].split(/\r?\n/).find((line) => /^name\s*:/.test(line.trim()));
if (!nameLine) return null;
const value = nameLine.split(":").slice(1).join(":").trim().replace(/^["']|["']$/g, "");
return value || null;
}
+5
View File
@@ -14,6 +14,8 @@ export interface SkillManifest {
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 {
@@ -42,6 +44,9 @@ function normalizeManifest(raw: Record<string, unknown>): SkillManifest {
inputs: raw.inputs ?? {},
outputs: raw.outputs ?? {},
};
if (typeof raw.description === "string" && raw.description.trim()) {
manifest.description = raw.description.trim();
}
return manifest;
}
+93 -54
View File
@@ -14,6 +14,20 @@ function slugify(value: string): string {
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
/**
* House-style skills every agent carries on every run, CEO included. They are
* a resolution rule, not a binding row, so no creation path — hire tool,
* onboarding, tests, raw SQL — can produce an agent without them.
*/
export const FORCED_SKILL_SLUGS = ["humanizer"] as const;
/**
* Compiled skill prompts stop packing past this size so one over-granted
* agent cannot overflow the model's context window. Skills pack whole, forced
* skills first, and anything omitted is named in the prompt.
*/
export const MAX_COMPILED_PROMPT_CHARS = 200_000;
export class SkillService {
private readonly audit: AuditService;
private readonly operations: OperationsService;
@@ -28,14 +42,14 @@ export class SkillService {
return this.importManifest(input.orgId, input.directoryPath, manifest);
}
/** True when the org already carries a skill with this name. */
hasSkill(orgId: string, name: string): boolean {
const slug = slugify(name);
/** True when the org already carries a skill under this slug (or name). */
hasSkill(orgId: string, slugOrName: string): boolean {
const slug = slugify(slugOrName);
return Boolean(this.db.prepare("select id from skill_definitions where org_id = ? and slug = ?").get(orgId, slug));
}
importManifest(orgId: string, directoryPath: string, manifest: SkillManifest): string {
const slug = slugify(manifest.name);
importManifest(orgId: string, directoryPath: string, manifest: SkillManifest, explicitSlug?: string): string {
const slug = explicitSlug ?? slugify(manifest.name);
if (!slug) throw new Error("Skill name could not be derived.");
const now = new Date().toISOString();
const skillId = createId("skl");
@@ -139,77 +153,97 @@ export class SkillService {
}
/**
* True when a run for this agent would compile at least one skill: the CEO
* carries every active skill by role; other agents need enabled bindings.
* True when a run for this agent would compile at least one skill: every
* agent carries the forced house-style skills once the catalogue holds
* them, plus whatever bindings the CEO granted.
*/
hasRuntimeSkills(orgId: string, agentId: string): boolean {
if (this.isCSuite(orgId, agentId)) {
return Boolean(this.db.prepare(`
select sd.id from skill_definitions sd
join skill_revisions sr on sr.skill_id = sd.id
where sd.org_id = ? and sd.status = 'active'
limit 1
`).get(orgId));
}
const forced = this.db.prepare(`
select sd.id from skill_definitions sd
join skill_revisions sr on sr.skill_id = sd.id
where sd.org_id = ? and sd.status = 'active' and sd.slug in (${forcedSlugPlaceholders()})
limit 1
`).get(orgId, ...FORCED_SKILL_SLUGS);
if (forced) return true;
return Boolean(this.db.prepare(
"select id from skill_bindings where org_id = ? and agent_id = ? and enabled = 1 limit 1",
).get(orgId, agentId));
}
private isCSuite(orgId: string, agentId: string): boolean {
const agent = this.db.prepare("select role_level from agents where id = ? and org_id = ?")
.get(agentId, orgId) as { role_level: string } | undefined;
if (!agent) throw new Error("Agent was not found.");
return agent.role_level === "c_suite";
}
/**
* Freezes the exact skill set this run carries: forced house-style skills
* first (identical for everyone, CEO included), then the agent's enabled
* bindings in priority order, deduplicated per skill. Skills pack whole
* until MAX_COMPILED_PROMPT_CHARS; whatever does not fit is named in the
* compiled prompt instead of silently overflowing the model's context.
*/
resolveRuntimeSnapshot(input: ResolveAgentSkillsInput): string {
const cSuite = this.isCSuite(input.orgId, input.agentId);
const existingSnapshot = this.db.prepare("select id from skill_runtime_snapshots where agent_id = ? and run_id = ?")
.get(input.agentId, input.runId) as { id: string } | undefined;
if (existingSnapshot) throw new Error("Skill runtime snapshot already exists for this run.");
const rows = (cSuite
? this.db.prepare(`
select
sr.id as revision_id,
sr.version_semver,
sr.manifest_json,
sa.object_key
from skill_definitions sd
join skill_revisions sr on sr.id = (
select id from skill_revisions where skill_id = sd.id order by published_at desc, id desc limit 1
)
join skill_artifacts sa on sa.revision_id = sr.id and sa.artifact_type in ('entrypoint', 'markdown')
where sd.org_id = ? and sd.status = 'active'
order by sr.published_at asc
`).all(input.orgId)
: this.db.prepare(`
select
sr.id as revision_id,
sr.version_semver,
sr.manifest_json,
sa.object_key
from skill_bindings sb
join skill_revisions sr on sr.id = sb.pinned_revision_id
join skill_artifacts sa on sa.revision_id = sr.id and sa.artifact_type in ('entrypoint', 'markdown')
where sb.org_id = ? and sb.agent_id = ? and sb.enabled = 1
order by sb.priority asc, sr.published_at asc
`).all(input.orgId, input.agentId)) as unknown as RuntimeSkillRow[];
const forcedRows = this.db.prepare(`
select
sd.id as skill_id,
sr.id as revision_id,
sr.version_semver,
sr.manifest_json,
sa.object_key
from skill_definitions sd
join skill_revisions sr on sr.id = (
select id from skill_revisions where skill_id = sd.id order by published_at desc, id desc limit 1
)
join skill_artifacts sa on sa.revision_id = sr.id and sa.artifact_type in ('entrypoint', 'markdown')
where sd.org_id = ? and sd.status = 'active' and sd.slug in (${forcedSlugPlaceholders()})
order by sd.slug asc
`).all(input.orgId, ...FORCED_SKILL_SLUGS) as unknown as RuntimeSkillRow[];
const boundRows = this.db.prepare(`
select
sb.skill_id as skill_id,
sr.id as revision_id,
sr.version_semver,
sr.manifest_json,
sa.object_key
from skill_bindings sb
join skill_revisions sr on sr.id = sb.pinned_revision_id
join skill_artifacts sa on sa.revision_id = sr.id and sa.artifact_type in ('entrypoint', 'markdown')
where sb.org_id = ? and sb.agent_id = ? and sb.enabled = 1
order by sb.priority asc, sr.published_at asc
`).all(input.orgId, input.agentId) as unknown as RuntimeSkillRow[];
const rows: RuntimeSkillRow[] = [];
const seenSkillIds = new Set<string>();
for (const row of [...forcedRows, ...boundRows]) {
if (seenSkillIds.has(row.skill_id)) continue;
seenSkillIds.add(row.skill_id);
rows.push(row);
}
if (rows.length === 0) throw new Error("Agent has no enabled skills.");
const compiledParts: string[] = [];
const tools = new Set<string>();
const revisionIds: string[] = [];
const omitted: string[] = [];
let compiledLength = 0;
for (const row of rows) {
if (!existsSync(row.object_key)) throw new Error(`Skill artifact is missing: ${row.object_key}`);
const manifest = JSON.parse(row.manifest_json) as SkillManifest;
for (const tool of manifest.requiredTools) tools.add(tool);
revisionIds.push(row.revision_id);
compiledParts.push([
const part = [
`# Skill: ${manifest.name}`,
`Version: ${row.version_semver}`,
readFileSync(row.object_key, "utf8").trim(),
].join("\n\n"));
].join("\n\n");
if (compiledParts.length > 0 && compiledLength + part.length > MAX_COMPILED_PROMPT_CHARS) {
omitted.push(manifest.name);
continue;
}
for (const tool of manifest.requiredTools) tools.add(tool);
revisionIds.push(row.revision_id);
compiledParts.push(part);
compiledLength += part.length;
}
if (omitted.length > 0) {
compiledParts.push(
`# Skills omitted for size\n\nThese granted skills did not fit this run's prompt budget: ${omitted.join(", ")}. Ask the CEO to trim grants if they are essential.`,
);
}
const snapshotId = createId("srs");
@@ -307,6 +341,10 @@ export class SkillService {
}
}
function forcedSlugPlaceholders(): string {
return FORCED_SKILL_SLUGS.map(() => "?").join(", ");
}
function artifactType(relativePath: string, manifest: SkillManifest): string {
if (relativePath === "skill.manifest.json") return "manifest";
if (relativePath === manifest.entrypoint) return "entrypoint";
@@ -315,6 +353,7 @@ function artifactType(relativePath: string, manifest: SkillManifest): string {
}
interface RuntimeSkillRow {
skill_id: string;
revision_id: string;
version_semver: string;
manifest_json: string;
+2 -2
View File
@@ -120,7 +120,7 @@ function capabilityBriefing(input: AgentInstructionContext): string {
);
if (input.isCeo) {
lines.push(
"You are the CEO: you automatically carry every connected service and every company skill. Access for everyone else is yours to manage — when you delegate work that needs an external service or a skill, grant it to that teammate first with grant_connector or grant_skill (match the tool to the job), and take it back with revoke_connector or revoke_skill when the work is done. A teammate without the grant cannot use the tool, so grant before you assign.",
"You are the CEO: you automatically carry every connected service, and access for everyone else is yours to manage — when you delegate work that needs an external service, grant it first with grant_connector and take it back with revoke_connector when done. The company also ships a built-in skill library: staff every hire with the skills their role needs by passing skills to hire_agent (several per person is normal), and adjust later with grant_skill / revoke_skill. A teammate without the grant cannot use the tool or skill, so grant before you assign. The humanizer skill is company house style — every agent, you included, carries it automatically and must write like a person, not like an AI.",
);
} else if (input.connectorNames.length === 0) {
lines.push(
@@ -194,7 +194,7 @@ export function buildTaskPrompt(input: {
if (input.skillRuntimeSnapshot) {
lines.push(
"",
"Skill runtime:",
"Skills you carry this run — these are mandatory working practice, not suggestions. Apply each one to how you work and what you produce:",
input.skillRuntimeSnapshot.compiled_prompt,
"",
`Tool allowlist: ${input.skillRuntimeSnapshot.tools_manifest}`,
-4
View File
@@ -56,10 +56,6 @@ const api: DesktopApi = {
createProviderResponse: (input) => invoke(IPC_CHANNELS.createProviderResponse, input),
streamProviderResponse: (input) => invoke(IPC_CHANNELS.streamProviderResponse, input),
cancelProviderResponse: (input) => invoke(IPC_CHANNELS.cancelProviderResponse, input),
importSkillDirectory: (input) => invoke(IPC_CHANNELS.importSkillDirectory, input),
syncSkillsFromDisk: (input) => invoke(IPC_CHANNELS.syncSkillsFromDisk, input),
resolveAgentSkills: (input) => invoke(IPC_CHANNELS.resolveAgentSkills, input),
runSkillTests: (input) => invoke(IPC_CHANNELS.runSkillTests, input),
createSocialAccount: (input) => invoke(IPC_CHANNELS.createSocialAccount, input),
launchSocialLogin: (input) => invoke(IPC_CHANNELS.launchSocialLogin, input),
createScheduledPost: (input) => invoke(IPC_CHANNELS.createScheduledPost, input),
+1 -24
View File
@@ -1,13 +1,12 @@
import { useMemo, useState } from "react";
import { ProvidersPanel } from "./settings/ProvidersPanel";
import { SkillsPanel } from "./settings/SkillsPanel";
import { WorkspacePanel } from "./settings/WorkspacePanel";
import { AdvancedPanel } from "./settings/AdvancedPanel";
import { t } from "../i18n/i18n";
import type { PageProps } from "./types";
import { activeOrgId, requireActiveEmployee, requireActiveOrganization } from "./types";
type SettingsTab = "general" | "providers" | "skills" | "advanced";
type SettingsTab = "general" | "providers" | "advanced";
interface NavSpec {
id: SettingsTab;
@@ -17,7 +16,6 @@ interface NavSpec {
const TABS: NavSpec[] = [
{ id: "general", label: t("General") },
{ id: "providers", label: t("Providers") },
{ id: "skills", label: t("Skills") },
{ id: "advanced", label: t("Advanced") },
];
@@ -31,16 +29,6 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
const providers = useMemo(() => snapshot.providers.filter((item) => item.orgId === orgId), [snapshot.providers, orgId]);
const providerAuthProfiles = useMemo(() => snapshot.providerAuthProfiles.filter((item) => item.orgId === orgId), [snapshot.providerAuthProfiles, orgId]);
const providerRateLimits = useMemo(() => snapshot.providerRateLimits.filter((item) => item.orgId === orgId), [snapshot.providerRateLimits, orgId]);
const skills = useMemo(() => snapshot.skillDefinitions.filter((item) => item.orgId === orgId), [snapshot.skillDefinitions, orgId]);
const skillRevisions = useMemo(() => {
const skillIds = new Set(skills.map((skill) => skill.id));
return snapshot.skillRevisions.filter((revision) => skillIds.has(revision.skillId));
}, [snapshot.skillRevisions, skills]);
const skillTests = useMemo(() => {
const revisionIds = new Set(skillRevisions.map((revision) => revision.id));
return snapshot.skillTests.filter((test) => revisionIds.has(test.revisionId));
}, [snapshot.skillTests, skillRevisions]);
const skillBindings = useMemo(() => snapshot.skillBindings.filter((item) => item.orgId === orgId), [snapshot.skillBindings, orgId]);
const workspaces = useMemo(() => snapshot.workspaces.filter((item) => item.orgId === orgId), [snapshot.workspaces, orgId]);
const providerRequests = useMemo(() => snapshot.providerRequests.filter((item) => item.orgId === orgId), [snapshot.providerRequests, orgId]);
const hiringPolicies = useMemo(() => snapshot.hiringPolicies.filter((item) => item.orgId === orgId), [snapshot.hiringPolicies, orgId]);
@@ -68,17 +56,6 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
actions={actions}
/>
)}
{tab === "skills" && (
<SkillsPanel
orgId={orgId}
skills={skills}
revisions={skillRevisions}
bindings={skillBindings}
tests={skillTests}
agents={agents}
actions={actions}
/>
)}
{tab === "advanced" && (
<AdvancedPanel
orgId={orgId}
@@ -1,123 +0,0 @@
import { useEffect, useState, type FormEvent } from "react";
import { Button, Field, Icon, Pill } from "../../shared/ui";
import { Dialog } from "../../shared/Dialog";
import { t } from "../../i18n/i18n";
import type {
AgentRecord,
SkillBindingRecord,
SkillDefinitionRecord,
SkillRevisionRecord,
SkillTestRecord,
} from "../../../../shared/domain";
import type { SnapshotActions } from "../../app/useSnapshot";
interface SkillsPanelProps {
orgId: string;
skills: SkillDefinitionRecord[];
revisions: SkillRevisionRecord[];
bindings: SkillBindingRecord[];
tests: SkillTestRecord[];
agents: AgentRecord[];
actions: SnapshotActions;
}
export function SkillsPanel({ orgId, skills, revisions, bindings, tests, agents, actions }: SkillsPanelProps) {
const [importOpen, setImportOpen] = useState(false);
// The on-disk skill library (global ~/.agents/skills and each workspace's
// .agents/skills) syncs in when the panel opens.
useEffect(() => {
void actions.run(() => window.agentCompany.syncSkillsFromDisk({ orgId }));
}, [orgId, actions]);
const importSkill = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.importSkillDirectory({
orgId,
directoryPath: String(form.get("directoryPath") ?? "").trim(),
versionSemver: String(form.get("versionSemver") ?? "").trim(),
})).then((result) => { if (result) setImportOpen(false); });
};
const runTests = (revisionId: string) => {
void actions.run(() => window.agentCompany.runSkillTests({ orgId, revisionId }));
};
return (
<div className="page reveal-row">
<div className="row" style={{ marginBottom: 24 }}>
<h1 className="page-h1" style={{ margin: 0 }}>{t("Skills")}</h1>
<div className="reveal" style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={() => setImportOpen(true)}><Icon name="plus" size={16} /> {t("Import")}</Button>
</div>
</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div
className="table-row head"
style={{ gridTemplateColumns: "minmax(0, 1.4fr) 92px 132px minmax(0, 1.2fr) 132px" }}
>
<div>{t("Skill")}</div><div>{t("Version")}</div><div>{t("Tests")}</div><div>{t("Held by")}</div><div>{t("Actions")}</div>
</div>
{skills.map((skill, index) => {
const last = index === skills.length - 1;
const latest = revisions.find((revision) => revision.skillId === skill.id);
const skillBindings = bindings.filter((binding) => binding.skillId === skill.id && binding.enabled);
const revisionTests = latest ? tests.filter((test) => test.revisionId === latest.id) : [];
return (
<div
key={skill.id}
className="table-row"
style={{ gridTemplateColumns: "minmax(0, 1.4fr) 92px 132px minmax(0, 1.2fr) 132px", borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{skill.name}</div>
<div style={{ color: "var(--text-3)", fontSize: 12 }}>{skill.slug}</div>
</div>
<Pill>{latest ? latest.versionSemver : "—"}</Pill>
<Pill>{testSummary(revisionTests)}</Pill>
<div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
{skillBindings.map((binding) => (
<Pill key={binding.id}>{agentName(binding.agentId, agents)}</Pill>
))}
</div>
<div className="row" style={{ minWidth: 0 }}>
<Button size="small" onClick={() => { if (latest && revisionTests.length > 0) runTests(latest.id); }} disabled={!latest || revisionTests.length === 0}>{t("Run tests")}</Button>
</div>
</div>
);
})}
</div>
<Dialog title={t("Import skill")} open={importOpen} submitLabel={t("Import")} onClose={() => setImportOpen(false)} onSubmit={importSkill}>
<Field label={t("Directory path")}><input className="input" name="directoryPath" required /></Field>
<Field label={t("Version (semver)")}><input className="input" name="versionSemver" required defaultValue="1.0.0" /></Field>
</Dialog>
</div>
);
}
function agentName(agentId: string, agents: AgentRecord[]): string {
return agents.find((agent) => agent.id === agentId)?.name ?? agentId;
}
function testSummary(tests: SkillTestRecord[]): string {
if (tests.length === 0) return t("No tests");
const completed = tests.filter((test) => test.lastResult);
if (completed.length === 0) return t("Never run");
const passed = completed.filter((test) => testStatus(test) === "passed").length;
const failed = completed.length - passed;
if (failed > 0) return t("{n} failed", { n: failed });
return t("{passed}/{total} passed", { passed, total: tests.length });
}
function testStatus(test: SkillTestRecord): "passed" | "failed" {
if (!test.lastResult) return "failed";
try {
const result = JSON.parse(test.lastResult) as { status?: unknown };
return result.status === "passed" ? "passed" : "failed";
} catch {
return "failed";
}
}
-8
View File
@@ -49,18 +49,14 @@ import type {
EditMessageInput,
ImportFileInput,
OpenFileInput,
ImportSkillDirectoryInput,
GrantRoleInput,
LaunchProviderSignInInput,
LaunchSocialLoginInput,
ResolveAgentSkillsInput,
ResolveMigrationReconciliationInput,
RunChaosDrillInput,
RunEvalSuiteInput,
RunSdlcReviewInput,
RunSkillTestsInput,
RunTaskInput,
SyncSkillsFromDiskInput,
RevokeProviderCredentialInput,
SaveProviderCredentialInput,
ConnectComposioToolkitInput,
@@ -152,10 +148,6 @@ export interface DesktopApi {
createProviderResponse(input: CreateProviderResponseInput): Promise<DesktopResult<AppSnapshot>>;
streamProviderResponse(input: StreamProviderResponseInput): Promise<DesktopResult<AppSnapshot>>;
cancelProviderResponse(input: CancelProviderResponseInput): Promise<DesktopResult<AppSnapshot>>;
importSkillDirectory(input: ImportSkillDirectoryInput): Promise<DesktopResult<AppSnapshot>>;
syncSkillsFromDisk(input: SyncSkillsFromDiskInput): Promise<DesktopResult<AppSnapshot>>;
resolveAgentSkills(input: ResolveAgentSkillsInput): Promise<DesktopResult<AppSnapshot>>;
runSkillTests(input: RunSkillTestsInput): Promise<DesktopResult<AppSnapshot>>;
createSocialAccount(input: CreateSocialAccountInput): Promise<DesktopResult<AppSnapshot>>;
launchSocialLogin(input: LaunchSocialLoginInput): Promise<DesktopResult<null>>;
createScheduledPost(input: CreateScheduledPostInput): Promise<DesktopResult<AppSnapshot>>;
-4
View File
@@ -33,10 +33,6 @@ export const IPC_CHANNELS = {
createProviderResponse: "agentcompany:create-provider-response",
streamProviderResponse: "agentcompany:stream-provider-response",
cancelProviderResponse: "agentcompany:cancel-provider-response",
importSkillDirectory: "agentcompany:import-skill-directory",
syncSkillsFromDisk: "agentcompany:sync-skills-from-disk",
resolveAgentSkills: "agentcompany:resolve-agent-skills",
runSkillTests: "agentcompany:run-skill-tests",
createSocialAccount: "agentcompany:create-social-account",
launchSocialLogin: "agentcompany:launch-social-login",
createScheduledPost: "agentcompany:create-scheduled-post",