Skip to content

Commit ffea854

Browse files
committed
Use workflows
1 parent 4344f09 commit ffea854

4 files changed

Lines changed: 240 additions & 78 deletions

File tree

README.md

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,87 @@ To make your version of a tool usable with a one-line `npx` command:
198198
your project directory
199199
3. Now you can run it with `npx yourpackagename`
200200

201-
# Workflow queue
201+
# Wizard execution flow
202+
203+
## Full lifecycle
204+
205+
When a user runs `npx @posthog/wizard`, here's what happens end-to-end:
206+
207+
### 1. CLI parsing and framework detection (`bin.ts``src/run.ts`)
208+
209+
`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/`.
210+
211+
### 2. TUI startup and UI flow (`src/ui/tui/start-tui.ts`)
212+
213+
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.
214+
215+
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.
216+
217+
**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`.
218+
219+
### 3. Agent runner (`src/lib/agent-runner.ts`)
220+
221+
Once gates resolve, `runAgentWizard()` runs. This is where the queue takes over:
222+
223+
**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.
224+
225+
**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.
226+
227+
**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.
228+
229+
**Execution loop** — The runner pops items from the queue one at a time:
230+
```
231+
while (queue.length > 0) {
232+
dequeue → setCurrentQueueItem → build prompt → runAgent → completeQueueItem
233+
}
234+
```
235+
236+
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.
237+
238+
### 4. TUI progress tracking
239+
240+
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.
241+
242+
The queue is reactive on the store — `enqueue()` and `dequeue()` trigger `emitChange()` which re-renders the UI immediately.
202243

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

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

207-
1. **Bootstrap** runs first as a standalone query — installs the skill and emits the skill ID.
208-
2. The runner reads `SKILL.md` from the installed skill and parses the `workflow` array from its YAML frontmatter to discover the step list.
209-
3. A `WizardWorkflowQueue` is seeded from those steps plus an `env-vars` step at the end.
210-
4. The runner pops items from the queue and issues one continued query per item, preserving the conversation across steps.
248+
## Data flow diagram
249+
250+
```
251+
bin.ts
252+
253+
├─ Framework detection → FrameworkConfig
254+
├─ TUI startup → WizardStore + Router
255+
│ │
256+
│ └─ Workflow (WorkflowStep[])
257+
│ │
258+
│ └─ workflowToFlowEntries() → FlowEntry[] → Router (screen resolution)
259+
260+
├─ await setupComplete (gate)
261+
├─ await healthGateComplete (gate)
262+
263+
└─ runAgentWizard()
264+
265+
├─ Bootstrap query → skill installed → [WIZARD-SKILL-ID]
266+
267+
├─ Read SKILL.md → parseWorkflowStepsFromSkillMd() → WorkflowStepSeed[]
268+
269+
├─ createPostBootstrapQueue(steps) → WizardWorkflowQueue
270+
│ │
271+
│ └─ setWorkQueue(queue) → store (reactive, UI can enqueue)
272+
273+
└─ while (queue.length > 0)
274+
275+
├─ dequeue → setCurrentQueueItem
276+
├─ buildWorkflowStepPrompt / buildEnvVarPrompt
277+
├─ runAgent (continued conversation)
278+
└─ completeQueueItem
279+
```
280+
281+
# Workflow queue
211282

212283
## SKILL.md frontmatter format
213284

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

286357
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.
287358

359+
## Defining a workflow
360+
361+
A workflow is an ordered list of `WorkflowStep` objects. Each step can own a screen, agent work, or both.
362+
363+
```typescript
364+
// src/lib/workflow-step.ts
365+
interface WorkflowStep {
366+
id: string; // unique step id
367+
label: string; // shown in progress list
368+
screen?: string; // TUI screen (e.g. 'intro', 'run')
369+
show?: (session: WizardSession) => boolean; // visibility predicate
370+
isComplete?: (session: WizardSession) => boolean; // completion predicate
371+
gate?: 'setup' | 'health'; // blocks downstream code
372+
}
373+
```
374+
375+
The current PostHog integration workflow is defined in `src/lib/workflows/posthog-integration.ts`:
376+
377+
```typescript
378+
export const POSTHOG_INTEGRATION_WORKFLOW: Workflow = [
379+
{ id: 'intro', label: 'Welcome', screen: 'intro', gate: 'setup', isComplete: ... },
380+
{ id: 'health', label: 'Health check', screen: 'health-check', gate: 'health', ... },
381+
{ id: 'setup', label: 'Setup', screen: 'setup', show: needsSetup, ... },
382+
{ id: 'auth', label: 'Authentication', screen: 'auth', isComplete: ... },
383+
{ id: 'run', label: 'Integration', screen: 'run', isComplete: ... },
384+
{ id: 'mcp', label: 'MCP servers', screen: 'mcp', isComplete: ... },
385+
{ id: 'outro', label: 'Done', screen: 'outro', isComplete: ... },
386+
{ id: 'skills', label: 'Skills', screen: 'skills' },
387+
];
388+
```
389+
390+
### Creating a new workflow
391+
392+
1. Create a new file in `src/lib/workflows/` (e.g. `feature-flags.ts`)
393+
2. Export a `Workflow` array with your steps
394+
3. Each step with a `screen` field needs a matching component in the screen registry
395+
4. The flow engine converts your workflow to `FlowEntry[]` via `workflowToFlowEntries()` — the existing router handles the rest
396+
5. Agent work steps are seeded from SKILL.md frontmatter at runtime, not from the workflow definition
397+
398+
### How the pieces connect
399+
400+
```
401+
WorkflowStep[] ──workflowToFlowEntries()──> FlowEntry[] ──> Router (screen resolution)
402+
403+
SKILL.md frontmatter ──parseWorkflowStepsFromSkillMd()──> Queue ──> Agent runner (per-step queries)
404+
```
405+
406+
The workflow definition owns the UI flow. The SKILL.md frontmatter owns the agent work sequence. Both run during the same wizard session.
407+
288408
# Health checks
289409

290410
`src/lib/health-checks/` checks external status pages and PostHog-owned
Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { WizardSession } from './wizard-session';
66
* It can own:
77
* - a screen in the TUI (optional — some steps are headless)
88
* - agent work via a workflow reference (optional — some steps are UI-only)
9-
* - local state needs (selectors it depends on)
109
* - completion and visibility predicates
1110
*
1211
* The current PostHog integration flow is one ordered list of steps.
@@ -16,6 +15,9 @@ export interface WorkflowStep {
1615
/** Unique identifier for this step */
1716
id: string;
1817

18+
/** Human-readable label for progress display */
19+
label: string;
20+
1921
/**
2022
* TUI screen this step owns, if any.
2123
* Matches the Screen enum values (e.g. 'intro', 'run', 'outro').
@@ -34,33 +36,34 @@ export interface WorkflowStep {
3436
*/
3537
isComplete?: (session: WizardSession) => boolean;
3638

37-
/**
38-
* Workflow reference filename this step executes, if any.
39-
* When set, the runner issues a continued query for this reference.
40-
* e.g. "basic-integration-1.0-begin.md"
41-
*/
42-
workflowReference?: string;
43-
4439
/**
4540
* Whether this step blocks downstream code via a gate promise.
4641
* e.g. "setup" and "health-check" gate bin.ts before runWizard().
4742
*/
4843
gate?: 'setup' | 'health';
49-
50-
/**
51-
* Hook called when the step becomes active.
52-
*/
53-
onEnter?: () => void;
54-
55-
/**
56-
* Hook called when the step completes.
57-
*/
58-
onComplete?: () => void;
5944
}
6045

6146
/**
6247
* An ordered list of workflow steps that defines a wizard flow.
63-
* The first flow is the current PostHog integration.
64-
* Future flows register different step lists.
6548
*/
6649
export type Workflow = WorkflowStep[];
50+
51+
/**
52+
* Convert a Workflow into the FlowEntry shape the router expects.
53+
* This is the bridge between the new WorkflowStep model and the
54+
* existing router — lets us adopt WorkflowSteps without rewriting
55+
* the router.
56+
*/
57+
export function workflowToFlowEntries(workflow: Workflow): Array<{
58+
screen: string;
59+
show?: (session: WizardSession) => boolean;
60+
isComplete?: (session: WizardSession) => boolean;
61+
}> {
62+
return workflow
63+
.filter((step) => step.screen != null)
64+
.map((step) => ({
65+
screen: step.screen!,
66+
show: step.show,
67+
isComplete: step.isComplete,
68+
}));
69+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* PostHog integration workflow — the default wizard flow.
3+
*
4+
* This is a 1:1 mapping of the current FLOWS[Flow.Wizard] screen pipeline
5+
* expressed as WorkflowSteps. The flow engine derives FlowEntry[] from this
6+
* so the existing router continues to work unchanged.
7+
*/
8+
9+
import type { Workflow } from '@lib/workflow-step';
10+
import type { WizardSession } from '@lib/wizard-session';
11+
import { RunPhase } from '@lib/wizard-session';
12+
import { WizardReadiness } from '@lib/health-checks/readiness';
13+
14+
function needsSetup(session: WizardSession): boolean {
15+
const config = session.frameworkConfig;
16+
if (!config?.metadata.setup?.questions) return false;
17+
18+
return config.metadata.setup.questions.some(
19+
(q: { key: string }) => !(q.key in session.frameworkContext),
20+
);
21+
}
22+
23+
export const POSTHOG_INTEGRATION_WORKFLOW: Workflow = [
24+
{
25+
id: 'intro',
26+
label: 'Welcome',
27+
screen: 'intro',
28+
gate: 'setup',
29+
isComplete: (s) => s.setupConfirmed,
30+
},
31+
{
32+
id: 'health-check',
33+
label: 'Health check',
34+
screen: 'health-check',
35+
gate: 'health',
36+
isComplete: (s) => {
37+
if (!s.readinessResult) return false;
38+
if (s.readinessResult.decision === WizardReadiness.No)
39+
return s.outageDismissed;
40+
return true;
41+
},
42+
},
43+
{
44+
id: 'setup',
45+
label: 'Setup',
46+
screen: 'setup',
47+
show: needsSetup,
48+
isComplete: (s) => !needsSetup(s),
49+
},
50+
{
51+
id: 'auth',
52+
label: 'Authentication',
53+
screen: 'auth',
54+
isComplete: (s) => s.credentials !== null,
55+
},
56+
{
57+
id: 'run',
58+
label: 'Integration',
59+
screen: 'run',
60+
isComplete: (s) =>
61+
s.runPhase === RunPhase.Completed || s.runPhase === RunPhase.Error,
62+
},
63+
{
64+
id: 'mcp',
65+
label: 'MCP servers',
66+
screen: 'mcp',
67+
isComplete: (s) => s.mcpComplete,
68+
},
69+
{
70+
id: 'outro',
71+
label: 'Done',
72+
screen: 'outro',
73+
isComplete: (s) => s.outroDismissed,
74+
},
75+
{
76+
id: 'skills',
77+
label: 'Skills',
78+
screen: 'skills',
79+
},
80+
];

src/ui/tui/flows.ts

Lines changed: 10 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
* to resolve which screen to show.
1010
*/
1111

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

1516
// ── Screen + Flow enums ──────────────────────────────────────────────
1617

@@ -47,57 +48,15 @@ export interface FlowEntry {
4748
}
4849

4950
/**
50-
* Check if the SetupScreen is needed (unresolved framework questions).
51+
* All flow pipelines.
52+
*
53+
* The Wizard flow is derived from the PostHog integration workflow definition.
54+
* MCP add/remove flows are standalone since they don't go through the agent runner.
5155
*/
52-
function needsSetup(session: WizardSession): boolean {
53-
const config = session.frameworkConfig;
54-
if (!config?.metadata.setup?.questions) return false;
55-
56-
return config.metadata.setup.questions.some(
57-
(q: { key: string }) => !(q.key in session.frameworkContext),
58-
);
59-
}
60-
61-
/** All flow pipelines. Add new screens by appending entries. */
6256
export const FLOWS: Record<Flow, FlowEntry[]> = {
63-
[Flow.Wizard]: [
64-
{
65-
screen: Screen.Intro,
66-
isComplete: (s) => s.setupConfirmed,
67-
},
68-
{
69-
screen: Screen.HealthCheck,
70-
isComplete: (s) => {
71-
if (!s.readinessResult) return false;
72-
if (s.readinessResult.decision === WizardReadiness.No)
73-
return s.outageDismissed;
74-
return true;
75-
},
76-
},
77-
{
78-
screen: Screen.Setup,
79-
show: needsSetup,
80-
isComplete: (s) => !needsSetup(s),
81-
},
82-
{
83-
screen: Screen.Auth,
84-
isComplete: (s) => s.credentials !== null,
85-
},
86-
{
87-
screen: Screen.Run,
88-
isComplete: (s) =>
89-
s.runPhase === RunPhase.Completed || s.runPhase === RunPhase.Error,
90-
},
91-
{
92-
screen: Screen.Mcp,
93-
isComplete: (s) => s.mcpComplete,
94-
},
95-
{
96-
screen: Screen.Outro,
97-
isComplete: (s) => s.outroDismissed,
98-
},
99-
{ screen: Screen.Skills },
100-
],
57+
[Flow.Wizard]: workflowToFlowEntries(
58+
POSTHOG_INTEGRATION_WORKFLOW,
59+
) as FlowEntry[],
10160

10261
[Flow.McpAdd]: [
10362
{

0 commit comments

Comments
 (0)