Episode Publishing — Another Orange Morning (daily HubSpot show)

← Back to Operating Procedures episode-publishing-aom.md

title: "Episode Publishing — Another Orange Morning (daily HubSpot show)" owner: Marquee (Media BU Leader) domain: media risk_tier: 1 verification_rigor: "Tier 1 — Critical Pipeline (production-touching, multi-system, daily-frequency)" version: 1.0 last_updated: 2026-05-07 status: "Operational with mandatory pitfall checks — codifies AOM Paste-to-Publish launch (2026-05-06) + first end-to-end episode publish (2026-05-07 Casey paste-to-publish flow pending)"

Episode Publishing — Another Orange Morning (daily HubSpot show)

Process: End-to-end daily publishing rhythm Casey Hawkins runs after each AOM live show — the recap pair (show-recap + updates-blog), the granular HubSpot Update articles (Paste-to-Publish), the Mux asset link, and the cross-link graph between them. Owner: Marquee (Media BU Leader) Authoring delegations: Canon, Ledger, Splice, Quinn, Aegis, Pixel, Showcase, Mirror, Curator, Baldwin Last Verified: 2026-05-07 (against Paste-to-Publish launch 2026-05-06 + recap pair publish 2026-05-05; first end-to-end Casey-driven daily run pending) Verification Score: 70% (14 VERIFIED / 20 applicable steps) — Partially Verified, trending Operational Status: Operational with mandatory pitfall checks First execution (recap pair): 2026-05-05 (anotherorangemorning.com/recap/2026-05-05/show-recap + /updates-blog) First execution (Paste-to-Publish chain): 2026-05-06 (Breeze AI Agents GA, rollout=315000) Sibling procedure: TRTU per-episode publishing — docs/operating-procedures/episode-publishing.md (different show, different surfaces, shared owner) Show-launch procedure: docs/operating-procedures/show-launch.md (one-time activation; AOM activated 2026-04-30)


Operating Principle

A new (cold-start) session should be able to execute Casey's daily AOM publishing rhythm end-to-end without re-discovering the lessons learned during the May 5–7 launch window. Every step has an owning agent, a verification protocol, and an explicit pitfall callout where a prior execution went sideways. If a step claims VERIFIED, the evidence is dated and reproducible. If a step is UNTESTED at this run, the marker says so — do not invent confidence.

Distinct from TRTU episode-publishing. AOM is a daily show with a paste-driven editorial layer (granular HubSpot Update articles authored from HubSpot's release notes); TRTU is a weekly ensemble show with a Riverside-export pipeline. Both are Marquee-owned, both terminate at recap article + HubSpot Listing, but their sub-flows do not overlap. This procedure is the AOM analog and shares its sibling's verification rigor — not its mechanics.


Operating Principle (cont.) — verification rule

Never claim a step complete on the basis of "should work" reasoning. Every step that writes to a downstream system requires the verify command shown in its Verify with block to be executed and its actual output read. Canon writes specifically must produce a GROQ query result attached to the report, per feedback_canon_verify_after_write.md (4-defect patch round during the May 5 recap pair launch). The HTML→PortableText conversion at Step 3 must include both a markDef link count match and a strong-mark span count match against source HTML — link/strong stripping was the load-bearing bug on May 5.


Three intertwined sub-flows

This procedure covers three sub-flows that run on different cadences but share canonical inputs:

Sub-flow What it produces Cadence Trigger
A — Recap pair publishing Paired Sanity recapArticle documents (show-recap + updates-blog) + paired HubSpot Listings After each live show Casey stages show-recap.html + updates-may-N.html in the repo
B — Mux asset link Episode muxVideo.asset._ref patched on the Sanity episode document ~30 min post-show (Mux finalization) Mux asset reaches ready status for AOM live stream
C — Paste-to-Publish (granular HubSpot Updates) Sanity hubspotUpdate documents + paired HubSpot Listings + (optional) cross-link cards inside Sub-flow A's updates-blog On-demand throughout the morning Casey opens /admin/paste-to-publish and pastes a HubSpot release-note URL + raw text

Bridge between sub-flows: Sub-flow A's updates-blog body embeds hubspotUpdateLink Portable Text blocks pointing to the granular hubspotUpdate documents created by Sub-flow C. The cross-link mechanism's GROQ projection bug was caught and fixed May 6 (commit 5ca6e07: project slug onto each hubspotUpdateLink block via GROQ). See Step 18 for the cross-link insertion pattern.


Inputs (what arrives from Casey before any system writes)

Input Source Format Notes
show-recap.html Casey, staged in repo HTML fragment (h3 sections, p, strong, a) Long-form recap of the day's show. Replaces the placeholder [UPDATES_BLOG_URL] after Step 7.
updates-may-N.html Casey, staged in repo HTML fragment (h3 sections, h4 sub-sections, p, strong, a) Daily updates blog covering yesterday's HubSpot release. Replaces the placeholder [SHOW_RECAP_URL] after Step 7.
Casey's README.md Casey, staged in repo Markdown Hosts, segments covered, publishing status, artifact table. Source of truth for episode metadata.
Live HubSpot product update URL + raw text HubSpot release notes at https://app.hubspot.com/l/product-updates/ URL + multi-paragraph text One per granular hubspotUpdate article. Casey copy-pastes into /admin/paste-to-publish.

Path: clients/vf-team/content/media_network/Another_Morning_Show/episodes/{YYYY-MM-DD}/

The repo path is canonical input. Examples:

  • clients/vf-team/content/media_network/Another_Morning_Show/episodes/2026-05-06/ (commit b66df9f5f)
  • clients/vf-team/content/media_network/Another_Morning_Show/episodes/2026-05-07/ (commit 60a1f8c1d)

Each folder ships with README.md + show-recap.html + updates-may-N.html. Read them — these are Casey's canonical inputs.


Agent Ownership Table

Step Sub-flow Phase Agent Why
1 A Source ingestion Marquee (with Curator read-only) BU leader confirms Casey's drafts are present + read-only inventory
2 A HTML → Portable Text conversion Canon Sanity write gateway; conversion fidelity is load-bearing
3 A Recap pair Sanity write (paired recapArticle documents) Canon Sanity write gateway (mandatory)
4 A relatedRecap cross-references (both directions) Canon Sanity write gateway
5 A show._ref + episode._ref resolution Canon Sanity write gateway; deterministic IDs only
6 A recapPublishedAt timestamp Canon Sanity write gateway
7 A URL placeholder replacement ([SHOW_RECAP_URL], [UPDATES_BLOG_URL]) Canon Inline within Step 3-6 spawn; both URLs known after both writes
8 A Paired HubSpot Listings (recap_article content type) Ledger HubSpot write gateway (mandatory)
9 A Recap pair render verification Mirror (read-only) Visual QA on the live site
10 B Mux asset identification Splice Recording linking specialist; AOM live_stream_id filter
11 B Episode muxVideo.asset._ref patch Canon (spawned by Splice) Sanity write gateway (mandatory)
12 B Latest selector verification Mirror (read-only) Confirm home-page Latest section returns the new episode
13 C Paste-to-Publish admin auth (magic link) (Endpoint code) API endpoint owns auth; HubSpot workflow fires the email
14 C Dedupe check (Sanity GROQ on rolloutId) (Endpoint code) Pre-chain guard against duplicate writes
15 C Emerjent chain invocation (Quinn → Pixel) Aegis (chain wiring) + Quinn (system prompt) Aegis wires the chain; Quinn produces the structured JSON
16 C Hero image fallback resolution (Endpoint code) Per-Hub fallback registry; Pixel-live swap deferred to v1.0
17 C hubspotUpdate Sanity write (endpoint executes, NOT chain Canon) (Endpoint code) Architectural finding May 7 — Emerjent's Canon agent produces reasoning, not API mutations
18 C HubSpot Listing creation (endpoint executes, NOT chain Ledger) (Endpoint code) Same architectural finding
19 C Pre-return verification GROQ (Endpoint code) Endpoint-side post-write check before redirect
20 C (Optional) Cross-link card insertion in Sub-flow A's updates-blog Casey via Sanity Studio Replaces inline summary with hubspotUpdateLink block

Routing principle (May 7 lesson): Emerjent chain agents named Canon and Ledger produce reasoning JSON, not API mutations. The endpoint must own the data layer (Sanity write at Step 17, HubSpot Listing creation at Step 18). The team's gateway agents Canon and Ledger remain canonical for human-initiated writes inside Sub-flow A and Sub-flow B; the chain naming collision is a known artifact of the Emerjent agent registry.

Routing principle (May 6 lesson): Splice presented "delegating to Canon" as completion without actually firing the spawn. Splice may NEVER claim Sub-flow B complete on the basis of "I will delegate" reasoning — the Canon spawn must have actually been invoked, the GROQ verify must have actually returned the patched ref. See Pitfall (Ep May 6) #8.


Procedure

Sub-flow A — Recap pair publishing

Step 1 — Confirm Casey's drafts are present (Marquee, Curator read-only)

What happens: Marquee confirms that Casey has staged show-recap.html + updates-may-N.html + README.md in the canonical repo path. Curator performs a read-only file inventory and surfaces any missing or empty files.

Who does it: Marquee (BU leader confirmation); Curator (read-only file inventory) System writes: None Trigger: Casey commits the day's drafts (recent examples: commits b66df9f5f 2026-05-06, 60a1f8c1d 2026-05-07)

Verify with:

ls -la /mnt/d/Projects/value-first-operations/clients/vf-team/content/media_network/Another_Morning_Show/episodes/{YYYY-MM-DD}/
# Expected: README.md + show-recap.html + updates-may-N.html (3 files, all non-empty)

Expected outcome: Three files present, each non-empty. README.md lists hosts and segments covered. Both HTML files contain <h3> section headers; updates-may-N.html contains at least one <a href="https://app.hubspot.com/l/product-updates/?rollout=..."> anchor.

Status: VERIFIED 2026-05-06 (May 5 episode) and 2026-05-07 (May 6 + May 7 episodes) — Casey's repo staging pattern is reliable; three files always present, README.md always lists hosts and segments, HTML files always include rollout-anchor product update links.

Pitfall: Casey's drafts are evidence of an authored show. Synthesis files in clients/vf-team/sessions/ are NOT a substitute and may not reflect the day's editorial decisions (per wiki/conventions.md § Data tier hierarchy). Read the staged HTML and README directly — never derive from a synthesis.


Step 2 — Convert HTML to Portable Text (Canon)

What happens: Canon converts both HTML files to Sanity Portable Text format. Each file becomes the body array of its respective recapArticle document.

Conversion fidelity requirements (load-bearing):

HTML element Portable Text representation Verification check
<h3> block with style: "h3" Section count matches <h3> count in source
<h4> block with style: "h4" Sub-section count matches <h4> count in source
<p> block with style: "normal" Paragraph block count matches <p> count in source
<strong> inline span with marks: ["strong"] strong-mark span count matches <strong> count in source
<a href="..."> inline span with marks: [<markDefKey>] + matching markDefs entry of _type: "link" with the href markDef link count matches <a> count in source
Inline rollout anchors (https://app.hubspot.com/l/product-updates/?rollout=N) preserved as link markDefs Naked-URL check: zero plain-text URLs in body

Who does it: Canon (HTML→PT conversion is part of the Sanity write gateway's writes; do not perform conversion outside Canon) System writes: None at this step (Canon prepares the PT array; the actual write happens at Step 3) Trigger: Step 1 complete

Verify with (REQUIRED — run BEFORE the Sanity write at Step 3):

# Source-side counts
grep -c '<h3>' /path/to/show-recap.html        # = expected H3 block count
grep -c '<h4>' /path/to/updates-may-N.html     # = expected H4 block count in updates blog
grep -oc '<strong>' /path/to/show-recap.html   # = expected strong-mark span count
grep -oc '<a ' /path/to/show-recap.html        # = expected markDef link count

# Compare against Canon's prepared PT (during Canon's spawn, Canon must report all four counts)

Expected outcome: Canon's prepared PT array reports H3 block count, H4 block count, strong-mark span count, and markDef link count — all four match the source-HTML grep counts above. Naked-URL check returns zero plain-text URLs in body content.

Status: VERIFIED 2026-05-07 — Conversion fidelity protocol added after May 5 stripping bug. Both 2026-05-06 and 2026-05-07 recap pair drafts now passing the four-count match prior to publish.

Pitfall (Ep May 5) — load-bearing bug: On the first 2026-05-05 publish, Canon's HTML→PT conversion silently dropped both inline product-update anchors AND <strong> marks. The recap pair rendered on the live site with corrupted body content; Mirror + Baldwin caught the gap during post-publish QA. Re-parsing both articles required a 4-defect patch round before the launch was clean. The four-count verify protocol above is now binding — running this check BEFORE Step 3's write is what prevents the May 5 regression.


Step 3 — Create paired recapArticle documents (Canon)

What happens: Canon writes two paired Sanity recapArticle documents using deterministic IDs:

Document Deterministic ID Variant
Show recap recap-another-orange-morning-{YYYY-MM-DD}-show-recap "show_recap"
Updates blog recap-another-orange-morning-{YYYY-MM-DD}-updates-blog "updates_blog"

Required fields on each document:

Field Value Notes
_id Deterministic per table above Never random Sanity ID
_type recapArticle Schema shipped with AOM, commit 1587feaeb
variant "show_recap" or "updates_blog" Determines render template
title Editorial title from <h3> in source HTML, or Casey-confirmed override NEVER inferred from filename
slug.current Slugified date (2026-05-07) — slug is the date because URL pattern is /recap/{date}/{variant} Set BOTH on the show-recap and updates-blog documents — they share the same slug
body Portable Text array from Step 2 All four fidelity counts must match source HTML
show reference to AOM show document See Step 5
episode reference to AOM episode document See Step 5
recapPublishedAt ISO 8601 timestamp See Step 6
relatedRecap reference to the OTHER document in the pair See Step 4

Who does it: Canon (Sanity write gateway — mandatory; direct patch.js calls by other agents are governance bypasses) System writes: Sanity (production dataset, project 0efm0pow) — two new documents Trigger: Step 2 complete with all four conversion fidelity counts confirmed

Verify with (REQUIRED) — Query A (existence + variant):

node scripts/sanity/query.js custom "*[_id in ['recap-another-orange-morning-{date}-show-recap', 'recap-another-orange-morning-{date}-updates-blog']]{_id, _type, variant, title, 'slug': slug.current, 'bodyBlocks': count(body)}"

Expected outcome: Both documents returned. _type is recapArticle on both. variant is show_recap and updates_blog respectively. title is non-null on both. slug is the date string. bodyBlocks is non-zero and matches the source HTML's combined <h3> + <h4> + <p> count for each document.

Status: VERIFIED 2026-05-05 — Initial publish landed with both documents present; required 2 correction cycles to populate all fields correctly. Subsequent May 6 + May 7 staging runs landed cleaner.

Pitfall — Canon verify-after-write protocol (MANDATORY): During the May 5 launch, Canon reported "complete" three separate times on Sanity writes that subsequent GROQ verification proved were null/missing/wrong-typed. feedback_canon_verify_after_write.md is binding: every Canon spawn that writes recap pair documents MUST include the explicit Query A / Query B / Query C verify GROQ queries Canon executes after each write phase, with actual results pasted in the report. Never trust _rev alone — _rev only proves the document was patched, not what fields were set.


Step 4 — Wire relatedRecap cross-references (Canon)

What happens: Canon patches the relatedRecap field on each document to reference the OTHER document in the pair:

  • recap-another-orange-morning-{date}-show-recaprelatedRecap = ref to recap-another-orange-morning-{date}-updates-blog
  • recap-another-orange-morning-{date}-updates-blogrelatedRecap = ref to recap-another-orange-morning-{date}-show-recap

Who does it: Canon (same spawn as Step 3 OR separate spawn if Step 3 was multi-defect) System writes: Sanity (two patches) Trigger: Step 3 complete with both documents present

Verify with (REQUIRED) — Query B (cross-reference resolution):

node scripts/sanity/query.js custom "*[_id in ['recap-another-orange-morning-{date}-show-recap', 'recap-another-orange-morning-{date}-updates-blog']]{_id, 'relatedRecapId': relatedRecap._ref, 'relatedRecapResolves': defined(relatedRecap->_id)}"

Expected outcome: Both documents return with relatedRecapId pointing at the other document's _id, and relatedRecapResolves is true on both.

Status: VERIFIED 2026-05-05 — Pair successfully cross-linked after first patch round; render confirms both /recap/2026-05-05/show-recap and /recap/2026-05-05/updates-blog show "Read the paired recap" cards pointing at each other.

Pitfall: Canon must NOT use drafts.recap-... as the _ref value. Use the published _id directly. The drafts. prefix corrupts the cross-reference and the paired-recap card renders blank. See May 5 mistake at Step 5 (show._ref = "drafts.show-another-orange-morning") — same root cause, different field.


Step 5 — Resolve show._ref + episode._ref (Canon)

What happens: Canon sets the show and episode references on both recap documents:

  • show._ref = "show-another-orange-morning" (NEVER "drafts.show-another-orange-morning")
  • episode._ref = "episode-another-orange-morning-{date}" (deterministic ID format; NEVER a Sanity-generated random ID)

Who does it: Canon (typically same spawn as Step 3) System writes: Sanity (two patches) Trigger: Step 3 complete

Verify with (REQUIRED) — Query C (ref resolution + identity check):

node scripts/sanity/query.js custom "*[_id in ['recap-another-orange-morning-{date}-show-recap', 'recap-another-orange-morning-{date}-updates-blog']]{_id, 'showRef': show._ref, 'showResolvesTo': show->_id, 'episodeRef': episode._ref, 'episodeResolvesTo': episode->_id}"

Expected outcome: Both documents return with:

  • showRef = "show-another-orange-morning" — exact match, no drafts. prefix
  • showResolvesTo = "show-another-orange-morning" — confirms ref resolves to the published show document
  • episodeRef = "episode-another-orange-morning-{date}" — deterministic format
  • episodeResolvesTo = the same — confirms ref resolves to the canonical AOM episode for that date, NOT to another show's episode

Status: VERIFIED 2026-05-05 — After 4-defect patch round. The patch round was triggered specifically by this step's verification catching two distinct bugs (see Pitfalls).

Pitfall (Ep May 5) — show._ref drafts prefix: On the first publish, Canon set show._ref = "drafts.show-another-orange-morning". The drafts prefix made the show reference unresolvable in published GROQ queries; the recap pair render displayed empty show metadata. Fixed by Canon re-spawn that explicitly stripped the prefix.

Pitfall (Ep May 5) — wrong episode reference: On the first publish, Canon set episode._ref to the deterministic ID for the Value-First AI Daily episode of the same date instead of the AOM episode. This is an ambient hazard when multiple shows record on the same day — the episode-{show-slug}-{date} format means a date alone is insufficient to identify the canonical episode. Always include the show-slug prefix in the deterministic ID, and always run Query C above to confirm episodeResolvesTo is the AOM episode, not another show's.


Step 6 — Set recapPublishedAt timestamp (Canon)

What happens: Canon patches recapPublishedAt on both documents to the publish timestamp in ISO 8601 format. AOM is a Central Time show, so the canonical timestamp is in America/Chicago per feedback_central_time_canonical.md.

Who does it: Canon System writes: Sanity (two patches) Trigger: Step 3 complete

Verify with:

node scripts/sanity/query.js custom "*[_id == 'recap-another-orange-morning-{date}-show-recap'][0]{recapPublishedAt}"

Expected outcome: ISO 8601 timestamp with America/Chicago offset. Date portion matches the show's recording date.

Status: VERIFIED 2026-05-05 — Both documents stamped with America/Chicago timestamp.

Pitfall: Do NOT compute timestamps in agent reasoning. Use date -d or ./scripts/datetime.sh per wiki/conventions.md § Date and time conventions. Trusting model-derived timestamps caused a 6-month timezone drift bug in Slate that motivated feedback_central_time_canonical.md (2026-05-03).


Step 7 — Replace placeholder URLs (Canon)

What happens: Casey's source HTML drafts contain two placeholder tokens that must be replaced with the actual paired-recap URLs after both documents exist:

Placeholder Lives in Replace with
[SHOW_RECAP_URL] updates-may-N.html body https://anotherorangemorning.com/recap/{date}/show-recap
[UPDATES_BLOG_URL] show-recap.html body https://anotherorangemorning.com/recap/{date}/updates-blog

The replacement happens inside the Portable Text body — Canon must patch the relevant text spans, not the source HTML files (which remain canonical input).

Who does it: Canon (typically inside Step 3 spawn after both documents exist) System writes: Sanity (two patches across both documents) Trigger: Steps 3 and 4 complete (both documents and their cross-refs exist; the URLs are deterministic from the slug)

Verify with:

node scripts/sanity/query.js custom "*[_id in ['recap-another-orange-morning-{date}-show-recap', 'recap-another-orange-morning-{date}-updates-blog']]{_id, 'remainingPlaceholders': length(pt::text(body)[match: '\\[SHOW_RECAP_URL\\]|\\[UPDATES_BLOG_URL\\]'])}"

Expected outcome: remainingPlaceholders is 0 on both documents.

Status: VERIFIED 2026-05-05 — Both URLs resolved in published bodies; no placeholder leakage to live site.

Pitfall: Replacing in source HTML and re-running Step 2's conversion is the wrong fix. The source HTML is canonical input from Casey and may be re-parsed in the future for a re-publish; placeholders in the source remain intentional. Patch the Portable Text spans in Sanity, not the source files.


Step 8 — Create paired HubSpot Listings (Ledger)

What happens: Ledger creates two paired recap_article Listings — one per Sanity recap document — with these properties:

Property Show recap value Updates blog value
name "AOM {date} — Show Recap" "AOM {date} — Updates Blog"
listing_content_type "recap_article" "recap_article"
recap_episode_id "episode-another-orange-morning-{date}" "episode-another-orange-morning-{date}"
recap_sanity_id "recap-another-orange-morning-{date}-show-recap" "recap-another-orange-morning-{date}-updates-blog"
recap_variant "show_recap" "updates_blog"
recap_show "another-orange-morning" "another-orange-morning"
recap_published_at "{date}" (ISO date) "{date}" (ISO date)
document_authors "marquee" "marquee"

Reference: skills/hubspot/property-index/listing.json for the recap_article content-type-specific properties.

Who does it: Ledger (HubSpot write gateway — mandatory) System writes: HubSpot (two new Listing records) Trigger: Steps 3-7 complete (Sanity documents exist, properly typed, all refs resolved)

Verify with:

node scripts/hubspot/api.js search listings --query "AOM {date}" --properties hs_object_id,name,listing_content_type,recap_episode_id,recap_sanity_id,recap_variant,recap_show,recap_published_at --limit 5

Expected outcome: Two Listings returned. Each has listing_content_type: "recap_article". recap_sanity_id matches its Sanity document _id. recap_variant distinguishes the pair (show_recap vs updates_blog).

Status: VERIFIED 2026-05-05 — Paired Listings created with IDs 553897453667 (show recap) and 553871010065 (updates blog). All required recap_* properties populated.

Pitfall: The recap_article content type's required properties are content-type-specific — they exist on the Listing object but are only meaningful when listing_content_type = "recap_article". Ledger must NOT default to the generic Listing property set; explicitly include the four recap_* properties on every paired-Listing write. Missing recap_sanity_id breaks the /recap/{date}/{variant} route resolver.


Step 9 — Recap pair render verification (Mirror, read-only)

What happens: Mirror runs a Playwright check on both URLs to confirm rendering and visual integrity.

URLs to verify:

  • https://anotherorangemorning.com/recap/{date}/show-recap
  • https://anotherorangemorning.com/recap/{date}/updates-blog

Who does it: Mirror (visual QA, read-only) System writes: None Trigger: Step 8 complete (both Listings exist; routes resolve)

Verify with:

curl -sI https://anotherorangemorning.com/recap/{date}/show-recap | head -3
curl -sI https://anotherorangemorning.com/recap/{date}/updates-blog | head -3
# Then Mirror Playwright check:
# - All <h3> section headers render
# - All <strong> emphasis renders bold
# - All <a> product update links resolve to https://app.hubspot.com/l/product-updates/...
# - "Read the paired recap" card renders with the OTHER URL
# - Hosts metadata renders (from show ref)

Expected outcome: HTTP 200 on both. All four render checks pass on both URLs.

Status: VERIFIED 2026-05-05 — Both URLs return 200, render checks pass post-conversion-fidelity-fix. Pre-fix, the strong-mark and link-stripping bug from Step 2 manifested here as Mirror catching the gap.

Pitfall: Fallback rendering is a failure, not a feature (per wiki/conventions.md § Engineering standards). If a section appears via a fallback path (e.g., default-styled body without h3 sections), do NOT celebrate — investigate why the conversion at Step 2 dropped the structure and re-run Canon with the fidelity counts.


Sub-flow B — Mux asset link

Step 10 — Identify the correct Mux asset (Splice)

What happens: Splice queries Mux for the asset created from the AOM live stream (live_stream_id SYiRPfXqBBZaznzQ5mVWCE5cZLzYcbaN7nPYlOXTIHY). Filter by recording date. Confirm asset status is ready. Confirm the asset is not already linked to a different episode.

Who does it: Splice (recording linking specialist) System writes: None at this step (read-only Mux query) Trigger: ~30 minutes after the show ends (Mux recording finalization window)

Verify with:

# Mux REST API GET https://api.mux.com/video/v1/assets?live_stream_id=SYiRPfXqBBZaznzQ5mVWCE5cZLzYcbaN7nPYlOXTIHY&page=1
# Filter to assets with status=ready and created_at on the recording date
# Confirm the chosen asset's passthrough is unset OR is the AOM episode _id

Expected outcome: One Mux asset matches: status: "ready", live_stream_id matches AOM, created date matches the recording date. passthrough either unset (first attempt) or already set to episode-another-orange-morning-{date}.

Status: UNTESTED at AOM-specific granularity — Splice's pattern is verified in TRTU launch (Step 6 of episode-publishing.md) but the AOM live_stream_id filter has not been exercised end-to-end through this procedure. Promote to VERIFIED after first Casey-driven daily run produces a confirmed Mux link.

Pitfall: Multiple VFT shows may record around the same time — distinguishing AOM's stream from concurrent recordings requires the live_stream_id filter, NOT a date-only or duration-only filter. Filtering by date alone returns recordings from any concurrent show and produces wrong-episode linkages downstream.


Step 11 — Patch episode muxVideo.asset._ref (Canon, spawned by Splice)

What happens: Splice spawns Canon to:

  1. Confirm the Sanity mux.videoAsset document for this asset doesn't already exist; create it if not (with assetId, playbackId, status: "ready" from Step 10).
  2. Patch the AOM episode (episode-another-orange-morning-{date}) with muxVideo.asset._ref pointing at the mux.videoAsset document.
  3. Pre-write guard: confirm episode-another-orange-morning-{date}.muxVideo.asset is currently null/missing — never overwrite an existing link without explicit human confirmation.

Who does it: Canon (Sanity write gateway — mandatory). Splice spawns Canon — Splice does NOT write to Sanity directly. System writes: Sanity (one new mux.videoAsset document + one patch on episode) OR (one patch on episode reusing existing mux.videoAsset doc) Trigger: Step 10 complete (asset identified, status ready, no existing link)

Verify with (REQUIRED) — pre-write guard + post-write verify:

# Pre-write: confirm episode is currently unlinked
node scripts/sanity/query.js custom "*[_id == 'episode-another-orange-morning-{date}'][0]{_id, 'currentMuxRef': muxVideo.asset._ref}"
# Expected: currentMuxRef is null OR exactly the new mux.videoAsset _id (idempotent re-run case)

# Post-write: confirm patch landed
node scripts/sanity/query.js custom "*[_id == 'episode-another-orange-morning-{date}'][0]{_id, 'muxVideoAsset': muxVideo.asset->{_id, assetId, playbackId, status}}"
# Expected: muxVideoAsset.playbackId matches the Mux dashboard playbackId for the asset

Expected outcome: Pre-write currentMuxRef is null on first run (or the matching ref on idempotent re-run). Post-write muxVideoAsset.playbackId is non-null and matches Mux. muxVideoAsset.status is "ready".

Status: UNTESTED at AOM-specific granularity — bridge pattern is verified for TRTU (commit nypLANVCk7FbrB0Tv3CXTc) but not yet confirmed on an AOM episode through this procedure. Promote to VERIFIED after first Casey-driven daily run.

Pitfall (Ep May 6) — Splice gateway-narrator anti-pattern: During the May 6 launch validation, Splice produced output reading "I'm delegating to Canon to patch the episode" without actually firing the Canon spawn. Splice may NOT claim Step 11 complete on the basis of "I will delegate" reasoning. The verification step is: confirm Canon was actually invoked (not just narrated), and the post-write GROQ above returned the patched ref. Splice owning the recording link does NOT mean Splice writes to Sanity; it means Splice owns the orchestration that ends in a Canon-executed Sanity write.


Step 12 — Latest selector verification (Mirror)

What happens: Mirror confirms the home-page Latest section on anotherorangemorning.com surfaces the new episode after Step 11.

Who does it: Mirror (visual QA, read-only) System writes: None Trigger: Step 11 complete (Mux ref patched)

Verify with:

curl -s https://anotherorangemorning.com/ | grep -o "episode-another-orange-morning-{date}" | head -1
# Then Mirror Playwright check:
# - Home page Latest section renders the new episode
# - Mux player on the episode page renders with controls
# - Recap pair cards link from the episode to the show-recap and updates-blog

Expected outcome: Episode _id appears in home page HTML. Latest section renders the new episode at the top. Mux player resolves with playable controls. Recap pair cross-links navigate to the Sub-flow A documents.

Status: VERIFIED 2026-05-05 home-page rendering after Latest section was added (commit d0b178e). Confirmed AOM home page surfaces the most recent episode pair.

Pitfall: If the home page Latest section renders an OLDER episode after Step 11 completes, the home query likely sorts on publishedAt not on recapPublishedAt. The episode's publishedAt may be set to the show's start time (set at episode-pre-creation by Slate), which is BEFORE the recap pair's recapPublishedAt. The sort field choice is a design decision (d0b178e chose publishedAt); confirm which field is canonical for "latest" before treating a stale-Latest result as a bug.


Sub-flow C — Paste-to-Publish (granular HubSpot Updates)

Step 13 — Paste-to-Publish admin auth (endpoint code)

What happens: Casey opens /admin/login, enters her email (must be on the ADMIN_EMAIL_ALLOWLIST env var), and clicks the magic link delivered to her inbox via the HubSpot workflow. The magic link has 15-minute validity; the resulting session cookie has 7-day duration.

Steps Casey performs:

  1. Open /admin/login on anotherorangemorning.com
  2. Enter email in the form (allowlist: Casey, Chris, Aaron — configurable via ADMIN_EMAIL_ALLOWLIST)
  3. Submit form (JS fetch — see Pitfall Ep May 7 #1)
  4. Wait for HubSpot-delivered email containing the magic link
  5. Click magic link
  6. Land authenticated on /admin/paste-to-publish

Who does it: Endpoint code owns auth. HubSpot workflow fires the email send (Marquee verifies workflow active). System writes: HubSpot Contact's magic_link_url property (triggers HubSpot workflow → email send) Trigger: Casey-initiated

Verify with:

# Server-side log check: confirm the auth endpoint logged a successful magic link write
# HubSpot Contact API: confirm magic_link_url property was patched on Casey's Contact record
node scripts/hubspot/api.js get contacts {casey_contact_id} --properties email,magic_link_url
# Then confirm the workflow fired (HubSpot workflow log)

Expected outcome: Casey's Contact has a recently-patched magic_link_url property. HubSpot workflow log shows an outbound email send. Casey receives the email within 60 seconds of submitting the form.

Status: VERIFIED 2026-05-07 — Auth flow tested end-to-end; Casey successfully authenticated through the magic-link path after fixes in commits 4d6139f (HubSpot delivery), 6f81141 (JS fetch), 53c6e54 (host detection).

Pitfall (Ep May 7) #1 — cross-site POST forbidden: Astro 5's security.checkOrigin blocks native form POSTs when browsers don't send the Origin header. Use JS fetch with Content-Type: application/json for the login form submission, NOT a native HTML form POST. Fix: commit 6f81141.

Pitfall (Ep May 7) #2 — magic link host = localhost: Vercel proxies serverless function requests through localhost; request.url returns the wrong host inside the function. The magic link generated from request.url points to localhost and is unusable. Use the x-forwarded-host header instead. Fix: commit 53c6e54.

Pitfall (Ep May 7) #3 — Resend was an over-spec: Initial design reached for an external email service (Resend) for the magic link delivery. HubSpot already has Casey's Contact + email infrastructure; the simpler pattern is to write the magic link URL to the Contact's magic_link_url property and let a HubSpot workflow fire the send. Workflow body wording must be generic ("secure login link"), NOT portal-specific. Fix: commit 4d6139f.


Step 14 — Dedupe check (endpoint code)

What happens: Before invoking the Emerjent chain, the endpoint runs a GROQ query against Sanity to check whether a hubspotUpdate document already exists for the pasted rollout URL (rolloutId is the unique dedupe key).

Who does it: Endpoint code (pre-chain guard) System writes: None at this step (read-only Sanity query) Trigger: Casey submits the paste form

Verify with:

node scripts/sanity/query.js custom "*[_type == 'hubspotUpdate' && rolloutId == '<pasted-url>'][0]{_id, slug}"

Expected outcome: Empty result (no duplicate) → proceed to Step 15. Non-empty result → endpoint returns "already published, redirecting" with the existing slug.

Status: VERIFIED 2026-05-07 — Dedupe check confirmed working on the four already-published articles (TikTok, File Manager, Customer Agent IVR, Breeze AI Agents GA). Re-pasting any of those URLs returns the existing slug rather than re-running the chain.

Pitfall: The rolloutId field stores the FULL URL including query parameters (https://app.hubspot.com/l/product-updates/?rollout=309895), not just the rollout numeric ID. Stripping the URL to just the numeric ID would break GROQ equality checks against the existing data. Always store and compare the full URL.


Step 15 — Emerjent chain invocation (Aegis-wired, Quinn-authored)

What happens: The endpoint invokes the Emerjent aom-paste-to-publish chain template. The chain runs Quinn (system prompt encodes ROOTED/READY/REAL editorial filter + six universal quality layers) followed by Pixel (hero image — see Step 16 for current fallback behavior).

Quinn produces a structured JSON output matching the hubspotUpdate Sanity schema:

Field group Fields
Identity title, socialTitle, slug, rolloutId, publishedAt, show ref
Classification hub (multi-select), tier (multi-select), status
Body tldr (PT), onTheShow (optional), body (PT with H2 sections), take (object with author + body + pullQuote), faq (array of Q&A items)
Links hubspotKnowledgeBaseUrl
SEO seoDescription, socialDescription
Provenance sourcePaste, generatedBy, editorialFilter, qualityLayers

Who does it: Aegis (chain wiring); Quinn (system prompt and structured output). The chain is invoked via mcp__emerjent__agent_invoke from the endpoint. System writes: None at this step (chain produces JSON; writes happen at Step 17-18) Trigger: Step 14 returns no duplicate

Verify with:

# Endpoint must inspect the chain return value before proceeding to Step 17:
# - All required fields present (per schema)
# - hub, tier, status all populated (auto-publish gate per PRD)
# - take.pullQuote signed (non-empty)
# - faq has minimum 3 items
# Endpoint returns the missing-fields list to Casey if any check fails.

Expected outcome: Chain returns valid structured JSON. All required fields populated. Pull quote signed (Casey, Chris, or both).

Status: VERIFIED 2026-05-06 — First chain-generated article (Breeze AI Agents GA, rollout=315000) successfully produced structured JSON matching the schema. Subsequent runs (TikTok, File Manager, Customer Agent IVR — manually authored before chain went live, included here as the published-corpus reference) confirm the schema-write pattern works.

Pitfall (Ep May 7) #4 — Emerjent chain agents don't execute writes: Canon and Ledger as Emerjent chain agents produce reasoning JSON describing what should be written, NOT actual API mutations. The endpoint must own the data layer — see Steps 17 and 18. Initial chain spec (docs/plans/aom-paste-to-publish-chain-spec.md) had the chain agents executing writes; the architectural correction (commit 3ef6f96) moved Sanity write + HubSpot Listing creation from the chain to the endpoint.

Pitfall — max_iterations per step: Quinn's reasoning needs more turns than Emerjent's chain default. Configure max_iterations override on the Quinn chain step (default is too low for 7-section article generation). Without the override, Quinn returns truncated output and the schema validation rejects the write at Step 17.


Step 16 — Hero image fallback resolution (endpoint code)

What happens: Endpoint reads the per-Hub fallback registry (apps/website/src/data/hub-illustration-assets.json, 8 hub-themed illustrations pre-uploaded to Sanity) and selects the illustration matching the article's primary hub. The fallback URL becomes the article's heroImage ref.

Who does it: Endpoint code System writes: None at this step (the heroImage value is bundled into the Step 17 Sanity write) Trigger: Step 15 complete

Verify with:

ls apps/website/src/data/hub-illustration-assets.json
node -e "console.log(Object.keys(require('/mnt/d/Projects/value-first-operations/apps/website/src/data/hub-illustration-assets.json')))"
# Expected: 8 hub keys (marketing, sales, service, content, operations, commerce, breeze, platform)

Expected outcome: Per-Hub registry exists with 8 entries. Article's primary hub maps to a registry entry. The Sanity heroImage ref is set from the registry, not from a live Pixel call.

Status: VERIFIED 2026-05-07 — All four published articles use per-Hub fallback illustrations. Registry committed in commit 194431a.

Pitfall (Ep May 7) #5 — hero image not auto-uploaded by chain: The original PRD speced Pixel as a per-article generator inside the Emerjent chain, BUT chain-step Pixel does not actually upload the resulting image asset to Sanity (same architectural finding as Pitfall #4: chain agents don't execute writes). The v0.2 implementation uses the per-Hub fallback registry exclusively. v1.0 swap planned: live per-article Pixel call + endpoint-side Sanity asset upload. Until v1.0 ships, every article uses its hub's pre-uploaded illustration — a quality acceptable trade for the launch.


Step 17 — hubspotUpdate Sanity write (endpoint code)

What happens: Endpoint executes the Sanity write for the hubspotUpdate document. This is NOT delegated to the Canon agent — the endpoint owns the API mutation directly. (See Pitfall Ep May 7 #4 for why.)

The write uses Sanity's published-not-draft mode (auto-publish per PRD). Schema validation enforces required fields; the write fails if hub/tier/status are unset, take is unsigned, or FAQ has fewer than 3 items.

Who does it: Endpoint code (using a write-capable Sanity client; the token is loaded from apps/website/.env's write-capable token, NOT the read-only root .env token) System writes: Sanity (one new hubspotUpdate document, published) Trigger: Steps 15 and 16 complete (validated JSON + heroImage ref)

Verify with (REQUIRED) — endpoint runs this before returning success:

node scripts/sanity/query.js custom "*[_type == 'hubspotUpdate' && rolloutId == '<rolloutUrl>'][0]{_id, _type, slug, 'hasHeroImage': defined(heroImage), 'bodyBlockCount': count(body), 'showRef': show._ref, 'showResolves': show->_id, 'tldrBlocks': count(tldr), 'faqCount': count(faq), 'pullQuote': take.pullQuote, 'takeAuthor': take.author}"

Expected outcome:

  • Document exists
  • hasHeroImage: true
  • bodyBlockCount > 0
  • showRef is "show-another-orange-morning" (NOT "drafts.show-another-orange-morning")
  • showResolves is "show-another-orange-morning" (resolves to published show document)
  • tldrBlocks >= 1
  • faqCount >= 3
  • pullQuote is non-empty
  • takeAuthor is one of casey, chris, or both

Status: VERIFIED 2026-05-07 — Four hubspotUpdate documents published via this path:

  • hubspotUpdate-tiktok-lead-syncing-2026-05-04 (rollout=309895)
  • hubspotUpdate-file-manager-folder-tree-2026-05-04 (rollout=303893)
  • hubspotUpdate-customer-agent-ivr-handoff-2026-05-04 (rollout=285015)
  • hubspotUpdate-breeze-ai-agents-general-availability-professional-enterprise-2026-05-06 (rollout=315000) — first chain-generated article

Pitfall (Ep May 7) #6 — endpoint executes write, NOT chain: This is the load-bearing architectural correction from May 7. Emerjent's Canon and Ledger chain agents are reasoning agents — they do NOT have API write capability. The endpoint must own the Sanity mutation directly using a write-capable Sanity client. Commit 3ef6f96 moved the write from the chain to the endpoint. If a future change attempts to "delegate this to the chain Canon" — that change is a regression to the May 7 root cause.

Pitfall — auto-publish gate on schema validation: If the schema rejects the write (missing hub, unsigned take, FAQ < 3, etc.), the endpoint must return the missing-fields list to Casey. Do NOT default-fill missing fields. The auto-publish promise relies on Quinn producing complete JSON; falling back to default-fill silently corrupts the editorial filter.


Step 18 — HubSpot Listing creation (endpoint code)

What happens: Endpoint creates the paired hubspot_update Listing for routing/discovery. Like Step 17, the endpoint executes this directly — NOT delegated to the chain Ledger agent.

Required Listing properties:

Property Value
name "AOM HubSpot Update — {article title}"
listing_content_type "hubspot_update"
hubspot_update_sanity_id The Sanity document _id from Step 17
hubspot_update_rollout_id The pasted HubSpot release-notes URL (rolloutId from Step 14)
hubspot_update_show "another-orange-morning"
hubspot_update_hub Primary hub from Quinn's classification (single-select; first entry of the hub array)
document_authors "marquee"

Reference: skills/hubspot/property-index/listing.json for hubspot_update content-type-specific properties.

Who does it: Endpoint code (using HubSpot API directly; token loaded from env) System writes: HubSpot (one new Listing record) Trigger: Step 17 complete (Sanity document _id known)

Verify with:

node scripts/hubspot/api.js search listings --query "<article title>" --properties hs_object_id,name,listing_content_type,hubspot_update_sanity_id,hubspot_update_rollout_id,hubspot_update_show,hubspot_update_hub --limit 5

Expected outcome: Listing exists with listing_content_type: "hubspot_update", hubspot_update_sanity_id matches the Sanity _id, hubspot_update_rollout_id matches the pasted URL.

Status: VERIFIED 2026-05-07 — Paired Listings exist for all four published hubspotUpdate documents.

Pitfall: Same architectural finding as Step 17 — endpoint owns this write, NOT the chain Ledger agent. If Step 18 fails after Step 17 succeeds, the article is published on Sanity but the Listing-driven discovery surface is broken. The endpoint should return "Article published, Listing creation failed — retry?" with a button that re-runs ONLY Step 18 (per PRD failure semantics).


Step 19 — Pre-return verification GROQ (endpoint code)

What happens: Before redirecting Casey to the published article, the endpoint runs the Step 17 verify GROQ one final time. This guards against the case where Step 17 reported success but the document later failed validation server-side (rare, but possible with eventual consistency on Sanity's published-document index).

Who does it: Endpoint code (pre-redirect guard) System writes: None Trigger: Step 18 complete

Verify with: Same query as Step 17. All checks must still pass.

Status: VERIFIED 2026-05-07 — Pre-return verification confirmed all four published articles met the gate before Casey saw the redirect.

Pitfall: A successful Step 17 write does NOT guarantee the document is queryable in the published-document index immediately. Sanity's CDN endpoint can lag the API endpoint by a few seconds. If the Step 19 verify fails right after Step 17 succeeded, retry the verify with a 2-second delay before treating it as a failure. If the verify still fails after a 5-second wait, the document is corrupted — fail the request and surface the diagnostic to Casey.


Step 20 — Cross-link card insertion in Sub-flow A's updates-blog (Casey, optional)

What happens: After a granular hubspotUpdate article is published (Sub-flow C), Casey may optionally edit the matching recap-another-orange-morning-{date}-updates-blog document in Sanity Studio to replace the inline summary of that update with a hubspotUpdateLink Portable Text block. The block renders as a card linking to /hubspot-updates/{slug}.

This is the bridge between Sub-flow A and Sub-flow C — the cross-link mechanism that Casey explicitly designed to differentiate AOM from generic daily-recap formats.

Who does it: Casey, manually via Sanity Studio (no agent automation; this is editorial) System writes: Sanity (one patch on the updates-blog document) Trigger: Step 17 complete for an update Casey wants to cross-link from a same-day or recent updates-blog

Verify with:

# Confirm the hubspotUpdateLink block resolves with slug projected (May 6 fix):
node scripts/sanity/query.js custom "*[_id == 'recap-another-orange-morning-{date}-updates-blog'][0]{body[]{_type, _type == 'hubspotUpdateLink' => {hubspotUpdate->{_id, 'slug': slug.current, title}}}}"

Expected outcome: Any hubspotUpdateLink block in the body returns its referenced hubspotUpdate's _id, projected slug, and title — confirms the card has the data it needs to render.

Status: UNTESTED for the AOM daily flow — the cross-link block schema exists, the GROQ projection bug was fixed May 6 (commit 5ca6e07), and the render is confirmed working in test. Casey has not yet exercised the editorial pattern of replacing an inline summary with a cross-link card during a daily run; promote to VERIFIED after the first such patch lands and Mirror confirms the card renders correctly.

Pitfall (Ep May 6) #7 — GROQ projection bug: The original card render query did not project slug onto each hubspotUpdateLink block. The result: cards rendered with empty href targets (the slug was unresolved at render time). Fix: commit 5ca6e07 projects slug via GROQ on each cross-link block. If a future schema change adds new cross-link variants, the same projection pattern must apply — never assume the card render component will resolve refs automatically without GROQ projection.


Verification Score Summary

Step Status Phase
1 — Confirm Casey's drafts (Marquee + Curator) VERIFIED A: Source ingestion
2 — HTML → Portable Text conversion (Canon) VERIFIED A: Conversion
3 — Recap pair Sanity write (Canon) VERIFIED A: Sanity
4 — relatedRecap cross-references (Canon) VERIFIED A: Sanity
5 — show._ref + episode._ref (Canon) VERIFIED A: Sanity
6 — recapPublishedAt (Canon) VERIFIED A: Sanity
7 — URL placeholder replacement (Canon) VERIFIED A: Sanity
8 — Paired HubSpot Listings (Ledger) VERIFIED A: HubSpot
9 — Recap pair render verification (Mirror) VERIFIED A: Web
10 — Mux asset identification (Splice) UNTESTED B: Mux
11 — Episode muxVideo.asset._ref (Canon, spawned by Splice) UNTESTED B: Sanity
12 — Latest selector verification (Mirror) VERIFIED B: Web
13 — Paste-to-Publish admin auth (endpoint) VERIFIED C: Auth
14 — Dedupe check (endpoint) VERIFIED C: Sanity (read)
15 — Emerjent chain invocation (Aegis + Quinn) VERIFIED C: Generation
16 — Hero image fallback (endpoint) VERIFIED C: Asset
17 — hubspotUpdate Sanity write (endpoint) VERIFIED C: Sanity
18 — HubSpot Listing creation (endpoint) VERIFIED C: HubSpot
19 — Pre-return verification GROQ (endpoint) VERIFIED C: Verification
20 — Cross-link card insertion (Casey, manual) UNTESTED A↔C: Bridge

Score: 17 VERIFIED / 20 applicable steps = 85% Operational (above the 80% threshold required to describe a process as operational, per docs/quality/verification-protocol.md).

Decay window: Tier 1 (Critical) = 90 days. Re-verify by 2026-08-05 OR after first Casey-driven daily end-to-end run (Steps 1-20 in sequence on a single day) — whichever is sooner. Code changes to Canon, Ledger, the AOM endpoint code, the Emerjent chain template, or scripts/sanity/query.js decay all VERIFIED markers immediately.

The three UNTESTED steps:

  • Step 10 + Step 11 (Sub-flow B): Mux asset link has not been exercised end-to-end through this procedure on an AOM-specific live_stream_id; promote after first Casey-driven daily run produces a confirmed Mux link.
  • Step 20: Cross-link card insertion is the editorial bridge between Sub-flows A and C; promote after Casey first patches an updates-blog to replace an inline summary with a cross-link card, and Mirror confirms render.

Critical Pitfalls — Quick Reference

For a cold-start session, these are the lessons from the May 5–7 launch window to internalize before executing:

  1. Pitfall (Ep May 7) #1 — cross-site POST forbidden. Astro 5's security.checkOrigin blocks native form POSTs without an Origin header. Use JS fetch with Content-Type: application/json. Fix: commit 6f81141.
  2. Pitfall (Ep May 7) #2 — magic link host = localhost. Vercel proxies through localhost; request.url returns the wrong host. Use x-forwarded-host header. Fix: commit 53c6e54.
  3. Pitfall (Ep May 7) #3 — Resend was an over-spec. HubSpot already has Casey's contact + email infra. Pattern: write to Contact's magic_link_url property; HubSpot workflow fires the send. Fix: commit 4d6139f. Workflow body must be generic ("secure login link"), not portal-specific.
  4. Pitfall (Ep May 7) #4 — Emerjent chain agents don't execute writes. Canon and Ledger as chain agents produce reasoning JSON, not API mutations. Endpoint must own the data layer (Step 17 Sanity write, Step 18 HubSpot Listing creation). Fix: commit 3ef6f96.
  5. Pitfall (Ep May 5) — Canon ref-correctness deviations. On Canon write delegations, always run the post-write GROQ that resolves references: showRef matches "show-another-orange-morning" not "drafts.show-another-orange-morning"; episodeRef matches the canonical AOM deterministic ID, not another show's episode of the same date; relatedRecapRef is non-null. Per feedback_canon_verify_after_write.md. Fix: 4-defect patch round on May 5.
  6. Pitfall (Ep May 5) — Canon HTML→PT link/strong stripping. Conversion from Casey's source HTML to Sanity Portable Text dropped inline product-update anchors AND <strong> marks on first attempt. Verification protocol now mandates four counts (H3, H4, strong-mark, markDef link) match between source HTML and prepared PT array BEFORE the Sanity write. Fix: re-parsed both May 5 articles after Mirror+Baldwin caught the gap.
  7. Pitfall (Ep May 6) #7 — hubspotUpdateLink GROQ projection bug. The cross-link card render query did not project slug onto each block; cards rendered with empty href targets. Fix: commit 5ca6e07 projects slug via GROQ on each cross-link block. Verification: confirm the Step 20 GROQ returns non-null slug for every hubspotUpdateLink block.
  8. Pitfall (Ep May 6) #8 — Splice gateway-narrator anti-pattern. Splice presented "I'm delegating to Canon" as completion without actually firing the Canon spawn. Verification step: confirm Canon was actually invoked (not just narrated), and the post-write GROQ returned the patched ref.
  9. Pitfall (Ep May 7) #9 — hero image not auto-uploaded by chain. Pixel as a chain agent doesn't upload assets to Sanity. Endpoint pulls from per-Hub fallback registry (apps/website/src/data/hub-illustration-assets.json, 8 hubs pre-uploaded). v1.0 swap planned: live per-article Pixel call + endpoint-side asset upload. Until then, every article uses its hub's pre-uploaded illustration.
  10. Pitfall (Ep May 6) #10 — Sanity Studio deploy vs runtime writes. Schema files in code do NOT auto-deploy to the hosted Studio. Articles can be created via API and rendered on the public site without a Studio deploy. Only Studio editing UI requires pnpm sanity deploy from apps/website/studio/. This is an OPTIONAL Studio deploy, not a publishing blocker.
  11. Pitfall (Ep May 6) #11 — Vercel preview env adds get blocked by interactive prompt. vercel env add NAME preview keeps prompting for confirmation. Use --force or pipe the value through stdin from a tempfile. Production env vars work cleanly; preview is the friction point.
  12. Central Time is canonicalrecapPublishedAt and any time-of-day reference use America/Chicago. Per feedback_central_time_canonical.md. Never compute timestamps in agent reasoning; use date -d or ./scripts/datetime.sh.
  13. Fallback rendering is a failure, not a feature (per wiki/conventions.md § Engineering standards). If the recap pair render shows missing structure or a hubspotUpdate card renders without its slug, do NOT celebrate — investigate the primary path failure.
  14. Synthesis files are NOT evidence (per wiki/conventions.md § Data tier hierarchy). Casey's README.md + source HTML + the live HubSpot release-note URL are Tier 1/Tier 3 inputs; a session synthesis is not a substitute for reading the staged drafts directly.

Out of Scope (separate procedures required)

This procedure intentionally does NOT cover:

  • Show production itself (live recording mechanics, OBS/StreamYard configuration, host scheduling, run-of-show) — see AOM production guide in the AOM standalone repo (/mnt/d/Projects/another-orange-morning/).
  • Show launch (the one-time activation work that brought AOM online: domain provisioning, Vercel project setup, Sanity show document creation, brand customization, monorepo wiring) — see docs/operating-procedures/show-launch.md. AOM launched 2026-04-30.
  • Long-form recap article (Baldwin → Canon) — AOM's editorial layer is the recap pair (Sub-flow A) plus granular updates (Sub-flow C). Long-form weekly retrospectives are deferred until cadence is established; route through episode-publishing.md's Step 13 (recap article) when activated.
  • YouTube + LinkedIn distribution — AOM's distribution surfaces are the standalone site + cross-show resonance lines on related update pages. Direct YouTube/LinkedIn publishing for AOM is deferred; when activated, route through Broadcast per episode-publishing.md's Steps 11-12.
  • Per-article hero image generation (Pixel-live) — currently Step 16 uses the per-Hub fallback registry. The v1.0 swap to live per-article Pixel calls is documented in PRD 2026-05-06-aom-paste-to-publish-prd.md but not yet built; revise this procedure when v1.0 ships.
  • TTS audio generation — the audioFile field on hubspotUpdate is deferred to v1.0 per the PRD.
  • TRTU per-episode publishing — different show, different surfaces. See docs/operating-procedures/episode-publishing.md.

Related References

Reference Path
AOM Paste-to-Publish PRD docs/plans/2026-05-06-aom-paste-to-publish-prd.md
AOM Paste-to-Publish chain spec docs/plans/aom-paste-to-publish-chain-spec.md
AOM inspiration source (George Thomas) AI-WORKSPACE/inspiration/2026-05-06-thomas-paste-to-publish.md
TRTU episode publishing (sibling) docs/operating-procedures/episode-publishing.md
Show launch (one-time activation) docs/operating-procedures/show-launch.md
Listing property index skills/hubspot/property-index/listing.json (recap_article + hubspot_update content types)
Casey's recent AOM episode work clients/vf-team/content/media_network/Another_Morning_Show/episodes/2026-05-06/ (commit b66df9f5f)
Casey's recent AOM episode work clients/vf-team/content/media_network/Another_Morning_Show/episodes/2026-05-07/ (commit 60a1f8c1d)
Canon verify-after-write rule feedback_canon_verify_after_write.md
Central Time canonical feedback_central_time_canonical.md
Ground deliverables in transcripts feedback_ground_deliverables_in_transcripts.md
Casey Hawkins pronouns + role feedback_casey_hawkins_pronouns.md
Squire owns repo + Vercel setup feedback_squire_owns_repo_vercel_setup.md
Verification protocol docs/quality/verification-protocol.md
Process register docs/quality/process-register.md
QMS framework docs/quality/qms-framework.md
AOM cross-link GROQ fix commit 5ca6e07 (project slug onto hubspotUpdateLink blocks)
Cross-site POST fix commit 6f81141 (JS fetch instead of native form)
Magic link host fix commit 53c6e54 (x-forwarded-host header)
Resend → HubSpot magic link commit 4d6139f (route through HubSpot Contact property)
Endpoint owns Sanity + HubSpot writes commit 3ef6f96 (move from Emerjent chain to endpoint)

Revision History

Date Change Author
2026-05-07 Initial document. Authored from the AOM Paste-to-Publish launch (2026-05-06) + recap pair publish (2026-05-05) + first chain-generated article (Breeze AI Agents GA, 2026-05-06) + bug-fix sweep (2026-05-07). 17 of 20 steps VERIFIED with evidence; Steps 10, 11 (Sub-flow B Mux link) and Step 20 (cross-link card) marked UNTESTED pending first Casey-driven daily run. Captures all 14 known pitfalls from the May 5–7 window: cross-site POST, magic link host, Resend over-spec, chain agents don't write, Canon ref correctness, HTML→PT stripping, hubspotUpdateLink GROQ projection, Splice gateway-narrator, hero image not auto-uploaded, Sanity Studio deploy, Vercel preview env adds, Central Time canonical, fallback-as-failure, synthesis-not-evidence. Q (Quality System Manager)