Skip to content

spec(schemas): PascalCase titles on error-details schemas (partial fix for #3145)#3566

Merged
bokelley merged 2 commits intomainfrom
bokelley/3145-screaming-snake-rename
Apr 29, 2026
Merged

spec(schemas): PascalCase titles on error-details schemas (partial fix for #3145)#3566
bokelley merged 2 commits intomainfrom
bokelley/3145-screaming-snake-rename

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Partial fix for #3145. Six `static/schemas/source/error-details/*.json` files carried SCREAMING_SNAKE titles (`ACCOUNT_SETUP_REQUIRED Details`, etc.) that propagate awkwardly through `json-schema-to-typescript` into `@adcp/client`'s public type surface — names like `RATE_LIMITEDDetails_ScopeValues` read as typos to every consumer.

Changes

File Old title New title
account-setup-required.json ACCOUNT_SETUP_REQUIRED Details AccountSetupRequiredDetails
audience-too-small.json AUDIENCE_TOO_SMALL Details AudienceTooSmallDetails
budget-too-low.json BUDGET_TOO_LOW Details BudgetTooLowDetails
conflict.json CONFLICT Details ConflictDetails
creative-rejected.json CREATIVE_REJECTED Details CreativeRejectedDetails
policy-violation.json POLICY_VIOLATION Details PolicyViolationDetails

rate-limited.json already had PascalCase (`Rate Limited Details`); `vendor-error-codes.json` already had `Vendor Error Code Registry`. No change to either.

Wire-format unchanged: `$id` values stay kebab-case (`/schemas/error-details/account-setup-required.json`). Only the `title` field is touched, which controls codegen output. No producer/consumer needs to change anything.

Why `--empty` changeset

Title-only changes have no schema-shape impact and don't affect the wire. Counts as schema metadata cleanup.

What this PR doesn't fix

#3145's other half — `Foo1`-suffixed enum dupes (`AgeVerificationMethod1`, `BriefAsset1`, `VASTAsset1`, `DAASTAsset1`, `CatalogAsset1`) — is downstream codegen behavior in `json-schema-to-typescript` reaching the same enum through multiple parent paths. The shared `$ref` is already in place upstream (verified — both `targeting.json` and `get-adcp-capabilities-response.json` reach `/schemas/enums/age-verification-method.json` via `$ref` to the same `$id`), so the dedup needs SDK-side post-process renaming with one-minor-version aliases. Tracked at adcp-client#942 — out of scope for this PR.

Test plan

  • npm run test:schemas — 7/7 passing
  • npm run test:json-schema — 255/255 passing
  • node scripts/build-compliance.cjs — clean

🤖 Generated with Claude Code

Partial fix for #3145. Six error-details/*.json files carry SCREAMING_SNAKE
titles that propagate awkwardly through json-schema-to-typescript into
@adcp/client's public type surface (e.g., RATE_LIMITEDDetails_ScopeValues
read as a typo to every consumer of the SDK).

Renames (no wire-format change — $id values stay kebab-case; only the
title field is touched, which controls codegen):

- ACCOUNT_SETUP_REQUIRED Details -> AccountSetupRequiredDetails
- AUDIENCE_TOO_SMALL Details     -> AudienceTooSmallDetails
- BUDGET_TOO_LOW Details         -> BudgetTooLowDetails
- CONFLICT Details               -> ConflictDetails
- CREATIVE_REJECTED Details      -> CreativeRejectedDetails
- POLICY_VIOLATION Details       -> PolicyViolationDetails

rate-limited.json already had PascalCase (Rate Limited Details);
vendor-error-codes.json already had Vendor Error Code Registry; no change
to either.

#3145's other half — Foo1-suffixed enum dupes (AgeVerificationMethod1,
*Asset1, etc.) — is downstream codegen behavior. Both refs already point
at the same $id with $ref so the spec side is correct; the dupe shows up
because json-schema-to-typescript walks through different parent paths
and emits two inline copies. SDK-side post-process renaming (with one-
minor-version aliases) is the pragmatic fix. Tracked at adcp-client#942.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255,
build-compliance 20 universal / 6 protocols / 19 specialisms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley added this to the 3.1.0 milestone Apr 29, 2026
Protocol-expert review of #3566 flagged the style inconsistency: my
PR stripped spaces (AccountSetupRequiredDetails), but PR #3149 (Apr 25)
established the precedent of spaced Title Case for the same kind of
problem (Rate Limited Details). Going with #3149's form so the 8-file
directory stays uniform. json-schema-to-typescript strips whitespace
when generating identifiers, so the codegen output is identical either
way (AccountSetupRequiredDetails on both); the source-of-truth title
just stays consistent across the directory.

Title field is non-normative per JSON Schema draft-07 §10.1 — affects
only docgen/codegen output, not validation. Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

Sharpened per protocol-expert review (commit `80287a625`):

The expert verified everything but flagged one real finding — style inconsistency with #3149. My PR stripped spaces (`AccountSetupRequiredDetails`); #3149 (merged Apr 25 for the same kind of problem on `rate-limited.json`) used `Rate Limited Details` with spaces. After my PR landed, the 8-file directory would have read 6 no-spaces / 1 with-spaces / 1 Title Case — visibly inconsistent.

Switched to spaced form to match #3149's precedent:

  • AccountSetupRequiredDetailsAccount Setup Required Details
  • (etc. for all 6)

`json-schema-to-typescript` strips whitespace when generating identifiers, so codegen output is the same either way. The change keeps source-of-truth titles uniform across the directory.

Other expert findings (all confirmed sound):

  • ✅ Wire-unchanged claim TRUE — diff inspection confirms title-only changes; `$id`, `$schema`, properties, required, additionalProperties all untouched
  • ✅ Downstream codegen risk LOW — Pydantic, quicktype, Mintlify all key off `title` for naming and fall back to `$id`/path; none use `title` for cross-file identity
  • ✅ Empty changeset CORRECT — direct precedent at `.changeset/fix-rate-limited-schema-title.md` (PR fix(schema): correct title annotation in rate-limited error-details schema #3149)
  • ✅ Rename batch COMPLETE within error-details/ — grep across `static/schemas/source/**/*.json` shows zero other SCREAMING_SNAKE titles
  • ✅ Foo1 enum-dedup correctly diagnosed as SDK-side codegen issue (separate adcp-client follow-up after this lands)

Schema validators clean (test:schemas 7/7, test:json-schema 255/255).

@bokelley bokelley merged commit a5d9bff into main Apr 29, 2026
13 checks passed
@bokelley bokelley deleted the bokelley/3145-screaming-snake-rename branch April 29, 2026 23:59
bokelley added a commit that referenced this pull request May 2, 2026
* fix(schema): correct title annotation in rate-limited error-details schema (#3149)

* fix(schema): correct title annotation in rate-limited error-details schema

Change "RATE_LIMITED Details" to "Rate Limited Details" in the `title`
annotation of error-details/rate-limited.json. The title field is
non-normative per JSON Schema draft-07 (no validation or wire-format
impact); this corrects the downstream codegen output from
`RATE_LIMITEDDetails` to `RateLimitedDetails`. Applied to source and
all dist snapshots (3.0.0, 3.0.0-rc.3, latest).

Refs #3145. See adcp-client#942 for the SDK alias layer.

https://claude.ai/code/session_01E3LcN5g4tEZutKCTePUVbs

* revert: drop dist/schemas/ hand-edits per #3149 review

dist/schemas/3.0.0/ and dist/schemas/3.0.0-rc.3/ are immutable release
snapshots — scripts/build-schemas.cjs only writes them under --release
mode (the changesets release step), and dist/schemas/latest/ is
gitignored. Mutating frozen GA snapshots breaks the immutability
contract that lets buyers pin to 3.0.0 and trust they see exactly what
was published.

Source title fix is preserved. The next --release build picks up the
corrected title for that version's snapshot; past releases stay
byte-identical to what was published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
(cherry picked from commit fbf71ce)

* spec(error): standardize VALIDATION_ERROR issues[] on core/error.json (closes #3059) (#3562)

* spec(error): standardize VALIDATION_ERROR issues[] (closes #3059)

Adds an optional top-level issues array to core/error.json, normalizing
what @adcp/client already emits for multi-field validation rejections
(adcp-client#874 / #915). Other implementations (adcp-go,
adcp-client-python, hand-rolled sellers) would either miss the structured
pointer list, adopt it ad-hoc with different naming, or converge if the
spec normalizes it. Filing now keeps the ecosystem aligned before
adoption deepens.

Each issue entry: { pointer (RFC 6901), message, keyword, schemaPath? }.
schemaPath MAY be omitted in production to avoid fingerprinting oneOf
branch selection on adversarial payloads.

Backward compatibility:
- field (singular) is retained. When both are present, sellers SHOULD
  set field to issues[0].pointer for pre-3.1 consumers reading field
  only.
- details.issues mirror is permitted for consumers reading from details.
  New consumers should prefer top-level issues.

Files:
- static/schemas/source/core/error.json: adds issues property
- docs/building/implementation/error-handling.mdx: adds issues to the
  error-envelope field table; documents field/issues interaction

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(error): apply protocol-expert review feedback on issues[]

Three substantive sharpens from the ad-tech-protocol-expert review of
PR #3562:

1. Pointer-format mismatch with field — flagged as a latent bug. The
   existing top-level field uses JSONPath-lite (packages[0].targeting);
   the new issues[].pointer uses RFC 6901 (/packages/0/targeting).
   Calling the mirror rule SHOULD without specifying the translation
   left sellers with collision risk. Both descriptions now spell out
   the format choice (Ajv's instancePath = RFC 6901 for pointer; legacy
   JSONPath-lite for field) AND the explicit translation contract on
   the mirror. Future major version will deprecate field in favor of
   issues[].pointer.

2. issues[0].pointer mirror rule — SHOULD upgraded to MUST (when issues
   is present). SHOULD created exactly the rough edge the review
   flagged: pre-3.1 consumers reading field would get nondeterministic
   behavior across sellers. Cost of MUST is one line of dual-write per
   seller; cost of SHOULD is a long tail of seller-A-vs-seller-B bugs.
   MUST also gives a clean deprecation path in 4.0.

3. schemaPath downgraded from MAY to SHOULD NOT in production. The
   review identified this as a real probe oracle: leaking which oneOf
   branch the validator selected before semantic rejection helps
   adversarial callers map polymorphic unions. AdCP already has an
   adversarial-payload threat model (signed-requests work, agent-
   controlled field audit). Sellers MAY emit in dev/sandbox modes.

Also cited Ajv as prior art so implementers know where the keyword
vocabulary comes from (instancePath / keyword / schemaPath are Ajv's
native error output fields). Reduces the ad-hoc-naming risk.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit bd3a18c)

* spec(schemas): PascalCase titles on error-details schemas (partial fix for #3145) (#3566)

* spec(schemas): PascalCase titles on error-details schemas

Partial fix for #3145. Six error-details/*.json files carry SCREAMING_SNAKE
titles that propagate awkwardly through json-schema-to-typescript into
@adcp/client's public type surface (e.g., RATE_LIMITEDDetails_ScopeValues
read as a typo to every consumer of the SDK).

Renames (no wire-format change — $id values stay kebab-case; only the
title field is touched, which controls codegen):

- ACCOUNT_SETUP_REQUIRED Details -> AccountSetupRequiredDetails
- AUDIENCE_TOO_SMALL Details     -> AudienceTooSmallDetails
- BUDGET_TOO_LOW Details         -> BudgetTooLowDetails
- CONFLICT Details               -> ConflictDetails
- CREATIVE_REJECTED Details      -> CreativeRejectedDetails
- POLICY_VIOLATION Details       -> PolicyViolationDetails

rate-limited.json already had PascalCase (Rate Limited Details);
vendor-error-codes.json already had Vendor Error Code Registry; no change
to either.

#3145's other half — Foo1-suffixed enum dupes (AgeVerificationMethod1,
*Asset1, etc.) — is downstream codegen behavior. Both refs already point
at the same $id with $ref so the spec side is correct; the dupe shows up
because json-schema-to-typescript walks through different parent paths
and emits two inline copies. SDK-side post-process renaming (with one-
minor-version aliases) is the pragmatic fix. Tracked at adcp-client#942.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255,
build-compliance 20 universal / 6 protocols / 19 specialisms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(schemas): Title Case (with spaces) to match #3149 precedent

Protocol-expert review of #3566 flagged the style inconsistency: my
PR stripped spaces (AccountSetupRequiredDetails), but PR #3149 (Apr 25)
established the precedent of spaced Title Case for the same kind of
problem (Rate Limited Details). Going with #3149's form so the 8-file
directory stays uniform. json-schema-to-typescript strips whitespace
when generating identifiers, so the codegen output is identical either
way (AccountSetupRequiredDetails on both); the source-of-truth title
just stays consistent across the directory.

Title field is non-normative per JSON Schema draft-07 §10.1 — affects
only docgen/codegen output, not validation. Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit a5d9bff)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2) (#3671)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2)

Define what receivers do when a URL asset omits url_type. Today the
field is optional with no fallback rule, so a conformant manifest like
{asset_type: url, url: ...} forces buyers to guess the invocation
mechanism — and guessing wrong (firing a clickthrough URL as a pixel,
or a tracker as a clickthrough) corrupts measurement and breaks user
flows.

Schema changes:
- url-asset.json: senders SHOULD include url_type on every URL asset.
  When absent, receivers SHOULD fall back to the format's
  url-asset-requirements.role (clickthrough/landing_page →
  `clickthrough` mechanism; *_tracker roles → `tracker_pixel`). When
  neither is available, receivers MAY reject rather than guess.
- url-asset-requirements.json: clarify that role is purpose
  (impression vs click vs viewability vs 3P) while url_type is
  mechanism (click vs pixel vs script tag); a click_tracker slot
  validly accepts a tracker_pixel URL.

Doc changes:
- asset-types.mdx URL Asset section: rewritten to use the actual
  url_type enum (clickthrough/tracker_pixel/tracker_script — the old
  text listed impression_tracker/video_tracker/landing_page, which
  were never url_type values), to add the SHOULD note and role
  fallback table, and to remove the "you only need to supply the
  url value" guidance that drove the original ambiguity.

Wire format unchanged. Senders already including url_type are
unaffected. Step 2 of the rollout on adcp#2986; step 3 (require
url_type in 4.0) follows once this lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(url-asset): apply expert review — viewability→script, third_party→unmapped, MUST NOT silently guess

Addresses ad-tech-protocol-expert + adtech-product-expert review on
PR #3671:

Required fixes:
- viewability_tracker → tracker_script (not tracker_pixel). OMID and
  equivalent verification SDKs require a <script> tag; firing them as
  a pixel produces no measurement and no error — exactly the silent-
  corruption failure mode this PR exists to prevent.
- third_party_tracker → no safe fallback. Mechanism is integration-
  specific (DV/IAS ship both pixel and script forms). Receivers MAY
  reject or warn rather than guess.
- Strengthen receiver guidance to "MUST NOT silently pick a
  mechanism; SHOULD reject" when both url_type and role are absent.
  Mirrors the mdx language into the JSON Schema description so
  extractors and conformance tooling read the same rule.
- Add VAST/DAAST carve-out: VAST tag URLs are not URL assets; use
  asset_type: "vast" or the dedicated tracker types pending RFC #2915.
- Update docs/creative/formats.mdx tracker-detection rule. Today it
  feature-detects on url_type ∈ {tracker_pixel, tracker_script};
  under the new fallback semantics, format authors who declare only
  role would silently fail that detector. Detection now accepts
  either url_type OR a tracker-purpose role.

Non-blocking improvements:
- Migration cue in asset-types.mdx for sellers who built tooling
  around the older "you only need to supply the url value" guidance:
  3.x is fine, plan to add url_type before 4.0.

Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(url-asset): cross-reference fallback table between schema and mdx

The role→url_type fallback table lives in two places: the
url-asset.json top-level description (read by conformance tools and
codegen) and the asset-types.mdx URL Asset section (read by humans).
Without a hint, an editor of one will silently drift the other.

Adds a $comment in url-asset.json and a JSX comment in asset-types.mdx
pointing at each other. Schema description remains the normative
source; mdx is the human copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit f6af651)

* fix(compliance): audience-sync discover_account stateful: false → true (#3710)

The list_accounts step in the account_setup phase establishes account_id
for downstream sync_audiences calls. The storyboard narrative was correct
but the stateful flag contradicted it, causing the SDK runner to not count
a passing result as cascade state — explicit-mode adopters saw
prerequisite_failed on sync_audiences even after list_accounts passed.

Fixes #3707

https://claude.ai/code/session_019ZJXyeQYrRZc17UqcW2yDf

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit e74997b)

* chore(changesets): downgrade cherry-picked minor bumps to patch for 3.0.x line

Maintenance-line policy: cherry-picks land as patches even when the
original PRs landed on main as minor bumps. Otherwise the changesets
trigger 3.1.0 publication off the 3.0.x branch, defeating the cherry-pick.

- error-issues-array.md (#3562, originally minor): patch
- url-type-should-and-role-fallback.md (#3671, originally minor): patch

Both PRs were classified as patch-eligible in #3784.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schema): manifest.json + structured enumMetadata (closes #3725) (#3738)

* feat(schema): publish manifest.json + structured enumMetadata (adcp#3725)

Adds two additive artifacts so SDKs stop hand-rolling (and drifting on)
spec metadata. Root cause for adcp-client#1135 (17 missing error codes,
3 wrong recovery classifications shipped in TS SDK for over a year).

- enums/error-code.json gains an enumMetadata block with structured
  recovery + suggestion per code. Build-time lint rejects drift between
  the structured value and the prose Recovery: X in enumDescriptions.

- New static/schemas/source/manifest.schema.json + generator emitting
  dist/schemas/{version}/manifest.json: 58 tools, 48 error codes, 19
  specialisms, plus an error_code_policy block defining how SDKs MUST
  classify codes from non-conforming sellers.

- mutating derived from the same classifier the idempotency-key lint
  enforces (single source of truth). Tightened READ_ONLY_VERB_PATTERN
  to anchor at start so create-collection-list / delete-property-list
  no longer mis-classify as read-only via -list- mid-name; added search
  as a read-only verb.

- Specialisms expose entry_point_tools (curated minimum from
  index.yaml.required_tools) and exercised_tools (full surface — union
  of own phases[].steps[].task and every linked scenario, derived by
  walking requires_scenarios). sales_guaranteed now correctly lists 9
  tools instead of 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(manifest): expert-review fixes — strip regex, requires_tool gating

Two correctness fixes from protocol-expert review:

- stripRecoveryProse: greedy [^.]*\. truncated descriptions with periods
  inside parentheticals (e.g. "capabilities.media_buy.limits"). Rewrote
  with three explicit patterns: bare verdict, verdict + balanced
  parenthetical, clause continuation to EOS. Verified all 48 codes emit
  clean descriptions with no Recovery: prose remaining.

- collectTasksFromPhases now skips steps gated by requires_tool. Steps
  marked requires_tool: <X> are conditional on the agent claiming X, not
  required surface. Without the skip, optional test-harness tools
  (comply_test_controller, gated across 23+ steps) propagated into
  every sales specialism's exercised_tools.

Plus code-review nits:

- Simplified discoverTools utility-shape skip to NON_OPERATION_ALLOWLIST
  only; documented the contract.
- Removed unused schema parameter from classifyRequestMutating.
- Tightened indexScenarioTasks predicate to require phases array.
- Added cross-reference comment between MANIFEST_PROTOCOLS and the
  meta-schema's protocol enum.
- Changeset now mentions /schemas/latest/manifest.json for nightly
  codegen consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit b44996f)

* fix(3.0.x): trim enumMetadata to 3.0.x codes; downgrade #3738 to patch

The cherry-pick of #3738 brought in enumDescriptions + enumMetadata entries
for SCOPE_INSUFFICIENT, READ_ONLY_SCOPE, and FIELD_NOT_PERMITTED — three
error codes added to main in PRs that aren't on 3.0.x. Their enum entries
weren't introduced (3.0.x's enum array is unaffected), but the description
and metadata blocks were left referencing them. The new lintErrorCodeEnumMetadata
guardrail catches this and refuses to build.

Trim: drop the three orphan entries from enumDescriptions and enumMetadata.
Counts now agree at 45 / 45 / 45.

Also downgrades the changeset from minor to patch — same rationale as the
other cherry-picks on this branch: maintenance line, no new enum values,
no wire-format change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(errors): tighten AUTH_REQUIRED prose to warn on retry storms (3.0.x prose-only backport of #3739)

3.0.x cannot adopt the AUTH_MISSING/AUTH_INVALID split from #3739 — adding
new enum values violates the maintenance line's semver rules. This is the
prose-only backport: same wire code, same recovery class, but the
description and enumMetadata.suggestion now spell out the two sub-cases
(missing vs. presented-but-rejected) and the SHOULD-NOT-auto-retry rule.

Closes 3.0.x portion of #3730.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
bokelley added a commit that referenced this pull request May 2, 2026
…#3806)

* chore(3.0.x): re-cherry-pick #3461 and #3462 (envelope_field_present + asset-union) (#3608)

* feat(compliance): add envelope_field_present check type; update v3-envelope-integrity storyboard (#3461)

Closes #3429

Adds `envelope_field_present` as a recognized storyboard check type that
walks protocol-envelope.json instead of the step's response_schema_ref.
Updates v3-envelope-integrity.yaml to use it for the `status` presence
assertion, eliminating the VERIFIER_UNREACHABLE gap in the adcp-client
storyboard-drift verifier. Requires adcp-client#1045 for runtime + static
drift support.

Non-breaking justification: additive — new check type alongside existing
ones; no existing check type changed or removed; no storyboard currently
uses envelope_field_present.

Pre-PR review:
- code-reviewer: approved — lint change correct, test added for classifyOutcome, dist/3.0.1 frozen by design (ships as 3.0.2), no blockers
- ad-tech-protocol-expert: approved — semantics correct; runner-output-contract.yaml updated with check enum + expected/actual shape entries; patch bump confirmed correct per playbook conformance-harness rule

https://claude.ai/code/session_01Kwks2uGQS3ZVX7g4kujdGp

Co-authored-by: Claude <noreply@anthropic.com>

* fix(schema): promote asset-variant oneOf to canonical asset-union.json (#3462)

* fix(schema): promote asset-variant oneOf to canonical asset-union.json

Fixes json-schema-to-typescript emitting VASTAsset1, DAASTAsset1,
BriefAsset1, and CatalogAsset1 when creative-asset.json and
creative-manifest.json both inline identical 14-arm oneOf arrays.

Creates core/assets/asset-union.json (title: AssetVariant) as a single
$id-addressable source of truth. Both parent schemas now $ref this file
instead of duplicating the union inline. Wire format and validation
semantics are unchanged; the discriminator annotation moves inside the
canonical schema per OAS 3.1 §4.8.24.

Closes #3459

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

* docs(schema): document intentional subset in offering-asset-group.json

Clarify that offering-asset-group excludes brief-asset and catalog-asset
(campaign-input metadata, not delivery-ready creative types) and cross-link
to the new asset-union.json for the full union. Addresses code-reviewer
nit from pre-PR review.

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

* fix(schema): replace inline asset oneOf in list-creatives-response.json

Third copy of the 14-arm asset union, caught in post-PR code review.
Replace with $ref to asset-union.json for consistency.

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

---------

Co-authored-by: Claude <noreply@anthropic.com>

* chore(3.0.x): sync release-pipeline workflows from main (App token, 3.0.x triggers)

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Version Packages (#3615)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs(creative-channels): fix url_type tracker → tracker_pixel (#2986) (#3673)

The display, audio, carousels, and DOOH channel docs use
"url_type": "tracker", which is not a valid value in the
url-asset-type.json enum (clickthrough / tracker_pixel /
tracker_script). Sellers following these docs emit a non-conformant
url_type that buyers can't interpret without guessing.

Replaces all 10 occurrences with "tracker_pixel" to match the schema.
This is step 1 of the rollout proposed by Nastassia Fulconis on
adcp#2986: 3.0.x docs cleanup → 3.1 SHOULD + role-based fallback +
mechanism-vs-purpose clarification → 4.0 required.

The dist/docs/3.0.2 release snapshot still carries the old value;
backporting to the snapshot is intentionally out of scope here so the
fix lands on the live source first.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(storyboard): provides_state_for cascade-rescue field (closes #3734) (#3775)

Backport of e8fded9 from main to 3.0.x. Adds optional same-phase
substitution declaration on storyboard steps so explicit-mode social
platforms (Snap, Meta, TikTok) can pre-provision accounts out-of-band
and have list_accounts substitute for the missing sync_accounts state
contract — recovering 3 Tier-1 sandbox-verified adapters from 1/9/0 to
9/10 once the SDK cache refreshes against this version.

Storyboard schema (static/compliance/source/universal/storyboard-schema.yaml):
field documentation parallel to contributes_to. All-of array semantics,
same-phase only, target/substitute must be stateful, no self-reference,
acyclic peer-graph per phase.

Runner output contract (static/compliance/source/universal/
runner-output-contract.yaml): new peer_substituted skip reason in
skip_result.reasons, distinct from peer_branch_taken (branch-set routing)
and not_applicable (coverage gap).

Specialism YAML (static/compliance/source/specialisms/sales-social/
index.yaml): provides_state_for: sync_accounts on list_accounts in the
account_setup phase.

Build-time validation (scripts/lint-storyboard-provides-state-for.cjs +
test): wired into build-compliance.cjs lint chain. Covers shape,
self-reference, unknown target, cross-phase, target-stateful,
substitute-stateful, and direct-cycle violations.

Pure additive change; existing storyboards keep current cascade behavior.

Cherry-pick conflict resolved in package.json: kept 3.0.x's simpler
node --test invocation (no --test-force-exit / --test-timeout flags),
slotted in test:storyboard-provides-state-for entry consistent with
3.0.x style. --no-verify used because precommit fails on a pre-existing
3.0.x baseline @adcp/client install drift unrelated to this change.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3696)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* ci(release): auto-upload protocol tarball to GitHub Release (#3786) (#3787)

`createGithubReleases: true` from changesets/action only writes the
changelog body — files have to be uploaded separately. Without this
step the artifacts ship to the repo's dist/ tree but are never attached
as Release assets, leaving adopters who pin via release URL with 404s.

v3.0.0 had assets only because they were uploaded by hand on 2026-04-22;
v3.0.1 / v3.0.2 / v3.0.3 all shipped empty.

New step runs after changesets/action, gated on
`steps.changesets.outputs.published == 'true'` so it only fires on
tag-and-release runs (not Version Packages PR-creation runs). Uploads
${VERSION}.tgz plus .sha256 / .sig / .crt sidecars with --clobber so
re-runs are idempotent.

Backfill of the three missing releases (v3.0.1 / v3.0.2 / v3.0.3) is a
separate manual step against the assets already committed at
dist/protocol/ on the 3.0.x branch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(3.0.x): cherry-pick + adapt 7 spec fixes from main (#3784) (#3789)

* fix(schema): correct title annotation in rate-limited error-details schema (#3149)

* fix(schema): correct title annotation in rate-limited error-details schema

Change "RATE_LIMITED Details" to "Rate Limited Details" in the `title`
annotation of error-details/rate-limited.json. The title field is
non-normative per JSON Schema draft-07 (no validation or wire-format
impact); this corrects the downstream codegen output from
`RATE_LIMITEDDetails` to `RateLimitedDetails`. Applied to source and
all dist snapshots (3.0.0, 3.0.0-rc.3, latest).

Refs #3145. See adcp-client#942 for the SDK alias layer.

https://claude.ai/code/session_01E3LcN5g4tEZutKCTePUVbs

* revert: drop dist/schemas/ hand-edits per #3149 review

dist/schemas/3.0.0/ and dist/schemas/3.0.0-rc.3/ are immutable release
snapshots — scripts/build-schemas.cjs only writes them under --release
mode (the changesets release step), and dist/schemas/latest/ is
gitignored. Mutating frozen GA snapshots breaks the immutability
contract that lets buyers pin to 3.0.0 and trust they see exactly what
was published.

Source title fix is preserved. The next --release build picks up the
corrected title for that version's snapshot; past releases stay
byte-identical to what was published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
(cherry picked from commit fbf71ce)

* spec(error): standardize VALIDATION_ERROR issues[] on core/error.json (closes #3059) (#3562)

* spec(error): standardize VALIDATION_ERROR issues[] (closes #3059)

Adds an optional top-level issues array to core/error.json, normalizing
what @adcp/client already emits for multi-field validation rejections
(adcp-client#874 / #915). Other implementations (adcp-go,
adcp-client-python, hand-rolled sellers) would either miss the structured
pointer list, adopt it ad-hoc with different naming, or converge if the
spec normalizes it. Filing now keeps the ecosystem aligned before
adoption deepens.

Each issue entry: { pointer (RFC 6901), message, keyword, schemaPath? }.
schemaPath MAY be omitted in production to avoid fingerprinting oneOf
branch selection on adversarial payloads.

Backward compatibility:
- field (singular) is retained. When both are present, sellers SHOULD
  set field to issues[0].pointer for pre-3.1 consumers reading field
  only.
- details.issues mirror is permitted for consumers reading from details.
  New consumers should prefer top-level issues.

Files:
- static/schemas/source/core/error.json: adds issues property
- docs/building/implementation/error-handling.mdx: adds issues to the
  error-envelope field table; documents field/issues interaction

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(error): apply protocol-expert review feedback on issues[]

Three substantive sharpens from the ad-tech-protocol-expert review of
PR #3562:

1. Pointer-format mismatch with field — flagged as a latent bug. The
   existing top-level field uses JSONPath-lite (packages[0].targeting);
   the new issues[].pointer uses RFC 6901 (/packages/0/targeting).
   Calling the mirror rule SHOULD without specifying the translation
   left sellers with collision risk. Both descriptions now spell out
   the format choice (Ajv's instancePath = RFC 6901 for pointer; legacy
   JSONPath-lite for field) AND the explicit translation contract on
   the mirror. Future major version will deprecate field in favor of
   issues[].pointer.

2. issues[0].pointer mirror rule — SHOULD upgraded to MUST (when issues
   is present). SHOULD created exactly the rough edge the review
   flagged: pre-3.1 consumers reading field would get nondeterministic
   behavior across sellers. Cost of MUST is one line of dual-write per
   seller; cost of SHOULD is a long tail of seller-A-vs-seller-B bugs.
   MUST also gives a clean deprecation path in 4.0.

3. schemaPath downgraded from MAY to SHOULD NOT in production. The
   review identified this as a real probe oracle: leaking which oneOf
   branch the validator selected before semantic rejection helps
   adversarial callers map polymorphic unions. AdCP already has an
   adversarial-payload threat model (signed-requests work, agent-
   controlled field audit). Sellers MAY emit in dev/sandbox modes.

Also cited Ajv as prior art so implementers know where the keyword
vocabulary comes from (instancePath / keyword / schemaPath are Ajv's
native error output fields). Reduces the ad-hoc-naming risk.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit bd3a18c)

* spec(schemas): PascalCase titles on error-details schemas (partial fix for #3145) (#3566)

* spec(schemas): PascalCase titles on error-details schemas

Partial fix for #3145. Six error-details/*.json files carry SCREAMING_SNAKE
titles that propagate awkwardly through json-schema-to-typescript into
@adcp/client's public type surface (e.g., RATE_LIMITEDDetails_ScopeValues
read as a typo to every consumer of the SDK).

Renames (no wire-format change — $id values stay kebab-case; only the
title field is touched, which controls codegen):

- ACCOUNT_SETUP_REQUIRED Details -> AccountSetupRequiredDetails
- AUDIENCE_TOO_SMALL Details     -> AudienceTooSmallDetails
- BUDGET_TOO_LOW Details         -> BudgetTooLowDetails
- CONFLICT Details               -> ConflictDetails
- CREATIVE_REJECTED Details      -> CreativeRejectedDetails
- POLICY_VIOLATION Details       -> PolicyViolationDetails

rate-limited.json already had PascalCase (Rate Limited Details);
vendor-error-codes.json already had Vendor Error Code Registry; no change
to either.

#3145's other half — Foo1-suffixed enum dupes (AgeVerificationMethod1,
*Asset1, etc.) — is downstream codegen behavior. Both refs already point
at the same $id with $ref so the spec side is correct; the dupe shows up
because json-schema-to-typescript walks through different parent paths
and emits two inline copies. SDK-side post-process renaming (with one-
minor-version aliases) is the pragmatic fix. Tracked at adcp-client#942.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255,
build-compliance 20 universal / 6 protocols / 19 specialisms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(schemas): Title Case (with spaces) to match #3149 precedent

Protocol-expert review of #3566 flagged the style inconsistency: my
PR stripped spaces (AccountSetupRequiredDetails), but PR #3149 (Apr 25)
established the precedent of spaced Title Case for the same kind of
problem (Rate Limited Details). Going with #3149's form so the 8-file
directory stays uniform. json-schema-to-typescript strips whitespace
when generating identifiers, so the codegen output is identical either
way (AccountSetupRequiredDetails on both); the source-of-truth title
just stays consistent across the directory.

Title field is non-normative per JSON Schema draft-07 §10.1 — affects
only docgen/codegen output, not validation. Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit a5d9bff)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2) (#3671)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2)

Define what receivers do when a URL asset omits url_type. Today the
field is optional with no fallback rule, so a conformant manifest like
{asset_type: url, url: ...} forces buyers to guess the invocation
mechanism — and guessing wrong (firing a clickthrough URL as a pixel,
or a tracker as a clickthrough) corrupts measurement and breaks user
flows.

Schema changes:
- url-asset.json: senders SHOULD include url_type on every URL asset.
  When absent, receivers SHOULD fall back to the format's
  url-asset-requirements.role (clickthrough/landing_page →
  `clickthrough` mechanism; *_tracker roles → `tracker_pixel`). When
  neither is available, receivers MAY reject rather than guess.
- url-asset-requirements.json: clarify that role is purpose
  (impression vs click vs viewability vs 3P) while url_type is
  mechanism (click vs pixel vs script tag); a click_tracker slot
  validly accepts a tracker_pixel URL.

Doc changes:
- asset-types.mdx URL Asset section: rewritten to use the actual
  url_type enum (clickthrough/tracker_pixel/tracker_script — the old
  text listed impression_tracker/video_tracker/landing_page, which
  were never url_type values), to add the SHOULD note and role
  fallback table, and to remove the "you only need to supply the
  url value" guidance that drove the original ambiguity.

Wire format unchanged. Senders already including url_type are
unaffected. Step 2 of the rollout on adcp#2986; step 3 (require
url_type in 4.0) follows once this lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(url-asset): apply expert review — viewability→script, third_party→unmapped, MUST NOT silently guess

Addresses ad-tech-protocol-expert + adtech-product-expert review on
PR #3671:

Required fixes:
- viewability_tracker → tracker_script (not tracker_pixel). OMID and
  equivalent verification SDKs require a <script> tag; firing them as
  a pixel produces no measurement and no error — exactly the silent-
  corruption failure mode this PR exists to prevent.
- third_party_tracker → no safe fallback. Mechanism is integration-
  specific (DV/IAS ship both pixel and script forms). Receivers MAY
  reject or warn rather than guess.
- Strengthen receiver guidance to "MUST NOT silently pick a
  mechanism; SHOULD reject" when both url_type and role are absent.
  Mirrors the mdx language into the JSON Schema description so
  extractors and conformance tooling read the same rule.
- Add VAST/DAAST carve-out: VAST tag URLs are not URL assets; use
  asset_type: "vast" or the dedicated tracker types pending RFC #2915.
- Update docs/creative/formats.mdx tracker-detection rule. Today it
  feature-detects on url_type ∈ {tracker_pixel, tracker_script};
  under the new fallback semantics, format authors who declare only
  role would silently fail that detector. Detection now accepts
  either url_type OR a tracker-purpose role.

Non-blocking improvements:
- Migration cue in asset-types.mdx for sellers who built tooling
  around the older "you only need to supply the url value" guidance:
  3.x is fine, plan to add url_type before 4.0.

Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(url-asset): cross-reference fallback table between schema and mdx

The role→url_type fallback table lives in two places: the
url-asset.json top-level description (read by conformance tools and
codegen) and the asset-types.mdx URL Asset section (read by humans).
Without a hint, an editor of one will silently drift the other.

Adds a $comment in url-asset.json and a JSX comment in asset-types.mdx
pointing at each other. Schema description remains the normative
source; mdx is the human copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit f6af651)

* fix(compliance): audience-sync discover_account stateful: false → true (#3710)

The list_accounts step in the account_setup phase establishes account_id
for downstream sync_audiences calls. The storyboard narrative was correct
but the stateful flag contradicted it, causing the SDK runner to not count
a passing result as cascade state — explicit-mode adopters saw
prerequisite_failed on sync_audiences even after list_accounts passed.

Fixes #3707

https://claude.ai/code/session_019ZJXyeQYrRZc17UqcW2yDf

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit e74997b)

* chore(changesets): downgrade cherry-picked minor bumps to patch for 3.0.x line

Maintenance-line policy: cherry-picks land as patches even when the
original PRs landed on main as minor bumps. Otherwise the changesets
trigger 3.1.0 publication off the 3.0.x branch, defeating the cherry-pick.

- error-issues-array.md (#3562, originally minor): patch
- url-type-should-and-role-fallback.md (#3671, originally minor): patch

Both PRs were classified as patch-eligible in #3784.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schema): manifest.json + structured enumMetadata (closes #3725) (#3738)

* feat(schema): publish manifest.json + structured enumMetadata (adcp#3725)

Adds two additive artifacts so SDKs stop hand-rolling (and drifting on)
spec metadata. Root cause for adcp-client#1135 (17 missing error codes,
3 wrong recovery classifications shipped in TS SDK for over a year).

- enums/error-code.json gains an enumMetadata block with structured
  recovery + suggestion per code. Build-time lint rejects drift between
  the structured value and the prose Recovery: X in enumDescriptions.

- New static/schemas/source/manifest.schema.json + generator emitting
  dist/schemas/{version}/manifest.json: 58 tools, 48 error codes, 19
  specialisms, plus an error_code_policy block defining how SDKs MUST
  classify codes from non-conforming sellers.

- mutating derived from the same classifier the idempotency-key lint
  enforces (single source of truth). Tightened READ_ONLY_VERB_PATTERN
  to anchor at start so create-collection-list / delete-property-list
  no longer mis-classify as read-only via -list- mid-name; added search
  as a read-only verb.

- Specialisms expose entry_point_tools (curated minimum from
  index.yaml.required_tools) and exercised_tools (full surface — union
  of own phases[].steps[].task and every linked scenario, derived by
  walking requires_scenarios). sales_guaranteed now correctly lists 9
  tools instead of 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(manifest): expert-review fixes — strip regex, requires_tool gating

Two correctness fixes from protocol-expert review:

- stripRecoveryProse: greedy [^.]*\. truncated descriptions with periods
  inside parentheticals (e.g. "capabilities.media_buy.limits"). Rewrote
  with three explicit patterns: bare verdict, verdict + balanced
  parenthetical, clause continuation to EOS. Verified all 48 codes emit
  clean descriptions with no Recovery: prose remaining.

- collectTasksFromPhases now skips steps gated by requires_tool. Steps
  marked requires_tool: <X> are conditional on the agent claiming X, not
  required surface. Without the skip, optional test-harness tools
  (comply_test_controller, gated across 23+ steps) propagated into
  every sales specialism's exercised_tools.

Plus code-review nits:

- Simplified discoverTools utility-shape skip to NON_OPERATION_ALLOWLIST
  only; documented the contract.
- Removed unused schema parameter from classifyRequestMutating.
- Tightened indexScenarioTasks predicate to require phases array.
- Added cross-reference comment between MANIFEST_PROTOCOLS and the
  meta-schema's protocol enum.
- Changeset now mentions /schemas/latest/manifest.json for nightly
  codegen consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit b44996f)

* fix(3.0.x): trim enumMetadata to 3.0.x codes; downgrade #3738 to patch

The cherry-pick of #3738 brought in enumDescriptions + enumMetadata entries
for SCOPE_INSUFFICIENT, READ_ONLY_SCOPE, and FIELD_NOT_PERMITTED — three
error codes added to main in PRs that aren't on 3.0.x. Their enum entries
weren't introduced (3.0.x's enum array is unaffected), but the description
and metadata blocks were left referencing them. The new lintErrorCodeEnumMetadata
guardrail catches this and refuses to build.

Trim: drop the three orphan entries from enumDescriptions and enumMetadata.
Counts now agree at 45 / 45 / 45.

Also downgrades the changeset from minor to patch — same rationale as the
other cherry-picks on this branch: maintenance line, no new enum values,
no wire-format change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(errors): tighten AUTH_REQUIRED prose to warn on retry storms (3.0.x prose-only backport of #3739)

3.0.x cannot adopt the AUTH_MISSING/AUTH_INVALID split from #3739 — adding
new enum values violates the maintenance line's semver rules. This is the
prose-only backport: same wire code, same recovery class, but the
description and enumMetadata.suggestion now spell out the two sub-cases
(missing vs. presented-but-rejected) and the SHOULD-NOT-auto-retry rule.

Closes 3.0.x portion of #3730.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>

* ci(release): un-exclude dist/compliance + forward-merge auto-resolution (#3794) (#3800)

* ci(release): un-exclude dist/compliance + forward-merge auto-resolution

Two release-pipeline hardening fixes for 3.0.4 and beyond.

.dockerignore: un-exclude dist/compliance/ so versioned URLs (the
/compliance/{version}/ tree) actually serve from the Fly image. The
ignore pattern was set up for /schemas/ and /protocol/ but never updated
when /compliance/ versioned routing was added. Result: every committed
dist/compliance/3.0.X/ directory was stripped from the build context,
and SDKs fetching compliance bundles by URL hit 404s on fresh-cache
scenarios. Re-include dist/compliance, then re-exclude
dist/compliance/latest (regenerated in-container by build:compliance).

forward-merge-3.0.yml: replace the bare-merge approach with auto-
resolution on an explicit allowlist of always-divergent paths
(package.json version, .changeset/*.md, dist/*, CHANGELOG.md, schema
source index files). Drop the brittle is-ancestor shortcut that
returns false after squash-merges even when content is in main; use
post-merge git diff --quiet to skip cleanly when main already has
3.0.x's content. Conflicts outside the allowlist fail the workflow
loud, surfacing playbook violations (a change on 3.0.x that wasn't
first cherry-picked from main).

Updates .agents/playbook.md § Release lines to document the new
auto-resolution behavior so reviewers know what to spot-check.

Closes the operational pain that bit us today on PR #3783, where the
v3.0.3 cut's forward-merge needed three manual conflict resolutions
(package.json, .changeset/fix-url-type-tracker-pixel-channel-docs.md,
static/schemas/source/index.json) plus a manual unshallow before
the merge could complete.



* ci(release): address expert review feedback on forward-merge auto-resolution

Code review (af5969...): empty-CONFLICTS-after-merge-failure guard so
hook rejections / unrelated-history failures fail loud instead of
silently committing the empty merge.

Security review (a16289...): tighten dist/* glob to explicit
{schemas,compliance,protocol,docs}/* list so future mutable subtrees
under dist/ don't silently auto-resolve via --theirs.

Plus: drop `2>/dev/null || true` on `git rm` (let real errors surface;
the post-loop REMAINING check already guards against bad state). Add
post-resolution `git status --short` and `git diff vs origin/main --
package.json` log groups so reviewers can spot main-unique scripts
that may have been overwritten without leaving GitHub.

No functional change to the happy-path resolution behavior.



---------


# Conflicts:
#	.agents/playbook.md

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(forward-merge): keep main's package.json (--ours) instead of 3.0.x's

The original auto-resolution rule for package.json was --theirs, which
worked when main and 3.0.x diverged only on the version field. But main
has structural changes (package rename: @adcp/client@5.21.1 →
@adcp/sdk@5.25.1, plus undici@7 dep) that 3.0.x doesn't have. Wholesale
--theirs stripped main's package rename and broke npm ci.

Fix: take main's package.json + package-lock.json. Main's pre-mode
tracking is independent of 3.0.x's version anyway (main's next cut goes
3.1.0-beta.0 from accumulated changesets, regardless of starting
version). The version field doesn't NEED to propagate.

Follow-up: update the auto-resolution rule in
.github/workflows/forward-merge-3.0.yml to use --ours for
package.json + package-lock.json rather than --theirs. That changes the
workflow's behavior for all future forward-merges and prevents this
class of breakage.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: aao-release-bot[bot] <280565558+aao-release-bot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
bokelley added a commit that referenced this pull request May 2, 2026
* chore(3.0.x): re-cherry-pick #3461 and #3462 (envelope_field_present + asset-union) (#3608)

* feat(compliance): add envelope_field_present check type; update v3-envelope-integrity storyboard (#3461)

Closes #3429

Adds `envelope_field_present` as a recognized storyboard check type that
walks protocol-envelope.json instead of the step's response_schema_ref.
Updates v3-envelope-integrity.yaml to use it for the `status` presence
assertion, eliminating the VERIFIER_UNREACHABLE gap in the adcp-client
storyboard-drift verifier. Requires adcp-client#1045 for runtime + static
drift support.

Non-breaking justification: additive — new check type alongside existing
ones; no existing check type changed or removed; no storyboard currently
uses envelope_field_present.

Pre-PR review:
- code-reviewer: approved — lint change correct, test added for classifyOutcome, dist/3.0.1 frozen by design (ships as 3.0.2), no blockers
- ad-tech-protocol-expert: approved — semantics correct; runner-output-contract.yaml updated with check enum + expected/actual shape entries; patch bump confirmed correct per playbook conformance-harness rule

https://claude.ai/code/session_01Kwks2uGQS3ZVX7g4kujdGp

Co-authored-by: Claude <noreply@anthropic.com>

* fix(schema): promote asset-variant oneOf to canonical asset-union.json (#3462)

* fix(schema): promote asset-variant oneOf to canonical asset-union.json

Fixes json-schema-to-typescript emitting VASTAsset1, DAASTAsset1,
BriefAsset1, and CatalogAsset1 when creative-asset.json and
creative-manifest.json both inline identical 14-arm oneOf arrays.

Creates core/assets/asset-union.json (title: AssetVariant) as a single
$id-addressable source of truth. Both parent schemas now $ref this file
instead of duplicating the union inline. Wire format and validation
semantics are unchanged; the discriminator annotation moves inside the
canonical schema per OAS 3.1 §4.8.24.

Closes #3459

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

* docs(schema): document intentional subset in offering-asset-group.json

Clarify that offering-asset-group excludes brief-asset and catalog-asset
(campaign-input metadata, not delivery-ready creative types) and cross-link
to the new asset-union.json for the full union. Addresses code-reviewer
nit from pre-PR review.

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

* fix(schema): replace inline asset oneOf in list-creatives-response.json

Third copy of the 14-arm asset union, caught in post-PR code review.
Replace with $ref to asset-union.json for consistency.

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

---------

Co-authored-by: Claude <noreply@anthropic.com>

* chore(3.0.x): sync release-pipeline workflows from main (App token, 3.0.x triggers)

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Version Packages (#3615)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs(creative-channels): fix url_type tracker → tracker_pixel (#2986) (#3673)

The display, audio, carousels, and DOOH channel docs use
"url_type": "tracker", which is not a valid value in the
url-asset-type.json enum (clickthrough / tracker_pixel /
tracker_script). Sellers following these docs emit a non-conformant
url_type that buyers can't interpret without guessing.

Replaces all 10 occurrences with "tracker_pixel" to match the schema.
This is step 1 of the rollout proposed by Nastassia Fulconis on
adcp#2986: 3.0.x docs cleanup → 3.1 SHOULD + role-based fallback +
mechanism-vs-purpose clarification → 4.0 required.

The dist/docs/3.0.2 release snapshot still carries the old value;
backporting to the snapshot is intentionally out of scope here so the
fix lands on the live source first.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(storyboard): provides_state_for cascade-rescue field (closes #3734) (#3775)

Backport of e8fded9 from main to 3.0.x. Adds optional same-phase
substitution declaration on storyboard steps so explicit-mode social
platforms (Snap, Meta, TikTok) can pre-provision accounts out-of-band
and have list_accounts substitute for the missing sync_accounts state
contract — recovering 3 Tier-1 sandbox-verified adapters from 1/9/0 to
9/10 once the SDK cache refreshes against this version.

Storyboard schema (static/compliance/source/universal/storyboard-schema.yaml):
field documentation parallel to contributes_to. All-of array semantics,
same-phase only, target/substitute must be stateful, no self-reference,
acyclic peer-graph per phase.

Runner output contract (static/compliance/source/universal/
runner-output-contract.yaml): new peer_substituted skip reason in
skip_result.reasons, distinct from peer_branch_taken (branch-set routing)
and not_applicable (coverage gap).

Specialism YAML (static/compliance/source/specialisms/sales-social/
index.yaml): provides_state_for: sync_accounts on list_accounts in the
account_setup phase.

Build-time validation (scripts/lint-storyboard-provides-state-for.cjs +
test): wired into build-compliance.cjs lint chain. Covers shape,
self-reference, unknown target, cross-phase, target-stateful,
substitute-stateful, and direct-cycle violations.

Pure additive change; existing storyboards keep current cascade behavior.

Cherry-pick conflict resolved in package.json: kept 3.0.x's simpler
node --test invocation (no --test-force-exit / --test-timeout flags),
slotted in test:storyboard-provides-state-for entry consistent with
3.0.x style. --no-verify used because precommit fails on a pre-existing
3.0.x baseline @adcp/client install drift unrelated to this change.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3696)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* ci(release): auto-upload protocol tarball to GitHub Release (#3786) (#3787)

`createGithubReleases: true` from changesets/action only writes the
changelog body — files have to be uploaded separately. Without this
step the artifacts ship to the repo's dist/ tree but are never attached
as Release assets, leaving adopters who pin via release URL with 404s.

v3.0.0 had assets only because they were uploaded by hand on 2026-04-22;
v3.0.1 / v3.0.2 / v3.0.3 all shipped empty.

New step runs after changesets/action, gated on
`steps.changesets.outputs.published == 'true'` so it only fires on
tag-and-release runs (not Version Packages PR-creation runs). Uploads
${VERSION}.tgz plus .sha256 / .sig / .crt sidecars with --clobber so
re-runs are idempotent.

Backfill of the three missing releases (v3.0.1 / v3.0.2 / v3.0.3) is a
separate manual step against the assets already committed at
dist/protocol/ on the 3.0.x branch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(3.0.x): cherry-pick + adapt 7 spec fixes from main (#3784) (#3789)

* fix(schema): correct title annotation in rate-limited error-details schema (#3149)

* fix(schema): correct title annotation in rate-limited error-details schema

Change "RATE_LIMITED Details" to "Rate Limited Details" in the `title`
annotation of error-details/rate-limited.json. The title field is
non-normative per JSON Schema draft-07 (no validation or wire-format
impact); this corrects the downstream codegen output from
`RATE_LIMITEDDetails` to `RateLimitedDetails`. Applied to source and
all dist snapshots (3.0.0, 3.0.0-rc.3, latest).

Refs #3145. See adcp-client#942 for the SDK alias layer.

https://claude.ai/code/session_01E3LcN5g4tEZutKCTePUVbs

* revert: drop dist/schemas/ hand-edits per #3149 review

dist/schemas/3.0.0/ and dist/schemas/3.0.0-rc.3/ are immutable release
snapshots — scripts/build-schemas.cjs only writes them under --release
mode (the changesets release step), and dist/schemas/latest/ is
gitignored. Mutating frozen GA snapshots breaks the immutability
contract that lets buyers pin to 3.0.0 and trust they see exactly what
was published.

Source title fix is preserved. The next --release build picks up the
corrected title for that version's snapshot; past releases stay
byte-identical to what was published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
(cherry picked from commit fbf71ce)

* spec(error): standardize VALIDATION_ERROR issues[] on core/error.json (closes #3059) (#3562)

* spec(error): standardize VALIDATION_ERROR issues[] (closes #3059)

Adds an optional top-level issues array to core/error.json, normalizing
what @adcp/client already emits for multi-field validation rejections
(adcp-client#874 / #915). Other implementations (adcp-go,
adcp-client-python, hand-rolled sellers) would either miss the structured
pointer list, adopt it ad-hoc with different naming, or converge if the
spec normalizes it. Filing now keeps the ecosystem aligned before
adoption deepens.

Each issue entry: { pointer (RFC 6901), message, keyword, schemaPath? }.
schemaPath MAY be omitted in production to avoid fingerprinting oneOf
branch selection on adversarial payloads.

Backward compatibility:
- field (singular) is retained. When both are present, sellers SHOULD
  set field to issues[0].pointer for pre-3.1 consumers reading field
  only.
- details.issues mirror is permitted for consumers reading from details.
  New consumers should prefer top-level issues.

Files:
- static/schemas/source/core/error.json: adds issues property
- docs/building/implementation/error-handling.mdx: adds issues to the
  error-envelope field table; documents field/issues interaction

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(error): apply protocol-expert review feedback on issues[]

Three substantive sharpens from the ad-tech-protocol-expert review of
PR #3562:

1. Pointer-format mismatch with field — flagged as a latent bug. The
   existing top-level field uses JSONPath-lite (packages[0].targeting);
   the new issues[].pointer uses RFC 6901 (/packages/0/targeting).
   Calling the mirror rule SHOULD without specifying the translation
   left sellers with collision risk. Both descriptions now spell out
   the format choice (Ajv's instancePath = RFC 6901 for pointer; legacy
   JSONPath-lite for field) AND the explicit translation contract on
   the mirror. Future major version will deprecate field in favor of
   issues[].pointer.

2. issues[0].pointer mirror rule — SHOULD upgraded to MUST (when issues
   is present). SHOULD created exactly the rough edge the review
   flagged: pre-3.1 consumers reading field would get nondeterministic
   behavior across sellers. Cost of MUST is one line of dual-write per
   seller; cost of SHOULD is a long tail of seller-A-vs-seller-B bugs.
   MUST also gives a clean deprecation path in 4.0.

3. schemaPath downgraded from MAY to SHOULD NOT in production. The
   review identified this as a real probe oracle: leaking which oneOf
   branch the validator selected before semantic rejection helps
   adversarial callers map polymorphic unions. AdCP already has an
   adversarial-payload threat model (signed-requests work, agent-
   controlled field audit). Sellers MAY emit in dev/sandbox modes.

Also cited Ajv as prior art so implementers know where the keyword
vocabulary comes from (instancePath / keyword / schemaPath are Ajv's
native error output fields). Reduces the ad-hoc-naming risk.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit bd3a18c)

* spec(schemas): PascalCase titles on error-details schemas (partial fix for #3145) (#3566)

* spec(schemas): PascalCase titles on error-details schemas

Partial fix for #3145. Six error-details/*.json files carry SCREAMING_SNAKE
titles that propagate awkwardly through json-schema-to-typescript into
@adcp/client's public type surface (e.g., RATE_LIMITEDDetails_ScopeValues
read as a typo to every consumer of the SDK).

Renames (no wire-format change — $id values stay kebab-case; only the
title field is touched, which controls codegen):

- ACCOUNT_SETUP_REQUIRED Details -> AccountSetupRequiredDetails
- AUDIENCE_TOO_SMALL Details     -> AudienceTooSmallDetails
- BUDGET_TOO_LOW Details         -> BudgetTooLowDetails
- CONFLICT Details               -> ConflictDetails
- CREATIVE_REJECTED Details      -> CreativeRejectedDetails
- POLICY_VIOLATION Details       -> PolicyViolationDetails

rate-limited.json already had PascalCase (Rate Limited Details);
vendor-error-codes.json already had Vendor Error Code Registry; no change
to either.

#3145's other half — Foo1-suffixed enum dupes (AgeVerificationMethod1,
*Asset1, etc.) — is downstream codegen behavior. Both refs already point
at the same $id with $ref so the spec side is correct; the dupe shows up
because json-schema-to-typescript walks through different parent paths
and emits two inline copies. SDK-side post-process renaming (with one-
minor-version aliases) is the pragmatic fix. Tracked at adcp-client#942.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255,
build-compliance 20 universal / 6 protocols / 19 specialisms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(schemas): Title Case (with spaces) to match #3149 precedent

Protocol-expert review of #3566 flagged the style inconsistency: my
PR stripped spaces (AccountSetupRequiredDetails), but PR #3149 (Apr 25)
established the precedent of spaced Title Case for the same kind of
problem (Rate Limited Details). Going with #3149's form so the 8-file
directory stays uniform. json-schema-to-typescript strips whitespace
when generating identifiers, so the codegen output is identical either
way (AccountSetupRequiredDetails on both); the source-of-truth title
just stays consistent across the directory.

Title field is non-normative per JSON Schema draft-07 §10.1 — affects
only docgen/codegen output, not validation. Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit a5d9bff)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2) (#3671)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2)

Define what receivers do when a URL asset omits url_type. Today the
field is optional with no fallback rule, so a conformant manifest like
{asset_type: url, url: ...} forces buyers to guess the invocation
mechanism — and guessing wrong (firing a clickthrough URL as a pixel,
or a tracker as a clickthrough) corrupts measurement and breaks user
flows.

Schema changes:
- url-asset.json: senders SHOULD include url_type on every URL asset.
  When absent, receivers SHOULD fall back to the format's
  url-asset-requirements.role (clickthrough/landing_page →
  `clickthrough` mechanism; *_tracker roles → `tracker_pixel`). When
  neither is available, receivers MAY reject rather than guess.
- url-asset-requirements.json: clarify that role is purpose
  (impression vs click vs viewability vs 3P) while url_type is
  mechanism (click vs pixel vs script tag); a click_tracker slot
  validly accepts a tracker_pixel URL.

Doc changes:
- asset-types.mdx URL Asset section: rewritten to use the actual
  url_type enum (clickthrough/tracker_pixel/tracker_script — the old
  text listed impression_tracker/video_tracker/landing_page, which
  were never url_type values), to add the SHOULD note and role
  fallback table, and to remove the "you only need to supply the
  url value" guidance that drove the original ambiguity.

Wire format unchanged. Senders already including url_type are
unaffected. Step 2 of the rollout on adcp#2986; step 3 (require
url_type in 4.0) follows once this lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(url-asset): apply expert review — viewability→script, third_party→unmapped, MUST NOT silently guess

Addresses ad-tech-protocol-expert + adtech-product-expert review on
PR #3671:

Required fixes:
- viewability_tracker → tracker_script (not tracker_pixel). OMID and
  equivalent verification SDKs require a <script> tag; firing them as
  a pixel produces no measurement and no error — exactly the silent-
  corruption failure mode this PR exists to prevent.
- third_party_tracker → no safe fallback. Mechanism is integration-
  specific (DV/IAS ship both pixel and script forms). Receivers MAY
  reject or warn rather than guess.
- Strengthen receiver guidance to "MUST NOT silently pick a
  mechanism; SHOULD reject" when both url_type and role are absent.
  Mirrors the mdx language into the JSON Schema description so
  extractors and conformance tooling read the same rule.
- Add VAST/DAAST carve-out: VAST tag URLs are not URL assets; use
  asset_type: "vast" or the dedicated tracker types pending RFC #2915.
- Update docs/creative/formats.mdx tracker-detection rule. Today it
  feature-detects on url_type ∈ {tracker_pixel, tracker_script};
  under the new fallback semantics, format authors who declare only
  role would silently fail that detector. Detection now accepts
  either url_type OR a tracker-purpose role.

Non-blocking improvements:
- Migration cue in asset-types.mdx for sellers who built tooling
  around the older "you only need to supply the url value" guidance:
  3.x is fine, plan to add url_type before 4.0.

Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(url-asset): cross-reference fallback table between schema and mdx

The role→url_type fallback table lives in two places: the
url-asset.json top-level description (read by conformance tools and
codegen) and the asset-types.mdx URL Asset section (read by humans).
Without a hint, an editor of one will silently drift the other.

Adds a $comment in url-asset.json and a JSX comment in asset-types.mdx
pointing at each other. Schema description remains the normative
source; mdx is the human copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit f6af651)

* fix(compliance): audience-sync discover_account stateful: false → true (#3710)

The list_accounts step in the account_setup phase establishes account_id
for downstream sync_audiences calls. The storyboard narrative was correct
but the stateful flag contradicted it, causing the SDK runner to not count
a passing result as cascade state — explicit-mode adopters saw
prerequisite_failed on sync_audiences even after list_accounts passed.

Fixes #3707

https://claude.ai/code/session_019ZJXyeQYrRZc17UqcW2yDf

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit e74997b)

* chore(changesets): downgrade cherry-picked minor bumps to patch for 3.0.x line

Maintenance-line policy: cherry-picks land as patches even when the
original PRs landed on main as minor bumps. Otherwise the changesets
trigger 3.1.0 publication off the 3.0.x branch, defeating the cherry-pick.

- error-issues-array.md (#3562, originally minor): patch
- url-type-should-and-role-fallback.md (#3671, originally minor): patch

Both PRs were classified as patch-eligible in #3784.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schema): manifest.json + structured enumMetadata (closes #3725) (#3738)

* feat(schema): publish manifest.json + structured enumMetadata (adcp#3725)

Adds two additive artifacts so SDKs stop hand-rolling (and drifting on)
spec metadata. Root cause for adcp-client#1135 (17 missing error codes,
3 wrong recovery classifications shipped in TS SDK for over a year).

- enums/error-code.json gains an enumMetadata block with structured
  recovery + suggestion per code. Build-time lint rejects drift between
  the structured value and the prose Recovery: X in enumDescriptions.

- New static/schemas/source/manifest.schema.json + generator emitting
  dist/schemas/{version}/manifest.json: 58 tools, 48 error codes, 19
  specialisms, plus an error_code_policy block defining how SDKs MUST
  classify codes from non-conforming sellers.

- mutating derived from the same classifier the idempotency-key lint
  enforces (single source of truth). Tightened READ_ONLY_VERB_PATTERN
  to anchor at start so create-collection-list / delete-property-list
  no longer mis-classify as read-only via -list- mid-name; added search
  as a read-only verb.

- Specialisms expose entry_point_tools (curated minimum from
  index.yaml.required_tools) and exercised_tools (full surface — union
  of own phases[].steps[].task and every linked scenario, derived by
  walking requires_scenarios). sales_guaranteed now correctly lists 9
  tools instead of 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(manifest): expert-review fixes — strip regex, requires_tool gating

Two correctness fixes from protocol-expert review:

- stripRecoveryProse: greedy [^.]*\. truncated descriptions with periods
  inside parentheticals (e.g. "capabilities.media_buy.limits"). Rewrote
  with three explicit patterns: bare verdict, verdict + balanced
  parenthetical, clause continuation to EOS. Verified all 48 codes emit
  clean descriptions with no Recovery: prose remaining.

- collectTasksFromPhases now skips steps gated by requires_tool. Steps
  marked requires_tool: <X> are conditional on the agent claiming X, not
  required surface. Without the skip, optional test-harness tools
  (comply_test_controller, gated across 23+ steps) propagated into
  every sales specialism's exercised_tools.

Plus code-review nits:

- Simplified discoverTools utility-shape skip to NON_OPERATION_ALLOWLIST
  only; documented the contract.
- Removed unused schema parameter from classifyRequestMutating.
- Tightened indexScenarioTasks predicate to require phases array.
- Added cross-reference comment between MANIFEST_PROTOCOLS and the
  meta-schema's protocol enum.
- Changeset now mentions /schemas/latest/manifest.json for nightly
  codegen consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit b44996f)

* fix(3.0.x): trim enumMetadata to 3.0.x codes; downgrade #3738 to patch

The cherry-pick of #3738 brought in enumDescriptions + enumMetadata entries
for SCOPE_INSUFFICIENT, READ_ONLY_SCOPE, and FIELD_NOT_PERMITTED — three
error codes added to main in PRs that aren't on 3.0.x. Their enum entries
weren't introduced (3.0.x's enum array is unaffected), but the description
and metadata blocks were left referencing them. The new lintErrorCodeEnumMetadata
guardrail catches this and refuses to build.

Trim: drop the three orphan entries from enumDescriptions and enumMetadata.
Counts now agree at 45 / 45 / 45.

Also downgrades the changeset from minor to patch — same rationale as the
other cherry-picks on this branch: maintenance line, no new enum values,
no wire-format change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(errors): tighten AUTH_REQUIRED prose to warn on retry storms (3.0.x prose-only backport of #3739)

3.0.x cannot adopt the AUTH_MISSING/AUTH_INVALID split from #3739 — adding
new enum values violates the maintenance line's semver rules. This is the
prose-only backport: same wire code, same recovery class, but the
description and enumMetadata.suggestion now spell out the two sub-cases
(missing vs. presented-but-rejected) and the SHOULD-NOT-auto-retry rule.

Closes 3.0.x portion of #3730.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>

* ci(release): un-exclude dist/compliance + forward-merge auto-resolution (#3794) (#3800)

* ci(release): un-exclude dist/compliance + forward-merge auto-resolution

Two release-pipeline hardening fixes for 3.0.4 and beyond.

.dockerignore: un-exclude dist/compliance/ so versioned URLs (the
/compliance/{version}/ tree) actually serve from the Fly image. The
ignore pattern was set up for /schemas/ and /protocol/ but never updated
when /compliance/ versioned routing was added. Result: every committed
dist/compliance/3.0.X/ directory was stripped from the build context,
and SDKs fetching compliance bundles by URL hit 404s on fresh-cache
scenarios. Re-include dist/compliance, then re-exclude
dist/compliance/latest (regenerated in-container by build:compliance).

forward-merge-3.0.yml: replace the bare-merge approach with auto-
resolution on an explicit allowlist of always-divergent paths
(package.json version, .changeset/*.md, dist/*, CHANGELOG.md, schema
source index files). Drop the brittle is-ancestor shortcut that
returns false after squash-merges even when content is in main; use
post-merge git diff --quiet to skip cleanly when main already has
3.0.x's content. Conflicts outside the allowlist fail the workflow
loud, surfacing playbook violations (a change on 3.0.x that wasn't
first cherry-picked from main).

Updates .agents/playbook.md § Release lines to document the new
auto-resolution behavior so reviewers know what to spot-check.

Closes the operational pain that bit us today on PR #3783, where the
v3.0.3 cut's forward-merge needed three manual conflict resolutions
(package.json, .changeset/fix-url-type-tracker-pixel-channel-docs.md,
static/schemas/source/index.json) plus a manual unshallow before
the merge could complete.



* ci(release): address expert review feedback on forward-merge auto-resolution

Code review (af5969...): empty-CONFLICTS-after-merge-failure guard so
hook rejections / unrelated-history failures fail loud instead of
silently committing the empty merge.

Security review (a16289...): tighten dist/* glob to explicit
{schemas,compliance,protocol,docs}/* list so future mutable subtrees
under dist/ don't silently auto-resolve via --theirs.

Plus: drop `2>/dev/null || true` on `git rm` (let real errors surface;
the post-loop REMAINING check already guards against bad state). Add
post-resolution `git status --short` and `git diff vs origin/main --
package.json` log groups so reviewers can spot main-unique scripts
that may have been overwritten without leaving GitHub.

No functional change to the happy-path resolution behavior.



---------


# Conflicts:
#	.agents/playbook.md

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(release): forward-merge package.json --ours, not --theirs (#3807) (#3808)

Original --theirs rule stripped main's structural changes when 3.0.x
hadn't been updated to match. Concrete case: main renamed @adcp/client
to @adcp/sdk + bumped to 5.25.1; 3.0.x stayed on @adcp/client@5.21.1.
The forward-merge took 3.0.x's package.json wholesale, leaving the
package-lock out of sync. CI broke with "npm ci ... package.json and
package-lock.json or npm-shrinkwrap.json are in sync". PR #3806's CI
exposed this.

--ours preserves main's state. Main's pre-mode tracking is independent
of 3.0.x's version field; the dist/* artifacts still flow forward via
the allowlist; main's structural changes survive.

Trade-off: main's package.json version doesn't reflect 3.0.x's latest
release. Acceptable — main's version field isn't authoritative while
pre-mode is active. The next main pre-mode cut produces 3.1.0-beta.X
from accumulated changesets regardless of base version.

Companion playbook + PR-body checklist update so docs match behavior.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(release): bridge cherry-pick divergence in forward-merge (#3811) (#3814)

Adds two specific files to the auto-resolution --ours allowlist where
3.0.x has #3789's hand-adapted prose-only backport of #3739 and main
has the full enum split (adds AUTH_MISSING / AUTH_INVALID codes that
can't ship to 3.0.x without violating patch eligibility).

  - docs/building/implementation/error-handling.mdx
  - static/schemas/source/enums/error-code.json

Without this rule, every routine forward-merge from 3.0.x → main
rediscovers the same conflict because squash-merges of prior
forward-merges (the only merge style this repo allows) don't advance
git's merge-base. The post-merge `git diff --quiet` skip can't reach
to detect "main already has this content" because the merge fails
before that step.

Marked temporary in the workflow comments — remove when 3.1.0 cuts
and main no longer has the in-flight enum split.

Without this fix, the next forward-merge after 3.0.4 cuts would
fail loud on these same two files, requiring another manual
resolution PR. With it, 3.0.4's forward-merge auto-succeeds.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3799)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(changeset): empty changeset for 3.0.4 forward-merge

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: aao-release-bot[bot] <280565558+aao-release-bot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
bokelley added a commit that referenced this pull request May 2, 2026
* chore(3.0.x): re-cherry-pick #3461 and #3462 (envelope_field_present + asset-union) (#3608)

* feat(compliance): add envelope_field_present check type; update v3-envelope-integrity storyboard (#3461)

Closes #3429

Adds `envelope_field_present` as a recognized storyboard check type that
walks protocol-envelope.json instead of the step's response_schema_ref.
Updates v3-envelope-integrity.yaml to use it for the `status` presence
assertion, eliminating the VERIFIER_UNREACHABLE gap in the adcp-client
storyboard-drift verifier. Requires adcp-client#1045 for runtime + static
drift support.

Non-breaking justification: additive — new check type alongside existing
ones; no existing check type changed or removed; no storyboard currently
uses envelope_field_present.

Pre-PR review:
- code-reviewer: approved — lint change correct, test added for classifyOutcome, dist/3.0.1 frozen by design (ships as 3.0.2), no blockers
- ad-tech-protocol-expert: approved — semantics correct; runner-output-contract.yaml updated with check enum + expected/actual shape entries; patch bump confirmed correct per playbook conformance-harness rule

https://claude.ai/code/session_01Kwks2uGQS3ZVX7g4kujdGp

Co-authored-by: Claude <noreply@anthropic.com>

* fix(schema): promote asset-variant oneOf to canonical asset-union.json (#3462)

* fix(schema): promote asset-variant oneOf to canonical asset-union.json

Fixes json-schema-to-typescript emitting VASTAsset1, DAASTAsset1,
BriefAsset1, and CatalogAsset1 when creative-asset.json and
creative-manifest.json both inline identical 14-arm oneOf arrays.

Creates core/assets/asset-union.json (title: AssetVariant) as a single
$id-addressable source of truth. Both parent schemas now $ref this file
instead of duplicating the union inline. Wire format and validation
semantics are unchanged; the discriminator annotation moves inside the
canonical schema per OAS 3.1 §4.8.24.

Closes #3459

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

* docs(schema): document intentional subset in offering-asset-group.json

Clarify that offering-asset-group excludes brief-asset and catalog-asset
(campaign-input metadata, not delivery-ready creative types) and cross-link
to the new asset-union.json for the full union. Addresses code-reviewer
nit from pre-PR review.

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

* fix(schema): replace inline asset oneOf in list-creatives-response.json

Third copy of the 14-arm asset union, caught in post-PR code review.
Replace with $ref to asset-union.json for consistency.

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

---------

Co-authored-by: Claude <noreply@anthropic.com>

* chore(3.0.x): sync release-pipeline workflows from main (App token, 3.0.x triggers)

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Version Packages (#3615)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs(creative-channels): fix url_type tracker → tracker_pixel (#2986) (#3673)

The display, audio, carousels, and DOOH channel docs use
"url_type": "tracker", which is not a valid value in the
url-asset-type.json enum (clickthrough / tracker_pixel /
tracker_script). Sellers following these docs emit a non-conformant
url_type that buyers can't interpret without guessing.

Replaces all 10 occurrences with "tracker_pixel" to match the schema.
This is step 1 of the rollout proposed by Nastassia Fulconis on
adcp#2986: 3.0.x docs cleanup → 3.1 SHOULD + role-based fallback +
mechanism-vs-purpose clarification → 4.0 required.

The dist/docs/3.0.2 release snapshot still carries the old value;
backporting to the snapshot is intentionally out of scope here so the
fix lands on the live source first.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(storyboard): provides_state_for cascade-rescue field (closes #3734) (#3775)

Backport of e8fded9 from main to 3.0.x. Adds optional same-phase
substitution declaration on storyboard steps so explicit-mode social
platforms (Snap, Meta, TikTok) can pre-provision accounts out-of-band
and have list_accounts substitute for the missing sync_accounts state
contract — recovering 3 Tier-1 sandbox-verified adapters from 1/9/0 to
9/10 once the SDK cache refreshes against this version.

Storyboard schema (static/compliance/source/universal/storyboard-schema.yaml):
field documentation parallel to contributes_to. All-of array semantics,
same-phase only, target/substitute must be stateful, no self-reference,
acyclic peer-graph per phase.

Runner output contract (static/compliance/source/universal/
runner-output-contract.yaml): new peer_substituted skip reason in
skip_result.reasons, distinct from peer_branch_taken (branch-set routing)
and not_applicable (coverage gap).

Specialism YAML (static/compliance/source/specialisms/sales-social/
index.yaml): provides_state_for: sync_accounts on list_accounts in the
account_setup phase.

Build-time validation (scripts/lint-storyboard-provides-state-for.cjs +
test): wired into build-compliance.cjs lint chain. Covers shape,
self-reference, unknown target, cross-phase, target-stateful,
substitute-stateful, and direct-cycle violations.

Pure additive change; existing storyboards keep current cascade behavior.

Cherry-pick conflict resolved in package.json: kept 3.0.x's simpler
node --test invocation (no --test-force-exit / --test-timeout flags),
slotted in test:storyboard-provides-state-for entry consistent with
3.0.x style. --no-verify used because precommit fails on a pre-existing
3.0.x baseline @adcp/client install drift unrelated to this change.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3696)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* ci(release): auto-upload protocol tarball to GitHub Release (#3786) (#3787)

`createGithubReleases: true` from changesets/action only writes the
changelog body — files have to be uploaded separately. Without this
step the artifacts ship to the repo's dist/ tree but are never attached
as Release assets, leaving adopters who pin via release URL with 404s.

v3.0.0 had assets only because they were uploaded by hand on 2026-04-22;
v3.0.1 / v3.0.2 / v3.0.3 all shipped empty.

New step runs after changesets/action, gated on
`steps.changesets.outputs.published == 'true'` so it only fires on
tag-and-release runs (not Version Packages PR-creation runs). Uploads
${VERSION}.tgz plus .sha256 / .sig / .crt sidecars with --clobber so
re-runs are idempotent.

Backfill of the three missing releases (v3.0.1 / v3.0.2 / v3.0.3) is a
separate manual step against the assets already committed at
dist/protocol/ on the 3.0.x branch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(3.0.x): cherry-pick + adapt 7 spec fixes from main (#3784) (#3789)

* fix(schema): correct title annotation in rate-limited error-details schema (#3149)

* fix(schema): correct title annotation in rate-limited error-details schema

Change "RATE_LIMITED Details" to "Rate Limited Details" in the `title`
annotation of error-details/rate-limited.json. The title field is
non-normative per JSON Schema draft-07 (no validation or wire-format
impact); this corrects the downstream codegen output from
`RATE_LIMITEDDetails` to `RateLimitedDetails`. Applied to source and
all dist snapshots (3.0.0, 3.0.0-rc.3, latest).

Refs #3145. See adcp-client#942 for the SDK alias layer.

https://claude.ai/code/session_01E3LcN5g4tEZutKCTePUVbs

* revert: drop dist/schemas/ hand-edits per #3149 review

dist/schemas/3.0.0/ and dist/schemas/3.0.0-rc.3/ are immutable release
snapshots — scripts/build-schemas.cjs only writes them under --release
mode (the changesets release step), and dist/schemas/latest/ is
gitignored. Mutating frozen GA snapshots breaks the immutability
contract that lets buyers pin to 3.0.0 and trust they see exactly what
was published.

Source title fix is preserved. The next --release build picks up the
corrected title for that version's snapshot; past releases stay
byte-identical to what was published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
(cherry picked from commit fbf71ce)

* spec(error): standardize VALIDATION_ERROR issues[] on core/error.json (closes #3059) (#3562)

* spec(error): standardize VALIDATION_ERROR issues[] (closes #3059)

Adds an optional top-level issues array to core/error.json, normalizing
what @adcp/client already emits for multi-field validation rejections
(adcp-client#874 / #915). Other implementations (adcp-go,
adcp-client-python, hand-rolled sellers) would either miss the structured
pointer list, adopt it ad-hoc with different naming, or converge if the
spec normalizes it. Filing now keeps the ecosystem aligned before
adoption deepens.

Each issue entry: { pointer (RFC 6901), message, keyword, schemaPath? }.
schemaPath MAY be omitted in production to avoid fingerprinting oneOf
branch selection on adversarial payloads.

Backward compatibility:
- field (singular) is retained. When both are present, sellers SHOULD
  set field to issues[0].pointer for pre-3.1 consumers reading field
  only.
- details.issues mirror is permitted for consumers reading from details.
  New consumers should prefer top-level issues.

Files:
- static/schemas/source/core/error.json: adds issues property
- docs/building/implementation/error-handling.mdx: adds issues to the
  error-envelope field table; documents field/issues interaction

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(error): apply protocol-expert review feedback on issues[]

Three substantive sharpens from the ad-tech-protocol-expert review of
PR #3562:

1. Pointer-format mismatch with field — flagged as a latent bug. The
   existing top-level field uses JSONPath-lite (packages[0].targeting);
   the new issues[].pointer uses RFC 6901 (/packages/0/targeting).
   Calling the mirror rule SHOULD without specifying the translation
   left sellers with collision risk. Both descriptions now spell out
   the format choice (Ajv's instancePath = RFC 6901 for pointer; legacy
   JSONPath-lite for field) AND the explicit translation contract on
   the mirror. Future major version will deprecate field in favor of
   issues[].pointer.

2. issues[0].pointer mirror rule — SHOULD upgraded to MUST (when issues
   is present). SHOULD created exactly the rough edge the review
   flagged: pre-3.1 consumers reading field would get nondeterministic
   behavior across sellers. Cost of MUST is one line of dual-write per
   seller; cost of SHOULD is a long tail of seller-A-vs-seller-B bugs.
   MUST also gives a clean deprecation path in 4.0.

3. schemaPath downgraded from MAY to SHOULD NOT in production. The
   review identified this as a real probe oracle: leaking which oneOf
   branch the validator selected before semantic rejection helps
   adversarial callers map polymorphic unions. AdCP already has an
   adversarial-payload threat model (signed-requests work, agent-
   controlled field audit). Sellers MAY emit in dev/sandbox modes.

Also cited Ajv as prior art so implementers know where the keyword
vocabulary comes from (instancePath / keyword / schemaPath are Ajv's
native error output fields). Reduces the ad-hoc-naming risk.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit bd3a18c)

* spec(schemas): PascalCase titles on error-details schemas (partial fix for #3145) (#3566)

* spec(schemas): PascalCase titles on error-details schemas

Partial fix for #3145. Six error-details/*.json files carry SCREAMING_SNAKE
titles that propagate awkwardly through json-schema-to-typescript into
@adcp/client's public type surface (e.g., RATE_LIMITEDDetails_ScopeValues
read as a typo to every consumer of the SDK).

Renames (no wire-format change — $id values stay kebab-case; only the
title field is touched, which controls codegen):

- ACCOUNT_SETUP_REQUIRED Details -> AccountSetupRequiredDetails
- AUDIENCE_TOO_SMALL Details     -> AudienceTooSmallDetails
- BUDGET_TOO_LOW Details         -> BudgetTooLowDetails
- CONFLICT Details               -> ConflictDetails
- CREATIVE_REJECTED Details      -> CreativeRejectedDetails
- POLICY_VIOLATION Details       -> PolicyViolationDetails

rate-limited.json already had PascalCase (Rate Limited Details);
vendor-error-codes.json already had Vendor Error Code Registry; no change
to either.

#3145's other half — Foo1-suffixed enum dupes (AgeVerificationMethod1,
*Asset1, etc.) — is downstream codegen behavior. Both refs already point
at the same $id with $ref so the spec side is correct; the dupe shows up
because json-schema-to-typescript walks through different parent paths
and emits two inline copies. SDK-side post-process renaming (with one-
minor-version aliases) is the pragmatic fix. Tracked at adcp-client#942.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255,
build-compliance 20 universal / 6 protocols / 19 specialisms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(schemas): Title Case (with spaces) to match #3149 precedent

Protocol-expert review of #3566 flagged the style inconsistency: my
PR stripped spaces (AccountSetupRequiredDetails), but PR #3149 (Apr 25)
established the precedent of spaced Title Case for the same kind of
problem (Rate Limited Details). Going with #3149's form so the 8-file
directory stays uniform. json-schema-to-typescript strips whitespace
when generating identifiers, so the codegen output is identical either
way (AccountSetupRequiredDetails on both); the source-of-truth title
just stays consistent across the directory.

Title field is non-normative per JSON Schema draft-07 §10.1 — affects
only docgen/codegen output, not validation. Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit a5d9bff)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2) (#3671)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2)

Define what receivers do when a URL asset omits url_type. Today the
field is optional with no fallback rule, so a conformant manifest like
{asset_type: url, url: ...} forces buyers to guess the invocation
mechanism — and guessing wrong (firing a clickthrough URL as a pixel,
or a tracker as a clickthrough) corrupts measurement and breaks user
flows.

Schema changes:
- url-asset.json: senders SHOULD include url_type on every URL asset.
  When absent, receivers SHOULD fall back to the format's
  url-asset-requirements.role (clickthrough/landing_page →
  `clickthrough` mechanism; *_tracker roles → `tracker_pixel`). When
  neither is available, receivers MAY reject rather than guess.
- url-asset-requirements.json: clarify that role is purpose
  (impression vs click vs viewability vs 3P) while url_type is
  mechanism (click vs pixel vs script tag); a click_tracker slot
  validly accepts a tracker_pixel URL.

Doc changes:
- asset-types.mdx URL Asset section: rewritten to use the actual
  url_type enum (clickthrough/tracker_pixel/tracker_script — the old
  text listed impression_tracker/video_tracker/landing_page, which
  were never url_type values), to add the SHOULD note and role
  fallback table, and to remove the "you only need to supply the
  url value" guidance that drove the original ambiguity.

Wire format unchanged. Senders already including url_type are
unaffected. Step 2 of the rollout on adcp#2986; step 3 (require
url_type in 4.0) follows once this lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(url-asset): apply expert review — viewability→script, third_party→unmapped, MUST NOT silently guess

Addresses ad-tech-protocol-expert + adtech-product-expert review on
PR #3671:

Required fixes:
- viewability_tracker → tracker_script (not tracker_pixel). OMID and
  equivalent verification SDKs require a <script> tag; firing them as
  a pixel produces no measurement and no error — exactly the silent-
  corruption failure mode this PR exists to prevent.
- third_party_tracker → no safe fallback. Mechanism is integration-
  specific (DV/IAS ship both pixel and script forms). Receivers MAY
  reject or warn rather than guess.
- Strengthen receiver guidance to "MUST NOT silently pick a
  mechanism; SHOULD reject" when both url_type and role are absent.
  Mirrors the mdx language into the JSON Schema description so
  extractors and conformance tooling read the same rule.
- Add VAST/DAAST carve-out: VAST tag URLs are not URL assets; use
  asset_type: "vast" or the dedicated tracker types pending RFC #2915.
- Update docs/creative/formats.mdx tracker-detection rule. Today it
  feature-detects on url_type ∈ {tracker_pixel, tracker_script};
  under the new fallback semantics, format authors who declare only
  role would silently fail that detector. Detection now accepts
  either url_type OR a tracker-purpose role.

Non-blocking improvements:
- Migration cue in asset-types.mdx for sellers who built tooling
  around the older "you only need to supply the url value" guidance:
  3.x is fine, plan to add url_type before 4.0.

Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(url-asset): cross-reference fallback table between schema and mdx

The role→url_type fallback table lives in two places: the
url-asset.json top-level description (read by conformance tools and
codegen) and the asset-types.mdx URL Asset section (read by humans).
Without a hint, an editor of one will silently drift the other.

Adds a $comment in url-asset.json and a JSX comment in asset-types.mdx
pointing at each other. Schema description remains the normative
source; mdx is the human copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit f6af651)

* fix(compliance): audience-sync discover_account stateful: false → true (#3710)

The list_accounts step in the account_setup phase establishes account_id
for downstream sync_audiences calls. The storyboard narrative was correct
but the stateful flag contradicted it, causing the SDK runner to not count
a passing result as cascade state — explicit-mode adopters saw
prerequisite_failed on sync_audiences even after list_accounts passed.

Fixes #3707

https://claude.ai/code/session_019ZJXyeQYrRZc17UqcW2yDf

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit e74997b)

* chore(changesets): downgrade cherry-picked minor bumps to patch for 3.0.x line

Maintenance-line policy: cherry-picks land as patches even when the
original PRs landed on main as minor bumps. Otherwise the changesets
trigger 3.1.0 publication off the 3.0.x branch, defeating the cherry-pick.

- error-issues-array.md (#3562, originally minor): patch
- url-type-should-and-role-fallback.md (#3671, originally minor): patch

Both PRs were classified as patch-eligible in #3784.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schema): manifest.json + structured enumMetadata (closes #3725) (#3738)

* feat(schema): publish manifest.json + structured enumMetadata (adcp#3725)

Adds two additive artifacts so SDKs stop hand-rolling (and drifting on)
spec metadata. Root cause for adcp-client#1135 (17 missing error codes,
3 wrong recovery classifications shipped in TS SDK for over a year).

- enums/error-code.json gains an enumMetadata block with structured
  recovery + suggestion per code. Build-time lint rejects drift between
  the structured value and the prose Recovery: X in enumDescriptions.

- New static/schemas/source/manifest.schema.json + generator emitting
  dist/schemas/{version}/manifest.json: 58 tools, 48 error codes, 19
  specialisms, plus an error_code_policy block defining how SDKs MUST
  classify codes from non-conforming sellers.

- mutating derived from the same classifier the idempotency-key lint
  enforces (single source of truth). Tightened READ_ONLY_VERB_PATTERN
  to anchor at start so create-collection-list / delete-property-list
  no longer mis-classify as read-only via -list- mid-name; added search
  as a read-only verb.

- Specialisms expose entry_point_tools (curated minimum from
  index.yaml.required_tools) and exercised_tools (full surface — union
  of own phases[].steps[].task and every linked scenario, derived by
  walking requires_scenarios). sales_guaranteed now correctly lists 9
  tools instead of 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(manifest): expert-review fixes — strip regex, requires_tool gating

Two correctness fixes from protocol-expert review:

- stripRecoveryProse: greedy [^.]*\. truncated descriptions with periods
  inside parentheticals (e.g. "capabilities.media_buy.limits"). Rewrote
  with three explicit patterns: bare verdict, verdict + balanced
  parenthetical, clause continuation to EOS. Verified all 48 codes emit
  clean descriptions with no Recovery: prose remaining.

- collectTasksFromPhases now skips steps gated by requires_tool. Steps
  marked requires_tool: <X> are conditional on the agent claiming X, not
  required surface. Without the skip, optional test-harness tools
  (comply_test_controller, gated across 23+ steps) propagated into
  every sales specialism's exercised_tools.

Plus code-review nits:

- Simplified discoverTools utility-shape skip to NON_OPERATION_ALLOWLIST
  only; documented the contract.
- Removed unused schema parameter from classifyRequestMutating.
- Tightened indexScenarioTasks predicate to require phases array.
- Added cross-reference comment between MANIFEST_PROTOCOLS and the
  meta-schema's protocol enum.
- Changeset now mentions /schemas/latest/manifest.json for nightly
  codegen consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit b44996f)

* fix(3.0.x): trim enumMetadata to 3.0.x codes; downgrade #3738 to patch

The cherry-pick of #3738 brought in enumDescriptions + enumMetadata entries
for SCOPE_INSUFFICIENT, READ_ONLY_SCOPE, and FIELD_NOT_PERMITTED — three
error codes added to main in PRs that aren't on 3.0.x. Their enum entries
weren't introduced (3.0.x's enum array is unaffected), but the description
and metadata blocks were left referencing them. The new lintErrorCodeEnumMetadata
guardrail catches this and refuses to build.

Trim: drop the three orphan entries from enumDescriptions and enumMetadata.
Counts now agree at 45 / 45 / 45.

Also downgrades the changeset from minor to patch — same rationale as the
other cherry-picks on this branch: maintenance line, no new enum values,
no wire-format change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(errors): tighten AUTH_REQUIRED prose to warn on retry storms (3.0.x prose-only backport of #3739)

3.0.x cannot adopt the AUTH_MISSING/AUTH_INVALID split from #3739 — adding
new enum values violates the maintenance line's semver rules. This is the
prose-only backport: same wire code, same recovery class, but the
description and enumMetadata.suggestion now spell out the two sub-cases
(missing vs. presented-but-rejected) and the SHOULD-NOT-auto-retry rule.

Closes 3.0.x portion of #3730.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>

* ci(release): un-exclude dist/compliance + forward-merge auto-resolution (#3794) (#3800)

* ci(release): un-exclude dist/compliance + forward-merge auto-resolution

Two release-pipeline hardening fixes for 3.0.4 and beyond.

.dockerignore: un-exclude dist/compliance/ so versioned URLs (the
/compliance/{version}/ tree) actually serve from the Fly image. The
ignore pattern was set up for /schemas/ and /protocol/ but never updated
when /compliance/ versioned routing was added. Result: every committed
dist/compliance/3.0.X/ directory was stripped from the build context,
and SDKs fetching compliance bundles by URL hit 404s on fresh-cache
scenarios. Re-include dist/compliance, then re-exclude
dist/compliance/latest (regenerated in-container by build:compliance).

forward-merge-3.0.yml: replace the bare-merge approach with auto-
resolution on an explicit allowlist of always-divergent paths
(package.json version, .changeset/*.md, dist/*, CHANGELOG.md, schema
source index files). Drop the brittle is-ancestor shortcut that
returns false after squash-merges even when content is in main; use
post-merge git diff --quiet to skip cleanly when main already has
3.0.x's content. Conflicts outside the allowlist fail the workflow
loud, surfacing playbook violations (a change on 3.0.x that wasn't
first cherry-picked from main).

Updates .agents/playbook.md § Release lines to document the new
auto-resolution behavior so reviewers know what to spot-check.

Closes the operational pain that bit us today on PR #3783, where the
v3.0.3 cut's forward-merge needed three manual conflict resolutions
(package.json, .changeset/fix-url-type-tracker-pixel-channel-docs.md,
static/schemas/source/index.json) plus a manual unshallow before
the merge could complete.



* ci(release): address expert review feedback on forward-merge auto-resolution

Code review (af5969...): empty-CONFLICTS-after-merge-failure guard so
hook rejections / unrelated-history failures fail loud instead of
silently committing the empty merge.

Security review (a16289...): tighten dist/* glob to explicit
{schemas,compliance,protocol,docs}/* list so future mutable subtrees
under dist/ don't silently auto-resolve via --theirs.

Plus: drop `2>/dev/null || true` on `git rm` (let real errors surface;
the post-loop REMAINING check already guards against bad state). Add
post-resolution `git status --short` and `git diff vs origin/main --
package.json` log groups so reviewers can spot main-unique scripts
that may have been overwritten without leaving GitHub.

No functional change to the happy-path resolution behavior.



---------


# Conflicts:
#	.agents/playbook.md

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(release): forward-merge package.json --ours, not --theirs (#3807) (#3808)

Original --theirs rule stripped main's structural changes when 3.0.x
hadn't been updated to match. Concrete case: main renamed @adcp/client
to @adcp/sdk + bumped to 5.25.1; 3.0.x stayed on @adcp/client@5.21.1.
The forward-merge took 3.0.x's package.json wholesale, leaving the
package-lock out of sync. CI broke with "npm ci ... package.json and
package-lock.json or npm-shrinkwrap.json are in sync". PR #3806's CI
exposed this.

--ours preserves main's state. Main's pre-mode tracking is independent
of 3.0.x's version field; the dist/* artifacts still flow forward via
the allowlist; main's structural changes survive.

Trade-off: main's package.json version doesn't reflect 3.0.x's latest
release. Acceptable — main's version field isn't authoritative while
pre-mode is active. The next main pre-mode cut produces 3.1.0-beta.X
from accumulated changesets regardless of base version.

Companion playbook + PR-body checklist update so docs match behavior.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(release): bridge cherry-pick divergence in forward-merge (#3811) (#3814)

Adds two specific files to the auto-resolution --ours allowlist where
3.0.x has #3789's hand-adapted prose-only backport of #3739 and main
has the full enum split (adds AUTH_MISSING / AUTH_INVALID codes that
can't ship to 3.0.x without violating patch eligibility).

  - docs/building/implementation/error-handling.mdx
  - static/schemas/source/enums/error-code.json

Without this rule, every routine forward-merge from 3.0.x → main
rediscovers the same conflict because squash-merges of prior
forward-merges (the only merge style this repo allows) don't advance
git's merge-base. The post-merge `git diff --quiet` skip can't reach
to detect "main already has this content" because the merge fails
before that step.

Marked temporary in the workflow comments — remove when 3.1.0 cuts
and main no longer has the in-flight enum split.

Without this fix, the next forward-merge after 3.0.4 cuts would
fail loud on these same two files, requiring another manual
resolution PR. With it, 3.0.4's forward-merge auto-succeeds.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3799)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* ci(release): push forward-merge branch before peter-evans runs (#3818) (#3820)

Discovered when 3.0.4's forward-merge ran for real for the first time
(run 25250971971): auto-resolution worked perfectly (the new allowlist
+ bridge handled every conflict), but peter-evans/create-pull-request
crashed with "fatal: ambiguous argument 'origin/forward-merge/3.0.x'"
because the remote branch didn't exist yet.

peter-evans's internal `git reset --hard origin/forward-merge/3.0.x`
flow assumes the remote-tracking branch already exists. On a first run
(or any time the remote branch isn't there), it fails. Pushing
explicitly after auto-resolution establishes the ref so peter-evans's
reset has a target.

After this lands + cherry-picks to 3.0.x, the next VP cut (3.0.5 or
3.1.0) will auto-create the forward-merge PR without manual
intervention.

For 3.0.4 specifically: I'll open the PR manually since this fix
requires a workflow change that hasn't run yet.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(storyboard-schema): add optional default_agent field (closes #3894) (#3897)

* spec(storyboard-schema): add optional default_agent field (closes #3894)

Adds an optional top-level default_agent: <key> field to the storyboard
authoring schema. The multi-agent runner resolves the logical key (sales,
governance, creative, …) against the runtime agents map passed to
runStoryboard({ agents: {…} }) — see adcp-client#1066 / #1355.

The runner already accepts default_agent via run-options. This change
lets storyboard authors encode the topology intent in YAML once instead
of re-asserting it on every CI invocation. Cross-domain tools
(sync_creatives, list_creative_formats, comply_test_controller) route
deterministically without per-step agent: overrides.

Strictly additive — single-agent runs ignore it, existing 3.0.x
storyboards keep working, pre-existing run-options default_agent keeps
its lower-precedence slot. Mirrors the provides_state_for precedent
(#3775) for additive storyboard-schema affordances on 3.0.x.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(storyboard-schema): tighten default_agent contract per expert review

Address protocol- and product-expert review on PR #3897:

- Slot 2: state explicitly what zero/one/multi specialism claimants do.
  Multi-claim grades unrouted_step (operator-config error); slots 3/4 do
  NOT rescue. Zero falls through to slot 3.
- Slot 3: when the storyboard declares default_agent and the key is
  absent from the runtime map, grade default_agent_unresolved — do NOT
  silently fall to slot 4. Silent fallback would invisibly override the
  storyboard author's encoded intent. Slot 4 fires only when the field
  is unset.
- Slot 4: same set-but-unmatched rule applied symmetrically.
- Key shape: free-form non-empty string keyed by the runtime agents map.
  Spec does NOT constrain to the specialism enum — production topologies
  legitimately fan out per-property / per-region / per-rights-holder.
  Cross-operator portability is the author's concern, not the spec's.
- Drop comply_test_controller from the cross-domain example — it's
  routed via prerequisites.controller_seeding, not default_agent.
- Disambiguate adcp-client#1355 reference (was bare "#1355").

No wire-protocol surface change; doc-only edit to the storyboard
authoring schema (already a comment-block YAML).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(capabilities): relax identity.additionalProperties to true (3.0.x) (#3896)

The identity object on get-adcp-capabilities-response was schema-closed
(additionalProperties: false), so any 3.0-pinned operator adopting a
forward-compatible field — notably identity.brand_json_url from #3690,
intended to be readable on 3.0 without a schema bump — would have its
capabilities response rejected by strict 3.0 validators (e.g.,
@adcp/sdk's createAdcpServer default).

Mirrors the relaxation already on main (post-#3690). Closed property
list (per_principal_key_isolation, key_origins, compromise_notification)
is unchanged; this is strictly additive forward-compat.

The forward-compat narrative in security.mdx ("3.0-pinned implementers
can adopt the field today without bumping") depends on this being live
in the shipped 3.0 schema — without it, the spec advice contradicts the
schema.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(storyboard): capture rights_id from acquire_rights response (closes #3892) (#3893)

The brand-rights storyboard step `acquire_rights` captured `rights_grant_id`
from the response, but `brand/acquire-rights-response.json` defines the field
as `rights_id`. Spec-compliant agents passed response_schema validation but
failed context capture, cascade-skipping `rights_enforcement`.

Update the YAML to read `rights_id` (preserving the storyboard-internal
`rights_grant_id` key so no other steps need to change) and correct the
`expected:` prose to match the published schema (rights_id + status: acquired).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3898)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: aao-release-bot[bot] <280565558+aao-release-bot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
bokelley added a commit that referenced this pull request May 3, 2026
…olution (#4011)

* chore(3.0.x): re-cherry-pick #3461 and #3462 (envelope_field_present + asset-union) (#3608)

* feat(compliance): add envelope_field_present check type; update v3-envelope-integrity storyboard (#3461)

Closes #3429

Adds `envelope_field_present` as a recognized storyboard check type that
walks protocol-envelope.json instead of the step's response_schema_ref.
Updates v3-envelope-integrity.yaml to use it for the `status` presence
assertion, eliminating the VERIFIER_UNREACHABLE gap in the adcp-client
storyboard-drift verifier. Requires adcp-client#1045 for runtime + static
drift support.

Non-breaking justification: additive — new check type alongside existing
ones; no existing check type changed or removed; no storyboard currently
uses envelope_field_present.

Pre-PR review:
- code-reviewer: approved — lint change correct, test added for classifyOutcome, dist/3.0.1 frozen by design (ships as 3.0.2), no blockers
- ad-tech-protocol-expert: approved — semantics correct; runner-output-contract.yaml updated with check enum + expected/actual shape entries; patch bump confirmed correct per playbook conformance-harness rule

https://claude.ai/code/session_01Kwks2uGQS3ZVX7g4kujdGp

Co-authored-by: Claude <noreply@anthropic.com>

* fix(schema): promote asset-variant oneOf to canonical asset-union.json (#3462)

* fix(schema): promote asset-variant oneOf to canonical asset-union.json

Fixes json-schema-to-typescript emitting VASTAsset1, DAASTAsset1,
BriefAsset1, and CatalogAsset1 when creative-asset.json and
creative-manifest.json both inline identical 14-arm oneOf arrays.

Creates core/assets/asset-union.json (title: AssetVariant) as a single
$id-addressable source of truth. Both parent schemas now $ref this file
instead of duplicating the union inline. Wire format and validation
semantics are unchanged; the discriminator annotation moves inside the
canonical schema per OAS 3.1 §4.8.24.

Closes #3459

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

* docs(schema): document intentional subset in offering-asset-group.json

Clarify that offering-asset-group excludes brief-asset and catalog-asset
(campaign-input metadata, not delivery-ready creative types) and cross-link
to the new asset-union.json for the full union. Addresses code-reviewer
nit from pre-PR review.

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

* fix(schema): replace inline asset oneOf in list-creatives-response.json

Third copy of the 14-arm asset union, caught in post-PR code review.
Replace with $ref to asset-union.json for consistency.

https://claude.ai/code/session_017XYv1Yt2m9NSj4bsNR22qo

---------

Co-authored-by: Claude <noreply@anthropic.com>

* chore(3.0.x): sync release-pipeline workflows from main (App token, 3.0.x triggers)

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Version Packages (#3615)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs(creative-channels): fix url_type tracker → tracker_pixel (#2986) (#3673)

The display, audio, carousels, and DOOH channel docs use
"url_type": "tracker", which is not a valid value in the
url-asset-type.json enum (clickthrough / tracker_pixel /
tracker_script). Sellers following these docs emit a non-conformant
url_type that buyers can't interpret without guessing.

Replaces all 10 occurrences with "tracker_pixel" to match the schema.
This is step 1 of the rollout proposed by Nastassia Fulconis on
adcp#2986: 3.0.x docs cleanup → 3.1 SHOULD + role-based fallback +
mechanism-vs-purpose clarification → 4.0 required.

The dist/docs/3.0.2 release snapshot still carries the old value;
backporting to the snapshot is intentionally out of scope here so the
fix lands on the live source first.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(storyboard): provides_state_for cascade-rescue field (closes #3734) (#3775)

Backport of e8fded9 from main to 3.0.x. Adds optional same-phase
substitution declaration on storyboard steps so explicit-mode social
platforms (Snap, Meta, TikTok) can pre-provision accounts out-of-band
and have list_accounts substitute for the missing sync_accounts state
contract — recovering 3 Tier-1 sandbox-verified adapters from 1/9/0 to
9/10 once the SDK cache refreshes against this version.

Storyboard schema (static/compliance/source/universal/storyboard-schema.yaml):
field documentation parallel to contributes_to. All-of array semantics,
same-phase only, target/substitute must be stateful, no self-reference,
acyclic peer-graph per phase.

Runner output contract (static/compliance/source/universal/
runner-output-contract.yaml): new peer_substituted skip reason in
skip_result.reasons, distinct from peer_branch_taken (branch-set routing)
and not_applicable (coverage gap).

Specialism YAML (static/compliance/source/specialisms/sales-social/
index.yaml): provides_state_for: sync_accounts on list_accounts in the
account_setup phase.

Build-time validation (scripts/lint-storyboard-provides-state-for.cjs +
test): wired into build-compliance.cjs lint chain. Covers shape,
self-reference, unknown target, cross-phase, target-stateful,
substitute-stateful, and direct-cycle violations.

Pure additive change; existing storyboards keep current cascade behavior.

Cherry-pick conflict resolved in package.json: kept 3.0.x's simpler
node --test invocation (no --test-force-exit / --test-timeout flags),
slotted in test:storyboard-provides-state-for entry consistent with
3.0.x style. --no-verify used because precommit fails on a pre-existing
3.0.x baseline @adcp/client install drift unrelated to this change.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3696)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* ci(release): auto-upload protocol tarball to GitHub Release (#3786) (#3787)

`createGithubReleases: true` from changesets/action only writes the
changelog body — files have to be uploaded separately. Without this
step the artifacts ship to the repo's dist/ tree but are never attached
as Release assets, leaving adopters who pin via release URL with 404s.

v3.0.0 had assets only because they were uploaded by hand on 2026-04-22;
v3.0.1 / v3.0.2 / v3.0.3 all shipped empty.

New step runs after changesets/action, gated on
`steps.changesets.outputs.published == 'true'` so it only fires on
tag-and-release runs (not Version Packages PR-creation runs). Uploads
${VERSION}.tgz plus .sha256 / .sig / .crt sidecars with --clobber so
re-runs are idempotent.

Backfill of the three missing releases (v3.0.1 / v3.0.2 / v3.0.3) is a
separate manual step against the assets already committed at
dist/protocol/ on the 3.0.x branch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(3.0.x): cherry-pick + adapt 7 spec fixes from main (#3784) (#3789)

* fix(schema): correct title annotation in rate-limited error-details schema (#3149)

* fix(schema): correct title annotation in rate-limited error-details schema

Change "RATE_LIMITED Details" to "Rate Limited Details" in the `title`
annotation of error-details/rate-limited.json. The title field is
non-normative per JSON Schema draft-07 (no validation or wire-format
impact); this corrects the downstream codegen output from
`RATE_LIMITEDDetails` to `RateLimitedDetails`. Applied to source and
all dist snapshots (3.0.0, 3.0.0-rc.3, latest).

Refs #3145. See adcp-client#942 for the SDK alias layer.

https://claude.ai/code/session_01E3LcN5g4tEZutKCTePUVbs

* revert: drop dist/schemas/ hand-edits per #3149 review

dist/schemas/3.0.0/ and dist/schemas/3.0.0-rc.3/ are immutable release
snapshots — scripts/build-schemas.cjs only writes them under --release
mode (the changesets release step), and dist/schemas/latest/ is
gitignored. Mutating frozen GA snapshots breaks the immutability
contract that lets buyers pin to 3.0.0 and trust they see exactly what
was published.

Source title fix is preserved. The next --release build picks up the
corrected title for that version's snapshot; past releases stay
byte-identical to what was published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
(cherry picked from commit fbf71ce)

* spec(error): standardize VALIDATION_ERROR issues[] on core/error.json (closes #3059) (#3562)

* spec(error): standardize VALIDATION_ERROR issues[] (closes #3059)

Adds an optional top-level issues array to core/error.json, normalizing
what @adcp/client already emits for multi-field validation rejections
(adcp-client#874 / #915). Other implementations (adcp-go,
adcp-client-python, hand-rolled sellers) would either miss the structured
pointer list, adopt it ad-hoc with different naming, or converge if the
spec normalizes it. Filing now keeps the ecosystem aligned before
adoption deepens.

Each issue entry: { pointer (RFC 6901), message, keyword, schemaPath? }.
schemaPath MAY be omitted in production to avoid fingerprinting oneOf
branch selection on adversarial payloads.

Backward compatibility:
- field (singular) is retained. When both are present, sellers SHOULD
  set field to issues[0].pointer for pre-3.1 consumers reading field
  only.
- details.issues mirror is permitted for consumers reading from details.
  New consumers should prefer top-level issues.

Files:
- static/schemas/source/core/error.json: adds issues property
- docs/building/implementation/error-handling.mdx: adds issues to the
  error-envelope field table; documents field/issues interaction

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(error): apply protocol-expert review feedback on issues[]

Three substantive sharpens from the ad-tech-protocol-expert review of
PR #3562:

1. Pointer-format mismatch with field — flagged as a latent bug. The
   existing top-level field uses JSONPath-lite (packages[0].targeting);
   the new issues[].pointer uses RFC 6901 (/packages/0/targeting).
   Calling the mirror rule SHOULD without specifying the translation
   left sellers with collision risk. Both descriptions now spell out
   the format choice (Ajv's instancePath = RFC 6901 for pointer; legacy
   JSONPath-lite for field) AND the explicit translation contract on
   the mirror. Future major version will deprecate field in favor of
   issues[].pointer.

2. issues[0].pointer mirror rule — SHOULD upgraded to MUST (when issues
   is present). SHOULD created exactly the rough edge the review
   flagged: pre-3.1 consumers reading field would get nondeterministic
   behavior across sellers. Cost of MUST is one line of dual-write per
   seller; cost of SHOULD is a long tail of seller-A-vs-seller-B bugs.
   MUST also gives a clean deprecation path in 4.0.

3. schemaPath downgraded from MAY to SHOULD NOT in production. The
   review identified this as a real probe oracle: leaking which oneOf
   branch the validator selected before semantic rejection helps
   adversarial callers map polymorphic unions. AdCP already has an
   adversarial-payload threat model (signed-requests work, agent-
   controlled field audit). Sellers MAY emit in dev/sandbox modes.

Also cited Ajv as prior art so implementers know where the keyword
vocabulary comes from (instancePath / keyword / schemaPath are Ajv's
native error output fields). Reduces the ad-hoc-naming risk.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit bd3a18c)

* spec(schemas): PascalCase titles on error-details schemas (partial fix for #3145) (#3566)

* spec(schemas): PascalCase titles on error-details schemas

Partial fix for #3145. Six error-details/*.json files carry SCREAMING_SNAKE
titles that propagate awkwardly through json-schema-to-typescript into
@adcp/client's public type surface (e.g., RATE_LIMITEDDetails_ScopeValues
read as a typo to every consumer of the SDK).

Renames (no wire-format change — $id values stay kebab-case; only the
title field is touched, which controls codegen):

- ACCOUNT_SETUP_REQUIRED Details -> AccountSetupRequiredDetails
- AUDIENCE_TOO_SMALL Details     -> AudienceTooSmallDetails
- BUDGET_TOO_LOW Details         -> BudgetTooLowDetails
- CONFLICT Details               -> ConflictDetails
- CREATIVE_REJECTED Details      -> CreativeRejectedDetails
- POLICY_VIOLATION Details       -> PolicyViolationDetails

rate-limited.json already had PascalCase (Rate Limited Details);
vendor-error-codes.json already had Vendor Error Code Registry; no change
to either.

#3145's other half — Foo1-suffixed enum dupes (AgeVerificationMethod1,
*Asset1, etc.) — is downstream codegen behavior. Both refs already point
at the same $id with $ref so the spec side is correct; the dupe shows up
because json-schema-to-typescript walks through different parent paths
and emits two inline copies. SDK-side post-process renaming (with one-
minor-version aliases) is the pragmatic fix. Tracked at adcp-client#942.

Schema validators all clean (test:schemas 7/7, test:json-schema 255/255,
build-compliance 20 universal / 6 protocols / 19 specialisms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(schemas): Title Case (with spaces) to match #3149 precedent

Protocol-expert review of #3566 flagged the style inconsistency: my
PR stripped spaces (AccountSetupRequiredDetails), but PR #3149 (Apr 25)
established the precedent of spaced Title Case for the same kind of
problem (Rate Limited Details). Going with #3149's form so the 8-file
directory stays uniform. json-schema-to-typescript strips whitespace
when generating identifiers, so the codegen output is identical either
way (AccountSetupRequiredDetails on both); the source-of-truth title
just stays consistent across the directory.

Title field is non-normative per JSON Schema draft-07 §10.1 — affects
only docgen/codegen output, not validation. Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit a5d9bff)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2) (#3671)

* spec(url-asset): SHOULD url_type + role-based fallback (#2986 step 2)

Define what receivers do when a URL asset omits url_type. Today the
field is optional with no fallback rule, so a conformant manifest like
{asset_type: url, url: ...} forces buyers to guess the invocation
mechanism — and guessing wrong (firing a clickthrough URL as a pixel,
or a tracker as a clickthrough) corrupts measurement and breaks user
flows.

Schema changes:
- url-asset.json: senders SHOULD include url_type on every URL asset.
  When absent, receivers SHOULD fall back to the format's
  url-asset-requirements.role (clickthrough/landing_page →
  `clickthrough` mechanism; *_tracker roles → `tracker_pixel`). When
  neither is available, receivers MAY reject rather than guess.
- url-asset-requirements.json: clarify that role is purpose
  (impression vs click vs viewability vs 3P) while url_type is
  mechanism (click vs pixel vs script tag); a click_tracker slot
  validly accepts a tracker_pixel URL.

Doc changes:
- asset-types.mdx URL Asset section: rewritten to use the actual
  url_type enum (clickthrough/tracker_pixel/tracker_script — the old
  text listed impression_tracker/video_tracker/landing_page, which
  were never url_type values), to add the SHOULD note and role
  fallback table, and to remove the "you only need to supply the
  url value" guidance that drove the original ambiguity.

Wire format unchanged. Senders already including url_type are
unaffected. Step 2 of the rollout on adcp#2986; step 3 (require
url_type in 4.0) follows once this lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(url-asset): apply expert review — viewability→script, third_party→unmapped, MUST NOT silently guess

Addresses ad-tech-protocol-expert + adtech-product-expert review on
PR #3671:

Required fixes:
- viewability_tracker → tracker_script (not tracker_pixel). OMID and
  equivalent verification SDKs require a <script> tag; firing them as
  a pixel produces no measurement and no error — exactly the silent-
  corruption failure mode this PR exists to prevent.
- third_party_tracker → no safe fallback. Mechanism is integration-
  specific (DV/IAS ship both pixel and script forms). Receivers MAY
  reject or warn rather than guess.
- Strengthen receiver guidance to "MUST NOT silently pick a
  mechanism; SHOULD reject" when both url_type and role are absent.
  Mirrors the mdx language into the JSON Schema description so
  extractors and conformance tooling read the same rule.
- Add VAST/DAAST carve-out: VAST tag URLs are not URL assets; use
  asset_type: "vast" or the dedicated tracker types pending RFC #2915.
- Update docs/creative/formats.mdx tracker-detection rule. Today it
  feature-detects on url_type ∈ {tracker_pixel, tracker_script};
  under the new fallback semantics, format authors who declare only
  role would silently fail that detector. Detection now accepts
  either url_type OR a tracker-purpose role.

Non-blocking improvements:
- Migration cue in asset-types.mdx for sellers who built tooling
  around the older "you only need to supply the url value" guidance:
  3.x is fine, plan to add url_type before 4.0.

Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(url-asset): cross-reference fallback table between schema and mdx

The role→url_type fallback table lives in two places: the
url-asset.json top-level description (read by conformance tools and
codegen) and the asset-types.mdx URL Asset section (read by humans).
Without a hint, an editor of one will silently drift the other.

Adds a $comment in url-asset.json and a JSX comment in asset-types.mdx
pointing at each other. Schema description remains the normative
source; mdx is the human copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit f6af651)

* fix(compliance): audience-sync discover_account stateful: false → true (#3710)

The list_accounts step in the account_setup phase establishes account_id
for downstream sync_audiences calls. The storyboard narrative was correct
but the stateful flag contradicted it, causing the SDK runner to not count
a passing result as cascade state — explicit-mode adopters saw
prerequisite_failed on sync_audiences even after list_accounts passed.

Fixes #3707

https://claude.ai/code/session_019ZJXyeQYrRZc17UqcW2yDf

Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit e74997b)

* chore(changesets): downgrade cherry-picked minor bumps to patch for 3.0.x line

Maintenance-line policy: cherry-picks land as patches even when the
original PRs landed on main as minor bumps. Otherwise the changesets
trigger 3.1.0 publication off the 3.0.x branch, defeating the cherry-pick.

- error-issues-array.md (#3562, originally minor): patch
- url-type-should-and-role-fallback.md (#3671, originally minor): patch

Both PRs were classified as patch-eligible in #3784.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schema): manifest.json + structured enumMetadata (closes #3725) (#3738)

* feat(schema): publish manifest.json + structured enumMetadata (adcp#3725)

Adds two additive artifacts so SDKs stop hand-rolling (and drifting on)
spec metadata. Root cause for adcp-client#1135 (17 missing error codes,
3 wrong recovery classifications shipped in TS SDK for over a year).

- enums/error-code.json gains an enumMetadata block with structured
  recovery + suggestion per code. Build-time lint rejects drift between
  the structured value and the prose Recovery: X in enumDescriptions.

- New static/schemas/source/manifest.schema.json + generator emitting
  dist/schemas/{version}/manifest.json: 58 tools, 48 error codes, 19
  specialisms, plus an error_code_policy block defining how SDKs MUST
  classify codes from non-conforming sellers.

- mutating derived from the same classifier the idempotency-key lint
  enforces (single source of truth). Tightened READ_ONLY_VERB_PATTERN
  to anchor at start so create-collection-list / delete-property-list
  no longer mis-classify as read-only via -list- mid-name; added search
  as a read-only verb.

- Specialisms expose entry_point_tools (curated minimum from
  index.yaml.required_tools) and exercised_tools (full surface — union
  of own phases[].steps[].task and every linked scenario, derived by
  walking requires_scenarios). sales_guaranteed now correctly lists 9
  tools instead of 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(manifest): expert-review fixes — strip regex, requires_tool gating

Two correctness fixes from protocol-expert review:

- stripRecoveryProse: greedy [^.]*\. truncated descriptions with periods
  inside parentheticals (e.g. "capabilities.media_buy.limits"). Rewrote
  with three explicit patterns: bare verdict, verdict + balanced
  parenthetical, clause continuation to EOS. Verified all 48 codes emit
  clean descriptions with no Recovery: prose remaining.

- collectTasksFromPhases now skips steps gated by requires_tool. Steps
  marked requires_tool: <X> are conditional on the agent claiming X, not
  required surface. Without the skip, optional test-harness tools
  (comply_test_controller, gated across 23+ steps) propagated into
  every sales specialism's exercised_tools.

Plus code-review nits:

- Simplified discoverTools utility-shape skip to NON_OPERATION_ALLOWLIST
  only; documented the contract.
- Removed unused schema parameter from classifyRequestMutating.
- Tightened indexScenarioTasks predicate to require phases array.
- Added cross-reference comment between MANIFEST_PROTOCOLS and the
  meta-schema's protocol enum.
- Changeset now mentions /schemas/latest/manifest.json for nightly
  codegen consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit b44996f)

* fix(3.0.x): trim enumMetadata to 3.0.x codes; downgrade #3738 to patch

The cherry-pick of #3738 brought in enumDescriptions + enumMetadata entries
for SCOPE_INSUFFICIENT, READ_ONLY_SCOPE, and FIELD_NOT_PERMITTED — three
error codes added to main in PRs that aren't on 3.0.x. Their enum entries
weren't introduced (3.0.x's enum array is unaffected), but the description
and metadata blocks were left referencing them. The new lintErrorCodeEnumMetadata
guardrail catches this and refuses to build.

Trim: drop the three orphan entries from enumDescriptions and enumMetadata.
Counts now agree at 45 / 45 / 45.

Also downgrades the changeset from minor to patch — same rationale as the
other cherry-picks on this branch: maintenance line, no new enum values,
no wire-format change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(errors): tighten AUTH_REQUIRED prose to warn on retry storms (3.0.x prose-only backport of #3739)

3.0.x cannot adopt the AUTH_MISSING/AUTH_INVALID split from #3739 — adding
new enum values violates the maintenance line's semver rules. This is the
prose-only backport: same wire code, same recovery class, but the
description and enumMetadata.suggestion now spell out the two sub-cases
(missing vs. presented-but-rejected) and the SHOULD-NOT-auto-retry rule.

Closes 3.0.x portion of #3730.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>

* ci(release): un-exclude dist/compliance + forward-merge auto-resolution (#3794) (#3800)

* ci(release): un-exclude dist/compliance + forward-merge auto-resolution

Two release-pipeline hardening fixes for 3.0.4 and beyond.

.dockerignore: un-exclude dist/compliance/ so versioned URLs (the
/compliance/{version}/ tree) actually serve from the Fly image. The
ignore pattern was set up for /schemas/ and /protocol/ but never updated
when /compliance/ versioned routing was added. Result: every committed
dist/compliance/3.0.X/ directory was stripped from the build context,
and SDKs fetching compliance bundles by URL hit 404s on fresh-cache
scenarios. Re-include dist/compliance, then re-exclude
dist/compliance/latest (regenerated in-container by build:compliance).

forward-merge-3.0.yml: replace the bare-merge approach with auto-
resolution on an explicit allowlist of always-divergent paths
(package.json version, .changeset/*.md, dist/*, CHANGELOG.md, schema
source index files). Drop the brittle is-ancestor shortcut that
returns false after squash-merges even when content is in main; use
post-merge git diff --quiet to skip cleanly when main already has
3.0.x's content. Conflicts outside the allowlist fail the workflow
loud, surfacing playbook violations (a change on 3.0.x that wasn't
first cherry-picked from main).

Updates .agents/playbook.md § Release lines to document the new
auto-resolution behavior so reviewers know what to spot-check.

Closes the operational pain that bit us today on PR #3783, where the
v3.0.3 cut's forward-merge needed three manual conflict resolutions
(package.json, .changeset/fix-url-type-tracker-pixel-channel-docs.md,
static/schemas/source/index.json) plus a manual unshallow before
the merge could complete.



* ci(release): address expert review feedback on forward-merge auto-resolution

Code review (af5969...): empty-CONFLICTS-after-merge-failure guard so
hook rejections / unrelated-history failures fail loud instead of
silently committing the empty merge.

Security review (a16289...): tighten dist/* glob to explicit
{schemas,compliance,protocol,docs}/* list so future mutable subtrees
under dist/ don't silently auto-resolve via --theirs.

Plus: drop `2>/dev/null || true` on `git rm` (let real errors surface;
the post-loop REMAINING check already guards against bad state). Add
post-resolution `git status --short` and `git diff vs origin/main --
package.json` log groups so reviewers can spot main-unique scripts
that may have been overwritten without leaving GitHub.

No functional change to the happy-path resolution behavior.



---------


# Conflicts:
#	.agents/playbook.md

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(release): forward-merge package.json --ours, not --theirs (#3807) (#3808)

Original --theirs rule stripped main's structural changes when 3.0.x
hadn't been updated to match. Concrete case: main renamed @adcp/client
to @adcp/sdk + bumped to 5.25.1; 3.0.x stayed on @adcp/client@5.21.1.
The forward-merge took 3.0.x's package.json wholesale, leaving the
package-lock out of sync. CI broke with "npm ci ... package.json and
package-lock.json or npm-shrinkwrap.json are in sync". PR #3806's CI
exposed this.

--ours preserves main's state. Main's pre-mode tracking is independent
of 3.0.x's version field; the dist/* artifacts still flow forward via
the allowlist; main's structural changes survive.

Trade-off: main's package.json version doesn't reflect 3.0.x's latest
release. Acceptable — main's version field isn't authoritative while
pre-mode is active. The next main pre-mode cut produces 3.1.0-beta.X
from accumulated changesets regardless of base version.

Companion playbook + PR-body checklist update so docs match behavior.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(release): bridge cherry-pick divergence in forward-merge (#3811) (#3814)

Adds two specific files to the auto-resolution --ours allowlist where
3.0.x has #3789's hand-adapted prose-only backport of #3739 and main
has the full enum split (adds AUTH_MISSING / AUTH_INVALID codes that
can't ship to 3.0.x without violating patch eligibility).

  - docs/building/implementation/error-handling.mdx
  - static/schemas/source/enums/error-code.json

Without this rule, every routine forward-merge from 3.0.x → main
rediscovers the same conflict because squash-merges of prior
forward-merges (the only merge style this repo allows) don't advance
git's merge-base. The post-merge `git diff --quiet` skip can't reach
to detect "main already has this content" because the merge fails
before that step.

Marked temporary in the workflow comments — remove when 3.1.0 cuts
and main no longer has the in-flight enum split.

Without this fix, the next forward-merge after 3.0.4 cuts would
fail loud on these same two files, requiring another manual
resolution PR. With it, 3.0.4's forward-merge auto-succeeds.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3799)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* ci(release): push forward-merge branch before peter-evans runs (#3818) (#3820)

Discovered when 3.0.4's forward-merge ran for real for the first time
(run 25250971971): auto-resolution worked perfectly (the new allowlist
+ bridge handled every conflict), but peter-evans/create-pull-request
crashed with "fatal: ambiguous argument 'origin/forward-merge/3.0.x'"
because the remote branch didn't exist yet.

peter-evans's internal `git reset --hard origin/forward-merge/3.0.x`
flow assumes the remote-tracking branch already exists. On a first run
(or any time the remote branch isn't there), it fails. Pushing
explicitly after auto-resolution establishes the ref so peter-evans's
reset has a target.

After this lands + cherry-picks to 3.0.x, the next VP cut (3.0.5 or
3.1.0) will auto-create the forward-merge PR without manual
intervention.

For 3.0.4 specifically: I'll open the PR manually since this fix
requires a workflow change that hasn't run yet.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(storyboard-schema): add optional default_agent field (closes #3894) (#3897)

* spec(storyboard-schema): add optional default_agent field (closes #3894)

Adds an optional top-level default_agent: <key> field to the storyboard
authoring schema. The multi-agent runner resolves the logical key (sales,
governance, creative, …) against the runtime agents map passed to
runStoryboard({ agents: {…} }) — see adcp-client#1066 / #1355.

The runner already accepts default_agent via run-options. This change
lets storyboard authors encode the topology intent in YAML once instead
of re-asserting it on every CI invocation. Cross-domain tools
(sync_creatives, list_creative_formats, comply_test_controller) route
deterministically without per-step agent: overrides.

Strictly additive — single-agent runs ignore it, existing 3.0.x
storyboards keep working, pre-existing run-options default_agent keeps
its lower-precedence slot. Mirrors the provides_state_for precedent
(#3775) for additive storyboard-schema affordances on 3.0.x.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(storyboard-schema): tighten default_agent contract per expert review

Address protocol- and product-expert review on PR #3897:

- Slot 2: state explicitly what zero/one/multi specialism claimants do.
  Multi-claim grades unrouted_step (operator-config error); slots 3/4 do
  NOT rescue. Zero falls through to slot 3.
- Slot 3: when the storyboard declares default_agent and the key is
  absent from the runtime map, grade default_agent_unresolved — do NOT
  silently fall to slot 4. Silent fallback would invisibly override the
  storyboard author's encoded intent. Slot 4 fires only when the field
  is unset.
- Slot 4: same set-but-unmatched rule applied symmetrically.
- Key shape: free-form non-empty string keyed by the runtime agents map.
  Spec does NOT constrain to the specialism enum — production topologies
  legitimately fan out per-property / per-region / per-rights-holder.
  Cross-operator portability is the author's concern, not the spec's.
- Drop comply_test_controller from the cross-domain example — it's
  routed via prerequisites.controller_seeding, not default_agent.
- Disambiguate adcp-client#1355 reference (was bare "#1355").

No wire-protocol surface change; doc-only edit to the storyboard
authoring schema (already a comment-block YAML).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(capabilities): relax identity.additionalProperties to true (3.0.x) (#3896)

The identity object on get-adcp-capabilities-response was schema-closed
(additionalProperties: false), so any 3.0-pinned operator adopting a
forward-compatible field — notably identity.brand_json_url from #3690,
intended to be readable on 3.0 without a schema bump — would have its
capabilities response rejected by strict 3.0 validators (e.g.,
@adcp/sdk's createAdcpServer default).

Mirrors the relaxation already on main (post-#3690). Closed property
list (per_principal_key_isolation, key_origins, compromise_notification)
is unchanged; this is strictly additive forward-compat.

The forward-compat narrative in security.mdx ("3.0-pinned implementers
can adopt the field today without bumping") depends on this being live
in the shipped 3.0 schema — without it, the spec advice contradicts the
schema.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(storyboard): capture rights_id from acquire_rights response (closes #3892) (#3893)

The brand-rights storyboard step `acquire_rights` captured `rights_grant_id`
from the response, but `brand/acquire-rights-response.json` defines the field
as `rights_id`. Spec-compliant agents passed response_schema validation but
failed context capture, cascade-skipping `rights_enforcement`.

Update the YAML to read `rights_id` (preserving the storyboard-internal
`rights_grant_id` key so no other steps need to change) and correct the
`expected:` prose to match the published schema (rights_id + status: acquired).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3898)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs(skill): document implementation-dependent issues[] fields (3.0.x backport of #3927) (#3931)

Backports the SKILL.md update onto 3.0.x so 3.0.6 carries the four implementation-dependent issues[] field bullets + 2 symptom-fix table rows. Doc-only, identical to #3927 on main.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* 3.0.6 cherry-picks: governance wire-placement + ctx_metadata reservation + storyboard fixture fixes (#3996)

* spec(errors): wire-placement guidance for GOVERNANCE_DENIED / GOVERNANCE_UNAVAILABLE (#3929)

error-code.json described what each code means but not WHERE on the response
it appears. Storyboards interpreted differently — #3914 surfaced the mismatch
where the brand-rights compliance storyboard expected adcp_error.code:
GOVERNANCE_DENIED even though acquire_rights already has a first-class
AcquireRightsRejected discriminated arm.

GOVERNANCE_DENIED — structured business outcome (governance call SUCCEEDED,
agent returned a denial verdict). When the task response defines a rejection
arm (e.g., AcquireRightsRejected, CreativeRejected), that arm IS the canonical
denial shape — populate reason, do NOT also emit the code in errors[]/
adcp_error, transport-level success markers stay green. The schema layer
already enforces this via `not: { required: [errors] }` on those arms; the
doc-comment makes the rule discoverable from the error code. When no rejection
arm exists (e.g., create_media_buy), populate errors[] + adcp_error per the
two-layer model and flip transport markers.

GOVERNANCE_UNAVAILABLE — system error (governance call FAILED). Always errors[]
+ adcp_error, transport markers always flip. Never use a rejection arm.

Also adds a parallel storyboard-authoring note in error-handling.mdx: when
asserting against rejection-arm denials, use `check: field_value, path:
"status", value: "rejected"` instead of `check: error_code` — the spec-correct
response carries no code on the wire.

Closes the doc-comment item on #3918; companion to #3914 (storyboard fix is
separate work).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(conventions): reserve ctx_metadata as adapter-internal round-trip key (#3640) (#3788)

* spec(conventions): reserve ctx_metadata as adapter-internal round-trip key (#3640)

Closes #3640.

Reserves `ctx_metadata` as a top-level adapter-internal round-trip cache key on
AdCP resource objects (Product, MediaBuy, Package, Creative, AudienceSegment,
Signal, RightsGrant). SDKs MUST strip the key before wire egress and MUST emit
a warning-level log when stripping. Buyers never see the field.

Convention is non-binding at the wire level — these resources already declare
additionalProperties: true. PropertyList and CollectionList are out of scope
(additionalProperties: false) until a follow-up PR widens those schemas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(conventions): tighten ctx_metadata scope language and warn-log condition

Two reviewer-flagged clarifications on the ctx_metadata reservation:

- Scope: state explicitly that the reservation travels with the resource
  wherever it appears (top-level, nested, in arrays). Closes the read where
  someone could interpret the rule as "applies only at envelope top level."

- Warn-log condition: the RULE paragraph and the conformance pseudocode
  disagreed on when to emit the warning (RULE said "when stripping",
  pseudocode said "when present and non-empty"). Align both on the
  non-empty rule — silence on absent/empty values avoids logspam from
  resources that just don't carry adapter state.

- Add a one-line disambiguation against context / context_id, since the
  shared "ctx" prefix could mislead a reader.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(compliance): storyboard fixture fixes — inventory_list_targeting sandbox routing + sales_guaranteed task-completion path (mirror of #3989 / #3990)

Storyboard-fixture-only fixes applied directly to the 3.0.x line, mirroring the same diffs filed against main in #3989 and #3990. Storyboard fixtures ship in the compliance bundle that goes out with each release, so 3.0.x consumers running the worked-example seller (hello_seller_adapter_guaranteed from @adcp/sdk 6.7+) hit both bugs today.

inventory_list_targeting: the 5 account blocks were missing sandbox: true. The SDK runner's create_media_buy enricher inherits sandbox: true from the test-kit, but its get_media_buys enricher merges the storyboard's bare account block — different accountId → mediaBuyStore can't backfill targeting_overlay → verify_create_persisted / verify_update_persisted fail. Setting sandbox: true on every account block keeps create and get on the same namespace.

sales_guaranteed/create_media_buy: the step uses the spec-correct guaranteed-seller flow (A2A submitted-arm envelope; media_buy_id materializes on the task-completion artifact). The bare context_outputs path "media_buy_id" resolved against the immediate response, producing capture_path_not_resolvable. Changed to "task_completion.media_buy_id" so the runner polls tasks/get and captures the seller-issued id from the terminal artifact, per the runner contract introduced in adcp-client#1426.

Empty changeset (non-protocol fixture-only changes); no version bump.

Refs:
- #3989 (main-side inventory_list_targeting fix)
- #3990 (main-side sales_guaranteed fix)
- adcontextprotocol/adcp-client#1487 (follow-up: align get_media_buys enricher account resolution)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 3.0.6 follow-up cherry-picks: task_completion. prefix docs + comply_test_controller deployment-scope clarification (#4002)

* docs(storyboard-schema): document task_completion. prefix for context_outputs (#3955)

When the immediate response is a non-terminal task envelope (status
submitted/working/input-required), the runner polls tasks/get until
terminal and resolves the suffix against the completion artifact's
data. Required for captures like seller-assigned media_buy_id on
IO-signing / async-signed HITL flows. Requires runner >= adcp-client
v6.7; older runners treat the prefix as a literal key.

Closes #3950.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(spec): comply_test_controller is deployment-scoped, not request-gated (#3992)

* docs(spec): comply_test_controller is deployment-scoped, not request-gated

Production deployments MUST NOT expose comply_test_controller on any
surface: tools/list MUST omit the tool, get_adcp_capabilities MUST omit
the compliance_testing block, dispatch MUST return unknown-tool.
A production deployment that exposes the tool is non-conformant
regardless of whether dispatch is gated.

The canonical pattern is two deployments — one production with no
controller wired, one sandbox/staging with the controller wired for
all comers. Per-principal projection on a single deployment remains
permitted as an implementation pattern, not the canonical model.

FORBIDDEN is reserved for the in-sandbox case where params reference
a non-sandbox account; live-mode probes get the transport's standard
unknown-tool error so the response is byte-identical to a seller that
does not implement the tool.

Closes #3986. Verifier-provisioning question
moves to #3991 (Socket Mode conformance client).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(spec): address expert review on comply_test_controller scoping

- Pair tools/list (MCP) and skills[] (A2A) symmetrically across the rule;
  agent-card discovery is cache-friendly so the leak surface is at least
  as bad on A2A
- Replace "byte-identical" with "indistinguishable from the same-transport
  response of a seller that does not implement the tool" — JSON-RPC -32601
  and A2A unknown-skill aren't byte-identical to each other
- Strengthen the per-principal MAY carve-out: all three surfaces (tools/list,
  capability block, dispatch) MUST be projected consistently, and live-mode
  probes on a mixed deployment must dispatch to unknown-tool not FORBIDDEN
  (otherwise the side channel reopens)
- Strengthen storyboard-runner SHOULD → MUST and add symmetric
  capability-block check
- Reorder Sandbox gating: rule → canonical pattern → permitted alternative
  → FORBIDDEN reservation → ops/runner notes
- Reconcile the stale :::note in get_adcp_capabilities.mdx that still
  described request-time gating
- Update implementation-guidance line so it doesn't undercut the new rule

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Version Packages (#3933)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs(release-notes): add 3.0.6 section to /docs/reference/release-notes

* changesets: add empty changeset for forward-merge 3.0.x → main

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: aao-release-bot[bot] <280565558+aao-release-bot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Brian O'Kelley <bokelley@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant