Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .claude/skills/wizard-development/references/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,50 @@ tool names or discovery. Context-mill supplies skills and flow/task prompts.
instrumentation. The linear sequence creates the benchmark pipeline; there is no
pipeline construction in the compatibility `agent-runner.ts`. Inspect the actual
consumer before extending instrumentation to another sequence or harness.

## Surfaces and the control API

The tree is three surfaces plus a composition root, each with a `README.md` that
lists what it owns and may import:

| Surface | Owns | Imports |
| ----------- | ------------------------------------------------------------------------------- | --------------------------------------------------- |
| `src/store` | state, session, `WizardUI`, programs as data, tool behavior, the control API | `@env` |
| `src/agent` | one independent agent run: switchboard, sequences, harnesses, gateway | `@env`, `@store`, `@store/types`, `@store/programs` |
| `src/tui` | Ink screens, `UiStore`, program presentation, Ink-free console renderers | `@env`, `@store`, `@store/types`, `@store/programs` |
| `src/cli` | argv, command tree, runners that sequence runs and pass context, `ControlHooks` | every surface, through its public entries only |

Cross-surface imports go through `@store`, `@store/types`, `@store/programs`,
`@agent`, `@agent/types`, `@tui`, `@tui/types`, and `@tui/console`.
`src/__tests__/architecture` enforces the matrix and the public-entry rule;
`tsc -b tsconfig.solution.json` mirrors it with project references.

`--control-socket <path>` serves an HTTP/1.1 API over a unix socket from
`src/store/control`: state with long polling, actions that call one store setter
each, credentials, run arming on the TUI surface, detection and independent runs
on the headless surface, shutdown. The store owns the server;
`src/cli/control-hooks.ts` does the work that needs the agent. Every
`POST /runs` is one independent run with a clean run state and its own task
stream session. Published builds keep the server for headless runs and refuse
the flag on the TUI, whose bundle never contains it. The full route table and
the run instructions live in
[`e2e-harness/ARCHITECTURE.md`](../../../../e2e-harness/ARCHITECTURE.md).

Run it:

```bash
# headless, every build; the key travels in the environment
POSTHOG_WIZARD_API_KEY=phx_... WIZARD_CI_GATEWAY_TOKEN_FILE=/path/to/token \
npx tsx bin.ts --headless-DONOTUSE-EXPERIMENTAL --control-socket /tmp/w/w.sock \
--project-id <id> --region us --install-dir /tmp/app
curl -s --unix-socket /tmp/w/w.sock -X POST -H 'content-type: application/json' -d '{}' http://localhost/detect
curl -s --unix-socket /tmp/w/w.sock -X POST -H 'content-type: application/json' \
-d '{"programId":"posthog-integration"}' http://localhost/runs
curl -s --unix-socket /tmp/w/w.sock 'http://localhost/state?wait=60000&since=0' | jq '.state.run, .state.tasks'
curl -s --unix-socket /tmp/w/w.sock http://localhost/runs
curl -s --unix-socket /tmp/w/w.sock -X POST http://localhost/shutdown

# the same sequence, scripted
POSTHOG_WIZARD_API_KEY=phx_... WIZARD_CI_GATEWAY_TOKEN_FILE=/path/to/token \
npx tsx scripts/controlled-headless-smoke.no-jest.ts --app /tmp/app --project-id <id> posthog-integration
```
8 changes: 8 additions & 0 deletions .github/workflows/surfaces.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,15 @@ jobs:
run:
npx tsx scripts/chunk-manifest.no-jest.ts dist >
chunk-manifest.ci.json
- name: Chunk layout matches the committed fixtures
run: |
status=0
diff scripts/__fixtures__/chunk-manifest.prod.json chunk-manifest.prod.json || status=1
diff scripts/__fixtures__/chunk-manifest.ci.json chunk-manifest.ci.json || status=1
exit $status
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# A failed comparison still uploads: the artifact is how the fixtures get refreshed.
if: always()
with:
name: chunk-manifests-${{ github.sha }}
path: chunk-manifest.*.json
54 changes: 54 additions & 0 deletions e2e-harness/__tests__/control-actions-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import {
ACTION_REGISTRY,
NO_ACTION_SCREENS as HARNESS_NO_ACTION,
} from '@e2e-harness/action-registry';
import { Interrupt } from '@store';
import { actionsFor, NO_ACTION_SCREENS } from '@store/control';
import { flowFor, PROGRAM_REGISTRY } from '@store/programs';

/** The store adds the pick the e2e host used to inject through raw setters. */
const STORE_ONLY: Record<string, string[]> = {
'self-driving-integration-detect': ['pick_integration_target'],
'error-tracking-detect': ['pick_integration_target'],
};

describe('control actions parity with the e2e action registry', () => {
it('offers every harness action on every flow, plus only the documented picks', () => {
const drift: string[] = [];
for (const config of PROGRAM_REGISTRY) {
const { flow } = flowFor(config.id);
const screens = new Set<string>([
...flow.steps.flatMap((s) => (s.screenId ? [s.screenId] : [])),
...Object.values(Interrupt),
]);
for (const screen of screens) {
const harness = (
(
ACTION_REGISTRY as Record<string, Array<{ id: string }> | undefined>
)[screen] ?? []
).map((a) => a.id);
const store = actionsFor(flow, screen).map((a) => a.id);
for (const id of harness) {
if (!store.includes(id))
drift.push(`${config.id}:${screen} lost ${id}`);
}
for (const id of store) {
if (
!harness.includes(id) &&
!(STORE_ONLY[screen] ?? []).includes(id)
) {
drift.push(`${config.id}:${screen} added ${id}`);
}
}
}
}
expect(drift).toEqual([]);
});

it('keeps every no-action screen the harness lists, minus the detect screens the picks now cover', () => {
for (const screen of NO_ACTION_SCREENS) {
expect(HARNESS_NO_ACTION.has(screen as never), screen).toBe(true);
}
});
});
33 changes: 17 additions & 16 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ real ink render) and is driven purely by store state manipulation; a PTY parent
([`e2e-harness/tui-capture.ts`](../e2e-harness/tui-capture.ts), node-pty +
`@xterm/headless`) captures the real rendered screen.

| Script | What it does | Needs |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **`tui-host.no-jest.ts`** | Real TUI host: `MODE=fixed` follows a profile; `MODE=serve` accepts socket commands. | `APP_DIR`, `PROJECT_ID`, key for a full run, `SNAP_CTRL`; run under a PTY, with `CONTROL_SOCK` in serve mode |
| **`tui-snapshots.no-jest.ts`** | Runs the fixed host and saves colored `SNAP_OUT/NN-<screen>.ans` frames, including within-screen progress. | `SNAP_OUT`, `APP_DIR`, `PROJECT_ID`, `POSTHOG_KEY_FILE` or `POSTHOG_PERSONAL_API_KEY` |
| **`wizard-ci-mcp.no-jest.ts`** | Stdio MCP server: `open_app`, `read_state`, `perform_action`, `render_screen`, `run_agent`. Screen output is plain text. | Spawns the host; `open_app` requires `appDir` and `projectId`, with optional `keyFile`, `apiKey`, `region` |
| **`chunk-manifest.no-jest.ts`** | Prints a structural manifest of `dist/`: per chunk, the source files it contains and the chunks it imports, hash suffixes stripped. Baselines live in `scripts/__fixtures__/chunk-manifest.{prod,ci}.json`. | A built `dist/` |
| **`wizard-ci-explore.no-jest.ts`** | `pnpm wizard-ci-explore`: opens an app, confirms setup, reads state, prints one frame, and exits. It does not run the agent. | `APP_DIR`, `PROJECT_ID`; optional `POSTHOG_KEY_FILE` |
| Script | What it does | Needs |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **`tui-host.no-jest.ts`** | Real TUI host: `MODE=fixed` follows a profile; `MODE=serve` accepts socket commands. | `APP_DIR`, `PROJECT_ID`, key for a full run, `SNAP_CTRL`; run under a PTY, with `CONTROL_SOCK` in serve mode |
| **`tui-snapshots.no-jest.ts`** | Runs the fixed host and saves colored `SNAP_OUT/NN-<screen>.ans` frames, including within-screen progress. | `SNAP_OUT`, `APP_DIR`, `PROJECT_ID`, `POSTHOG_KEY_FILE` or `POSTHOG_PERSONAL_API_KEY` |
| **`wizard-ci-mcp.no-jest.ts`** | Stdio MCP server: `open_app`, `read_state`, `perform_action`, `render_screen`, `run_agent`. Screen output is plain text. | Spawns the host; `open_app` requires `appDir` and `projectId`, with optional `keyFile`, `apiKey`, `region` |
| **`chunk-manifest.no-jest.ts`** | Prints a structural manifest of `dist/`, keyed by source-module group rather than chunk file name: which sources share a chunk and which groups each imports. Baselines live in `scripts/__fixtures__/chunk-manifest.{prod,ci}.json`; CI diffs a fresh build against them. | A built `dist/` |
| **`controlled-headless-smoke.no-jest.ts`** | Drives a headless run over its control socket: detect, one independent run per program named, the run ledger, shutdown. Prints every request and a redacted view of every response. | `POSTHOG_WIZARD_API_KEY`, `WIZARD_CI_GATEWAY_TOKEN_FILE`; `--app`, `--project-id`; optional `--region`, `--bin dist/bin.js` |
| **`wizard-ci-explore.no-jest.ts`** | `pnpm wizard-ci-explore`: opens an app, confirms setup, reads state, prints one frame, and exits. It does not run the agent. | `APP_DIR`, `PROJECT_ID`; optional `POSTHOG_KEY_FILE` |

> You usually don't call these directly — `pnpm wizard-ci-snapshots` (in
> [wizard-workbench](https://github.com/PostHog/wizard-workbench)) orchestrates
> the snapshot route; the MCP server is registered in this repo's `.mcp.json` and
> used via the `exploring-the-wizard` skill.
> the snapshot route; the MCP server is registered in this repo's `.mcp.json`
> and used via the `exploring-the-wizard` skill.

`PROGRAM`, `SNAP_HARNESS`, `SNAP_SEQUENCE`, `SNAP_MODEL`, and `E2E_ASK` are host
environment inputs, inherited from the launcher; they are not MCP tool
Expand All @@ -35,17 +36,17 @@ before choosing credentials or an EU project.
## Credentials for full agent runs

Full TUI host and snapshot runs require both the personal API key (or
`POSTHOG_KEY_FILE`) and `WIZARD_CI_GATEWAY_TOKEN_FILE`, plus `PROJECT_ID`.
For MCP runs, pass the personal key through `open_app` and set the gateway
token file path in the server environment before launch. Restart the server
after changing it; the gateway path is not an MCP tool argument.
Detection-only exploration does not need either secret. See
`POSTHOG_KEY_FILE`) and `WIZARD_CI_GATEWAY_TOKEN_FILE`, plus `PROJECT_ID`. For
MCP runs, pass the personal key through `open_app` and set the gateway token
file path in the server environment before launch. Restart the server after
changing it; the gateway path is not an MCP tool argument. Detection-only
exploration does not need either secret. See
[local credential setup](../docs/local-dev.md#credentials-for-local-ci-and-headless-runs).

## Background

The control plane lives in [`e2e-harness/`](../e2e-harness/) — out of `src/`, so
none of it ships in prod. `WizardCiDriver` (read/act over the store), the
screen→action registry, the e2e profiles, and `tui-capture` (real-TUI PTY
capture). See [`ARCHITECTURE.md`](../e2e-harness/ARCHITECTURE.md) for how the two
routes drive these (env strip, scoped project id, gotchas).
capture). See [`ARCHITECTURE.md`](../e2e-harness/ARCHITECTURE.md) for how the
two routes drive these (env strip, scoped project id, gotchas).
Loading
Loading