Skip to content

feat: emit posthog-next-steps.md alongside the setup report#448

Open
ethangui wants to merge 6 commits into
PostHog:mainfrom
ethangui:feat/coding-agent-handoff
Open

feat: emit posthog-next-steps.md alongside the setup report#448
ethangui wants to merge 6 commits into
PostHog:mainfrom
ethangui:feat/coding-agent-handoff

Conversation

@ethangui
Copy link
Copy Markdown

@ethangui ethangui commented May 9, 2026

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:

  • That production builds may need re-running to catch lint / type issues from generated code.
  • Which env var names were just added (so they can be mirrored in .env.example, monorepo bootstrap scripts, etc.).
  • That a $ai_generation smoke-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.
  • That a "returning visitor" path can bypass the auth-handler-only identify call, 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 doc

Adds 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:

Section Condition
Run unit tests — wizard-rewritten or wizard-instrumented call sites may need updated mocks JS/TS integrations
LLM smoke-test ($ai_generation) — strategy-agnostic, points the reader at the setup report for specifics additionalFeatureQueue contains AdditionalFeature.LLM
Source-maps glue Browser-bundle integrations only (nextjs, nuxt, vue, react-router, tanstack-*, angular, astro, sveltekit, javascript_web)
Env var names in glue section, with singular / plural / empty-fallback prose Pulled from config.environment.getEnvVars(), never hardcoded

A 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:

Read posthog-setup-report.md and posthog-next-steps.md. Verify each item in the "Verify before merging" checklist. Apply any fixes for items that fail. Update the project glue listed in this file if it applies. Open a PR with the changes plus a summary of what was verified.

Built by buildCodingAgentPrompt(reportFile: string) in src/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 surface

The 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.md and posthog-next-steps.md" embedded inside posthog-next-steps.md is 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:

// src/lib/post-exit-message.ts
export function setPostExitMessage(session: WizardSession, message: string): void
export function getPostExitMessage(session: WizardSession): string | undefined

posthog-integration's postRun calls setPostExitMessage after a successful handoff write. start-tui.ts's cleanup reads via getPostExitMessage and writes to stdout AFTER releaseTerminal() — 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 direct process.exit, error paths).

What the user sees in their terminal after ✔ PostHog integration complete:

──────────────────────────────────────────────────────────────────────────────
Copy this into your coding agent (triple-click to select):
──────────────────────────────────────────────────────────────────────────────

Read `posthog-setup-report.md` and `posthog-next-steps.md`. Verify each item in the "Verify before merging" checklist. Apply any fixes for items that fail. Update the project glue listed in this file if it applies. Open a PR with the changes plus a summary of what was verified.

──────────────────────────────────────────────────────────────────────────────

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.postExitMessage

I tried OutroData.postExitMessage first. It silently failed end-to-end. Diagnostic instrumentation in cleanup confirmed the cause:

  • nanostores' setKey shallow-spreads the top-level session object, so frameworkContext (nested) is shared by reference across atom replacements, but outroData (top-level) is not.
  • agent-runner.ts captures a session reference at the start of runAgent, then near the end runs session.outroData = config.buildOutroData(...). By that point, every pushStatus / 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.outro reads this.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 reportFile today (which is why the Check ./posthog-setup-report.md for details exit-line suffix doesn't render — the team's existing partial workaround in ink-ui.outro covers message only). Worth a follow-up wizard-level fix that has agent-runner.ts push outroData through store.setOutroData(...) rather than direct mutation.

For this PR I worked around it via frameworkContext-as-side-channel. Same mechanism the existing getNextStepsHandoff / setNextStepsHandoff and DASHBOARD_DEEP_LINK_KEY accessors 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) on frameworkContext. Includes a docblock explaining the staleness rationale so a future contributor doesn't "simplify" it back to outroData.
  • 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 helper buildHandoffBullet(status). writeNextStepsFile returns a discriminated union { ok: true; path } | { ok: false; error } matching the existing wizard-tools.ts convention.
  • postRun in posthog-integration/index.ts — calls the writer with the actual integration enum value, the actual env var names from config.environment.getEnvVars(), and session.additionalFeatureQueue.includes(AdditionalFeature.LLM). Stashes the handoff result via setNextStepsHandoff, then on success stashes the framed prompt via setPostExitMessage. On failure: getUI().log.warn(...) + analytics.wizardCapture('next steps file write failed', ...).
  • buildOutroData — renders either Wrote posthog-next-steps.md ... or Could 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.tscleanup reads getPostExitMessage(store.session) and writes to stdout after releaseTerminal(). Workflow-agnostic surface; any future workflow can opt in.
  • OutroData.postExitMessage was removed. Advertising a field that the wizard's own session-mutation pattern silently drops is worse than not advertising it.

Type design

NextStepsContext.integration is the Integration enum (not a loose string), and KNOWN_QUIRKS_BY_INTEGRATION is Record<Integration, string[]> and exhaustive — adding a new Integration member requires declaring its quirks list (an empty array is fine and explicit). Mirrors the FRAMEWORK_REGISTRY pattern in src/lib/registry.ts.

writeNextStepsFile's return type, getNextStepsHandoff's read, and the ink-ui.outro-style fallback all use the same discriminated-union shape, narrowed via runtime type guards. The accessor pairs centralize the unsafe frameworkContext cast behind one runtime check per type, mirroring getAuditChecks in audit/types.ts.

Known limitations / open questions

  • Wizard architectural follow-up: agent-runner.ts session.outroData = X direct mutation drops fields silently due to nanostores' setKey replacing the top-level session. The right fix is wizard-level — push outroData through store.setOutroData(...) (probably accept OutroData as a second arg to getUI().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 overlap with the agent-written report: agent-prompt.ts:35-38 still tells the skill agent to write "any manual steps the user should take next" into posthog-setup-report.md. With the handoff doc those steps are now duplicative. Touching the shared skill prompt would also affect audit, agent-skill, and revenue-analytics — left for the team to decide direction.
  • No quirks pre-filled. All entries in KNOWN_QUIRKS_BY_INTEGRATION are empty arrays today. An earlier iteration registered an @posthog/ai messages.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.
  • Surfacing the agent prompt in-TUI is intentionally out of scope. setPostExitMessage solves the persistent-scrollback need at the smallest blast radius. Rendering it inside OutroScreen and/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), buildCopyPasteBlock shape, buildCodingAgentPrompt parameter 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 to outroData will fail in CI with a comment explaining why.

100% line coverage of handoff.ts and post-exit-message.ts.

Beyond unit tests:

  1. I drove posthogIntegrationConfig.run().postRun() + .buildOutroData() directly against a fresh git clone of 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.

  2. Then I ran the actual wizard end-to-end via node dist/bin.js against 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:

  • Empty installDir: path resolution is upstream; sess.installDir is always a real string by the time postRun fires.
  • Markdown special chars in frameworkName: sourced from config.metadata.name (a wizard-internal constant), not user input — a test would lock in an arbitrary escaping policy that doesn't currently exist.
  • Very long quirk lists: quirks are a hand-curated internal const, not external input.

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 review second-opinion pass per round. Concrete things each round caught:

  • Round 1. KNOWN_QUIRKS_BY_INTEGRATION keyed by skill IDs ('nextjs-app-router') but the runtime label is the Integration enum ('nextjs'); the map never matched in production. Tightened to Record<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). Hardcoded NEXT_PUBLIC_* env names. No telemetry on failure path.
  • Round 2. NEXT_STEPS_FILE constant dropped from imports during a refactor, leaving a hardcoded string in the warn line that could diverge from buildHandoffBullet. Type-design recommended exporting NextStepsHandoffStatus and adding a getNextStepsHandoff / setNextStepsHandoff accessor pair (mirroring getAuditChecks precedent). Silent-failure-hunter recommended a runtime type-guard for the frameworkContext read so a future shape drift is caught at runtime instead of producing garbage.
  • Round 3. Cross-read against a real wizard run revealed the LLM-quirks section assumed the @posthog/ai PostHogAnthropic wrapper — but the wizard chose OpenTelemetry auto-instrumentation that run. Strategy-agnostic refactor.
  • Round 4. Circular-reference critique on the embedded "Hand this to your coding agent" section. Moved the prompt out of the doc.
  • Round 5 (this round). Real wizard runs surfaced that the OutroData.postExitMessage field was silently dropped end-to-end. Diagnostic instrumentation in cleanup confirmed the stale-session bug. Pivoted to the workflow-agnostic setPostExitMessage / getPostExitMessage side-channel. Trimmed duplicated rationale in three places, narrowed buildCodingAgentPrompt to (reportFile: string), exported POST_EXIT_MESSAGE_KEY so 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.

ethangui and others added 6 commits May 9, 2026 09:25
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>
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.

Emit a coding-agent handoff doc alongside posthog-setup-report.md

1 participant