feat: emit posthog-next-steps.md alongside the setup report#448
Open
ethangui wants to merge 6 commits into
Open
feat: emit posthog-next-steps.md alongside the setup report#448ethangui wants to merge 6 commits into
ethangui wants to merge 6 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.