feat: emit posthog-next-steps.md alongside the setup report#448
feat: emit posthog-next-steps.md alongside the setup report#448ethangui wants to merge 7 commits into
Conversation
7ad5692 to
6133830
Compare
Closes the gap between "wizard finished" and "PostHog is integrated and merged" (issue PostHog#447). Today the wizard emits posthog-setup-report.md as a manifest of what changed; this PR adds a companion posthog-next-steps.md that tells the user (or their coding agent) what still needs to be verified, which SDK quirks to watch for, and which project-specific glue the wizard intentionally never touches. The doc is rendered deterministically by the wizard (not asked of the agent) so the verification checklist + known-quirk list stay stable across model versions. It is conditional on: - Whether the integration is JS/TS — gates the SDK-mock advice and the "grep for new Anthropic()" item that don't apply to Django, Rails, Swift, Android, etc. - Whether the integration produces minified browser bundles — gates the source-maps follow-up. - Whether LLM analytics is queued in the same run — gates the $ai_generation smoke-test and the @posthog/ai streaming quirk. - The actual env var names from config.environment.getEnvVars(), so Vue / Nuxt / Astro / etc. users see the right placeholders, not hardcoded NEXT_PUBLIC_* names. Type-tightening: NextStepsContext.integration is the Integration enum (not a loose string), and KNOWN_QUIRKS_BY_INTEGRATION is exhaustive over the enum. A future Integration addition will fail to compile until its quirks list is declared (empty array is a valid choice). Outro honesty: postRun stashes the writeNextStepsFile result on sess.frameworkContext, and buildOutroData renders either a "Wrote ..." or "Could NOT write ... — handoff steps are missing" bullet from that status. Failures also fire analytics.wizardCapture so the team learns about field failures (read-only fs, EACCES, etc.) instead of having to diagnose them off cli warnings. Tests: 13 cases covering both code paths, exhaustive Record, all conditional toggles, the agent-handoff filename binding, and a deterministic missing-intermediate-dir failure path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-2 review feedback (codex clean; 5 specialists). Two
recommendations converged:
1. Type-design + silent-failure: the discriminated union
`{ ok: true; path } | { ok: false; error }` was duplicated as an
inline return type on `writeNextStepsFile` AND a local alias in
`index.ts`. Cast-at-each-callsite let drift go silent.
2. Test-analyst + silent-failure: the new `buildOutroData` three-branch
render (ok / fail / undefined) had no tests. The whole point of the
"outro tells the truth on failure" change rested on inspection.
Fix: extract the surface to `handoff.ts`.
- `NextStepsHandoffStatus` is now an exported type; `writeNextStepsFile`
returns it by name.
- `getNextStepsHandoff(session)` reads + type-guards
`frameworkContext[NEXT_STEPS_HANDOFF_KEY]`. The previous unsafe `as`
cast became a runtime check that rejects bogus shapes (defends against
any other code overwriting the key with the wrong value type).
- `setNextStepsHandoff(session, status)` writes it. Mirrors the
`getAuditChecks` precedent in `audit/types.ts`.
- `buildHandoffBullet(status | undefined)` is the pure outro-rendering
helper. `index.ts` now does `buildHandoffBullet(getNextStepsHandoff(sess))`
and `.filter(Boolean)` drops the empty string for the "never wrote"
case.
Test coverage moved from 13 → 21:
- Three new tests for `buildHandoffBullet` (ok / fail / undefined).
- Four new tests for the accessor pair (round-trip, missing key returns
undefined, type-guard rejects 7 shapes of garbage).
- New test pinning that `react-native` is in `JS_INTEGRATIONS` but NOT
in `SOURCE_MAP_INTEGRATIONS` — a silent regression class the previous
set of tests missed.
- New test covering env-var-name pluralization across 3 branches
(singular / plural / empty fallback). Default ctx exercised only the
plural path; the off-by-one on `length === 1 ? '' : 's'` would have
shipped silently.
Comment cleanup from round-2 reviews:
- Tightened `NextStepsContext.integration` field doc to name what
*changes* when the value changes (quirk lookup, JS branching,
source-maps inclusion).
- Rephrased `writeNextStepsFile` try-block comment around the contract
("intentionally narrow — covers only the disk write") rather than the
fragile "only fs.writeFileSync may throw" line layout.
- Dropped misleading "Compute env vars first" comment — `getEnvVars`
was already the first call in `postRun` on main; the previous wording
implied a reorder that did not happen.
- Added a `SOURCE_MAP_INTEGRATIONS ⊆ JS_INTEGRATIONS` subset note next
to the Set so the implicit invariant is documented.
`pnpm typecheck` clean. `pnpm lint` 0 errors. `pnpm jest` 36 suites,
618 / 621 passed (3 pre-existing skips). `handoff.ts` at 100% line
coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-import the constant and template it into the warn line. The previous commit dropped NEXT_STEPS_FILE from the import block when the warn line was rewritten, leaving a hardcoded "posthog-next-steps.md" string. The outro bullet (rendered via buildHandoffBullet) and the failure warn line could have silently diverged on a future filename rename. Caught by round-2 code review (confidence 80, DRY/maintenance issue). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rompt End-to-end run against a fresh flashy clone revealed the LLM-content section assumed the @posthog/ai PostHogAnthropic wrapper path, but the wizard can also pick OpenTelemetry auto-instrumentation for the same JS framework + same package set (and did pick OTel in this run). My wrapper-specific guidance was therefore wrong half the time. Round-3 changes: - Drop LLM_ANALYTICS_QUIRKS_FOR_JS entirely. The PostHogAnthropic streaming quirk only applies to one of two installation strategies; baking it into the handoff was wrong for OTel runs. Strategy-specific guidance belongs in the agent-written setup report. - Drop the "grep for new Anthropic() constructors / @anthropic-ai/sdk imports" verify item — same reason. - Soften the "wizard-rewritten routes may have outdated mocks" wording to "wizard-rewritten or wizard-instrumented call sites may need updated mocks" — true under both strategies. - Soften the token-absent prose from "PostHog client wrapper" to "PostHog client setup" — the wizard might have written a wrapper or an OTel boot file, the noop-shim advice applies generically either way. - Add a pointer in the LLM smoke-test bullet: "(See posthog-setup-report.md for the specific LLM-analytics approach this run used.)" so the reader knows where strategy-specific details live. Surface the coding-agent prompt for easy copy/paste: - Extract `buildCodingAgentPrompt(ctx)` as a pure exported function. Now reusable from the TUI / CLI without scraping the rendered markdown. - Wrap the embedded prompt in a fenced code block instead of a blockquote. Triple-click selects cleanly in any editor / terminal; the previous `> ` blockquote pulled in the prefix on copy. Tests: 22 → 25 cases. New buildCodingAgentPrompt block (single-line contract, alternate reportFile name), new fenced-code-block embedding test. Replaced wrapper-specific tests with the strategy-agnostic counterpart and a regression test that no wrapper-specific advice sneaks back in. `pnpm typecheck` clean. `pnpm lint` 0 errors. `pnpm jest` 619 passed. e2e check against fresh flashy clone: all 16 content checks pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ollback The handoff doc embedded a "Hand this to your coding agent" section containing the literal text "Read posthog-setup-report.md AND posthog-next-steps.md" — a circular reference inside the very file the prompt instructs the agent to read. Every time the agent re-reads the file it re-tokenizes the same prompt block. Wasteful, weird. Move the prompt out of the doc: - buildNextStepsMarkdown drops the trailing section. The doc body is now purely the content the agent needs (verify / quirks / glue / token-absent). - buildCodingAgentPrompt remains exported as a pure function (sourced separately by the wizard's CLI). - buildCopyPasteBlock wraps the prompt with a ─-rule frame for terminal-scrollback visibility. Surface the prompt in the user's normal terminal scrollback (where they can triple-click to copy): - New OutroData.postExitMessage field — workflow-agnostic, any future workflow can opt in. - bin.ts captures `tui.store.session.outroData?.postExitMessage` before tui.unmount() and writes it to stdout AFTER the alternate screen tears down. The TUI's alternate-screen erases anything printed during the run; this is the workflow-agnostic way to surface persistent post-exit text. - posthog-integration's buildOutroData populates postExitMessage with buildCopyPasteBlock(buildCodingAgentPrompt(ctx)) — only when the handoff write succeeded (no point dangling the prompt if the file isn't there). Doc intro updated to be honest: "Work through the checklist below before merging. If you are handing this to a coding agent, the wizard printed a copy-paste-ready prompt at the end of its run." Tests: 23 → 25 cases. New `buildCopyPasteBlock` shape test, new "handoff doc does NOT embed the agent prompt" regression catch (so the circular section can't sneak back in). Replaced the in-doc-embedding test with a "points the reader at the wizard run output" test. `pnpm typecheck` clean. `pnpm lint` 0 errors. `pnpm jest` 620 passed. e2e check: all 16 content checks pass, including a new check that postExitMessage is populated with the wrapped prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real-run testing surfaced that the prior `OutroData.postExitMessage`
field was silently lost. Diagnostic instrumentation in cleanup confirmed
the cause: nanostores' `setKey` shallow-spreads the top-level session
object, which means `frameworkContext` (a nested field) is shared by
reference across atom replacements but `outroData` (a top-level field)
is NOT. agent-runner.ts captures a session reference at the start of
`runAgent`, then runs `session.outroData = config.buildOutroData(...)`
near the end — by which time pushStatus / setOutroDismissed / etc. have
replaced the atom session many times. The mutation goes to a stranded
object the atom no longer references; ink-ui.outro reads the current
atom view (undefined) and falls back to `{kind, message}` (no reportFile,
no postExitMessage, no changes).
This is a wizard-architectural bug — the same pattern silently drops
`reportFile` from the exit-line "Check ./posthog-setup-report.md for
details" suffix today. The team's existing partial workaround in
ink-ui.outro covers `message` only.
The right fix is wizard-level (agent-runner should push outroData via
`store.setOutroData`, not direct mutation). For this PR I work around
it via a workflow-agnostic side channel that doesn't need any
agent-runner changes:
- `src/lib/post-exit-message.ts` — `setPostExitMessage(session, text)` /
`getPostExitMessage(session)`. Direct mutation on `frameworkContext`,
which IS shared by reference across `setKey` shallow-spreads. Same
pattern the existing `getNextStepsHandoff` / `setNextStepsHandoff` and
`DASHBOARD_DEEP_LINK_KEY` accessors use, just generalized for any
workflow.
- `src/ui/tui/start-tui.ts` cleanup reads via `getPostExitMessage` and
prints to scrollback after `releaseTerminal()`. Covers every exit
path — explicit `tui.unmount()` from bin.ts AND any caller-driven
`process.exit(N)` (e.g. `KeepSkillsScreen.tsx` exits the process
directly when the user declines).
- `src/lib/wizard-session.ts` — drops `OutroData.postExitMessage`. That
field was a broken contract: the type advertised it, but the wizard's
own session-mutation pattern silently dropped writes to it. Better to
not advertise it at all than to give workflows a foot-gun.
- `posthog-integration/index.ts` `postRun` calls `setPostExitMessage`
on a successful handoff write.
Side-by-side simplifications from the in-tree review pass:
- `buildCodingAgentPrompt(reportFile: string)` instead of a 5-field
context. The function read exactly one field; `NextStepsContext` was
borrowed-shape coupling.
- bin.ts comment updated to reference the actual mechanism (was still
naming the dropped `outroData.postExitMessage` field).
- start-tui.ts cleanup comment shrinks to a one-line pointer at
post-exit-message.ts (was duplicating the rationale).
- `POST_EXIT_MESSAGE_KEY` exported and used in the test (was a
module-private const, with the test hardcoding the literal string —
silent drift risk).
- Inlined the redundant `handoffStatus` intermediate in buildOutroData.
Tests +4: `post-exit-message.test.ts` covers round-trip, missing-key
returns undefined, type-guard rejects non-string values, AND the
"survives shallow setKey clones of the top-level session" regression
catch — pins the actual bug we hit so a future "simplification" back to
outroData fails CI with a comment explaining why.
`pnpm typecheck` clean. `pnpm lint` 0 errors. `pnpm jest` 624 / 627
(3 pre-existing skips). e2e check (drives postRun + buildOutroData
against a fresh flashy clone with mocked credentials) passes 16 / 16
content checks. Verified end-to-end with a real wizard run that
exercises the agent loop (cost ~$5, ~12 min): the framed prompt block
now appears in scrollback after `tui.unmount`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6133830 to
ceec61b
Compare
Resolves conflicts from two mechanical refactors that landed on main: - workflows -> programs rename (PostHog#476): handoff.ts, handoff.test.ts and the posthog-integration index moved into src/lib/programs/; symbol references (POSTHOG_INTEGRATION_PROGRAM, ProgramConfig/ProgramRun) auto-merged. - relative-import -> path-alias migration (PostHog#485): converted the handoff feature's cross-boundary imports (@lib/...) to match the new convention; resolved the start-tui.ts import-block conflict. No behavioral changes to the handoff feature. Build, lint (0 errors) and full test suite (672 tests) pass.
gewenyu99
left a comment
There was a problem hiding this comment.
Hey! This is a great idea :D
There's a few things I'm gonna ask you to do to get this merged:
- I think this PR should go mostly in the PostHog/context-mill repo as a resource.
- I think this should be part of the same report, we can have this next steps as a check list at the bottom.
- I would let the model pick out relevant items to plop into the report.
I have some more comments on the actual content, but I'll defer the review there.
Please remember to @PostHog/team-docs-wizard for review! We sometimes miss these PRs.
Thanks for doing this!
|
Thanks @gewenyu99, agreed — much more elegant to keep the content in context-mill. Turns out the report template (incl. One open question: the original PR also printed a copy-paste prompt to the terminal after exit ("read the report, verify the checklist, open a PR"). That can't move to context-mill — terminal output is wizard-side. My preference is to keep a simple one-liner handoff there so the operator can paste it straight into their coding agent — feels like the piece that turns "report written" into "one prompt from done." What do you think? |
|
@ethangui Agreed. I think that can stay in this PR. A custom handoff screen is a good idea |
|
Closing this in favor of two focused PRs that implement the approach we settled on in review — thanks @gewenyu99 for the steer. After your feedback the change drifted substantially from what's here: the verification content moved into context-mill, and the wizard side slimmed to just surfacing the handoff. Rather than force-push a wholesale rewrite over this thread, it's cleaner to split it:
Both reference #447, which stays open until they land and context-mill releases. Review continues on those two PRs (cc @PostHog/team-docs-wizard). |
Problem
Closes #447.
The wizard finishes a successful run by emitting
posthog-setup-report.md— a manifest of what changed. That works as a record but leaves a real gap between "wizard finished" and "PostHog is integrated and merged." Closing that gap requires the developer (or their coding agent) to discover several things independently — most of which the wizard already knows at run time:.env.example, monorepo bootstrap scripts, etc.).$ai_generationsmoke-test is useful when LLM analytics is queued — and irrelevant on a Django / Rails / Swift / Android / etc. integration that doesn't have an LLM call site.identifycall, leaving session events on anonymous distinct ids.Encoding that as a deterministic handoff turns "90% done" into "one prompt away from done" for any agent picking up the work.
Changes
posthog-next-steps.md— the handoff docAdds a deterministic markdown file alongside the agent-written
posthog-setup-report.md. Where the report describes WHAT changed, the new file describes what still needs to happen — verification steps, known SDK quirks (currently empty per integration; see "open questions"), and project glue the wizard intentionally never touches.The doc adapts to the run:
Run unit tests — wizard-rewritten or wizard-instrumented call sites may need updated mocks$ai_generation) — strategy-agnostic, points the reader at the setup report for specificsadditionalFeatureQueuecontainsAdditionalFeature.LLMnextjs,nuxt,vue,react-router,tanstack-*,angular,astro,sveltekit,javascript_web)config.environment.getEnvVars(), never hardcodedA Django, Rails, Swift, Android, etc. user no longer gets a handoff doc telling them to grep for
@anthropic-ai/sdk.The agent prompt itself — please review the wording
This PR introduces a single canned prompt the user copies into their coding agent. The exact text is up for review — it's the handoff's load-bearing UX, and the wizard team should have final say on tone, length, and instructions:
Built by
buildCodingAgentPrompt(reportFile: string)insrc/lib/workflows/posthog-integration/handoff.ts. Single paragraph (no embedded newlines, so triple-click selects cleanly). If you want different wording, that one function is the only place to change it.setPostExitMessage/getPostExitMessage— the agent-prompt surfaceThe handoff doc deliberately does not embed the prompt. An earlier iteration did, and the obvious problem surfaced quickly: a prompt instructing the agent to "Read
posthog-setup-report.mdandposthog-next-steps.md" embedded insideposthog-next-steps.mdis a circular reference — the agent re-tokenizes the same prompt every time it re-reads the file.So the prompt is sourced separately, wrapped in a horizontal-rule frame (
buildCopyPasteBlock), and surfaced through a new workflow-agnostic accessor pair:posthog-integration'spostRuncallssetPostExitMessageafter a successful handoff write.start-tui.ts'scleanupreads viagetPostExitMessageand writes to stdout AFTERreleaseTerminal()— so the message lands in the user's normal scrollback (where they can triple-click it for copy) regardless of which exit path the wizard takes (tui.unmount,KeepSkillsScreen.tsx's directprocess.exit, error paths).What the user sees in their terminal after
✔ PostHog integration complete:The horizontal rules and header sit at column 0; the prompt body is on its own line surrounded by blanks so triple-click selects only the prompt.
Why a side channel and not
OutroData.postExitMessageI tried
OutroData.postExitMessagefirst. It silently failed end-to-end. Diagnostic instrumentation incleanupconfirmed the cause:setKeyshallow-spreads the top-level session object, soframeworkContext(nested) is shared by reference across atom replacements, butoutroData(top-level) is not.agent-runner.tscaptures a session reference at the start ofrunAgent, then near the end runssession.outroData = config.buildOutroData(...). By that point, everypushStatus/setOutroDismissed/ etc. during the agent run has replaced the atom session many times. The mutation goes to a stranded object the atom no longer references.ink-ui.outroreadsthis.store.session.outroData(current atom view, undefined) and falls back to{kind, message: stripAnsi(config.successMessage)}. Every other field —reportFile,changes,postExitMessage— is silently dropped on the floor.This is a wizard-architectural bug that already affects
reportFiletoday (which is why theCheck ./posthog-setup-report.md for detailsexit-line suffix doesn't render — the team's existing partial workaround inink-ui.outrocoversmessageonly). Worth a follow-up wizard-level fix that hasagent-runner.tspushoutroDatathroughstore.setOutroData(...)rather than direct mutation.For this PR I worked around it via
frameworkContext-as-side-channel. Same mechanism the existinggetNextStepsHandoff/setNextStepsHandoffandDASHBOARD_DEEP_LINK_KEYaccessors use, just generalized to any workflow that wants to surface persistent post-exit text.Implementation map
src/lib/post-exit-message.ts(new) — workflow-agnostic accessor pair (setPostExitMessage/getPostExitMessage) onframeworkContext. Includes a docblock explaining the staleness rationale so a future contributor doesn't "simplify" it back tooutroData.src/lib/workflows/posthog-integration/handoff.ts(new) — pure helpers:buildNextStepsMarkdown(ctx),writeNextStepsFile(installDir, ctx),buildCodingAgentPrompt(reportFile),buildCopyPasteBlock(prompt), plus handoff-status accessors (getNextStepsHandoff/setNextStepsHandoff) and the outro-bullet helperbuildHandoffBullet(status).writeNextStepsFilereturns a discriminated union{ ok: true; path } | { ok: false; error }matching the existingwizard-tools.tsconvention.postRuninposthog-integration/index.ts— calls the writer with the actual integration enum value, the actual env var names fromconfig.environment.getEnvVars(), andsession.additionalFeatureQueue.includes(AdditionalFeature.LLM). Stashes the handoff result viasetNextStepsHandoff, then on success stashes the framed prompt viasetPostExitMessage. On failure:getUI().log.warn(...)+analytics.wizardCapture('next steps file write failed', ...).buildOutroData— renders eitherWrote posthog-next-steps.md ...orCould NOT write posthog-next-steps.md (...)in the changes list. (Note: this bullet is itself silently dropped due to the same stale-session bug above; it'll show up in the OutroScreen once the wizard team fixes that.)bin.ts+src/ui/tui/start-tui.ts—cleanupreadsgetPostExitMessage(store.session)and writes to stdout afterreleaseTerminal(). Workflow-agnostic surface; any future workflow can opt in.OutroData.postExitMessagewas removed. Advertising a field that the wizard's own session-mutation pattern silently drops is worse than not advertising it.Type design
NextStepsContext.integrationis theIntegrationenum (not a loosestring), andKNOWN_QUIRKS_BY_INTEGRATIONisRecord<Integration, string[]>and exhaustive — adding a newIntegrationmember requires declaring its quirks list (an empty array is fine and explicit). Mirrors theFRAMEWORK_REGISTRYpattern insrc/lib/registry.ts.writeNextStepsFile's return type,getNextStepsHandoff's read, and theink-ui.outro-style fallback all use the same discriminated-union shape, narrowed via runtime type guards. The accessor pairs centralize the unsafeframeworkContextcast behind one runtime check per type, mirroringgetAuditChecksinaudit/types.ts.Known limitations / open questions
agent-runner.tssession.outroData = Xdirect mutation drops fields silently due to nanostores'setKeyreplacing the top-level session. The right fix is wizard-level — push outroData throughstore.setOutroData(...)(probably acceptOutroDataas a second arg togetUI().outro(message, data)so the call site doesn't need store access). Out of scope for this PR; happy to follow up if the team wants.agent-prompt.ts:35-38still tells the skill agent to write "any manual steps the user should take next" intoposthog-setup-report.md. With the handoff doc those steps are now duplicative. Touching the shared skill prompt would also affectaudit,agent-skill, andrevenue-analytics— left for the team to decide direction.KNOWN_QUIRKS_BY_INTEGRATIONare empty arrays today. An earlier iteration registered an@posthog/aimessages.stream(...)quirk for JS+LLM runs, but a real end-to-end wizard run revealed the wizard sometimes uses OpenTelemetry auto-instrumentation rather than the wrapper, where that quirk does not apply. I dropped the registration to avoid shipping wrong advice for half of all installs. The team is the right owner to register strategy-agnostic quirks when they emerge.setPostExitMessagesolves the persistent-scrollback need at the smallest blast radius. Rendering it insideOutroScreenand/or hooking up a[c]clipboard copy hotkey would be nicer UX but adds a clipboard dep + cross-platform handling and crosses workflow → TUI boundaries the team should design intentionally.Test plan
pnpm typecheck— clean.pnpm lint— 0 errors (warnings unchanged from main).pnpm jest— 36 suites, 624 passed / 3 pre-existing skips.Two new test files:
src/lib/workflows/posthog-integration/__tests__/handoff.test.ts— 25 cases covering markdown rendering, conditional content (LLM/JS/source-maps), env-var-name passthrough (singular/plural/empty),buildCopyPasteBlockshape,buildCodingAgentPromptparameter behavior, accessor pair, file-write happy + failure paths, and a "handoff doc does NOT embed the agent prompt" regression catch.src/lib/__tests__/post-exit-message.test.ts— 4 cases covering round-trip, missing-key returns undefined, type-guard rejects non-string values, and a "survives shallow setKey clones of the top-level session" test that pins the exact bug we hit. A future "simplification" back tooutroDatawill fail in CI with a comment explaining why.100% line coverage of
handoff.tsandpost-exit-message.ts.Beyond unit tests:
I drove
posthogIntegrationConfig.run().postRun()+.buildOutroData()directly against a freshgit cloneof a real Next.js project (/tmp/flashy-fresh-wizard-test) with mocked credentials — exercises every code path end-to-end except the LLM agent loop. All 16 content checks pass.Then I ran the actual wizard end-to-end via
node dist/bin.jsagainst the same fresh clone (real OAuth, real agent run, real PostHog project — full ~$5 / ~12 min loop). The handoff doc, the success bullet, and the post-exit prompt block all appeared as designed. The end-to-end run also surfaced two bugs that drove changes in this PR: the OTel-vs-wrapper LLM-analytics non-determinism (motivated dropping wrapper-specific content from the handoff) and the stale-session bug above (motivated the side-channel mechanism).Edge cases I considered but did not test:
installDir: path resolution is upstream;sess.installDiris always a real string by the timepostRunfires.frameworkName: sourced fromconfig.metadata.name(a wizard-internal constant), not user input — a test would lock in an arbitrary escaping policy that doesn't currently exist.LLM context
This PR was co-authored with Claude Code (Opus 4.7). Multiple rounds of multi-agent code review ran across the working tree before this body was finalized — five specialist reviewers (general code, test coverage, comment / docstring review, silent-failure hunt, type design) plus a
codex reviewsecond-opinion pass per round. Concrete things each round caught:KNOWN_QUIRKS_BY_INTEGRATIONkeyed by skill IDs ('nextjs-app-router') but the runtime label is theIntegrationenum ('nextjs'); the map never matched in production. Tightened toRecord<Integration, string[]>exhaustive over the enum. Handoff content over-fitted to JS+Anthropic; conditional sections gated on JS-ness + LLM-queued. Outro lied on write failure (changes-list bullet was unconditional). HardcodedNEXT_PUBLIC_*env names. No telemetry on failure path.NEXT_STEPS_FILEconstant dropped from imports during a refactor, leaving a hardcoded string in the warn line that could diverge frombuildHandoffBullet. Type-design recommended exportingNextStepsHandoffStatusand adding agetNextStepsHandoff/setNextStepsHandoffaccessor pair (mirroringgetAuditChecksprecedent). Silent-failure-hunter recommended a runtime type-guard for theframeworkContextread so a future shape drift is caught at runtime instead of producing garbage.@posthog/aiPostHogAnthropicwrapper — but the wizard chose OpenTelemetry auto-instrumentation that run. Strategy-agnostic refactor.OutroData.postExitMessagefield was silently dropped end-to-end. Diagnostic instrumentation incleanupconfirmed the stale-session bug. Pivoted to the workflow-agnosticsetPostExitMessage/getPostExitMessageside-channel. Trimmed duplicated rationale in three places, narrowedbuildCodingAgentPromptto(reportFile: string), exportedPOST_EXIT_MESSAGE_KEYso the test stops hardcoding the literal.I read the PostHog CONTRIBUTING.md and AI policy before submitting; the PR template structure here matches
.github/pull_request_template.md. Test coverage is unit + a deep integration script + a real wizard end-to-end run; full details in the test plan above.