Skip to content

feat(skills): move Resend sends to posthog-node in migrate-email - #451

Open
Silthus wants to merge 37 commits into
feat/migrate-email-resendfrom
feat/migrate-email-resend-cutover
Open

Silthus wants to merge 37 commits into
feat/migrate-email-resendfrom
feat/migrate-email-resend-cutover

Conversation

@Silthus

@Silthus Silthus commented Nov 2, 2026 •

Copy link
Copy Markdown

Problem

migrate-email-resend (#450) finds every Resend send and gets the account ready, but it leaves every send on Resend. The person then rewrites each send by hand.

With this PR the skill moves the sends in code. Each send PostHog can take moves to posthog.emails.send in posthog-node. Every other send stays on Resend with a reason. The skill then checks the build and one email on one person, and writes the report with the real sites_rewritten.

Important

Stacked on #450, which is based on main. Merge that first. This PR's own commits: 07dbf3a...5dcdd21.
The skill sends content from code with templateKey, and sets migratedFrom on the client. Both ship in the next posthog-node minor release, from PostHog/posthog-js#5509. Hold the merge until that release is out.

Merge risk

Warning

🟡 Merge risk: medium · two-way door

  • The skill now edits the person's send code. A wrong rewrite changes how mail goes out once they deploy. The report's "Before cutover" list names what the person checks before that deploy, including each PostHog template.
  • A revert brings back the version from #450, which changes no send.

Changes

Three steps take the place of the old report step:

  1. 7, rewrite. It installs or upgrades posthog-node and checks the installed types for what the moving sites need. It then sets up one server-side client with secretKey and migratedFrom: 'resend'. Each site whose plan row says it moves is rewritten in one of two forms:
    • Content from code. A React site renders in the app with @react-email/render and sends the result as html. An html or text site sends its content as it is. Each send gets a templateKey named for the kind of email.
    • A template made in PostHog. A Resend template moves only when the person makes it in PostHog, makes it transactional and gives its template ID in Step 3. The site sends templateId with templateData, which takes the place of Resend's variables.
    • from, replyTo, the idempotency key and error.name branches keep working. distinctId is the ID the app already identifies people with. A send to a suppressed address gets a branch that treats recipient_suppressed like a sent email.
    • cc, bcc, headers, tags, attachments, scheduled sends and batch sends stay on Resend.
  2. 8, verify. A typecheck against step 7's baseline, and the tests of the changed files. Then, with the person's consent, one real email: $workflows_email_sent, an engagement event of the same workflow, and a product event on the same distinct ID, read through the MCP's execute-sql.
  3. 9, report. The moved sites, the sites that stay with what would let them move, and the checks. A "Before cutover" list holds what must be done before the deploy, including a test of each PostHog template. "After cutover" says when to remove Resend.

Step 3 asks the person once per Resend template whether to move it or keep it on Resend, reads its content to classify it, and asks for the PostHog template ID and the address it sends from. Step 4 then sets up a sender for that address. mapping.md gains the rewrite rules, and account.md covers reading a template's content.

This revision (2026-11-03) adds "Link emails to your users" to Step 7: pass the distinctId the app already passes to identify(), so each send and its sent, opened and clicked events land on the user's person. Without it, PostHog records the email against a person whose distinct ID is the recipient's address, as master does today (distinct_id or to). An invite or an address check before the account exists leaves it out. Step 8's test email now comes from a site whose plan row passes distinctId.

The revision before (2026-11-02) matches the skill to the first release of the code send:

  • No template module, upload or template key in code. A Resend template moves only to a template the person makes in PostHog.
  • cc, bcc, headers, tags and attachments stay on Resend.
  • migratedFrom is back on the client, now that posthog-node has the option.
  • Three review rounds: a PostHog template's sender gets set up in Step 4, the report asks for a test of each PostHog template, the full-access Resend key is deleted on every way out of Step 5, and Step 8's test email comes from a site that names a person.

Evidence

  • Bundle test (vitest): runs the rewrite and verify steps between the key and the report expects the steps 1-detect to 6-key, then 7-rewrite, 8-verify, 9-report. On d157859, pnpm test passes 225 tests in 29 files, and pnpm build builds migrate-email-resend.zip.

  • Agent runs on 85f6096. Claude Code on Sonnet 5.5 ran the skill as a dry run, headless, with only read tools, on a fixture app with four Resend sends: a React email, html and text with replyTo, the Resend template invoice, and one with an attachment. The person's answers were in the prompt. Neither run changed a file in the fixture.

    Run What the person answered Result
    keep Keep invoice on Resend 2 of 4 sites planned to move: the React email renders with render(<WelcomeEmail … />) and sends html with templateKey: 'welcome'; the html and text email keeps replyTo. invoice stays (kept on Resend for now), the attachment email stays (attachments). The client sets migratedFrom: 'resend'
    move Move invoice; its PostHog template ID and sender invoice moves with templateId and templateData: { INVOICE_URL: invoiceUrl }, and its from column holds the template's sender, so Step 4 covers that domain. "Before cutover" lists a test of that template: transactional, verified sender, INVOICE_URL filled

    The runs are on 85f6096. Since then, a759ff4 changes only Steps 5 and 8 and a comment in Step 7's example, the merges of feat(skills): add the migrate-email skill with a Resend variant #450 add one sentence to Step 3 for a template site that another field keeps on Resend, 2c36853 rewords account.md to match Step 3, 21f683e ends Step 8's email check when the person declines the test email, and d157859 and 5dcdd21 rewrite Step 7's distinctId section and change Step 8's example site. The distinctId section changes which ID a moved site passes, not which sites move. The fixture's template site sets no other field, so the runs take the same path. Raw sessions and prompts are in proof storage.

  • SDK facts come from posthog-node at #5509: migratedFrom?: string on the client options, sent as the X-PostHog-Migrated-From header, and the error.name values the rewrite keeps, such as validation_error for recipient_suppressed.

  • Not checked:

    • A run that writes the code, typechecks and sends: the posthog-node release isn't out.
    • A run with the PostHog MCP connected, so Step 8's event check has not run.
    • A run inside the Wizard.
    • Prod shape: the rewrite reads the person's code and Resend templates, which PostHog doesn't store.

Decisions

Question Answer Who Where
How a send moves Two forms only: a PostHog template by templateId, or templateKey with rendered html or text. No templates in code, no upload 👤 Human Silthus/posthog#461
React The app renders with @react-email/render and sends html 👤 Human Silthus/posthog#461
cc, bcc, headers, tags, attachments Stay on Resend: the first release doesn't send them 👤 Human Silthus/posthog#461
migratedFrom on the client Set it, when the installed types take it. It was removed in an earlier revision because posthog-node lacked the option; #5509 adds it 🤖 Agent this PR
A template made in PostHog The person makes it, makes it transactional, and gives its ID and sender in Step 3; a template without an ID stays on Resend 🤖 Agent review round 1
Checking that template The person sends it a test from PostHog before the deploy, from the report's "Before cutover" list. The skill names no MCP tool that reads a template 🤖 Agent review round 2
Template content From a dashboard copy the person saves outside the repo, or Resend's API with a full-access key that the person deletes at the end of Step 5 🤖 Agent this PR
Suppressed recipient Treat recipient_suppressed like a sent email at a site that throws or retries on errors 🤖 Agent this PR
Resend package and key Kept; "After cutover" says when to remove them 🤖 Agent this PR
Test email match The send, an engagement event of the same workflow and a product event on the same distinct ID, all after the start of the test. The events carry no per-send ID, so a late event of an earlier send of the same email to the same person can count 🤖 Agent this PR

🤖 Agent context

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5), as the docs, skills and wizard slice of Silthus/posthog#461. Reviews in fleet proof, by Claude Opus 5.5 (high) on fresh eyes, correctness and risk: round 1 on 5edeb92 (findings fixed in 64cf8ef) and round 2 (findings fixed in 85f6096), and round 3 (findings fixed in a759ff4). GPT-6.1 Sol (high) final reads on a759ff4, e053dab, 2c36853 and 21f683e: no findings. Opus 5.5 (high) final reads on the distinctId section: d157859 (one minor finding, fixed in 5dcdd21) and 5dcdd21 (no findings).

Earlier revisions of this description

The earlier text described templates converted to modules in the app's code, an upload on the wire, and cc, bcc, headers, tags and attachments moving. The first release has none of these. Its "Impact" paragraph is removed. The rest is kept as it was.

Moved from Silthus#2.

Problem

migrate-email-resend (PostHog/context-mill#450) finds every Resend send and prepares the account, but it leaves every call site on Resend. The person still rewrites each send by hand.

With this PR the skill does the cutover in code. It moves each send PostHog can take to posthog.emails.send from posthog-node, leaves the rest on Resend with a reason, checks the build and one email on one person, and writes the report with the real sites_rewritten.

[!IMPORTANT]
Stacked on PostHog/context-mill#450, which is based on main. Merge it first. This PR's own commits: 4ad8c6e...50b5f27.
posthog.emails.send with a template ID is in posthog-node (PostHog/posthog-js#5418). This skill sends with templateKey, the code form, which posthog-node does not have yet. Hold the merge until a posthog-node release has the code form.

Tracking: Silthus/posthog#539, slice 15 of the Resend parity spec. Contract: PostHog/posthog#120921.

Merge Danger

[!WARNING]
🟡 Merge risk: medium · two-way door (merge-risk rubric)

  • The skill now edits the person's send code. A wrong rewrite would send mail differently once they deploy. Fixture runs on five apps found two such patterns (a recipient list looped or truncated, a test faking Resend), and the review found four more (suppressed recipients, per-email subjects, fallback quoting, idempotency across providers). All are fixed, and the fixtures were rerun.
  • A revert removes the cutover steps and leaves v0.

Summary

What this adds beyond moving-from-other-providers.md (PostHog/posthog#125415): that reference maps any provider's send to a PostHog template. This PR moves Resend sends in code to the code form of posthog-node: the field mapping, the Resend template conversion to Liquid, the suppressed-recipient and idempotency rules, and the verification chain from the send to the engagement and product events.

Three steps replace the old step 7:

  1. 7, rewrite. A typecheck baseline, then one posthog-node client with secretKey. Then each site whose plan row says it moves:
    • A Resend template becomes a module in the app's code: {{{NAME}}} turns into {{ variables.NAME }}, and a fallback turns into Liquid's default. The call site sends templateKey, subject, html and templateData with the old variables. A template with an HTML-valued variable or an HTML fallback, or anything beyond plain variables and fallbacks, stays on Resend. Step 3 saves the content in a temporary file, and step 7 writes the module from it. The templateKey is the alias or name, lowercased, with other characters turned into -, and unique.
    • React sites render in the app with @react-email/render, and html/text sites send their content as is. Each gets a templateKey named for the kind of email.
    • from, replyTo, cc, bcc, headers, tags, attachments and the idempotency key move unchanged. error.name branches keep working. distinctId is the ID the app already identifies people with.
    • A subject the site builds per email becomes {{ variables.* }} placeholders in the module, so the template hash stays stable and doesn't upload a new version on every send. Rendered html and react content sends without templateData and uploads nothing.
    • PostHog refuses a send to a suppressed address (recipient_suppressed), where Resend dropped it later. A site that throws or retries on errors gets a branch that treats it like a sent email, unless it sets cc or bcc, where the report explains that a suppressed copy address now stops the email.
    • The report warns that Resend's and PostHog's idempotency keys don't cross: a retry that crosses the deploy can send twice.
    • A helper that takes a list of recipients but always gets one is narrowed to one address. The skill never loops or drops addresses.
  2. 8, verify. Typecheck against step 7's baseline, the tests that cover the changed files, and, with the person's consent and when the app's project token matches the MCP's project, one real email: $workflows_email_sent, an engagement event with the same $workflow_id, and a product event on the same distinct ID, all after the start of the test, read through the MCP's execute-sql.
  3. 9, report. Moved sites with their keys, the sites that stay with what would let them move, the checks, each behaviour note from the rewrite, a "Before cutover" list that has to be done before the deploy, and "After cutover" for removing Resend and the full-access key. The summary block keeps its keys, and sites_rewritten is now real.

mapping.md gains a Rewrite section, account.md covers getting a template's content (dashboard copy or GET /templates/{id} with a full-access key), and sdk-reference.md adds template fallbacks and defaults.

Evidence

Bundle test (vitest, red then green): runs the rewrite and verify steps between the key and the report expects the workflow 1-detect … 6-key, 7-rewrite, 8-verify, 9-report. Red before: 7-report.md where the three new steps belong. Lane gate on 50b5f27: vitest run scripts/plugins/tests scripts/lib/tests, 29 files, 225 tests pass. CI runs on this PR. Up to date on 2026-11-02: 50b5f27 already holds its base, #450 at 4ad8c6e, and main at b699b20.

Agent runs on fixture apps. Claude Code on Sonnet 5.5 with only this skill installed, headless, with no PostHog MCP and the person's answers in the prompt. posthog-node came from a local build of the release branch, because the release isn't out yet.

Fixture Runs Result
SvelteKit app on the resend SDK (PostHog/wizard-workbench#2221) 2 + 2 after the fix 2 of 2 sites moved, no from 'resend' left, 2 posthog.emails.send for 2 old sends. svelte-check 0 errors, mailer tests 3 passed. The first two runs looped or truncated a one-recipient list; after the fix both narrow it to one address
Node app with two Resend templates (written for this PR) 3 welcome (a plain variable and a fallback) moved, order-receipt (HTML rows in a variable) stayed with that reason. tsc passes. Every report notes that an empty name now shows the fallback
Node app on Resend REST 2 The order confirmation moved with its tag and idempotency key; the scheduled digest and the invoice with a path attachment stayed
Convex app on Resend REST with an .ics attachment (Silthus/next-session) 2 + 1 after the fix + 1 on 4499957 The calendar send moved with its attachment and its derived idempotency key, with maxRetries: 0 because the app's retrier retries. Convex tsc passes, 665 tests pass. Before the fix, one run kept the tests green by mocking posthog-node to rebuild a Resend request; the rerun fakes posthog.emails.send and asserts on its options. The run on the reviewed head also returns early on recipient_suppressed (the site sets no cc) and lists the cross-provider duplicate in "Before cutover"
Node app with React Email and Nodemailer, no Resend 1 Stops at step 1, no diff

Templates render as before. Each moved welcome module, rendered with liquidjs (HTML escaping on) and compared with Resend's triple-brace output:

"Jane" subject: same              "Jane" html: same after entity decode
(missing) subject: same           (missing) html: same after entity decode
"" subject: DIFFERENT             "" html: DIFFERENT   ← the fallback now shows, noted in every report

"After entity decode" is & in the link's href written as &amp;, which browsers read back as &.

On the wire (earlier revision, with the migratedFrom client option that 03011c3 removes). The migrated template app's sendWelcome against a local stub of the PostHog API:

POST /api/projects/@current/messaging/send/email/    X-PostHog-Migrated-From: resend  Idempotency-Key: welcome:user_42  distinct_id: user_42  template: {key: welcome, hash: v1:11d8…}
PUT  /api/projects/@current/messaging_templates/by_key/welcome/   (after the stub's 409 template_outdated)
POST /api/projects/@current/messaging/send/email/    same Idempotency-Key, same body
  • Test seams: the bundle's workflow order, the files an agent run leaves in a fixture (diff, typecheck, tests, report), the rendered output of a converted template, and the request a migrated send makes.
  • Verify: pnpm exec vitest run scripts/lib/tests/migrate-email-skill.test.js passes.
  • Gaps on 50b5f27:
    • No run against a local PostHog stack, so the dev mail catcher is unproven. The code form of the send route and of posthog-node is still open.
    • No agent rerun after 03011c3. It drops one client option and renames one docs file in step 8, so the rewrite steps the runs above exercised are unchanged.
    • No run with the PostHog MCP connected, so the chain check in step 8 has not run.
    • No run inside the Wizard.
    • Real delivery and DNS publish.
    • Resend's handling of an empty string with a fallback is assumed to insert the empty string. The skill tells the person either way.
    • No agent rerun on a fixture app after 8c841cd, which changes step 8's query and adds the typecheck baseline and the temporary template file.
    • Prod shape: the rewrite reads the person's own code and Resend templates, which PostHog does not store, so there is nothing to measure. No fixture has run the template-name key normalization or an HTML fallback.

Decisions

Question Answer Who Where
Template content From the dashboard copy, or GET /templates/{id} with a full-access key the person creates and deletes afterwards; the same key now also reads suppressions (RESEND_FULL_ACCESS_API_KEY) 🤖 Agent this PR
One-recipient list Narrow the helper to one address and update its callers; never loop or drop 🤖 Agent this PR
Resend package and key Keep them; the report's "After cutover" tells the person when to remove them 🤖 Agent this PR
Chain proof The person triggers one moved email as themselves; the skill reads the three events through execute-sql 🤖 Agent this PR
Suppressed recipient Treat recipient_suppressed like a sent email, except on sites with cc/bcc, which keep failing with a report note 🤖 Agent review round 1
Escaping in subjects No rule: PostHog renders subject and text without HTML escaping (EMAIL_PLAIN_TEXT_FIELDS in nodejs/src/cdp/services/hog-inputs.service.ts) 🤖 Agent final read 1
migratedFrom client option Removed: posthog-node has no such option. The Wizard sets migrated_from on setup wizard finished instead 🤖 Agent Silthus/posthog#461
Summary block Same keys as PostHog/context-mill#450; sites_rewritten counts moved sites 🤖 Agent this PR
posthog-node without the code form Check the installed types per site; a site that needs a missing capability stays on Resend 🤖 Agent review round 1
Template content between steps 3 and 7 A file in the system's temporary folder, deleted once the module is written; not a second fetch or a second ask in step 7 🤖 Agent pending approval
Test email correlation Events since the start of the test, with the engagement event's $workflow_id matching the send's; no message id is on the events 🤖 Agent pending approval
Typecheck errors Step 7 keeps a baseline; step 8 fixes every new error in any file, including other posthog-node code the upgrade breaks, and the report lists those files; not a pinned older release 🤖 Agent pending approval
Template key already in the project Not checked: the upload carries if_code_managed and if_not_superseded, so it never replaces a template edited in PostHog; another app's code-managed key is a follow-up 🤖 Agent pending approval
Template churn on rendered sites No rule: posthog-node sends rendered content without templateData and uploads nothing 🤖 Agent pending approval

Open: accept the live-run and prod-shape gaps. Recommended: accept; the fixture-app rerun of the skill on this head is owed.

Follow-up: a templateKey that another app's code-managed template already uses in the same project. It needs a tool that lists the project's template keys, which the skill's MCP tools don't name today.

Involvement: no grilling rounds, no prototypes, one Opus 5.5 review of the strand (#435 and #436) with three final reads, ending clean, and one GPT-6.1 Sol (high) round on this revision (one finding, fixed) with a clean read of the fix. QA swarm 2026-11-01: round 3, the first two-family panel (Opus 5.5 fresh eyes, GPT-6.1 Sol on nine lenses), round 4 on the fixes (seven lenses), and three final reads ending clean: 23 findings, 21 fixed, 1 rejected (template churn on rendered sites), 1 follow-up. Proof captured.

Agent context

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5). Reviews: GPT-6.1 Sol (high) and Claude Opus 5.5 (high) in fleet proof panels on the fork. The reviewed head 50b5f27 is pushed here unchanged; its commits are signed.

🤖 Generated with Claude Code

Silthus and others added 14 commits November 1, 2026 13:02
The migrate-email skill now rewrites every call site PostHog can send:
hosted Resend templates become local templates, React and html/text sites
send rendered content, and the client carries migratedFrom. It checks the
build and one email on one person before it writes the report.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…d provider in tests

Fixture runs looped over or truncated a one-recipient list, and one run
mocked posthog-node to rebuild a Resend request. The rewrite now narrows
the helper's parameter to one address, and the verify step asserts on the
fields posthog.emails.send gets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…rate-email

A moved site now treats recipient_suppressed like a send that went out,
a site-built subject becomes a module placeholder so the template hash
stays stable, fallbacks use single quotes, and the report warns that
idempotency keys don't cross providers. The rules and Step 7 no longer
contradict the mapping.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e-email

posthog-node has no migratedFrom option. The Wizard records the migration
on its setup wizard finished event instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…-email rewrites a site

A posthog-node release with posthog.emails can still lack templateKey,
local template content, or the extra send fields. The rewrite now checks
the installed types for what each moving site needs, and keeps a site
on Resend when the latest release lacks it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…rt every behaviour change

Step 8 checks that the app sends to the MCP's project, notes the start of the
test, and counts only events since then whose workflow matches the send. It
compares typecheck errors with a baseline step 7 takes before it changes code.
A template's content waits in a temporary file for step 7 instead of in memory,
an HTML fallback keeps a template on Resend, and a derived templateKey is
normalized and unique. The report lists each suppressed-recipient branch,
copy-address refusal, and cross-provider idempotency risk, and reminds the
person to delete the full-access Resend key. The posthog-node client setup is
inlined into step 7, and description.md says this skill wins over the general
moving guide.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… keep template files traceable

Step 8 takes the start of the test from `execute-sql` and compares Unix
timestamps, so the project's timezone can't shift the window. The plan lists
each saved template file, step 7 reads the module from it, and step 9 deletes
every listed file. A derived templateKey falls back to the template id when
the name normalizes to nothing and stays unique across the run. Step 8 may fix
other posthog-node code the upgrade breaks, and the report lists those files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ndency upgrades

The clock query carries a fresh random literal so a cached answer can't widen
the test window. Step 3 may write the template files it saves, the dashboard
copy includes the template's alias or name for its key, and a major
dependency upgrade lands in the report's "Before cutover" list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci ai-observability
  • /wizard-ci basic-integration
  • /wizard-ci feature-flags
  • /wizard-ci mcp-analytics
  • /wizard-ci replay-vision
  • /wizard-ci revenue
  • /wizard-ci self-driving
  • /wizard-ci warehouse
  • /wizard-ci warehouse-seeded

Test an individual app:

  • /wizard-ci ai-observability/anthropic
  • /wizard-ci ai-observability/google-adk
  • /wizard-ci ai-observability/groq
Show more apps
  • /wizard-ci ai-observability/manual-capture
  • /wizard-ci ai-observability/openai
  • /wizard-ci ai-observability/openai-agents
  • /wizard-ci ai-observability/opentelemetry
  • /wizard-ci ai-observability/vercel-ai
  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/flutter
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci feature-flags/django
  • /wizard-ci feature-flags/next-js
  • /wizard-ci mcp-analytics/custom-dispatcher
  • /wizard-ci mcp-analytics/typescript-sdk
  • /wizard-ci replay-vision/javascript-node
  • /wizard-ci replay-vision/next-js
  • /wizard-ci replay-vision/react-vite
  • /wizard-ci revenue/stripe
  • /wizard-ci self-driving/astro
  • /wizard-ci self-driving/fastapi
  • /wizard-ci self-driving/nuxt
  • /wizard-ci self-driving/react-router
  • /wizard-ci self-driving/sveltekit
  • /wizard-ci warehouse/monorepo-env
  • /wizard-ci warehouse/multi-source-next
  • /wizard-ci warehouse/stripe-node
  • /wizard-ci warehouse/zero-source
  • /wizard-ci warehouse-seeded/next-stripe
  • /wizard-ci warehouse-seeded/next-stripe-declined

Test against a wizard branch:

  • /wizard-ci all wizard:my-branch

Add wizard:<branch> to any command above to pin the wizard branch. It defaults to main.

Results will be posted here when complete.

Silthus and others added 11 commits November 2, 2026 15:05
Resolves the inventory, mapping and report conflicts toward the scope cut:
a Resend template site moves only to a template made in PostHog, sent by
templateId, and headers, tags and attachments stay on Resend.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under the scope cut, a moved template site sends a template made in
PostHog by templateId with templateData. Rendered sends keep templateKey,
subject, html and text. No local-template module, no upload on first send,
and no cc, bcc, headers, tags or attachments.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
posthog-node's migratedFrom client option sends the provider a send
moved from with every email request. Step 7 now sets it, and the
Resend mapping names its value.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… site moves

Review round 1 on #451:
- Step 3 asks for the template ID once the person made the template in
  PostHog. A template without one stays: PostHog template not made yet.
- Step 7 checks that the client takes migratedFrom, and undoes the
  posthog-node install when no site is left to move.
- Template files from an API read are deleted once their site is
  classified, and the full-access Resend key right after Step 5.
- Step 8 reads only the token's line of the env file, and picks a
  signed-in flow for the test email.
- replyTo moves as one address or a list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… cutover

Review round 2 on #451:
- Step 3 asks for the address a PostHog template sends from and writes it
  in the site's from column, so Step 4 sets up a sender for its domain.
- The report asks the person to send a test of each PostHog template and
  check that it is transactional, has a verified sender and fills its
  variables.
- Step 5 asks to delete the full-access key on every way out of the step,
  and after a partial import only once the person won't repeat it.
- Step 8 allows a signed-out flow: the person signs in as the recipient
  afterwards, so product events show on the same distinct ID.
- migratedFrom is set only when the client options take it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…mes a person

Review round 3 on #451:
- Step 5 asks once whether to repeat an import that stopped partway, and
  asks about the full-access key after that answer or the repeat.
- Step 8's test email comes from a moved site that passes distinctId;
  without one, the chain is recorded as not checked.
- The client example marks migratedFrom as conditional.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts:
#	context/skills/migrate-email/references/3-inventory.md
#	context/skills/migrate-email/references/7-report.md
#	context/skills/migrate-email/references/resend/mapping.md
account.md said template content goes only into a temporary file, while
Step 3 reads the person's dashboard copies where they saved them. Only an
API answer goes into the temporary file now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts:
#	context/skills/migrate-email/references/9-report.md
#	context/skills/migrate-email/references/resend/mapping.md
@Silthus
Silthus force-pushed the feat/migrate-email-resend-cutover branch from 21b5edd to 21f683e Compare November 2, 2026 16:49
Rename "Find the user ID" to "Link emails to your users", name the
identify() ID, say that without distinctId PostHog records the email on a
separate person keyed by the address, and add invites and signup checks to
the cases that leave it out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 7 now leaves distinctId off an address check before the account
exists, so Step 8 picks its test site from the plan rows that pass one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus and others added 3 commits November 4, 2026 08:24
The rows said only "Always", so an agent made up a reason, such as
posthog-node not taking attachments. They now say this skill doesn't
move them yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 9's report tells the person what would let a site move, from the
row's reason. cc, bcc, attachments, headers and tags now say PostHog's
send doesn't take them yet, not that this skill doesn't move them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus added a commit that referenced this pull request Nov 4, 2026
Carry #451's reason wording to SendGrid's field rows that kept a field
with a bare "Always". For Resend's cc, bcc and attachments, say the skill
doesn't move them yet: posthog-sdks.md in this PR lists them for Node.js,
so "PostHog's send doesn't take it" would contradict it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus and others added 5 commits November 4, 2026 08:48
…unded

A key built from a URL or caller text could read as either "can be
longer than 255 characters" or not. Such a key now counts as one that
can, so the site stays on Resend and the report names the value to bound.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A double-quoted Liquid fallback inside the template's JSON body raises
TokenizationError. Every fallback is quoted with ', and a fallback that
holds a ' keeps the site on Resend, since Liquid has no escape character.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…finds it

Step 6 ticked the key phase as set when the person said they would add
the key themselves. The step now checks the env file with a count-only
grep (or a successful set_env_values in the Wizard). A promise to add it
later, or a refusal, records the key as not set, and the report lists it
before cutover.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
dotenv loads a key with leading whitespace or spaces around =, so the
count-only grep accepts them too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The count-only grep counted a key that an env loader reads as empty: a
value that is only a comment (= #phs_x), and a key overridden by a later
empty line. The check now reads only the last line that sets the
variable and needs its value to start with phs_, still printing only a
count. The test runs the documented command against fixture env files
in place of matching its wording.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Silthus
Silthus marked this pull request as ready for review November 5, 2026 08:42
@Silthus
Silthus requested a review from a team as a code owner November 5, 2026 08:42
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.

2 participants