From 5e1db0fa894c370541dd9019ed0a1f11697e0a01 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 23 Apr 2026 13:18:35 -0400 Subject: [PATCH 1/3] docs(creative-agent): louder build_creative shape callouts + audio creative-template example Discoverability fix for existing SDK surface (factories and response helpers already shipped; integrators weren't finding them). - scripts/generate-agent-docs.ts: TOOL_GOTCHAS data map drives new 'Watch out:' blocks on build_creative, preview_creative, and list_creative_formats. Points at the compile-time-safe response helpers and flags the audio-formats renders gap. - skills/build-creative-agent/SKILL.md: cross-cutting pitfalls now name audioAsset and spell out that platform-native top-level fields are invalid. New Audio subsection under creative-template covering type: 'audio', renders with duration_seconds, async render pipelines, and a handler using buildCreativeResponse + audioAsset. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../creative-agent-docs-audio-examples.md | 12 ++++ docs/llms.txt | 18 +++++- package-lock.json | 4 +- scripts/generate-agent-docs.ts | 28 +++++++++ skills/build-creative-agent/SKILL.md | 62 ++++++++++++++++++- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 .changeset/creative-agent-docs-audio-examples.md diff --git a/.changeset/creative-agent-docs-audio-examples.md b/.changeset/creative-agent-docs-audio-examples.md new file mode 100644 index 000000000..512ad9532 --- /dev/null +++ b/.changeset/creative-agent-docs-audio-examples.md @@ -0,0 +1,12 @@ +--- +'@adcp/client': patch +--- + +docs(creative-agent): louder build_creative response-shape callouts, add audio creative-template example + +Makes discoverability of existing SDK surface better for creative agents: + +- `docs/llms.txt` — new "Watch out:" blocks on `build_creative`, `preview_creative`, and `list_creative_formats` that point at `buildCreativeResponse`/`buildCreativeMultiResponse`/typed asset factories and flag the audio-formats `renders` gotcha. Driven by a data map in `scripts/generate-agent-docs.ts`. +- `skills/build-creative-agent/SKILL.md` — cross-cutting pitfalls now mention `audioAsset` and spell out that platform-native top-level fields (`tag_url`, `creative_id`, `media_type`) are invalid responses. Adds an Audio subsection under `creative-template` covering format declaration (`type: 'audio'`, `renders: [{ role, duration_seconds }]`), async render pipelines, and a handler example using `buildCreativeResponse` + `audioAsset`. + +No library code changes — the factories and response helpers already shipped in prior releases. diff --git a/docs/llms.txt b/docs/llms.txt index fb18317c5..d3745f5b8 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,6 +1,6 @@ # Ad Context Protocol (AdCP) -> Generated at: 2026-04-22 +> Generated at: 2026-04-23 > Library: @adcp/client v5.13.0 > AdCP major version: 3 > Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt @@ -265,6 +265,10 @@ Request parameters for discovering format IDs and creative agents supported by t - Required: `formats: object[]` - Optional: `creative_agents: object[]`, `errors: object[]`, `pagination: Pagination Response`, `sandbox: boolean`, `context: Context` +**Watch out:** +- Each `renders[]` entry needs `role` + exactly one of `dimensions` (object) OR `parameters_from_format_id: true`. Top-level `{ width, height }` fails — wrap in `dimensions`. +- Audio formats (`type: "audio"`) have no width/height — declare `renders: [{ role: "primary", duration_seconds: N }]` so storyboard `field_present formats[0].renders` validations still pass. + #### `create_media_buy` Request parameters for creating a media buy. @@ -391,6 +395,11 @@ Request parameters for AI-powered creative generation. - Required: `creative_manifest: Creative Manifest` - Optional: `sandbox: boolean`, `expires_at: string`, `preview: object`, `preview_error: Error`, `pricing_option_id: string`, `vendor_cost: number`, `currency: string`, `consumption: Creative Consumption`, +1 more +**Watch out:** +- Response is ALWAYS `{ creative_manifest }` (single) or `{ creative_manifests }` (multi). Platform-native fields at the top level (`tag_url`, `creative_id`, `media_type`) are invalid. +- Use `buildCreativeResponse({ creative_manifest })` / `buildCreativeMultiResponse({ creative_manifests })` from `@adcp/client/server` to enforce the shape at compile time. +- Each asset under `creative_manifest.assets` needs an `asset_type` discriminator — use the factories: `imageAsset`, `videoAsset`, `audioAsset`, `htmlAsset`, `urlAsset`, `textAsset` (or `Asset.image(...)`). + #### `preview_creative` Request parameters for generating creative previews. @@ -403,6 +412,9 @@ Request parameters for generating creative previews. - Required: `response_type: 'single'`, `previews: object[]`, `expires_at: string` - Optional: `interactive_url: string`, `context: Context` +**Watch out:** +- Each `renders[]` entry is a oneOf on `output_format` — use `urlRender({...})`, `htmlRender({...})`, or `bothRender({...})` to inject the discriminator and require the matching `preview_url`/`preview_html` field. + #### `list_creative_formats` Request parameters for discovering creative formats from this creative agent. @@ -414,6 +426,10 @@ Request parameters for discovering creative formats from this creative agent. - Required: `formats: object[]` - Optional: `creative_agents: object[]`, `errors: object[]`, `pagination: Pagination Response`, `context: Context` +**Watch out:** +- Each `renders[]` entry needs `role` + exactly one of `dimensions` (object) OR `parameters_from_format_id: true`. Top-level `{ width, height }` fails — wrap in `dimensions`. +- Audio formats (`type: "audio"`) have no width/height — declare `renders: [{ role: "primary", duration_seconds: N }]` so storyboard `field_present formats[0].renders` validations still pass. + #### `get_creative_delivery` Request parameters for retrieving creative delivery data with variant-level breakdowns. diff --git a/package-lock.json b/package-lock.json index f26b59e98..170e6b9e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@adcp/client", - "version": "5.11.0", + "version": "5.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@adcp/client", - "version": "5.11.0", + "version": "5.13.0", "license": "Apache-2.0", "dependencies": { "ajv": "^8.18.0", diff --git a/scripts/generate-agent-docs.ts b/scripts/generate-agent-docs.ts index 3b09665fb..89c997049 100644 --- a/scripts/generate-agent-docs.ts +++ b/scripts/generate-agent-docs.ts @@ -43,6 +43,25 @@ const OPERATION_DOMAINS = ['trusted-match'] as const; // Skip internal/sandbox-only tools const SKIP_TOOLS = new Set(['comply-test-controller']); +// Short, high-signal "watch out" notes appended below the Response block for +// specific tools. Kept here so regenerating llms.txt from the schema index +// still carries the operational lessons that bit real integrators. +// Keep each entry under ~5 lines — llms.txt is a scan surface, not a tutorial. +const TOOL_GOTCHAS: Record = { + build_creative: [ + 'Response is ALWAYS `{ creative_manifest }` (single) or `{ creative_manifests }` (multi). Platform-native fields at the top level (`tag_url`, `creative_id`, `media_type`) are invalid.', + 'Use `buildCreativeResponse({ creative_manifest })` / `buildCreativeMultiResponse({ creative_manifests })` from `@adcp/client/server` to enforce the shape at compile time.', + 'Each asset under `creative_manifest.assets` needs an `asset_type` discriminator — use the factories: `imageAsset`, `videoAsset`, `audioAsset`, `htmlAsset`, `urlAsset`, `textAsset` (or `Asset.image(...)`).', + ], + preview_creative: [ + 'Each `renders[]` entry is a oneOf on `output_format` — use `urlRender({...})`, `htmlRender({...})`, or `bothRender({...})` to inject the discriminator and require the matching `preview_url`/`preview_html` field.', + ], + list_creative_formats: [ + 'Each `renders[]` entry needs `role` + exactly one of `dimensions` (object) OR `parameters_from_format_id: true`. Top-level `{ width, height }` fails — wrap in `dimensions`.', + 'Audio formats (`type: "audio"`) have no width/height — declare `renders: [{ role: "primary", duration_seconds: N }]` so storyboard `field_present formats[0].renders` validations still pass.', + ], +}; + // GitHub Pages base URL for published docs const DOCS_BASE_URL = 'https://adcontextprotocol.github.io/adcp-client'; @@ -716,6 +735,15 @@ function generateLlmsTxt( } ln(); } + + const gotchas = TOOL_GOTCHAS[tool.name]; + if (gotchas?.length) { + ln(`**Watch out:**`); + for (const note of gotchas) { + ln(`- ${note}`); + } + ln(); + } } // Deep dive links for this domain diff --git a/skills/build-creative-agent/SKILL.md b/skills/build-creative-agent/SKILL.md index b152558fe..751e70823 100644 --- a/skills/build-creative-agent/SKILL.md +++ b/skills/build-creative-agent/SKILL.md @@ -93,7 +93,7 @@ What happens when a creative is synced: > **Cross-cutting pitfalls matrix runs keep catching:** > > - `capabilities.specialisms` is `string[]` of enum ids (e.g. `['creative-ad-server']`), NOT `[{id, version}]` objects. -> - `build_creative` response is `{ creative_manifest: { format_id, assets } }`. Each asset in `creative_manifest.assets` requires an `asset_type` discriminator — use the typed factories (`imageAsset({...})`, `videoAsset({...})`, `htmlAsset({...})`, `urlAsset({...})`) so the discriminator is injected for you; a plain `{ serving_tag: { content: '...' } }` fails validation. +> - `build_creative` response is `{ creative_manifest: { format_id, assets } }` (single) or `{ creative_manifests: [...] }` (multi). Platform-native fields at the top level (`tag_url`, `creative_id`, `media_type`) are **invalid** — use `buildCreativeResponse({ creative_manifest })` / `buildCreativeMultiResponse({ creative_manifests })` from `@adcp/client/server` to lock the shape at compile time. Each asset in `creative_manifest.assets` requires an `asset_type` discriminator — use the typed factories (`imageAsset({...})`, `videoAsset({...})`, `audioAsset({...})`, `htmlAsset({...})`, `urlAsset({...})`) so the discriminator is injected for you; a plain `{ serving_tag: { content: '...' } }` fails validation. > - `preview_creative` renders have the same pattern — each `renders[]` entry is a oneOf on `output_format`. Use `urlRender({...})`, `htmlRender({...})`, or `bothRender({...})` to inject the discriminator and require the matching `preview_url` / `preview_html` field automatically. > - `get_creative_delivery` requires **top-level `currency: string`** (ISO 4217), in addition to any per-row spend fields. `reporting_period/start` and `/end` are ISO 8601 **date-time** strings (`new Date().toISOString()`), not date-only. > - `videoAsset({...})` requires `width` + `height` per GA (previously optional). Set realistic pixel values — `{ url, width: 1920, height: 1080 }`. @@ -113,6 +113,7 @@ Asset values use type-specific shapes, not a generic `asset_type` discriminator: - Image: `{ url: string, width: number, height: number, format: string }` - Video: `{ url: string, duration_ms: number, format: string }` +- Audio: `{ url: string, container_format: string, codec: string, duration_ms: number, channels?: string, sampling_rate_hz?: number }` - HTML: `{ content: string }` (not `{ html: string }`) - Text: `{ text: string }` @@ -619,6 +620,65 @@ Output can be HTML (`{ content: '
...
' }`), JavaScript tag (`{ content `list_creative_formats` accepts filter params (`type`, `max_width`, `max_height`). Return an empty array — not an error — when nothing matches. +#### Audio creative-template (TTS / mix / master) + +Audio creative agents (AudioStack, ElevenLabs, Resemble) fit the `creative-template` archetype — stateless transform from an inline manifest to a rendered audio file. Two things differ from display: + +1. **No width/height.** Declare `renders: [{ role: 'primary', duration_seconds: N }]` — the `renders` array is still required by the `discover_formats` storyboard validation, but audio formats surface duration instead of dimensions. +2. **Async render pipelines.** TTS → mix → master is typically minutes long. Don't block the `build_creative` call waiting for the pipeline; the platform-native SDK (AudioStack's 300s poll window, etc.) belongs inside a task worker. If the platform's API returns quickly, build synchronously; otherwise return the task envelope and emit a `creative_review` completion webhook. + +```typescript +// Format declaration +listCreativeFormats: async () => ({ + formats: [{ + format_id: { agent_url: AGENT_URL, id: 'audio_ad_30s' }, + name: 'Audio Ad — 30s', + type: 'audio' as const, + renders: [{ role: 'primary', duration_seconds: 30 }], + assets: [ + { asset_id: 'script', asset_type: 'text', required: true, item_type: 'individual', description: 'Ad script (~70-75 words for a 30s read)' }, + { asset_id: 'voice', asset_type: 'text', required: false, item_type: 'individual', description: 'TTS voice name (e.g. "sara", "isaac")' }, + { asset_id: 'music_template', asset_type: 'text', required: false, item_type: 'individual', description: 'Music-bed template; omit for voice-only' }, + ], + }], +}), + +// Handler — inline manifest in, rendered audio out +import { buildCreativeResponse, audioAsset } from '@adcp/client/server'; + +buildCreative: async (params) => { + const inputManifest = params.creative_manifest; // already inline — no lookup + const targetFid = params.target_format_id ?? inputManifest.format_id; + + // Read inputs from the inline manifest's assets + const script = inputManifest.assets.script?.content ?? ''; + const voice = inputManifest.assets.voice?.content; + const musicTemplate = inputManifest.assets.music_template?.content; + + // Platform pipeline (script → speech → mix). Wrap in a task worker if long-running. + const rendered = await renderAudio({ script, voice, musicTemplate }); + + return buildCreativeResponse({ + creative_manifest: { + format_id: targetFid, + assets: { + audio: audioAsset({ + url: rendered.url, + container_format: 'mp3', + codec: 'mp3', + duration_ms: rendered.durationMs, + channels: 'stereo', + sampling_rate_hz: 44100, + }), + }, + }, + sandbox: params.account?.sandbox === true, + }); +}, +``` + +Common trap — returning platform-native fields (`{ tag_url, creative_id, media_type }`) at the top level instead of wrapping in `creative_manifest`. The wire schema rejects it; `buildCreativeResponse` catches it at compile time. + ### creative-generative Storyboard: `creative_generative`. Takes a brief (`message`) and brand reference (`brand.domain`), generates finished assets. From d583d4755bd5d46cda61ec0188db6282f856f0b6 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 23 Apr 2026 13:23:24 -0400 Subject: [PATCH 2/3] feat(cli): warn on removed flags instead of silently ignoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --platform-type was removed from the SDK in 5.1; comply() throws when it's passed programmatically, but the CLI was capturing and dropping it silently. Third-party CI scripts that still pass it believe they're filtering agent selection when they aren't. storyboard run and its comply alias now emit a stderr warning naming the flag, removal version, and migration path. Suppressed under --json. Detection covers both space-separated and equals forms. REMOVED_FLAGS is a single data map for future deprecations. Closes #842. (#841 deferred pending convention RFC — see issue thread.) Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/cli-removed-flag-warnings.md | 19 +++++++ bin/adcp.js | 29 ++++++++++ test/lib/cli-removed-flags.test.js | 71 +++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 .changeset/cli-removed-flag-warnings.md create mode 100644 test/lib/cli-removed-flags.test.js diff --git a/.changeset/cli-removed-flag-warnings.md b/.changeset/cli-removed-flag-warnings.md new file mode 100644 index 000000000..dc201880c --- /dev/null +++ b/.changeset/cli-removed-flag-warnings.md @@ -0,0 +1,19 @@ +--- +'@adcp/client': patch +--- + +cli: warn on removed flags instead of silently ignoring + +`--platform-type` was removed from the SDK in 5.1 (`comply()` throws when it's passed programmatically), but the CLI was still capturing and silently dropping the flag. Third-party CI scripts that pass it today believe they're filtering agent selection when they aren't. + +`adcp storyboard run` (and its `adcp comply` deprecated alias) now emits a stderr warning naming the flag, the version it was removed in, and the migration path: + +``` +[warn] --platform-type was removed in 5.1.0 and is being ignored. +Agent selection is now driven by get_adcp_capabilities (supported_protocols + specialisms). +Pass --storyboards to target a specific bundle. +``` + +Non-breaking — execution continues. Warnings are suppressed under `--json` to keep stdout as pure JSON. Detection covers both space-separated (`--platform-type value`) and equals (`--platform-type=value`) forms. + +The `REMOVED_FLAGS` map in `bin/adcp.js` is a single location to extend as we deprecate additional flags. diff --git a/bin/adcp.js b/bin/adcp.js index e31206fc7..0c0560886 100755 --- a/bin/adcp.js +++ b/bin/adcp.js @@ -1354,10 +1354,39 @@ async function handleStoryboardShow(args) { } } +// Flags that used to be accepted but have been removed from the SDK/CLI. +// Silent-ignore hides real divergence between author intent and runtime behavior +// (observed in third-party CI scripts that still pass `--platform-type`). +// Each entry: { since: version-string, hint: what-to-do-instead }. +const REMOVED_FLAGS = { + '--platform-type': { + since: '5.1.0', + hint: 'Agent selection is now driven by get_adcp_capabilities (supported_protocols + specialisms). Pass --storyboards to target a specific bundle.', + }, +}; + +// Scan `args` for removed flags and emit a stderr warning for each one found. +// Returns an array of `{ flag, since, hint }` for callers that want to include +// the notices in `--json` output. Non-breaking — execution continues. +function warnRemovedFlags(args, { jsonOutput } = {}) { + const found = []; + for (const [flag, meta] of Object.entries(REMOVED_FLAGS)) { + if (args.includes(flag) || args.some(a => a.startsWith(`${flag}=`))) { + found.push({ flag, since: meta.since, hint: meta.hint }); + if (!jsonOutput) { + console.error(`[warn] ${flag} was removed in ${meta.since} and is being ignored. ${meta.hint}`); + } + } + } + return found; +} + async function handleStoryboardRun(args) { const opts = parseAgentOptions(args); const { authToken, protocolFlag, jsonOutput, dryRun, positionalArgs, file: filePath, localAgent, format } = opts; + warnRemovedFlags(args, { jsonOutput }); + // --local-agent : spin the agent up in-process, seed fixtures, // run storyboards, tear down. Collapses the 300-line seller-side // bootstrap into one command. See `runAgainstLocalAgent` in diff --git a/test/lib/cli-removed-flags.test.js b/test/lib/cli-removed-flags.test.js new file mode 100644 index 000000000..e486bb39f --- /dev/null +++ b/test/lib/cli-removed-flags.test.js @@ -0,0 +1,71 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const { spawnSync } = require('node:child_process'); +const path = require('node:path'); + +const CLI = path.resolve(__dirname, '../../bin/adcp.js'); + +function runCli(args) { + return spawnSync('node', [CLI, ...args], { encoding: 'utf8' }); +} + +test('--platform-type emits a deprecation warning on stderr under storyboard run', () => { + const result = runCli([ + 'storyboard', + 'run', + 'test-mcp', + '--platform-type', + 'creative_transformer', + '--dry-run', + 'capability_discovery', + ]); + assert.match( + result.stderr, + /\[warn\] --platform-type was removed in 5\.1\.0/, + `expected removed-flag warning on stderr, got: ${result.stderr}` + ); + assert.match(result.stderr, /get_adcp_capabilities/); +}); + +test('--platform-type warning is suppressed under --json so stdout stays pure JSON', () => { + const result = runCli([ + 'storyboard', + 'run', + 'test-mcp', + '--platform-type', + 'creative_transformer', + '--dry-run', + '--json', + 'capability_discovery', + ]); + assert.doesNotMatch(result.stderr, /\[warn\] --platform-type/); +}); + +test('--platform-type=value form is also detected', () => { + const result = runCli([ + 'storyboard', + 'run', + 'test-mcp', + '--platform-type=creative_transformer', + '--dry-run', + 'capability_discovery', + ]); + assert.match(result.stderr, /\[warn\] --platform-type was removed/); +}); + +test('adcp comply (deprecated alias) still surfaces removed-flag warnings', () => { + const result = runCli([ + 'comply', + 'test-mcp', + '--platform-type', + 'creative_transformer', + '--dry-run', + 'capability_discovery', + ]); + assert.match(result.stderr, /\[warn\] --platform-type was removed/); +}); + +test('no warning when --platform-type is absent', () => { + const result = runCli(['storyboard', 'run', 'test-mcp', '--dry-run', 'capability_discovery']); + assert.doesNotMatch(result.stderr, /removed in 5\.1\.0/); +}); From bdbfded284051edfca489d1ebf04962f04fcb0d7 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 23 Apr 2026 13:57:49 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20address=20expert=20review=20?= =?UTF-8?q?=E2=80=94=20network-free=20tests,=20stderr=20under=20--json,=20?= =?UTF-8?q?stale-gotcha=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses three reviewer findings on PR #844: - tests hit live network: CLI tests passed --dry-run + storyboard id, but runFullAssessment doesn't short-circuit on --dry-run. Rewrote the suite to call with no agent arg so handleStoryboardRun exits at the Usage check (exit 2) before any network call. Added 10s spawnSync timeouts so a future regression fails fast instead of hanging. Added a test that asserts the warning is advisory (doesn't alter exit status). - --json suppressed the stderr warning: wrong contract. stderr doesn't pollute stdout JSON, and CI scripts running under --json are precisely where the warning needs to land. Removed the jsonOutput guard; flipped the test accordingly. - stale-gotcha guard: a tool rename would silently drop the "Watch out:" block from llms.txt. Generator now fails with a named error if any TOOL_GOTCHAS key doesn't resolve to a tool in the schema index. Also: - Prefix changed from "[warn]" to "DEPRECATED:" for consistency with the existing comply-alias deprecation banner at bin/adcp.js:995. - Skill: split long cross-cutting-pitfalls bullet into shape-rules + asset-factory-rules. Split audio code block into format-decl and handler, so the import isn't visually nested inside a listCreativeFormats literal. Added third delta note ("inputs are text assets keyed by asset_id — read .content not .text") and a pointer to the webhooks section for async render pipelines. Fixed pre-existing TextAsset shape doc (was "{ text: string }", schema says { content: string }). Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/adcp.js | 21 ++++---- scripts/generate-agent-docs.ts | 12 +++++ skills/build-creative-agent/SKILL.md | 20 +++++--- test/lib/cli-removed-flags.test.js | 74 ++++++++++++---------------- 4 files changed, 68 insertions(+), 59 deletions(-) diff --git a/bin/adcp.js b/bin/adcp.js index 0c0560886..b6b1f825b 100755 --- a/bin/adcp.js +++ b/bin/adcp.js @@ -1358,6 +1358,11 @@ async function handleStoryboardShow(args) { // Silent-ignore hides real divergence between author intent and runtime behavior // (observed in third-party CI scripts that still pass `--platform-type`). // Each entry: { since: version-string, hint: what-to-do-instead }. +// +// NOTE: `parseAgentOptions` still captures `--platform-type`'s value (line ~602) +// solely so the value doesn't leak into `positionalArgs`. That capture + this +// warning are both necessary — removing the capture would make a bare +// `--platform-type X` turn `X` into an agent alias lookup. const REMOVED_FLAGS = { '--platform-type': { since: '5.1.0', @@ -1366,26 +1371,22 @@ const REMOVED_FLAGS = { }; // Scan `args` for removed flags and emit a stderr warning for each one found. -// Returns an array of `{ flag, since, hint }` for callers that want to include -// the notices in `--json` output. Non-breaking — execution continues. -function warnRemovedFlags(args, { jsonOutput } = {}) { - const found = []; +// Writes to stderr unconditionally — stderr does not corrupt `--json` stdout, +// and the CI logs where these warnings need to land capture both streams. +// Non-breaking — execution continues. +function warnRemovedFlags(args) { for (const [flag, meta] of Object.entries(REMOVED_FLAGS)) { if (args.includes(flag) || args.some(a => a.startsWith(`${flag}=`))) { - found.push({ flag, since: meta.since, hint: meta.hint }); - if (!jsonOutput) { - console.error(`[warn] ${flag} was removed in ${meta.since} and is being ignored. ${meta.hint}`); - } + console.error(`DEPRECATED: ${flag} was removed in ${meta.since} and is being ignored. ${meta.hint}`); } } - return found; } async function handleStoryboardRun(args) { const opts = parseAgentOptions(args); const { authToken, protocolFlag, jsonOutput, dryRun, positionalArgs, file: filePath, localAgent, format } = opts; - warnRemovedFlags(args, { jsonOutput }); + warnRemovedFlags(args); // --local-agent : spin the agent up in-process, seed fixtures, // run storyboards, tear down. Collapses the 300-line seller-side diff --git a/scripts/generate-agent-docs.ts b/scripts/generate-agent-docs.ts index 89c997049..2e52039de 100644 --- a/scripts/generate-agent-docs.ts +++ b/scripts/generate-agent-docs.ts @@ -1208,6 +1208,18 @@ function main() { `Found ${tools.length} tools, ${errorCodes.length} error codes, ${storyboards.length} storyboards, ${scenarios.length} test scenarios` ); + // Fail loudly if TOOL_GOTCHAS grows stale. A tool rename would otherwise + // silently drop its "Watch out:" block from llms.txt with no CI signal. + const knownToolNames = new Set(tools.map(t => t.name)); + const orphanGotchas = Object.keys(TOOL_GOTCHAS).filter(name => !knownToolNames.has(name)); + if (orphanGotchas.length > 0) { + console.error( + `ERROR: TOOL_GOTCHAS references unknown tool(s): ${orphanGotchas.join(', ')}. ` + + `A tool was renamed or removed — update TOOL_GOTCHAS in scripts/generate-agent-docs.ts.` + ); + process.exit(1); + } + const llmsTxt = generateLlmsTxt(index, tools, errorCodes, storyboards, scenarios); const typeSummary = generateTypeSummary(index, tools); diff --git a/skills/build-creative-agent/SKILL.md b/skills/build-creative-agent/SKILL.md index 751e70823..aff2a1093 100644 --- a/skills/build-creative-agent/SKILL.md +++ b/skills/build-creative-agent/SKILL.md @@ -93,7 +93,8 @@ What happens when a creative is synced: > **Cross-cutting pitfalls matrix runs keep catching:** > > - `capabilities.specialisms` is `string[]` of enum ids (e.g. `['creative-ad-server']`), NOT `[{id, version}]` objects. -> - `build_creative` response is `{ creative_manifest: { format_id, assets } }` (single) or `{ creative_manifests: [...] }` (multi). Platform-native fields at the top level (`tag_url`, `creative_id`, `media_type`) are **invalid** — use `buildCreativeResponse({ creative_manifest })` / `buildCreativeMultiResponse({ creative_manifests })` from `@adcp/client/server` to lock the shape at compile time. Each asset in `creative_manifest.assets` requires an `asset_type` discriminator — use the typed factories (`imageAsset({...})`, `videoAsset({...})`, `audioAsset({...})`, `htmlAsset({...})`, `urlAsset({...})`) so the discriminator is injected for you; a plain `{ serving_tag: { content: '...' } }` fails validation. +> - `build_creative` response is `{ creative_manifest: { format_id, assets } }` (single) or `{ creative_manifests: [...] }` (multi). Platform-native fields at the top level (`tag_url`, `creative_id`, `media_type`) are **invalid** — use `buildCreativeResponse({ creative_manifest })` / `buildCreativeMultiResponse({ creative_manifests })` from `@adcp/client/server` to lock the shape at compile time. +> - Each asset in `creative_manifest.assets` requires an `asset_type` discriminator. Use the typed factories (`imageAsset`, `videoAsset`, `audioAsset`, `htmlAsset`, `urlAsset`, `textAsset`) so the discriminator is injected for you; a plain `{ serving_tag: { content: '...' } }` fails validation. > - `preview_creative` renders have the same pattern — each `renders[]` entry is a oneOf on `output_format`. Use `urlRender({...})`, `htmlRender({...})`, or `bothRender({...})` to inject the discriminator and require the matching `preview_url` / `preview_html` field automatically. > - `get_creative_delivery` requires **top-level `currency: string`** (ISO 4217), in addition to any per-row spend fields. `reporting_period/start` and `/end` are ISO 8601 **date-time** strings (`new Date().toISOString()`), not date-only. > - `videoAsset({...})` requires `width` + `height` per GA (previously optional). Set realistic pixel values — `{ url, width: 1920, height: 1080 }`. @@ -115,7 +116,7 @@ Asset values use type-specific shapes, not a generic `asset_type` discriminator: - Video: `{ url: string, duration_ms: number, format: string }` - Audio: `{ url: string, container_format: string, codec: string, duration_ms: number, channels?: string, sampling_rate_hz?: number }` - HTML: `{ content: string }` (not `{ html: string }`) -- Text: `{ text: string }` +- Text: `{ content: string }` (not `{ text: string }` — the field is `content`, same as HTML) ### Context and Ext Passthrough @@ -622,13 +623,15 @@ Output can be HTML (`{ content: '
...
' }`), JavaScript tag (`{ content #### Audio creative-template (TTS / mix / master) -Audio creative agents (AudioStack, ElevenLabs, Resemble) fit the `creative-template` archetype — stateless transform from an inline manifest to a rendered audio file. Two things differ from display: +Audio creative agents (AudioStack, ElevenLabs, Resemble) fit the `creative-template` archetype — stateless transform from an inline manifest to a rendered audio file. Three things differ from display: 1. **No width/height.** Declare `renders: [{ role: 'primary', duration_seconds: N }]` — the `renders` array is still required by the `discover_formats` storyboard validation, but audio formats surface duration instead of dimensions. -2. **Async render pipelines.** TTS → mix → master is typically minutes long. Don't block the `build_creative` call waiting for the pipeline; the platform-native SDK (AudioStack's 300s poll window, etc.) belongs inside a task worker. If the platform's API returns quickly, build synchronously; otherwise return the task envelope and emit a `creative_review` completion webhook. +2. **Async render pipelines.** TTS → mix → master is typically minutes long. Don't block the `build_creative` call waiting for the pipeline; the platform-native SDK (AudioStack's 300s poll window, etc.) belongs inside a task worker. If the platform's API returns quickly, build synchronously; otherwise return the task envelope and emit a `creative_review` completion webhook (see the [Webhooks](#webhooks-for-async-review-pipelines) section above for the wiring). +3. **Inputs are text assets keyed by `asset_id`.** The buyer sends `creative_manifest.assets.script` (a `TextAsset` with `content: string`) — read `inputManifest.assets.script?.content`, not `.text`. + +Format declaration: ```typescript -// Format declaration listCreativeFormats: async () => ({ formats: [{ format_id: { agent_url: AGENT_URL, id: 'audio_ad_30s' }, @@ -642,15 +645,18 @@ listCreativeFormats: async () => ({ ], }], }), +``` -// Handler — inline manifest in, rendered audio out +Handler — inline manifest in, rendered audio out: + +```typescript import { buildCreativeResponse, audioAsset } from '@adcp/client/server'; buildCreative: async (params) => { const inputManifest = params.creative_manifest; // already inline — no lookup const targetFid = params.target_format_id ?? inputManifest.format_id; - // Read inputs from the inline manifest's assets + // Read inputs from the inline manifest's assets (TextAsset.content, not .text) const script = inputManifest.assets.script?.content ?? ''; const voice = inputManifest.assets.voice?.content; const musicTemplate = inputManifest.assets.music_template?.content; diff --git a/test/lib/cli-removed-flags.test.js b/test/lib/cli-removed-flags.test.js index e486bb39f..a7f9acf7f 100644 --- a/test/lib/cli-removed-flags.test.js +++ b/test/lib/cli-removed-flags.test.js @@ -5,67 +5,57 @@ const path = require('node:path'); const CLI = path.resolve(__dirname, '../../bin/adcp.js'); +// Every spawn gets a timeout so a regression that accidentally reaches a live +// agent path (runFullAssessment doesn't honor --dry-run) fails fast instead of +// hanging CI. function runCli(args) { - return spawnSync('node', [CLI, ...args], { encoding: 'utf8' }); + return spawnSync('node', [CLI, ...args], { encoding: 'utf8', timeout: 10_000 }); } +// Call the command with no agent arg so `handleStoryboardRun` exits at the +// "Usage:" check (exit 2). `warnRemovedFlags` fires at the top of the handler, +// before that check — so we get the warning without any network call. + test('--platform-type emits a deprecation warning on stderr under storyboard run', () => { - const result = runCli([ - 'storyboard', - 'run', - 'test-mcp', - '--platform-type', - 'creative_transformer', - '--dry-run', - 'capability_discovery', - ]); + const result = runCli(['storyboard', 'run', '--platform-type', 'creative_transformer']); + assert.strictEqual(result.status, 2, `expected exit 2 (usage), got ${result.status}. stderr: ${result.stderr}`); assert.match( result.stderr, - /\[warn\] --platform-type was removed in 5\.1\.0/, + /DEPRECATED: --platform-type was removed in 5\.1\.0/, `expected removed-flag warning on stderr, got: ${result.stderr}` ); assert.match(result.stderr, /get_adcp_capabilities/); }); -test('--platform-type warning is suppressed under --json so stdout stays pure JSON', () => { - const result = runCli([ - 'storyboard', - 'run', - 'test-mcp', - '--platform-type', - 'creative_transformer', - '--dry-run', - '--json', - 'capability_discovery', - ]); - assert.doesNotMatch(result.stderr, /\[warn\] --platform-type/); +test('--platform-type warning still reaches stderr under --json (stdout stays pure JSON)', () => { + const result = runCli(['storyboard', 'run', '--platform-type', 'creative_transformer', '--json']); + // Warning must reach stderr so CI log streams capture it. stderr never + // pollutes stdout JSON, so --json is not a reason to suppress. + assert.match(result.stderr, /DEPRECATED: --platform-type was removed/); }); test('--platform-type=value form is also detected', () => { - const result = runCli([ - 'storyboard', - 'run', - 'test-mcp', - '--platform-type=creative_transformer', - '--dry-run', - 'capability_discovery', - ]); - assert.match(result.stderr, /\[warn\] --platform-type was removed/); + const result = runCli(['storyboard', 'run', '--platform-type=creative_transformer']); + assert.match(result.stderr, /DEPRECATED: --platform-type was removed/); }); test('adcp comply (deprecated alias) still surfaces removed-flag warnings', () => { - const result = runCli([ - 'comply', - 'test-mcp', - '--platform-type', - 'creative_transformer', - '--dry-run', - 'capability_discovery', - ]); - assert.match(result.stderr, /\[warn\] --platform-type was removed/); + const result = runCli(['comply', '--platform-type', 'creative_transformer']); + assert.match(result.stderr, /DEPRECATED: --platform-type was removed/); }); test('no warning when --platform-type is absent', () => { - const result = runCli(['storyboard', 'run', 'test-mcp', '--dry-run', 'capability_discovery']); + const result = runCli(['storyboard', 'run']); assert.doesNotMatch(result.stderr, /removed in 5\.1\.0/); }); + +test('warning is advisory — exit status reflects the real command outcome, not the warning', () => { + // No agent arg → exit 2 (usage). Adding --platform-type must not change that. + const withFlag = runCli(['storyboard', 'run', '--platform-type', 'creative_transformer']); + const withoutFlag = runCli(['storyboard', 'run']); + assert.strictEqual( + withFlag.status, + withoutFlag.status, + 'removed-flag warning must not alter exit status — it is advisory' + ); +});