Skip to content

chore(harness): WIP drive the e2e routes over the control socket - #1278

Closed
gewenyu99 wants to merge 10 commits into
posthog/split-p4-controlfrom
posthog/split-p5-harness
Closed

gewenyu99 wants to merge 10 commits into
posthog/split-p4-controlfrom
posthog/split-p5-harness

Conversation

@gewenyu99

Copy link
Copy Markdown
Collaborator

Drive the snapshot route and the wizard-ci MCP server over the control socket.

Both spawn the real binary with --ci --control-socket in a PTY; the in-process host, store driver, and action registry are gone. Harness tests run against the store's ControlDriver with unchanged goldens, two process specs cover each surface, and a real sweep of eight programs matched the old route on every transition frame. Stacked on #1277.


Created with PostHog Desktop

The snapshot route and the wizard-ci MCP server spawn the real binary with
`--ci --control-socket` in a PTY and drive it through the control API: the
run is released with POST /run, state is long polled, and every commit goes
through POST /actions. The in-process host, its store driver, and the action
registry are gone; the harness keeps the launcher, the detection picks, the
profiles, and the result payload. Harness tests run against the store's
ControlDriver and per-flow actions with unchanged goldens, and two process
specs exercise each surface end to end. Docs describe the API and the
launcher; the workbench environment contract is unchanged.

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
self-driving, upload-source-maps, doctor, and skill branched on --ci
themselves and never saw the control socket. They dispatch through
dispatchProgram now, so --ci with --control-socket drives the real TUI for
every command, and self-driving refuses --ci only when no parent is attached.

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
@github-actions

Copy link
Copy Markdown

🧙 Wizard CI

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

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

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

Test an individual app:

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

Test against a Context Mill branch:

  • /wizard-ci all context-mill:my-branch

Add context-mill:<branch> to any command above to pin the Context Mill branch. It defaults to main.

Results will be posted here when complete.

A failed run parks the TUI on the handoff screen until someone chooses. The
fixed profile now exits from it and ends the walk, so the result payload
carries the abort reason instead of the run waiting out its timeout.

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
@gewenyu99

gewenyu99 commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Harness routes: why each piece exists

Top to bottom is the direction of a run. The harness never touches the TUI's input: it commits over the socket and reads the frame back through the PTY.

flowchart TB
  classDef wave_entry fill:#fff8e6,stroke:#d9a400,stroke-width:2px,color:#d9a400;
  classDef card_entry fill:#fff1c7,stroke:#d9a400,stroke-width:1px,color:#1d1d1f;
  classDef wave_cli fill:#fff1e8,stroke:#e8632c,stroke-width:2px,color:#e8632c;
  classDef card_cli fill:#ffe1cc,stroke:#e8632c,stroke-width:1px,color:#1d1d1f;
  classDef wave_tui fill:#e9f0ff,stroke:#2f6fed,stroke-width:2px,color:#2f6fed;
  classDef card_tui fill:#d6e4ff,stroke:#2f6fed,stroke-width:1px,color:#1d1d1f;
  classDef wave_agent fill:#f1e9ff,stroke:#7a3fe0,stroke-width:2px,color:#7a3fe0;
  classDef card_agent fill:#e6d8ff,stroke:#7a3fe0,stroke-width:1px,color:#1d1d1f;
  classDef wave_store fill:#e8f8ee,stroke:#1f9d55,stroke-width:2px,color:#1f9d55;
  classDef card_store fill:#d4f2df,stroke:#1f9d55,stroke-width:1px,color:#1d1d1f;
  classDef wave_control fill:#e4f5f3,stroke:#148f7a,stroke-width:2px,color:#148f7a;
  classDef card_control fill:#cfeeea,stroke:#148f7a,stroke-width:1px,color:#1d1d1f;
  classDef wave_harness fill:#f2f2f2,stroke:#6e6e73,stroke-width:2px,color:#6e6e73;
  classDef card_harness fill:#e6e6e6,stroke:#6e6e73,stroke-width:1px,color:#1d1d1f;
  classDef wave_guard fill:#fbe9e7,stroke:#c0392b,stroke-width:2px,color:#c0392b;
  classDef card_guard fill:#f8d7d2,stroke:#c0392b,stroke-width:1px,color:#1d1d1f;
  subgraph W1["ROUTES: prove the real TUI runs end to end without a keyboard"]
    direction LR
    W1C1("<b>tui-snapshots.no-jest.ts</b><br/>the real TUI in a PTY; a fixed profile decides<br/>each screen; frames become the evidence")
    W1C2("<b>wizard-ci-mcp.no-jest.ts</b><br/>a coding agent decides each screen through<br/>MCP tools; same socket, same TUI")
    W1C3("<b>controlled-headless-smoke.no-jest.ts</b><br/>the published surface: detect, independent<br/>runs, the ledger, a clean exit")
    W1C4("<b>control-socket-*.test.ts</b><br/>the real binary on both surfaces in CI,<br/>no credentials, no agent run")
    W1C1 ~~~ W1C2 ~~~ W1C3 ~~~ W1C4
  end
  class W1 wave_harness
  class W1C1,W1C2,W1C3,W1C4 card_harness
  subgraph W2["e2e-harness: the parent's side, pure where it decides, real where it launches"]
    direction LR
    W2C1("<b>launch.ts</b><br/>starts the real binary from each program's<br/>own command; waits for the server to answer")
    W2C2("<b>e2e-profile.ts + profiles.ts</b><br/>pure policy: screen in, commit out;<br/>no env, no store, no socket")
    W2C3("<b>picks.ts</b><br/>computes what a picker screen would ask<br/>a human, then commits it as an action")
    W2C4("<b>e2e-result.ts</b><br/>records question ids and prompts,<br/>never an answer value")
    W2C5("<b>tui-capture.ts</b><br/>reads the frame back through the PTY;<br/>never types into it")
    W2C1 ~~~ W2C2 ~~~ W2C3 ~~~ W2C4 ~~~ W2C5
  end
  class W2 wave_harness
  class W2C1,W2C2,W2C3,W2C4,W2C5 card_harness
  subgraph W3["THE WIZARD PROCESS: unchanged code, driven from outside"]
    direction LR
    W3C1("<b>src/cli</b><br/>picks the surface: controlled TUI<br/>or headless, from the same flags")
    W3C2("<b>src/store/control</b><br/>the store on a socket (P4)")
    W3C3("<b>src/store  FlowStore + RunStore</b><br/>one flow, one store per run; the TUI and<br/>the socket read and commit the same one")
    W3C4("<b>src/tui</b><br/>the real Ink render, reacting to<br/>every commit, captured as frames")
    W3C1 ~~~ W3C2 ~~~ W3C3 ~~~ W3C4
  end
  class W3 wave_store
  class W3C1,W3C2,W3C3,W3C4 card_store
  subgraph W4["EVIDENCE: what a reviewer or the workbench reads"]
    direction LR
    W4C1("<b>NN-<screen>.ans</b><br/>one frame per screen change or task update,<br/>colors kept")
    W4C2("<b>E2E_RESULT_JSON</b><br/>run phase, screen path, asks, notices, tasks,<br/>abort reason: what happened, never how it was answered")
    W4C3("<b>GET /runs</b><br/>each independent run and the state it ended in")
    W4C1 ~~~ W4C2 ~~~ W4C3
  end
  class W4 wave_guard
  class W4C1,W4C2,W4C3 card_guard
  W1 -->|"Launch = buildLaunch(...)  the key in env only, outer agent credentials stripped<br/>ControlClient  long-poll GET /state · POST /actions · POST /run · POST /credentials"| W2
  W2 -->|"every decision enters as one control action; every frame leaves through the PTY"| W3
  W3 -->|"frames · result payload · MCP replies · run ledger"| W4
Loading

Interfaces, one sheet each

┌─ Launch ─────────────────────────────────────────────────────────────────────────────────────┐
│ e2e-harness/launch.ts · how every route starts the wizard                                    │
│                                                                                              │
│ LaunchOptions { programId, appDir, socketPath, projectId, region?, apiKey?, surface?,        │
│   e2eAsk?,                                                                                   │
│                 integrate?, harness?, sequence?, model?, taskStreamLog?, bin?, env? } →      │
│   Launch { cmd, args, env }                                                                  │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ · launchWords(programId) reads the program's own command; two overrides (default flow: none, │
│   audit: audit all)                                                                          │
│ · the API key travels only in env (POSTHOG_WIZARD_API_KEY), never in argv;                   │
│   CLAUDE*/ANTHROPIC* stripped                                                                │
│ · surface headless swaps --ci for the published headless flag; waitForSocket waits for GET   │
│   /health                                                                                    │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
┌─ E2eObservedState · E2eDecision ─────────────────────────────────────────────────────────────┐
│ e2e-harness/e2e-profile.ts · the scripted walk policy                                        │
│                                                                                              │
│ E2eObservedState = Pick<ControlState, "currentScreen" | "setupQuestions">                    │
│                  & { session: Pick<ControlSession, "pendingQuestion" | "taskNotice"> }       │
│ E2eDecision { action?: { id, params? }, wait?, done?, skillsPolicy?, report? }               │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ · decideE2eAction(state, profile) is pure; profiles come from each program's test/e2e.json   │
│ · mint-failure (any failed run) → dismiss_outro, done: the route exits from the handoff      │
│   screen                                                                                     │
│ · E2E_DRIVABLE_SCREENS is derived by a test from the function; keyboard parity is asserted   │
│   per pair                                                                                   │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
┌─ E2eRunRecorder · buildE2eResult ────────────────────────────────────────────────────────────┐
│ e2e-harness/e2e-result.ts · E2E_RESULT_JSON                                                  │
│                                                                                              │
│ observe(session: { pendingQuestion, taskNotice })   applyReport(report)                      │
│ buildE2eResult({ base, recorder, session: { frameworkContext, outroData }, tasks, reportFile │
│   })                                                                                         │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ · edge-triggered: each ask batch and notice is recorded once, however many commits it        │
│   survives                                                                                   │
│ · question ids and prompts only; no answer value ever enters the payload (the workbench      │
│   scans for leaks)                                                                           │
│ · reportFile reads a regular file inside appDir only, never through a symlink                │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
┌─ wizard-ci MCP tools ────────────────────────────────────────────────────────────────────────┐
│ scripts/wizard-ci-mcp.no-jest.ts · the agent route                                           │
│                                                                                              │
│ open_app { appDir, projectId, keyFile?, apiKey?, region? }   read_state {}   render_screen   │
│   {}                                                                                         │
│ perform_action { action, params? }                          run_agent {}  (POST /run)        │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ · read_state = GET /state + integration (idle | running | done | failed) and                 │
│   integrationError,                                                                          │
│   derived in e2e-harness/run-status.ts from session.runPhase, runRequested, outroData        │
│ · perform_action returns the bare state; legal ids come from state.actions                   │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
┌─ Picker actions ─────────────────────────────────────────────────────────────────────────────┐
│ program-declared commits, values computed client side in picks.ts                            │
│                                                                                              │
│ pick_integration_target { path, integration }   on self-driving-integration-detect,          │
│   error-tracking-detect                                                                      │
│ pick_source_maps_project { variant, path }      on source-maps-detect                        │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ · the program declares the commit as FlowStep.controlActions; the harness computes the value │
│ · the MCP route offers the same actions and lets the agent supply the values                 │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

How to run this step

# Snapshot route: spawns the real binary in a PTY, drives the fixed profile over the socket
PROGRAM=posthog-integration E2E_ASK=true \
POSTHOG_KEY_FILE=/path/to/phx-key.txt WIZARD_CI_GATEWAY_TOKEN_FILE=/path/to/token.txt \
PROJECT_ID=<id> POSTHOG_REGION=us APP_DIR=/tmp/app \
SNAP_OUT=/tmp/snaps E2E_RESULT_JSON=/tmp/snaps/result.json \
npx tsx scripts/tui-snapshots.no-jest.ts

# MCP route: the wizard-ci server from .mcp.json, driven by the explore script
APP_DIR=/tmp/app PROJECT_ID=<id> POSTHOG_KEY_FILE=/path/to/phx-key.txt npx tsx scripts/wizard-ci-explore.no-jest.ts

# Process specs: the real binary on both surfaces, no credentials, no agent run
pnpm test:harness                # WIZARD_PTY_TESTS=0 skips the PTY spec

The workbench environment contract for tui-snapshots is unchanged: pnpm wizard-ci-snapshots <app> in wizard-workbench works as before. Full route table and env reference: e2e-harness/ARCHITECTURE.md.

Evidence: basic integration through the snapshot path (socket route)

Run on 2026-09-19 against a throwaway copy of express-todo, project 228144, issued gateway bearer, no mint. The route spawns the real binary with --ci --control-socket in a PTY and drives it over the socket.

PROGRAM=posthog-integration E2E_ASK=true WIZARD_CI_GATEWAY_TOKEN_FILE=… POSTHOG_KEY_FILE=… \
PROJECT_ID=228144 POSTHOG_REGION=us APP_DIR=/tmp/wizard-e2e-express-todo \
SNAP_OUT=/tmp/wz-snaps-p5 E2E_RESULT_JSON=/tmp/wz-snaps-p5/result.json \
npx tsx scripts/tui-snapshots.no-jest.ts

E2E_RESULT_JSON:

{
  "runPhase": "completed",
  "screenPath": [
    "intro",
    "auth",
    "run",
    "outro",
    "mcp",
    "slack-connect",
    "keep-skills"
  ],
  "hasPosthogDep": true,
  "newDeps": [
    "posthog-node"
  ],
  "skillsComplete": false,
  "unansweredAsks": 0,
  "refusedAsks": 0,
  "abort": null,
  "tasks": [
    "completed:Add PostHog SDK",
    "completed:Initialize PostHog",
    "skipped:Track user identity",
    "completed:Capture unhandled errors",
    "completed:Instrument key events",
    "completed:Review integration",
    "completed:Create product dashboard",
    "completed:Report integration"
  ]
}

Against the main baseline captured by the old host: mcp, slack-connect, keep-skills frames identical; outro identical apart from the created dashboard and notebook ids; intro gains the Program ✔ posthog-integration row because runWizard sets session.programLabel, which the old host never did. screenPath now records the tail after the outro (the old host's final write was skipped by its own guard).

Frames (16)
01-intro.ans
02-auth.ans
03-run.ans
04-run.ans
05-run.ans
06-run.ans
07-run.ans
08-run.ans
09-run.ans
10-run.ans
11-run.ans
12-run.ans
13-outro.ans
14-mcp.ans
15-slack-connect.ans
16-keep-skills.ans
Intro frame (ANSI stripped)
                               PostHog Wizard v2.76.0                                                                    Feedback: wizard@posthog.com
                                                                               ███ PostHog Wizard 🦔
                                                               We'll use AI to analyze your project and complete work.
                                                                   Review what data is shared in "Privacy & data."
                                                                         .env* values stay on your machine.
                                                                    Let's do two hours of work in eight minutes.
                                                                        Directory ✔ /wizard-e2e-express-todo
                                                                        Framework ✔ Node.js (detected)
                                                                        Program   ✔ posthog-integration
                                                                              ▸ Continue
                                                                                Change framework
                                                                                More info
                                                                                Privacy & data
                                                                                Cancel
                                ↑↓ navigate  enter select
Outro frame (ANSI stripped)
                               PostHog Wizard v2.76.0                                                                    Feedback: wizard@posthog.com
                               ✔ PostHog set up: 7/7 steps completed (1 skipped as not required).
                               Dashboard:
                               https://us.posthog.com/project/2/dashboard/2113210?utm_source=wizard&utm_medium=cli&utm_content=outro-dashboard
                               Notebook:
                               https://us.posthog.com/project/2/notebooks/ISFtF9IL?utm_source=wizard&utm_medium=cli&utm_content=outro-notebook
                               Learn more: https://posthog.com/docs/ai-engineering/ai-wizard?utm_source=wizard&utm_medium=cli&utm_content=outro-docs
                               Note: This wizard uses an LLM agent to analyze and modify your project. Please review the changes made.
                               How did this work for you? Drop us a line: wizard@posthog.com
                               Press any key to continue

Re-run after the control state became a store mirror (session.runPhase, session.pendingQuestion, session.taskNotice, redacted session.frameworkContext): same route, same fixture, 16 frames, runPhase: completed, and an identical screenPath (intro, auth, run, outro, mcp, slack-connect, keep-skills). The controlled TUI probe and the headless transcript for the same change are on #1277.

Evidence: MCP route (wizard-ci-mcp over the socket)

APP_DIR=/tmp/wz-explore-app PROJECT_ID=228144 POSTHOG_KEY_FILE=… npx tsx scripts/wizard-ci-explore.no-jest.ts
wizard-ci-mcp: proxy ready on stdio
open_app       → intro
confirm_setup  → health-check
read_state     → health-check

=== render_screen (the REAL TUI) ===
                               PostHog Wizard v2.76.0                                                                    Feedback: wizard@posthog.com 























                                                                            ⠙ Checking service status...
exit 0

Evidence: eight programs, old host at the branch base vs this route

Per program: one baseline run through the old host (/tmp/wz-base-wt, commit b300b401) and one run through the socket route; rc is the exit code, frames the count written.

metrics base app=basic-integration/javascript-node/express-todo rc=0 secs=214 frames=13 01:25:07
metrics p5 app=basic-integration/javascript-node/express-todo rc=0 secs=189 frames=9 01:28:16
audit base app=audit/posthog-demo-3000 rc=142 secs=1500 frames=9 01:53:17
audit p5 app=audit/posthog-demo-3000 rc=0 secs=568 frames=23 02:02:46
replay-vision base app=replay-vision/react-vite/court-booking rc=0 secs=357 frames=25 02:08:43
replay-vision p5 app=replay-vision/react-vite/court-booking rc=0 secs=322 frames=12 02:14:05
ai-observability base app=ai-observability/openai/node-weather rc=0 secs=212 frames=24 02:17:37
ai-observability p5 app=ai-observability/openai/node-weather rc=0 secs=330 frames=24 02:23:07
error-tracking base app=basic-integration/next-js/15-app-router-saas rc=0 secs=459 frames=28 02:30:47
error-tracking p5 app=basic-integration/next-js/15-app-router-saas rc=0 secs=398 frames=19 02:37:27
error-tracking-upload-source-maps base app=error-tracking-upload-source-maps/node-webpack rc=0 secs=225 frames=38 02:41:13
error-tracking-upload-source-maps p5 app=error-tracking-upload-source-maps/node-webpack rc=1 secs=1 frames=0 02:41:14
warehouse-source base app=basic-integration/javascript-node/express-todo rc=0 secs=82 frames=16 02:42:36
warehouse-source p5 app=basic-integration/javascript-node/express-todo rc=0 secs=51 frames=11 02:43:27
self-driving base app=self-driving/sveltekit/habit-tracker rc=0 secs=694 frames=77 02:55:01
self-driving p5 app=self-driving/sveltekit/habit-tracker rc=1 secs=120 frames=0 02:57:02
DONE 02:57:02
error-tracking-upload-source-maps p5 app=error-tracking-upload-source-maps/node-webpack rc=0 secs=160 frames=35 11:45:54
self-driving p5 app=self-driving/sveltekit/habit-tracker rc=0 secs=607 frames=42 11:56:02
audit base app=audit/posthog-demo-3000 rc=0 secs=403 frames=16 12:02:46
DONE 12:02:46

Compared after stripping ANSI and masking the version header, the temp dir name, and MCP-created ids: every transition frame identical for all eight programs (intro, detect, wizard-ask, outro, mcp, slack-connect, keep-skills). Remaining diffs are agent output (task labels, report line numbers) and run-screen progress frame counts. The first-pass failures of self-driving and upload-source-maps on this route were the command-level --ci handling fixed in this PR; the first audit baseline parked on the failure handoff after a warlock abort in the fixture, which the profile now exits from.

Process specs in the harness project spawn the real binary on both surfaces: e2e-harness/__tests__/control-socket-headless.test.ts, control-socket-tui.test.ts.

@gewenyu99

Copy link
Copy Markdown
Collaborator Author

Review of the stack: what changed, what is left, what to check

Five independent reviews ran over main..posthog/split-p5-harness (architecture, code quality, tests, docs and goal audit, idiom). This lists what they found, what the last two commits fixed, and every boundary exception for you to check.

Fixed in this stack

Finding Fix
POST /detect, /credentials, /shutdown rewrote the session or exited while a run was in flight 409 RunInFlightError while the ledger has an active run or session.runPhase is running
Headless /shutdown answered ok and the runner returned while the agent kept running Same 409; the run has to end first
Long-poll timers and store subscriptions outlived close() (up to 10 min) Polls take an AbortSignal; close() aborts them, lets them answer, sweeps idle connections, then closes
claimSocketPath unlinked any non-socket path (--control-socket ./package.json deleted the file) lstat().isSocket() or refuse
The 413 never reached the client (req.destroy() before the reply); the test masked it with .catch Pause the body, send 413 with connection: close, destroy after; the test asserts 413 over the cap and 200 at it
decodeURIComponent on a bad action id was a 500 400
Socket file leaked on Ctrl-C before the stream existed (close() unlinked asynchronously) close() unlinks synchronously first
ledger.start ran before the hook; a synchronous throw left the record running forever; no trailing catch Promise.resolve().then(startRun) chain with a final .catch
Ledger recorded the raw relative installDir; the hook resolved a different one The server resolves once (resolveInstallDir in store/shared/paths.ts) and both use it
ciPreRun wrote to a forked session object after authenticate committed; direct writes (typescript, scoped installDir) were lost detect hands ciPreRun a copy and republishes its changed keys over the live session
The headless PostHogDestination read credentials from the launch object, never the store: the stream never pushed Reads the live store's credentials
Independent runs streamed under the launch program's skill id TaskStreamPush takes skillId; the runner passes the run's
Neither headless success path nor /runs called runCleanups(): .claude/settings.json stayed modified finally { runCleanups() } in the hook
WIZARD_CI_GATEWAY_TOKEN_FILE triggered the CI gateway in published builds, which throws Dev builds only; skipped for programs without an agent run
POST /credentials with no API key fell into browser OAuth 400
hooks.shutdown() rejection was unhandled Logged
Action params were coerced (outcome: 'maybe' became skipped, connected: 'false' became true, answers unchecked) optionalBoolean / optionalOneOf / requireRecord; bad input is a 400
confirm_setup skipped the scan-sharing grant Enter performs on the default intro; keyboard-equivalence recorded six unequal pairs and could never fail The default intro's action follows the toggle (share param); the test asserts equality, with the next screen's mount effects named per pair
Redaction matched monkey, keyboard as secrets Word-based isSecretKey (apiKey, upload-api-key, ACCESS_TOKEN yes; monkey no)
outroData.errorDetail projected raw sanitizeErrorDetail
PROGRAM_COMMANDS restated every program's command launchWords(programId) reads ProgramConfig.command; two overrides
waitForSocket returned on a stale socket file Probes GET /health
The smoke script re-copied waitForSocket, the env strip, the key read, and used a third argv/credential contract Uses buildLaunch({ surface: 'headless' }), readApiKey, and the APP_DIR / PROJECT_ID / POSTHOG_KEY_FILE contract
tui-snapshots treated every client error as "the wizard exited" Rethrows ControlClientError
E2E_DRIVABLE_SCREENS was stale and self-referential Listed from decideE2eAction; a test derives the set and compares
No tests for picks.ts, withRunStatus, resetRunState's full atom set, skillId precedence, the ciPreRun branch, the error-outro branch, the ledger order, long-poll edges, the session key set Added
Dead fixtures (control-state-baseline.json, control-frame-*.txt) Deleted
Route table in e2e-harness/ARCHITECTURE.md untied from the server ROUTES exported; a harness test compares the table
Docs: .state.run, bin.ts registration, router.ts as resolver, agent-runner.ts, InkUI, missing @store/control in the alias lists, src/lib paths, .txt frames in tui-replay, broken src/frameworks/ link, stale @lib/utils/ comments Corrected
Tests imported from 'vitest' against the repo's globals convention Dropped in the stack's tests

Left as is, for you to decide

  • bootstrap.ts:80 writes session.skillId directly on the run's own session copy (pre-existing). The agent README names it as the one exception. A setSkillId on WizardUI would close it; it touches every renderer.
  • Controlled TUI runs ci: true without e2eAsk, so wizard_ask is off unless --e2e-ask is passed, while the API advertises answer_question. The plan deferred a uniform ask policy; the harness passes the flag.
  • The generic action table knows program screens (audit-outro, source-maps-outro, mcp-add, doctor-report, …). Moving each to its program's controlActions is the pattern; not done here.
  • store.ts imports programs/posthog-integration/detect for the warehouse-sources report on completeSetup (pre-existing product knowledge in the state machinery).
  • control/types.ts and control/params.ts are imported by state/flow.ts and programs/shared/control-actions.ts, so those two small modules ship in every chunk; only server.ts is lazy.
  • start-tui.ts installs StoreUI itself; the design has the cli install WizardUI.
  • Session-building, stream construction, gateway configuration, and teardown are written three times across run-wizard.ts, run-non-interactive.ts, and control-hooks.ts. The controlled paths already diverge on controlSocket in the session and on skillId precedence. A runners/session.ts helper is the fix; left out of this stack.
  • The world-shared /tmp/posthog-wizard.log and the agent middleware recomputing the log path (pre-existing).
  • 'detectError' is a literal shared by four programs, three screens, and the headless runner.
  • The architecture test scans src/** and bin.ts only; e2e-harness/** and scripts/** deep imports are unenforced (listed below).

Exceptions to check in the PR

Cross-surface edges that exist on purpose:

  • src/cli/runners/run-wizard.ts, src/cli/runners/run-non-interactive.ts → await import('@store/control') (the only importers)
  • src/cli/** → dynamic @agent, @agent/types (RunAgent), @tui/console (LoggingUI, HeadlessUI)
  • bin.ts → e2e-tests/mocks/server.ts under NODE_ENV=test (TEST_ONLY_EDGES)
  • src/tui/start-tui.ts → setUI(new StoreUI(store))
  • src/tui/services/mcp-suggested-prompts-services.ts → getMcpPromptRunner (the TUI triggers agent work through the store seam)
  • src/agent/runner/index.ts → @store/programs startAuditLedgerWatcher; src/agent/tools/mcp.ts, harness/pi/tools.ts → AUDIT_CHECKS_FILE; runner/shared/bootstrap.ts → maybeStampAiSdkDetected; orchestrator-runner.ts → FRAMEWORK_REGISTRY
  • Shared by agent and tui from @store: analytics, logToFile, getLogFilePath, WIZARD_LOG_FILE, wizardAbort, ErrorCodes, OutroKind, POSTHOG_DOCS_URL, FRAMEWORK_REGISTRY, SERVICE_LABELS, SIGNUP_WIZARD_READINESS_CONFIG, ServiceHealthStatus, WizardReadiness, getBlockingServiceKeys, downloadSkill, fetchSkillMenu, getSkillsBaseUrl (the snapshot in boundary-contracts.test.ts.snap is the full list). Two screens do network work through these: HealthCheckScreen (wizardAbort, readiness config) and SkillSourceInfo (fetchSkillMenu).
  • Direct session.x = writes inside the store: programs/{posthog-integration,error-tracking,replay-vision}/index.ts (ciPreRun), events-audit/index.ts, posthog-integration/detect.ts, detection/project-scope.ts, services/authenticate.ts (also commits through getUI().setCredentials), state/store.ts:715
  • src/env.ts reads process.argv for RUN_SURFACE; bin.ts sets the undici dispatcher
  • Harness and scripts deep imports (unscanned by the architecture test): e2e-harness/e2e-profile.ts → @tui/router, @store/session/wizard-session; e2e-result.ts → @store/session/wizard-session, @store/programs/warehouse-source/detect, @store/services/warehouse-sources/types; profiles.ts → @store/programs/program-registry, @store/programs/*/test/e2e.json; scripts/check-screens.tsx → @tui/screens/*, @tui/primitives/ProgressList; scripts/warlock-smoke-test.ts → @store/security/*; scripts/mcp-install-smoke-test.ts → @store/shared/headless-mode; every harness test → store and tui internals

# Conflicts:
#	scripts/README.md
#	src/store/control/__tests__/state.test.ts
#	src/store/control/state.ts
#	src/store/control/types.ts
Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
The harness architecture doc gains the commands for the snapshot route, the
MCP route, the controlled headless smoke, and the process specs; AGENTS.md
points at it. The store boundary constant matches this tree's usage.

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
The routes, the profile, the recorder, and the process specs read
session.runPhase, session.pendingQuestion, session.taskNotice, and the
redacted session.frameworkContext where the control state now mirrors the
store; the MCP proxy derives its integration status from the run phase.

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
…rect the docs

launchWords reads each program's command instead of a second table;
waitForSocket waits for the server to answer; the headless smoke script
shares buildLaunch, readApiKey, and the APP_DIR/PROJECT_ID/POSTHOG_KEY_FILE
contract; the snapshot route rethrows client errors instead of treating
them as an exit; the MCP run status lives in run-status.ts with a test.
Keyboard equivalence asserts equality per pair and names the next screen's
mount effects; E2E_DRIVABLE_SCREENS is derived from decideE2eAction; picks,
the route table, and the doctor dispatch gain tests; dead fixtures go.
Docs drop the stale aliases, the router-as-resolver claim, the bin.ts
registration, InkUI, and the .txt frames, and list @store/control.

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
@gewenyu99

Copy link
Copy Markdown
Collaborator Author

Snapshot frames: every profiled program on this branch

Each program ran once through scripts/tui-snapshots.no-jest.ts on a throwaway copy of its workbench fixture (project 228144, issued gateway bearer, no mint), the same route the workbench CI runs. One frame per screen plus the last run frame is shown; every captured frame is on the branch posthog/split-snapshots-assets. Screen paths match the pre-split baseline for every program.

posthog-integration: 16 frames captured, 8 shown · runPhase completed

Screen path: intro → auth → run → outro → mcp → slack-connect → keep-skills

01-intro

posthog-integration 01-intro

02-auth

posthog-integration 02-auth

03-run

posthog-integration 03-run

12-run

posthog-integration 12-run

13-outro

posthog-integration 13-outro

14-mcp

posthog-integration 14-mcp

15-slack-connect

posthog-integration 15-slack-connect

16-keep-skills

posthog-integration 16-keep-skills

self-driving: 41 frames captured, 10 shown · runPhase completed

Screen path: self-driving-intro → self-driving-integration-check → auth → self-driving-integration-detect → run → self-driving-handoff → self-driving-github → run → wizard-ask → run → wizard-ask → run → outro

01-self-driving-intro

self-driving 01-self-driving-intro

02-self-driving-integration-check

self-driving 02-self-driving-integration-check

03-auth

self-driving 03-auth

04-self-driving-integration-detect

self-driving 04-self-driving-integration-detect

05-run

self-driving 05-run

21-self-driving-handoff

self-driving 21-self-driving-handoff

22-self-driving-github

self-driving 22-self-driving-github

32-wizard-ask

self-driving 32-wizard-ask

40-run

self-driving 40-run

41-outro

self-driving 41-outro

metrics: 9 frames captured, 6 shown · runPhase completed

Screen path: metrics-intro → auth → run → outro → keep-skills

01-metrics-intro

metrics 01-metrics-intro

02-auth

metrics 02-auth

03-run

metrics 03-run

07-run

metrics 07-run

08-outro

metrics 08-outro

09-keep-skills

metrics 09-keep-skills

replay-vision: 13 frames captured, 6 shown · runPhase completed

Screen path: agent-skill-intro → auth → run → outro → keep-skills

01-agent-skill-intro

replay-vision 01-agent-skill-intro

02-auth

replay-vision 02-auth

03-run

replay-vision 03-run

11-run

replay-vision 11-run

12-outro

replay-vision 12-outro

13-keep-skills

replay-vision 13-keep-skills

ai-observability: 14 frames captured, 6 shown · runPhase completed

Screen path: ai-observability-intro → auth → run → outro → keep-skills

01-ai-observability-intro

ai-observability 01-ai-observability-intro

02-auth

ai-observability 02-auth

03-run

ai-observability 03-run

12-run

ai-observability 12-run

13-outro

ai-observability 13-outro

14-keep-skills

ai-observability 14-keep-skills

error-tracking: 19 frames captured, 8 shown · runPhase completed

Screen path: error-tracking-intro → auth → error-tracking-detect → run → wizard-ask → run → wizard-ask → run → outro → keep-skills

01-error-tracking-intro

error-tracking 01-error-tracking-intro

02-auth

error-tracking 02-auth

03-error-tracking-detect

error-tracking 03-error-tracking-detect

04-run

error-tracking 04-run

07-wizard-ask

error-tracking 07-wizard-ask

17-run

error-tracking 17-run

18-outro

error-tracking 18-outro

19-keep-skills

error-tracking 19-keep-skills

error-tracking-upload-source-maps: 19 frames captured, 8 shown · runPhase completed

Screen path: source-maps-intro → auth → source-maps-detect → run → wizard-ask → run → wizard-ask → run → source-maps-outro → keep-skills

01-source-maps-intro

error-tracking-upload-source-maps 01-source-maps-intro

02-auth

error-tracking-upload-source-maps 02-auth

03-source-maps-detect

error-tracking-upload-source-maps 03-source-maps-detect

04-run

error-tracking-upload-source-maps 04-run

06-wizard-ask

error-tracking-upload-source-maps 06-wizard-ask

17-run

error-tracking-upload-source-maps 17-run

18-source-maps-outro

error-tracking-upload-source-maps 18-source-maps-outro

19-keep-skills

error-tracking-upload-source-maps 19-keep-skills

warehouse-source: 6 frames captured, 6 shown · runPhase completed

Screen path: warehouse-intro → auth → run → outro → keep-skills

01-warehouse-intro

warehouse-source 01-warehouse-intro

02-auth

warehouse-source 02-auth

03-run

warehouse-source 03-run

04-run

warehouse-source 04-run

05-outro

warehouse-source 05-outro

06-keep-skills

warehouse-source 06-keep-skills

audit: 14 frames captured, 6 shown · runPhase completed

Screen path: audit-intro → auth → audit-run → audit-outro → keep-skills

01-audit-intro

audit 01-audit-intro

02-auth

audit 02-auth

03-audit-run

audit 03-audit-run

12-audit-run

audit 12-audit-run

13-audit-outro

audit 13-audit-outro

14-keep-skills

audit 14-keep-skills

The bundle audit compares these with the manifests CI builds; the new modules
and the regrouped chunks belong in them. A failed comparison now uploads its
manifests and reports both files.

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
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.

1 participant