|
1 | 1 | --- |
2 | 2 | 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. |
4 | 4 | compatibility: Designed for Claude Code working on the PostHog wizard codebase. |
5 | 5 | metadata: |
6 | 6 | author: posthog |
7 | | - version: "1.1" |
| 7 | + version: "2.0" |
8 | 8 | --- |
9 | 9 |
|
10 | 10 | # Adding a Skill-Based Workflow |
11 | 11 |
|
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). |
13 | 13 |
|
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"). |
15 | 15 |
|
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 |
19 | 17 |
|
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.** |
28 | 19 |
|
29 | | -## How It Works |
| 20 | +## The common case: `createSkillWorkflow` |
30 | 21 |
|
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`: |
32 | 23 |
|
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'; |
35 | 27 |
|
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 | +``` |
37 | 42 |
|
38 | | -Detection is split into two pieces: |
| 43 | +Then register it in two places: |
39 | 44 |
|
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` |
42 | 47 |
|
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. |
44 | 49 |
|
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. |
56 | 51 |
|
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 |
68 | 53 |
|
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`: |
70 | 55 |
|
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 | |
72 | 62 |
|
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. |
74 | 64 |
|
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 |
80 | 66 |
|
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`: |
82 | 68 |
|
83 | | -### 1. Define workflow steps |
| 69 | +```ts |
| 70 | +const baseConfig = createSkillWorkflow({ /* ... */ }); |
84 | 71 |
|
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 | +}; |
86 | 79 |
|
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 | +``` |
90 | 85 |
|
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. |
95 | 87 |
|
96 | | -### 3. Create the runner |
| 88 | +## Custom screens |
97 | 89 |
|
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: |
99 | 91 |
|
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 | +}; |
102 | 97 |
|
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 | +}); |
105 | 102 |
|
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 | +}; |
117 | 107 | ``` |
118 | 108 |
|
119 | | -Use the actual skill ID from context-mill's skill menu — don't guess. |
| 109 | +Then: |
120 | 110 |
|
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` |
122 | 114 |
|
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.** |
124 | 116 |
|
125 | | -**a.** Add a screen ID to the `Screen` enum in `src/ui/tui/flows.ts`. |
| 117 | +## Detection / prerequisite checking |
126 | 118 |
|
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: |
128 | 120 |
|
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 |
133 | 122 | { |
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 | + }, |
139 | 131 | }, |
140 | 132 | ``` |
141 | 133 |
|
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. |
143 | 135 |
|
144 | | -### 5. Add the CLI command |
| 136 | +The `revenue-analytics` workflow is the canonical example of this pattern (detect step + custom intro + abort cases). |
145 | 137 |
|
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 |
154 | 139 |
|
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 | +``` |
156 | 145 |
|
157 | | -### 6. Verify |
| 146 | +Then run end-to-end against a real test app: |
158 | 147 |
|
159 | 148 | ```bash |
160 | | -pnpm build # Must compile |
161 | | -pnpm test # All tests pass |
| 149 | +pnpm try --install-dir=<path> <your-command> |
162 | 150 | ``` |
163 | 151 |
|
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. |
167 | 153 |
|
168 | | -See `references/WORKFLOW-GUIDE.md` for the full step-by-step guide with complete code examples. |
| 154 | +## Canonical examples in the codebase |
169 | 155 |
|
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`) |
171 | 159 |
|
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