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