v1.0 to v2.0

112 added, 161 removed. Audit A to A.

---
name: wizard-development
description: >
- Design philosophy and architectural principles for the PostHog wizard.
- Read this before making any structural change — adding features, extending
- the runner, modifying the TUI, or introducing new abstractions. Covers
- why the architecture is shaped this way, how to evaluate whether a change
- fits, and how to extend the system into territory no existing skill covers.
- Complements the existing skills in this repo targeted at specific subsystems.
- compatibility: Designed for Claude Code or similar coding agents working on the PostHog wizard codebase.
+ Architectural guidance for changes to the PostHog Wizard: program and
+ framework boundaries, agent routing, gateway admission, security, and TUI
+ state. Read before structural changes, then load the relevant procedural
+ skill.
+ compatibility: Coding agents working in the PostHog Wizard repository.
metadata:
author: posthog
- version: "1.0"
+ version: '2.0'
---
- # Wizard Development
-
- This skill teaches the design discipline behind the wizard's architecture. The procedural skills tell you *how* to add things. This skill tells you *why* the system is shaped the way it is, so you can make good decisions when extending into territory no existing skill covers.
-
- Read this first. Then any procedural skill that's relevant for what you're building.
-
- ## The core discipline
-
- Every architectural decision in this codebase is guided by one question:
-
- > **Who will need to change this next, and what's the smallest thing they
- > should have to understand?**
-
- This question produces a specific, testable commitment: **product knowledge never enters infrastructure code.** The runner pipeline, the TUI store, the detection loop, the prompt assembler — these are machinery. They don't know what PostHog is. They don't know what a framework is. They execute a pipeline driven by configuration.
-
- The knowledge — what PostHog needs from a Next.js project, how to detect SvelteKit, what security rules to enforce — lives in configuration surfaces with typed boundaries:
-
- | Which domain has leverage | Where it lives | What they need to understand |
- |---|---|---|
- | Frameworks | `FrameworkConfig` (~70-120 lines) | Detection, env vars, prompts |
- | Docs | Skill markdown in context-mill | Workflow steps, example code |
- | Security | YARA-X rules in the [warlock](https://github.com/PostHog/warlock) sibling repo | Rule content (patterns, severity, category). Wizard wires the engine via hooks. |
- | Context | Step array in `programs/` | Gates, screens, predicates |
- | UI | Screen component + primitives | Ink, store getters, layout |
- | Agent development | Runner, store, detection loop, tools | The machinery itself |
-
- These tight boundaries increase the scope of who is able to contribute to the wizard, while lowering the costs of those contributions. PostHog has a variety of skillsets across many kinds of teams. The wizard should be as accessible to contributions as possible to as many contributor skill sets as possible.
-
- ## The five principles
-
- ### 1. Route to the narrowest configuration surface
-
- When a new concern arises, find the narrowest typed boundary where it belongs. Don't add it to the runner. Don't add it to the store. Don't create a new abstraction. Find the existing configuration surface — or, if truly needed, create a new one that follows the same pattern.
-
- **The test:** Can someone modify this concern without reading the agent runner?
-
- **What goes wrong when violated:** Product-specific logic gets inlined into infrastructure files. The runner grows from hundreds of lines to thousands. Changes in one product domain risk breaking another. Contributors need to understand the full system to make local changes. Recovery infrastructure proliferates to handle the unpredictable interactions between concerns that should have been separated.
-
- ### 2. Knowledge lives in markdown, not code
-
- The agent's behavior is shaped by skill content and commandments, not by conditional logic in the runner. Framework-specific integration knowledge is authored as static markdown artifacts — reviewed, tested, versioned, and delivered separately from the engine via the context-mill repo.
-
- This separation exists because PostHog's product surface is vast (analytics, session replay, feature flags, experiments, surveys, error tracking, LLM analytics, revenue analytics, data warehouse). Each domain has its own SDK patterns and its own update cadence. If integration knowledge lived in wizard code, every docs update would require a wizard release.
-
- **The test:** Can the docs team update integration instructions without a wizard code change? Is the prompt assembler under 70 lines?
-
- **What goes wrong when violated:** The prompt builder accumulates framework-specific conditionals. The commandments grow from terse rules into paragraph-length tutorials. Knowledge that changes at docs-team cadence gets locked to wizard release cadence. The system prompt bloats, consuming context window that the agent needs for actual work.
-
- ### 3. Prevent rather than recover
-
- Invest in making the agent go right rather than building recovery infrastructure for when it goes wrong. Prevention at the boundary is cheaper and more reliable than detection-and-repair after the fact.
-
- Three prevention layers:
-
- - **L0 — Commandments** (`commandments.ts`): Terse rules in the system prompt. Keep these short. If a rule needs a paragraph of explanation, it belongs in a skill reference file, not in the per-turn system prompt.
-
- - **L1 — canUseTool allowlist** (`agent-interface.ts`): Blocks dangerous bash commands before execution. The `wizard-tools` MCP fences `.env` files so the agent can't read them directly. Secrets the user supplies are handled separately by the **session secret vault** (`secret-vault.ts`): `wizard_ask` and `set_env_values` exchange the raw value for an opaque `secret:<uuid>` ref, so a user's API key reaches the `.env` file without ever entering the LLM conversation. See [ARCHITECTURE.md](references/ARCHITECTURE.md#secret-vault-keeping-values-out-of-the-model).
-
- - **L2 — warlock scanner** (rules live in [warlock](https://github.com/PostHog/warlock); wizard wires hooks in `yara-hooks.ts`): Real YARA-X rules running as pre/post tool-use hooks. Catches PII in capture calls, hardcoded keys, prompt injection, secret exfiltration, supply chain attacks, destructive operations. The scanner is engine-only — it returns matches with category/severity/action metadata; the wizard decides how to respond. Critical violations terminate the session. **Fails closed** — scanner error means block, not pass. Flagged matches are passed through warlock's LLM triage to drop false positives before the wizard acts. New rules go in warlock, not the wizard.
-
- **The test:** When the agent misbehaves, does the system prevent the damage or detect it afterward? Does the system fail closed on uncertainty?
-
- **What goes wrong when violated:** You build circuit breakers, checkpoints, retry-from-checkpoint, self-heal logic, graceful-exit handling. Each of these is code that exists because the boundary didn't prevent the problem. The codebase grows to compensate for the absence of prevention, and the recovery code itself becomes a source of complexity and bugs.
-
- ### 4. New capability is a new program, not a new branch
-
- When the product needs a new capability (revenue analytics, audit, LLM analytics), express it as a new program — a separate step array with its own config — not as conditional logic in the existing runner.
-
- `ProgramConfig` is a uniform type. The program registry is an array. CLI subcommands, screen sequences, the router, and the store all derive from the registry automatically. Adding a program means:
-
- 1. Create `src/lib/programs/<name>/` with `index.ts` exporting a `ProgramConfig`
- 2. Add it to `PROGRAM_REGISTRY` in `program-registry.ts`
-
- No changes to `bin.ts`, the store, the router, or the screen-sequences projection.
-
- **The test:** Can a new program ship without modifying the runner, the store, or `bin.ts`?
-
- **What goes wrong when violated:** The runner becomes a monolithic function with product-specific branches. Each new capability increases the blast radius of every other capability's changes. The function grows past the point where anyone can hold it in their head.
-
- ### 5. The UI is a pure function of state
-
- The screen the user sees is computed from `WizardSession` — the single source of truth. No component sets its own visibility. No code imperatively navigates. The router walks the program's step list and returns the first step whose `isComplete` predicate is still false.
-
- Every session mutation goes through an explicit store setter that calls `emitChange()`. The version counter bumps. Gate predicates are re-evaluated. Screen transitions are detected. React re-renders.
-
- **The test:** Can you predict which screen is active by reading only the session state? Is there any imperative navigation (`goTo`, `push`, `navigate`) in the codebase?
-
- **What goes wrong when violated:** The UI can reach states that don't correspond to any valid session. Screens get stuck. Flows advance when they shouldn't. Contributors add imperative transitions to "fix" rendering bugs that are actually state bugs, creating a tangle of navigation logic that obscures the real control flow.
-
- ## Extending into new territory
-
- The five principles above cover the existing patterns. But the wizard will need capabilities that don't fit neatly into any current configuration surface. Here's how to evaluate new extensions:
-
- ### Decision framework
-
- When you're adding something that doesn't have a precedent in the codebase, ask these questions in order:
-
- 1. **Which domain does this belong to?** If it's framework-specific, it goes in `FrameworkConfig`. If it's integration knowledge, it goes in skill content. If it's a security constraint, it goes in a warlock rule (the sibling repo). If you can't name a specific domain from the table above, the concern may not be well-defined yet.
-
- 2. **Does this change at a different rate than the code around it?** Knowledge that updates weekly shouldn't live in code that releases monthly. If the concern changes faster than its container, it needs a decoupled delivery mechanism (like context-mill skills). If it changes slower, it can live in code.
-
- 3. **Can I express this as configuration rather than logic?** A typed interface with six fields is better than a function with six conditionals. The interface is documentation. The conditionals are implementation detail that future readers must reverse-engineer.
-
- 4. **What happens if this concern is wrong?** If a wrong value causes a bad user experience, the boundary should provide fast feedback (compiler error, test failure). If a wrong value causes a security incident, the boundary should prevent execution (YARA, canUseTool). Match the severity of failure to the strength of the boundary.
-
- 5. **Does this expand the required knowledge for a domain?** If adding this feature means working on frameworks now requires understanding the TUI, or working on security now requires understanding the runner, the boundary is in the wrong place. Refactor until the required knowledge for each domain stays within its column.
-
- ### Patterns for common extension types
-
- **New agent tool (in-process MCP):** Add it to `wizard-tools.ts` alongside `check_env_keys`, `set_env_values`, etc. The tool runs locally — secret values never leave the machine. Register the tool name in `WIZARD_TOOL_NAMES` so the SDK allowlist includes it. Follow the existing tool pattern: zod schema, path-traversal protection, logging. If the tool handles a user secret, route it through the secret vault (`secret-vault.ts`) — return a `secret:<uuid>` ref, not the value, and resolve it host-side at the point of use — so the secret stays out of the LLM conversation.
-
- **New security rule:** Contribute the rule to [warlock](https://github.com/PostHog/warlock), not to the wizard. Warlock is the YARA-X engine that backs the wizard's security scanning; it's an append-only sibling repo with its own contribution process. Add a `.yar` file under `src/scanner/rules/` with a meta block (description, severity, category, scan_context, action) and a test under `src/scanner/__tests__/rules/`. The wizard's hooks in `yara-hooks.ts` automatically pick up new rules when it bumps its warlock dependency — wizard-side changes are unnecessary unless the response to a new category needs different handling. Filter consumed matches by `category` and `severity` (the append-only API contract), not by individual rule names.
-
- **New detection signal:** If you need to detect something about the project beyond framework identity (e.g., Stripe presence, LLM SDK usage), add it to `detection/features.ts`. The `discoverFeatures()` function returns an array of `DiscoveredFeature` enums. The intro screen and program can read discovered features from the session. Detection functions are pure — no store mutations, no UI calls.
-
- **New middleware:** Implement the `Middleware` interface from `middleware/types.ts`: a `name` and optional lifecycle hooks (`onInit`, `onMessage`, `onPhaseTransition`, `onFinalize`). Add it to the pipeline construction in `agent-runner.ts`. The pipeline dispatches to middlewares in order. Each middleware publishes to a shared store that downstream middleware can read. The pipeline itself doesn't change shape.
-
- **New TUI primitive:** Create the component in `src/ui/tui/primitives/`. Export from `primitives/index.ts`. Primitives are pure rendering — they take props and return Ink JSX. They don't import the store, don't call `getUI()`, don't mutate state. Screens compose primitives; primitives don't know what screen they're in. **Add a demo under `src/ui/tui/playground/demos/` and register it in `PlaygroundApp.tsx`** — primitives that aren't in the playground are invisible to future contributors, who will build duplicates instead of reusing yours. See the `ink-tui` skill for the full component catalog and layout patterns.
-
- **New post-agent step:** If you need to do something after the agent completes (upload env vars, install MCP servers, etc.), use the `postRun` hook on `ProgramRun`. The hook receives the session and credentials. It runs after the agent succeeds but before the outro. It doesn't modify the runner pipeline — it's a callback on the program configuration.
-
- **Custom program outro:** Set `buildOutroData` on the `ProgramRun`. The function receives the session, credentials, and cloud region; returns an `OutroData` object that drives the outro screen. Use this for program-specific success messages, change lists, dashboard URLs, or seasonal copy. The runner uses sensible defaults from `successMessage`/`reportFile`/`docsUrl` when `buildOutroData` is omitted.
-
- **New environment variable convention:** Each framework's `FrameworkConfig.environment.getEnvVars()` returns the env var names and values. The naming convention is framework-specific (e.g., `NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN` for Next.js, `PUBLIC_POSTHOG_PROJECT_TOKEN` for SvelteKit). If you need a new convention, add it to the framework config. The runner doesn't know or care what the env vars are called.
-
- ## Tests
+ # Wizard development
- Tests use vitest and live in `__tests__/` directories adjacent to the source they cover. The architecture is test-friendly because most hot zones are pure functions: router resolution, gate predicates, detection logic, prompt assembly, hook wiring, factory output. Test these directly — no mocking, no fixtures. (Security rule matching is tested in the warlock repo, not here.)
+ Ask who owns the concern and what they should need to understand to change it.
+ Product knowledge belongs behind typed configuration boundaries; runner and UI
+ infrastructure should consume those boundaries.
- What to test when extending the system:
+ | Concern | Owner |
+ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+ | Framework detection, context, env conventions | [FrameworkConfig](../../../src/lib/framework-config.ts) and [framework configs](../../../src/frameworks/) |
+ | Integration instructions and orchestrator flows/tasks | [context-mill](https://github.com/PostHog/context-mill) |
+ | Programs, steps, prerequisites and outcomes | [programs](../../../src/lib/programs/) |
+ | Sequence, harness, model and effort selection | [switchboard](../../../src/lib/agent/runner/switchboard/) |
+ | Local tool permissions and scanner adapters | [agent-interface](../../../src/lib/agent/agent-interface.ts), [YARA hooks](../../../src/lib/yara-hooks.ts), [Pi security](../../../src/lib/agent/runner/harness/pi/security.ts) |
+ | Scanner rules | [warlock](https://github.com/PostHog/warlock) |
+ | Token admission and budgets | [PostHog mint endpoint](https://github.com/PostHog/posthog/blob/master/posthog/llm/wizard_gateway_token.py) and [ai-gateway](https://github.com/PostHog/ai-gateway) |
+ | Screen resolution and rendering | [TUI](../../../src/ui/tui/) through [WizardUI](../../../src/ui/wizard-ui.ts) |
- - **New factory or typed interface:** add contract tests in a sibling `__tests__/` that lock down shape and defaults. Future readers (and future skill writers) lean on these as the durable record of what the abstraction guarantees.
- - **New gate or `isComplete` predicate:** test the predicate as a pure function with mutated `buildSession({})` results. Don't try to integration-test the router around it.
- - **New warlock rule:** tests live alongside the rule in the warlock repo (`src/scanner/__tests__/rules/`). Cover at least one matching string and one near-miss to catch over-matching. The wizard doesn't need tests for rules it merely consumes.
- - **New detection function:** test against fixture inputs (parsed `package.json` shapes, file contents). Detection is pure — no install dir mocking needed if you call the parser directly.
+ ## Execution policy and model admission
- What NOT to test:
+ - **Pi is the default choice for new work.** Harness choice and model provider
+ are separate: Pi supports gateway-backed Anthropic and OpenAI transports.
+ - **Prefer orchestration.** A seed agent plans work, then task agents execute
+ focused conversations. Start from
+ [metrics](../../../src/lib/programs/metrics/) and its context-mill flow.
+ - **Linear is for very simple tasks and legacy support.** Composed program
+ sub-runs are also structurally clamped to linear; orchestration cannot nest
+ through that seam.
+ - **Anthropic Agent SDK remains supported as a legacy fallback, deprecated as
+ the default.** Retain it for major Pi vulnerabilities or missing support for
+ new Anthropic models.
- - The agent run itself, the SDK call, real network calls. These are integration concerns; the unit boundary stops at "the configuration the runner consumes is correct."
- - The TUI's full render output. Test screen resolution (router predicates) and store setters; trust Ink to render.
- - Things the type system already guarantees. If the compiler catches it, a test for it is redundant.
+ Existing routing has not all migrated:
+ [DEFAULT_BINDING](../../../src/lib/agent/runner/switchboard/index.ts) still
+ selects Anthropic + linear, with per-program and flag overrides. Set new
+ bindings explicitly. Migrating an existing program requires checking its flow,
+ tasks, and lifecycle hooks; changing the default constant alone is insufficient.
+ Both harnesses implement `run` and `runTask`.
- Verify your changes with `pnpm build && pnpm test && pnpm fix` before finishing. If you can't write a clean test for what you're adding, that's often a signal that the boundary is in the wrong place.
+ Adding a model or effort is a cross-repository change:
- ## What to watch for
+ 1. Define the Wizard model ID and capabilities in
+ [constants](../../../src/lib/constants.ts) and
+ [models](../../../src/lib/agent/runner/switchboard/models.ts); select it
+ through the switchboard or supported context-mill stage metadata.
+ 2. Check the PostHog mint's `WIZARD_MODEL_ALLOWLIST` and allowed efforts. The
+ token's policy is enforced by the gateway; a local constant or CLI override
+ cannot authorize a model.
+ 3. Check the gateway's provider/catalog support and **required Wizard
+ system-prompt policy**, including the security-triage request shape. A model
+ not supported by that infrastructure needs a coordinated gateway/mint change
+ before use.
+ 4. Verify the chosen harness transport and effective effort with the same
+ admission policy used by the target environment.
- These are the early warning signs that a change is drifting from the discipline:
+ Local [commandments](../../../src/lib/agent/runner/switchboard/commandments.ts)
+ provide runtime and tool guidance. They do not replace the gateway's required
+ safety prompt. Keep gateway policy owned there rather than copying it into
+ skills. Do not infer per-model effort authorization merely from the local
+ capabilities table.
- - **The runner is getting longer.** If you're adding lines to `agent-runner.ts` or `agent-interface.ts`, ask whether the concern belongs in a `ProgramRun` config, a middleware, a post-run hook, or a skill file.
+ ## Choose the extension surface
- - **A framework contributor needs to read the runner.** The `FrameworkConfig` interface should be sufficient. If it's not, the interface is missing a field — extend the interface, don't add special-case logic to the runner.
+ - For a framework, follow
+ [adding-framework-support](../adding-framework-support/SKILL.md). Detection is
+ pure; use bounded filesystem helpers and preserve specific-before-generic
+ ordering.
+ - For a capability, follow
+ [adding-skill-program](../adding-skill-program/SKILL.md). Prefer a
+ context-mill command within an existing family when that is sufficient. A
+ native command needs a command module and registration in `bin.ts`, as well as
+ program and binding registration.
+ - For screens or primitives, follow [ink-tui](../ink-tui/SKILL.md). Program
+ steps drive screen sequences; business logic calls `getUI()`, and session
+ mutations use store setters that emit changes.
+ - For headless exploration, follow
+ [exploring-the-wizard](../exploring-the-wizard/SKILL.md). Drive current legal
+ actions and inspect error outros and pending questions throughout execution.
- - **The commandments are getting long.** Commandments are per-turn system prompt — every token counts. If a rule needs explanation, move the explanation to a skill reference file and leave only the invariant in the commandment.
+ For a new concern, first look for an existing typed surface. Add an abstraction
+ only when it gives a real owner a smaller, reusable boundary.
- - **You're building recovery infrastructure.** If you're writing retry logic, checkpoint saving, or self-heal code, ask whether a prevention layer (warlock rule, canUseTool rule, skill content improvement) would eliminate the failure mode instead of recovering from it.
+ ## Lifecycle and security
- - **You're adding imperative UI transitions.** If you're writing `goTo`, `navigate`, or `if (screen === X) show Y`, the session state doesn't accurately represent what the user should see. Fix the state model and let the router derive the screen.
+ [runner/index.ts](../../../src/lib/agent/runner/index.ts) bootstraps, resolves a
+ binding, and dispatches to a sequence. `agent-runner.ts` is a compatibility
+ re-export, not the implementation. Sequences own their lifecycle; harnesses own
+ SDK calls. `ProgramRun.postRun`, `buildOutroData`, `customPrompt`, and
+ `abortCases` are consumed by the linear sequence, not by the orchestrator. Put
+ orchestrated work in its flow/tasks and inspect its completion path when
+ extending it.
- - **A change in one program breaks another.** Programs should be independent. If they share mutable state beyond the session, that state needs an explicit boundary (a store setter, a program config field) instead of implicit coupling through the runner.
+ Keep tool enforcement at the boundary: shared permissions, harness-specific
+ adapters, and warlock scanning. Scanner failure must not silently allow an
+ unsafe operation. Review each adapter's blocking and termination behavior rather
+ than assuming every rejection terminates the run. New scanner rules belong in
+ warlock; changes to how a match is handled belong in Wizard.
- ## Compactness is an indicator, not a goal
+ User-provided secrets should travel through
+ [secret-vault](../../../src/lib/secret-vault.ts) references. Tool
+ implementations resolve values host-side where they are used; they must not
+ return the raw value to the model. Inspect the
+ [wizard-tools](../../../src/lib/wizard-tools/) implementations and the Pi
+ adapter when extending this shared tool surface.
- The wizard is ~20K lines of source. This isn't minimalism for its own sake. It's a side effect of routing each concern to the right configuration surface.
+ ## Verification and maintenance
- When concerns are correctly separated, each change is local. Defensive code is unnecessary because boundaries prevent damage from propagating. The codebase stays small because it doesn't need recovery infrastructure, special-case branches, or coordination code between concerns that shouldn't know about each other.
+ Reuse existing tests for routing, registration, detection, state transitions,
+ and security behavior. Add a focused regression only when it protects a
+ meaningful failure mode that existing checks do not cover. Avoid tests of prose,
+ duplicated shape checks, and assertions already enforced by TypeScript.
- If the codebase is growing faster than the capability it delivers, the boundaries are in the wrong place. Measure the ratio, not the absolute size.
+ For docs, verify local links and claimed APIs, then format edited files. For
+ code, run `pnpm typecheck` and the relevant existing Vitest files; build when
+ the change affects bundling or runtime behavior. `pnpm test` already builds.
+ Avoid repository-wide `pnpm fix` for a scoped edit. Keep new code comments to
+ one line.
- ## Reference files
+ Keep desired design policy distinguishable from current runtime behavior.
- - [references/ARCHITECTURE.md](references/ARCHITECTURE.md) — Pipeline anatomy, data flow, security boundaries, screen resolution, MCP topology, middleware pipeline. **Read when you need to understand where a new concern hooks in.**
- - [references/ANTI-PATTERNS.md](references/ANTI-PATTERNS.md) — Concrete failure modes with alternatives: inlining product logic, bloating commandments, building recovery instead of prevention, imperative navigation, bundling mismatched-rate knowledge. **Read when evaluating whether a proposed change fits.**
- - [references/MAINTAINING-SKILLS.md](references/MAINTAINING-SKILLS.md) — How to keep the skills under `.claude/skills/` accurate over time: drift sources, review checklist, review triggers, patterns that age well, the deletion question, versioning convention. **Read when making any major architecture change in this repo.**
+ Read references as needed:
+ - [Architecture](references/ARCHITECTURE.md): routing, lifecycle, security and
+ UI boundaries.
+ - [Anti-patterns](references/ANTI-PATTERNS.md): evaluate whether an extension
+ fits.
+ - [Maintaining skills](references/MAINTAINING-SKILLS.md): update skills when
+ architecture or APIs change.