diff --git a/scripts/skill-check.mjs b/scripts/skill-check.mjs index 3ad7041..2c78d63 100644 --- a/scripts/skill-check.mjs +++ b/scripts/skill-check.mjs @@ -5212,6 +5212,53 @@ record('...and leaves the date-anchored shift records to the Go seeder', !('ShiftRecord' in JSON.parse(builtFixture).entities) && seedModule.seedData.ShiftRecord.length > 0, `${seedModule.seedData.ShiftRecord.length} shift records held in seed.js only`); +/* ── The registry does not report a shipped agent as an override of itself ── */ + +/** + * Authored agents now come from `/api/v1/agent-definitions`, and that registry + * holds both kinds: the shipped set, published from the same files this build + * ships, and anything a person authored. Passing the first kind through as an + * authored source made every shipped agent report that it "replaces the + * built-in agent of the same id" — nine warnings on the one screen that exists + * to say what is wrong, about a deployment that was working correctly. + * + * The rows below are the shape the live registry actually returns. + */ +const storeModule = await server.ssrLoadModule('/src/lib/agents/agentStore.js'); +const agentsModule = await server.ssrLoadModule('/src/lib/agents/registry.js'); +const shippedIds = new Set(agentsModule.AGENTS.map((a) => a.id)); +const registryRows = [ + ...agentsModule.AGENTS.map((a) => ({ + definition_id: a.id, visibility: 'organization', markdown: a.markdown, + })), + { + definition_id: 'authored-example', visibility: 'personal', + markdown: '---\nid: authored-example\nname: Authored Example\nstatus: published\n' + + 'version: 1\npages:\n - control-center\n---\n\n# Authored Example\n', + }, +]; +const built = agentsModule.readAgentRegistry( + storeModule.sourcesFrom(registryRows, shippedIds), { customSkills: [] }); +const falseShadows = built.diagnostics.filter((d) => d.kind === 'shadowed'); +record('a shipped agent published to the registry is not reported as overriding itself', + falseShadows.length === 0, + falseShadows.map((d) => d.agentId).join(', ') || 'no false overrides'); +record('...and an authored agent still appears beside the shipped ones', + built.agents.some((a) => a.id === 'authored-example') + && built.agents.length === agentsModule.AGENTS.length + 1, + `${built.agents.length} agents from ${registryRows.length} rows`); + +/* A genuine personal override of a shipped id is still reported — the + diagnostic is useful, it was only being raised for the wrong rows. */ +const overridden = agentsModule.readAgentRegistry( + storeModule.sourcesFrom([{ + definition_id: agentsModule.AGENTS[0].id, visibility: 'personal', + markdown: agentsModule.AGENTS[0].markdown, + }], shippedIds), { customSkills: [] }); +record('a personal override of a shipped agent is still reported as one', + overridden.diagnostics.some((d) => d.kind === 'shadowed'), + overridden.diagnostics.map((d) => d.kind).join(', ') || 'none'); + /* The seed must exercise every status, or none of the above can catch anything. */ const seededStatuses = new Set(seedModule.seedData.JobApplication.map((a) => a.status)); const neverSeeded = records.APPLICATION_STATUSES.filter((st) => !seededStatuses.has(st)); diff --git a/src/lib/agents/agentStore.js b/src/lib/agents/agentStore.js index cfadbca..dd1d0c0 100644 --- a/src/lib/agents/agentStore.js +++ b/src/lib/agents/agentStore.js @@ -39,9 +39,20 @@ export function useAgentDefinitions() { * `{ path, raw }` is the shape `readAgentRegistry` already takes, so the reader * did not have to learn where definitions come from — only the store changed. */ -export function sourcesFrom(rows) { +export function sourcesFrom(rows, shippedIds = new Set()) { return (rows || []) .filter((r) => r && typeof r.markdown === 'string' && r.markdown.trim()) + /* An organization row whose id is a shipped agent IS that shipped agent: + `importagents` publishes the same files this build ships, so the registry + and the bundle hold one definition between them, not two. + + Passing it through as an authored source made every shipped agent report + that it "replaces the built-in agent of the same id" — nine warnings, on + the one screen that exists to say what is wrong, describing a deployment + that was working correctly. A personal row is a genuine override and is + still reported as one; an organization row with an id nothing ships is a + shared agent somebody authored, and belongs in the list. */ + .filter((r) => !(r.visibility === 'organization' && shippedIds.has(r.definition_id))) .map((r) => ({ path: `authored/${r.definition_id || r.id}.md`, raw: r.markdown })); } @@ -110,8 +121,8 @@ export function useRowFor(rows) { } /** Memoised sources, so the registry is not rebuilt on every render. */ -export function useAuthoredSources(rows) { - return useMemo(() => sourcesFrom(rows), [rows]); +export function useAuthoredSources(rows, shippedIds) { + return useMemo(() => sourcesFrom(rows, shippedIds), [rows, shippedIds]); } /** diff --git a/src/lib/agents/useAgents.js b/src/lib/agents/useAgents.js index ee4620b..9274f92 100644 --- a/src/lib/agents/useAgents.js +++ b/src/lib/agents/useAgents.js @@ -39,7 +39,10 @@ export function useAgents() { const deleteDefinition = useDeleteAgentDefinition(); const rows = useMemo(() => definitions.data || [], [definitions.data]); - const stored = useAuthoredSources(rows); + /* The ids this build ships. An organization row carrying one of them is that + same definition as published by importagents, not an override of it. */ + const shippedIds = useMemo(() => new Set(AGENTS.map((a) => a.id)), []); + const stored = useAuthoredSources(rows, shippedIds); const customSkills = useMemo(() => preferences.customSkills || [], [preferences.customSkills]); /* Anything an author wrote before the store moved. Without this it would stop