update owliver agent

This commit is contained in:
2026-08-24 20:06:11 +05:30
parent 02eb48af99
commit 6d8b4dbf36
28 changed files with 3176 additions and 692 deletions

View File

@@ -7,14 +7,18 @@ import {
import { cn } from '@/lib/utils';
import { agentIconFor } from '@/components/agents/icons';
import OwliverAvatar from '@/components/krow/OwliverAvatar';
import { usePreferences } from '@/lib/krowHooks';
import { usePreferences, useUpdatePreferences } from '@/lib/krowHooks';
import { reportSave } from '@/lib/skills/saveFeedback';
import { useAgents } from '@/lib/agents/useAgents';
import { agentTemplate } from '@/lib/agents/customAgents';
import { agentFieldsFromSource, applyAgentFields } from '@/lib/agents/agentFields';
import { validateAgentSource } from '@/lib/agents/registry';
import { AgentConfigure } from '@/components/agents/AgentConfigure';
import { AgentSkillWorkspace } from '@/components/agents/skills/AgentSkillWorkspace';
import { AgentTestPanel } from '@/components/agents/AgentTestPanel';
import { AgentInsightsPanel } from '@/components/agents/AgentInsightsPanel';
import { useAssistantPanel } from '@/components/ai-assistant';
import { boardCatalog, skillCatalog } from '@/lib/skills/catalog';
/**
* One agent, configured.
@@ -31,6 +35,44 @@ import { AgentInsightsPanel } from '@/components/agents/AgentInsightsPanel';
const STATUS_TONE = { published: 'success', draft: 'neutral', archived: 'warning' };
/**
* What Owliver says when a skill joins or leaves an agent.
*
* Composed from the definition itself — its name, its own description, and the
* questions it declares — so the sentence cannot promise a capability the skill
* does not have. Nothing is generated: this is the workspace stating a fact in
* the conversation, and the chips under it are the skill's own suggestions, so
* the next step is a question that skill was written to answer.
*/
function skillNotice({ entry, id, agentName, attached }) {
const name = entry?.name || id;
const on = agentName ? `**${agentName}**` : 'this agent';
if (!attached) {
return {
text: `**${name}** has been removed from ${on}. It no longer answers here.`,
followUp: null,
};
}
const opening = `**${name}** is now available to ${on}.`;
const what = entry?.description ? ` ${entry.description}` : '';
const invitation = entry?.suggestions?.length
? ' Ask me one of these, or anything else it covers.'
: ' Ask me about it whenever you need it.';
return {
text: `${opening}${what}${invitation}`,
followUp: entry?.suggestions?.length
? entry.suggestions.slice(0, 3).map((s) => ({
label: s.label,
prompt: s.prompt,
...(s.capability ? { capability: s.capability } : null),
}))
: null,
};
}
export default function AdminAgentDetail() {
const { id } = useParams();
const navigate = useNavigate();
@@ -53,6 +95,50 @@ export default function AdminAgentDetail() {
const [fields, setFields] = useState(() => (baseSource ? agentFieldsFromSource(baseSource) : null));
const [dirty, setDirty] = useState(false);
const [view, setView] = useState('configure');
/* The skill currently being written, so its card can say so and a second
click cannot race the first. */
const [pendingSkill, setPendingSkill] = useState(/** @type {string|null} */ (null));
/* The same catalog the workspace renders, read here so the sentence Owliver
says about a skill comes from the definition rather than from the card. */
const customSkills = useMemo(() => preferences.customSkills || [], [preferences.customSkills]);
const catalog = useMemo(() => skillCatalog(customSkills), [customSkills]);
/* Owliver's existing panel, on this page already. Attaching a skill states a
fact in that conversation — it never opens a second one. */
const { announce, ask, test } = useAssistantPanel();
/**
* Trying a capability, in the panel already on this page.
*
* `scope` runs the turn against the surface the capability actually answers
* on — see `useConversation.send`. Nothing is written: this is the whole of
* "test", and the agent is unchanged until Enable is pressed.
*/
const onTestCapability = useCallback(({ question, scope, capability, trace }) => {
ask({ question, scope, capability, trace });
}, [ask]);
/* Board skills are governed by the account's `disabledSkills` — the one list
`SkillSurface` actually reads. Written through the same `updatePreferences`
the Skills page uses, so there is no second persistence for it. */
const updatePreferences = useUpdatePreferences();
const disabledSkills = useMemo(
() => preferences.disabledSkills || [],
[preferences.disabledSkills]
);
const onToggleBoardSkill = useCallback((skillId, enabled) => {
const entry = boardCatalog(customSkills).find((e) => e.id === skillId);
const next = enabled
? disabledSkills.filter((x) => x !== skillId)
: [...new Set([...disabledSkills, skillId])];
updatePreferences.mutate(
{ disabledSkills: next },
reportSave(`${entry?.name || skillId} ${enabled ? 'enabled' : 'disabled'}`)
);
}, [customSkills, disabledSkills, updatePreferences]);
/* Reload when the address changes, but never overwrite an edit in progress. */
useEffect(() => {
@@ -86,6 +172,82 @@ export default function AdminAgentDetail() {
return result.agent;
}, [fields, baseSource, save, creating, navigate]);
/**
* Attaching and detaching a skill.
*
* The one write path for `fields.skills`, and deliberately not the same one
* the rest of the form uses. Every other field is a *draft* until Save — a
* half-typed name should never reach Owliver. A skill is not like that: the
* whole point of the catalog is that the agent gains the capability there and
* then, and the tree, the card and Owliver all say so immediately.
*
* So it is optimistic, through the existing `save`, with one honest exception
* and one honest revert:
*
* - **A draft the author is still editing is left a draft.** With unsaved
* changes on screen — or a brand-new agent with no definition yet — a
* write here would silently persist a half-finished name along with the
* skill. The change joins the draft instead, and the header already says
* the agent has unsaved changes.
* - **A refused write is put back.** The list is restored to exactly what it
* was and the reason is shown, rather than leaving a tree claiming a skill
* the stored definition does not carry.
*/
const onToggleSkill = useCallback(async (skillId) => {
if (!fields || pendingSkill) return;
const attached = !fields.skills.includes(skillId);
const previous = fields.skills;
const nextSkills = attached
/* Never a duplicate: the id is added only when it is absent, so clicking
Add twice cannot list the same skill twice. */
? [...previous, skillId]
: previous.filter((x) => x !== skillId);
const nextFields = { ...fields, skills: nextSkills };
const notice = skillNotice({
entry: catalog.find((e) => e.id === skillId) || null,
id: skillId,
agentName: fields.name,
attached,
});
/* A draft stays a draft — see above. */
if (creating || dirty || !agent) {
setFields(nextFields);
setDirty(true);
announce(notice);
return;
}
setFields(nextFields);
setPendingSkill(skillId);
const composed = applyAgentFields(baseSource, nextFields);
const problem = validateAgentSource(composed);
if (problem) {
setFields((current) => ({ ...current, skills: previous }));
setPendingSkill(null);
toast.error(problem);
return;
}
const skillName = catalog.find((e) => e.id === skillId)?.name || skillId;
const result = await save(composed, {
message: attached ? `${skillName} attached` : `${skillName} removed`,
});
setPendingSkill(null);
if (!result.ok) {
setFields((current) => ({ ...current, skills: previous }));
toast.error(result.error);
return;
}
announce(notice);
}, [fields, pendingSkill, creating, dirty, agent, catalog, baseSource, save, announce]);
const onPublish = useCallback(async () => {
const stored = dirty ? await persist() : agent;
if (!stored) return;
@@ -230,7 +392,9 @@ export default function AdminAgentDetail() {
)}
<Tabs
className="mt-3 mb-1"
/* Four tabs do not fit a 320px screen and the tablist does not wrap,
so the strip scrolls rather than the page. */
className="mt-3 mb-1 -mx-0.5 flex-nowrap overflow-x-auto px-0.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
variant="underline"
size="sm"
ariaLabel="Agent views"
@@ -238,6 +402,11 @@ export default function AdminAgentDetail() {
onChange={setView}
tabs={[
{ value: 'configure', label: 'Configure' },
/* Skills earn their own view rather than a section inside
Configure: composing an agent is a workspace — tree, catalog and
Owliver at once — and it cannot be that inside a column of form
fields. */
{ value: 'skills', label: 'Skills', count: fields.skills.length },
{ value: 'test', label: 'Test' },
{ value: 'insights', label: 'Insights' },
]}
@@ -248,15 +417,31 @@ export default function AdminAgentDetail() {
<AgentConfigure
fields={fields}
agents={agents}
customSkills={preferences.customSkills || []}
customSkills={customSkills}
onChange={onChange}
onOpenSkills={() => setView('skills')}
scopeLocked={shipped}
/>
)}
{view === 'skills' && (
<AgentSkillWorkspace
fields={fields}
customSkills={customSkills}
onToggleSkill={onToggleSkill}
disabledSkills={disabledSkills}
onToggleBoardSkill={onToggleBoardSkill}
onTestCapability={onTestCapability}
test={test}
busy={saving || Boolean(pendingSkill)}
pendingId={pendingSkill}
dirty={dirty || creating}
/>
)}
{view === 'test' && (
<AgentTestPanel
fields={fields}
dirty={dirty}
customSkills={preferences.customSkills || []}
customSkills={customSkills}
/>
)}
{view === 'insights' && (

View File

@@ -198,7 +198,7 @@ export default function AdminSkillDevelopment() {
actions={
<>
<Button variant="outline" size="sm" shape="rounded" onClick={() => navigate('/admin/workspace')}>
<ChevronLeft aria-hidden="true" /> Workspace
<ChevronLeft aria-hidden="true" /> Agent Registry
</Button>
<Button variant="outline" size="sm" shape="rounded" onClick={() => navigate('/admin/university')}>
Manage training

View File

@@ -1,35 +1,39 @@
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import {
LayoutTemplate, Sparkles,
BookOpen, ArrowRight
} from 'lucide-react';
import { ArrowRight, BookOpen, Sparkles } from 'lucide-react';
import { usePreferences } from '@/lib/krowHooks';
import { aiAgentSkills, allSkills, skillsWithFacet } from '@/lib/skills/registry';
import { aiAgentSkills, allSkills } from '@/lib/skills/registry';
import { allAgents } from '@/lib/agents/registry';
import { AdminPage } from '@/components/admin/PageShell';
import OwliverAvatar from '@/components/krow/OwliverAvatar';
import { cn } from '@/lib/utils';
/**
* Workspace & Skills — AI agent capability governance.
* Workspace — the way in to AI configuration, and only that.
*
* This page governs what *Owliver* can do. It used to govern workforce training
* as well — Training Paths and Skill Progression sat in the right-hand column —
* and that conflated two unrelated things under one word. A "skill" here is a
* capability the assistant gains; a "skill" in KROW Forge is something a person
* learns, proves and is verified in. They have different owners, different
* lifecycles and different audiences, and putting them side by side made the page
* read as though enabling a Bartending path taught Owliver to tend bar.
* This page used to offer three destinations side by side: Owliver Agents,
* Owliver Skills, and Board Skills. Each was a real thing with a real page
* behind it, and together they asked the reader a question the product should
* never have asked — *which of these three am I supposed to open?* Nothing on
* screen answered it, because the answer is structural: skills are configured
* **on an agent**, and an agent is the only thing here anyone actually manages.
*
* Workforce training was not removed, only returned to where it belongs: the
* definitions, the routes and the pages are untouched, and KROW Forge
* (`/admin/university`) and Skill Development still read them. See
* `workforceTrainingSkills` in the registry for the other half of the split.
* So there is one destination now. Agents. Everything else is reached through
* the agent it belongs to:
*
* Workspace → Agents → Configure → Skills → Owliver | Board
*
* Nothing was deleted to achieve that. `/admin/workspace/skills` still exists
* and still authors definitions — it is where the agent editor's own links go —
* it simply stopped being advertised as a peer of the thing that uses it.
*
* The counts are the same registry reads as before, minus the one that named an
* implementation detail: "Skill Kinds" told a recruiter that the product has two
* internal facets, which is true and is not their problem.
*/
export default function AdminWorkspace() {
const preferences = usePreferences();
/* Read through the agent registry, never by counting skills — one registry
answers "how many agents", exactly as the skill registry answers its own. */
const agentCount = useMemo(
@@ -53,220 +57,112 @@ export default function AdminWorkspace() {
const disabledSkills = preferences.disabledSkills || [];
const activeSkills = skills.filter((s) => !disabledSkills.includes(s.id)).length;
/* The two kinds of agent skill — the same two lists the management page is
split into, read here from the same filtered registry so the landing page and
the manager can never disagree about what is registered. */
const owliverSkills = useMemo(() => skillsWithFacet(skills, 'owliver'), [skills]);
const boardSkills = useMemo(() => skillsWithFacet(skills, 'ui'), [skills]);
const owliverCount = owliverSkills.length;
const boardCount = boardSkills.length;
const metrics = [
{ label: 'Agents', value: agentCount },
{ label: 'Skills', value: skills.length },
{ label: 'Active', value: activeSkills },
];
return (
<AdminPage
title="Workspace"
subtitle="Govern the AI agent capabilities that extend Owliver and KROW pages."
title="Agent Registry"
subtitle="Manage how Owliver behaves across Krow."
>
<div className="space-y-6 w-full">
{/* ── 1. Hero Banner (Full Width) ───────────────────────────────── */}
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-[#0838E0] via-[#1E40AF] to-[#0284C7] p-6 sm:p-8 text-white shadow-xl w-full">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-white/15 via-transparent to-transparent pointer-events-none" />
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-2">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/20 backdrop-blur-md text-white text-[12px] font-semibold border border-white/30">
<Sparkles className="w-3.5 h-3.5 text-sky-300" />
AI Capability Governance
<div className="w-full space-y-6">
{/* ── Hero ───────────────────────────────────────────────────────
The same banner, saying three plain numbers instead of two figures
and a word only this codebase uses. */}
<div className="relative w-full overflow-hidden rounded-3xl bg-gradient-to-r from-[#0838E0] via-[#1E40AF] to-[#0284C7] p-6 text-white shadow-xl sm:p-8">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-white/15 via-transparent to-transparent" />
<div className="relative z-10 flex flex-col justify-between gap-6 md:flex-row md:items-center">
<div className="min-w-0 space-y-2">
<div className="inline-flex items-center gap-2 rounded-full border border-white/30 bg-white/20 px-3 py-1 text-[12px] font-semibold text-white backdrop-blur-md">
<Sparkles className="h-3.5 w-3.5 text-sky-300" />
AI Agent Registry
</div>
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-white">AI Agent Capabilities</h1>
<p className="text-[14px] text-blue-100 font-normal max-w-2xl">
Govern what Owliver can understand, analyze and execute. Workforce training
paths and talent progression are managed in KROW Forge.
<h1 className="text-2xl font-bold tracking-tight text-white sm:text-3xl">
AI Agent Registry
</h1>
<p className="max-w-2xl text-[14px] font-normal text-blue-100">
Manage the agents that power Owliver across Krow. Each agent decides which
pages it answers on and which skills it may use.
</p>
</div>
<div className="flex flex-wrap items-center gap-3 sm:flex-nowrap sm:shrink-0">
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Registered Skills</p>
<p className="text-2xl font-bold text-white mt-0.5">{skills.length}</p>
<span className="text-[10px] text-emerald-300 font-medium">{activeSkills} Active</span>
</div>
{/* Was "Training Paths". Replaced rather than dropped: the slot is
useful, and the split of agent skills across the two lists is
what this page actually governs. */}
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Skill Kinds</p>
<p className="text-2xl font-bold text-sky-300 mt-0.5">{owliverCount + boardCount}</p>
<span className="text-[10px] text-blue-200">{owliverCount} Owliver · {boardCount} Board</span>
</div>
{metrics.map((metric) => (
<div
key={metric.label}
className="min-w-[100px] flex-1 rounded-2xl border border-white/15 bg-white/10 p-4 text-center backdrop-blur-md sm:flex-none"
>
<p className="text-[11px] font-semibold uppercase tracking-wider text-blue-200">
{metric.label}
</p>
<p className="mt-0.5 text-2xl font-bold tabular-nums text-white">{metric.value}</p>
</div>
))}
</div>
</div>
</div>
{/* ── Agents ─────────────────────────────────────────────────────
Above the two skill columns because it is the layer over them: an
agent decides which of these skills Owliver may use on a page. */}
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs space-y-4">
<div className="flex items-center justify-between border-b border-[#E0E4EA] pb-3">
<div className="flex items-center gap-2">
<OwliverAvatar className="w-6 h-6" rounded="rounded-md" />
<h2 className="text-[16px] font-bold text-[#101820]">Owliver Agents</h2>
{/* ── The one destination ────────────────────────────────────────
Skills are not a card beside this one. They are configured on an
agent, and this is the door to the agents. */}
<div className="space-y-4 rounded-2xl border border-[#E0E4EA] bg-white p-6 shadow-xs">
<div className="flex items-center justify-between gap-3 border-b border-[#E0E4EA] pb-3">
<div className="flex min-w-0 items-center gap-2">
<OwliverAvatar className="h-6 w-6" rounded="rounded-md" />
<h2 className="truncate text-[16px] font-bold text-[#101820]">Agents</h2>
</div>
<span className="text-[12px] font-semibold text-[#0838E0] bg-blue-50 px-3 py-1 rounded-full border border-blue-200/60">
<span className="shrink-0 rounded-full border border-blue-200/60 bg-blue-50 px-3 py-1 text-[12px] font-semibold text-[#0838E0]">
{agentCount} Registered
</span>
</div>
<p className="text-[13px] text-[#687386]">
An agent is how Owliver answers on a page: which skills it may use, what it knows, and
how much work an answer is worth. Agents narrow what a page offers — they never widen it.
An agent is how Owliver answers on a page: which skills it may use, what it knows,
and how much work an answer is worth. Agents narrow what a page offers — they never
widen it.
</p>
{/* The hierarchy, said once, so the reader knows where skills live
before they click rather than after. */}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 rounded-xl border border-[#E0E4EA] bg-[#F8F8F8]/60 px-3.5 py-2.5 text-[12px] text-[#687386]">
<span className="font-semibold text-[#101820]">Agents</span>
<ArrowRight className="h-3 w-3 shrink-0" aria-hidden="true" />
<span>Configure</span>
<ArrowRight className="h-3 w-3 shrink-0" aria-hidden="true" />
<span>Skills</span>
<ArrowRight className="h-3 w-3 shrink-0" aria-hidden="true" />
<span className="font-medium text-[#101820]">Owliver Skills</span>
<span aria-hidden="true">/</span>
<span className="font-medium text-[#101820]">Board Skills</span>
</div>
<Link
to="/admin/workspace/agents"
className="w-full inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl bg-[#0838E0] hover:bg-[#062BAF] text-white text-[13px] font-semibold transition-all shadow-md shadow-blue-500/20 hover:scale-[1.01]"
className="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-[#0838E0] px-5 py-3 text-[13px] font-semibold text-white shadow-md shadow-blue-500/20 transition-all hover:scale-[1.01] hover:bg-[#062BAF]"
>
<Sparkles className="w-4 h-4" />
<Sparkles className="h-4 w-4" />
Manage Agents
<ArrowRight className="w-4 h-4" />
<ArrowRight className="h-4 w-4" />
</Link>
</div>
{/* ── 2. Full Width 2-Column Grid ───────────────────────────────── */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
{/* ── Left Column: Owliver AI Capabilities ───────────────────── */}
<div className="space-y-6">
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs space-y-5">
<div className="flex items-center justify-between border-b border-[#E0E4EA] pb-3">
<div className="flex items-center gap-2">
<OwliverAvatar className="w-6 h-6" rounded="rounded-md" />
<h2 className="text-[16px] font-bold text-[#101820]">Owliver Skills</h2>
</div>
<span className="text-[12px] font-semibold text-[#0838E0] bg-blue-50 px-3 py-1 rounded-full border border-blue-200/60">
{owliverCount} Registered
</span>
</div>
<p className="text-[13px] text-[#687386]">
Owliver skills teach the assistant what it can be asked on a page and how to answer —
screening, matching and contextual analysis. They define what the AI understands, not
what a candidate learns.
</p>
{/* Skills Sample List */}
<div className="space-y-3">
{owliverSkills.slice(0, 4).map((s) => {
const isActive = !disabledSkills.includes(s.id);
return (
<div key={s.id} className="p-3.5 rounded-xl border border-[#E0E4EA] bg-[#F8F8F8]/50 flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<p className="text-[13px] font-bold text-[#101820]">{s.name || s.id}</p>
{s.description && <p className="text-[12px] text-[#687386] truncate mt-0.5">{s.description}</p>}
</div>
<span className={cn(
'px-2.5 py-0.5 rounded-full text-[11px] font-semibold shrink-0',
isActive ? 'bg-emerald-50 text-[#16A765] border border-emerald-200' : 'bg-gray-100 text-gray-500'
)}>
{isActive ? 'Active' : 'Disabled'}
</span>
</div>
);
})}
</div>
{/* CTA Button */}
<div className="pt-2">
<Link
to="/admin/workspace/skills?tab=owliver"
className="w-full inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl bg-[#0838E0] hover:bg-[#062BAF] text-white text-[13px] font-semibold transition-all shadow-md shadow-blue-500/20 hover:scale-[1.01]"
>
<Sparkles className="w-4 h-4" />
Manage Owliver Skills
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
</div>
{/* ── Right Column: Board Skills ─────────────────────────────────
The other kind of AI agent skill. This column previously held
Workforce Training Paths and Skill Progression Tracking, both of
which are KROW Forge concerns — see the note at the top of the file.
Board skills belong here because, like Owliver skills, they are
capabilities the product gains rather than something a person
learns. */}
<div className="space-y-6">
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs space-y-5">
<div className="flex items-center justify-between border-b border-[#E0E4EA] pb-3">
<div className="flex items-center gap-2">
<LayoutTemplate className="w-5 h-5 text-[#0838E0]" />
<h2 className="text-[16px] font-bold text-[#101820]">Board Skills</h2>
</div>
<span className="text-[12px] font-semibold text-[#0838E0] bg-blue-50 px-3 py-1 rounded-full border border-blue-200/60">
{boardCount} Registered
</span>
</div>
<p className="text-[13px] text-[#687386]">
Board skills add dynamic sections to KROW pages, read from real page data. They
extend what a page shows, not what a candidate learns.
</p>
{/* Sample list, from the same filtered registry read as the left
column — so the two columns can never disagree about what is
registered. */}
<div className="space-y-3">
{boardSkills.slice(0, 4).map((s) => {
const isActive = !disabledSkills.includes(s.id);
return (
<div key={s.id} className="p-3.5 rounded-xl border border-[#E0E4EA] bg-[#F8F8F8]/50 flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<p className="text-[13px] font-bold text-[#101820]">{s.name || s.id}</p>
{s.description && <p className="text-[12px] text-[#687386] truncate mt-0.5">{s.description}</p>}
</div>
<span className={cn(
'px-2.5 py-0.5 rounded-full text-[11px] font-semibold shrink-0',
isActive ? 'bg-emerald-50 text-[#16A765] border border-emerald-200' : 'bg-gray-100 text-gray-500'
)}>
{isActive ? 'Active' : 'Disabled'}
</span>
</div>
);
})}
{boardSkills.length === 0 && (
<p className="text-[13px] text-[#687386] py-2">
No Board skills registered yet. A Board skill declares a section, where it
sits and what it reads.
</p>
)}
</div>
<div className="pt-2">
<Link
to="/admin/workspace/skills?tab=ui"
className="w-full inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl border border-[#E0E4EA] bg-white hover:bg-[#F8F8F8] text-[#101820] text-[13px] font-semibold transition-all shadow-xs"
>
<LayoutTemplate className="w-4 h-4 text-[#0838E0]" />
Manage Board Skills
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
</div>
</div>
{/* ── 3. Unified Registry Architecture Banner ───────────────────── */}
<div className="flex items-start gap-3.5 p-5 rounded-2xl bg-blue-50/60 border border-blue-200/50 text-[#0838E0] text-[13px] leading-relaxed">
<BookOpen className="w-5 h-5 shrink-0 text-[#0838E0] mt-0.5" />
<div>
{/* ── The one distinction worth keeping ──────────────────────────
Not a third destination: a standing fact about a word that means two
unrelated things in this product, and the reason KROW Forge is not
on this page. */}
<div className="flex items-start gap-3.5 rounded-2xl border border-blue-200/50 bg-blue-50/60 p-5 text-[13px] leading-relaxed text-[#0838E0]">
<BookOpen className="mt-0.5 h-5 w-5 shrink-0 text-[#0838E0]" />
<div className="min-w-0">
<p className="font-bold text-[#101820]">AI skills and workforce training are different things</p>
<p className="text-[#687386] mt-0.5">
<p className="mt-0.5 text-[#687386]">
This page governs <span className="font-semibold">AI agent skills</span> — what Owliver
understands and what a Board skill draws on a page. <span className="font-semibold">Workforce
training</span> — training paths, proving challenges, skill verification and talent
understands, and what a Board skill draws on a page. <span className="font-semibold">Workforce
training</span> — training paths, proving challenges, verification and talent
progression — is what a person learns, and it is managed in{' '}
<Link to="/admin/university" className="font-semibold underline hover:no-underline">KROW Forge</Link>.
Both are Markdown in one registry, read through separate queries so neither appears in the
other’s surface.
</p>
</div>
</div>

View File

@@ -469,7 +469,7 @@ export default function AdminWorkspaceAgents() {
className="hidden md:inline-flex items-center gap-1.5 rounded-lg border border-border/70 bg-surface px-3 py-1.5 text-body-sm text-ink-3 transition-colors hover:border-krow-blue/40 hover:text-krow-blue hover:bg-surface-subtle"
>
<span>Skills managed in</span>
<span className="font-semibold text-krow-blue">Workspace &rarr; Skills</span>
<span className="font-semibold text-krow-blue">Agent Registry &rarr; Skills</span>
</Link>
<SegmentedToggle

View File

@@ -504,7 +504,7 @@ export default function WorkspaceSkills() {
actions={
<>
<Button variant="outline" size="sm" shape="rounded" onClick={() => navigate('/admin/workspace')}>
<ChevronLeft aria-hidden="true" /> Workspace
<ChevronLeft aria-hidden="true" /> Agent Registry
</Button>
{/* Account definitions live in this browser's storage and nowhere
else. Import and export are what make that a place rather than a