|
| 1 | +# Driving & exploring the wizard as an agent |
| 2 | + |
| 3 | +A runbook for a future AI agent (you) that wants to **run the real wizard |
| 4 | +headlessly, drive its state, and snapshot the TUI to view it** — to explore or |
| 5 | +test the app with no terminal. It uses the control plane in this folder |
| 6 | +(`WizardCiDriver` + the `wizard-ci-tools` MCP server). For _how_ it works under |
| 7 | +the hood, read [`ARCHITECTURE.md`](ARCHITECTURE.md); this is the _how to do it_. |
| 8 | + |
| 9 | +## 0. Ask for the key, then set up |
| 10 | + |
| 11 | +**First, ask the user for the path to their PostHog key file** — e.g. "What's |
| 12 | +the absolute path to your phx key file?" — plus the project id and region if you |
| 13 | +don't have them. Then, in the shell you'll run from: |
| 14 | + |
| 15 | +```bash |
| 16 | +export POSTHOG_PERSONAL_API_KEY="$(cat <key-file-path>)" # the phx key |
| 17 | +export POSTHOG_WIZARD_PROJECT_ID=<project-id> # the team the key is scoped to |
| 18 | +export POSTHOG_REGION=us # or eu |
| 19 | +export WIZARD_PATH=<this wizard repo> # where e2e-harness/ lives |
| 20 | +``` |
| 21 | + |
| 22 | +Rules: **never print or commit the key.** Always run against a **`/tmp` copy** |
| 23 | +of an app, never a real fixture. If you're inside a Claude Code session, the |
| 24 | +harness strips the host `CLAUDE_*`/`ANTHROPIC_*` env for the child so the |
| 25 | +spawned agent auths with the phx key (the `apiKeySource: none` → 401 trap). |
| 26 | + |
| 27 | +## 1. Full run, then view it (the easy path) |
| 28 | + |
| 29 | +From [wizard-workbench](https://github.com/PostHog/wizard-workbench): |
| 30 | + |
| 31 | +```bash |
| 32 | +pnpm wizard-ci <app> --e2e # real agent, headless; writes a recording |
| 33 | +pnpm wizard-ci-snapshots <app> # renders each key moment → .ans + report.html |
| 34 | +``` |
| 35 | + |
| 36 | +To watch it back: |
| 37 | +`pnpm wizard-ci --replay /tmp/wizard-e2e-<app>.recording.json --step`, or just |
| 38 | +read the `.ans` frames / `report.html`. This is the whole flow, real agent, no |
| 39 | +decisions for you to make. |
| 40 | + |
| 41 | +## 2. Drive it yourself (the control plane) |
| 42 | + |
| 43 | +To step the flow and **decide each screen**, use the three primitives — |
| 44 | +`read_state`, `list_actions`, `perform_action`. They're exposed as the |
| 45 | +`wizard-ci-tools` MCP server (`createWizardCiToolsServer`) for a connected |
| 46 | +driver model; the same primitives are `WizardCiDriver` methods you can call |
| 47 | +directly from a tsx script. The loop is always: |
| 48 | + |
| 49 | +``` |
| 50 | +read_state → look at currentScreen + the legal actions → perform_action(one of them) → read_state → … |
| 51 | +``` |
| 52 | + |
| 53 | +Recipe — write it to a scratch file **inside this repo** so the `@lib`/`@ui`/ |
| 54 | +`@e2e-harness` aliases resolve (a `/tmp` file won't see the tsconfig). Name it |
| 55 | +`scripts/explore.no-jest.ts` (the `.no-jest` suffix keeps Jest from picking it |
| 56 | +up), run `npx tsx scripts/explore.no-jest.ts` from `$WIZARD_PATH`, then delete |
| 57 | +it. It drives the **UI** screens offline (no agent/auth) and renders each one so |
| 58 | +you can see it: |
| 59 | + |
| 60 | +```ts |
| 61 | +import { WizardStore } from '@ui/tui/store'; |
| 62 | +import { InkUI } from '@ui/tui/ink-ui'; |
| 63 | +import { setUI } from '@ui/index'; |
| 64 | +import { buildSession } from '@lib/wizard-session'; |
| 65 | +import { Program } from '@lib/programs/program-registry'; |
| 66 | +import { WizardCiDriver } from '@e2e-harness/wizard-ci-driver'; |
| 67 | +import { WizardRecorder } from '@e2e-harness/recorder'; |
| 68 | +import { renderFrame } from '@e2e-harness/replay'; |
| 69 | + |
| 70 | +async function main() { |
| 71 | + const store = new WizardStore(Program.PostHogIntegration); |
| 72 | + setUI(new InkUI(store)); |
| 73 | + store.session = buildSession({ installDir: '/tmp/app-copy', ci: true }); |
| 74 | + await store.runReadyHooks(); // real framework detection |
| 75 | + |
| 76 | + const rec = new WizardRecorder(store, { program: 'posthog-integration' }); |
| 77 | + rec.start(); |
| 78 | + const driver = new WizardCiDriver(store); |
| 79 | + |
| 80 | + // LOOK: where am I, and what can I commit? |
| 81 | + console.log( |
| 82 | + driver.readState().currentScreen, |
| 83 | + driver.listActions().map((a) => a.id), |
| 84 | + ); |
| 85 | + |
| 86 | + // ACT: name an action from list_actions (not a keystroke) |
| 87 | + driver.performAction('confirm_setup'); |
| 88 | + console.log( |
| 89 | + driver.readState().currentScreen, |
| 90 | + driver.listActions().map((a) => a.id), |
| 91 | + ); |
| 92 | + // …repeat: read_state → decide → perform_action… |
| 93 | + |
| 94 | + // VIEW: render every captured frame as the real TUI (ANSI) so you can see it |
| 95 | + rec.stop(); |
| 96 | + for (const f of rec.getRecording().frames) { |
| 97 | + console.log( |
| 98 | + `\n=== ${f.screen} ===\n` + renderFrame(f, Program.PostHogIntegration), |
| 99 | + ); |
| 100 | + } |
| 101 | +} |
| 102 | +main(); |
| 103 | +``` |
| 104 | + |
| 105 | +`auth` and `run` are _external_ steps (the runner sets credentials, the agent |
| 106 | +sets run state) — for those, drive the full `--e2e` path in §1, which runs the |
| 107 | +real agent and records it for you. |
| 108 | + |
| 109 | +## 3. Snapshot for yourself to view |
| 110 | + |
| 111 | +Two ways to "see" a screen as an agent: |
| 112 | + |
| 113 | +- **`renderFrame(frame, program)`** → the real Ink screen as an ANSI string you |
| 114 | + can print and read (used above). Strip ANSI if you want plain text. |
| 115 | +- **The recording JSON** — each frame already carries `screen`, `tasks`, |
| 116 | + `statusMessages`, and the (secret-redacted) `session`, so you can assert on |
| 117 | + what happened without rendering. |
| 118 | + |
| 119 | +The access token is redacted in both `read_state` and recordings, so anything |
| 120 | +you capture is safe to share. |
| 121 | + |
| 122 | +## Key facts |
| 123 | + |
| 124 | +- **State → screen.** You never navigate; you flip a session flag (via an |
| 125 | + action's store setter) and the router re-derives the active screen. Name |
| 126 | + actions, not keys. |
| 127 | +- **Secrets stay out.** `read_state` reduces credentials to `hasCredentials` + |
| 128 | + `projectId`; the token is never serialized. |
| 129 | +- **None of this ships.** The harness lives in `e2e-harness/`, out of `src/`, |
| 130 | + and is absent from the production bundle. |
0 commit comments