Skip to content
Draft
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
134 changes: 127 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,87 @@ To make your version of a tool usable with a one-line `npx` command:
your project directory
3. Now you can run it with `npx yourpackagename`

# Workflow queue
# Wizard execution flow

## Full lifecycle

When a user runs `npx @posthog/wizard`, here's what happens end-to-end:

### 1. CLI parsing and framework detection (`bin.ts` → `src/run.ts`)

`bin.ts` parses CLI args, checks Node version, and calls `runWizard()` in `src/run.ts`. The run function detects the project framework (Next.js, React, etc.) by inspecting `package.json` and project structure, then loads the matching `FrameworkConfig` from `src/frameworks/`.

### 2. TUI startup and UI flow (`src/ui/tui/start-tui.ts`)

The TUI renders and the user progresses through screens. Screen order is driven by a `Workflow` — an ordered list of `WorkflowStep` objects defined in `src/lib/workflows/posthog-integration.ts`. Each step declares which screen it owns and when that screen is complete.

The workflow is converted to `FlowEntry[]` via `workflowToFlowEntries()` and fed to the router. The router walks the entries, skipping completed/hidden screens, and returns the first incomplete one. This is reactive — every session mutation re-resolves the active screen.

**Gate steps** block downstream code. The `intro` step has `gate: 'setup'` — `bin.ts` awaits `store.setupComplete` before proceeding. The `health-check` step has `gate: 'health'` — `bin.ts` awaits `store.healthGateComplete`.

### 3. Agent runner (`src/lib/agent-runner.ts`)

Once gates resolve, `runAgentWizard()` runs. This is where the queue takes over:

**Bootstrap query** — A standalone query tells the agent to load the skill menu, pick and install a skill, read SKILL.md, and emit the installed skill ID via `[WIZARD-SKILL-ID] <id>`. The model does NOT know about the queue — it just prepares the skill.

**SKILL.md parsing** — After bootstrap, the runner reads `.claude/skills/<id>/SKILL.md` from disk and parses the `workflow` array from its YAML frontmatter using `parseWorkflowStepsFromSkillMd()`. This produces a `WorkflowStepSeed[]` with step ids, reference filenames, and display titles.

**Queue seeding** — `createPostBootstrapQueue(steps)` builds a `WizardWorkflowQueue` from the parsed steps plus an `env-vars` step at the end. The queue is set on the store via `getUI().setWorkQueue(queue)` so the TUI can display it and dynamically enqueue new work.

**Execution loop** — The runner pops items from the queue one at a time:
```
while (queue.length > 0) {
dequeue → setCurrentQueueItem → build prompt → runAgent → completeQueueItem
}
```

Each `runAgent` call continues the same conversation via `resumeSessionId`. The model sees one prompt per step — either "read and follow this reference file" (for workflow items) or "set up environment variables" (for env-vars). The stop hook only fires the remark/feature-queue on the last item.

### 4. TUI progress tracking

During the run, the RunScreen displays a stage-grouped progress list. Stage headers come from queue item labels (which come from SKILL.md frontmatter titles). Nested tasks come from the agent's `TodoWrite` tool calls. When the runner advances to a new queue item, `setCurrentQueueItem()` fires, the store clears the task list, and the previous item moves to the completed list.

The queue is reactive on the store — `enqueue()` and `dequeue()` trigger `emitChange()` which re-renders the UI immediately.

The wizard executes agent work through a queue-backed runner. Instead of one monolithic prompt, each workflow step is a separate continued query.
### 5. Post-run (`agent-runner.ts` after loop)

## How it works
After the queue drains: error handling, env var upload to hosting providers, outro data construction, analytics shutdown.

1. **Bootstrap** runs first as a standalone query — installs the skill and emits the skill ID.
2. The runner reads `SKILL.md` from the installed skill and parses the `workflow` array from its YAML frontmatter to discover the step list.
3. A `WizardWorkflowQueue` is seeded from those steps plus an `env-vars` step at the end.
4. The runner pops items from the queue and issues one continued query per item, preserving the conversation across steps.
## Data flow diagram

```
bin.ts
├─ Framework detection → FrameworkConfig
├─ TUI startup → WizardStore + Router
│ │
│ └─ Workflow (WorkflowStep[])
│ │
│ └─ workflowToFlowEntries() → FlowEntry[] → Router (screen resolution)
├─ await setupComplete (gate)
├─ await healthGateComplete (gate)
└─ runAgentWizard()
├─ Bootstrap query → skill installed → [WIZARD-SKILL-ID]
├─ Read SKILL.md → parseWorkflowStepsFromSkillMd() → WorkflowStepSeed[]
├─ createPostBootstrapQueue(steps) → WizardWorkflowQueue
│ │
│ └─ setWorkQueue(queue) → store (reactive, UI can enqueue)
└─ while (queue.length > 0)
├─ dequeue → setCurrentQueueItem
├─ buildWorkflowStepPrompt / buildEnvVarPrompt
├─ runAgent (continued conversation)
└─ completeQueueItem
```

# Workflow queue

## SKILL.md frontmatter format

Expand Down Expand Up @@ -285,6 +356,55 @@ The RunScreen shows a stage-grouped progress list:

Stage headers come from queue item labels. Nested tasks come from the agent's `TodoWrite` calls. Tasks reset when the runner advances to a new stage.

## Defining a workflow

A workflow is an ordered list of `WorkflowStep` objects. Each step can own a screen, agent work, or both.

```typescript
// src/lib/workflow-step.ts
interface WorkflowStep {
id: string; // unique step id
label: string; // shown in progress list
screen?: string; // TUI screen (e.g. 'intro', 'run')
show?: (session: WizardSession) => boolean; // visibility predicate
isComplete?: (session: WizardSession) => boolean; // completion predicate
gate?: 'setup' | 'health'; // blocks downstream code
}
```

The current PostHog integration workflow is defined in `src/lib/workflows/posthog-integration.ts`:

```typescript
export const POSTHOG_INTEGRATION_WORKFLOW: Workflow = [
{ id: 'intro', label: 'Welcome', screen: 'intro', gate: 'setup', isComplete: ... },
{ id: 'health', label: 'Health check', screen: 'health-check', gate: 'health', ... },
{ id: 'setup', label: 'Setup', screen: 'setup', show: needsSetup, ... },
{ id: 'auth', label: 'Authentication', screen: 'auth', isComplete: ... },
{ id: 'run', label: 'Integration', screen: 'run', isComplete: ... },
{ id: 'mcp', label: 'MCP servers', screen: 'mcp', isComplete: ... },
{ id: 'outro', label: 'Done', screen: 'outro', isComplete: ... },
{ id: 'skills', label: 'Skills', screen: 'skills' },
];
```

### Creating a new workflow

1. Create a new file in `src/lib/workflows/` (e.g. `feature-flags.ts`)
2. Export a `Workflow` array with your steps
3. Each step with a `screen` field needs a matching component in the screen registry
4. The flow engine converts your workflow to `FlowEntry[]` via `workflowToFlowEntries()` — the existing router handles the rest
5. Agent work steps are seeded from SKILL.md frontmatter at runtime, not from the workflow definition

### How the pieces connect

```
WorkflowStep[] ──workflowToFlowEntries()──> FlowEntry[] ──> Router (screen resolution)
SKILL.md frontmatter ──parseWorkflowStepsFromSkillMd()──> Queue ──> Agent runner (per-step queries)
```

The workflow definition owns the UI flow. The SKILL.md frontmatter owns the agent work sequence. Both run during the same wizard session.

# Health checks

`src/lib/health-checks/` checks external status pages and PostHog-owned
Expand Down
43 changes: 23 additions & 20 deletions src/lib/stage.ts → src/lib/workflow-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type { WizardSession } from './wizard-session';
* It can own:
* - a screen in the TUI (optional — some steps are headless)
* - agent work via a workflow reference (optional — some steps are UI-only)
* - local state needs (selectors it depends on)
* - completion and visibility predicates
*
* The current PostHog integration flow is one ordered list of steps.
Expand All @@ -16,6 +15,9 @@ export interface WorkflowStep {
/** Unique identifier for this step */
id: string;

/** Human-readable label for progress display */
label: string;

/**
* TUI screen this step owns, if any.
* Matches the Screen enum values (e.g. 'intro', 'run', 'outro').
Expand All @@ -34,33 +36,34 @@ export interface WorkflowStep {
*/
isComplete?: (session: WizardSession) => boolean;

/**
* Workflow reference filename this step executes, if any.
* When set, the runner issues a continued query for this reference.
* e.g. "basic-integration-1.0-begin.md"
*/
workflowReference?: string;

/**
* Whether this step blocks downstream code via a gate promise.
* e.g. "setup" and "health-check" gate bin.ts before runWizard().
*/
gate?: 'setup' | 'health';

/**
* Hook called when the step becomes active.
*/
onEnter?: () => void;

/**
* Hook called when the step completes.
*/
onComplete?: () => void;
}

/**
* An ordered list of workflow steps that defines a wizard flow.
* The first flow is the current PostHog integration.
* Future flows register different step lists.
*/
export type Workflow = WorkflowStep[];

/**
* Convert a Workflow into the FlowEntry shape the router expects.
* This is the bridge between the new WorkflowStep model and the
* existing router — lets us adopt WorkflowSteps without rewriting
* the router.
*/
export function workflowToFlowEntries(workflow: Workflow): Array<{
screen: string;
show?: (session: WizardSession) => boolean;
isComplete?: (session: WizardSession) => boolean;
}> {
return workflow
.filter((step) => step.screen != null)
.map((step) => ({
screen: step.screen!,
show: step.show,
isComplete: step.isComplete,
}));
}
80 changes: 80 additions & 0 deletions src/lib/workflows/posthog-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* PostHog integration workflow — the default wizard flow.
*
* This is a 1:1 mapping of the current FLOWS[Flow.Wizard] screen pipeline
* expressed as WorkflowSteps. The flow engine derives FlowEntry[] from this
* so the existing router continues to work unchanged.
*/

import type { Workflow } from '../workflow-step.js';
import type { WizardSession } from '../wizard-session.js';
import { RunPhase } from '../wizard-session.js';
import { WizardReadiness } from '../health-checks/readiness.js';

function needsSetup(session: WizardSession): boolean {
const config = session.frameworkConfig;
if (!config?.metadata.setup?.questions) return false;

return config.metadata.setup.questions.some(
(q: { key: string }) => !(q.key in session.frameworkContext),
);
}

export const POSTHOG_INTEGRATION_WORKFLOW: Workflow = [
{
id: 'intro',
label: 'Welcome',
screen: 'intro',
gate: 'setup',
isComplete: (s) => s.setupConfirmed,
},
{
id: 'health-check',
label: 'Health check',
screen: 'health-check',
gate: 'health',
isComplete: (s) => {
if (!s.readinessResult) return false;
if (s.readinessResult.decision === WizardReadiness.No)
return s.outageDismissed;
return true;
},
},
{
id: 'setup',
label: 'Setup',
screen: 'setup',
show: needsSetup,
isComplete: (s) => !needsSetup(s),
},
{
id: 'auth',
label: 'Authentication',
screen: 'auth',
isComplete: (s) => s.credentials !== null,
},
{
id: 'run',
label: 'Integration',
screen: 'run',
isComplete: (s) =>
s.runPhase === RunPhase.Completed || s.runPhase === RunPhase.Error,
},
{
id: 'mcp',
label: 'MCP servers',
screen: 'mcp',
isComplete: (s) => s.mcpComplete,
},
{
id: 'outro',
label: 'Done',
screen: 'outro',
isComplete: (s) => s.outroDismissed,
},
{
id: 'skills',
label: 'Skills',
screen: 'skills',
},
];
61 changes: 10 additions & 51 deletions src/ui/tui/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
* to resolve which screen to show.
*/

import { type WizardSession, RunPhase } from '../../lib/wizard-session.js';
import { WizardReadiness } from '../../lib/health-checks/readiness.js';
import type { WizardSession } from '../../lib/wizard-session.js';
import { workflowToFlowEntries } from '../../lib/workflow-step.js';
import { POSTHOG_INTEGRATION_WORKFLOW } from '../../lib/workflows/posthog-integration.js';

// ── Screen + Flow enums ──────────────────────────────────────────────

Expand Down Expand Up @@ -47,57 +48,15 @@ export interface FlowEntry {
}

/**
* Check if the SetupScreen is needed (unresolved framework questions).
* All flow pipelines.
*
* The Wizard flow is derived from the PostHog integration workflow definition.
* MCP add/remove flows are standalone since they don't go through the agent runner.
*/
function needsSetup(session: WizardSession): boolean {
const config = session.frameworkConfig;
if (!config?.metadata.setup?.questions) return false;

return config.metadata.setup.questions.some(
(q: { key: string }) => !(q.key in session.frameworkContext),
);
}

/** All flow pipelines. Add new screens by appending entries. */
export const FLOWS: Record<Flow, FlowEntry[]> = {
[Flow.Wizard]: [
{
screen: Screen.Intro,
isComplete: (s) => s.setupConfirmed,
},
{
screen: Screen.HealthCheck,
isComplete: (s) => {
if (!s.readinessResult) return false;
if (s.readinessResult.decision === WizardReadiness.No)
return s.outageDismissed;
return true;
},
},
{
screen: Screen.Setup,
show: needsSetup,
isComplete: (s) => !needsSetup(s),
},
{
screen: Screen.Auth,
isComplete: (s) => s.credentials !== null,
},
{
screen: Screen.Run,
isComplete: (s) =>
s.runPhase === RunPhase.Completed || s.runPhase === RunPhase.Error,
},
{
screen: Screen.Mcp,
isComplete: (s) => s.mcpComplete,
},
{
screen: Screen.Outro,
isComplete: (s) => s.outroDismissed,
},
{ screen: Screen.Skills },
],
[Flow.Wizard]: workflowToFlowEntries(
POSTHOG_INTEGRATION_WORKFLOW,
) as FlowEntry[],

[Flow.McpAdd]: [
{
Expand Down
Loading