diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 046cd338..bbd07bdb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: - name: Lint env var naming conventions run: bash scripts/lint-env-naming.sh - # No individual skill should be larger than 1MB, and the bundle should be less than 5MB. + # No individual skill should be larger than 1MB, and the bundle should be less than 8MB. - name: Check bundle and skill sizes run: | set -e @@ -61,8 +61,8 @@ jobs: bundle_bytes=$(stat -c%s "$BUNDLE" 2>/dev/null || stat -f%z "$BUNDLE") echo "skills-mcp-resources.zip size: ${bundle_bytes} bytes" - if [ "$bundle_bytes" -gt $((5 * 1024 * 1024)) ]; then - echo "::error::skills-mcp-resources.zip is larger than 5MB (current: ${bundle_bytes} bytes)" + if [ "$bundle_bytes" -gt $((8 * 1024 * 1024)) ]; then + echo "::error::skills-mcp-resources.zip is larger than 8MB (current: ${bundle_bytes} bytes)" exit 1 fi diff --git a/context/agents/README.md b/context/agents/README.md new file mode 100644 index 00000000..3450bf84 --- /dev/null +++ b/context/agents/README.md @@ -0,0 +1,51 @@ +# Agent prompts + +One `/.md` per orchestrator task — the WHAT of the task. The folder +is the flow: the wizard's registry is scoped per flow, so audit or migration +agent sets get sibling folders. The frontmatter carries the artifacts the +executor configures the run with: the model, the mini-skills to load (the HOW), +the tools the task may and may not use, and the tasks it depends on. `flow` +repeats the folder name so the file stays self-describing on disk (the build +rejects a mismatch), and one prompt per flow is marked `seed: true`: the +planner that seeds the queue, not an enqueueable task type. + +`model` is a gateway model id, not a runner binding — the wizard's switchboard +routes it, and its CLI/flag rungs may override it per run. `allowedTools` / +`disallowedTools` use the wizard's tool vocabulary; the executing harness maps +them to its native tool names. + +The body is intent only — what to do and what done looks like. The client +injects the basics (project context, how to report, how to surface progress), +so a prompt never restates them. Frontmatter stays flat: scalars and inline +`[a, b]` arrays only, the wizard's parser is deliberately not a YAML engine. + +This README is documentation, not data: the build serves every other `.md` +under a flow folder as an agent prompt. + +Each agent names its model per harness profile — `model_pi`/`effort_pi` for the +gpt (pi) run and `model_sdk`/`effort_sdk` for the anthropic (sdk) run — so the +same flow benchmarks either provider. The mapping is not 1:1: a light task can be +luna on pi but sonnet on sdk. Effort is optional and overrides the model default. + +```markdown +--- +type: example +flow: my-flow +model_pi: openai/gpt-5.6-luna +effort_pi: low +model_sdk: claude-haiku-4-5-20251001 +skills: [] +allowedTools: [Read, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +What this task does, in plain prose. + +## How you know you succeeded + +Plain-text success criteria live here. State what done looks like, and what to +do when the task cannot be completed. +``` diff --git a/context/agents/integration-v2/build.md b/context/agents/integration-v2/build.md new file mode 100644 index 00000000..69bc9e60 --- /dev/null +++ b/context/agents/integration-v2/build.md @@ -0,0 +1,30 @@ +--- +type: build +flow: integration-v2 +label: Build and review the integration +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-4-6 +effort_sdk: high +skills: [integration-v2-build] +allowedTools: [Read, Edit, Glob, Grep, Bash] +disallowedTools: [enqueue_task] +dependsOn: [install, init, identify, error-tracking, capture] +--- + +## Goal + +Bring the integration together and review it: install the dependencies the earlier +steps declared, verify the project builds and lints, then review every change for +convention fit and unintended edits. Until now the steps only edited code and the +manifest — this is where it installs, is checked, and is reviewed as a whole. + +## How you know you succeeded + +The install completes, the integration is in place, and the changes read like the +rest of the codebase with nothing unrelated touched or mangled. If the build or +lint fails only on pre-existing errors you did not introduce, that still counts as +done — note the conflict and finish. Reserve a failed status for when your own +changes break the build. Put a one-line summary of any conflict in your handoff's +`conflict` field and the full detail in what you did; the user sees the one-liner +in the outro and the detail in the report. diff --git a/context/agents/integration-v2/capture.md b/context/agents/integration-v2/capture.md new file mode 100644 index 00000000..08b5cdd2 --- /dev/null +++ b/context/agents/integration-v2/capture.md @@ -0,0 +1,25 @@ +--- +type: capture +flow: integration-v2 +label: Capture events +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-4-6 +effort_sdk: high +skills: [integration-v2-capture] +allowedTools: [Read, Write, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [install, init] +--- + +## Goal + +Decide which events are worth capturing in this app, then instrument them in the +same pass — read each file once, choose the events, and add the capture calls +while the file is already open. + +## How you know you succeeded + +The meaningful user actions across the app have capture calls that fire on the +real action, not on page load, and `.posthog-wizard-cache/.posthog-events.json` +lists the events you instrumented. diff --git a/context/agents/integration-v2/dashboard.md b/context/agents/integration-v2/dashboard.md new file mode 100644 index 00000000..e3844527 --- /dev/null +++ b/context/agents/integration-v2/dashboard.md @@ -0,0 +1,23 @@ +--- +type: dashboard +flow: integration-v2 +label: Create a starter dashboard +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-4-6 +effort_sdk: high +skills: [integration-v2-dashboard, integration-v2-insight, integration-v2-mcp] +allowedTools: [Read, Glob, Grep] +disallowedTools: [Write, Edit, Bash, enqueue_task] +dependsOn: [build] +--- + +## Goal + +Create a starter PostHog dashboard with a few insights built on the events this +integration instruments, using the PostHog MCP. + +## How you know you succeeded + +A dashboard exists with a handful of insights on the captured events, and you hand +off its URL for the report to link. diff --git a/context/agents/integration-v2/error-tracking.md b/context/agents/integration-v2/error-tracking.md new file mode 100644 index 00000000..93a7bcd9 --- /dev/null +++ b/context/agents/integration-v2/error-tracking.md @@ -0,0 +1,31 @@ +--- +type: error-tracking +flow: integration-v2 +label: Add error tracking +model_pi: openai/gpt-5.6-luna +effort_pi: low +model_sdk: claude-sonnet-4-6 +effort_sdk: high +skills: [integration-v2-error-tracking-step] +allowedTools: [Read, Write, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [install, init] +--- + +## Goal + +Set up the framework's single global error boundary so uncaught errors reach +PostHog. One place — the init or app entry — following the docs and the reference +example, not manual capture calls sprinkled across files. The SDK is already +installed and initialized (see the context from previous steps); build on that, +do not re-check it. + +This is an instrument-only task. Do not install dependencies, run the build, run +tests, or start the app — a later `build` step does all verification. Stay inside +this project's directory and edit the one global handler; that is the whole job. + +## How you know you succeeded + +A global error handler forwards exceptions to PostHog. You did not install +anything, run a build or tests, search outside the project, or read through the +whole app or hand-wrap individual components or routes. diff --git a/context/agents/integration-v2/identify.md b/context/agents/integration-v2/identify.md new file mode 100644 index 00000000..02eeb7c1 --- /dev/null +++ b/context/agents/integration-v2/identify.md @@ -0,0 +1,23 @@ +--- +type: identify +flow: integration-v2 +label: Wire user identification +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-4-6 +effort_sdk: high +skills: [integration-v2-identify] +allowedTools: [Read, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [install, init] +--- + +## Goal + +Wire user identification: call PostHog identify wherever the app establishes who +the user is, typically at login and signup. + +## How you know you succeeded + +An identify call fires at the point the user becomes known, with a stable +distinct id. If the app has no auth or user concept, say so and stop. diff --git a/context/agents/integration-v2/init.md b/context/agents/integration-v2/init.md new file mode 100644 index 00000000..3d4a67e3 --- /dev/null +++ b/context/agents/integration-v2/init.md @@ -0,0 +1,24 @@ +--- +type: init +flow: integration-v2 +label: Set up PostHog initialization +model_pi: openai/gpt-5.6-luna +effort_pi: low +model_sdk: claude-haiku-4-5-20251001 +skills: [integration-v2-init] +allowedTools: [Read, Write, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Initialize PostHog: create the framework's init point so the SDK is configured +once and available across the app, set the PostHog environment variables through +the wizard tools, and document those keys in `.env.example` for other developers. + +## How you know you succeeded + +The init file exists and the PostHog env keys are present. Keys live in the env +file, never hardcoded in source, and `.env.example` lists the key names (with +placeholder values) so the next developer knows what to set. diff --git a/context/agents/integration-v2/install.md b/context/agents/integration-v2/install.md new file mode 100644 index 00000000..92f1f8a3 --- /dev/null +++ b/context/agents/integration-v2/install.md @@ -0,0 +1,23 @@ +--- +type: install +flow: integration-v2 +label: Add the PostHog SDK to the manifest +model_pi: openai/gpt-5.6-luna +effort_pi: low +model_sdk: claude-haiku-4-5-20251001 +skills: [integration-v2-install] +allowedTools: [Read, Edit, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [] +--- + +## Goal + +Declare the PostHog SDK in the project's package manifest. Do not run the package +manager and do not build — the build task installs and verifies everything at the +end. + +## How you know you succeeded + +The SDK is listed in the manifest's dependencies at a sensible version. If it is +already declared, leave it and say so. diff --git a/context/agents/integration-v2/integrate-posthog.md b/context/agents/integration-v2/integrate-posthog.md new file mode 100644 index 00000000..c14ce296 --- /dev/null +++ b/context/agents/integration-v2/integrate-posthog.md @@ -0,0 +1,34 @@ +--- +type: integrate-posthog +flow: integration-v2 +seed: true +model_pi: openai/gpt-5.6-terra +effort_pi: medium +model_sdk: claude-sonnet-4-6 +effort_sdk: high +skills: [] +allowedTools: [Read, Glob, Grep] +disallowedTools: [Write, Edit, Bash, complete_task] +dependsOn: [] +--- + +## Goal + +Plan a PostHog integration and seed the task queue with this graph: + +- `install` and `init`, independent of each other. +- `identify`, `capture`, and `error-tracking`, each after `install` and `init` + and independent of one another, so they run in parallel. `capture` decides the + events and instruments them; `error-tracking` wires the single global error + boundary — it needs the SDK installed and initialized, not the events. +- `build`, after `install`, `init`, `identify`, `capture`, and `error-tracking` — + it installs the dependencies and verifies the project builds, lints, and passes + its tests. +- `dashboard`, after `build` — only once the integration is confirmed building, + linting, and testing cleanly. +- `report`, after `dashboard` — it writes the setup report last. + +## How you know you succeeded + +Every task in the graph is queued with that dependency shape, the report last, +and the first task runnable. Keep labels short — the action in a few words. diff --git a/context/agents/integration-v2/report.md b/context/agents/integration-v2/report.md new file mode 100644 index 00000000..7c80deed --- /dev/null +++ b/context/agents/integration-v2/report.md @@ -0,0 +1,27 @@ +--- +type: report +flow: integration-v2 +label: Write the setup report +model_pi: openai/gpt-5.6-luna +effort_pi: low +model_sdk: claude-sonnet-4-6 +effort_sdk: high +skills: [integration-v2-report, integration-v2-notebook, integration-v2-mcp] +allowedTools: [Read, Write, Glob, Grep] +disallowedTools: [enqueue_task] +dependsOn: [dashboard] +--- + +## Goal + +Write the setup report summarizing what this integration did, drawing only on the +run's queue log and event plan in `.posthog-wizard-cache/` (`queue.json` and +`.posthog-events.json`), then mirror it into a shareable PostHog notebook. + +## How you know you succeeded + +`posthog-setup-report.md` exists at the project root: what was installed and +initialized, the events captured, whether identify was wired or skipped, error +tracking added, the dashboard link, any build conflict in full, and the next +steps for the user. The report is also mirrored into a PostHog notebook whose URL +is emitted with the `[NOTEBOOK_URL]` marker. diff --git a/context/docs.yaml b/context/docs.yaml index 1ca6752d..4e29e14e 100644 --- a/context/docs.yaml +++ b/context/docs.yaml @@ -8,6 +8,7 @@ docs: tags: [core, users, identity] urls: - https://posthog.com/docs/getting-started/identify-users.md + - https://posthog.com/docs/product-analytics/identity-resolution.md - id: cloudflare-workers display_name: Cloudflare Workers diff --git a/context/skills/integration-v2/build/config.yaml b/context/skills/integration-v2/build/config.yaml new file mode 100644 index 00000000..61265467 --- /dev/null +++ b/context/skills/integration-v2/build/config.yaml @@ -0,0 +1,11 @@ +type: docs-only +template: description.md +description: Install dependencies and verify the project builds +tags: [orchestrator, build] +variants: + - id: all + display_name: PostHog build step + tags: [orchestrator, build] + docs_urls: [] +cli: + role: internal diff --git a/context/skills/integration-v2/build/description.md b/context/skills/integration-v2/build/description.md new file mode 100644 index 00000000..dc21207c --- /dev/null +++ b/context/skills/integration-v2/build/description.md @@ -0,0 +1,66 @@ +# Install, build, and review + +Install the declared dependencies, verify the project builds, then review the +whole integration. This is the last check before the report — it is where the +run either earns trust or loses it. Be quick and decisive; this step must not +spiral. + +## Install + +Detect the package manager from the project's lockfile and run its install once, +in this project directory. The manifest already declares PostHog. If install is +slow, errors, or can't resolve a package, do not retry with offline flags and do +not poke the package manager's global store or cache — a failed install is a +conflict to report (below), not a problem to fight. + +## Build and verify + +Run the project's build (or typecheck) and lint scripts if they exist +(check the manifest's scripts). Do not run the test suite — the runtime only +permits install, build, typecheck, lint, and format commands, so a test command +is blocked; a build that compiles is sufficient verification. Only errors in the +files this integration changed are yours to fix — a missing import, a wrong call shape. +An error in a file the integration never touched is pre-existing: note it and +move on (below). Do not re-run build, typecheck, or lint hoping a pre-existing +failure clears — it will not, and each re-run is slow. + +## Review the integration + +Before you finish, review everything the earlier steps changed — the queue log's +handoffs name the files each one touched. This catches the things users complain +about most: + +- **It builds.** A clean build or typecheck is the goal. If it fails, say so + plainly in the handoff `conflict` — the exact failing command and why — and, if + the failure is pre-existing, that the integration did not cause it. A silent or + vague build failure is the worst outcome; a clearly-reported one is fine. +- **It matches the codebase.** The added PostHog code must read like the code + around it — import style, naming, quotes, indentation, and the framework's + idioms. Fix anything that looks foreign to the file it lives in. +- **Nothing was mangled.** Confirm each touched file still has its original code + intact next to the PostHog additions, and that no file unrelated to the + integration was edited. If a step deleted or rewrote code it shouldn't have, or + reformatted an untouched region, restore it. Flag anything you can't safely fix + in `conflict`. + +## Flag out-of-scope conflicts and move on + +Work only inside this project's own directory. Never read, search, or write +anywhere outside it — not other repos, not the OS, not the package manager's +global store or cache. + +A build or install you can't cleanly finish is a perfectly acceptable outcome — +**as long as you report it.** If you hit a conflict that is excessively difficult +or outside the scope of this integration — a dependency clash, a pre-existing +build break, an install that won't resolve, an environment issue unrelated to the +PostHog code — stop fighting it: put a one-line summary in your handoff `conflict` +field and the full detail in `did`, then complete the task. The user sees it in +the outro and the report; reporting it and moving on is the right outcome. + +Complete this task with status **done** whenever the integration itself is in +place — even if the build or typecheck still fails on pre-existing errors in files +you never touched. Note the pre-existing failure in `conflict` and move on. Only +use status **failed** when your own integration changes are what break the build +and you cannot resolve them. A `failed` status stops later steps that depend on +this one (the dashboard and report), so do not fail the task over breakage the +integration did not cause. diff --git a/context/skills/integration-v2/capture/config.yaml b/context/skills/integration-v2/capture/config.yaml new file mode 100644 index 00000000..22ad9dd6 --- /dev/null +++ b/context/skills/integration-v2/capture/config.yaml @@ -0,0 +1,9 @@ +# Orchestrator step-skill: fetched by task agents, never surfaced as a command. +# The framework matrix is borrowed from the canonical integration group. +type: docs-only +template: description.md +description: Instrument the planned events with PostHog capture calls +tags: [orchestrator, capture] +cli: + role: internal +variants_from: integration diff --git a/context/skills/integration-v2/capture/description.md b/context/skills/integration-v2/capture/description.md new file mode 100644 index 00000000..b0a7d1b7 --- /dev/null +++ b/context/skills/integration-v2/capture/description.md @@ -0,0 +1,42 @@ +# Plan and capture events + +Decide which custom events are worth capturing, then instrument them — in one +pass, reading each file once. + +## Choose and record + +From the project's files, select between 10 and 15 that might have business value +for event tracking — especially conversion and churn events. Read them. Track +actions, not pageviews (autocapture covers those). Server-side events are required +if there is instrumentable server-side code (API routes, server actions): +payment/checkout completion, webhook handlers, and auth endpoints. + +First scan for capture calls the project already makes, and note how their event +names are formatted. Event names, property names, and feature flag keys are an +analytics contract: reuse the existing names and follow the patterns already in +the project rather than inventing parallel ones, and don't duplicate events that +already exist. + +Write the chosen events to `.posthog-wizard-cache/.posthog-events.json` — a JSON +array of `{ event, description, file }`, one entry per event. That cache directory +is the wizard's, already created for the run; write the plan there, not at the +project root. Write it before you start editing: it drives the event-plan view, +and it is the source the report reads later. + +## Instrument + +For each event call the SDK's capture method on the real user action — the click +or submit handler, the server action — not on render or page load. Use clear +`lower_snake_case` names and useful properties. Edit each file while it is already +open. + +Server-side, use the authenticated user's id as the distinct id. For a genuinely +unauthenticated action, emit a personless event — never fabricate a placeholder +id like `'anonymous'`, which collapses every anonymous user into one person and +corrupts the data. + +Leave `.posthog-wizard-cache/.posthog-events.json` in place for the report. + +## Reference + +{references} diff --git a/context/skills/integration-v2/dashboard/config.yaml b/context/skills/integration-v2/dashboard/config.yaml new file mode 100644 index 00000000..f5cc272e --- /dev/null +++ b/context/skills/integration-v2/dashboard/config.yaml @@ -0,0 +1,11 @@ +type: docs-only +template: description.md +description: Create a starter PostHog dashboard from the captured events +tags: [orchestrator, dashboard] +variants: + - id: all + display_name: PostHog dashboard step + tags: [orchestrator, dashboard] + docs_urls: [] +cli: + role: internal diff --git a/context/skills/integration-v2/dashboard/description.md b/context/skills/integration-v2/dashboard/description.md new file mode 100644 index 00000000..1c5d1456 --- /dev/null +++ b/context/skills/integration-v2/dashboard/description.md @@ -0,0 +1,28 @@ +# Create a starter dashboard + +Create a live PostHog dashboard named `Analytics basics (wizard)` to hold the +views for the events this integration instrumented. Keep the `(wizard)` tag with +that exact casing so a search for `(wizard)` surfaces every wizard-created +artifact. + +Everything here goes through `posthog_exec` (`call `). + +## Create the dashboard + +Create the parent dashboard first with `dashboard-create`, and capture its +returned `id` — the insight step attaches every insight to it via +`dashboards: []`: + +```json +{ "name": "Analytics basics (wizard)", "description": "Key views for the events instrumented by the PostHog wizard.", "tags": ["wizard"] } +``` + +Then create the insights (see the insight step) and attach each one to this +dashboard's `id`. + +## Hand off the dashboard + +Emit the dashboard URL on its own line in your final message with this exact +marker so the wizard surfaces it: `[DASHBOARD_URL] `. A URL only +in prose, without the marker, is dropped. Also record the dashboard URL in your +handoff so the report step can link it. diff --git a/context/skills/integration-v2/error-tracking-step/config.yaml b/context/skills/integration-v2/error-tracking-step/config.yaml new file mode 100644 index 00000000..9e7dc25a --- /dev/null +++ b/context/skills/integration-v2/error-tracking-step/config.yaml @@ -0,0 +1,9 @@ +# Orchestrator step-skill: fetched by task agents, never surfaced as a command. +# The framework matrix is borrowed from the canonical integration group. +type: docs-only +template: description.md +description: Capture exceptions with PostHog around critical flows +tags: [orchestrator, error-tracking] +cli: + role: internal +variants_from: integration diff --git a/context/skills/integration-v2/error-tracking-step/description.md b/context/skills/integration-v2/error-tracking-step/description.md new file mode 100644 index 00000000..352bbea7 --- /dev/null +++ b/context/skills/integration-v2/error-tracking-step/description.md @@ -0,0 +1,17 @@ +# Add error tracking + +Set up the framework's GLOBAL error boundary so uncaught errors and exceptions +reach PostHog — one handler, not hand-wrapped across files. + +Follow the framework's own mechanism for a global error handler, using the +reference example and the docs for the exact pattern. Find the init or app entry, +add the handler there. Do not read through the whole app or wrap individual +components or routes by hand. + +If the app already has natural exception boundaries — a server-side API error +handler, a critical flow with its own try/catch — capturing there too is worth a +single edit. The global handler is the floor, not the only place errors matter. + +## Reference + +{references} diff --git a/context/skills/integration-v2/identify/config.yaml b/context/skills/integration-v2/identify/config.yaml new file mode 100644 index 00000000..6def463a --- /dev/null +++ b/context/skills/integration-v2/identify/config.yaml @@ -0,0 +1,13 @@ +type: docs-only +template: description.md +description: Identify users at login and signup +tags: [orchestrator, identify] +variants: + - id: all + display_name: PostHog identify step + tags: [orchestrator, identify] + docs_urls: + - https://posthog.com/docs/getting-started/identify-users.md + - https://posthog.com/docs/product-analytics/identity-resolution.md +cli: + role: internal diff --git a/context/skills/integration-v2/identify/description.md b/context/skills/integration-v2/identify/description.md new file mode 100644 index 00000000..c5a002bc --- /dev/null +++ b/context/skills/integration-v2/identify/description.md @@ -0,0 +1,22 @@ +# Identify users + +Call the SDK's identify method at the moment the app learns who the user is — +typically on login and signup success. + +- Use a stable unique id as the distinct id (the user id from your auth), not an + email or display name. +- Attach useful person properties (email, name, plan). +- Reset the session on logout, where the SDK supports it, so the next user starts + clean. + +Find the auth flow first: login and signup handlers, session callbacks. If the +app has no concept of a user, there is nothing to identify — report that and stop. + +If the app has both a client and a server, keep them on the same person: forward +the client's distinct id and session id to the backend (the +`X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` request headers are the usual +carrier) so server-side events stitch to the same user rather than splitting off. + +## Reference + +{references} diff --git a/context/skills/integration-v2/init/config.yaml b/context/skills/integration-v2/init/config.yaml new file mode 100644 index 00000000..9392ae80 --- /dev/null +++ b/context/skills/integration-v2/init/config.yaml @@ -0,0 +1,9 @@ +# Orchestrator step-skill: fetched by task agents, never surfaced as a command. +# The framework matrix is borrowed from the canonical integration group. +type: docs-only +template: description.md +description: Initialize PostHog and set its environment variables +tags: [orchestrator, init] +cli: + role: internal +variants_from: integration diff --git a/context/skills/integration-v2/init/description.md b/context/skills/integration-v2/init/description.md new file mode 100644 index 00000000..bf283ce5 --- /dev/null +++ b/context/skills/integration-v2/init/description.md @@ -0,0 +1,28 @@ +# Initialize PostHog + +Set up PostHog so the SDK is configured once and available across the app. + +## Environment variables + +Set the PostHog keys through the wizard tools (`set_env_values`), never hardcoded. +Use the framework's public env-var convention so the client can read them. + +- the public project token +- the PostHog host + +Then document these keys for other developers: add them to `.env.example` (create +it if the project has none), with the real names and empty or placeholder values — +never the real secret. This file is committed, so the next developer knows which +keys to set. The example file is the only `.env*` you may write directly; the +actual `.env` still goes through `set_env_values`. + +## Init point + +Create the framework's single initialization point that runs once on the client, +following the reference example and the docs for the right pattern. Read the +existing provider or entry file before editing, and add PostHog alongside what is +already there rather than replacing it. + +## Reference + +{references} diff --git a/context/skills/integration-v2/insight/config.yaml b/context/skills/integration-v2/insight/config.yaml new file mode 100644 index 00000000..c367b9e2 --- /dev/null +++ b/context/skills/integration-v2/insight/config.yaml @@ -0,0 +1,11 @@ +type: docs-only +template: description.md +description: Create PostHog insights (trends, funnels) attached to a dashboard +tags: [orchestrator, insight] +variants: + - id: all + display_name: PostHog insight step + tags: [orchestrator, insight] + docs_urls: [] +cli: + role: internal diff --git a/context/skills/integration-v2/insight/description.md b/context/skills/integration-v2/insight/description.md new file mode 100644 index 00000000..d9c6a2ee --- /dev/null +++ b/context/skills/integration-v2/insight/description.md @@ -0,0 +1,56 @@ +# Create insights + +Populate the dashboard with up to five insights from the events this integration +instrumented — lead with the business-critical views: conversion funnels, +activation, and churn signals. Use the exact event names from the capture step's +handoff; never invent events that were not instrumented. Keep each name tagged +`(wizard)` with that exact casing. + +Every call goes through `posthog_exec` (`call insight-create `). Attach each +insight to the dashboard by passing its id in `dashboards: []`. + +Use these known-good query shapes — they are verified against the MCP schema, and +the common variations around them are rejected. + +A trends insight with a breakdown (breakdowns go in `breakdownFilter.breakdowns`, +an array — there is NO top-level `breakdown` field on `TrendsQuery`): + +```json +{ + "name": "Signups by plan (wizard)", + "dashboards": [], + "query": { "kind": "InsightVizNode", "source": { + "kind": "TrendsQuery", + "series": [{ "kind": "EventsNode", "event": "user_signed_up", "math": "total" }], + "interval": "day", + "dateRange": { "date_from": "-30d" }, + "breakdownFilter": { "breakdowns": [{ "type": "event", "property": "plan" }] }, + "trendsFilter": { "display": "ActionsBar" } + }} +} +``` + +A conversion funnel (window fields are camelCase and live INSIDE `funnelsFilter`, +not at the top level of `FunnelsQuery`, and not snake_case): + +```json +{ + "name": "Signup funnel (wizard)", + "dashboards": [], + "query": { "kind": "InsightVizNode", "source": { + "kind": "FunnelsQuery", + "series": [ + { "kind": "EventsNode", "event": "page_viewed" }, + { "kind": "EventsNode", "event": "user_signed_up" } + ], + "dateRange": { "date_from": "-30d" }, + "funnelsFilter": { "funnelVizType": "steps", "funnelOrderType": "ordered", "funnelWindowInterval": 14, "funnelWindowIntervalUnit": "day" } + }} +} +``` + +Valid `trendsFilter.display` values: `ActionsLineGraph`, `ActionsBar`, +`ActionsAreaGraph`, `ActionsPie`, `ActionsStackedBar`, `BoldNumber`, +`ActionsTable`. Names like `ActionsBarChart`/`ActionsBarGraph` are rejected. If an +insight call is rejected, fix the payload against these examples rather than +retrying variations. diff --git a/context/skills/integration-v2/install/config.yaml b/context/skills/integration-v2/install/config.yaml new file mode 100644 index 00000000..a27929d2 --- /dev/null +++ b/context/skills/integration-v2/install/config.yaml @@ -0,0 +1,9 @@ +# Orchestrator step-skill: fetched by task agents, never surfaced as a command. +# The framework matrix is borrowed from the canonical integration group. +type: docs-only +template: description.md +description: Install the PostHog SDK with the project's package manager +tags: [orchestrator, install] +cli: + role: internal +variants_from: integration diff --git a/context/skills/integration-v2/install/description.md b/context/skills/integration-v2/install/description.md new file mode 100644 index 00000000..f914b263 --- /dev/null +++ b/context/skills/integration-v2/install/description.md @@ -0,0 +1,18 @@ +# Add the PostHog SDK to the manifest + +Declare PostHog in the project's package manifest directly. Do not run the package +manager, and do not build — the build task installs and verifies at the end. +Adding it to the manifest now keeps this step fast and batches the real install +into one place. + +Add the PostHog library appropriate for the app — the client library, plus the +server library if the app has server-side code that should send events. Use the +docs and the reference example to pick the right package and a current version +range, and match the style of the dependencies already in the manifest. + +Read the manifest first. If the dependency is already declared, leave it as is and +say so. Edit only the manifest — no lockfile, no install command. + +## Reference + +{references} diff --git a/context/skills/integration-v2/mcp/config.yaml b/context/skills/integration-v2/mcp/config.yaml new file mode 100644 index 00000000..2419adb1 --- /dev/null +++ b/context/skills/integration-v2/mcp/config.yaml @@ -0,0 +1,11 @@ +type: docs-only +template: description.md +description: How to call PostHog MCP tools (discover, inspect, then call) +tags: [orchestrator, mcp] +variants: + - id: all + display_name: PostHog MCP step + tags: [orchestrator, mcp] + docs_urls: [] +cli: + role: internal diff --git a/context/skills/integration-v2/mcp/description.md b/context/skills/integration-v2/mcp/description.md new file mode 100644 index 00000000..5c472fb0 --- /dev/null +++ b/context/skills/integration-v2/mcp/description.md @@ -0,0 +1,7 @@ +# Call PostHog MCP tools + +Any step that creates something in PostHog — a dashboard, an insight, a notebook — +does it through the PostHog MCP. Discover and inspect before you call; never guess +a tool name or the shape of its input. + +{{> mcp-tool-calling}} diff --git a/context/skills/integration-v2/notebook/config.yaml b/context/skills/integration-v2/notebook/config.yaml new file mode 100644 index 00000000..35504077 --- /dev/null +++ b/context/skills/integration-v2/notebook/config.yaml @@ -0,0 +1,11 @@ +type: docs-only +template: description.md +description: Mirror the setup report into a shareable PostHog notebook +tags: [orchestrator, notebook] +variants: + - id: all + display_name: PostHog notebook step + tags: [orchestrator, notebook] + docs_urls: [] +cli: + role: internal diff --git a/context/skills/integration-v2/notebook/description.md b/context/skills/integration-v2/notebook/description.md new file mode 100644 index 00000000..bbe41b83 --- /dev/null +++ b/context/skills/integration-v2/notebook/description.md @@ -0,0 +1,31 @@ +# Mirror the report into a PostHog notebook + +Once `posthog-setup-report.md` exists, mirror it into a shareable PostHog notebook +so the user has an in-app copy to link and comment on. The notebook is an extra +copy, not a replacement — keep the local report file in place. + +First `Read` the finished `posthog-setup-report.md` (don't reconstruct it from +memory, and don't read it before the report step has written it). Then create the +notebook in a single `notebooks-create` call through `posthog_exec` — that exact +tool name, no tool search — with a `title` and `content` that wraps the report in +one `ph-markdown-notebook` node: + +```json +{ + "title": "PostHog setup (wizard) – ", + "content": { "type": "doc", "content": [ + { "type": "ph-markdown-notebook", "attrs": { "nodeId": "markdown-notebook-v2", "markdown": "" } } + ]} +} +``` + +The report goes in verbatim, but `markdown` is a JSON string field: build the +whole argument as one valid JSON value so the report's newlines and quotes are +escaped as normal JSON string encoding (`\n`, `\"`, `\\`). Never paste raw +multi-line text into the JSON, and never trim or summarize the report to make it +parse — if the call is rejected, the fix is always the escaping. + +Take the `short_id` from the response, build the URL as +`/project//notebooks/`, and emit it on its own line in +your final message with this exact marker so the wizard surfaces it: +`[NOTEBOOK_URL] `. A URL only in prose, without the marker, is dropped. diff --git a/context/skills/integration-v2/report/config.yaml b/context/skills/integration-v2/report/config.yaml new file mode 100644 index 00000000..4215277d --- /dev/null +++ b/context/skills/integration-v2/report/config.yaml @@ -0,0 +1,11 @@ +type: docs-only +template: description.md +description: Write the PostHog setup report from the run's handoffs +tags: [orchestrator, report] +variants: + - id: all + display_name: PostHog report step + tags: [orchestrator, report] + docs_urls: [] +cli: + role: internal diff --git a/context/skills/integration-v2/report/description.md b/context/skills/integration-v2/report/description.md new file mode 100644 index 00000000..80a45c62 --- /dev/null +++ b/context/skills/integration-v2/report/description.md @@ -0,0 +1,47 @@ +# Write the setup report + +Write `posthog-setup-report.md` at the project root summarizing the integration. +When the file doesn't exist, write it directly — don't attempt a read first. If a +previous run left one behind, `Read` it, then replace it wholesale (harnesses +refuse to overwrite a file that wasn't read; nothing in the old report is worth +merging). +Draw on two sources only: + +- the run's queue log — `.posthog-wizard-cache/queue.json`, which holds each + task's handoff inline — for what each step did, whether identify was wired + or skipped, and any build conflict; +- `.posthog-wizard-cache/.posthog-events.json` — the events that were instrumented. + If that file is missing or empty, reconstruct the list instead of dropping the + table: grep the changed files for `capture(` calls and read the capture step's + handoff in `queue.json`. + +Include: + +- A one-line summary of what was set up. +- What was installed and how PostHog was initialized. +- The events instrumented, as a table: event name, what it measures, and the file + (from `.posthog-wizard-cache/.posthog-events.json`). +- Whether user identification was wired or skipped, and why. +- The error tracking added. +- The dashboard link. +- Any build conflict, in full. +- Clear next steps for the user. + +End with a "Before you merge" checklist as GitHub-style checkboxes (`- [ ] …`), +including only the items that apply to what was set up — judge each against the +code changed this run and drop the ones that don't fit: + +- Always: run a full production build (the wizard only verified the files it + touched) and fix any lint or type errors the generated code introduced. +- Always: run the test suite — instrumented call sites may need updated mocks or + fixtures. +- If env vars were added: their exact names are in `.env.example` and any + monorepo/bootstrap scripts, and set in the deploy environments, not just locally. +- If the app ships minified browser bundles: wire source-map upload into CI so + production stack traces de-minify — call it out with the docs link. +- If LLM analytics was set up: trigger the instrumented call path and confirm + `$ai_generation` events appear in PostHog. +- If auth exists and identify was wired: the returning-visitor path also calls + identify, so returning sessions don't fragment onto anonymous distinct IDs. + +Keep it skimmable. This is the artifact the user opens after the run. diff --git a/context/skills/integration/config.yaml b/context/skills/integration/config.yaml index c518d0d1..d388ee82 100644 --- a/context/skills/integration/config.yaml +++ b/context/skills/integration/config.yaml @@ -1,4 +1,5 @@ # Integration skills - example-based (code + docs) +# `framework` = the detection id a variant serves; `default: true` marks the family default. Both are emitted into skill-menu.json. type: skill template: description.md description: PostHog integration for {display_name} applications @@ -6,6 +7,8 @@ shared_docs: - https://posthog.com/docs/getting-started/identify-users.md variants: - id: nextjs-app-router + framework: nextjs + default: true example_paths: example-apps/next-app-router display_name: Next.js App Router tags: [nextjs, react, ssr, app-router, javascript, javascript_web, javascript_node] @@ -13,6 +16,7 @@ variants: - https://posthog.com/docs/libraries/next-js.md - id: nextjs-pages-router + framework: nextjs example_paths: example-apps/next-pages-router display_name: Next.js Pages Router tags: [nextjs, react, ssr, pages-router, javascript, javascript_web, javascript_node] @@ -20,6 +24,8 @@ variants: - https://posthog.com/docs/libraries/next-js.md - id: react-react-router-6 + framework: react-router + default: true example_paths: example-apps/react-react-router-6 display_name: React Router v6 tags: [react, react-router, v6, spa, javascript, javascript_web] @@ -27,6 +33,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v6.md - id: react-react-router-7-framework + framework: react-router example_paths: example-apps/react-react-router-7-framework display_name: React Router v7 - Framework mode tags: [react, react-router, v7, framework, ssr, javascript, javascript_node, javascript_web] @@ -34,6 +41,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v7-framework-mode.md - id: react-react-router-7-data + framework: react-router example_paths: example-apps/react-react-router-7-data display_name: React Router v7 - Data mode tags: [react, react-router, v7, data, spa, javascript, javascript_web] @@ -41,6 +49,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v7-data-mode.md - id: react-react-router-7-declarative + framework: react-router example_paths: example-apps/react-react-router-7-declarative display_name: React Router v7 - Declarative mode tags: [react, react-router, v7, declarative, spa, javascript, javascript_web] @@ -56,6 +65,8 @@ variants: - https://posthog.com/docs/libraries/react.md - id: nuxt-3-6 + framework: nuxt + default: true example_paths: example-apps/nuxt-3-6 display_name: Nuxt 3.6 description: PostHog integration for Nuxt versions 3.0 to 3.6 @@ -64,6 +75,7 @@ variants: - https://posthog.com/docs/libraries/nuxt-js-3-6.md - id: nuxt-4 + framework: nuxt example_paths: example-apps/nuxt-4 display_name: Nuxt 4 description: PostHog integration for Nuxt 4 applications @@ -72,6 +84,7 @@ variants: - https://posthog.com/docs/libraries/nuxt-js.md - id: vue-3 + framework: vue example_paths: example-apps/vue-3 display_name: Vue 3 description: PostHog integration for Vue 3 applications @@ -80,6 +93,7 @@ variants: - https://posthog.com/docs/libraries/vue-js.md - id: django + framework: django example_paths: example-apps/django display_name: Django tags: [django, python] @@ -87,6 +101,7 @@ variants: - https://posthog.com/docs/libraries/django.md - id: flask + framework: flask example_paths: example-apps/flask display_name: Flask tags: [flask, python] @@ -94,6 +109,7 @@ variants: - https://posthog.com/docs/libraries/flask.md - id: fastapi + framework: fastapi example_paths: example-apps/fastapi display_name: FastAPI description: PostHog integration for FastAPI applications @@ -102,6 +118,7 @@ variants: - https://posthog.com/docs/libraries/python.md - id: react-tanstack-router-file-based + framework: tanstack-router example_paths: example-apps/react-tanstack-router-file-based display_name: React with TanStack Router (file-based) description: PostHog integration for React applications using TanStack Router with file-based routing @@ -110,6 +127,8 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: react-tanstack-router-code-based + framework: tanstack-router + default: true example_paths: example-apps/react-tanstack-router-code-based display_name: React with TanStack Router (code-based) description: PostHog integration for React applications using TanStack Router with code-based routing @@ -118,6 +137,7 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: tanstack-start + framework: tanstack-start example_paths: example-apps/tanstack-start display_name: TanStack Start description: PostHog integration for TanStack Start full-stack applications @@ -126,6 +146,7 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: laravel + framework: laravel example_paths: example-apps/laravel display_name: Laravel tags: [laravel, php] @@ -141,6 +162,7 @@ variants: - https://posthog.com/docs/libraries/php.md - id: ruby-on-rails + framework: rails example_paths: example-apps/ruby-on-rails display_name: Ruby on Rails tags: [ruby-on-rails, ruby] @@ -149,6 +171,7 @@ variants: - https://posthog.com/docs/libraries/ruby.md - id: android + framework: android example_paths: example-apps/android display_name: Android description: PostHog integration for Android applications @@ -157,6 +180,7 @@ variants: - https://posthog.com/docs/libraries/android.md - id: sveltekit + framework: sveltekit example_paths: example-apps/sveltekit display_name: SvelteKit description: PostHog integration for SvelteKit applications @@ -166,6 +190,7 @@ variants: # Language fallback skills, used when no specific framework is detected - id: python + framework: python example_paths: example-apps/python display_name: Python description: PostHog integration for any Python application using the Python SDK @@ -175,6 +200,7 @@ variants: - https://posthog.com/docs/references/posthog-python.md - id: javascript_node + framework: javascript_node example_path: example-apps/javascript-node display_name: JavaScript Node description: PostHog integration for server-side Node.js applications using posthog-node @@ -184,6 +210,7 @@ variants: - https://posthog.com/docs/references/posthog-node.md - id: javascript_web + framework: javascript_web example_path: example-apps/javascript-web display_name: JavaScript Web description: PostHog integration for client-side web JavaScript applications using posthog-js @@ -193,6 +220,7 @@ variants: - https://posthog.com/docs/references/posthog-js.md - id: ruby + framework: ruby example_paths: example-apps/ruby display_name: Ruby description: PostHog integration for any Ruby application using the Ruby SDK @@ -217,6 +245,7 @@ variants: - https://posthog.com/docs/libraries/go.md - id: swift + framework: swift example_paths: - example-apps/swift - example-apps/swift-xcodegen @@ -245,6 +274,8 @@ variants: - https://posthog.com/docs/libraries/kmp.md - id: react-native + framework: react-native + default: true example_paths: example-apps/react-native display_name: React Native description: PostHog integration for React Native applications @@ -253,6 +284,7 @@ variants: - https://posthog.com/docs/libraries/react-native.md - id: expo + framework: react-native example_paths: example-apps/expo display_name: Expo description: PostHog integration for Expo applications @@ -261,6 +293,7 @@ variants: - https://posthog.com/docs/libraries/react-native.md - id: astro-static + framework: astro example_paths: example-apps/astro-static display_name: Astro (Static) description: PostHog integration for static Astro sites using SSG @@ -269,6 +302,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-view-transitions + framework: astro example_paths: example-apps/astro-view-transitions display_name: Astro (View Transitions) description: PostHog integration for Astro with ClientRouter view transitions @@ -277,6 +311,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-ssr + framework: astro example_paths: example-apps/astro-ssr display_name: Astro (SSR) description: PostHog integration for server-rendered Astro applications with API routes @@ -285,6 +320,8 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-hybrid + framework: astro + default: true example_paths: example-apps/astro-hybrid display_name: Astro (Hybrid) description: PostHog integration for Astro hybrid rendering with both static and server-rendered pages @@ -293,6 +330,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: angular + framework: angular example_paths: example-apps/angular display_name: Angular description: PostHog integration for Angular applications diff --git a/scripts/build.js b/scripts/build.js index 28809b68..e8a4f7aa 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -12,6 +12,7 @@ import fs from 'fs'; import path from 'path'; import { generateAllSkills, fetchDoc } from './lib/skill-generator.js'; +import { buildAgents } from './lib/agent-generator.js'; import { generateMarketplace } from './lib/marketplace-generator.js'; import { loadDocsConfig, @@ -103,6 +104,15 @@ async function main() { const skillMenu = JSON.parse(fs.readFileSync(path.join(skillsDir, 'skill-menu.json'), 'utf8')); console.log(` ✓ skill-menu.json (${Object.keys(skillMenu.categories).length} categories, ${skills.length} skills)`); + console.log('\nBuilding agent prompts...'); + const agentsResult = buildAgents({ + configDir, + distDir, + baseUrl: process.env.AGENTS_BASE_URL, + version: BUILD_VERSION, + }); + console.log(` ✓ ${agentsResult.count} agent prompt(s) → dist/agents/ + agent-menu.json`); + const releaseAssetDocs = docEntries.filter(d => d.release_asset); if (releaseAssetDocs.length > 0) { console.log('\nWriting release-asset docs...'); diff --git a/scripts/dev-server.js b/scripts/dev-server.js index ee99c60d..d69cfa83 100644 --- a/scripts/dev-server.js +++ b/scripts/dev-server.js @@ -30,6 +30,7 @@ import { spawn } from 'child_process'; import chokidar from 'chokidar'; import { loadAndExpandSkills } from './lib/skill-generator.js'; +import { buildAgents } from './lib/agent-generator.js'; import { partialRebuild, loadDocContentsFromManifest, @@ -50,13 +51,18 @@ const configDir = path.join(repoRoot, 'context'); const distDir = path.join(repoRoot, 'dist'); const skillsDir = path.join(distDir, 'skills'); const skillsSourceDir = path.join(configDir, 'skills'); +const agentsSourceDir = path.join(configDir, 'agents'); +const agentsDir = path.join(distDir, 'agents'); const exampleAppsDir = path.join(repoRoot, 'example-apps'); const localSkillsUrl = `http://localhost:${PORT}/skills`; +const localAgentsUrl = `http://localhost:${PORT}/agents`; -// `generateManifest` reads SKILLS_BASE_URL from process.env. Partial rebuilds -// run in this process, so set it here too — the subprocess inherits it. +// `generateManifest` reads SKILLS_BASE_URL from process.env, and the agent build +// reads AGENTS_BASE_URL. Partial rebuilds run in this process, so set both here — +// the build subprocess inherits them. process.env.SKILLS_BASE_URL = localSkillsUrl; +process.env.AGENTS_BASE_URL = localAgentsUrl; // --- state --- @@ -184,6 +190,25 @@ async function drainQueue() { // --- watcher --- function handleEvent(event, absPath) { + // Agent prompts are decoupled from the skill incremental machinery. A change + // under context/agents/ just re-copies them wholesale — cheap. + if (absPath.startsWith(agentsSourceDir)) { + try { + const { count } = buildAgents({ + configDir, + distDir, + baseUrl: localAgentsUrl, + version: BUILD_VERSION, + }); + console.log( + `📝 ${event}: ${path.relative(repoRoot, absPath)} → rebuilt ${count} agent prompt(s)`, + ); + } catch (err) { + console.error(`❌ Agent rebuild failed: ${err.message}`); + } + return; + } + const decision = routeChange({ event, absPath, @@ -209,7 +234,7 @@ function handleEvent(event, absPath) { function setupWatcher() { const sep = path.sep; - const watcher = chokidar.watch([skillsSourceDir, exampleAppsDir], { + const watcher = chokidar.watch([skillsSourceDir, agentsSourceDir, exampleAppsDir], { ignoreInitial: true, persistent: true, followSymlinks: false, @@ -221,6 +246,7 @@ function setupWatcher() { console.log('\n👀 Watching:'); console.log(` 📁 ${path.relative(repoRoot, skillsSourceDir)}`); + console.log(` 📁 ${path.relative(repoRoot, agentsSourceDir)}`); console.log(` 📁 ${path.relative(repoRoot, exampleAppsDir)}`); return watcher; @@ -268,6 +294,17 @@ function createServer() { return; } + const agentMatch = req.url?.match(/^\/agents\/([\w-]+)\/([\w-]+\.md)$/); + if (agentMatch) { + serveFile(res, path.join(agentsDir, agentMatch[1], agentMatch[2]), 'text/markdown; charset=utf-8'); + return; + } + + if (req.url === '/agent-menu.json') { + serveFile(res, path.join(agentsDir, 'agent-menu.json'), 'application/json'); + return; + } + if (req.url === '/skills-mcp-resources.zip' || req.url === '/') { serveFile( res, @@ -279,7 +316,7 @@ function createServer() { } res.writeHead(404, { 'Content-Type': 'text/plain', ...NO_CACHE_HEADERS }); - res.end('Not found. Available endpoints:\n /skill-menu.json\n /skills-mcp-resources.zip\n /skills/{id}.zip'); + res.end('Not found. Available endpoints:\n /skill-menu.json\n /skills-mcp-resources.zip\n /skills/{id}.zip\n /agent-menu.json\n /agents/{flow}/{type}.md'); }); server.listen(PORT, () => { @@ -287,6 +324,8 @@ function createServer() { console.log(`\n📍 Skills bundle: http://localhost:${PORT}/skills-mcp-resources.zip`); console.log(`📍 Individual skill: http://localhost:${PORT}/skills/{id}.zip`); console.log(`📋 Skills menu: http://localhost:${PORT}/skill-menu.json`); + console.log(`🤖 Agent prompt: http://localhost:${PORT}/agents/{flow}/{type}.md`); + console.log(`📋 Agents menu: http://localhost:${PORT}/agent-menu.json`); }); return server; diff --git a/scripts/lib/agent-generator.js b/scripts/lib/agent-generator.js new file mode 100644 index 00000000..93e8aac3 --- /dev/null +++ b/scripts/lib/agent-generator.js @@ -0,0 +1,114 @@ +/** + * Agent-prompt content type — the WHAT of the orchestrator runner. + * + * An agent prompt is one markdown file per task type, grouped in a folder per + * flow. Its frontmatter carries the artifacts the executor configures the run + * with (model, skills, tools, dependsOn); its body is the instruction the task + * agent reads. Unlike skills, agent prompts are self-contained single files + * with no references/, so they are served as raw markdown rather than zipped. + * The wizard parses the frontmatter when it loads a task by type. + * + * Source: context/agents//.md + * Output: dist/agents//.md + dist/agents/agent-menu.json + */ + +import fs from 'fs'; +import path from 'path'; + +const DEFAULT_AGENTS_BASE_URL = + 'https://github.com/PostHog/context-mill/releases/latest/download/agents'; + +/** + * The agent prompts available in source: one { flow, id } per + * `/.md`. README.md files are author documentation, not served + * prompts. A markdown file directly under agents/ is a layout error — prompts + * are always flow-scoped. + */ +export function loadAgentEntries(agentsSourceDir) { + if (!fs.existsSync(agentsSourceDir)) return []; + const entries = []; + for (const dirent of fs.readdirSync(agentsSourceDir, { withFileTypes: true })) { + if (dirent.name === 'README.md') continue; + if (dirent.isFile() && dirent.name.endsWith('.md')) { + throw new Error( + `Agent prompt "${dirent.name}" sits directly under agents/ — move it into a flow folder (agents//${dirent.name})`, + ); + } + if (!dirent.isDirectory()) continue; + const flow = dirent.name; + const flowDir = path.join(agentsSourceDir, flow); + for (const file of fs.readdirSync(flowDir)) { + if (!file.endsWith('.md') || file === 'README.md') continue; + entries.push({ flow, id: file.slice(0, -'.md'.length) }); + } + } + return entries.sort((a, b) => a.flow.localeCompare(b.flow) || a.id.localeCompare(b.id)); +} + +/** + * A prompt's frontmatter `flow:` must be present and match its folder — + * consumers filter by it, so a missing key would silently drop the prompt. + */ +function assertFlowMatches(sourcePath, flow) { + const text = fs.readFileSync(sourcePath, 'utf8'); + const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); + const declared = match?.[1].match(/^flow:\s*(.+?)\s*$/m)?.[1]; + if (!declared) { + throw new Error( + `Agent prompt ${sourcePath} is missing the "flow:" frontmatter key — declare flow: ${flow}`, + ); + } + if (declared !== flow) { + throw new Error( + `Agent prompt ${sourcePath} declares flow "${declared}" but lives in agents/${flow}/`, + ); + } +} + +/** + * Copy every agent-prompt markdown file into dist/agents// and write the + * menu the wizard fetches to discover available types. Each menu entry carries + * its flow and a full downloadUrl so the dev-server and the release host can + * differ without the wizard composing URLs. Returns { count, agentsDistDir }. + */ +export function buildAgents({ configDir, distDir, baseUrl, version = 'dev' }) { + const agentsSourceDir = path.join(configDir, 'agents'); + const agentsDistDir = path.join(distDir, 'agents'); + const resolvedBase = (baseUrl || DEFAULT_AGENTS_BASE_URL).replace(/\/+$/, ''); + + fs.mkdirSync(agentsDistDir, { recursive: true }); + + const entries = loadAgentEntries(agentsSourceDir); + const agents = []; + for (const { flow, id } of entries) { + const sourcePath = path.join(agentsSourceDir, flow, `${id}.md`); + assertFlowMatches(sourcePath, flow); + fs.mkdirSync(path.join(agentsDistDir, flow), { recursive: true }); + fs.copyFileSync(sourcePath, path.join(agentsDistDir, flow, `${id}.md`)); + agents.push({ id, flow, downloadUrl: `${resolvedBase}/${flow}/${id}.md` }); + } + + const menu = { version: '1.0', buildVersion: version, agents }; + fs.writeFileSync( + path.join(agentsDistDir, 'agent-menu.json'), + JSON.stringify(menu, null, 2) + '\n', + ); + + // Reconcile: drop dist files and folders whose source markdown was removed. + const keep = new Set(entries.map(e => path.join(e.flow, `${e.id}.md`))); + keep.add('agent-menu.json'); + const walk = dir => { + for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) { + const abs = path.join(dir, dirent.name); + if (dirent.isDirectory()) { + walk(abs); + if (fs.readdirSync(abs).length === 0) fs.rmdirSync(abs); + } else if (!keep.has(path.relative(agentsDistDir, abs))) { + fs.rmSync(abs); + } + } + }; + walk(agentsDistDir); + + return { count: agents.length, agentsDistDir }; +} diff --git a/scripts/lib/build-phases.js b/scripts/lib/build-phases.js index a52e08d8..d65370de 100644 --- a/scripts/lib/build-phases.js +++ b/scripts/lib/build-phases.js @@ -190,11 +190,16 @@ function writeManifestAndMenu({ allSkills, docContents, distDir, configDir, vers for (const skill of allSkills) { const cat = skill.group; if (!skillsByCategory[cat]) skillsByCategory[cat] = []; - skillsByCategory[cat].push({ + // group/framework/default let consumers resolve a bare skill id + framework by exact match. + const entry = { id: skill.id, name: skill.name, + group: skill.group.replace(/\//g, '-'), downloadUrl: manifest.resources.find(r => r.id === skill.id)?.downloadUrl, - }); + }; + if (skill.framework) entry.framework = skill.framework; + if (skill.default) entry.default = true; + skillsByCategory[cat].push(entry); } // The CLI entries are the lookup table the wizard's runtime resolver uses diff --git a/scripts/lib/skill-generator.js b/scripts/lib/skill-generator.js index 5e461ce0..86b8b825 100644 --- a/scripts/lib/skill-generator.js +++ b/scripts/lib/skill-generator.js @@ -176,7 +176,7 @@ function loadSkillsConfig(configDir) { const configFile = path.join(dir, 'config.yaml'); if (fs.existsSync(configFile)) { const localConfig = loadYaml(configFile); - if (localConfig?.variants) { + if (localConfig?.variants || localConfig?.variants_from) { config[keyParts.join('/')] = localConfig; } } @@ -225,15 +225,56 @@ function normalizeExamplePaths(value) { return Array.isArray(value) ? value : [value]; } +/** + * Resolve `variants_from` references: a group may borrow another group's + * variant matrix instead of duplicating it. Only the framework identity comes + * across — id, display_name, tags, docs_urls, framework, default — never + * example paths, templates, cli blocks, or shared docs, which stay the + * borrowing group's own concern. One level only; a source group must declare + * its variants literally. Pure — returns a new config, never mutates the input. + */ +function resolveVariantsFrom(config) { + const resolved = {}; + for (const [key, group] of Object.entries(config)) { + if (!group.variants_from) { + resolved[key] = group; + continue; + } + if (group.variants) { + throw new Error(`Skill group "${key}": declare either variants or variants_from, not both`); + } + const source = config[group.variants_from]; + if (!source) { + throw new Error(`Skill group "${key}": variants_from "${group.variants_from}" does not name a skill group`); + } + if (source.variants_from) { + throw new Error(`Skill group "${key}": variants_from cannot chain ("${group.variants_from}" also uses variants_from)`); + } + const variants = source.variants.map(v => { + const variant = { id: v.id, display_name: v.display_name }; + if (v.tags) variant.tags = [...v.tags]; + if (v.docs_urls) variant.docs_urls = [...v.docs_urls]; + if (v.framework) variant.framework = v.framework; + if (v.default) variant.default = v.default; + return variant; + }); + // Drop the reference key — the resolved copy declares variants literally. + const { variants_from: _from, ...rest } = group; + resolved[key] = { ...rest, variants }; + } + return resolved; +} + /** * Expand grouped skill config into a flat array of skill objects. * Each top-level key (except shared_docs) is a skill group with - * base properties and a variants array. + * base properties and a variants array (literal or via variants_from). */ function expandSkillGroups(config, configDir) { const skills = []; + const resolvedConfig = resolveVariantsFrom(config); - for (const [key, group] of Object.entries(config)) { + for (const [key, group] of Object.entries(resolvedConfig)) { if (key === 'shared_docs') continue; if (!group.variants) continue; @@ -703,15 +744,30 @@ async function generateSkill({ await processDoc(docEntry); } + // Collect commandments for this skill's tags. + const rules = collectCommandments(skill.tags || [], commandmentsConfig); + const commandmentsText = formatCommandments(rules); + + // Also emit them as a reference file. The orchestrator installs this skill + // as the framework reference and points its task agents at individual + // files, so the rules must exist outside the SKILL.md body. + if (rules.length > 0) { + fs.writeFileSync( + path.join(referencesDir, 'COMMANDMENTS.md'), + `# Framework rules\n\nFollow these when integrating PostHog into this framework.\n\n${commandmentsText}\n`, + 'utf8', + ); + references.push({ + filename: 'COMMANDMENTS.md', + description: 'Framework-specific rules the integration must follow', + }); + } + // Build references list for SKILL.md const referencesText = references .map(ref => `- \`references/${ref.filename}\` - ${ref.description}`) .join('\n'); - // Collect commandments for this skill's tags - const rules = collectCommandments(skill.tags || [], commandmentsConfig); - const commandmentsText = formatCommandments(rules); - // Format workflow steps for skills that use the {workflow} placeholder const workflowText = formatWorkflowSteps(workflowSteps); @@ -749,6 +805,12 @@ function serializeSkill(s) { description: s.description, tags: s.tags || [], }; + if (s.framework) { + result.framework = s.framework; + } + if (s.default) { + result.default = true; + } if (s._cli) { result.cli = s._cli; } diff --git a/scripts/lib/tests/agent-generator-flow.test.js b/scripts/lib/tests/agent-generator-flow.test.js new file mode 100644 index 00000000..8e4e54d1 --- /dev/null +++ b/scripts/lib/tests/agent-generator-flow.test.js @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, writeFileSync, mkdtempSync, rmSync, readFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +import { buildAgents } from '../agent-generator.js'; + +const prompt = (frontmatter) => `---\n${frontmatter}\n---\n\n## Goal\n\nDo the thing.\n`; + +describe('buildAgents flow frontmatter', () => { + let tmpDir; + let configDir; + let distDir; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), 'agents-test-')); + configDir = join(tmpDir, 'context'); + distDir = join(tmpDir, 'dist'); + mkdirSync(join(configDir, 'agents', 'my-flow'), { recursive: true }); + }); + + afterEach(() => rmSync(tmpDir, { recursive: true, force: true })); + + it('builds a prompt whose flow matches its folder', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: my-flow'), + ); + const { count } = buildAgents({ configDir, distDir, baseUrl: 'http://x' }); + expect(count).toBe(1); + const menu = JSON.parse(readFileSync(join(distDir, 'agents', 'agent-menu.json'), 'utf8')); + expect(menu.agents).toEqual([ + { id: 'task', flow: 'my-flow', downloadUrl: 'http://x/my-flow/task.md' }, + ]); + }); + + it('rejects a prompt missing the flow key — consumers filter by it', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task'), + ); + expect(() => buildAgents({ configDir, distDir, baseUrl: 'http://x' })).toThrow( + /missing the "flow:" frontmatter key/, + ); + }); + + it('rejects a prompt whose flow contradicts its folder', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: other-flow'), + ); + expect(() => buildAgents({ configDir, distDir, baseUrl: 'http://x' })).toThrow( + /declares flow "other-flow"/, + ); + }); + + it('still ignores README.md files at both levels', () => { + writeFileSync(join(configDir, 'agents', 'README.md'), '# docs'); + writeFileSync(join(configDir, 'agents', 'my-flow', 'README.md'), '# docs'); + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: my-flow'), + ); + const { count } = buildAgents({ configDir, distDir, baseUrl: 'http://x' }); + expect(count).toBe(1); + expect(existsSync(join(distDir, 'agents', 'my-flow', 'README.md'))).toBe(false); + }); +}); diff --git a/scripts/lib/tests/skill-variants-from.test.js b/scripts/lib/tests/skill-variants-from.test.js new file mode 100644 index 00000000..acf5d833 --- /dev/null +++ b/scripts/lib/tests/skill-variants-from.test.js @@ -0,0 +1,133 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +import { expandSkillGroups } from '../skill-generator.js'; + +function createFixture(tree, baseDir) { + for (const [name, content] of Object.entries(tree)) { + const fullPath = join(baseDir, name); + if (typeof content === 'string') { + writeFileSync(fullPath, content); + } else { + mkdirSync(fullPath, { recursive: true }); + createFixture(content, fullPath); + } + } +} + +describe('variants_from', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), 'skills-test-')); + mkdirSync(join(tmpDir, 'skills')); + createFixture({ + skills: { + integration: { 'description.md': '# Integration for {display_name}' }, + 'flow-x': { install: { 'description.md': '# Install for {display_name}' } }, + }, + }, tmpDir); + }); + + afterEach(() => rmSync(tmpDir, { recursive: true, force: true })); + + const integrationGroup = () => ({ + type: 'skill', + template: 'description.md', + variants: [ + { + id: 'django', + display_name: 'Django', + framework: 'django', + default: true, + tags: ['django', 'python'], + docs_urls: ['https://posthog.com/docs/libraries/django.md'], + example_paths: 'example-apps/django', + }, + ], + }); + + const borrowingGroup = () => ({ + type: 'docs-only', + template: 'description.md', + tags: ['orchestrator', 'install'], + variants_from: 'integration', + }); + + it('borrows the source matrix: id, display_name, tags, docs_urls', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + const skills = expandSkillGroups(config, tmpDir); + const step = skills.find(s => s.id === 'flow-x-install-django'); + expect(step).toBeDefined(); + expect(step.display_name).toBe('Django'); + expect(step.docs_urls).toEqual(['https://posthog.com/docs/libraries/django.md']); + }); + + it('merges the borrowing group tags with the source variant tags', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + const skills = expandSkillGroups(config, tmpDir); + const step = skills.find(s => s.id === 'flow-x-install-django'); + expect(step.tags).toEqual(['orchestrator', 'install', 'django', 'python']); + }); + + it('does not inherit example paths from the source variants', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + const skills = expandSkillGroups(config, tmpDir); + const step = skills.find(s => s.id === 'flow-x-install-django'); + expect(step._examplePaths).toEqual([]); + }); + + it('borrows framework and default so variant steps resolve like the source', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + const skills = expandSkillGroups(config, tmpDir); + const step = skills.find(s => s.id === 'flow-x-install-django'); + expect(step.framework).toBe('django'); + expect(step.default).toBe(true); + }); + + it('does not mutate the input config', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + expandSkillGroups(config, tmpDir); + expect(config['flow-x/install'].variants).toBeUndefined(); + expect(config['flow-x/install']._variantsResolved).toBeUndefined(); + }); + + it('leaves the source group untouched', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + const skills = expandSkillGroups(config, tmpDir); + const source = skills.find(s => s.id === 'integration-django'); + expect(source.tags).toEqual(['django', 'python']); + expect(source._examplePaths).toEqual(['example-apps/django']); + }); + + it('expanding the same config twice is stable', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + expandSkillGroups(config, tmpDir); + const skills = expandSkillGroups(config, tmpDir); + expect(skills.filter(s => s.id === 'flow-x-install-django')).toHaveLength(1); + }); + + it('throws when the source group does not exist', () => { + const config = { 'flow-x/install': { ...borrowingGroup(), variants_from: 'nope' } }; + expect(() => expandSkillGroups(config, tmpDir)).toThrow(/does not name a skill group/); + }); + + it('throws when a group declares both variants and variants_from', () => { + const config = { + integration: integrationGroup(), + 'flow-x/install': { ...borrowingGroup(), variants: [{ id: 'x', display_name: 'X' }] }, + }; + expect(() => expandSkillGroups(config, tmpDir)).toThrow(/not both/); + }); + + it('throws when variants_from chains', () => { + const config = { + integration: integrationGroup(), + 'flow-x/install': borrowingGroup(), + 'flow-x/init': { ...borrowingGroup(), variants_from: 'flow-x/install' }, + }; + expect(() => expandSkillGroups(config, tmpDir)).toThrow(/cannot chain/); + }); +});