From cffbfc73719e5ff1073fe872ae917b448b67cab0 Mon Sep 17 00:00:00 2001 From: Mohith26 <109003762+Mohith26@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:20:42 -0500 Subject: [PATCH] fix(squads): resolve structured output names in membersOverrides.artifactPlan A squad's membersOverrides.artifactPlan.structuredOutputIds was sent to the API as written, so a name there failed the push with a 400 ("each value in structuredOutputIds must be a UUID"), while the same name works in an assistant's artifactPlan. Push now resolves those names like an assistant's artifactPlan, pull writes them back as names, extractReferencedIds counts them (so validate, delete protection and squad dependency creation see them), and pushing a squad creates a missing structured output it names first. Fixes #8 --- improvements.md | 8 ++- src/canonical.ts | 18 ++++++ src/push.ts | 4 ++ src/resolver.ts | 35 +++++++++++ src/validate-refs.ts | 5 +- tests/members-overrides-refs.test.ts | 91 ++++++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 tests/members-overrides-refs.test.ts diff --git a/improvements.md b/improvements.md index ef4ca0e..161b1c9 100644 --- a/improvements.md +++ b/improvements.md @@ -1680,9 +1680,11 @@ open: state entry before the apply pass, and the reference then hits the resolver's silent drop. - Names push never resolves still go unchecked: `hooks[].do[].toolId` and - `artifactPlan.structuredOutputIds` inside overrides, `model.toolRefs` at any - depth, and `toolIds` in a squad's inline `members[].assistant`. Only - `toolIds` inside overrides are checked. + `artifactPlan.structuredOutputIds` inside overrides (except a squad's + `membersOverrides.artifactPlan`, which push resolves and validate checks + like any other reference, #8), `model.toolRefs` at any depth, and `toolIds` + in a squad's inline `members[].assistant`. Only `toolIds` inside overrides + are checked. - Annotations carry a file but no line, so GitHub shows them at the top of the file, and it keeps at most 10 per type per step. diff --git a/src/canonical.ts b/src/canonical.ts index 5c49b62..aad0844 100644 --- a/src/canonical.ts +++ b/src/canonical.ts @@ -13,6 +13,7 @@ // ───────────────────────────────────────────────────────────────────────────── import { replaceCredentialRefs } from "./credentials.ts"; +import { membersOverridesArtifactPlan } from "./resolver.ts"; import type { ResourceType, StateFile } from "./types.ts"; export interface VapiResource { @@ -108,6 +109,23 @@ export function resolveReferencesToResourceIds( resolved.artifactPlan = artifactPlan; } + // Resolve structuredOutputIds in a squad's membersOverrides.artifactPlan + const membersArtifactPlan = membersOverridesArtifactPlan(resolved); + if ( + membersArtifactPlan && + Array.isArray(membersArtifactPlan.structuredOutputIds) + ) { + resolved.membersOverrides = { + ...(resolved.membersOverrides as Record), + artifactPlan: { + ...membersArtifactPlan, + structuredOutputIds: membersArtifactPlan.structuredOutputIds.map( + (uuid: string) => structuredOutputsMap.get(uuid) ?? uuid, + ), + }, + }; + } + // Resolve assistantIds in structured outputs (API returns camelCase) if (Array.isArray(resolved.assistantIds)) { resolved.assistant_ids = (resolved.assistantIds as string[]).map( diff --git a/src/push.ts b/src/push.ts index c22f41d..32db2af 100644 --- a/src/push.ts +++ b/src/push.ts @@ -2058,6 +2058,7 @@ async function main(): Promise { if (squads.length > 0) { console.log("\n👥 Applying squads...\n"); // Auto-resolve missing assistant dependencies (recursively resolves tools/SOs) + // and structured outputs named in membersOverrides.artifactPlan for (const squad of squads) { const refs = extractReferencedIds( squad.data as Record, @@ -2065,6 +2066,9 @@ async function main(): Promise { for (const assistantId of refs.assistants) { await ensureAssistantExists(assistantId, depCtx); } + for (const outputId of refs.structuredOutputs) { + await ensureStructuredOutputExists(outputId, depCtx); + } } for (const squad of squads) { try { diff --git a/src/resolver.ts b/src/resolver.ts index e3ef263..9e3d3c8 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -175,6 +175,18 @@ export function resolveSimulationIds( .filter((id): id is string => id !== null); } +// A squad's `membersOverrides.artifactPlan`, or undefined when absent. It +// names structured outputs the same way an assistant's `artifactPlan` does. +export function membersOverridesArtifactPlan( + data: Record, +): Record | undefined { + const overrides = data.membersOverrides; + if (!overrides || typeof overrides !== "object") return undefined; + const artifactPlan = (overrides as Record).artifactPlan; + if (!artifactPlan || typeof artifactPlan !== "object") return undefined; + return artifactPlan as Record; +} + export function resolveReferences( data: Record, state: StateFile, @@ -209,6 +221,18 @@ export function resolveReferences( ); } + // Resolve structuredOutputIds in a squad's membersOverrides.artifactPlan + const membersArtifactPlan = membersOverridesArtifactPlan(resolved); + if ( + membersArtifactPlan && + Array.isArray(membersArtifactPlan.structuredOutputIds) + ) { + membersArtifactPlan.structuredOutputIds = resolveStructuredOutputIds( + membersArtifactPlan.structuredOutputIds as string[], + state, + ); + } + // Resolve assistant_ids in structured outputs if (Array.isArray(resolved.assistant_ids)) { resolved.assistantIds = resolveAssistantIds( @@ -386,6 +410,17 @@ export function extractReferencedIds( } } + // Check membersOverrides.artifactPlan.structuredOutputIds in squads + const membersArtifactPlan = membersOverridesArtifactPlan(data); + if ( + membersArtifactPlan && + Array.isArray(membersArtifactPlan.structuredOutputIds) + ) { + structuredOutputs.push( + ...(membersArtifactPlan.structuredOutputIds as string[]).map(cleanId), + ); + } + // Check assistant_ids in structured outputs if (Array.isArray(data.assistant_ids)) { assistants.push(...(data.assistant_ids as string[]).map(cleanId)); diff --git a/src/validate-refs.ts b/src/validate-refs.ts index 38a41b4..b2b0b93 100644 --- a/src/validate-refs.ts +++ b/src/validate-refs.ts @@ -22,8 +22,9 @@ import type { ValidationFinding } from "./validate.ts"; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -// Push never resolves references inside these objects, so a name there -// reaches the API as-is. +// Push never resolves tool references inside these objects, so a tool name +// there reaches the API as-is. (A squad's membersOverrides.artifactPlan +// structuredOutputIds is the one override field push does resolve.) const OVERRIDE_KEYS = new Set([ "assistantOverrides", "membersOverrides", diff --git a/tests/members-overrides-refs.test.ts b/tests/members-overrides-refs.test.ts new file mode 100644 index 0000000..55ba4e2 --- /dev/null +++ b/tests/members-overrides-refs.test.ts @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { resolveReferencesToResourceIds } from "../src/canonical.ts"; +import { extractReferencedIds, resolveReferences } from "../src/resolver.ts"; +import type { StateFile } from "../src/types.ts"; + +// A squad's membersOverrides.artifactPlan.structuredOutputIds names structured +// outputs the same way an assistant's artifactPlan does (issue #8): push sends +// UUIDs, pull writes the names back, and the names count as references. + +const SUMMARY_UUID = "3f2b1c4d-5e6f-4a1b-8c9d-0e1f2a3b4c5d"; +const DASHBOARD_UUID = "a0000000-0000-4000-8000-000000000001"; + +function state(): StateFile { + return { + credentials: {}, + assistants: {}, + structuredOutputs: { "call-summary": { uuid: SUMMARY_UUID } }, + tools: {}, + squads: {}, + personalities: {}, + scenarios: {}, + simulations: {}, + simulationSuites: {}, + evals: {}, + }; +} + +test("push resolves structured output names in membersOverrides.artifactPlan", () => { + const squad = { + name: "Team", + members: [{ assistantId: "intake" }], + membersOverrides: { + artifactPlan: { + structuredOutputIds: ["call-summary ## note", DASHBOARD_UUID], + recordingEnabled: true, + }, + }, + }; + const payload = resolveReferences(squad, state()); + assert.deepEqual(payload.membersOverrides, { + artifactPlan: { + structuredOutputIds: [SUMMARY_UUID, DASHBOARD_UUID], + recordingEnabled: true, + }, + }); + // The local data is not mutated. + assert.deepEqual(squad.membersOverrides.artifactPlan.structuredOutputIds, [ + "call-summary ## note", + DASHBOARD_UUID, + ]); +}); + +test("pull writes membersOverrides.artifactPlan structured outputs back as names", () => { + const platform = { + name: "Team", + members: [{ assistantId: DASHBOARD_UUID }], + membersOverrides: { + artifactPlan: { structuredOutputIds: [SUMMARY_UUID, DASHBOARD_UUID] }, + }, + }; + const local = resolveReferencesToResourceIds(platform, state()); + assert.deepEqual(local.membersOverrides, { + artifactPlan: { structuredOutputIds: ["call-summary", DASHBOARD_UUID] }, + }); + // Round trip: pushing what pull wrote sends the platform's UUIDs again. + const pushed = resolveReferences(local, state()); + assert.deepEqual(pushed.membersOverrides, platform.membersOverrides); +}); + +test("membersOverrides.artifactPlan structured outputs count as references", () => { + const refs = extractReferencedIds({ + members: [{ assistantId: "intake" }], + membersOverrides: { + artifactPlan: { structuredOutputIds: ["call-summary ## note"] }, + }, + }); + assert.deepEqual(refs.structuredOutputs, ["call-summary"]); + assert.deepEqual(refs.assistants, ["intake"]); +}); + +test("squads without membersOverrides.artifactPlan are unchanged", () => { + const squad = { + name: "Team", + members: [{ assistantId: "intake" }], + membersOverrides: { voice: { provider: "vapi", voiceId: "Elliot" } }, + }; + assert.deepEqual(resolveReferences(squad, state()), squad); + assert.deepEqual(resolveReferencesToResourceIds(squad, state()), squad); + assert.deepEqual(extractReferencedIds(squad).structuredOutputs, []); +});