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
@@ -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 "";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user