This is the contributor doc for context/skills/ and the
scripts/ build pipeline. For docs aimed at consumers of the published
manifest, see the README.
Every skill ships with a config.yaml. An optional cli: block on that
config tells the PostHog wizard whether and how this skill appears as a
command. The block is parsed in scripts/lib/skill-generator.js and
emitted as cliEntries inside dist/skills/skill-menu.json. The wizard
fetches skill-menu.json at runtime and registers each entry as a
command, so adding a new skill-backed command is a context-mill release
— no wizard release needed.
type: docs-only
description: Audit captured events
cli:
role: command # command | skill | internal
parentCommand: audit # the command this skill nests under (optional)
command: events # the user-typed word; required when role is command
default: true # optional — the leaf `wizard <family>` runs by defaultThree values for role:
| Role | Where it shows up |
|---|---|
command |
Registered as wizard <parentCommand> <command> (or wizard <command> if no parent). The user-facing CLI. |
skill |
Reachable only via wizard skill <id>. The full discoverable set. |
internal |
Hidden everywhere. Only reachable via wizard --skill=<id> (a dev escape hatch). Useful for in-progress skills that aren't ready to expose. |
Skills with no cli: block default to the skill role — they're
discoverable via wizard skill list but don't get a top-level command.
The same skill can be either surface: audit-events sets role: command so it's
wizard audit events; a role: skill skill is only wizard skill <id>. One
mechanism, two surfaces — so wizard audit <subcommand> chooses an audit area,
it does not take a skill name.
A command is flat when there's only one option today, a family when the user must pick among multiple distinct things.
Don't pre-create a family form for a single-option command. If only one
migration vendor exists, the command is wizard migrate — not
wizard migrate statsig. When a second vendor arrives, restructure to
a family at that moment and document the UX change in the wizard's
release notes. Forced abstraction (wizard migrate <vendor> with one
vendor) is worse than the breaking change you'd cause later — that
change is real and worth notifying users about explicitly.
When a flat command needs to grow into a family (a second option arrived,
the original is being renamed, etc.), the cli: block restructures like
this:
# Before — flat command. Registers `wizard investigate`.
cli:
role: command
command: investigate
# After — family with a subcommand. Registers `wizard investigate events`.
cli:
role: command
parentCommand: investigate
command: eventsThis is a breaking change for users. Anyone scripting the old flat
form (e.g. wizard investigate in CI) will break the moment the new
manifest ships — the parent name now expects a subcommand and opens the
family picker. Treat this exactly like any other breaking CLI change:
- Land the YAML change in context-mill.
- Call out the migration explicitly in the wizard release notes for the release that picks up the new manifest — what the old command was, what the new shape is, and what users need to change.
- If the old flat name is still meaningful as a default leaf of the new
family, mark that leaf
default: truesowizard investigate→ Enter still runs the intended action with one keystroke.
Going the other way — collapsing a family back to a flat command — works the same way and is also a breaking change. Don't do it casually.
Use the full PostHog product name with hyphens, not abbreviations.
| Good | Bad | |
|---|---|---|
| Feature flags audit | wizard audit feature-flags |
wizard audit flags |
| Session replay audit | wizard audit session-replay |
wizard audit replay |
| Revenue analytics | wizard revenue-analytics |
wizard revenue |
| Web analytics | wizard web-analytics |
wizard web |
| LLM analytics | wizard llm-analytics |
wizard llms |
The kebab-case / length / reserved-word checks in parseCliBlock
enforce the mechanics; this rule is the naming taste layer on top of
them. Users typing the full product name once is cheap; getting them
to relearn an abbreviation we changed our mind on later is not.
# 1. Flat command (single option today)
cli: → wizard revenue-analytics
role: command
command: revenue-analytics
# 2. Nested command inside an existing family
cli: → wizard audit feature-flags
role: command
parentCommand: audit
command: feature-flags
# 3. Default leaf — what `wizard audit` runs with no subcommand
cli: → wizard audit all
role: command `wizard audit` runs
parentCommand: audit this leaf by default
command: all (and pre-highlights it
default: true in the picker later).
# 4. Skill-only (reachable via `wizard skill <id>`)
cli: → wizard skill <skill-id>
role: skillThe block can live at the group level (defaults for every variant) or
inside a single variant (overrides the group-level defaults). When
role: command and command is omitted, the variant id fills in as the
command name — except for the magic id: all variant, which collapses to
the group key and so requires an explicit command at the group level.
cli: only configures the command shape — the verbs the user types.
Flags and positional arguments live on the wizard side
(ProgramConfig.cliOptions), not here.
default: true marks the leaf that wizard <family> runs when no subcommand
is given. Today, when a family surfaces a single leaf, wizard <family> runs it
directly — the user lands on its intro screen. Once a family surfaces several,
the wizard opens an interactive picker with the default leaf pre-highlighted,
so a single Enter runs it while the others stay visible. Set default on the
leaf a user typing wizard <family> should get by default. At most one leaf per
family should be marked.
The wizard's command surface is curated, not inclusive. Every command is one we're willing to teach in our docs, announce, and support for end users — not just every skill we've authored.
A skill should be promoted to role: command when all of these are
true:
- It's user-facing, not infrastructure. The skill represents a setup,
audit, or migration workflow an end user would reasonably invoke
directly. Internal helpers and scaffolding skills stay at
role: skill. - The name reads naturally.
wizard audit eventsis obvious.wizard do-the-thing-with-eventsis not. If you have to explain the command in the docs before someone could guess what it does, the name needs more work or the skill belongs atrole: skilluntil it does. - It's stable. The command surface is hard to deprecate without breaking
users. If the skill is still iterating on what it does or how it
prompts the agent, ship it as
role: skillfirst. Promote when the shape has held for a release or two. - It plays well with the family it lives in. If
parentCommand: audit, the skill should slot alongside the other audits at the same level of abstraction. Don't put a one-off in an existing family just because the words overlap. - A wizard maintainer has reviewed the role change. Adding to the
command surface is a permanent commitment to that name. Loop in the wizard
docs team / maintainers on PRs that change a skill to
role: command.
When in doubt, ship as role: skill. Promoting from skill to command is
cheap; demoting from command to skill breaks user scripts.
The base path is the same regardless of the skill's CLI role:
- Create
context/skills/<your-skill>/. - Add a
config.yamldeclaringtype,description,variants, etc. See an existing skill (e.g.audit-events,migrate) for the shape. - Add a
description.mdtemplate and anyreferences/*.mdfiles. - If the skill should be a wizard command, add a
cli:block per the schema above. - Run
npm test && npm run build. The build emits the new skill intodist/skills/<your-skill>.zipand lists it in the manifest.
When you've decided your skill meets the role: command criterion:
- Add the
cli:block to the skill'sconfig.yamlwithrole: command, the rightparentCommand(if it nests under an existing family), andcommand. - Confirm
npm run buildemits the entry undercliEntriesinsidedist/skills/skill-menu.jsonwith the rightparentCommand/commandvalues. The wizard picks it up on its next invocation (no wizard release needed). - No wizard PR is needed for skill-backed public commands. If you also need wizard-side hooks (custom outro, content blocks, abort cases), that's a wizard PR — but the CLI registration is handled by the manifest.
| If you're changing… | …PR goes to |
|---|---|
| Skill markdown content | context-mill |
Skill config.yaml (including cli: blocks) |
context-mill |
Skill-generation scripts (scripts/lib/) |
context-mill |
| YARA-X security rules | warlock |
| Wizard runner pipeline, TUI, agent runtime | wizard |
| Wizard-native programs (doctor, mcp, source-maps) | wizard |
| Wizard CLI factories and bin.ts wiring | wizard |
If a change crosses two repos, ship the context-mill PR first so the manifest is published before the wizard tries to consume it.
- Skill schema details:
scripts/lib/skill-generator.js(parseCliBlock,expandSkillGroups, JSDoc typedef for thecli:block) - CLI entries emit:
scripts/lib/build-phases.js(generateCliEntries) - Tests for the cli block parser:
scripts/lib/tests/cli-block.test.js - The wizard's side of the contract: PostHog/wizard CONTRIBUTING.md
Questions: drop a note in #team-docs-and-wizard or open an issue.