Skip to content

Commit 77fae72

Browse files
danilocgewenyu99
andauthored
chore: Create a skill file that captures the wizard architecture principles (#438)
Co-authored-by: Vincent (Wen Yu) Ge <29069505+gewenyu99@users.noreply.github.com>
1 parent 721a598 commit 77fae72

8 files changed

Lines changed: 712 additions & 367 deletions

File tree

Lines changed: 103 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,160 @@
11
---
22
name: adding-skill-workflow
3-
description: Create a new skill-based workflow for the PostHog wizard. Use when adding a new workflow type (like revenue analytics, error tracking, feature flags) that installs a context-mill skill and runs an agent. Covers workflow steps, detection, flow registration, runner, custom screens, and CLI command.
3+
description: Create a new skill-based workflow for the PostHog wizard. Use when adding a workflow type (like revenue analytics, audit, error tracking) that installs a context-mill skill and runs an agent against it. Covers the createSkillWorkflow factory for the common case, customization via WorkflowRun, and advanced patterns for custom screens or detection.
44
compatibility: Designed for Claude Code working on the PostHog wizard codebase.
55
metadata:
66
author: posthog
7-
version: "1.1"
7+
version: "2.0"
88
---
99

1010
# Adding a Skill-Based Workflow
1111

12-
## Architecture Overview
12+
A skill-based workflow installs a context-mill skill and runs the agent against it. Examples in the codebase: the `audit` workflow (clean factory call), the `revenue-analytics` workflow (factory + custom intro screen + detect step).
1313

14-
Skill-based workflows (like revenue analytics) follow a different path from framework integrations. Instead of the agent runner building a prompt from a `FrameworkConfig`, a skill-based workflow:
14+
Before reading this, read `wizard-development/SKILL.md` for the architectural context — particularly principle 4 ("New capability is a new workflow, not a new branch").
1515

16-
1. **Detects prerequisites** and downloads a skill from context-mill
17-
2. **Runs the agent** against the installed skill using the generic `skill-runner.ts`
18-
3. **Shows results** via data-driven outro (no hardcoded messages)
16+
## Architecture
1917

20-
Key files:
21-
- `src/lib/workflow-step.ts``WorkflowStep` interface with `gate`, `onInit`, `StoreInitContext`
22-
- `src/lib/skill-runner.ts` — Generic runner: takes a skill path, builds bootstrap prompt, runs agent
23-
- `src/lib/wizard-tools.ts``fetchSkillMenu()` and `downloadSkill()` for installing skills via code
24-
- `src/utils/file-utils.ts` — Shared `IGNORED_DIRS` for project-tree scans
25-
- `src/ui/tui/flows.ts``Flow` enum, `Screen` enum, `WORKFLOW_STEPS`, `FLOWS` maps
26-
- `src/ui/tui/screen-registry.tsx` — Maps screen IDs to React components
27-
- `src/ui/tui/store.ts` — Gate system derived from workflow step definitions
18+
The wizard's runner pipeline is fixed. What varies between workflows is a `WorkflowRun` configuration object that controls the skill ID, prompt, success message, abort cases, and post-run hooks. A `WorkflowConfig` ties together: the CLI command, the step list, and the `WorkflowRun`. The workflow registry derives all downstream wiring — CLI subcommands, TUI flows, the router — from a single array. **Adding a workflow is configuration, not code.**
2819

29-
## How It Works
20+
## The common case: `createSkillWorkflow`
3021

31-
### Gates and isComplete
22+
For workflows that just install a skill and let the agent run it (most workflows), use the factory in `agent-skill/index.ts`:
3223

33-
- **`isComplete`** — exit condition for the screen. Router advances past the step when true. Defaults to `gate` if unset.
34-
- **`gate`** — define this if your screen needs to await user interactions. bin.ts pauses on `await store.getGate(stepId)` until the predicate becomes true.
24+
```ts
25+
// src/lib/workflows/error-tracking/index.ts
26+
import { createSkillWorkflow } from '../agent-skill/index.js';
3527

36-
### Detect step pattern
28+
export const errorTrackingConfig = createSkillWorkflow({
29+
skillId: 'error-tracking-setup',
30+
command: 'errors',
31+
flowKey: 'error-tracking',
32+
description: 'Set up PostHog error tracking',
33+
integrationLabel: 'error-tracking',
34+
successMessage: 'Error tracking configured!',
35+
reportFile: 'posthog-error-tracking-report.md',
36+
docsUrl: 'https://posthog.com/docs/error-tracking',
37+
spinnerMessage: 'Setting up error tracking...',
38+
estimatedDurationMinutes: 5,
39+
requires: ['posthog-integration'], // optional: prior workflows that must run first
40+
});
41+
```
3742

38-
Detection is split into two pieces:
43+
Then register it in two places:
3944

40-
1. **A headless `detect` workflow step** with a gate predicate that resolves once `frameworkContext.skillPath` or `frameworkContext.detectError` is set.
41-
2. **An exported `detect*Prerequisites()` async function** that bin.ts calls AFTER the session is assigned to the store.
45+
1. `src/lib/workflows/workflow-registry.ts` — add to `WORKFLOW_REGISTRY` array
46+
2. `src/ui/tui/flows.ts` — add a `Flow` enum entry whose value matches `flowKey`
4247

43-
**Why not `onInit`?** Because `onInit` fires during store construction (inside `_initFromWorkflow`), which runs BEFORE `tui.store.session = session` in bin.ts. Any `onInit` that reads `session.installDir` would get the default `process.cwd()`, not the app directory. `onInit` is fine for session-independent work like the integration flow's health check.
48+
That's the entire workflow. **bin.ts, the store, the agent runner, and the router all derive their wiring from the registry automatically.** Don't add a yargs command. Don't add a runner function. Don't touch bin.ts.
4449

45-
```typescript
46-
// In your workflow file
47-
export async function detectYourPrerequisites(
48-
session: WizardSession,
49-
setFrameworkContext: (key: string, value: unknown) => void,
50-
): Promise<void> {
51-
// Verify session.installDir, scan for required artifacts, fetch + download
52-
// the skill. On failure: setFrameworkContext('detectError', '...').
53-
// On success: setFrameworkContext('skillPath', '.claude/skills/...').
54-
// Optionally store any data the intro screen should render.
55-
}
50+
The `audit` workflow (`src/lib/workflows/audit/`) is the cleanest example of this pattern.
5651

57-
export const YOUR_WORKFLOW: Workflow = [
58-
{
59-
id: 'detect',
60-
label: 'Detecting prerequisites',
61-
gate: (s) =>
62-
s.frameworkContext.skillPath != null ||
63-
s.frameworkContext.detectError != null,
64-
},
65-
// ...
66-
];
67-
```
52+
## Customizing the agent run
6853

69-
### Error handling — never console.error from inside the TUI
54+
`createSkillWorkflow` accepts these optional fields on `SkillWorkflowOptions`, all of which flow through to the `WorkflowRun`:
7055

71-
When the Ink TUI is rendering, calling `console.error` and `process.exit(1)` mangles the screen. Instead, your custom intro screen reads `frameworkContext.detectError` and renders an error view with an Exit option. bin.ts just awaits the intro gate — the screen handles both success and error states.
56+
| Option | Purpose |
57+
|---|---|
58+
| `customPrompt` | Extra prompt instructions appended after the default project prompt |
59+
| `buildOutroData` | Override the default outro. Receives session, credentials, cloud region. Returns `OutroData`. |
60+
| `abortCases` | Array of `{ match: RegExp, message, body, docsUrl? }` that match `[ABORT] <reason>` signals from the skill |
61+
| `requires` | Other workflow `flowKey`s that must be satisfied first |
7262

73-
### StoreInitContext
63+
For more complex post-agent work (env var upload, dashboard creation, anything that needs to run after the agent completes but before the outro), drop the factory and build the `WorkflowConfig` directly so you can set `WorkflowRun.postRun`. See `posthog-integration` for that pattern.
7464

75-
Available in `onInit` callbacks (use only for session-independent work):
76-
- `ctx.session` — read current session state
77-
- `ctx.setReadinessResult(result)` — store health check results
78-
- `ctx.setFrameworkContext(key, value)` — store detection results
79-
- `ctx.emitChange()` — trigger gate re-evaluation
65+
## Dynamic run configuration
8066

81-
## Steps to Add a Workflow
67+
If your workflow needs to inspect the session before building the run config (read framework context, seed state on disk, set per-session prompt fragments), pass an async function as the workflow's `run`:
8268

83-
### 1. Define workflow steps
69+
```ts
70+
const baseConfig = createSkillWorkflow({ /* ... */ });
8471

85-
Create `src/lib/workflows/<your-workflow>.ts` with a detect step + (optional) intro step + auth + run + outro.
72+
const dynamicRun = async (session: WizardSession): Promise<WorkflowRun> => {
73+
// do per-session work here (e.g. seed a ledger, populate frameworkContext)
74+
if (!baseConfig.run) throw new Error('missing run');
75+
return typeof baseConfig.run === 'function'
76+
? baseConfig.run(session)
77+
: baseConfig.run;
78+
};
8679

87-
Export `detect*Prerequisites()` as a standalone async function — do NOT put detection in `onInit`.
88-
89-
### 2. Register the flow
80+
export const yourConfig: WorkflowConfig = {
81+
...baseConfig,
82+
run: dynamicRun,
83+
};
84+
```
9085

91-
In `src/ui/tui/flows.ts`:
92-
- Add to `Flow` enum
93-
- Add to `WORKFLOW_STEPS` map
94-
- Add to `FLOWS` record via `workflowToFlowEntries()`
86+
The `audit` workflow uses this pattern to seed a checks ledger on disk before the agent run.
9587

96-
### 3. Create the runner
88+
## Custom screens
9789

98-
The runner is trivial — it reads the skill path from session and delegates to `runSkillBootstrap()`:
90+
Skill-based workflows default to the generic step list in `agent-skill/steps.ts` (intro → auth → run → outro → keep-skills). To use workflow-specific screens (a custom intro that displays detection results, a custom outro with workflow-specific bullets), override the relevant step's `screen` field:
9991

100-
```typescript
101-
import { runSkillBootstrap } from './skill-runner';
92+
```ts
93+
const SCREEN_BY_STEP: Record<string, string> = {
94+
intro: 'your-intro',
95+
outro: 'your-outro',
96+
};
10297

103-
export async function runYourWizard(session: WizardSession): Promise<void> {
104-
const skillPath = session.frameworkContext.skillPath as string;
98+
const yourSteps: Workflow = AGENT_SKILL_STEPS.map((step) => {
99+
const override = SCREEN_BY_STEP[step.id];
100+
return override ? { ...step, screen: override } : step;
101+
});
105102

106-
await runSkillBootstrap(session, {
107-
skillPath,
108-
integrationLabel: 'your-workflow',
109-
promptContext: 'Set up X for this project.',
110-
successMessage: 'X configured!',
111-
reportFile: 'posthog-x-report.md',
112-
docsUrl: 'https://posthog.com/docs/x',
113-
spinnerMessage: 'Setting up X...',
114-
estimatedDurationMinutes: 5,
115-
});
116-
}
103+
export const yourConfig: WorkflowConfig = {
104+
...baseConfig,
105+
steps: yourSteps,
106+
};
117107
```
118108

119-
Use the actual skill ID from context-mill's skill menu — don't guess.
109+
Then:
120110

121-
### 4. (Optional) Custom intro screen
111+
1. Add the screen IDs to the `Screen` enum in `flows.ts`
112+
2. Create the React component(s) under `src/ui/tui/screens/`
113+
3. Register them in `src/ui/tui/screen-registry.tsx`
122114

123-
If you want a workflow-specific welcome screen, create one. The screen should also handle the `detectError` state since that's where errors are rendered.
115+
The screen reads from the store (via `useWizardStore`), renders error states from `frameworkContext.detectError` if present, and calls `store.completeSetup()` (or equivalent) when the user advances. The router resolves the active screen from session state — see `wizard-development/references/ARCHITECTURE.md` for the full screen resolution flow. **Never call `console.error` or imperatively navigate from inside the TUI.**
124116

125-
**a.** Add a screen ID to the `Screen` enum in `src/ui/tui/flows.ts`.
117+
## Detection / prerequisite checking
126118

127-
**b.** Create `src/ui/tui/screens/YourIntroScreen.tsx`. Subscribe to the store, read `detectError` and detection results from `session.frameworkContext`, render either an error view (with Exit) or the welcome view (with Continue/Cancel). On confirm, call `store.completeSetup()`.
119+
If your workflow needs to verify prerequisites before showing the intro screen (e.g. PostHog must already be installed, certain SDKs must be present), add a headless detect step at the top of the workflow with an `onReady` hook:
128120

129-
**c.** Register it in `src/ui/tui/screen-registry.tsx`.
130-
131-
**d.** Add an intro step to your workflow (after `detect`, before `auth`):
132-
```typescript
121+
```ts
133122
{
134-
id: 'intro',
135-
label: 'Welcome',
136-
screen: 'your-intro',
137-
gate: (s) => s.setupConfirmed,
138-
isComplete: (s) => s.setupConfirmed,
123+
id: 'detect',
124+
label: 'Detecting prerequisites',
125+
// No screen — this step is headless
126+
onReady: async (ctx) => {
127+
// ctx.session.installDir is the user's project dir
128+
// On success: ctx.setFrameworkContext('skillPath', '...')
129+
// On failure: ctx.setFrameworkContext('detectError', { kind: '...', ... })
130+
},
139131
},
140132
```
141133

142-
In bin.ts, await the intro gate after detect. Don't pre-set `setupConfirmed = true` if you have a custom intro — the user confirms via the screen.
134+
Use `onReady`, not `onInit``onInit` fires during store construction before `session` is assigned, so it can't read `installDir`. The custom intro screen reads `frameworkContext.detectError` and renders an error view (with an Exit option) when present, or the welcome view otherwise.
143135

144-
### 5. Add the CLI command
136+
The `revenue-analytics` workflow is the canonical example of this pattern (detect step + custom intro + abort cases).
145137

146-
In `bin.ts`, add a yargs command. The pattern:
147-
1. Start the TUI with your `Flow`
148-
2. Build session, assign to store
149-
3. Call `detect*Prerequisites()` explicitly
150-
4. Await `getGate('detect')`
151-
5. Await `getGate('intro')` — the screen handles both error and success states
152-
6. Call your runner
153-
7. Wait for `outroDismissed` via store subscribe, then `process.exit(0)` — without this, the process exits before the user can read the outro
138+
## Verification
154139

155-
**Do not** `console.error` or `process.exit` for `detectError` from bin.ts — that mangles the Ink output. Let the intro screen render the error.
140+
```bash
141+
pnpm build
142+
pnpm test
143+
pnpm fix
144+
```
156145

157-
### 6. Verify
146+
Then run end-to-end against a real test app:
158147

159148
```bash
160-
pnpm build # Must compile
161-
pnpm test # All tests pass
149+
pnpm try --install-dir=<path> <your-command>
162150
```
163151

164-
Then run your command end-to-end against a real test app, including failure cases (missing prerequisites, bad directories) to confirm graceful handling.
165-
166-
## Reference
152+
Test failure cases too — missing prerequisites, bad install directories, network errors during skill download. The wizard should render structured error outros, not stack traces.
167153

168-
See `references/WORKFLOW-GUIDE.md` for the full step-by-step guide with complete code examples.
154+
## Canonical examples in the codebase
169155

170-
## Canonical Example
156+
- `src/lib/workflows/audit/` — clean `createSkillWorkflow` call with abort cases, custom screens, and a dynamic `run` function for per-session seeding
157+
- `src/lib/workflows/revenue-analytics/` — factory + custom intro screen + detect step with prerequisite checking
158+
- `src/lib/workflows/agent-skill/` — the factory itself (`createSkillWorkflow`) and the generic step list (`AGENT_SKILL_STEPS`)
171159

172-
`src/lib/workflows/revenue-analytics.ts`read this for a full working implementation of every piece described above.
160+
When in doubt, read the directory of the workflow that most resembles what you're building.

0 commit comments

Comments
 (0)